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
2525 import { graphqlPlugin } from "@executor/plugin-graphql";
2626 import { workosVaultPlugin } from "@executor/plugin-workos-vault";
2727
28+import { env } from "cloudflare:workers";
2829 import { DbService } from "./db";
29−import { server } from "../env";
3030
3131 // ---------------------------------------------------------------------------
3232 // Plugin list — one place, used for both the runtime and the CLI config
@@ -45,24 +45,31 @@ const createOrgPlugins = () => · 12 unmodified lines
4545 graphqlPlugin(),
4646 workosVaultPlugin({
4747 credentials: {
48− apiKey: server.WORKOS_API_KEY,
49− clientId: server.WORKOS_CLIENT_ID,
48+ apiKey: env.WORKOS_API_KEY,
49+ clientId: env.WORKOS_CLIENT_ID,
5050 },
5151 }),
5252 ] as const;
5353
5454 // ---------------------------------------------------------------------------
55−// Create a fresh executor for a scope (stateless, per-request).
55+// Create a fresh executor for a (user, org) pair (stateless, per-request).
5656 //
57−// Today "scope" is the WorkOS organization — orgs are the only scope
58−// level the cloud app exposes. When workspace / user scopes land, this
59−// function grows to accept a `ScopeStack` instead of a single scope id,
60−// and nothing downstream (executor, plugins, storage) has to change.
57+// Scope stack is `[userOrgScope, orgScope]` — innermost first. The
58+// user-within-org scope id (`user-org:${userId}:${orgId}`) intentionally
59+// includes the org id so the same WorkOS user in a different org gets a
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.
6167 // ---------------------------------------------------------------------------
6268
6369 export const createScopedExecutor = (
64− scopeId: string,
65− scopeName: string,
70+ userId: string,
71+ organizationId: string,
72+ organizationName: string,
6673 ) =>
6774 Effect.gen(function* () {
6875 const { db } = yield* DbService;
@@ -72,9 +79,14 @@ export const createScopedExecutor = ( · 3 unmodified lines
7279 const adapter = makePostgresAdapter({ db, schema });
7380 const blobs = makePostgresBlobStore({ db });
7481
75− const scope = new Scope({
76− id: ScopeId.make(scopeId),
77− name: scopeName,
82+ const orgScope = new Scope({
83+ id: ScopeId.make(organizationId),
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}`,
7890 createdAt: new Date(),
7991 });
8092
@@ -82,5 +94,10 @@ export const createScopedExecutor = ( · 1 unmodified line
8294 // the opaque `InternalError({ traceId })` happens at the HTTP edge
8395 // via `withCapture` (see `api/protected-layers.ts`). That's
8496 // 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+ });
86103 });