129 lines · 4.9 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Owner-isolation bearer-token auth on the OpenAPI plugin (v2).//// v1 modelled a Vercel-style "admin uploads the spec once; each user binds the// shared Authorization slot to their own per-scope secret" flow through the// scope-partitioning SecretProvider + credential-binding slots. v2 deletes that// machinery: a connection IS the credential, owner-scoped (org vs user), and a// connection's value renders through the integration's `authenticationTemplate`.//// The surviving, real behaviour: one integration in the catalog, distinct// connections per owner, each injecting its own token. The org connection's// value and a user connection's value are isolated and applied independently.//// removed: the per-scope-secret-id, slot-binding, source-shadow, and// "same secret id distinct value per user scope" cases — those exercised the v1// scope stack + SecretProvider(scope) + credential_binding model that no longer// exists.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Effect, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { FetchHttpClient, HttpServerRequest } from "effect/unstable/http"; import {  createExecutor,  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  ToolAddress,} from "@executor-js/sdk";import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";import {  serveOpenApiHttpApiTestServer,  unwrapInvocation,} from "@executor-js/plugin-openapi/testing"; import { openApiPlugin } from "./plugin";import { type AuthenticationInput } from "./types"; // ---------------------------------------------------------------------------// Test API — a single endpoint that echoes the Authorization header so the// test can assert which owner's token got injected.// --------------------------------------------------------------------------- const EchoHeaders = Schema.Struct({  authorization: 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 ProjectsGroupLive = HttpApiBuilder.group(VercelApi, "projects", (handlers) =>  handlers.handle("list", () =>    Effect.gen(function* () {      const req = yield* HttpServerRequest.HttpServerRequest;      return EchoHeaders.make({        authorization: req.headers["authorization"],      });    }),  ),); const serveVercel = () =>  serveOpenApiHttpApiTestServer({ api: VercelApi, handlersLayer: ProjectsGroupLive }); // Bearer template: the connection value renders into `Authorization: Bearer …`.const bearerTemplate: AuthenticationInput = {  slug: AuthTemplateSlug.make("bearer"),  type: "apiKey",  headers: { authorization: ["Bearer ", { type: "variable" as const, name: "token" }] },}; const testPlugins = () =>  [openApiPlugin({ httpClientLayer: FetchHttpClient.layer }), memoryCredentialsPlugin()] as const; const VERCEL = IntegrationSlug.make("vercel");const TEMPLATE = AuthTemplateSlug.make("bearer");const LIST = "projects.list"; describe("OpenAPI owner-isolated bearer", () => {  it.effect("org and user connections each inject their own token", () =>    Effect.scoped(      Effect.gen(function* () {        const server = yield* serveVercel();        const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() }));         // Admin uploads the spec once; the integration declares a bearer        // template but no token value.        yield* executor.openapi.addSpec({          spec: { kind: "blob", value: server.specJson },          slug: "vercel",          baseUrl: server.baseUrl,          authenticationTemplate: [bearerTemplate],        });         // An org-shared connection and a user-owned connection, each with their        // own token.        yield* executor.connections.create({          owner: "org",          name: ConnectionName.make("shared"),          integration: VERCEL,          template: TEMPLATE,          value: "org-token",        });        yield* executor.connections.create({          owner: "user",          name: ConnectionName.make("alice"),          integration: VERCEL,          template: TEMPLATE,          value: "alice-token",        });         const orgResult = unwrapInvocation(          yield* executor.execute(ToolAddress.make(`tools.vercel.org.shared.${LIST}`), {}),        ).data as { authorization?: string };        const userResult = unwrapInvocation(          yield* executor.execute(ToolAddress.make(`tools.vercel.user.alice.${LIST}`), {}),        ).data as { authorization?: string };         expect(orgResult.authorization).toBe("Bearer org-token");        expect(userResult.authorization).toBe("Bearer alice-token");      }),    ),  );});