Found while implementing #9391 (the datasource-admin authentication guard). Filed rather than fixed there: it is a different registrar in a different package, and #9391's scope was ruled to be exactly the datasource-admin family.
Disclosure posture — inherited deliberately
⛔ No reproduction here, in any PR body, or in any commit message.#9391 was revised on 2026-08-17 to remove its request/response table after the maintainer deleted the public reproduction; that decision travels with adjacent work. What follows is the cause, the source location, and the contract a fix would restore — which is what someone fixing it needs.
What was observed
registerExternalDatasourceRoutes (packages/rest/src/external-datasource-routes.ts) mounts five routes under /api/v1/datasources/:name/external/* and performs no authentication check. Read at source: the module imports nothing auth-related, and none of the five handlers consults an identity before dispatching.
The five, by disposition:
| route | kind |
|---|
GET .../external/tables | read — remote-table introspection |
POST .../external/tables/:remote/draft | read — introspect + type-map, no persistence |
POST .../external/tables/:remote/import | write — creates a live runtime-origin federated object |
POST .../external/refresh-catalog | write — refreshes the cached catalog snapshot |
POST .../external/validate | read — validates the federated objects |
Cause — the same shape #9391 had, one package over
These routes are composed by mountAndRecordDirectRoutes (packages/rest/src/direct-mount-composition.ts), which mounts two registrars. That composition resolves the RestServer's own execution-context resolver and passes it to one of them:
registerPackageRoutes receives resolveExecutionContext and applies the shared anonymous floor (shouldDenyAnonymous over the resolved context, failing closed when the resolver is absent — packages/rest/src/package-routes.ts);registerExternalDatasourceRoutes receives no resolver and contains no check.
Being composed by RestServer is not itself a guard: enforceAuth is a private method invoked inside RestServer's own handlers, not a middleware a direct mount passes through. That is the same reason the datasource-admin family needed its own line, and the same reason registerPackageRoutes grew one.
Why this is worth a card of its own, not a footnote on #9391
Two of these five routes are the declared twins of routes #9391 just guarded: GET .../external/tables and GET /api/v1/datasources/:name/remote-tables reach the same IExternalDatasourceService.listRemoteTables, and POST .../external/tables/:remote/draft and POST /api/v1/datasources/:name/object-draft reach the same generateObjectDraft. The equivalence is deliberate and pinned (packages/rest/src/remote-tables-twin.equivalence.test.ts; #4249 gave the two spellings one failure contract, #7955 one request shape). After #9391 lands, one operation is reachable at one spelling only with authentication and at the other without — so the family's own equivalence principle now argues for closing this, independently of the exposure.
Unlike the admin spelling, this family is SDK-expressed (datasources.external.* on ObjectStackClient, per the REST route ledger), so a fix should be checked against those callers presenting a credential.
What a fix would look like (a lead, not a decision)
Hand the federation registrar the same resolveExecutionContext the composition already resolves for its sibling, and apply the same shared anonymous floor — decision from shouldDenyAnonymous, identity from the resolver, envelope from this surface's sendError, fail-closed when unwired. Whether these routes should require a capability beyond authentication is a separate question of the kind #9593 answers for the admin family, and should not be folded in.
Not triaged here: the other direct-mount registrars vary by design (service-settings gates inside the service, service-storage gates uploads and parent-governed downloads, service-i18n serves locale bundles that may legitimately be pre-login). Each needs its own reading; this card is only about the federation family, where the read is unambiguous.
Backlinks: #9391 (the sibling family, guarded), #7744 (added this area's route ledgers without a guard), #9593 (the capability question for the admin family). Unassigned and unqueued — for the triage seat to level and route.
Found while implementing #9391 (the datasource-admin authentication guard). Filed rather than fixed there: it is a different registrar in a different package, and #9391's scope was ruled to be exactly the datasource-admin family.
Disclosure posture — inherited deliberately
⛔ No reproduction here, in any PR body, or in any commit message.#9391 was revised on 2026-08-17 to remove its request/response table after the maintainer deleted the public reproduction; that decision travels with adjacent work. What follows is the cause, the source location, and the contract a fix would restore — which is what someone fixing it needs.
What was observed
registerExternalDatasourceRoutes(packages/rest/src/external-datasource-routes.ts) mounts five routes under/api/v1/datasources/:name/external/*and performs no authentication check. Read at source: the module imports nothing auth-related, and none of the five handlers consults an identity before dispatching.The five, by disposition:
GET .../external/tablesPOST .../external/tables/:remote/draftPOST .../external/tables/:remote/importPOST .../external/refresh-catalogPOST .../external/validateCause — the same shape #9391 had, one package over
These routes are composed by
mountAndRecordDirectRoutes(packages/rest/src/direct-mount-composition.ts), which mounts two registrars. That composition resolves theRestServer's own execution-context resolver and passes it to one of them:registerPackageRoutesreceivesresolveExecutionContextand applies the shared anonymous floor (shouldDenyAnonymousover the resolved context, failing closed when the resolver is absent —packages/rest/src/package-routes.ts);registerExternalDatasourceRoutesreceives no resolver and contains no check.Being composed by
RestServeris not itself a guard:enforceAuthis a private method invoked insideRestServer's own handlers, not a middleware a direct mount passes through. That is the same reason the datasource-admin family needed its own line, and the same reasonregisterPackageRoutesgrew one.Why this is worth a card of its own, not a footnote on #9391
Two of these five routes are the declared twins of routes #9391 just guarded:
GET .../external/tablesandGET /api/v1/datasources/:name/remote-tablesreach the sameIExternalDatasourceService.listRemoteTables, andPOST .../external/tables/:remote/draftandPOST /api/v1/datasources/:name/object-draftreach the samegenerateObjectDraft. The equivalence is deliberate and pinned (packages/rest/src/remote-tables-twin.equivalence.test.ts; #4249 gave the two spellings one failure contract, #7955 one request shape). After #9391 lands, one operation is reachable at one spelling only with authentication and at the other without — so the family's own equivalence principle now argues for closing this, independently of the exposure.Unlike the admin spelling, this family is SDK-expressed (
datasources.external.*onObjectStackClient, per the REST route ledger), so a fix should be checked against those callers presenting a credential.What a fix would look like (a lead, not a decision)
Hand the federation registrar the same
resolveExecutionContextthe composition already resolves for its sibling, and apply the same shared anonymous floor — decision fromshouldDenyAnonymous, identity from the resolver, envelope from this surface'ssendError, fail-closed when unwired. Whether these routes should require a capability beyond authentication is a separate question of the kind #9593 answers for the admin family, and should not be folded in.Not triaged here: the other direct-mount registrars vary by design (
service-settingsgates inside the service,service-storagegates uploads and parent-governed downloads,service-i18nserves locale bundles that may legitimately be pre-login). Each needs its own reading; this card is only about the federation family, where the read is unambiguous.Backlinks: #9391 (the sibling family, guarded), #7744 (added this area's route ledgers without a guard), #9593 (the capability question for the admin family). Unassigned and unqueued — for the triage seat to level and route.