- 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 | 25 | import { graphqlPlugin } from "@executor/plugin-graphql"; |
| 26 | 26 | import { workosVaultPlugin } from "@executor/plugin-workos-vault"; |
| 27 | 27 | |
| 28 | +import { env } from "cloudflare:workers"; | |
| 28 | 29 | import { DbService } from "./db"; |
| 29 | −import { server } from "../env"; | |
| 30 | 30 | |
| 31 | 31 | // --------------------------------------------------------------------------- |
| 32 | 32 | // Plugin list — one place, used for both the runtime and the CLI config |
| @@ -45,24 +45,31 @@ const createOrgPlugins = () => · 12 unmodified lines | ||
| 45 | 45 | graphqlPlugin(), |
| 46 | 46 | workosVaultPlugin({ |
| 47 | 47 | 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, | |
| 50 | 50 | }, |
| 51 | 51 | }), |
| 52 | 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 | |
| 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. | |
| 61 | 67 | // --------------------------------------------------------------------------- |
| 62 | 68 | |
| 63 | 69 | export const createScopedExecutor = ( |
| 64 | − scopeId: string, | |
| 65 | − scopeName: string, | |
| 70 | + userId: string, | |
| 71 | + organizationId: string, | |
| 72 | + organizationName: string, | |
| 66 | 73 | ) => |
| 67 | 74 | Effect.gen(function* () { |
| 68 | 75 | const { db } = yield* DbService; |
| @@ -72,9 +79,14 @@ export const createScopedExecutor = ( · 3 unmodified lines | ||
| 72 | 79 | const adapter = makePostgresAdapter({ db, schema }); |
| 73 | 80 | const blobs = makePostgresBlobStore({ db }); |
| 74 | 81 | |
| 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}`, | |
| 78 | 90 | createdAt: new Date(), |
| 79 | 91 | }); |
| 80 | 92 | |
| @@ -82,5 +94,10 @@ export const createScopedExecutor = ( · 1 unmodified line | ||
| 82 | 94 | // the opaque `InternalError({ traceId })` happens at the HTTP edge |
| 83 | 95 | // via `withCapture` (see `api/protected-layers.ts`). That's |
| 84 | 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 | }); |