// ---------------------------------------------------------------------------// End-to-end shape test for multi-user bearer-token auth on the OpenAPI// plugin. Models the Vercel-style scenario://// - An org admin uploads the Vercel OpenAPI spec once. The stored source// declares an `Authorization` credential slot, but NOT the token value// or even a concrete secret id.// - Each user (alice, bob) writes their own personal access token at// their own user scope and binds that same slot to their own row.// - Invoking a Vercel tool through alice injects alice's token;// through bob injects bob's. The org scope never stores a value —// per-user scopes are the only source of truth for the bearer.//// This is the tier-1 win: the scope-partitioning `SecretProvider` lets// the same secret id carry a distinct value in each user's scope, so a// single stored source description serves every user without duplicating// source rows per tenant.// --------------------------------------------------------------------------- import { expect, layer } from "@effect/vitest";import { Effect, Layer, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi,} from "effect/unstable/httpapi";import { HttpClient, HttpRouter, HttpServerRequest } from "effect/unstable/http";import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import { collectSchemas, createExecutor, definePlugin, makeInMemoryBlobStore, Scope, ScopeId, SecretId, SetSecretInput, ToolInvocationError, type InvokeOptions, type SecretProvider,} from "@executor-js/sdk";import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory"; import { openApiPlugin } from "./plugin";import { ConfiguredHeaderBinding, OpenApiSourceBindingInput } from "./types"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; // ---------------------------------------------------------------------------// Test API — a single endpoint that echoes the Authorization header so the// test can assert which user's token got injected.// --------------------------------------------------------------------------- class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ authorization: Schema.optional(Schema.String), token: Schema.optional(Schema.String),}) {} const ProjectsGroup = HttpApiGroup.make("projects").add( HttpApiEndpoint.get("list", "/v9/projects", { success: EchoHeaders }),); const VercelApi = HttpApi.make("vercelApi").add(ProjectsGroup);const specJson = JSON.stringify(OpenApi.fromApi(VercelApi)); const ProjectsGroupLive = HttpApiBuilder.group(VercelApi, "projects", (handlers) => handlers.handle("list", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; const url = new URL(req.url, "http://executor.test"); return new EchoHeaders({ authorization: req.headers["authorization"], token: url.searchParams.get("token") ?? undefined, }); }), ),); const ApiLive = HttpApiBuilder.layer(VercelApi).pipe(Layer.provide(ProjectsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true,}).pipe(Layer.provideMerge(NodeHttpServer.layerTest)); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI multi-scope bearer (Vercel-style)", (it) => { it.effect("admin-added source; each user's per-scope token wins on invocation", () => Effect.gen(function* () { // Scope-partitioning in-memory provider. The composite key is // what makes the tier-1 fix observable: same secret id, different // value per scope. A flat `Map<id, value>` provider would lose // one of the users' tokens on the second write. const secretStore = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; const memoryProvider: SecretProvider = { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => secretStore.get(key(scope, id)) ?? null), set: (id, value, scope) => Effect.sync(() => { secretStore.set(key(scope, id), value); }), delete: (id, scope) => Effect.sync(() => secretStore.delete(key(scope, id))), }; const memorySecretsPlugin = definePlugin(() => ({ id: "memory-secrets" as const, storage: () => ({}), secretProviders: [memoryProvider], })); const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const baseUrl = ""; const plugins = [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const; // One adapter + blob store backing all three executors: mirrors a // multi-tenant deployment where admin + users share infra but // each sits at a different scope stack. const schema = collectSchemas(plugins); const adapter = makeMemoryAdapter({ schema }); const blobs = makeInMemoryBlobStore(); const now = new Date(); const orgScope = new Scope({ id: ScopeId.make("org"), name: "acme-org", createdAt: now, }); const aliceScope = new Scope({ id: ScopeId.make("user-alice"), name: "alice", createdAt: now, }); const bobScope = new Scope({ id: ScopeId.make("user-bob"), name: "bob", createdAt: now, }); const adminExec = yield* createExecutor({ scopes: [orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const aliceExec = yield* createExecutor({ scopes: [aliceScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const bobExec = yield* createExecutor({ scopes: [bobScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); // ------------------------------------------------------------- // 1. Admin adds the Vercel OpenAPI source at org scope. The // stored source declares a credential slot, not a concrete // credential. Each user will bind their own secret to that slot. // ------------------------------------------------------------- yield* adminExec.openapi.addSpec({ spec: specJson, scope: String(orgScope.id), namespace: "vercel", baseUrl, headers: { Authorization: new ConfiguredHeaderBinding({ kind: "binding", slot: "auth:vercel_api_token", prefix: "Bearer ", }), }, queryParams: { token: new ConfiguredHeaderBinding({ kind: "binding", slot: "query_param:vercel_team_token", }), }, }); // ------------------------------------------------------------- // 2. Each user writes their personal access token under the // same secret id, but at their own scope. Tier-1 scope // routing means these coexist in the provider — alice's // write does not overwrite bob's. // ------------------------------------------------------------- yield* aliceExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_api_token"), scope: aliceScope.id, name: "Vercel API Token (alice)", value: "alice-vercel-token", }), ); yield* aliceExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_team_token"), scope: aliceScope.id, name: "Vercel Team Token (alice)", value: "alice-team", }), ); yield* bobExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_api_token"), scope: bobScope.id, name: "Vercel API Token (bob)", value: "bob-vercel-token", }), ); yield* bobExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_team_token"), scope: bobScope.id, name: "Vercel Team Token (bob)", value: "bob-team", }), ); // ------------------------------------------------------------- // 3. Each user explicitly binds the inherited source slot at // their own scope. Same secret id, same source, different // binding owner and provider value. // ------------------------------------------------------------- yield* aliceExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: aliceScope.id, slot: "auth:vercel_api_token", value: { kind: "secret", secretId: SecretId.make("vercel_api_token"), }, }), ); yield* aliceExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: aliceScope.id, slot: "query_param:vercel_team_token", value: { kind: "secret", secretId: SecretId.make("vercel_team_token"), }, }), ); yield* bobExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: bobScope.id, slot: "auth:vercel_api_token", value: { kind: "secret", secretId: SecretId.make("vercel_api_token"), }, }), ); yield* bobExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: bobScope.id, slot: "query_param:vercel_team_token", value: { kind: "secret", secretId: SecretId.make("vercel_team_token"), }, }), ); // ------------------------------------------------------------- // 4. Invoking the shared tool through each user's executor // resolves the nearest credential binding. Alice's scope // yields her token; bob's scope yields his. Same source, same // tool, different injected bearer. // ------------------------------------------------------------- const aliceResult = (yield* aliceExec.tools.invoke( "vercel.projects.list", {}, autoApprove, )) as { data: { authorization?: string; token?: string } | null; error: unknown; }; expect(aliceResult.error).toBeNull(); expect(aliceResult.data?.authorization).toBe("Bearer alice-vercel-token"); expect(aliceResult.data?.token).toBe("alice-team"); const bobResult = (yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { data: { authorization?: string; token?: string } | null; error: unknown; }; expect(bobResult.error).toBeNull(); expect(bobResult.data?.authorization).toBe("Bearer bob-vercel-token"); expect(bobResult.data?.token).toBe("bob-team"); // ------------------------------------------------------------- // 5. Scope attribution: each user's token is pinned to their // own scope, never smuggled into the org fallback. // ------------------------------------------------------------- const aliceRows = yield* aliceExec.secrets.list(); const aliceToken = aliceRows.find((r) => String(r.id) === "vercel_api_token"); expect(String(aliceToken?.scopeId)).toBe("user-alice"); const bobRows = yield* bobExec.secrets.list(); const bobToken = bobRows.find((r) => String(r.id) === "vercel_api_token"); expect(String(bobToken?.scopeId)).toBe("user-bob"); // Admin's scope never received a token — `get` at the org // scope yields null and the source is effectively unusable // for the admin role, exactly as designed. const adminToken = yield* adminExec.secrets.get("vercel_api_token"); expect(adminToken).toBeNull(); // ------------------------------------------------------------- // 6. Cross-user isolation on enumeration: alice does not see // bob's token row, and vice versa. // ------------------------------------------------------------- const aliceIds = new Set(aliceRows.map((r) => `${String(r.scopeId)}:${String(r.id)}`)); expect(aliceIds).toContain("user-alice:vercel_api_token"); expect(aliceIds).not.toContain("user-bob:vercel_api_token"); const bobIds = new Set(bobRows.map((r) => `${String(r.scopeId)}:${String(r.id)}`)); expect(bobIds).toContain("user-bob:vercel_api_token"); expect(bobIds).not.toContain("user-alice:vercel_api_token"); }), ); it.effect("per-user bindings can point the same shared header slot at different secret ids", () => Effect.gen(function* () { const secretStore = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; const memoryProvider: SecretProvider = { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => secretStore.get(key(scope, id)) ?? null), set: (id, value, scope) => Effect.sync(() => { secretStore.set(key(scope, id), value); }), delete: (id, scope) => Effect.sync(() => secretStore.delete(key(scope, id))), }; const memorySecretsPlugin = definePlugin(() => ({ id: "memory-secrets" as const, storage: () => ({}), secretProviders: [memoryProvider], })); const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const baseUrl = ""; const plugins = [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const; const schema = collectSchemas(plugins); const adapter = makeMemoryAdapter({ schema }); const blobs = makeInMemoryBlobStore(); const now = new Date(); const orgScope = new Scope({ id: ScopeId.make("org"), name: "acme-org", createdAt: now, }); const aliceScope = new Scope({ id: ScopeId.make("user-alice"), name: "alice", createdAt: now, }); const bobScope = new Scope({ id: ScopeId.make("user-bob"), name: "bob", createdAt: now, }); const adminExec = yield* createExecutor({ scopes: [orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const aliceExec = yield* createExecutor({ scopes: [aliceScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const bobExec = yield* createExecutor({ scopes: [bobScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); yield* adminExec.openapi.addSpec({ spec: specJson, scope: String(orgScope.id), namespace: "vercel", baseUrl, headers: { Authorization: new ConfiguredHeaderBinding({ kind: "binding", slot: "auth:personal-token", prefix: "Bearer ", }), }, }); yield* aliceExec.secrets.set( new SetSecretInput({ id: SecretId.make("alice_vercel_pat"), scope: aliceScope.id, name: "Alice Vercel PAT", value: "alice-vercel-token", }), ); yield* bobExec.secrets.set( new SetSecretInput({ id: SecretId.make("bob_vercel_pat"), scope: bobScope.id, name: "Bob Vercel PAT", value: "bob-vercel-token", }), ); yield* aliceExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: aliceScope.id, slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("alice_vercel_pat"), }, }), ); yield* bobExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: bobScope.id, slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("bob_vercel_pat"), }, }), ); const aliceResult = (yield* aliceExec.tools.invoke( "vercel.projects.list", {}, autoApprove, )) as { data: { authorization?: string } | null; error: unknown }; expect(aliceResult.error).toBeNull(); expect(aliceResult.data?.authorization).toBe("Bearer alice-vercel-token"); const bobResult = (yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { data: { authorization?: string } | null; error: unknown; }; expect(bobResult.error).toBeNull(); expect(bobResult.data?.authorization).toBe("Bearer bob-vercel-token"); }), ); it.effect( "source binding precedence falls back to shared auth and source deletion clears descendant bindings", () => Effect.gen(function* () { const secretStore = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; const memoryProvider: SecretProvider = { key: "memory", writable: true,Lines 1–500 of 1066. Load more
// --------------------------------------------------------------------------- // End-to-end shape test for multi-user bearer-token auth on the OpenAPI // plugin. Models the Vercel-style scenario: // // - An org admin uploads the Vercel OpenAPI spec once. The stored source // declares an `Authorization` credential slot, but NOT the token value // or even a concrete secret id. // - Each user (alice, bob) writes their own personal access token at // their own user scope and binds that same slot to their own row. // - Invoking a Vercel tool through alice injects alice's token; // through bob injects bob's. The org scope never stores a value — // per-user scopes are the only source of truth for the bearer. // // This is the tier-1 win: the scope-partitioning `SecretProvider` lets // the same secret id carry a distinct value in each user's scope, so a // single stored source description serves every user without duplicating // source rows per tenant. // --------------------------------------------------------------------------- import { expect, layer } from "@effect/vitest"; import { Effect, Layer, Schema } from "effect"; import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi, } from "effect/unstable/httpapi"; import { HttpClient, HttpRouter, HttpServerRequest } from "effect/unstable/http"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import { collectSchemas, createExecutor, definePlugin, makeInMemoryBlobStore, Scope, ScopeId, SecretId, SetSecretInput, ToolInvocationError, type InvokeOptions, type SecretProvider, } from "@executor-js/sdk"; import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory"; import { openApiPlugin } from "./plugin"; import { ConfiguredHeaderBinding, OpenApiSourceBindingInput } from "./types"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; // --------------------------------------------------------------------------- // Test API — a single endpoint that echoes the Authorization header so the // test can assert which user's token got injected. // --------------------------------------------------------------------------- class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ authorization: Schema.optional(Schema.String), token: Schema.optional(Schema.String), }) {} const ProjectsGroup = HttpApiGroup.make("projects").add( HttpApiEndpoint.get("list", "/v9/projects", { success: EchoHeaders }), ); const VercelApi = HttpApi.make("vercelApi").add(ProjectsGroup); const specJson = JSON.stringify(OpenApi.fromApi(VercelApi)); const ProjectsGroupLive = HttpApiBuilder.group(VercelApi, "projects", (handlers) => handlers.handle("list", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; const url = new URL(req.url, "http://executor.test"); return new EchoHeaders({ authorization: req.headers["authorization"], token: url.searchParams.get("token") ?? undefined, }); }), ), ); const ApiLive = HttpApiBuilder.layer(VercelApi).pipe(Layer.provide(ProjectsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true, }).pipe(Layer.provideMerge(NodeHttpServer.layerTest)); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI multi-scope bearer (Vercel-style)", (it) => { it.effect("admin-added source; each user's per-scope token wins on invocation", () => Effect.gen(function* () { // Scope-partitioning in-memory provider. The composite key is // what makes the tier-1 fix observable: same secret id, different // value per scope. A flat `Map<id, value>` provider would lose // one of the users' tokens on the second write. const secretStore = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; const memoryProvider: SecretProvider = { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => secretStore.get(key(scope, id)) ?? null), set: (id, value, scope) => Effect.sync(() => { secretStore.set(key(scope, id), value); }), delete: (id, scope) => Effect.sync(() => secretStore.delete(key(scope, id))), }; const memorySecretsPlugin = definePlugin(() => ({ id: "memory-secrets" as const, storage: () => ({}), secretProviders: [memoryProvider], })); const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const baseUrl = ""; const plugins = [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const; // One adapter + blob store backing all three executors: mirrors a // multi-tenant deployment where admin + users share infra but // each sits at a different scope stack. const schema = collectSchemas(plugins); const adapter = makeMemoryAdapter({ schema }); const blobs = makeInMemoryBlobStore(); const now = new Date(); const orgScope = new Scope({ id: ScopeId.make("org"), name: "acme-org", createdAt: now, }); const aliceScope = new Scope({ id: ScopeId.make("user-alice"), name: "alice", createdAt: now, }); const bobScope = new Scope({ id: ScopeId.make("user-bob"), name: "bob", createdAt: now, }); const adminExec = yield* createExecutor({ scopes: [orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const aliceExec = yield* createExecutor({ scopes: [aliceScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const bobExec = yield* createExecutor({ scopes: [bobScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); // ------------------------------------------------------------- // 1. Admin adds the Vercel OpenAPI source at org scope. The // stored source declares a credential slot, not a concrete // credential. Each user will bind their own secret to that slot. // ------------------------------------------------------------- yield* adminExec.openapi.addSpec({ spec: specJson, scope: String(orgScope.id), namespace: "vercel", baseUrl, headers: { Authorization: new ConfiguredHeaderBinding({ kind: "binding", slot: "auth:vercel_api_token", prefix: "Bearer ", }), }, queryParams: { token: new ConfiguredHeaderBinding({ kind: "binding", slot: "query_param:vercel_team_token", }), }, }); // ------------------------------------------------------------- // 2. Each user writes their personal access token under the // same secret id, but at their own scope. Tier-1 scope // routing means these coexist in the provider — alice's // write does not overwrite bob's. // ------------------------------------------------------------- yield* aliceExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_api_token"), scope: aliceScope.id, name: "Vercel API Token (alice)", value: "alice-vercel-token", }), ); yield* aliceExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_team_token"), scope: aliceScope.id, name: "Vercel Team Token (alice)", value: "alice-team", }), ); yield* bobExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_api_token"), scope: bobScope.id, name: "Vercel API Token (bob)", value: "bob-vercel-token", }), ); yield* bobExec.secrets.set( new SetSecretInput({ id: SecretId.make("vercel_team_token"), scope: bobScope.id, name: "Vercel Team Token (bob)", value: "bob-team", }), ); // ------------------------------------------------------------- // 3. Each user explicitly binds the inherited source slot at // their own scope. Same secret id, same source, different // binding owner and provider value. // ------------------------------------------------------------- yield* aliceExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: aliceScope.id, slot: "auth:vercel_api_token", value: { kind: "secret", secretId: SecretId.make("vercel_api_token"), }, }), ); yield* aliceExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: aliceScope.id, slot: "query_param:vercel_team_token", value: { kind: "secret", secretId: SecretId.make("vercel_team_token"), }, }), ); yield* bobExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: bobScope.id, slot: "auth:vercel_api_token", value: { kind: "secret", secretId: SecretId.make("vercel_api_token"), }, }), ); yield* bobExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: bobScope.id, slot: "query_param:vercel_team_token", value: { kind: "secret", secretId: SecretId.make("vercel_team_token"), }, }), ); // ------------------------------------------------------------- // 4. Invoking the shared tool through each user's executor // resolves the nearest credential binding. Alice's scope // yields her token; bob's scope yields his. Same source, same // tool, different injected bearer. // ------------------------------------------------------------- const aliceResult = (yield* aliceExec.tools.invoke( "vercel.projects.list", {}, autoApprove, )) as { data: { authorization?: string; token?: string } | null; error: unknown; }; expect(aliceResult.error).toBeNull(); expect(aliceResult.data?.authorization).toBe("Bearer alice-vercel-token"); expect(aliceResult.data?.token).toBe("alice-team"); const bobResult = (yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { data: { authorization?: string; token?: string } | null; error: unknown; }; expect(bobResult.error).toBeNull(); expect(bobResult.data?.authorization).toBe("Bearer bob-vercel-token"); expect(bobResult.data?.token).toBe("bob-team"); // ------------------------------------------------------------- // 5. Scope attribution: each user's token is pinned to their // own scope, never smuggled into the org fallback. // ------------------------------------------------------------- const aliceRows = yield* aliceExec.secrets.list(); const aliceToken = aliceRows.find((r) => String(r.id) === "vercel_api_token"); expect(String(aliceToken?.scopeId)).toBe("user-alice"); const bobRows = yield* bobExec.secrets.list(); const bobToken = bobRows.find((r) => String(r.id) === "vercel_api_token"); expect(String(bobToken?.scopeId)).toBe("user-bob"); // Admin's scope never received a token — `get` at the org // scope yields null and the source is effectively unusable // for the admin role, exactly as designed. const adminToken = yield* adminExec.secrets.get("vercel_api_token"); expect(adminToken).toBeNull(); // ------------------------------------------------------------- // 6. Cross-user isolation on enumeration: alice does not see // bob's token row, and vice versa. // ------------------------------------------------------------- const aliceIds = new Set(aliceRows.map((r) => `${String(r.scopeId)}:${String(r.id)}`)); expect(aliceIds).toContain("user-alice:vercel_api_token"); expect(aliceIds).not.toContain("user-bob:vercel_api_token"); const bobIds = new Set(bobRows.map((r) => `${String(r.scopeId)}:${String(r.id)}`)); expect(bobIds).toContain("user-bob:vercel_api_token"); expect(bobIds).not.toContain("user-alice:vercel_api_token"); }), ); it.effect("per-user bindings can point the same shared header slot at different secret ids", () => Effect.gen(function* () { const secretStore = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; const memoryProvider: SecretProvider = { key: "memory", writable: true, get: (id, scope) => Effect.sync(() => secretStore.get(key(scope, id)) ?? null), set: (id, value, scope) => Effect.sync(() => { secretStore.set(key(scope, id), value); }), delete: (id, scope) => Effect.sync(() => secretStore.delete(key(scope, id))), }; const memorySecretsPlugin = definePlugin(() => ({ id: "memory-secrets" as const, storage: () => ({}), secretProviders: [memoryProvider], })); const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const baseUrl = ""; const plugins = [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const; const schema = collectSchemas(plugins); const adapter = makeMemoryAdapter({ schema }); const blobs = makeInMemoryBlobStore(); const now = new Date(); const orgScope = new Scope({ id: ScopeId.make("org"), name: "acme-org", createdAt: now, }); const aliceScope = new Scope({ id: ScopeId.make("user-alice"), name: "alice", createdAt: now, }); const bobScope = new Scope({ id: ScopeId.make("user-bob"), name: "bob", createdAt: now, }); const adminExec = yield* createExecutor({ scopes: [orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const aliceExec = yield* createExecutor({ scopes: [aliceScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); const bobExec = yield* createExecutor({ scopes: [bobScope, orgScope], adapter, blobs, plugins, onElicitation: "accept-all", }); yield* adminExec.openapi.addSpec({ spec: specJson, scope: String(orgScope.id), namespace: "vercel", baseUrl, headers: { Authorization: new ConfiguredHeaderBinding({ kind: "binding", slot: "auth:personal-token", prefix: "Bearer ", }), }, }); yield* aliceExec.secrets.set( new SetSecretInput({ id: SecretId.make("alice_vercel_pat"), scope: aliceScope.id, name: "Alice Vercel PAT", value: "alice-vercel-token", }), ); yield* bobExec.secrets.set( new SetSecretInput({ id: SecretId.make("bob_vercel_pat"), scope: bobScope.id, name: "Bob Vercel PAT", value: "bob-vercel-token", }), ); yield* aliceExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: aliceScope.id, slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("alice_vercel_pat"), }, }), ); yield* bobExec.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "vercel", sourceScope: orgScope.id, scope: bobScope.id, slot: "auth:personal-token", value: { kind: "secret", secretId: SecretId.make("bob_vercel_pat"), }, }), ); const aliceResult = (yield* aliceExec.tools.invoke( "vercel.projects.list", {}, autoApprove, )) as { data: { authorization?: string } | null; error: unknown }; expect(aliceResult.error).toBeNull(); expect(aliceResult.data?.authorization).toBe("Bearer alice-vercel-token"); const bobResult = (yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { data: { authorization?: string } | null; error: unknown; }; expect(bobResult.error).toBeNull(); expect(bobResult.data?.authorization).toBe("Bearer bob-vercel-token"); }), ); it.effect( "source binding precedence falls back to shared auth and source deletion clears descendant bindings", () => Effect.gen(function* () { const secretStore = new Map<string, string>(); const key = (scope: string, id: string) => `${scope}\u0000${id}`; const memoryProvider: SecretProvider = { key: "memory", writable: true,