History

cloud/local/examples: thread per-user ScopeStack through HTTP + MCP

- cloud: resolves userId from the authenticated session and builds a [userOrgScope, orgScope] stack via createScopedExecutor(userId, orgId, orgName). userId threaded through execution-stack, mcp-session, and MCP dispatch init. - cloud env refactor: deletes the lazy Proxy apps/cloud/src/env.ts in favor of cloudflare:workers env imported at call sites; adds env-augment.d.ts to type secrets not present in generated wrangler types. - cloud/server.ts: env-guarded OTEL wiring (skips instrument() when AXIOM_TOKEN absent); captures rawFetch before instrument() mutates it so dev requests still land on the Hono app. - cloud/services/db.ts: prefers DATABASE_URL over the hyperdrive proxy so local dev works without the tunnel. - vite.config.ts: devCrashGuard plugin that prints uncaught Worker errors with a stable exit. - hosts/mcp: optional debug flag surfaces verbose session logging, wired up from cloud mcp-session. - local/examples/marketing: migrated to scopes: [scope].

Rhys Sullivan committed 6 months ago

Commit d4e311e1 parent 427699c25 files · +320 −174Browse files

Showing the changes to apps/cloud/src/services/executor.ts (1 of 25 files). Show all

Unified · Split

apps/cloud/src/services/executor.ts+31−14
@@ -25,8 +25,8 @@ import { mcpPlugin } from "@executor/plugin-mcp"; · 24 unmodified lines
25import { graphqlPlugin } from "@executor/plugin-graphql";25import { graphqlPlugin } from "@executor/plugin-graphql";
26import { workosVaultPlugin } from "@executor/plugin-workos-vault";26import { workosVaultPlugin } from "@executor/plugin-workos-vault";
27 27
28import { env } from "cloudflare:workers";
28import { DbService } from "./db";29import { DbService } from "./db";
29import { server } from "../env";
30 30
31// ---------------------------------------------------------------------------31// ---------------------------------------------------------------------------
32// Plugin list — one place, used for both the runtime and the CLI config32// Plugin list — one place, used for both the runtime and the CLI config
@@ -45,24 +45,31 @@ const createOrgPlugins = () => · 12 unmodified lines
45 graphqlPlugin(),45 graphqlPlugin(),
46 workosVaultPlugin({46 workosVaultPlugin({
47 credentials: {47 credentials: {
48 apiKey: server.WORKOS_API_KEY,48 apiKey: env.WORKOS_API_KEY,
49 clientId: server.WORKOS_CLIENT_ID,49 clientId: env.WORKOS_CLIENT_ID,
50 },50 },
51 }),51 }),
52 ] as const;52 ] as const;
53 53
54// ---------------------------------------------------------------------------54// ---------------------------------------------------------------------------
55// Create a fresh executor for a scope (stateless, per-request).55// Create a fresh executor for a (user, org) pair (stateless, per-request).
56//56//
57// Today "scope" is the WorkOS organization — orgs are the only scope57// Scope stack is `[userOrgScope, orgScope]` — innermost first. The
58// level the cloud app exposes. When workspace / user scopes land, this58// user-within-org scope id (`user-org:${userId}:${orgId}`) intentionally
59// function grows to accept a `ScopeStack` instead of a single scope id,59// includes the org id so the same WorkOS user in a different org gets a
60// and nothing downstream (executor, plugins, storage) has to change.60// distinct scope row; future workspace scopes can slot in between without
61// conflicting with a hypothetical global user scope.
62//
63// OAuth tokens land at `ctx.scopes[0]` (the user-org scope) by default, so
64// a member's access/refresh tokens can't leak to other members via
65// `secrets.list`, while source rows and org-wide credentials live on the
66// outer scope.
61// ---------------------------------------------------------------------------67// ---------------------------------------------------------------------------
62 68
63export const createScopedExecutor = (69export const createScopedExecutor = (
64 scopeId: string,70 userId: string,
65 scopeName: string,71 organizationId: string,
72 organizationName: string,
66) =>73) =>
67 Effect.gen(function* () {74 Effect.gen(function* () {
68 const { db } = yield* DbService;75 const { db } = yield* DbService;
@@ -72,9 +79,14 @@ export const createScopedExecutor = ( · 3 unmodified lines
72 const adapter = makePostgresAdapter({ db, schema });79 const adapter = makePostgresAdapter({ db, schema });
73 const blobs = makePostgresBlobStore({ db });80 const blobs = makePostgresBlobStore({ db });
74 81
75 const scope = new Scope({82 const orgScope = new Scope({
76 id: ScopeId.make(scopeId),83 id: ScopeId.make(organizationId),
77 name: scopeName,84 name: organizationName,
85 createdAt: new Date(),
86 });
87 const userOrgScope = new Scope({
88 id: ScopeId.make(`user-org:${userId}:${organizationId}`),
89 name: `Personal · ${organizationName}`,
78 createdAt: new Date(),90 createdAt: new Date(),
79 });91 });
80 92
@@ -82,5 +94,10 @@ export const createScopedExecutor = ( · 1 unmodified line
82 // the opaque `InternalError({ traceId })` happens at the HTTP edge94 // the opaque `InternalError({ traceId })` happens at the HTTP edge
83 // via `withCapture` (see `api/protected-layers.ts`). That's95 // via `withCapture` (see `api/protected-layers.ts`). That's
84 // where `ErrorCaptureLive` (Sentry) gets wired in.96 // where `ErrorCaptureLive` (Sentry) gets wired in.
85 return yield* createExecutor({ scope, adapter, blobs, plugins });97 return yield* createExecutor({
98 scopes: [userOrgScope, orgScope],
99 adapter,
100 blobs,
101 plugins,
102 });
86 });103 });