Found while documenting the X-Tenant-ID edge contract for #5279 (PR forthcoming on that card). Not fixed there — different package, different fence.
What was measured
createAuthenticatedFetch({ sameOriginOnly: true }) exists because of #2725: a fetch whose target URL comes from view metadata may name a third-party host that must never see the platform bearer token. Reading every construction site in the repo, exactly one passes the option:
packages/app-shell/src/console/ConsoleShell.tsx:213 — const apiProviderFetch = withSettleSignal(createAuthenticatedFetch({ sameOriginOnly: true }));, the provider: 'api' data-source lane.
Every other site builds the bare wrapper, including the one that takes a URL from metadata just as freely:
packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:300 — const authFetch = useMemo(() => createAuthenticatedFetch(), []);
That wrapper is what apiHandler calls, and apiHandler accepts an absolute target straight from the action metadata (useConsoleActionRuntime.tsx:315-337):
const isAbsolute = targetStr.startsWith('/') || /^https?:\/\//i.test(targetStr);
...
const url = resolvedTarget.startsWith('http') ? resolvedTarget : `${baseUrl}${resolvedTarget}`;
So a metadata type: 'api' action whose target is https://third-party.example/... is fetched through the bare wrapper. Two headers ride along:
| Header | Gate in createAuthenticatedFetch | Sent to a third-party host? |
|---|
Authorization: Bearer <session token> | isApiCall = /\/api\//i.test(url) | Yes, whenever the third-party URL happens to contain /api/ — which is the common shape for one |
X-Tenant-ID: <active organization id> | none at all | Yes, unconditionally |
The bearer half is already pinned as intended-for-the-other-lane by an existing test — packages/auth/src/__tests__/createAuthenticatedFetch.test.tsx, "without sameOriginOnly, cross-origin /api/ URLs keep the legacy attach behaviour". That test records the wrapper's behaviour; it says nothing about whether this call site should be using the bare wrapper.
Why it looks like an oversight rather than a decision
#2725's fix hardened the lane it was reported on. Both lanes take their URL from author-supplied metadata, and nothing in the code or comments distinguishes them on that axis — apiHandler's own comment describes the wrapper as "Bearer + X-Tenant-ID + same-origin cookies" without noting that the target may be off-origin.
Not covered by this report
Whether the tenant stamp should be gated on isApiCall at all (it is the only one of the three headers that is not) is a separate question raised on #5279. It is listed here only because it changes what a third-party host receives from this call site.
Suggested repair, for triage rather than as a decision
useConsoleActionRuntime.tsx:300 builds createAuthenticatedFetch({ sameOriginOnly: true }), matching ConsoleShell. That would stop attaching credentials to off-origin action targets while leaving every same-origin action untouched. It is a behaviour change for any deployment that today relies on the bearer reaching an off-origin /api/ target, so it needs a deliberate call, not a drive-by.
Found while documenting the
X-Tenant-IDedge contract for #5279 (PR forthcoming on that card). Not fixed there — different package, different fence.What was measured
createAuthenticatedFetch({ sameOriginOnly: true })exists because of #2725: a fetch whose target URL comes from view metadata may name a third-party host that must never see the platform bearer token. Reading every construction site in the repo, exactly one passes the option:packages/app-shell/src/console/ConsoleShell.tsx:213—const apiProviderFetch = withSettleSignal(createAuthenticatedFetch({ sameOriginOnly: true }));, theprovider: 'api'data-source lane.Every other site builds the bare wrapper, including the one that takes a URL from metadata just as freely:
packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:300—const authFetch = useMemo(() => createAuthenticatedFetch(), []);That wrapper is what
apiHandlercalls, andapiHandleraccepts an absolute target straight from the action metadata (useConsoleActionRuntime.tsx:315-337):So a metadata
type: 'api'action whosetargetishttps://third-party.example/...is fetched through the bare wrapper. Two headers ride along:createAuthenticatedFetchAuthorization: Bearer <session token>isApiCall=/\/api\//i.test(url)/api/— which is the common shape for oneX-Tenant-ID: <active organization id>The bearer half is already pinned as intended-for-the-other-lane by an existing test —
packages/auth/src/__tests__/createAuthenticatedFetch.test.tsx, "without sameOriginOnly, cross-origin /api/ URLs keep the legacy attach behaviour". That test records the wrapper's behaviour; it says nothing about whether this call site should be using the bare wrapper.Why it looks like an oversight rather than a decision
#2725's fix hardened the lane it was reported on. Both lanes take their URL from author-supplied metadata, and nothing in the code or comments distinguishes them on that axis —
apiHandler's own comment describes the wrapper as "Bearer + X-Tenant-ID + same-origin cookies" without noting that the target may be off-origin.Not covered by this report
Whether the tenant stamp should be gated on
isApiCallat all (it is the only one of the three headers that is not) is a separate question raised on #5279. It is listed here only because it changes what a third-party host receives from this call site.Suggested repair, for triage rather than as a decision
useConsoleActionRuntime.tsx:300buildscreateAuthenticatedFetch({ sameOriginOnly: true }), matchingConsoleShell. That would stop attaching credentials to off-origin action targets while leaving every same-origin action untouched. It is a behaviour change for any deployment that today relies on the bearer reaching an off-origin/api/target, so it needs a deliberate call, not a drive-by.