@@ -38,7 +38,13 @@ import {
3838 Unauthorized ,
3939 Unavailable ,
4040} from "@executor-js/api/server" ;
41- import type { FailureRenderingStrategy , IdentityFailure , Principal } from "@executor-js/api/server" ;
41+ import type {
42+ FailureRenderingStrategy ,
43+ IdentityFailure ,
44+ PlatformPrincipal ,
45+ Principal ,
46+ ResolvedPrincipal ,
47+ } from "@executor-js/api/server" ;
4248
4349import { ApiKeyService } from "./api-keys" ;
4450import { workosApiJwtBearerConfig } from "./api-jwt-bearer" ;
@@ -104,14 +110,6 @@ const NO_ORGANIZATION_IN_ACCESS_TOKEN = {
104110 code : "no_organization" ,
105111 message : "No organization in access token" ,
106112} ;
107- // An org-level key resolves to the PLATFORM view, which has no acting member.
108- // The product endpoints are bound to one subject, so they reject it outright
109- // rather than inventing a subject for it to act as.
110- const ORG_KEY_ON_PRODUCT_SURFACE = {
111- code : "invalid_api_key" ,
112- message : "Organization API keys cannot be used on this endpoint" ,
113- } ;
114-
115113// A bearer value with three dot-separated segments is a JWT (a WorkOS access
116114// token from the CLI device-login); anything else is treated as an API key.
117115// Same discriminator the MCP plane uses (`mcp/auth.ts`).
@@ -147,6 +145,7 @@ const resolveJwtPrincipal = (token: string, jwt: JwtBearerConfig) =>
147145 if ( ! org ) return yield * new NoOrganization ( NO_ORGANIZATION_IN_ACCESS_TOKEN ) ;
148146
149147 return {
148+ kind : "member" ,
150149 accountId : verified . accountId ,
151150 organizationId : org . id ,
152151 organizationName : org . name ,
@@ -245,6 +244,7 @@ export const resolveBearerAuth = (
245244 if ( ! org ) return yield * new NoOrganization ( NO_ORGANIZATION_IN_API_KEY ) ;
246245
247246 return {
247+ kind : "member" ,
248248 accountId : owner . accountId ,
249249 organizationId : org . id ,
250250 organizationName : org . name ,
@@ -257,23 +257,36 @@ export const resolveBearerAuth = (
257257 } ) ;
258258
259259/**
260- * The PRODUCT-view bearer resolver: as {@link resolveBearerAuth}, but an
261- * org-level key is REJECTED rather than downgraded. The product endpoints are
262- * bound to one acting subject, so there is no honest way to serve them an
263- * org key — those belong at the `/admin/*` mount instead. (Kept the historical
264- * name; the re-export and resolver tests reference it.)
260+ * The PRODUCT-plane bearer resolver. An org-level key resolves to the neutral
261+ * seam's {@link PlatformPrincipal} — NOT a member `Principal` — and the shared
262+ * middleware routes it to the subject-less, read-only platform executor
263+ * (refusing non-GET up front). Previously the product plane rejected org keys
264+ * outright; serving tenant-level READS to them is deliberate: the catalog,
265+ * tools, policies, and org-owned connection listings are tenant-shared answers
266+ * a machine credential can honestly receive, while everything subject-bound
267+ * stays structurally out of reach (a platform executor binds no subject, so no
268+ * member's personal rows resolve). (Kept the historical name; the re-export and
269+ * resolver tests reference it.)
265270 */
266271export const resolveApiKeyPrincipal = (
267272 request : Request ,
268273 jwt : JwtBearerConfig | null = null ,
269274) : Effect . Effect <
270- Principal | null ,
275+ ResolvedPrincipal | null ,
271276 Unauthorized | NoOrganization | Unavailable | UserStoreError | WorkOSError ,
272277 WorkOSClient | ApiKeyService | UserStoreService
273278> =>
274279 Effect . gen ( function * ( ) {
275280 const auth = yield * resolveBearerAuth ( request , jwt ) ;
276- if ( isPlatformAuth ( auth ) ) return yield * new Unauthorized ( ORG_KEY_ON_PRODUCT_SURFACE ) ;
281+ if ( isPlatformAuth ( auth ) ) {
282+ return {
283+ kind : "platform" ,
284+ organizationId : auth . organizationId ,
285+ organizationName : auth . organizationName ,
286+ ...( auth . organizationSlug === undefined ? { } : { organizationSlug : auth . organizationSlug } ) ,
287+ keyId : auth . keyId ,
288+ } satisfies PlatformPrincipal ;
289+ }
277290 return auth ;
278291 } ) ;
279292
@@ -304,6 +317,7 @@ export const resolveSessionPrincipal = (request: Request) =>
304317 const org = yield * authorizeOrganizationSelector ( session . userId , selector ) ;
305318 if ( ! org ) return yield * new NoOrganization ( NO_ORGANIZATION_IN_SESSION ) ;
306319 return {
320+ kind : "member" ,
307321 accountId : session . userId ,
308322 organizationId : org . id ,
309323 organizationName : org . name ,
@@ -330,7 +344,7 @@ export const resolveProtectedPrincipal = (
330344 request : Request ,
331345 jwt : JwtBearerConfig | null = null ,
332346) : Effect . Effect <
333- Principal ,
347+ ResolvedPrincipal ,
334348 Unauthorized | NoOrganization | Unavailable | UserStoreError | WorkOSError ,
335349 WorkOSClient | ApiKeyService | UserStoreService
336350> =>
@@ -411,6 +425,11 @@ export const cloudIdentityFailureStrategy: FailureRenderingStrategy<IdentityFail
411425 "service_unavailable" ,
412426 "Service temporarily unavailable" ,
413427 ) ,
428+ ReadOnlyCredential : renderIdentityFailure (
429+ 403 ,
430+ "read_only_credential" ,
431+ "Organization API keys are read-only" ,
432+ ) ,
414433 } ) ,
415434 ) ,
416435} ;
0 commit comments