Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions apps/cloud/src/mcp-session.e2e.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,14 @@ import {
createExecutor,
definePlugin,
} from "@executor/sdk";
import {
makePostgresAdapter,
makePostgresBlobStore,
} from "@executor/storage-postgres";
import { makePostgresAdapter, makePostgresBlobStore } from "@executor/storage-postgres";
import { openApiPlugin } from "@executor/plugin-openapi";
import { mcpPlugin } from "@executor/plugin-mcp";
import { graphqlPlugin } from "@executor/plugin-graphql";
import { workosVaultPlugin } from "@executor/plugin-workos-vault";
import { makeTestWorkOSVaultClient } from "@executor/plugin-workos-vault/testing";

import { DbService } from "./services/db";
import { makeFakeVaultClient } from "./services/__test-harness__/api-harness";

// ---------------------------------------------------------------------------
// Test-only plugin: exposes one in-memory tool that elicits once. Lets the
Expand All @@ -69,7 +66,11 @@ const elicitingTestPlugin = definePlugin(() => ({
name: "needsApproval",
description: "Tool that asks the caller to approve before returning.",
inputSchema: EMPTY_INPUT_SCHEMA,
handler: ({ elicit }: { elicit: (r: FormElicitation) => Effect.Effect<typeof ElicitationResponse.Type, unknown> }) =>
handler: ({
elicit,
}: {
elicit: (r: FormElicitation) => Effect.Effect<typeof ElicitationResponse.Type, unknown>;
}) =>
Effect.gen(function* () {
const response = yield* elicit(
new FormElicitation({
Expand Down Expand Up @@ -98,18 +99,14 @@ const ELICITATION_CAPS: ClientCapabilities = {

type BuildOptions = { readonly withElicitingPlugin?: boolean };

const buildScopedExecutor = (
scopeId: string,
scopeName: string,
options: BuildOptions = {},
) =>
const buildScopedExecutor = (scopeId: string, scopeName: string, options: BuildOptions = {}) =>
Effect.gen(function* () {
const { db } = yield* DbService;
const basePlugins = [
openApiPlugin(),
mcpPlugin({ dangerouslyAllowStdioMCP: false }),
graphqlPlugin(),
workosVaultPlugin({ client: makeFakeVaultClient() }),
workosVaultPlugin({ client: makeTestWorkOSVaultClient() }),
] as const;
const plugins = options.withElicitingPlugin
? ([...basePlugins, elicitingTestPlugin()] as const)
Expand Down
32 changes: 5 additions & 27 deletions apps/cloud/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,11 @@
import { env } from "cloudflare:workers";
import { HttpApp, HttpServerRequest, HttpServerResponse } from "@effect/platform";
import * as Sentry from "@sentry/cloudflare";
import { Context, Effect, Either, Layer, Option, Schema } from "effect";
import { Context, Effect, Layer, Option, Schema } from "effect";
import { createRemoteJWKSet } from "jose";

import { TelemetryLive } from "./services/telemetry";
import {
McpJwtVerificationError,
verifyMcpAccessToken,
type VerifiedToken,
} from "./mcp-auth";
import { verifyMcpAccessToken, type VerifiedToken } from "./mcp-auth";

// ---------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -96,27 +92,9 @@ export class McpAuth extends Context.Tag("@executor/cloud/McpAuth")<
>() {}

const verifyJwt = (token: string) =>
Effect.gen(function* () {
const strictResult = yield* verifyMcpAccessToken(token, jwks, {
issuer: AUTHKIT_DOMAIN,
audience: RESOURCE_URL,
}).pipe(Effect.either);

if (Either.isRight(strictResult)) {
return strictResult.right;
}

if (env.MCP_STRICT_AUDIENCE === "true") {
return yield* Effect.fail(strictResult.left);
}

const verified = yield* verifyMcpAccessToken(token, jwks, {
issuer: AUTHKIT_DOMAIN,
});
yield* Effect.annotateCurrentSpan({
"mcp.auth.audience_fallback": true,
});
return verified;
verifyMcpAccessToken(token, jwks, {
issuer: AUTHKIT_DOMAIN,
audience: RESOURCE_URL,
});

export const McpAuthLive = Layer.succeed(McpAuth, {
Expand Down
173 changes: 25 additions & 148 deletions apps/cloud/src/services/__test-harness__/api-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,16 @@ import {
HttpServerRequest,
} from "@effect/platform";

import {
ExecutionEngineService,
ExecutorService,
} from "@executor/api/server";
import { ExecutionEngineService, ExecutorService } from "@executor/api/server";
import { createExecutionEngine } from "@executor/execution";
import { makeQuickJsExecutor } from "@executor/runtime-quickjs";
import {
Scope,
ScopeId,
collectSchemas,
createExecutor,
} from "@executor/sdk";
import {
makePostgresAdapter,
makePostgresBlobStore,
} from "@executor/storage-postgres";
import { Scope, ScopeId, collectSchemas, createExecutor } from "@executor/sdk";
import { makePostgresAdapter, makePostgresBlobStore } from "@executor/storage-postgres";
import { openApiPlugin } from "@executor/plugin-openapi";
import { mcpPlugin } from "@executor/plugin-mcp";
import { graphqlPlugin } from "@executor/plugin-graphql";
import {
workosVaultPlugin,
WorkOSVaultClientError,
type WorkOSVaultClient,
type WorkOSVaultObject,
type WorkOSVaultObjectMetadata,
} from "@executor/plugin-workos-vault";
import { workosVaultPlugin } from "@executor/plugin-workos-vault";
import { makeTestWorkOSVaultClient } from "@executor/plugin-workos-vault/testing";
import { OpenApiExtensionService } from "@executor/plugin-openapi/api";
import { McpExtensionService } from "@executor/plugin-mcp/api";
import { GraphqlExtensionService } from "@executor/plugin-graphql/api";
Expand All @@ -71,124 +55,29 @@ export const TEST_USER_HEADER = "x-test-user-id";
// Mirrors apps/cloud/src/services/executor.ts#createScopedExecutor — the
// per-user scope id bakes in the org so the same user id in a different
// org gets a distinct scope row.
const userOrgScopeId = (userId: string, orgId: string) =>
`user-org:${userId}:${orgId}`;
const userOrgScopeId = (userId: string, orgId: string) => `user-org:${userId}:${orgId}`;

// `asOrg(orgId, …)` callers don't care which specific user they are, only
// that the executor has a valid user-org scope. We give each org a stable
// default user so list/get operations at the org scope remain deterministic
// across calls within a single test.
const defaultUserFor = (orgId: string) => `default_user_${orgId}`;

// ---------------------------------------------------------------------------
// Fake WorkOS Vault client — in-memory map keyed by name.
// ---------------------------------------------------------------------------

export const makeFakeVaultClient = (): WorkOSVaultClient => {
const byName = new Map<string, WorkOSVaultObject>();
let seq = 0;
const nextId = () => `vault_${++seq}_${crypto.randomUUID().slice(0, 8)}`;

const create = (opts: { name: string; value: string; context: Record<string, string> }) => {
const id = nextId();
const metadata: WorkOSVaultObjectMetadata = {
context: opts.context,
id,
updatedAt: new Date(),
versionId: `v_${seq}`,
};
byName.set(opts.name, { id, name: opts.name, value: opts.value, metadata });
return metadata;
};

const notFound = (name: string) =>
Object.assign(new Error(`not found: ${name}`), { status: 404 });

const read = (name: string): WorkOSVaultObject => {
const obj = byName.get(name);
if (!obj) throw notFound(name);
return obj;
};

const update = (opts: { id: string; value: string }): WorkOSVaultObject => {
for (const [name, obj] of byName.entries()) {
if (obj.id === opts.id) {
const updated: WorkOSVaultObject = {
...obj,
value: opts.value,
metadata: { ...obj.metadata, updatedAt: new Date(), versionId: `v_${++seq}` },
};
byName.set(name, updated);
return updated;
}
}
throw notFound(opts.id);
};

const remove = (opts: { id: string }) => {
for (const [name, obj] of byName.entries()) {
if (obj.id === opts.id) byName.delete(name);
}
};

return {
use: (_op, fn) =>
Effect.tryPromise({
try: () =>
fn({
createObject: async (opts) => create(opts),
readObjectByName: async (name) => read(name),
updateObject: async (opts) => update(opts),
deleteObject: async (opts) => remove(opts),
}),
catch: (cause) => new Error(String(cause)) as never,
}) as never,
// The real client wraps SDK rejections in WorkOSVaultClientError so
// provider-side `isStatusError` checks can introspect `cause.status`.
// Mirror that here so our 404s flow through the same unwrap path.
createObject: (opts) =>
Effect.try({
try: () => create(opts),
catch: (cause) => new WorkOSVaultClientError({ cause, operation: "create_object" }),
}),
readObjectByName: (name) =>
Effect.try({
try: () => read(name),
catch: (cause) =>
new WorkOSVaultClientError({ cause, operation: "read_object_by_name" }),
}),
updateObject: (opts) =>
Effect.try({
try: () => update(opts),
catch: (cause) => new WorkOSVaultClientError({ cause, operation: "update_object" }),
}),
deleteObject: (opts) =>
Effect.try({
try: () => remove(opts),
catch: (cause) => new WorkOSVaultClientError({ cause, operation: "delete_object" }),
}),
};
};

// ---------------------------------------------------------------------------
// Executor factory — mirrors apps/cloud/services/executor#createScopedExecutor
// but with a fake vault client.
// but with the plugin package's shared test vault client.
// ---------------------------------------------------------------------------

const fakeVault = makeFakeVaultClient();
const testVault = makeTestWorkOSVaultClient();

const createTestScopedExecutor = (
userId: string,
orgId: string,
orgName: string,
) =>
const createTestScopedExecutor = (userId: string, orgId: string, orgName: string) =>
Effect.gen(function* () {
const { db } = yield* DbService;
const plugins = [
openApiPlugin(),
mcpPlugin({ dangerouslyAllowStdioMCP: false }),
graphqlPlugin(),
workosVaultPlugin({ client: fakeVault }),
workosVaultPlugin({ client: testVault }),
] as const;
const schema = collectSchemas(plugins);
const adapter = makePostgresAdapter({ db, schema });
Expand Down Expand Up @@ -279,17 +168,12 @@ const RouterApp = Effect.gen(function* () {
}
const userHeader = request.headers[TEST_USER_HEADER];
const userId =
typeof userHeader === "string" && userHeader.length > 0
? userHeader
: defaultUserFor(orgId);
typeof userHeader === "string" && userHeader.length > 0 ? userHeader : defaultUserFor(orgId);
return yield* yield* buildAppForScope(userId, orgId, `Org ${orgId}`);
});

const handler = HttpApp.toWebHandler(
RouterApp.pipe(
Effect.provide(DbService.Live),
Effect.provide(HttpServer.layerContext),
),
RouterApp.pipe(Effect.provide(DbService.Live), Effect.provide(HttpServer.layerContext)),
);

export const fetchForOrg = (orgId: string): typeof globalThis.fetch =>
Expand All @@ -301,10 +185,7 @@ export const fetchForOrg = (orgId: string): typeof globalThis.fetch =>
return handler(req);
}) as typeof globalThis.fetch;

export const fetchForUser = (
userId: string,
orgId: string,
): typeof globalThis.fetch =>
export const fetchForUser = (userId: string, orgId: string): typeof globalThis.fetch =>
((input: RequestInfo | URL, init?: RequestInit) => {
const base = input instanceof Request ? input : new Request(input, init);
const req = new Request(base, {
Expand All @@ -324,22 +205,21 @@ export const clientLayerForOrg = (orgId: string) =>

export const clientLayerForUser = (userId: string, orgId: string) =>
FetchHttpClient.layer.pipe(
Layer.provide(
Layer.succeed(FetchHttpClient.Fetch, fetchForUser(userId, orgId)),
),
Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchForUser(userId, orgId))),
);

// Constructs an HttpApiClient bound to the given org, hands it to `body`,
// and provides the org-scoped fetch layer in one step. Keeps per-test
// Effect blocks focused on the actual assertions.
type ApiShape = typeof ProtectedCloudApi extends HttpApi.HttpApi<
infer _Id,
infer Groups,
infer ApiError,
infer _ApiR
>
? HttpApiClient.Client<Groups, ApiError, never>
: never;
type ApiShape =
typeof ProtectedCloudApi extends HttpApi.HttpApi<
infer _Id,
infer Groups,
infer ApiError,
infer _ApiR
>
? HttpApiClient.Client<Groups, ApiError, never>
: never;

export const asOrg = <A, E>(
orgId: string,
Expand All @@ -362,14 +242,11 @@ export const asUser = <A, E>(
Effect.gen(function* () {
const client = yield* HttpApiClient.make(ProtectedCloudApi, { baseUrl: TEST_BASE_URL });
return yield* body(client);
}).pipe(
Effect.provide(clientLayerForUser(userId, orgId)),
) as Effect.Effect<A, E>;
}).pipe(Effect.provide(clientLayerForUser(userId, orgId))) as Effect.Effect<A, E>;

// Exposed so tests can build the same user-org scope id the harness uses
// when writing at a specific user's scope.
export const testUserOrgScopeId = (userId: string, orgId: string) =>
userOrgScopeId(userId, orgId);
export const testUserOrgScopeId = (userId: string, orgId: string) => userOrgScopeId(userId, orgId);

// Re-exports so call sites don't need a second import.
export { ProtectedCloudApi };
8 changes: 8 additions & 0 deletions packages/plugins/workos-vault/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"type": "module",
"exports": {
".": "./src/sdk/index.ts",
"./testing": "./src/sdk/testing.ts",
"./promise": "./src/promise.ts",
"./react": "./src/react/index.ts"
},
Expand All @@ -34,13 +35,20 @@
"types": "./dist/sdk/index.d.ts",
"default": "./dist/core.js"
}
},
"./testing": {
"import": {
"types": "./dist/sdk/testing.d.ts",
"default": "./dist/testing.js"
}
}
}
},
"scripts": {
"build": "tsup && (tsc --declaration --emitDeclarationOnly --outDir dist --rootDir src || true)",
"typecheck": "tsgo --noEmit",
"test": "vitest run",
"test:contract:workos-vault": "WORKOS_VAULT_CONTRACT=1 vitest run src/sdk/workos-vault.contract.test.ts",
"test:watch": "vitest",
"typecheck:slow": "bunx tsc --noEmit -p tsconfig.json"
},
Expand Down
Loading
Loading