import { expect, layer } from "@effect/vitest";import { Effect, Layer, Predicate, 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 http from "node:http";import type { AddressInfo } from "node:net"; import { createExecutor, definePlugin, type DBAdapter, makeTestConfig, RemoveSecretInput, Scope, ScopeId, SecretId, SetSecretInput, type InvokeOptions, type SecretProvider, type Where,} from "@executor-js/sdk";import { memorySecretsPlugin } from "@executor-js/sdk/testing"; const TEST_SCOPE = "test-scope";import { openApiPlugin } from "./plugin";import { ConfiguredHeaderBinding, OAuth2SourceConfig, OpenApiSourceBindingInput } from "./types";import { makeOpenApiTestServer } from "../testing"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FindManyCall = { readonly model: string; readonly where?: readonly Where[];}; const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => ({ ...adapter, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return adapter.findMany(data); }, transaction: (callback) => adapter.transaction((trx) => callback({ ...trx, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return trx.findMany(data); }, }), ),}); // ---------------------------------------------------------------------------// Define a test API with Effect HttpApi// --------------------------------------------------------------------------- class Item extends Schema.Class<Item>("Item")({ id: Schema.Number, name: Schema.String,}) {} class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ authorization: Schema.optional(Schema.String), "x-static": Schema.optional(Schema.String),}) {} class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()( "QueryValidationError", { message: Schema.String, },) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) })) .add( HttpApiEndpoint.get("getItem", "/items/:itemId", { params: Schema.Struct({ itemId: Schema.NumberFromString }), success: Item, }), ) .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders })) .add( HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", { params: Schema.Struct({ entryTypeId: Schema.String }), success: Schema.Unknown, error: QueryValidationError, }), ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const spec = OpenApi.fromApi(TestApi);const specJson = JSON.stringify(spec); // ---------------------------------------------------------------------------// Implement handlers// --------------------------------------------------------------------------- const ITEMS = [ { id: 1, name: "Widget" }, { id: 2, name: "Gadget" }, { id: 3, name: "Doohickey" },]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => handlers .handle("listItems", () => Effect.succeed(ITEMS.map((item) => new Item(item)))) .handle("getItem", (req) => Effect.succeed( new Item( ITEMS.find((i) => i.id === req.params.itemId) ?? { id: 0, name: "Unknown", }, ), ), ) .handle("echoHeaders", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; return new EchoHeaders({ authorization: req.headers["authorization"], "x-static": req.headers["x-static"], }); }), ) .handle("queryRows", () => Effect.fail( new QueryValidationError({ message: 'Field with name "DisplayName" does not exist', }), ), ),); // ---------------------------------------------------------------------------// Test layer: real server on port 0 + HttpClient pointing at it// --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.layer(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true }).pipe( Layer.provideMerge(NodeHttpServer.layerTest),); const serveSpecRequiringHeader = () => { const state = { requests: 0, lastToken: null as string | null }; const server = http.createServer((req, res) => { state.requests++; state.lastToken = req.headers["x-spec-token"]?.toString() ?? null; if (state.lastToken !== "org-token") { res.writeHead(401, { "content-type": "application/json" }); res.end(JSON.stringify({ error: "missing token" })); return; } res.writeHead(200, { "content-type": "application/json" }); res.end(specJson); }); return new Promise<{ readonly specUrl: string; readonly requestCount: () => number; readonly lastToken: () => string | null; readonly close: () => Promise<void>; }>((resolve) => { server.listen(0, "127.0.0.1", () => { const { port } = server.address() as AddressInfo; resolve({ specUrl: `http://127.0.0.1:${port}/spec.json`, requestCount: () => state.requests, lastToken: () => state.lastToken, close: () => new Promise((closeResolve) => { server.close(() => closeResolve()); }), }); }); });}; // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI Plugin", (it) => { it.effect("previewSpec returns metadata and header presets", () => Effect.gen(function* () { const server = yield* makeOpenApiTestServer({ spec }); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: server.httpClientLayer }), memorySecretsPlugin(), ] as const, }), ); const preview = yield* executor.openapi.previewSpec(server.specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ); it.effect("registers static openapi control tools", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id); expect(ids).toContain("openapi.previewSpec"); expect(ids).toContain("openapi.addSource"); }), ); it.effect("lists openapi as a static runtime source", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const sources = yield* executor.sources.list(); const control = sources.find((s) => s.id === "openapi"); expect(control).toBeDefined(); expect(control!.runtime).toBe(true); expect(control!.canRemove).toBe(false); }), ); it.effect("invokes static previewSpec through executor.tools.invoke", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const result = (yield* executor.tools.invoke( "openapi.previewSpec", { spec: specJson }, autoApprove, )) as { operationCount: number }; expect(result.operationCount).toBeGreaterThanOrEqual(2); }), ); it.effect("invokes static addSource through executor.tools.invoke", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const userScope = ScopeId.make("static-user"); const orgScope = ScopeId.make("static-org"); const executor = yield* createExecutor( makeTestConfig({ scopes: [ new Scope({ id: userScope, name: "user", createdAt: new Date() }), new Scope({ id: orgScope, name: "org", createdAt: new Date() }), ], plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const result = (yield* executor.tools.invoke( "openapi.addSource", { scope: String(orgScope), spec: specJson, namespace: "runtime" }, autoApprove, )) as { sourceId: string; toolCount: number }; expect(result).toEqual({ sourceId: "runtime", toolCount: 4 }); expect(yield* executor.openapi.getSource("runtime", String(userScope))).toBeNull(); expect((yield* executor.openapi.getSource("runtime", String(orgScope)))?.scope).toBe( orgScope, ); expect((yield* executor.tools.list()).map((t) => t.id)).toContain("runtime.items.listItems"); }), ); it.effect("requires approval before adding a source through the runtime tool", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [openApiPlugin()] as const }), ); const declined = yield* executor.tools .invoke( "openapi.addSource", { scope: TEST_SCOPE, spec: specJson, namespace: "runtime_declined" }, { onElicitation: () => Effect.succeed({ action: "decline" as const }) }, ) .pipe(Effect.flip); expect(Predicate.isTagged(declined, "ElicitationDeclinedError")).toBe(true); expect(yield* executor.openapi.getSource("runtime_declined", TEST_SCOPE)).toBeNull(); expect((yield* executor.tools.list()).map((t) => t.id)).not.toContain( "runtime_declined.items.listItems", ); }), ); it.effect("adds an org source whose direct credentials are owned by the user scope", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const userScope = ScopeId.make("openapi-user"); const orgScope = ScopeId.make("openapi-org"); const executor = yield* createExecutor( makeTestConfig({ scopes: [ new Scope({ id: userScope, name: "user", createdAt: new Date() }), new Scope({ id: orgScope, name: "org", createdAt: new Date() }), ], plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); yield* executor.secrets.set( new SetSecretInput({ id: SecretId.make("user-query-token"), scope: userScope, name: "User query token", value: "user-token", }), ); const input = { spec: specJson, scope: String(orgScope), namespace: "org_direct_user_credential", queryParams: { token: { secretId: "user-query-token" } }, credentialTargetScope: String(userScope), }; yield* executor.openapi.addSpec(input); const bindings = yield* executor.openapi.listSourceBindings( "org_direct_user_credential", String(orgScope), ); expect(bindings).toHaveLength(1); expect(bindings[0]).toMatchObject({ scopeId: userScope, slot: "query_param:token", value: { kind: "secret", secretId: SecretId.make("user-query-token") }, }); }), ); it.effect("updateSource removes bindings for credential slots no longer present", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); yield* executor.secrets.set( new SetSecretInput({ id: SecretId.make("old-token"), scope: ScopeId.make(TEST_SCOPE), name: "Old token", value: "old-secret", }), ); yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "stale_binding", baseUrl: "", credentialTargetScope: TEST_SCOPE, headers: { "X-Old": { secretId: "old-token" }, }, }); yield* executor.openapi.updateSource("stale_binding", TEST_SCOPE, { headers: {}, }); const bindings = yield* executor.openapi.listSourceBindings("stale_binding", TEST_SCOPE); expect(bindings).toEqual([]); }), ); it.effect("updateSource removes stale OAuth2 bindings when the OAuth template changes", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); yield* executor.secrets.set( new SetSecretInput({ id: SecretId.make("old-client-id"), scope: ScopeId.make(TEST_SCOPE), name: "Old client ID", value: "client-id", }), ); const oldOAuth = new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: "old", flow: "authorizationCode", tokenUrl: "https://auth.example.com/token", authorizationUrl: "https://auth.example.com/authorize", clientIdSlot: "oauth2:old:client-id", clientSecretSlot: null, connectionSlot: "oauth2:old:connection", scopes: ["read"], }); yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "stale_oauth", baseUrl: "", oauth2: oldOAuth, }); yield* executor.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "stale_oauth", sourceScope: ScopeId.make(TEST_SCOPE), scope: ScopeId.make(TEST_SCOPE), slot: oldOAuth.clientIdSlot, value: { kind: "secret", secretId: SecretId.make("old-client-id") }, }), ); yield* executor.openapi.updateSource("stale_oauth", TEST_SCOPE, { oauth2: new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: "new", flow: "authorizationCode", tokenUrl: "https://auth.example.com/token", authorizationUrl: "https://auth.example.com/authorize", clientIdSlot: "oauth2:new:client-id", clientSecretSlot: null, connectionSlot: "oauth2:new:connection", scopes: ["read"], }), }); const bindings = yield* executor.openapi.listSourceBindings("stale_oauth", TEST_SCOPE); expect(bindings.some((binding) => binding.slot === oldOAuth.clientIdSlot)).toBe(false);Lines 1–500 of 1388. Load more
import { expect, layer } from "@effect/vitest"; import { Effect, Layer, Predicate, 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 http from "node:http"; import type { AddressInfo } from "node:net"; import { createExecutor, definePlugin, type DBAdapter, makeTestConfig, RemoveSecretInput, Scope, ScopeId, SecretId, SetSecretInput, type InvokeOptions, type SecretProvider, type Where, } from "@executor-js/sdk"; import { memorySecretsPlugin } from "@executor-js/sdk/testing"; const TEST_SCOPE = "test-scope"; import { openApiPlugin } from "./plugin"; import { ConfiguredHeaderBinding, OAuth2SourceConfig, OpenApiSourceBindingInput } from "./types"; import { makeOpenApiTestServer } from "../testing"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FindManyCall = { readonly model: string; readonly where?: readonly Where[]; }; const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => ({ ...adapter, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return adapter.findMany(data); }, transaction: (callback) => adapter.transaction((trx) => callback({ ...trx, findMany: (data) => { calls.push({ model: data.model, where: data.where }); return trx.findMany(data); }, }), ), }); // --------------------------------------------------------------------------- // Define a test API with Effect HttpApi // --------------------------------------------------------------------------- class Item extends Schema.Class<Item>("Item")({ id: Schema.Number, name: Schema.String, }) {} class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ authorization: Schema.optional(Schema.String), "x-static": Schema.optional(Schema.String), }) {} class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()( "QueryValidationError", { message: Schema.String, }, ) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) })) .add( HttpApiEndpoint.get("getItem", "/items/:itemId", { params: Schema.Struct({ itemId: Schema.NumberFromString }), success: Item, }), ) .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders })) .add( HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", { params: Schema.Struct({ entryTypeId: Schema.String }), success: Schema.Unknown, error: QueryValidationError, }), ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const spec = OpenApi.fromApi(TestApi); const specJson = JSON.stringify(spec); // --------------------------------------------------------------------------- // Implement handlers // --------------------------------------------------------------------------- const ITEMS = [ { id: 1, name: "Widget" }, { id: 2, name: "Gadget" }, { id: 3, name: "Doohickey" }, ]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => handlers .handle("listItems", () => Effect.succeed(ITEMS.map((item) => new Item(item)))) .handle("getItem", (req) => Effect.succeed( new Item( ITEMS.find((i) => i.id === req.params.itemId) ?? { id: 0, name: "Unknown", }, ), ), ) .handle("echoHeaders", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; return new EchoHeaders({ authorization: req.headers["authorization"], "x-static": req.headers["x-static"], }); }), ) .handle("queryRows", () => Effect.fail( new QueryValidationError({ message: 'Field with name "DisplayName" does not exist', }), ), ), ); // --------------------------------------------------------------------------- // Test layer: real server on port 0 + HttpClient pointing at it // --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.layer(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true }).pipe( Layer.provideMerge(NodeHttpServer.layerTest), ); const serveSpecRequiringHeader = () => { const state = { requests: 0, lastToken: null as string | null }; const server = http.createServer((req, res) => { state.requests++; state.lastToken = req.headers["x-spec-token"]?.toString() ?? null; if (state.lastToken !== "org-token") { res.writeHead(401, { "content-type": "application/json" }); res.end(JSON.stringify({ error: "missing token" })); return; } res.writeHead(200, { "content-type": "application/json" }); res.end(specJson); }); return new Promise<{ readonly specUrl: string; readonly requestCount: () => number; readonly lastToken: () => string | null; readonly close: () => Promise<void>; }>((resolve) => { server.listen(0, "127.0.0.1", () => { const { port } = server.address() as AddressInfo; resolve({ specUrl: `http://127.0.0.1:${port}/spec.json`, requestCount: () => state.requests, lastToken: () => state.lastToken, close: () => new Promise((closeResolve) => { server.close(() => closeResolve()); }), }); }); }); }; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI Plugin", (it) => { it.effect("previewSpec returns metadata and header presets", () => Effect.gen(function* () { const server = yield* makeOpenApiTestServer({ spec }); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: server.httpClientLayer }), memorySecretsPlugin(), ] as const, }), ); const preview = yield* executor.openapi.previewSpec(server.specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ); it.effect("registers static openapi control tools", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const tools = yield* executor.tools.list(); const ids = tools.map((t) => t.id); expect(ids).toContain("openapi.previewSpec"); expect(ids).toContain("openapi.addSource"); }), ); it.effect("lists openapi as a static runtime source", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const sources = yield* executor.sources.list(); const control = sources.find((s) => s.id === "openapi"); expect(control).toBeDefined(); expect(control!.runtime).toBe(true); expect(control!.canRemove).toBe(false); }), ); it.effect("invokes static previewSpec through executor.tools.invoke", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const result = (yield* executor.tools.invoke( "openapi.previewSpec", { spec: specJson }, autoApprove, )) as { operationCount: number }; expect(result.operationCount).toBeGreaterThanOrEqual(2); }), ); it.effect("invokes static addSource through executor.tools.invoke", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const userScope = ScopeId.make("static-user"); const orgScope = ScopeId.make("static-org"); const executor = yield* createExecutor( makeTestConfig({ scopes: [ new Scope({ id: userScope, name: "user", createdAt: new Date() }), new Scope({ id: orgScope, name: "org", createdAt: new Date() }), ], plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); const result = (yield* executor.tools.invoke( "openapi.addSource", { scope: String(orgScope), spec: specJson, namespace: "runtime" }, autoApprove, )) as { sourceId: string; toolCount: number }; expect(result).toEqual({ sourceId: "runtime", toolCount: 4 }); expect(yield* executor.openapi.getSource("runtime", String(userScope))).toBeNull(); expect((yield* executor.openapi.getSource("runtime", String(orgScope)))?.scope).toBe( orgScope, ); expect((yield* executor.tools.list()).map((t) => t.id)).toContain("runtime.items.listItems"); }), ); it.effect("requires approval before adding a source through the runtime tool", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [openApiPlugin()] as const }), ); const declined = yield* executor.tools .invoke( "openapi.addSource", { scope: TEST_SCOPE, spec: specJson, namespace: "runtime_declined" }, { onElicitation: () => Effect.succeed({ action: "decline" as const }) }, ) .pipe(Effect.flip); expect(Predicate.isTagged(declined, "ElicitationDeclinedError")).toBe(true); expect(yield* executor.openapi.getSource("runtime_declined", TEST_SCOPE)).toBeNull(); expect((yield* executor.tools.list()).map((t) => t.id)).not.toContain( "runtime_declined.items.listItems", ); }), ); it.effect("adds an org source whose direct credentials are owned by the user scope", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const userScope = ScopeId.make("openapi-user"); const orgScope = ScopeId.make("openapi-org"); const executor = yield* createExecutor( makeTestConfig({ scopes: [ new Scope({ id: userScope, name: "user", createdAt: new Date() }), new Scope({ id: orgScope, name: "org", createdAt: new Date() }), ], plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); yield* executor.secrets.set( new SetSecretInput({ id: SecretId.make("user-query-token"), scope: userScope, name: "User query token", value: "user-token", }), ); const input = { spec: specJson, scope: String(orgScope), namespace: "org_direct_user_credential", queryParams: { token: { secretId: "user-query-token" } }, credentialTargetScope: String(userScope), }; yield* executor.openapi.addSpec(input); const bindings = yield* executor.openapi.listSourceBindings( "org_direct_user_credential", String(orgScope), ); expect(bindings).toHaveLength(1); expect(bindings[0]).toMatchObject({ scopeId: userScope, slot: "query_param:token", value: { kind: "secret", secretId: SecretId.make("user-query-token") }, }); }), ); it.effect("updateSource removes bindings for credential slots no longer present", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); yield* executor.secrets.set( new SetSecretInput({ id: SecretId.make("old-token"), scope: ScopeId.make(TEST_SCOPE), name: "Old token", value: "old-secret", }), ); yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "stale_binding", baseUrl: "", credentialTargetScope: TEST_SCOPE, headers: { "X-Old": { secretId: "old-token" }, }, }); yield* executor.openapi.updateSource("stale_binding", TEST_SCOPE, { headers: {}, }); const bindings = yield* executor.openapi.listSourceBindings("stale_binding", TEST_SCOPE); expect(bindings).toEqual([]); }), ); it.effect("updateSource removes stale OAuth2 bindings when the OAuth template changes", () => Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); const executor = yield* createExecutor( makeTestConfig({ plugins: [ openApiPlugin({ httpClientLayer: clientLayer }), memorySecretsPlugin(), ] as const, }), ); yield* executor.secrets.set( new SetSecretInput({ id: SecretId.make("old-client-id"), scope: ScopeId.make(TEST_SCOPE), name: "Old client ID", value: "client-id", }), ); const oldOAuth = new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: "old", flow: "authorizationCode", tokenUrl: "https://auth.example.com/token", authorizationUrl: "https://auth.example.com/authorize", clientIdSlot: "oauth2:old:client-id", clientSecretSlot: null, connectionSlot: "oauth2:old:connection", scopes: ["read"], }); yield* executor.openapi.addSpec({ spec: specJson, scope: TEST_SCOPE, namespace: "stale_oauth", baseUrl: "", oauth2: oldOAuth, }); yield* executor.openapi.setSourceBinding( new OpenApiSourceBindingInput({ sourceId: "stale_oauth", sourceScope: ScopeId.make(TEST_SCOPE), scope: ScopeId.make(TEST_SCOPE), slot: oldOAuth.clientIdSlot, value: { kind: "secret", secretId: SecretId.make("old-client-id") }, }), ); yield* executor.openapi.updateSource("stale_oauth", TEST_SCOPE, { oauth2: new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: "new", flow: "authorizationCode", tokenUrl: "https://auth.example.com/token", authorizationUrl: "https://auth.example.com/authorize", clientIdSlot: "oauth2:new:client-id", clientSecretSlot: null, connectionSlot: "oauth2:new:connection", scopes: ["read"], }), }); const bindings = yield* executor.openapi.listSourceBindings("stale_oauth", TEST_SCOPE); expect(bindings.some((binding) => binding.slot === oldOAuth.clientIdSlot)).toBe(false);