// ---------------------------------------------------------------------------// 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"); }), ), );});
// ---------------------------------------------------------------------------
// 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");
}),
),
);
});