File size: 7,525 Bytes
96c8713 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | import {
parseTraceParent,
shouldPropagateToTarget,
getDefaultPropagationTargets,
type TraceContext,
type TracePropagationTarget,
} from '@supabase/tracing'
import { getTraceContextExtractor } from './tracingRegistry'
import type { TracePropagationOptions } from './types'
type Fetch = typeof fetch
export const resolveFetch = (customFetch?: Fetch): Fetch => {
if (customFetch) {
return (...args: Parameters<Fetch>) => customFetch(...args)
}
return (...args: Parameters<Fetch>) => fetch(...args)
}
export const resolveHeadersConstructor = () => {
return Headers
}
/**
* New-format Supabase API keys (`sb_publishable_…` / `sb_secret_…`) are not JWTs and
* must never be sent as a Bearer token — they belong only in the `apikey` header.
* All other keys (legacy JWT keys, `sb_temp_…` temporary keys, unrecognized `sb_`
* subtypes) keep the Bearer fallback.
*/
const isNewApiKey = (key: string): boolean =>
key.startsWith('sb_publishable_') || key.startsWith('sb_secret_')
const TEMP_KEY_PREFIX = 'sb_temp_'
const warnedKeySubtypes = new Set<string>()
/**
* Warn (once per subtype) when an `sb_` key isn't a subtype this SDK version recognizes.
* Never throws — the server, not the SDK, decides key validity. The key value is never
* included in the message.
*/
export const checkApiKeyFormat = (key: string): void => {
if (!key.startsWith('sb_') || isNewApiKey(key) || key.startsWith(TEMP_KEY_PREFIX)) {
return
}
const subtype = key.match(/^sb_[a-zA-Z0-9]+_/)?.[0] ?? 'unknown'
if (warnedKeySubtypes.has(subtype)) {
return
}
warnedKeySubtypes.add(subtype)
console.warn(
'@supabase/supabase-js: Unrecognized Supabase API key format. The client will proceed ' +
'and send this key as-is; if you see authentication errors you may need to upgrade ' +
'@supabase/supabase-js to a version that recognizes this key type.'
)
}
export const fetchWithAuth = (
supabaseKey: string,
supabaseUrl: string,
getAccessToken: () => Promise<string | null>,
customFetch?: Fetch,
tracePropagationOptions?: TracePropagationOptions,
options?: { omitApiKeyAsBearer?: boolean }
): Fetch => {
const fetch = resolveFetch(customFetch)
const HeadersConstructor = resolveHeadersConstructor()
// Pre-compute trace propagation state once. When disabled, the per-request
// path skips all tracing work with a single truthy check.
const traceEnabled = tracePropagationOptions?.enabled === true
const respectSampling = tracePropagationOptions?.respectSamplingDecision !== false
const traceTargets: TracePropagationTarget[] | null = traceEnabled
? getDefaultPropagationTargets(supabaseUrl)
: null
// Whether the API key may be used as the `Authorization` Bearer fallback when there is no
// session token. Disabled for Edge Functions with a new-format key (see `isNewApiKey`).
// Static per instance, so it is computed once here rather than on every request.
const allowKeyAsBearer = !(options?.omitApiKeyAsBearer && isNewApiKey(supabaseKey))
return async (input, init) => {
const realToken = await getAccessToken()
let headers = new HeadersConstructor(init?.headers)
if (!headers.has('apikey')) {
headers.set('apikey', supabaseKey)
}
if (!headers.has('Authorization')) {
const bearer = realToken ?? (allowKeyAsBearer ? supabaseKey : null)
if (bearer) {
headers.set('Authorization', `Bearer ${bearer}`)
}
}
if (traceTargets) {
const traceHeaders = getTraceHeaders(input, traceTargets, respectSampling)
if (traceHeaders) {
if (traceHeaders.traceparent && !headers.has('traceparent')) {
headers.set('traceparent', traceHeaders.traceparent)
}
if (traceHeaders.tracestate && !headers.has('tracestate')) {
headers.set('tracestate', traceHeaders.tracestate)
}
if (traceHeaders.baggage && !headers.has('baggage')) {
headers.set('baggage', traceHeaders.baggage)
}
}
}
return fetch(input, { ...init, headers })
}
}
let warnedMissingTracingRuntime = false
let warnedNonW3CPropagator = false
/**
* For tests only. Resets the one-time missing-tracing-runtime warning.
*
* @internal
*/
export function _resetTracingRuntimeWarning(): void {
warnedMissingTracingRuntime = false
}
/**
* For tests only. Resets the one-time non-W3C-propagator warning.
*
* @internal
*/
export function _resetNonW3CPropagatorWarning(): void {
warnedNonW3CPropagator = false
}
function getTraceHeaders(
input: RequestInfo | URL,
targets: TracePropagationTarget[],
respectSampling: boolean
): TraceContext | null {
// Read the registry before the target check so the warning fires on the
// first request with tracing enabled, not only on Supabase-target ones.
// Reading per request (one globalThis property access) deliberately
// supports late registration: with ESM evaluation order, `createClient`
// can run in a module evaluated before the application entry point's
// `import '@supabase/supabase-js/tracing'`.
const extractTraceContext = getTraceContextExtractor()
if (!extractTraceContext) {
if (!warnedMissingTracingRuntime) {
warnedMissingTracingRuntime = true
console.warn(
'@supabase/supabase-js: tracePropagation is enabled but the tracing runtime is not loaded, ' +
"so trace headers will not be attached. Add `import '@supabase/supabase-js/tracing'` at " +
'your application entry point (requires the OpenTelemetry API package to be installed). ' +
'The CDN/UMD build does not support trace propagation.'
)
}
return null
}
const targetUrl: string | URL =
typeof input === 'string' ? input : input instanceof URL ? input : input.url
if (!shouldPropagateToTarget(targetUrl, targets)) {
return null
}
const traceContext = extractTraceContext()
if (!traceContext || !traceContext.traceparent) {
// An active propagator that writes vendor headers but no W3C
// `traceparent` (e.g. Sentry without `propagateTraceparent: true`) is
// otherwise indistinguishable from "no active trace" — warn once so the
// user learns why no trace headers are attached. An empty carrier means
// no active trace: normal, stay silent.
if (traceContext?.carrierKeys?.length && !warnedNonW3CPropagator) {
warnedNonW3CPropagator = true
const sentryHint = traceContext.carrierKeys.includes('sentry-trace')
? ' Sentry detected: set `propagateTraceparent: true` in Sentry.init() to emit it.'
: ' Configure your tracing SDK to emit W3C trace context on outgoing requests.'
console.warn(
'@supabase/supabase-js: tracePropagation is enabled and a tracing SDK is active, but its ' +
`propagator wrote [${traceContext.carrierKeys.join(', ')}] and no W3C traceparent header, ` +
'so trace headers will not be attached.' +
sentryHint
)
}
return null
}
if (respectSampling) {
const parsed = parseTraceParent(traceContext.traceparent)
if (parsed && !parsed.isSampled) {
// Unsampled traces still carry `traceparent` so Supabase logs get a
// trace_id to correlate on; the flag stays `00`, so downstream tracing
// never records it as sampled. `tracestate` and `baggage` (the
// vendor/application data channels) are withheld on these requests.
return { traceparent: traceContext.traceparent }
}
}
return traceContext
}
|