- 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].
Showing the changes to apps/cloud/src/services/executor.ts (1 of 25 files). Show all
Unified · Split
| @@ -25,8 +25,8 @@ import { mcpPlugin } from "@executor/plugin-mcp"; · 24 unmodified lines | |||
| 25 | import { graphqlPlugin } from "@executor/plugin-graphql"; | 25 | import { graphqlPlugin } from "@executor/plugin-graphql"; |
| 26 | import { workosVaultPlugin } from "@executor/plugin-workos-vault"; | 26 | import { workosVaultPlugin } from "@executor/plugin-workos-vault"; |
| 27 | 27 | ||
| 28 | import { env } from "cloudflare:workers"; | ||
| 28 | import { DbService } from "./db"; | 29 | import { DbService } from "./db"; |
| 29 | import { server } from "../env"; | ||
| 30 | 30 | ||
| 31 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
| 32 | // Plugin list — one place, used for both the runtime and the CLI config | 32 | // 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 scope | 57 | // Scope stack is `[userOrgScope, orgScope]` — innermost first. The |
| 58 | // level the cloud app exposes. When workspace / user scopes land, this | 58 | // 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 | ||
| 63 | export const createScopedExecutor = ( | 69 | export 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 edge | 94 | // the opaque `InternalError({ traceId })` happens at the HTTP edge |
| 83 | // via `withCapture` (see `api/protected-layers.ts`). That's | 95 | // 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 | }); |