* feat(cloudflare): serve MCP sessions with Agents * feat(execution): add paused execution introspection API Expose pausedExecutionCount and hasPausedExecutions on ExecutionEngine so Cloudflare MCP session DOs can distinguish idle cleanup from paused continuations. * feat(cloudflare): extend MCP session lease while paused continuations exist Add shared alarm policy so idle sessions still destroy after 5 minutes, but sessions with in-memory paused executions reschedule a 10-minute lease instead of tearing down the runtime. Preserves the legacy unaddressable-alarm guard. * fix(cloud): forward paused execution introspection * fix(cloudflare): cap paused-session lease at one approval window (greptile) The extend_paused_lease branch rescheduled the alarm unconditionally, so a session whose paused execution was never resumed (browser tab closed, client disconnect) would extend the lease forever and keep the Durable Object alive and billable indefinitely. Bound the lease to a single approval window: once idle exceeds SESSION_TIMEOUT_MS + PAUSED_EXECUTION_LEASE_MS (15 min), the session is torn down regardless of outstanding paused work. By that point the browser approval wait (tool-server, 10 min) has already timed out, so the paused execution is no longer resumable. idleMs grows across extensions because activity is not marked during the wait, so decideSessionAlarm stays pure. Also add the missing coverage: idle_within_timeout wins regardless of paused work, and the new cap forces destroy_idle_session. * chore: update lockfile * fix(mcp): settle approval pauses under hibernation Add host-owned paused execution hooks so Cloudflare MCP sessions can hold a bounded pending-approval lease, keep active POST requests alive until their response drains, and release hibernation blockers on resume or timeout. Also bound the approval wait window and resolve OpenAPI-backed annotations with point lookups so large specs do not reset the Durable Object before returning a pause envelope. This is the upstream port of the core runtime pieces from aryaSaatvik/executor#56. The execution-history payload compaction from that PR is omitted because this upstream branch does not contain that plugin. * refactor(mcp): align pause lifecycle telemetry Remove MCP host console logging wrappers and rely on existing debug logging plus span annotations for pause lifecycle state. Keep Cloudflare Durable Object logs focused on exceptional boundary events by dropping happy-path active POST and pending approval lease messages. * fix(mcp): guard approval timeout races Handle malformed active POST metadata without failing the Agent connection. Avoid overwriting a stored browser approval on timeout, and only resume timed-out executions directly when no MCP waiter is already registered. * refactor(cloudflare): remove legacy MCP session DO and finish cloud migration The hibernatable Agent-based MCP Durable Object now fully replaces the old bespoke session DO. This removes the superseded implementation instead of leaving it alongside the new one: - delete the old McpSessionDOBase + its worker transport and request-id queue (nothing extends it anymore; both hosts' concrete McpSessionDO already extend McpAgentSessionDOBase) - finish the cloud migration: cloud only kept the legacy McpServingRoutes session-store dispatch to publish its OAuth discovery docs. Make mcp.sessions optional and add a discovery-only McpDiscoveryRoutes layer so a host whose /mcp transport lives outside the envelope (the Agent bridge) can serve just the discovery docs. Cloud now wires only the auth seam. - drop the now-dead cloud session-store, seams, response-peek, and reporter wiring plus the stale package exports Net: the old impl and the half-migrated cloud envelope path are gone. Co-authored-by: Saatvik Arya <aryasaatvik@gmail.com> * style: oxfmt two react files left unformatted by the merge * fix: regenerate bun.lock to match merged manifests (frozen install was failing) * fix: reconcile bun.lock against main pins plus branch deps (frozen install passes) ---------
Unified · Split
| @@ -67,7 +67,6 @@ · 66 unmodified lines | ||
| 67 | 67 | "@tanstack/react-router": "catalog:", |
| 68 | 68 | "@tanstack/react-start": "catalog:", |
| 69 | 69 | "@workos-inc/node": "^8.11.1", |
| 70 | − "agents": "^0.10.0", | |
| 71 | 70 | "autumn-js": "^1.2.8", |
| 72 | 71 | "drizzle-orm": "catalog:", |
| 73 | 72 | "effect": "catalog:", |
| @@ -17,6 +17,8 @@ const makeBaseEngine = (): ExecutionEngine => · 16 unmodified lines | ||
| 17 | 17 | result: { result: "ok", logs: [] }, |
| 18 | 18 | }), |
| 19 | 19 | getPausedExecution: () => Effect.succeed(null), |
| 20 | + pausedExecutionCount: () => Effect.succeed(0), | |
| 21 | + hasPausedExecutions: () => Effect.succeed(false), | |
| 20 | 22 | getDescription: Effect.succeed("desc"), |
| 21 | 23 | }) as ExecutionEngine; |
| 22 | 24 | |
| @@ -12,7 +12,7 @@ import { workosAccountMiddleware } from "./account/account-api"; · 11 unmodified lines | ||
| 12 | 12 | import { ApiKeyService } from "./auth/api-keys"; |
| 13 | 13 | import { cloudIdentityFailureStrategy, workosIdentityLayer } from "./auth/workos-auth-provider"; |
| 14 | 14 | import { DbService } from "./db/db"; |
| 15 | −import { cloudMcpAuth, cloudMcpReporter, cloudMcpSessions } from "./mcp"; | |
| 15 | +import { cloudMcpAuth } from "./mcp"; | |
| 16 | 16 | import { McpSessionDO } from "./mcp/session-durable-object"; |
| 17 | 17 | import { ErrorCaptureLive } from "./observability"; |
| 18 | 18 | import { AutumnService } from "./extensions/billing/service"; |
| @@ -89,10 +89,13 @@ const { appLayer, toWebHandler, mcpExport } = ExecutorApp.make({ · 70 unmodified lines | ||
| 89 | 89 | // Billing-as-extension #1: the usage-metering decorator (reads AutumnService). |
| 90 | 90 | decorator: CloudMeteringEngineDecorator, |
| 91 | 91 | }, |
| 92 | + // No `sessions` seam: `server.ts` intercepts `/mcp` transport for the | |
| 93 | + // hibernatable Agent bridge, so the app envelope mounts only cloud's OAuth | |
| 94 | + // discovery docs (via the `auth` seam). No `reporter` either: it only wrapped | |
| 95 | + // the envelope's `/mcp` transport route, which cloud no longer serves here | |
| 96 | + // (the Agent DO captures its own defects via Sentry in `server.ts`). | |
| 92 | 97 | mcp: { |
| 93 | 98 | auth: cloudMcpAuth, |
| 94 | − sessions: cloudMcpSessions, | |
| 95 | − reporter: cloudMcpReporter, | |
| 96 | 99 | }, |
| 97 | 100 | plugins: { provider: CloudPluginsProvider, config: CloudHostConfig }, |
| 98 | 101 | errorCapture: ErrorCaptureLive, |
| @@ -19,5 +19,7 @@ export const withExecutionUsageTracking = <E extends Cause.YieldableError>( · 18 unmodified lines | ||
| 19 | 19 | // resume doesn't count as usage |
| 20 | 20 | resume: (executionId, response) => engine.resume(executionId, response), |
| 21 | 21 | getPausedExecution: (executionId) => engine.getPausedExecution(executionId), |
| 22 | + pausedExecutionCount: () => engine.pausedExecutionCount(), | |
| 23 | + hasPausedExecutions: () => engine.hasPausedExecutions(), | |
| 22 | 24 | getDescription: engine.getDescription, |
| 23 | 25 | }); |
| @@ -0,0 +1,163 @@ | ||
| 1 | +import { Effect, Predicate } from "effect"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + McpAuthProvider, | |
| 5 | + jsonRpcErrorBody, | |
| 6 | + defaultMcpResource, | |
| 7 | + type AuthOutcome, | |
| 8 | + type McpResource, | |
| 9 | +} from "@executor-js/host-mcp"; | |
| 10 | +import { | |
| 11 | + currentPropagationHeaders, | |
| 12 | + readElicitationMode, | |
| 13 | + withVerifiedIdentityHeaders, | |
| 14 | +} from "@executor-js/cloudflare/mcp/do-headers"; | |
| 15 | +import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object"; | |
| 16 | + | |
| 17 | +import { cloudMcpAuth } from "./auth-provider"; | |
| 18 | +import { McpSessionDO } from "./session-durable-object"; | |
| 19 | + | |
| 20 | +interface McpAgentSessionStub { | |
| 21 | + readonly validateMcpSessionOwner: (identity: { | |
| 22 | + readonly accountId: string; | |
| 23 | + readonly organizationId: string; | |
| 24 | + }) => Promise<"ok" | "not_found" | "forbidden">; | |
| 25 | + readonly _cf_scheduleDestroy: () => Promise<void>; | |
| 26 | +} | |
| 27 | + | |
| 28 | +const corsPreflightResponse = (): Response => | |
| 29 | + new Response(null, { | |
| 30 | + status: 204, | |
| 31 | + headers: { | |
| 32 | + "access-control-allow-origin": "*", | |
| 33 | + "access-control-allow-methods": "GET, POST, DELETE, OPTIONS", | |
| 34 | + "access-control-allow-headers": | |
| 35 | + "content-type, authorization, mcp-session-id, accept, mcp-protocol-version", | |
| 36 | + "access-control-expose-headers": "mcp-session-id, WWW-Authenticate", | |
| 37 | + }, | |
| 38 | + }); | |
| 39 | + | |
| 40 | +const jsonRpcResponse = ( | |
| 41 | + status: number, | |
| 42 | + code: number, | |
| 43 | + message: string, | |
| 44 | + challenge?: string, | |
| 45 | +): Response => | |
| 46 | + challenge === undefined | |
| 47 | + ? jsonRpcErrorBody(status, code, message) | |
| 48 | + : jsonRpcErrorBody(status, code, message, { challenge }); | |
| 49 | + | |
| 50 | +const renderAuthError = ( | |
| 51 | + auth: McpAuthProvider["Service"], | |
| 52 | + request: Request, | |
| 53 | + outcome: Exclude<AuthOutcome, { readonly _tag: "Authenticated" }>, | |
| 54 | +): Response => { | |
| 55 | + if (Predicate.isTagged(outcome, "Unauthorized")) { | |
| 56 | + return jsonRpcResponse( | |
| 57 | + 401, | |
| 58 | + -32001, | |
| 59 | + "Unauthorized", | |
| 60 | + outcome.challenge ?? `Bearer resource_metadata="${auth.resourceMetadataUrl(request)}"`, | |
| 61 | + ); | |
| 62 | + } | |
| 63 | + if (Predicate.isTagged(outcome, "Forbidden")) { | |
| 64 | + return jsonRpcResponse(403, outcome.code ?? -32001, outcome.message); | |
| 65 | + } | |
| 66 | + return jsonRpcResponse(503, -32001, outcome.message); | |
| 67 | +}; | |
| 68 | + | |
| 69 | +const sessionStub = (env: Env, sessionId: string): McpAgentSessionStub => | |
| 70 | + // oxlint-disable-next-line executor/no-double-cast -- boundary: Workers types expose only DurableObjectStub, but RPC methods are generated from the bound DO class. | |
| 71 | + env.MCP_SESSION.get( | |
| 72 | + env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`), | |
| 73 | + ) as unknown as McpAgentSessionStub; | |
| 74 | + | |
| 75 | +const authenticate = (request: Request) => | |
| 76 | + Effect.gen(function* () { | |
| 77 | + const auth = yield* McpAuthProvider; | |
| 78 | + const outcome = yield* auth.authenticate(request); | |
| 79 | + return { auth, outcome }; | |
| 80 | + }).pipe(Effect.provide(cloudMcpAuth)); | |
| 81 | + | |
| 82 | +// The MCP resource the request targets. `server.ts` routes both the bare `/mcp` | |
| 83 | +// and `/mcp/toolkits/<slug>` to this handler (`prepareMcpOrgScope` strips the org | |
| 84 | +// selector but keeps the toolkit segment), so a session minted on a toolkit path | |
| 85 | +// scopes its tool catalog to that toolkit. | |
| 86 | +const resourceFromPath = (request: Request): McpResource => { | |
| 87 | + const segments = new URL(request.url).pathname.split("/").filter((s) => s.length > 0); | |
| 88 | + if (segments.length === 3 && segments[0] === "mcp" && segments[1] === "toolkits" && segments[2]) { | |
| 89 | + return { kind: "toolkit", slug: segments[2] }; | |
| 90 | + } | |
| 91 | + return defaultMcpResource; | |
| 92 | +}; | |
| 93 | + | |
| 94 | +const propsForPrincipal = ( | |
| 95 | + request: Request, | |
| 96 | + principal: Extract<AuthOutcome, { readonly _tag: "Authenticated" }>["principal"], | |
| 97 | + resource: McpResource, | |
| 98 | +): Effect.Effect<McpSessionProps> => | |
| 99 | + Effect.gen(function* () { | |
| 100 | + const propagation = yield* currentPropagationHeaders(request); | |
| 101 | + return { | |
| 102 | + session: { | |
| 103 | + organizationId: principal.organizationId, | |
| 104 | + userId: principal.accountId, | |
| 105 | + elicitationMode: readElicitationMode(request), | |
| 106 | + resource, | |
| 107 | + webOrigin: new URL(request.url).origin, | |
| 108 | + }, | |
| 109 | + propagation, | |
| 110 | + }; | |
| 111 | + }); | |
| 112 | + | |
| 113 | +export const makeCloudMcpAgentHandler = () => { | |
| 114 | + const serve = McpSessionDO.serve("/mcp", { | |
| 115 | + binding: "MCP_SESSION", | |
| 116 | + transport: "streamable-http", | |
| 117 | + }); | |
| 118 | + | |
| 119 | + return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => { | |
| 120 | + if (request.method === "OPTIONS") return corsPreflightResponse(); | |
| 121 | + const sessionId = request.headers.get("mcp-session-id"); | |
| 122 | + | |
| 123 | + const { auth, outcome } = await Effect.runPromise(authenticate(request)); | |
| 124 | + if (!Predicate.isTagged(outcome, "Authenticated")) { | |
| 125 | + if (Predicate.isTagged(outcome, "Forbidden") && sessionId) { | |
| 126 | + await Effect.runPromise( | |
| 127 | + Effect.ignore(Effect.tryPromise(() => sessionStub(env, sessionId)._cf_scheduleDestroy())), | |
| 128 | + ); | |
| 129 | + } | |
| 130 | + return renderAuthError(auth, request, outcome); | |
| 131 | + } | |
| 132 | + | |
| 133 | + if (!sessionId && request.method === "DELETE") { | |
| 134 | + return new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } }); | |
| 135 | + } | |
| 136 | + | |
| 137 | + if (sessionId) { | |
| 138 | + const owner = await sessionStub(env, sessionId).validateMcpSessionOwner({ | |
| 139 | + accountId: outcome.principal.accountId, | |
| 140 | + organizationId: outcome.principal.organizationId, | |
| 141 | + }); | |
| 142 | + if (owner === "not_found") { | |
| 143 | + return jsonRpcResponse(404, -32001, "Session not found"); | |
| 144 | + } | |
| 145 | + if (owner === "forbidden") { | |
| 146 | + return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer"); | |
| 147 | + } | |
| 148 | + } | |
| 149 | + | |
| 150 | + const resource = resourceFromPath(request); | |
| 151 | + const props = await Effect.runPromise(propsForPrincipal(request, outcome.principal, resource)); | |
| 152 | + (ctx as ExecutionContext & { props?: McpSessionProps }).props = props; | |
| 153 | + const forwarded = withVerifiedIdentityHeaders( | |
| 154 | + request, | |
| 155 | + { | |
| 156 | + accountId: outcome.principal.accountId, | |
| 157 | + organizationId: outcome.principal.organizationId, | |
| 158 | + }, | |
| 159 | + resource, | |
| 160 | + ); | |
| 161 | + return serve.fetch(forwarded, env, ctx); | |
| 162 | + }; | |
| 163 | +}; | |
| @@ -1,19 +1,15 @@ | ||
| 1 | 1 | // --------------------------------------------------------------------------- |
| 2 | −// Cloud MCP — the three provider seams behind the shared host-mcp envelope, | |
| 3 | −// named to match the app composition root's `mcp: { auth, sessions, reporter }`: | |
| 2 | +// Cloud MCP — the auth seam behind the shared host-mcp envelope, slotted into | |
| 3 | +// the app composition root's `mcp: { auth }`: | |
| 4 | 4 | // |
| 5 | −// - auth -> cloudMcpAuth (WorkOS JWT + API-key + org-liveness + the | |
| 6 | −// two OAuth discovery docs) | |
| 7 | −// - sessions -> cloudMcpSessions (the Durable-Object session dispatch) | |
| 8 | −// - reporter -> cloudMcpReporter (forwards request-orchestration defects to | |
| 9 | −// Sentry + the dev console) | |
| 5 | +// - auth -> cloudMcpAuth (WorkOS JWT + API-key + org-liveness + the two OAuth | |
| 6 | +// discovery docs) | |
| 10 | 7 | // |
| 11 | −// These three are what `app.ts`'s `ExecutorApp.make` slots into its `mcp` | |
| 12 | −// providers; the unified app handler serves /mcp from the app layer (like | |
| 13 | −// self-host), so start.ts no longer hand-mounts MCP. The MCP-path predicate + | |
| 14 | −// test-worker envelope builder live in `./mount` (`classifyMcpPath` / | |
| 15 | −// `makeMcpWebHandler`), imported directly there. The MCP session Durable Object | |
| 16 | −// class itself stays a platform-side export (server.ts) and imports its | |
| 8 | +// `server.ts` intercepts `/mcp` transport for the hibernatable Agent bridge, so | |
| 9 | +// the app envelope mounts only cloud's OAuth discovery docs (no `sessions` or | |
| 10 | +// `reporter` seam). The MCP-path predicate lives in `./mount` (`classifyMcpPath` | |
| 11 | +// / `prepareMcpOrgScope`), imported directly there. The MCP session Durable | |
| 12 | +// Object class itself stays a platform-side export (server.ts) and imports its | |
| 17 | 13 | // siblings directly, NOT this barrel, to keep the DO bundle react-start-free. |
| 18 | 14 | // --------------------------------------------------------------------------- |
| 19 | 15 | |
| @@ -21,5 +17,3 @@ · 1 unmodified line | ||
| 21 | 17 | // its `McpAuth`/`McpOrganizationAuth` seams provided internally), shaped as the |
| 22 | 18 | // `Layer<McpAuthProvider, never, IdentityProvider>` `ExecutorApp.make` expects. |
| 23 | 19 | export { cloudMcpAuth } from "./auth-provider"; |
| 24 | −export { cloudMcpSessionStoreLayer as cloudMcpSessions } from "./session-store"; | |
| 25 | −export { cloudMcpReporter } from "./reporter"; | |
| @@ -1,43 +1,20 @@ | ||
| 1 | 1 | // --------------------------------------------------------------------------- |
| 2 | −// Cloud MCP front — test-worker helpers for the shared, provider-neutral | |
| 3 | −// host-mcp serving envelope (@executor-js/host-mcp) behind cloud's two seams. | |
| 2 | +// Cloud MCP front — the MCP-path predicate + org-scope rewrite shared by | |
| 3 | +// `server.ts`'s request dispatch. | |
| 4 | 4 | // --------------------------------------------------------------------------- |
| 5 | 5 | // |
| 6 | −// PRODUCTION serves /mcp through `app.ts`'s unified `ExecutorApp.make` handler | |
| 7 | −// (the same `McpServingRoutes` envelope provided `cloudMcpAuth` + | |
| 8 | −// `cloudMcpSessions`), dispatched by start.ts alongside /api. This module is the | |
| 9 | −// TEST-WORKER counterpart: it exposes the two pieces `test-worker.ts` needs to | |
| 10 | −// build the identical envelope with swapped auth seams — | |
| 11 | −// - `makeMcpWebHandler` — bind `McpServingRoutes` to a web handler over a | |
| 12 | −// given auth provider + seam requirements + telemetry runtime, mirroring the | |
| 13 | −// self-host mount (`HttpRouter.toWebHandler`). | |
| 6 | +// PRODUCTION serves /mcp through `server.ts`'s hibernatable Agent bridge. | |
| 7 | +// Discovery docs flow through `app.ts`'s unified `ExecutorApp.make` handler | |
| 8 | +// (the `auth` seam's discovery routes). This module exposes: | |
| 14 | 9 | // - `classifyMcpPath` — the "is this an MCP path?" predicate (`/mcp` + the |
| 15 | −// two discovery docs) that start.ts's dispatch and the test worker share. | |
| 16 | −// | |
| 17 | −// Cloud's two envelope seams: | |
| 18 | −// - McpAuthProvider -> cloudMcpAuthProviderLayer (WorkOS JWT + API key + | |
| 19 | −// per-request org-liveness + the two OAuth discovery docs) | |
| 20 | −// - McpSessionStore -> cloudMcpSessionStoreLayer (Durable-Object dispatch) | |
| 21 | −// | |
| 22 | −// Streaming passthrough — the DO returns a `Response` whose body is a | |
| 23 | −// `ReadableStream` (SSE). The envelope wraps it with `HttpServerResponse.raw`, | |
| 24 | −// which passes the `Response` body through unchanged. | |
| 10 | +// two discovery docs, plus their org-scoped variants). | |
| 11 | +// - `prepareMcpOrgScope` — rewrite an org-scoped MCP request to the bare path | |
| 12 | +// the handler routes, carrying the URL-pinned org in an internal header. | |
| 25 | 13 | // --------------------------------------------------------------------------- |
| 26 | 14 | |
| 27 | −import { HttpRouter, HttpServer } from "effect/unstable/http"; | |
| 28 | −import { Layer } from "effect"; | |
| 29 | − | |
| 30 | 15 | import { isValidOrgSlug } from "@executor-js/api"; |
| 31 | −import { McpServingRoutes } from "@executor-js/host-mcp"; | |
| 32 | 16 | |
| 33 | −import { | |
| 34 | − McpAuth, | |
| 35 | − McpOrganizationAuth, | |
| 36 | − MCP_ORGANIZATION_HEADER, | |
| 37 | − PROTECTED_RESOURCE_METADATA_PATH, | |
| 38 | −} from "./auth"; | |
| 39 | −import { cloudMcpReporter } from "./reporter"; | |
| 40 | −import { cloudMcpSessionStoreLayer } from "./session-store"; | |
| 17 | +import { MCP_ORGANIZATION_HEADER, PROTECTED_RESOURCE_METADATA_PATH } from "./auth"; | |
| 41 | 18 | |
| 42 | 19 | const MCP_PATH = "/mcp"; |
| 43 | 20 | const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"; |
| @@ -152,47 +129,8 @@ export const prepareMcpOrgScope = (request: Request): Request => { · 108 unmodified lines | ||
| 152 | 129 | return rewritten; |
| 153 | 130 | }; |
| 154 | 131 | |
| 155 | −/** | |
| 156 | − * Build the envelope web handler from the shared `McpServingRoutes` Layer, | |
| 157 | − * provided cloud's two seams. Mirrors the self-host mount (apps/host-selfhost | |
| 158 | − * api.ts): `HttpRouter.provideRequest` clears the route handlers' per-request | |
| 159 | − * seam requirements, the build-time `Layer.provide(McpAuthProviderLive)` | |
| 160 | − * satisfies the `HttpRouter.use` callback's read of `discoveryRoutes`, and | |
| 161 | − * `HttpServer.layerServices` supplies the platform services for the web | |
| 162 | − * handler binding. | |
| 163 | − * | |
| 164 | − * `seamsRequirements` resolves the McpAuth + McpOrganizationAuth tags the | |
| 165 | − * provider reads; `runtime` (the WebSdk telemetry layer) is provided to the | |
| 166 | − * WHOLE router so every route-handler span lands on cloud's tracer — the same | |
| 167 | − * tracer the old `mcpApp` was provided. | |
| 168 | − * | |
| 169 | − * Exported so the test worker can build the same handler with test seam Layers. | |
| 170 | − */ | |
| 171 | −export const makeMcpWebHandler = <SeamsError = never>(options: { | |
| 172 | − readonly authProvider: Layer.Layer< | |
| 173 | − import("@executor-js/host-mcp").McpAuthProvider, | |
| 174 | − never, | |
| 175 | − McpAuth | McpOrganizationAuth | |
| 176 | − >; | |
| 177 | − readonly seamsRequirements: Layer.Layer<McpAuth | McpOrganizationAuth, SeamsError>; | |
| 178 | − readonly runtime: Layer.Layer<never>; | |
| 179 | −}): ((request: Request) => Promise<Response>) => { | |
| 180 | − const McpAuthProviderLive = options.authProvider.pipe(Layer.provide(options.seamsRequirements)); | |
| 181 | − const McpSeams = Layer.mergeAll(McpAuthProviderLive, cloudMcpSessionStoreLayer, cloudMcpReporter); | |
| 182 | − const McpRouteLive = McpServingRoutes.pipe( | |
| 183 | − HttpRouter.provideRequest(McpSeams), | |
| 184 | − Layer.provide(McpAuthProviderLive), | |
| 185 | − ); | |
| 186 | − return HttpRouter.toWebHandler( | |
| 187 | − McpRouteLive.pipe( | |
| 188 | − Layer.provideMerge(Layer.mergeAll(options.runtime, HttpServer.layerServices)), | |
| 189 | − ), | |
| 190 | − ).handler; | |
| 191 | −}; | |
| 192 | − | |
| 193 | −// Production no longer mounts /mcp here — `app.ts`'s unified `ExecutorApp.make` | |
| 194 | −// handler serves it (the same `McpServingRoutes` envelope + cloud seams as | |
| 195 | −// `cloudMcpAuth`/`cloudMcpSessions`), dispatched by start.ts alongside /api. | |
| 196 | −// `classifyMcpPath` + `makeMcpWebHandler` remain because the workerd/miniflare | |
| 197 | −// test worker (`test-worker.ts`) builds the same envelope with swapped auth | |
| 198 | −// seams and classifies MCP paths with the identical predicate. | |
| 132 | +// Production no longer mounts the /mcp transport here. `server.ts` intercepts MCP | |
| 133 | +// transport requests for the hibernatable Agent bridge, while `ExecutorApp.make` | |
| 134 | +// serves the OAuth discovery docs through the `auth` seam's discovery routes. | |
| 135 | +// `classifyMcpPath` + `prepareMcpOrgScope` remain because `server.ts`'s request | |
| 136 | +// dispatch uses them to recognize and normalize MCP paths. | |
| @@ -1,24 +0,0 @@ | ||
| 1 | −// --------------------------------------------------------------------------- | |
| 2 | −// Cloud MCP error reporter seam — `cloudMcpReporter`. | |
| 3 | −// | |
| 4 | −// Forwards a request-orchestration defect the shared host-mcp envelope is about | |
| 5 | −// to render as a JSON-RPC 500 to Sentry (`captureCause`) and the dev console, | |
| 6 | −// preserving the OLD `mcpApp`'s top-level | |
| 7 | −// `console.error('[mcp] request failed', …)` + `captureCause` behavior that the | |
| 8 | −// shared envelope would otherwise swallow (it returns a `Response`). | |
| 9 | −// --------------------------------------------------------------------------- | |
| 10 | − | |
| 11 | −import { Cause, Effect, Layer } from "effect"; | |
| 12 | − | |
| 13 | −import { McpErrorReporter } from "@executor-js/host-mcp"; | |
| 14 | − | |
| 15 | −import { captureCause } from "../observability"; | |
| 16 | − | |
| 17 | −export const cloudMcpReporter: Layer.Layer<McpErrorReporter> = Layer.succeed(McpErrorReporter)({ | |
| 18 | − report: (cause) => | |
| 19 | − Effect.sync(() => { | |
| 20 | − // oxlint-disable-next-line no-console -- boundary: preserve the old mcpApp top-level request-failure log | |
| 21 | − console.error("[mcp] request failed:", Cause.pretty(cause)); | |
| 22 | − captureCause(cause); | |
| 23 | − }), | |
| 24 | −}); | |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | // --------------------------------------------------------------------------- |
| 2 | 2 | // Cloud MCP Session Durable Object — the cloud binding of the shared |
| 3 | −// `McpSessionDOBase` (@executor-js/cloudflare). All session lifecycle (cold | |
| 4 | −// restore, the inactivity alarm, owner validation, transport upgrade, the | |
| 5 | −// browser-approval store, the per-request span bridge) lives in the base; cloud | |
| 3 | +// `McpAgentSessionDOBase` (@executor-js/cloudflare). Hibernatable transport | |
| 4 | +// serving, cold restore, the inactivity alarm, owner validation, browser | |
| 5 | +// approval storage, and the per-request span bridge live in the base. Cloud | |
| 6 | 6 | // supplies ONLY its injected dependencies: |
| 7 | 7 | // - openSessionDb → a long-lived postgres.js handle |
| 8 | 8 | // - resolveSessionMeta → WorkOS/UserStore organization resolution |
| @@ -24,12 +24,12 @@ import postgres, { type Sql } from "postgres"; · 15 unmodified lines | ||
| 24 | 24 | import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server"; |
| 25 | 25 | import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval"; |
| 26 | 26 | import { |
| 27 | − McpSessionDOBase, | |
| 27 | + McpAgentSessionDOBase, | |
| 28 | 28 | type BuiltMcpServer, |
| 29 | 29 | type IncomingTraceHeaders, |
| 30 | 30 | type McpSessionInit, |
| 31 | 31 | type SessionMeta, |
| 32 | −} from "@executor-js/cloudflare/mcp/durable-object"; | |
| 32 | +} from "@executor-js/cloudflare/mcp/agent-durable-object"; | |
| 33 | 33 | import { buildExecuteDescription } from "@executor-js/execution"; |
| 34 | 34 | |
| 35 | 35 | // The DO meters executions just like the HTTP `/api/*` plane: it builds its |
| @@ -67,7 +67,7 @@ export type { · 31 unmodified lines | ||
| 67 | 67 | McpSessionResumeApprovalResult, |
| 68 | 68 | McpSessionInit, |
| 69 | 69 | IncomingTraceHeaders, |
| 70 | −} from "@executor-js/cloudflare/mcp/durable-object"; | |
| 70 | +} from "@executor-js/cloudflare/mcp/agent-durable-object"; | |
| 71 | 71 | |
| 72 | 72 | // --------------------------------------------------------------------------- |
| 73 | 73 | // Cloud DB handle — one postgres.js client per session runtime |
| @@ -157,7 +157,7 @@ const makeSessionServices = (dbHandle: CloudSessionDbHandle) => { · 83 unmodified lines | ||
| 157 | 157 | // Durable Object |
| 158 | 158 | // --------------------------------------------------------------------------- |
| 159 | 159 | |
| 160 | −export class McpSessionDO extends McpSessionDOBase<CloudSessionDbHandle> { | |
| 160 | +export class McpSessionDO extends McpAgentSessionDOBase<Env, CloudSessionDbHandle> { | |
| 161 | 161 | protected override openSessionDb(): CloudSessionDbHandle { |
| 162 | 162 | return makeDbHandle({ |
| 163 | 163 | idleTimeout: LONG_LIVED_DB_IDLE_TIMEOUT_SECONDS, |
| @@ -221,6 +221,7 @@ export class McpSessionDO extends McpSessionDOBase<CloudSessionDbHandle> { · 57 unmodified lines | ||
| 221 | 221 | parentSpan: () => self.currentParentSpan(), |
| 222 | 222 | debug: env.EXECUTOR_MCP_DEBUG === "true", |
| 223 | 223 | browserApprovalStore: self.browserApprovalStore, |
| 224 | + pausedExecutionHooks: self.pausedExecutionHooks, | |
| 224 | 225 | elicitationMode: |
| 225 | 226 | sessionElicitationMode === "browser" |
| 226 | 227 | ? { |
| @@ -1,35 +0,0 @@ | ||
| 1 | −// --------------------------------------------------------------------------- | |
| 2 | −// Cloud McpSessionStore — the shared Durable-Object dispatcher | |
| 3 | −// (@executor-js/cloudflare) over cloud's `env.MCP_SESSION` namespace. Cloud | |
| 4 | −// supplies only the stub accessors + the Sentry capture for internal errors; | |
| 5 | −// all dispatch/identity/trace/peek logic is in the shared package, identical to | |
| 6 | −// host-cloudflare. | |
| 7 | −// --------------------------------------------------------------------------- | |
| 8 | − | |
| 9 | −import * as Sentry from "@sentry/cloudflare"; | |
| 10 | −import { env } from "cloudflare:workers"; | |
| 11 | −import { Data } from "effect"; | |
| 12 | − | |
| 13 | −import { | |
| 14 | − makeDurableObjectMcpSessionStore, | |
| 15 | − type McpSessionDOStub, | |
| 16 | −} from "@executor-js/cloudflare/mcp/session-store"; | |
| 17 | − | |
| 18 | −// Cloud's Sentry capture for a JSON-RPC internal (-32603) error the response | |
| 19 | −// peeker surfaces — injected into the shared store. | |
| 20 | −class McpInternalJsonRpcError extends Data.TaggedError("McpInternalJsonRpcError")<{ | |
| 21 | − readonly message: string; | |
| 22 | −}> {} | |
| 23 | − | |
| 24 | −// The DO RPC stub structurally satisfies `McpSessionDOStub` (init/handleRequest/ | |
| 25 | −// clearSession), but `@cloudflare/workers-types` types it as a generic | |
| 26 | −// `DurableObjectStub`. Narrow at this one boundary via an `unknown` hop — a | |
| 27 | −// single cast, so no double-cast through the worker-types stub type. | |
| 28 | −const toSessionStub = (stub: unknown): McpSessionDOStub => stub as McpSessionDOStub; | |
| 29 | − | |
| 30 | −export const cloudMcpSessionStoreLayer = makeDurableObjectMcpSessionStore({ | |
| 31 | − getStub: (sessionId) => | |
| 32 | − toSessionStub(env.MCP_SESSION.get(env.MCP_SESSION.idFromString(sessionId))), | |
| 33 | − newStub: () => toSessionStub(env.MCP_SESSION.get(env.MCP_SESSION.newUniqueId())), | |
| 34 | − onInternalError: (message) => Sentry.captureException(new McpInternalJsonRpcError({ message })), | |
| 35 | −}); | |
| @@ -1,196 +0,0 @@ | ||
| 1 | −import { describe, expect, it } from "@effect/vitest"; | |
| 2 | − | |
| 3 | −import { | |
| 4 | − JsonRpcRequestIdQueue, | |
| 5 | − PREVIOUS_REQUEST_TIMEOUT_MS, | |
| 6 | −} from "@executor-js/cloudflare/mcp/request-id-queue"; | |
| 7 | − | |
| 8 | −const jsonRpcRequest = (body: unknown): Request => | |
| 9 | − new Request("https://example.invalid/mcp", { | |
| 10 | − method: "POST", | |
| 11 | − headers: { "content-type": "application/json" }, | |
| 12 | − body: JSON.stringify(body), | |
| 13 | − }); | |
| 14 | − | |
| 15 | −describe("JsonRpcRequestIdQueue", () => { | |
| 16 | − it("serialises requests with the same id", async () => { | |
| 17 | − const queue = new JsonRpcRequestIdQueue(); | |
| 18 | − const order: string[] = []; | |
| 19 | − | |
| 20 | − let releaseFirst!: () => void; | |
| 21 | − const firstStarted = new Promise<void>((resolve) => { | |
| 22 | − const firstRunning = new Promise<void>((release) => { | |
| 23 | − releaseFirst = release; | |
| 24 | − }); | |
| 25 | − queue.run(jsonRpcRequest({ jsonrpc: "2.0", id: 1, method: "tools/call" }), async () => { | |
| 26 | − order.push("first:start"); | |
| 27 | − resolve(); | |
| 28 | − await firstRunning; | |
| 29 | − order.push("first:end"); | |
| 30 | − }); | |
| 31 | − }); | |
| 32 | − | |
| 33 | − await firstStarted; | |
| 34 | − | |
| 35 | − const secondDone = queue.run( | |
| 36 | − jsonRpcRequest({ jsonrpc: "2.0", id: 1, method: "tools/call" }), | |
| 37 | − async () => { | |
| 38 | − order.push("second"); | |
| 39 | − }, | |
| 40 | − ); | |
| 41 | − | |
| 42 | − // Second must wait for first. | |
| 43 | − await new Promise((r) => setTimeout(r, 20)); | |
| 44 | − expect(order).toEqual(["first:start"]); | |
| 45 | − | |
| 46 | − releaseFirst(); | |
| 47 | − await secondDone; | |
| 48 | − expect(order).toEqual(["first:start", "first:end", "second"]); | |
| 49 | − }); | |
| 50 | − | |
| 51 | − it("does not block requests with different ids", async () => { | |
| 52 | − const queue = new JsonRpcRequestIdQueue(); | |
| 53 | − let release!: () => void; | |
| 54 | − const hung = new Promise<void>((resolve) => { | |
| 55 | − release = resolve; | |
| 56 | − }); | |
| 57 | − | |
| 58 | − queue.run(jsonRpcRequest({ jsonrpc: "2.0", id: 1, method: "tools/call" }), () => hung); | |
| 59 | − | |
| 60 | − const otherDone = await Promise.race([ | |
| 61 | − queue | |
| 62 | − .run(jsonRpcRequest({ jsonrpc: "2.0", id: 2, method: "tools/call" }), async () => "done") | |
| 63 | − .then((v) => ({ kind: "settled" as const, v })), | |
| 64 | − new Promise<{ kind: "blocked" }>((r) => setTimeout(() => r({ kind: "blocked" }), 200)), | |
| 65 | − ]); | |
| 66 | − | |
| 67 | − expect(otherDone.kind).toBe("settled"); | |
| 68 | − release(); | |
| 69 | − }); | |
| 70 | − | |
| 71 | − it("does not let a waiting resume block an unrelated same-id tool call", async () => { | |
| 72 | − const queue = new JsonRpcRequestIdQueue(); | |
| 73 | − let releaseResume!: () => void; | |
| 74 | − const hungResume = new Promise<void>((resolve) => { | |
| 75 | − releaseResume = resolve; | |
| 76 | − }); | |
| 77 | − | |
| 78 | − const resumeStarted = new Promise<void>((resolve) => { | |
| 79 | − queue.run( | |
| 80 | − jsonRpcRequest({ | |
| 81 | − jsonrpc: "2.0", | |
| 82 | − id: 1, | |
| 83 | − method: "tools/call", | |
| 84 | − params: { name: "resume", arguments: { executionId: "exec_1" } }, | |
| 85 | − }), | |
| 86 | − async () => { | |
| 87 | − resolve(); | |
| 88 | − await hungResume; | |
| 89 | − }, | |
| 90 | − ); | |
| 91 | − }); | |
| 92 | − await resumeStarted; | |
| 93 | − | |
| 94 | − const executeDone = await Promise.race([ | |
| 95 | − queue | |
| 96 | − .run( | |
| 97 | − jsonRpcRequest({ | |
| 98 | − jsonrpc: "2.0", | |
| 99 | − id: 1, | |
| 100 | − method: "tools/call", | |
| 101 | − params: { name: "execute", arguments: { code: "return 1" } }, | |
| 102 | − }), | |
| 103 | − async () => "done", | |
| 104 | − ) | |
| 105 | − .then((v) => ({ kind: "settled" as const, v })), | |
| 106 | − new Promise<{ kind: "blocked" }>((r) => setTimeout(() => r({ kind: "blocked" }), 200)), | |
| 107 | − ]); | |
| 108 | − | |
| 109 | − expect(executeDone.kind).toBe("settled"); | |
| 110 | − releaseResume(); | |
| 111 | − }); | |
| 112 | − | |
| 113 | − it("serialises same-id resume calls for the same execution", async () => { | |
| 114 | − const queue = new JsonRpcRequestIdQueue(); | |
| 115 | − const order: string[] = []; | |
| 116 | − | |
| 117 | − let releaseFirst!: () => void; | |
| 118 | − const firstStarted = new Promise<void>((resolve) => { | |
| 119 | − const firstRunning = new Promise<void>((release) => { | |
| 120 | − releaseFirst = release; | |
| 121 | − }); | |
| 122 | − queue.run( | |
| 123 | − jsonRpcRequest({ | |
| 124 | − jsonrpc: "2.0", | |
| 125 | − id: 1, | |
| 126 | − method: "tools/call", | |
| 127 | − params: { name: "resume", arguments: { executionId: "exec_1" } }, | |
| 128 | − }), | |
| 129 | − async () => { | |
| 130 | − order.push("first:start"); | |
| 131 | − resolve(); | |
| 132 | − await firstRunning; | |
| 133 | − order.push("first:end"); | |
| 134 | − }, | |
| 135 | − ); | |
| 136 | − }); | |
| 137 | − await firstStarted; | |
| 138 | − | |
| 139 | − const secondDone = queue.run( | |
| 140 | − jsonRpcRequest({ | |
| 141 | − jsonrpc: "2.0", | |
| 142 | − id: 1, | |
| 143 | − method: "tools/call", | |
| 144 | − params: { name: "resume", arguments: { executionId: "exec_1" } }, | |
| 145 | − }), | |
| 146 | − async () => { | |
| 147 | − order.push("second"); | |
| 148 | − }, | |
| 149 | − ); | |
| 150 | − | |
| 151 | − await new Promise((r) => setTimeout(r, 20)); | |
| 152 | − expect(order).toEqual(["first:start"]); | |
| 153 | − | |
| 154 | − releaseFirst(); | |
| 155 | − await secondDone; | |
| 156 | − expect(order).toEqual(["first:start", "first:end", "second"]); | |
| 157 | − }); | |
| 158 | − | |
| 159 | − it("regression: caps wait on a hung previous request and dispatches anyway", async () => { | |
| 160 | − // Override the timeout for fast CI — the production default is | |
| 161 | − // PREVIOUS_REQUEST_TIMEOUT_MS (60s) which we cap test-side to 100ms. | |
| 162 | − // Same behaviour, same code path; only the wall-clock budget changes. | |
| 163 | − const queue = new JsonRpcRequestIdQueue({ previousTimeoutMs: 100 }); | |
| 164 | − const order: string[] = []; | |
| 165 | − | |
| 166 | − // Kick off a request and never release it — the poisoned-queue | |
| 167 | − // shape that used to cascade for the full upstream 180s timeout. | |
| 168 | − const firstStarted = new Promise<void>((resolve) => { | |
| 169 | − queue.run(jsonRpcRequest({ jsonrpc: "2.0", id: 1, method: "tools/call" }), async () => { | |
| 170 | − order.push("first:start"); | |
| 171 | − resolve(); | |
| 172 | − await new Promise(() => undefined); // hang forever | |
| 173 | − }); | |
| 174 | − }); | |
| 175 | − await firstStarted; | |
| 176 | − | |
| 177 | − const result = await queue.run( | |
| 178 | − jsonRpcRequest({ jsonrpc: "2.0", id: 1, method: "tools/call" }), | |
| 179 | − async () => { | |
| 180 | − order.push("second"); | |
| 181 | − return "ok"; | |
| 182 | − }, | |
| 183 | − ); | |
| 184 | − | |
| 185 | − expect(result).toBe("ok"); | |
| 186 | − expect(order).toEqual(["first:start", "second"]); | |
| 187 | − }); | |
| 188 | − | |
| 189 | − it("exposes a sane production timeout", () => { | |
| 190 | − // Sanity guard: must stay below the 180s upstream timeout that | |
| 191 | − // Claude / Cowork enforce, but be long enough to outlast a normal | |
| 192 | − // dynamic-worker execution under load. | |
| 193 | − expect(PREVIOUS_REQUEST_TIMEOUT_MS).toBeGreaterThan(10_000); | |
| 194 | − expect(PREVIOUS_REQUEST_TIMEOUT_MS).toBeLessThan(180_000); | |
| 195 | − }); | |
| 196 | −}); | |
| @@ -10,6 +10,8 @@ import * as Sentry from "@sentry/cloudflare"; · 9 unmodified lines | ||
| 10 | 10 | import handler from "@tanstack/react-start/server-entry"; |
| 11 | 11 | |
| 12 | 12 | import { isAppOwnedPath } from "./app-paths"; |
| 13 | +import { makeCloudMcpAgentHandler } from "./mcp/agent-handler"; | |
| 14 | +import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount"; | |
| 13 | 15 | import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object"; |
| 14 | 16 | import { browserTracesResponse } from "./observability/browser-traces"; |
| 15 | 17 | import { flushTracerProvider, installTracerProvider } from "./observability/telemetry"; |
| @@ -77,6 +79,7 @@ const fetchHandler = handler.fetch as ( · 61 unmodified lines | ||
| 77 | 79 | ) => Response | Promise<Response>; |
| 78 | 80 | |
| 79 | 81 | const tracer = trace.getTracer("executor-cloud-worker"); |
| 82 | +const mcpAgentHandler = makeCloudMcpAgentHandler(); | |
| 80 | 83 | |
| 81 | 84 | const cloudflareHandler: ExportedHandler<Env> = { |
| 82 | 85 | fetch: async (request, env, ctx) => { |
| @@ -89,6 +92,18 @@ const cloudflareHandler: ExportedHandler<Env> = { · 6 unmodified lines | ||
| 89 | 92 | return fetchHandler(request, env, ctx); |
| 90 | 93 | } |
| 91 | 94 | const url = new URL(request.url); |
| 95 | + const mcpRoute = classifyMcpPath(url.pathname); | |
| 96 | + if (mcpRoute?.kind === "mcp") { | |
| 97 | + // The Cloudflare Agents MCP bridge needs the platform ExecutionContext | |
| 98 | + // to pass authenticated session props into the hibernatable DO. | |
| 99 | + // Discovery docs still flow through the app-level MCP envelope. | |
| 100 | + // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; keep trace export alive after the Agents bridge resolves or rejects | |
| 101 | + try { | |
| 102 | + return await mcpAgentHandler(prepareMcpOrgScope(request), env, ctx); | |
| 103 | + } finally { | |
| 104 | + ctx.waitUntil(flushTracerProvider()); | |
| 105 | + } | |
| 106 | + } | |
| 92 | 107 | // Effect-served paths bring their own http.server span (with traceparent |
| 93 | 108 | // join) — opening one here too would duplicate it. See the header note. |
| 94 | 109 | if (isAppOwnedPath(url.pathname)) { |
| @@ -14,15 +14,16 @@ import { · 13 unmodified lines | ||
| 14 | 14 | } from "./execution"; |
| 15 | 15 | import { ErrorCaptureLive } from "./observability"; |
| 16 | 16 | import { cloudflareAccountMiddleware } from "./account/account-provider"; |
| 17 | −import { makeCloudflareMcpSeams } from "./mcp"; | |
| 17 | +import { makeCloudflareApprovalHandler } from "./mcp"; | |
| 18 | +import { makeCloudflareMcpAgentHandler } from "./mcp/agent-handler"; | |
| 18 | 19 | import { preloadQuickJs } from "./quickjs"; |
| 19 | 20 | |
| 20 | 21 | // =========================================================================== |
| 21 | −// The Cloudflare host, as ONE `ExecutorApp.make` call — the 4th app alongside | |
| 22 | +// The Cloudflare host, as ONE `ExecutorApp.make` call: the 4th app alongside | |
| 22 | 23 | // cloud / self-host / local, differing only by the injected Layers. |
| 23 | 24 | // |
| 24 | 25 | // The whole scenario in 60 seconds: Cloudflare Access is the identity (validate |
| 25 | −// the Cf-Access-Jwt-Assertion JWT — no Better Auth, no WorkOS, no app login), | |
| 26 | +// the Cf-Access-Jwt-Assertion JWT, no Better Auth, no WorkOS, no app login), | |
| 26 | 27 | // D1 is the SQLite store (same FumaDB assembly as self-host), QuickJS is the |
| 27 | 28 | // in-process code substrate, no billing, single-tenant. `diff` against |
| 28 | 29 | // host-selfhost/src/app.ts is three injected Layers: identity, db, plugins/config. |
| @@ -37,23 +38,22 @@ export const makeCloudflareApp = async (env: CloudflareEnv) => { · 8 unmodified lines | ||
| 37 | 38 | const plugins = makeCloudflarePlugins(config.secretKey); |
| 38 | 39 | |
| 39 | 40 | // Load the Workers-compatible (WASM-inlined) QuickJS variant before any |
| 40 | − // executor is built — the default variant can't fetch its .wasm on Workers. | |
| 41 | + // executor is built, the default variant cannot fetch its .wasm on Workers. | |
| 41 | 42 | await preloadQuickJs(); |
| 42 | 43 | |
| 43 | − // Open + idempotently bring up the D1 schema once (the long-lived handle the | |
| 44 | − // per-request scoped executor reads through the DbProvider seam). | |
| 44 | + // Open and idempotently bring up the D1 schema once. This is the long-lived | |
| 45 | + // handle the per-request scoped executor reads through the DbProvider seam. | |
| 45 | 46 | const dbHandle = await createD1ExecutorDb(env.DB, env.BLOBS); |
| 46 | 47 | const identityLayer = cloudflareAccessIdentityLayer(config); |
| 47 | − // MCP runs through the `MCP_SESSION` Durable Object (cross-isolate sessions); | |
| 48 | − // each session DO opens its own D1 handle, so it takes `env`, not `dbHandle`. | |
| 49 | − const mcp = makeCloudflareMcpSeams(config, env); | |
| 48 | + const mcpAgentHandler = makeCloudflareMcpAgentHandler(config); | |
| 49 | + const approvalHandler = makeCloudflareApprovalHandler(config, env); | |
| 50 | 50 | |
| 51 | 51 | const { appLayer, toWebHandler } = ExecutorApp.make({ |
| 52 | 52 | plugins, |
| 53 | 53 | providers: { |
| 54 | 54 | identity: identityLayer, |
| 55 | 55 | db: dbProviderLayer(Effect.succeed(dbHandle)), |
| 56 | − engine: { codeExecutor: CloudflareCodeExecutorProvider }, // decorator defaults to no-op | |
| 56 | + engine: { codeExecutor: CloudflareCodeExecutorProvider }, | |
| 57 | 57 | plugins: { |
| 58 | 58 | provider: makeCloudflarePluginsProvider(config), |
| 59 | 59 | config: makeCloudflareHostConfig(config), |
| @@ -63,21 +63,18 @@ export const makeCloudflareApp = async (env: CloudflareEnv) => { · 3 unmodified lines | ||
| 63 | 63 | // auth context; `me` reflects the Access principal. Members/keys are |
| 64 | 64 | // Access-managed, so the rest of the surface is stubbed. |
| 65 | 65 | account: cloudflareAccountMiddleware(config), |
| 66 | − // The MCP serving envelope: Access-JWT auth + the shared in-process session | |
| 67 | − // store over the QuickJS engine. | |
| 68 | − mcp: { auth: mcp.auth, sessions: mcp.sessions, reporter: mcp.reporter }, | |
| 69 | 66 | }, |
| 70 | 67 | extensions: { |
| 71 | 68 | routes: [ |
| 72 | 69 | // Browser approval of paused MCP executions: the console resume page |
| 73 | 70 | // reads paused detail (GET) and records the decision (POST .../resume), |
| 74 | 71 | // Access-gated, routed to the owning session's Durable Object. |
| 75 | − HttpRouter.add("*", "/api/mcp-sessions/*", HttpEffect.fromWebHandler(mcp.approvalHandler)), | |
| 72 | + HttpRouter.add("*", "/api/mcp-sessions/*", HttpEffect.fromWebHandler(approvalHandler)), | |
| 76 | 73 | ], |
| 77 | 74 | }, |
| 78 | 75 | config: { mountPrefix: "/api", failure: textFailureStrategy }, |
| 79 | 76 | boot: identityLayer, |
| 80 | 77 | }); |
| 81 | 78 | |
| 82 | − return { appLayer, toWebHandler }; | |
| 79 | + return { appLayer, toWebHandler, mcpAgentHandler }; | |
| 83 | 80 | }; |
| @@ -0,0 +1,153 @@ | ||
| 1 | +import { Effect, Predicate } from "effect"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + McpAuthProvider, | |
| 5 | + jsonRpcErrorBody, | |
| 6 | + defaultMcpResource, | |
| 7 | + type AuthOutcome, | |
| 8 | + type Principal, | |
| 9 | +} from "@executor-js/host-mcp"; | |
| 10 | +import { | |
| 11 | + currentPropagationHeaders, | |
| 12 | + readElicitationMode, | |
| 13 | + withVerifiedIdentityHeaders, | |
| 14 | +} from "@executor-js/cloudflare/mcp/do-headers"; | |
| 15 | +import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object"; | |
| 16 | + | |
| 17 | +import type { CloudflareConfig, CloudflareEnv } from "../config"; | |
| 18 | +import { cloudflareAccessMcpAuth } from "./auth"; | |
| 19 | +import { McpSessionDO } from "./session-durable-object"; | |
| 20 | + | |
| 21 | +interface McpAgentSessionStub { | |
| 22 | + readonly validateMcpSessionOwner: (identity: { | |
| 23 | + readonly accountId: string; | |
| 24 | + readonly organizationId: string; | |
| 25 | + }) => Promise<"ok" | "not_found" | "forbidden">; | |
| 26 | + readonly _cf_scheduleDestroy: () => Promise<void>; | |
| 27 | +} | |
| 28 | + | |
| 29 | +const corsPreflightResponse = (): Response => | |
| 30 | + new Response(null, { | |
| 31 | + status: 204, | |
| 32 | + headers: { | |
| 33 | + "access-control-allow-origin": "*", | |
| 34 | + "access-control-allow-methods": "GET, POST, DELETE, OPTIONS", | |
| 35 | + "access-control-allow-headers": | |
| 36 | + "content-type, authorization, mcp-session-id, accept, mcp-protocol-version", | |
| 37 | + "access-control-expose-headers": "mcp-session-id, WWW-Authenticate", | |
| 38 | + }, | |
| 39 | + }); | |
| 40 | + | |
| 41 | +const jsonRpcResponse = ( | |
| 42 | + status: number, | |
| 43 | + code: number, | |
| 44 | + message: string, | |
| 45 | + challenge?: string, | |
| 46 | +): Response => | |
| 47 | + challenge === undefined | |
| 48 | + ? jsonRpcErrorBody(status, code, message) | |
| 49 | + : jsonRpcErrorBody(status, code, message, { challenge }); | |
| 50 | + | |
| 51 | +const renderAuthError = ( | |
| 52 | + auth: McpAuthProvider["Service"], | |
| 53 | + request: Request, | |
| 54 | + outcome: Exclude<AuthOutcome, { readonly _tag: "Authenticated" }>, | |
| 55 | +): Response => { | |
| 56 | + if (Predicate.isTagged(outcome, "Unauthorized")) { | |
| 57 | + return jsonRpcResponse( | |
| 58 | + 401, | |
| 59 | + -32001, | |
| 60 | + "Unauthorized", | |
| 61 | + outcome.challenge ?? `Bearer resource_metadata="${auth.resourceMetadataUrl(request)}"`, | |
| 62 | + ); | |
| 63 | + } | |
| 64 | + if (Predicate.isTagged(outcome, "Forbidden")) { | |
| 65 | + return jsonRpcResponse(403, outcome.code ?? -32001, outcome.message); | |
| 66 | + } | |
| 67 | + return jsonRpcResponse(503, -32001, outcome.message); | |
| 68 | +}; | |
| 69 | + | |
| 70 | +const sessionStub = (env: CloudflareEnv, sessionId: string): McpAgentSessionStub => | |
| 71 | + // oxlint-disable-next-line executor/no-double-cast -- boundary: Workers types expose only DurableObjectStub, but RPC methods are generated from the bound DO class. | |
| 72 | + env.MCP_SESSION.get( | |
| 73 | + env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`), | |
| 74 | + ) as unknown as McpAgentSessionStub; | |
| 75 | + | |
| 76 | +const authenticate = (request: Request, config: CloudflareConfig) => | |
| 77 | + Effect.gen(function* () { | |
| 78 | + const auth = yield* McpAuthProvider; | |
| 79 | + const outcome = yield* auth.authenticate(request); | |
| 80 | + return { auth, outcome }; | |
| 81 | + }).pipe(Effect.provide(cloudflareAccessMcpAuth(config))); | |
| 82 | + | |
| 83 | +const propsForPrincipal = ( | |
| 84 | + request: Request, | |
| 85 | + principal: Principal, | |
| 86 | +): Effect.Effect<McpSessionProps> => | |
| 87 | + Effect.gen(function* () { | |
| 88 | + const propagation = yield* currentPropagationHeaders(request); | |
| 89 | + return { | |
| 90 | + session: { | |
| 91 | + organizationId: principal.organizationId, | |
| 92 | + userId: principal.accountId, | |
| 93 | + elicitationMode: readElicitationMode(request), | |
| 94 | + // host-cloudflare only routes the bare `/mcp` endpoint to the Agent | |
| 95 | + // bridge (see worker.ts), so the session always serves the default | |
| 96 | + // resource. | |
| 97 | + resource: defaultMcpResource, | |
| 98 | + webOrigin: new URL(request.url).origin, | |
| 99 | + }, | |
| 100 | + propagation, | |
| 101 | + }; | |
| 102 | + }); | |
| 103 | + | |
| 104 | +export const makeCloudflareMcpAgentHandler = (config: CloudflareConfig) => { | |
| 105 | + const serve = McpSessionDO.serve("/mcp", { | |
| 106 | + binding: "MCP_SESSION", | |
| 107 | + transport: "streamable-http", | |
| 108 | + }); | |
| 109 | + | |
| 110 | + return async (request: Request, env: CloudflareEnv, ctx: ExecutionContext): Promise<Response> => { | |
| 111 | + if (request.method === "OPTIONS") return corsPreflightResponse(); | |
| 112 | + const sessionId = request.headers.get("mcp-session-id"); | |
| 113 | + | |
| 114 | + const { auth, outcome } = await Effect.runPromise(authenticate(request, config)); | |
| 115 | + if (!Predicate.isTagged(outcome, "Authenticated")) { | |
| 116 | + if (Predicate.isTagged(outcome, "Forbidden") && sessionId) { | |
| 117 | + await Effect.runPromise( | |
| 118 | + Effect.ignore(Effect.tryPromise(() => sessionStub(env, sessionId)._cf_scheduleDestroy())), | |
| 119 | + ); | |
| 120 | + } | |
| 121 | + return renderAuthError(auth, request, outcome); | |
| 122 | + } | |
| 123 | + | |
| 124 | + if (!sessionId && request.method === "DELETE") { | |
| 125 | + return new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } }); | |
| 126 | + } | |
| 127 | + | |
| 128 | + if (sessionId) { | |
| 129 | + const owner = await sessionStub(env, sessionId).validateMcpSessionOwner({ | |
| 130 | + accountId: outcome.principal.accountId, | |
| 131 | + organizationId: outcome.principal.organizationId, | |
| 132 | + }); | |
| 133 | + if (owner === "not_found") { | |
| 134 | + return jsonRpcResponse(404, -32001, "Session not found"); | |
| 135 | + } | |
| 136 | + if (owner === "forbidden") { | |
| 137 | + return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer"); | |
| 138 | + } | |
| 139 | + } | |
| 140 | + | |
| 141 | + const props = await Effect.runPromise(propsForPrincipal(request, outcome.principal)); | |
| 142 | + (ctx as ExecutionContext & { props?: McpSessionProps }).props = props; | |
| 143 | + const forwarded = withVerifiedIdentityHeaders( | |
| 144 | + request, | |
| 145 | + { | |
| 146 | + accountId: outcome.principal.accountId, | |
| 147 | + organizationId: outcome.principal.organizationId, | |
| 148 | + }, | |
| 149 | + defaultMcpResource, | |
| 150 | + ); | |
| 151 | + return serve.fetch(forwarded, env, ctx); | |
| 152 | + }; | |
| 153 | +}; | |
| @@ -1,58 +1,19 @@ | ||
| 1 | −import { Effect, type Layer } from "effect"; | |
| 1 | +import { Effect } from "effect"; | |
| 2 | 2 | |
| 3 | −import type { McpAuthProvider, McpErrorReporter, McpSessionStore } from "@executor-js/host-mcp"; | |
| 4 | 3 | import { decodeResumeResponse } from "@executor-js/host-mcp/browser-approval"; |
| 5 | 4 | import type { |
| 6 | 5 | McpApprovalOwner, |
| 7 | 6 | McpSessionApprovalResult, |
| 8 | 7 | McpSessionResumeApprovalResult, |
| 9 | −} from "@executor-js/cloudflare/mcp/durable-object"; | |
| 8 | +} from "@executor-js/cloudflare/mcp/agent-durable-object"; | |
| 10 | 9 | import type { ResumeResponse } from "@executor-js/execution"; |
| 11 | 10 | |
| 12 | 11 | import type { CloudflareConfig, CloudflareEnv } from "../config"; |
| 13 | 12 | import { makeAccessVerifier } from "../auth/cloudflare-access"; |
| 14 | −import { cloudflareAccessMcpAuth } from "./auth"; | |
| 15 | −import { cloudflareMcpReporter, makeCloudflareMcpSessionStore } from "./session-store"; | |
| 16 | 13 | |
| 17 | 14 | export { cloudflareAccessMcpAuth } from "./auth"; |
| 18 | −export { cloudflareMcpReporter, makeCloudflareMcpSessionStore } from "./session-store"; | |
| 19 | 15 | export { McpSessionDO } from "./session-durable-object"; |
| 20 | 16 | |
| 21 | −// --------------------------------------------------------------------------- | |
| 22 | −// The Cloudflare MCP serving seams, fed to `ExecutorApp.make`'s `mcp` group. | |
| 23 | −// | |
| 24 | −// `ExecutorApp.make` mounts the shared, provider-neutral MCP serving envelope | |
| 25 | −// (@executor-js/host-mcp) at the top-level `/mcp`, outside the API's execution | |
| 26 | −// middleware. The Cloudflare host provides the two envelope seams plus the | |
| 27 | −// error-reporter override: | |
| 28 | −// - McpAuthProvider -> `cloudflareAccessMcpAuth`: validate the Access JWT | |
| 29 | −// (same identity as the API gate); no MCP OAuth. | |
| 30 | −// - McpSessionStore -> the shared Durable-Object dispatcher over the host's | |
| 31 | −// `MCP_SESSION` namespace (cross-isolate, same as cloud). | |
| 32 | −// - McpErrorReporter -> `cloudflareMcpReporter`: route 500 defects through the | |
| 33 | −// host's console capture. | |
| 34 | −// --------------------------------------------------------------------------- | |
| 35 | − | |
| 36 | −export interface CloudflareMcpSeams { | |
| 37 | − /** Validate the Access JWT to an MCP `AuthOutcome`; declares no discovery routes. */ | |
| 38 | − readonly auth: Layer.Layer<McpAuthProvider>; | |
| 39 | − /** The Durable-Object session store seam (dispatch + lifetime). */ | |
| 40 | − readonly sessions: Layer.Layer<McpSessionStore>; | |
| 41 | − /** Route 500 defects through the host's console `ErrorCapture`. */ | |
| 42 | − readonly reporter: Layer.Layer<McpErrorReporter>; | |
| 43 | − /** | |
| 44 | − * The browser-approval HTTP handler, mounted by the app at | |
| 45 | − * `/api/mcp-sessions/*`: an Access-gated web handler that reads paused-execution | |
| 46 | − * detail (GET) and records the human's decision (POST `/resume`) for the console | |
| 47 | − * resume page, routing each to the owning session's Durable Object RPCs. | |
| 48 | − */ | |
| 49 | − readonly approvalHandler: (request: Request) => Promise<Response>; | |
| 50 | −} | |
| 51 | − | |
| 52 | −// The MCP session Durable Object exposes the approval RPCs (the base class | |
| 53 | −// implements them); `@cloudflare/workers-types` types the stub generically, so | |
| 54 | −// narrow at this one boundary via a single `unknown`-param hop (same shape the | |
| 55 | −// session-store seam uses for the dispatch stub). | |
| 56 | 17 | const toApprovalStub = (stub: unknown): McpApprovalStub => stub as McpApprovalStub; |
| 57 | 18 | |
| 58 | 19 | interface McpApprovalStub { |
| @@ -73,19 +34,13 @@ const RESUME_PATH = /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)\/res · 14 unmodified lines | ||
| 73 | 34 | const jsonResponse = (value: unknown, status: number): Response => |
| 74 | 35 | new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }); |
| 75 | 36 | |
| 76 | −/** | |
| 77 | − * Resolve the request to its Access principal (dev-auth → the fixed dev admin), | |
| 78 | − * then route the browser-approval call to the owning session's Durable Object — | |
| 79 | − * the same RPCs cloud serves through its HttpApi. The DO validates that the | |
| 80 | − * principal owns the session before reading or resuming. | |
| 81 | − */ | |
| 82 | −const makeCloudflareApprovalHandler = ( | |
| 37 | +export const makeCloudflareApprovalHandler = ( | |
| 83 | 38 | config: CloudflareConfig, |
| 84 | 39 | env: CloudflareEnv, |
| 85 | 40 | ): ((request: Request) => Promise<Response>) => { |
| 86 | 41 | const { verify } = makeAccessVerifier(config); |
| 87 | 42 | const stubFor = (sessionId: string): McpApprovalStub => |
| 88 | − toApprovalStub(env.MCP_SESSION.get(env.MCP_SESSION.idFromString(sessionId))); | |
| 43 | + toApprovalStub(env.MCP_SESSION.get(env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`))); | |
| 89 | 44 | |
| 90 | 45 | return async (request) => { |
| 91 | 46 | const principal = await Effect.runPromise(verify(request)); |
| @@ -136,18 +91,3 @@ const makeCloudflareApprovalHandler = ( · 44 unmodified lines | ||
| 136 | 91 | return jsonResponse({ error: "Not found" }, 404); |
| 137 | 92 | }; |
| 138 | 93 | }; |
| 139 | − | |
| 140 | −/** | |
| 141 | − * Build the Cloudflare MCP serving seams over the host's `MCP_SESSION` Durable | |
| 142 | − * Object namespace. No per-session DB handle is threaded here — each session DO | |
| 143 | − * opens its own D1 handle in its own isolate. | |
| 144 | − */ | |
| 145 | −export const makeCloudflareMcpSeams = ( | |
| 146 | − config: CloudflareConfig, | |
| 147 | − env: CloudflareEnv, | |
| 148 | −): CloudflareMcpSeams => ({ | |
| 149 | − auth: cloudflareAccessMcpAuth(config), | |
| 150 | − sessions: makeCloudflareMcpSessionStore(env), | |
| 151 | − reporter: cloudflareMcpReporter, | |
| 152 | − approvalHandler: makeCloudflareApprovalHandler(config, env), | |
| 153 | −}); | |
| @@ -4,11 +4,11 @@ import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server"; · 3 unmodified lines | ||
| 4 | 4 | import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval"; |
| 5 | 5 | import type { ExecutorDbHandle } from "@executor-js/api/server"; |
| 6 | 6 | import { |
| 7 | − McpSessionDOBase, | |
| 7 | + McpAgentSessionDOBase, | |
| 8 | 8 | type BuiltMcpServer, |
| 9 | 9 | type McpSessionInit, |
| 10 | 10 | type SessionMeta, |
| 11 | −} from "@executor-js/cloudflare/mcp/durable-object"; | |
| 11 | +} from "@executor-js/cloudflare/mcp/agent-durable-object"; | |
| 12 | 12 | |
| 13 | 13 | import { loadConfig, type CloudflareConfig, type CloudflareEnv } from "../config"; |
| 14 | 14 | import { createD1ExecutorDb } from "../db/d1"; |
| @@ -17,7 +17,7 @@ import { preloadQuickJs } from "../quickjs"; · 2 unmodified lines | ||
| 17 | 17 | |
| 18 | 18 | // --------------------------------------------------------------------------- |
| 19 | 19 | // Cloudflare (self-host) MCP Session Durable Object — the host-cloudflare |
| 20 | −// binding of the shared `McpSessionDOBase` (@executor-js/cloudflare). Identical | |
| 20 | +// binding of the shared `McpAgentSessionDOBase` (@executor-js/cloudflare). Identical | |
| 21 | 21 | // base to cloud; the ONLY differences are the injected dependencies: |
| 22 | 22 | // - openSessionDb → a long-lived D1 `ExecutorDbHandle` (same FumaDB |
| 23 | 23 | // assembly the HTTP path uses), adapted to the base's |
| @@ -35,14 +35,14 @@ import { preloadQuickJs } from "../quickjs"; · 11 unmodified lines | ||
| 35 | 35 | // own lifecycle (the binding is the connection), so `end` is `close` — a no-op. |
| 36 | 36 | type CfSessionDbHandle = ExecutorDbHandle & { readonly end: () => Promise<void> }; |
| 37 | 37 | |
| 38 | −export class McpSessionDO extends McpSessionDOBase<CfSessionDbHandle> { | |
| 38 | +export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle> { | |
| 39 | 39 | private readonly cfEnv: CloudflareEnv; |
| 40 | 40 | private readonly cfConfig: CloudflareConfig; |
| 41 | 41 | |
| 42 | − // `ctx`'s type is taken from the base constructor so it tracks whichever | |
| 43 | − // `@cloudflare/workers-types` the shared package resolves (avoids a | |
| 44 | − // cross-version `DurableObjectState` mismatch at the `super` call). | |
| 45 | − constructor(ctx: ConstructorParameters<typeof McpSessionDOBase>[0], env: CloudflareEnv) { | |
| 42 | + constructor( | |
| 43 | + ctx: ConstructorParameters<typeof McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle>>[0], | |
| 44 | + env: CloudflareEnv, | |
| 45 | + ) { | |
| 46 | 46 | super(ctx, env); |
| 47 | 47 | this.cfEnv = env; |
| 48 | 48 | this.cfConfig = loadConfig(env); |
| @@ -90,6 +90,7 @@ export class McpSessionDO extends McpSessionDOBase<CfSessionDbHandle> { · 41 unmodified lines | ||
| 90 | 90 | const mcpServer = yield* createExecutorMcpServer({ |
| 91 | 91 | engine, |
| 92 | 92 | browserApprovalStore: self.browserApprovalStore, |
| 93 | + pausedExecutionHooks: self.pausedExecutionHooks, | |
| 93 | 94 | elicitationMode: |
| 94 | 95 | elicitationMode === "browser" |
| 95 | 96 | ? { |
| @@ -1,43 +0,0 @@ | ||
| 1 | −import { Layer } from "effect"; | |
| 2 | − | |
| 3 | −import { makeConsoleMcpErrorReporter } from "@executor-js/api/server"; | |
| 4 | −import type { McpErrorReporter, McpSessionStore } from "@executor-js/host-mcp"; | |
| 5 | −import { | |
| 6 | − makeDurableObjectMcpSessionStore, | |
| 7 | − type McpSessionDOStub, | |
| 8 | −} from "@executor-js/cloudflare/mcp/session-store"; | |
| 9 | − | |
| 10 | −import type { CloudflareEnv } from "../config"; | |
| 11 | −import { ErrorCaptureLive } from "../observability"; | |
| 12 | − | |
| 13 | −// --------------------------------------------------------------------------- | |
| 14 | −// Cloudflare McpSessionStore wiring — the SAME shared Durable-Object dispatcher | |
| 15 | −// as cloud (@executor-js/cloudflare), over host-cloudflare's `MCP_SESSION` | |
| 16 | −// namespace. The dispatch/identity/trace/peek logic all lives in the shared | |
| 17 | −// package; the host supplies ONLY its DO stub accessors (the session id IS the | |
| 18 | −// DO id, so every follow-up request routes back to the same isolate). | |
| 19 | −// | |
| 20 | −// This replaces the in-process store: an in-memory session map is invisible to | |
| 21 | −// the next Worker isolate, so `tools/list` after `initialize` failed in | |
| 22 | −// production ("Not connected"). The DO holds the session in one addressable | |
| 23 | −// isolate, fixing that across the board. | |
| 24 | −// --------------------------------------------------------------------------- | |
| 25 | − | |
| 26 | −// The DO RPC stub structurally satisfies `McpSessionDOStub` (init/handleRequest/ | |
| 27 | −// clearSession), but `@cloudflare/workers-types` types it as a generic | |
| 28 | −// `DurableObjectStub`. Narrow at this one boundary via an `unknown` hop — a | |
| 29 | −// single cast, so no double-cast through the worker-types stub type. | |
| 30 | −const toSessionStub = (stub: unknown): McpSessionDOStub => stub as McpSessionDOStub; | |
| 31 | − | |
| 32 | −/** Build the DO-backed MCP session store over the host's `MCP_SESSION` namespace. */ | |
| 33 | −export const makeCloudflareMcpSessionStore = (env: CloudflareEnv): Layer.Layer<McpSessionStore> => | |
| 34 | − makeDurableObjectMcpSessionStore({ | |
| 35 | − getStub: (sessionId) => | |
| 36 | − toSessionStub(env.MCP_SESSION.get(env.MCP_SESSION.idFromString(sessionId))), | |
| 37 | − newStub: () => toSessionStub(env.MCP_SESSION.get(env.MCP_SESSION.newUniqueId())), | |
| 38 | − // host-cf has no Sentry; a 500-defect surfaces through the reporter seam below. | |
| 39 | − }); | |
| 40 | − | |
| 41 | −/** Route 500-defects through the host's console `ErrorCapture`. */ | |
| 42 | −export const cloudflareMcpReporter: Layer.Layer<McpErrorReporter> = | |
| 43 | − makeConsoleMcpErrorReporter(ErrorCaptureLive); | |
| @@ -4,6 +4,7 @@ import { resolve } from "node:path"; · 3 unmodified lines | ||
| 4 | 4 | import { fileURLToPath } from "node:url"; |
| 5 | 5 | |
| 6 | 6 | import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"; |
| 7 | +import { Schema } from "effect"; | |
| 7 | 8 | import { unstable_dev, type Unstable_DevWorker } from "wrangler"; |
| 8 | 9 | |
| 9 | 10 | // --------------------------------------------------------------------------- |
| @@ -29,6 +30,28 @@ const SPEC = JSON.stringify({ · 19 unmodified lines | ||
| 29 | 30 | }, |
| 30 | 31 | }); |
| 31 | 32 | |
| 33 | +const decodeUnknownJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); | |
| 34 | + | |
| 35 | +interface JsonReadableResponse { | |
| 36 | + readonly headers: { get(name: string): string | null }; | |
| 37 | + json(): Promise<unknown>; | |
| 38 | + text(): Promise<string>; | |
| 39 | +} | |
| 40 | + | |
| 41 | +const readMcpJson = async <A>(response: JsonReadableResponse): Promise<A> => { | |
| 42 | + const contentType = response.headers.get("content-type") ?? ""; | |
| 43 | + if (contentType.includes("application/json")) { | |
| 44 | + return (await response.json()) as A; | |
| 45 | + } | |
| 46 | + const text = await response.text(); | |
| 47 | + const data = text | |
| 48 | + .split("\n") | |
| 49 | + .find((line) => line.startsWith("data: ")) | |
| 50 | + ?.slice("data: ".length); | |
| 51 | + expect(data).toBeTruthy(); | |
| 52 | + return decodeUnknownJson(data!) as A; | |
| 53 | +}; | |
| 54 | + | |
| 32 | 55 | describe("cloudflare host e2e (workerd/miniflare)", () => { |
| 33 | 56 | let worker: Unstable_DevWorker; |
| 34 | 57 | |
| @@ -209,13 +232,55 @@ describe("cloudflare host e2e (workerd/miniflare)", () => { · 174 unmodified lines | ||
| 209 | 232 | method: "tools/list", |
| 210 | 233 | }); |
| 211 | 234 | expect(list.status).toBe(200); |
| 212 | − const listed = (await list.json()) as { | |
| 235 | + const listed = await readMcpJson<{ | |
| 213 | 236 | result?: { tools?: ReadonlyArray<{ name: string }> }; |
| 214 | − }; | |
| 237 | + }>(list); | |
| 215 | 238 | const toolNames = listed.result?.tools?.map((t) => t.name) ?? []; |
| 216 | 239 | expect(toolNames).toContain("execute"); |
| 217 | 240 | }, 60_000); |
| 218 | 241 | |
| 242 | + it("serves streamable HTTP GET only for initialized sessions", async () => { | |
| 243 | + const missing = await worker.fetch("/mcp", { | |
| 244 | + method: "GET", | |
| 245 | + headers: { accept: "text/event-stream" }, | |
| 246 | + }); | |
| 247 | + expect(missing.status).toBe(400); | |
| 248 | + await missing.text(); | |
| 249 | + | |
| 250 | + const init = await worker.fetch("/mcp", { | |
| 251 | + method: "POST", | |
| 252 | + headers: { | |
| 253 | + "content-type": "application/json", | |
| 254 | + accept: "application/json, text/event-stream", | |
| 255 | + }, | |
| 256 | + body: JSON.stringify({ | |
| 257 | + jsonrpc: "2.0", | |
| 258 | + id: 1, | |
| 259 | + method: "initialize", | |
| 260 | + params: { | |
| 261 | + protocolVersion: "2025-03-26", | |
| 262 | + capabilities: {}, | |
| 263 | + clientInfo: { name: "test", version: "1" }, | |
| 264 | + }, | |
| 265 | + }), | |
| 266 | + }); | |
| 267 | + expect(init.status).toBe(200); | |
| 268 | + const sessionId = init.headers.get("mcp-session-id"); | |
| 269 | + expect(sessionId).toBeTruthy(); | |
| 270 | + await init.text(); | |
| 271 | + | |
| 272 | + const stream = await worker.fetch("/mcp", { | |
| 273 | + method: "GET", | |
| 274 | + headers: { | |
| 275 | + accept: "text/event-stream", | |
| 276 | + "mcp-session-id": sessionId!, | |
| 277 | + }, | |
| 278 | + }); | |
| 279 | + expect(stream.status).toBe(200); | |
| 280 | + expect(stream.headers.get("content-type")).toContain("text/event-stream"); | |
| 281 | + await stream.body?.cancel(); | |
| 282 | + }, 60_000); | |
| 283 | + | |
| 219 | 284 | it("invokes the execute tool over MCP (initialize → tools/call → QuickJS)", async () => { |
| 220 | 285 | const accept = "application/json, text/event-stream"; |
| 221 | 286 | const rpc = (sessionId: string | null, body: unknown) => |
| @@ -255,9 +320,9 @@ describe("cloudflare host e2e (workerd/miniflare)", () => { · 33 unmodified lines | ||
| 255 | 320 | params: { name: "execute", arguments: { code: "export default 6 * 7" } }, |
| 256 | 321 | }); |
| 257 | 322 | expect(call.status).toBe(200); |
| 258 | − const result = (await call.json()) as { | |
| 323 | + const result = await readMcpJson<{ | |
| 259 | 324 | result?: { structuredContent?: { result?: number } }; |
| 260 | − }; | |
| 325 | + }>(call); | |
| 261 | 326 | expect(result.result?.structuredContent?.result).toBe(42); |
| 262 | 327 | }, 60_000); |
| 263 | 328 | }); |
| @@ -6,25 +6,33 @@ import type { CloudflareEnv } from "./config"; · 5 unmodified lines | ||
| 6 | 6 | export { McpSessionDO } from "./mcp"; |
| 7 | 7 | |
| 8 | 8 | // --------------------------------------------------------------------------- |
| 9 | −// The Worker fetch entry. `ExecutorApp.make`'s `toWebHandler()` produces a | |
| 10 | −// `(Request) => Promise<Response>` — exactly a Worker handler — so the entry is | |
| 11 | −// thin: build the app ONCE per isolate (memoized; the build runs the D1 schema | |
| 12 | −// bring-up), then forward every request to its handler. `env` (the D1 binding + | |
| 13 | −// Access vars) arrives with the request and is captured at build time. | |
| 9 | +// The Worker fetch entry. Most requests go to `ExecutorApp.make`'s Effect web | |
| 10 | +// handler. `/mcp` stays at this edge boundary because `McpAgent.serve()` needs | |
| 11 | +// the Cloudflare `ExecutionContext` to pass authenticated session props into the | |
| 12 | +// hibernatable Durable Object bridge. | |
| 14 | 13 | // --------------------------------------------------------------------------- |
| 15 | 14 | |
| 16 | −let handlerPromise: Promise<(request: Request) => Promise<Response>> | null = null; | |
| 15 | +let handlerPromise: Promise<{ | |
| 16 | + readonly app: (request: Request) => Promise<Response>; | |
| 17 | + readonly mcp: (request: Request, env: CloudflareEnv, ctx: ExecutionContext) => Promise<Response>; | |
| 18 | +}> | null = null; | |
| 17 | 19 | |
| 18 | −const resolveHandler = (env: CloudflareEnv): Promise<(request: Request) => Promise<Response>> => { | |
| 20 | +const resolveHandler = (env: CloudflareEnv) => { | |
| 19 | 21 | if (!handlerPromise) { |
| 20 | − handlerPromise = makeCloudflareApp(env).then(({ toWebHandler }) => toWebHandler().handler); | |
| 22 | + handlerPromise = makeCloudflareApp(env).then(({ toWebHandler, mcpAgentHandler }) => ({ | |
| 23 | + app: toWebHandler().handler, | |
| 24 | + mcp: mcpAgentHandler, | |
| 25 | + })); | |
| 21 | 26 | } |
| 22 | 27 | return handlerPromise; |
| 23 | 28 | }; |
| 24 | 29 | |
| 25 | 30 | export default { |
| 26 | − fetch: async (request: Request, env: CloudflareEnv): Promise<Response> => { | |
| 31 | + fetch: async (request: Request, env: CloudflareEnv, ctx: ExecutionContext): Promise<Response> => { | |
| 27 | 32 | const serve = await resolveHandler(env); |
| 28 | − return serve(request); | |
| 33 | + if (new URL(request.url).pathname === "/mcp") { | |
| 34 | + return serve.mcp(request, env, ctx); | |
| 35 | + } | |
| 36 | + return serve.app(request); | |
| 29 | 37 | }, |
| 30 | 38 | }; |
| @@ -91,7 +91,6 @@ · 90 unmodified lines | ||
| 91 | 91 | "@tanstack/react-router": "catalog:", |
| 92 | 92 | "@tanstack/react-start": "catalog:", |
| 93 | 93 | "@workos-inc/node": "^8.11.1", |
| 94 | − "agents": "^0.10.0", | |
| 95 | 94 | "autumn-js": "^1.2.8", |
| 96 | 95 | "drizzle-orm": "catalog:", |
| 97 | 96 | "effect": "catalog:", |
| @@ -655,7 +654,7 @@ · 557 unmodified lines | ||
| 655 | 654 | "@executor-js/host-mcp": "workspace:*", |
| 656 | 655 | "@executor-js/sdk": "workspace:*", |
| 657 | 656 | "@modelcontextprotocol/sdk": "^1.29.0", |
| 658 | − "agents": "^0.10.0", | |
| 657 | + "agents": "^0.16.2", | |
| 659 | 658 | "effect": "catalog:", |
| 660 | 659 | }, |
| 661 | 660 | "devDependencies": { |
| @@ -1286,8 +1285,6 @@ · 624 unmodified lines | ||
| 1286 | 1285 | |
| 1287 | 1286 | "@antfu/ni": ["@antfu/ni@0.23.2", "", { "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nu": "bin/nu.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs" } }, "sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ=="], |
| 1288 | 1287 | |
| 1289 | − "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], | |
| 1290 | − | |
| 1291 | 1288 | "@astrojs/cloudflare": ["@astrojs/cloudflare@13.1.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/underscore-redirects": "1.0.3", "@cloudflare/vite-plugin": "^1.25.6", "piccolore": "^0.1.3", "tinyglobby": "^0.2.15", "vite": "^7.3.1" }, "peerDependencies": { "astro": "^6.0.0", "wrangler": "^4.61.1" } }, "sha512-Ogl8p4MifPMHbpHKJL78eqEL+I3wbHrYmo83P/FkKZQ9VzzKi2A1RjnbaiiPAwrA8xQOqIUo4zlN7+Mse0aJAQ=="], |
| 1292 | 1289 | |
| 1293 | 1290 | "@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="], |
| @@ -1392,27 +1389,27 @@ · 98 unmodified lines | ||
| 1392 | 1389 | |
| 1393 | 1390 | "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], |
| 1394 | 1391 | |
| 1395 | − "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], | |
| 1392 | + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], | |
| 1396 | 1393 | |
| 1397 | 1394 | "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], |
| 1398 | 1395 | |
| 1399 | − "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], | |
| 1396 | + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], | |
| 1400 | 1397 | |
| 1401 | 1398 | "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], |
| 1402 | 1399 | |
| 1403 | − "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], | |
| 1400 | + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], | |
| 1404 | 1401 | |
| 1405 | 1402 | "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], |
| 1406 | 1403 | |
| 1407 | 1404 | "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], |
| 1408 | 1405 | |
| 1409 | − "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], | |
| 1406 | + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], | |
| 1410 | 1407 | |
| 1411 | 1408 | "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], |
| 1412 | 1409 | |
| 1413 | − "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], | |
| 1410 | + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], | |
| 1414 | 1411 | |
| 1415 | − "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], | |
| 1412 | + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], | |
| 1416 | 1413 | |
| 1417 | 1414 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], |
| 1418 | 1415 | |
| @@ -1422,11 +1419,11 @@ · 3 unmodified lines | ||
| 1422 | 1419 | |
| 1423 | 1420 | "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], |
| 1424 | 1421 | |
| 1425 | − "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 1422 | + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], | |
| 1426 | 1423 | |
| 1427 | − "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], | |
| 1424 | + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-decorators": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg=="], | |
| 1428 | 1425 | |
| 1429 | − "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="], | |
| 1426 | + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg=="], | |
| 1430 | 1427 | |
| 1431 | 1428 | "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], |
| 1432 | 1429 | |
| @@ -1472,6 +1469,8 @@ · 39 unmodified lines | ||
| 1472 | 1469 | |
| 1473 | 1470 | "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], |
| 1474 | 1471 | |
| 1472 | + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], | |
| 1473 | + | |
| 1475 | 1474 | "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], |
| 1476 | 1475 | |
| 1477 | 1476 | "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], |
| @@ -1534,6 +1533,8 @@ · 56 unmodified lines | ||
| 1534 | 1533 | |
| 1535 | 1534 | "@clerk/shared": ["@clerk/shared@4.22.0", "", { "dependencies": { "@tanstack/query-core": "^5.100.6", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.7" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-GZ56kzUB2UBb8MCF+Eo8WIey+W4RP1tAkyOL+geiHxrQxJlUcgD+xalY2YPJLH8WVFwtOnfIl8KPEo0M0e/DRg=="], |
| 1536 | 1535 | |
| 1536 | + "@cloudflare/codemode": ["@cloudflare/codemode@0.4.2", "", { "dependencies": { "@types/json-schema": "^7.0.15", "acorn": "^8.17.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.0", "@tanstack/ai": ">=0.8.0 <1.0.0", "ai": "^6.0.0", "zod": "^4.0.0" }, "optionalPeers": ["@modelcontextprotocol/sdk", "@tanstack/ai", "ai", "zod"] }, "sha512-6sLMZDRY2USbXirrI4tjmGSMYAYM+E/PJIaDQLLbMdvQjk1z1TmJNSLomrZwErO1zFPXvGX2kaqkkxvixkfV2w=="], | |
| 1537 | + | |
| 1537 | 1538 | "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], |
| 1538 | 1539 | |
| 1539 | 1540 | "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], |
| @@ -2022,8 +2023,6 @@ · 482 unmodified lines | ||
| 2022 | 2023 | |
| 2023 | 2024 | "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], |
| 2024 | 2025 | |
| 2025 | − "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], | |
| 2026 | − | |
| 2027 | 2026 | "@kitlangton/terminal-control": ["@kitlangton/terminal-control@0.3.0", "", { "optionalDependencies": { "@kitlangton/terminal-control-darwin-arm64": "0.3.0", "@kitlangton/terminal-control-darwin-x64": "0.3.0", "@kitlangton/terminal-control-linux-arm64-gnu": "0.3.0", "@kitlangton/terminal-control-linux-x64-gnu": "0.3.0" }, "peerDependencies": { "vitest": ">=3.0.0" }, "optionalPeers": ["vitest"] }, "sha512-YlOx9ztDInHBETgxWNRd6nq/YzpMlw1YEJ1OIrhdeWTqrQdJHIYThWGiWda9GxmQAThWNULkKR17as+QDgyKdQ=="], |
| 2028 | 2027 | |
| 2029 | 2028 | "@kitlangton/terminal-control-darwin-arm64": ["@kitlangton/terminal-control-darwin-arm64@0.3.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "termctrl": "bin/termctrl" } }, "sha512-HydPv5if5pAjwHNbA7TnZAJYKfq9AiZITaJuFyF1HsUMedzin3qzv9WRVBqIiGsQ/FiR1i1kaGhyLzaVpZR35w=="], |
| @@ -2092,8 +2091,12 @@ · 62 unmodified lines | ||
| 2092 | 2091 | |
| 2093 | 2092 | "@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="], |
| 2094 | 2093 | |
| 2094 | + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], | |
| 2095 | + | |
| 2095 | 2096 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], |
| 2096 | 2097 | |
| 2098 | + "@mongodb-js/zstd": ["@mongodb-js/zstd@7.0.0", "", { "dependencies": { "node-addon-api": "^8.5.0", "prebuild-install": "^7.1.3" } }, "sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA=="], | |
| 2099 | + | |
| 2097 | 2100 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], |
| 2098 | 2101 | |
| 2099 | 2102 | "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], |
| @@ -2140,6 +2143,8 @@ · 40 unmodified lines | ||
| 2140 | 2143 | |
| 2141 | 2144 | "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], |
| 2142 | 2145 | |
| 2146 | + "@nodable/entities": ["@nodable/entities@2.2.0", "", {}, "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg=="], | |
| 2147 | + | |
| 2143 | 2148 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], |
| 2144 | 2149 | |
| 2145 | 2150 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], |
| @@ -2984,6 +2989,8 @@ · 838 unmodified lines | ||
| 2984 | 2989 | |
| 2985 | 2990 | "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], |
| 2986 | 2991 | |
| 2992 | + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], | |
| 2993 | + | |
| 2987 | 2994 | "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], |
| 2988 | 2995 | |
| 2989 | 2996 | "@turbo/darwin-64": ["@turbo/darwin-64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg=="], |
| @@ -3104,8 +3111,6 @@ · 114 unmodified lines | ||
| 3104 | 3111 | |
| 3105 | 3112 | "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], |
| 3106 | 3113 | |
| 3107 | − "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="], | |
| 3108 | − | |
| 3109 | 3114 | "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], |
| 3110 | 3115 | |
| 3111 | 3116 | "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], |
| @@ -3232,7 +3237,7 @@ · 120 unmodified lines | ||
| 3232 | 3237 | |
| 3233 | 3238 | "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], |
| 3234 | 3239 | |
| 3235 | − "agents": ["agents@0.10.2", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.29.0", "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.29.0", "@rolldown/plugin-babel": "^0.2.3", "cron-schedule": "^6.0.0", "json-schema": "^0.4.0", "json-schema-to-typescript": "^15.0.4", "mimetext": "^3.0.28", "nanoid": "^5.1.7", "partyserver": "^0.4.1", "partysocket": "1.1.16", "picomatch": "^4.0.4", "yargs": "^18.0.0" }, "peerDependencies": { "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.0", "@cloudflare/ai-chat": "^0.4.2", "@cloudflare/codemode": "^0.3.4", "@x402/core": "^2.0.0", "@x402/evm": "^2.0.0", "ai": "^6.0.0", "react": "^19.0.0", "viem": ">=2.0.0", "vite": ">=6.0.0 <9.0.0", "zod": "^4.0.0" }, "optionalPeers": ["@ai-sdk/openai", "@ai-sdk/react", "@cloudflare/ai-chat", "@cloudflare/codemode", "@x402/core", "@x402/evm", "viem", "vite"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-zltFKKENDClZgIJB7aRneI8tRpnCbMbP4RGPh4t8OgqKHomxUNZJEqMrjcgwP/0FX1W9GGrrLJVo9Ez2ibHVvA=="], | |
| 3240 | + "agents": ["agents@0.16.2", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.29.7", "@cfworker/json-schema": "^4.1.1", "@cloudflare/codemode": "^0.4.1", "@modelcontextprotocol/sdk": "1.29.0", "@rolldown/plugin-babel": "^0.2.3", "cron-schedule": "^6.0.0", "esbuild": "^0.28.1", "just-bash": "^3.0.1", "mimetext": "^3.0.28", "nanoid": "^5.1.11", "partyserver": "^0.5.8", "partysocket": "1.2.0", "yaml": "^2.9.0", "yargs": "^18.0.0" }, "peerDependencies": { "@cloudflare/ai-chat": ">=0.8.5 <1.0.0", "@tanstack/ai": ">=0.10.2 <1.0.0", "@x402/core": "^2.0.0", "@x402/evm": "^2.0.0", "ai": "^6.0.0", "chat": "^4.29.0", "react": "^19.0.0", "vite": ">=6.0.0 <9.0.0", "zod": "^4.0.0" }, "optionalPeers": ["@cloudflare/ai-chat", "@tanstack/ai", "@x402/core", "@x402/evm", "chat", "vite"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-pSWRw4l5j14cGsCPLDhXGjEfv4RX/kUHdgYsqCohd3AfutPNUEyGhbqfNzg8HklfQdZLpjJj+X7vsRAect5/4Q=="], | |
| 3236 | 3241 | |
| 3237 | 3242 | "ahooks": ["ahooks@3.9.7", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw=="], |
| 3238 | 3243 | |
| @@ -3264,6 +3269,8 @@ · 25 unmodified lines | ||
| 3264 | 3269 | |
| 3265 | 3270 | "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], |
| 3266 | 3271 | |
| 3272 | + "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], | |
| 3273 | + | |
| 3267 | 3274 | "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], |
| 3268 | 3275 | |
| 3269 | 3276 | "app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], |
| @@ -3944,6 +3951,10 @@ · 674 unmodified lines | ||
| 3944 | 3951 | |
| 3945 | 3952 | "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], |
| 3946 | 3953 | |
| 3954 | + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], | |
| 3955 | + | |
| 3956 | + "fast-xml-parser": ["fast-xml-parser@5.9.3", "", { "dependencies": { "@nodable/entities": "^2.2.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^1.0.1", "path-expression-matcher": "^1.5.0", "strnum": "^2.4.1", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g=="], | |
| 3957 | + | |
| 3947 | 3958 | "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], |
| 3948 | 3959 | |
| 3949 | 3960 | "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], |
| @@ -3960,7 +3971,7 @@ · 10 unmodified lines | ||
| 3960 | 3971 | |
| 3961 | 3972 | "file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], |
| 3962 | 3973 | |
| 3963 | − "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], | |
| 3974 | + "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], | |
| 3964 | 3975 | |
| 3965 | 3976 | "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], |
| 3966 | 3977 | |
| @@ -4296,6 +4307,8 @@ · 329 unmodified lines | ||
| 4296 | 4307 | |
| 4297 | 4308 | "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], |
| 4298 | 4309 | |
| 4310 | + "is-unsafe": ["is-unsafe@1.0.1", "", {}, "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA=="], | |
| 4311 | + | |
| 4299 | 4312 | "is-wayland": ["is-wayland@0.1.0", "", {}, "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA=="], |
| 4300 | 4313 | |
| 4301 | 4314 | "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], |
| @@ -4350,8 +4363,6 @@ · 48 unmodified lines | ||
| 4350 | 4363 | |
| 4351 | 4364 | "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], |
| 4352 | 4365 | |
| 4353 | − "json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="], | |
| 4354 | − | |
| 4355 | 4366 | "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], |
| 4356 | 4367 | |
| 4357 | 4368 | "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], |
| @@ -4374,6 +4385,8 @@ · 16 unmodified lines | ||
| 4374 | 4385 | |
| 4375 | 4386 | "jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="], |
| 4376 | 4387 | |
| 4388 | + "just-bash": ["just-bash@3.0.2", "", { "dependencies": { "diff": "^8.0.2", "fast-xml-parser": "^5.7.3", "file-type": "^21.2.0", "ini": "^6.0.0", "minimatch": "^10.1.1", "modern-tar": "^0.7.3", "papaparse": "^5.5.3", "quickjs-emscripten": "^0.32.0", "re2js": "^1.2.1", "seek-bzip": "^2.0.0", "smol-toml": "^1.6.0", "sprintf-js": "^1.1.3", "sql.js": "^1.13.0", "turndown": "^7.2.2", "yaml": "^2.8.2" }, "optionalDependencies": { "@mongodb-js/zstd": "^7.0.0", "node-liblzma": "^2.0.3" }, "bin": { "just-bash": "dist/bin/just-bash.js", "just-bash-shell": "dist/bin/shell/shell.js" } }, "sha512-uY8LMD2NpADp1HlYUcZSw9ffuq0tRhW76sg8xkeo+ZRDMu7mbekWbKbYDWquazVVi7pqpGZlic/liMFktQKy6w=="], | |
| 4389 | + | |
| 4377 | 4390 | "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], |
| 4378 | 4391 | |
| 4379 | 4392 | "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], |
| @@ -4674,6 +4687,8 @@ · 294 unmodified lines | ||
| 4674 | 4687 | |
| 4675 | 4688 | "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], |
| 4676 | 4689 | |
| 4690 | + "modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="], | |
| 4691 | + | |
| 4677 | 4692 | "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], |
| 4678 | 4693 | |
| 4679 | 4694 | "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], |
| @@ -4702,7 +4717,7 @@ · 22 unmodified lines | ||
| 4702 | 4717 | |
| 4703 | 4718 | "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], |
| 4704 | 4719 | |
| 4705 | − "nanoid": ["nanoid@5.1.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw=="], | |
| 4720 | + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], | |
| 4706 | 4721 | |
| 4707 | 4722 | "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], |
| 4708 | 4723 | |
| @@ -4716,7 +4731,7 @@ · 7 unmodified lines | ||
| 4716 | 4731 | |
| 4717 | 4732 | "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], |
| 4718 | 4733 | |
| 4719 | − "node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], | |
| 4734 | + "node-addon-api": ["node-addon-api@8.9.0", "", {}, "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q=="], | |
| 4720 | 4735 | |
| 4721 | 4736 | "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], |
| 4722 | 4737 | |
| @@ -4730,8 +4745,12 @@ · 7 unmodified lines | ||
| 4730 | 4745 | |
| 4731 | 4746 | "node-gyp": ["node-gyp@12.3.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="], |
| 4732 | 4747 | |
| 4748 | + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], | |
| 4749 | + | |
| 4733 | 4750 | "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], |
| 4734 | 4751 | |
| 4752 | + "node-liblzma": ["node-liblzma@2.2.0", "", { "dependencies": { "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" }, "bin": { "nxz": "lib/cli/nxz.js" } }, "sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g=="], | |
| 4753 | + | |
| 4735 | 4754 | "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], |
| 4736 | 4755 | |
| 4737 | 4756 | "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], |
| @@ -4830,6 +4849,8 @@ · 92 unmodified lines | ||
| 4830 | 4849 | |
| 4831 | 4850 | "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], |
| 4832 | 4851 | |
| 4852 | + "papaparse": ["papaparse@5.5.4", "", {}, "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ=="], | |
| 4853 | + | |
| 4833 | 4854 | "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], |
| 4834 | 4855 | |
| 4835 | 4856 | "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], |
| @@ -4854,9 +4875,9 @@ · 18 unmodified lines | ||
| 4854 | 4875 | |
| 4855 | 4876 | "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], |
| 4856 | 4877 | |
| 4857 | − "partyserver": ["partyserver@0.4.1", "", { "dependencies": { "nanoid": "^5.1.6" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240729.0" } }, "sha512-StSs0oY8RmTxjGNil7VbCG4gnTN+4rYX20fiUIItAxTPpr/5rPDZT6PIvMROkk9M1Gn7GzE1wuQXwhxceaGhXA=="], | |
| 4878 | + "partyserver": ["partyserver@0.5.8", "", { "dependencies": { "nanoid": "^5.1.9" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260424.1" } }, "sha512-htgSwiBcBu9zIYLrsxBAOvdkjukHvncbTk0nDrJgfruvZ08rxtEN1Ab4T7j9osykP80Bq3zA2oWFd3ngc4Z9uw=="], | |
| 4858 | 4879 | |
| 4859 | − "partysocket": ["partysocket@1.1.16", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w=="], | |
| 4880 | + "partysocket": ["partysocket@1.2.0", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-xgXql4N0b3umN263lHkrZMvvtYC906h4YjY8l63LNcF0x1bfJmQtZLQGqNQWzFHAxLns/6h6/rjdLW4EdYqQwA=="], | |
| 4860 | 4881 | |
| 4861 | 4882 | "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], |
| 4862 | 4883 | |
| @@ -4864,6 +4885,8 @@ · 1 unmodified line | ||
| 4864 | 4885 | |
| 4865 | 4886 | "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], |
| 4866 | 4887 | |
| 4888 | + "path-expression-matcher": ["path-expression-matcher@1.6.1", "", {}, "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ=="], | |
| 4889 | + | |
| 4867 | 4890 | "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], |
| 4868 | 4891 | |
| 4869 | 4892 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], |
| @@ -5050,6 +5073,8 @@ · 180 unmodified lines | ||
| 5050 | 5073 | |
| 5051 | 5074 | "re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="], |
| 5052 | 5075 | |
| 5076 | + "re2js": ["re2js@1.4.0", "", {}, "sha512-KTOIcZTSOpOxbu3i0+T6mFQ6tkxXKlTxfcMFs1trQbsMnG84qNq+DjXr8Afu+FEFjvF1NNlldpC7roPyazFI8g=="], | |
| 5077 | + | |
| 5053 | 5078 | "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], |
| 5054 | 5079 | |
| 5055 | 5080 | "react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="], |
| @@ -5268,6 +5293,8 @@ · 212 unmodified lines | ||
| 5268 | 5293 | |
| 5269 | 5294 | "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], |
| 5270 | 5295 | |
| 5296 | + "seek-bzip": ["seek-bzip@2.0.0", "", { "dependencies": { "commander": "^6.0.0" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg=="], | |
| 5297 | + | |
| 5271 | 5298 | "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], |
| 5272 | 5299 | |
| 5273 | 5300 | "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], |
| @@ -5364,6 +5391,8 @@ · 90 unmodified lines | ||
| 5364 | 5391 | |
| 5365 | 5392 | "sql-highlight": ["sql-highlight@6.1.0", "", {}, "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA=="], |
| 5366 | 5393 | |
| 5394 | + "sql.js": ["sql.js@1.14.1", "", {}, "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A=="], | |
| 5395 | + | |
| 5367 | 5396 | "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], |
| 5368 | 5397 | |
| 5369 | 5398 | "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], |
| @@ -5410,7 +5439,9 @@ · 40 unmodified lines | ||
| 5410 | 5439 | |
| 5411 | 5440 | "stripe": ["stripe@22.3.0", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ypO6xjVrMWs9SmIMeHr8naCx3dAQ0clxMdUTxn7Ejd7hmY9meBGfE+N4pVHkf9sUNebAHp6uJo6mV3GxDIc2cA=="], |
| 5412 | 5441 | |
| 5413 | − "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], | |
| 5442 | + "strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], | |
| 5443 | + | |
| 5444 | + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], | |
| 5414 | 5445 | |
| 5415 | 5446 | "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], |
| 5416 | 5447 | |
| @@ -5518,7 +5549,7 @@ · 101 unmodified lines | ||
| 5518 | 5549 | |
| 5519 | 5550 | "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], |
| 5520 | 5551 | |
| 5521 | − "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], | |
| 5552 | + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], | |
| 5522 | 5553 | |
| 5523 | 5554 | "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], |
| 5524 | 5555 | |
| @@ -5552,6 +5583,8 @@ · 27 unmodified lines | ||
| 5552 | 5583 | |
| 5553 | 5584 | "turbo": ["turbo@2.9.6", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.6", "@turbo/darwin-arm64": "2.9.6", "@turbo/linux-64": "2.9.6", "@turbo/linux-arm64": "2.9.6", "@turbo/windows-64": "2.9.6", "@turbo/windows-arm64": "2.9.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg=="], |
| 5554 | 5585 | |
| 5586 | + "turndown": ["turndown@7.2.4", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="], | |
| 5587 | + | |
| 5555 | 5588 | "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], |
| 5556 | 5589 | |
| 5557 | 5590 | "twitter-api-v2": ["twitter-api-v2@1.29.0", "", {}, "sha512-v473q5bwme4N+DWSg6qY+JCvfg1nSJRWwui3HUALafxfqCvVkKiYmS/5x/pVeJwTmyeBxexMbzHwnzrH4h6oYQ=="], |
| @@ -5734,6 +5767,8 @@ · 176 unmodified lines | ||
| 5734 | 5767 | |
| 5735 | 5768 | "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], |
| 5736 | 5769 | |
| 5770 | + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], | |
| 5771 | + | |
| 5737 | 5772 | "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], |
| 5738 | 5773 | |
| 5739 | 5774 | "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], |
| @@ -5796,14 +5831,44 @@ · 56 unmodified lines | ||
| 5796 | 5831 | |
| 5797 | 5832 | "@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@16.4.1", "", {}, "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw=="], |
| 5798 | 5833 | |
| 5834 | + "@babel/core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 5835 | + | |
| 5799 | 5836 | "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| 5800 | 5837 | |
| 5838 | + "@babel/generator/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 5839 | + | |
| 5840 | + "@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 5841 | + | |
| 5801 | 5842 | "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| 5802 | 5843 | |
| 5803 | 5844 | "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| 5804 | 5845 | |
| 5846 | + "@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], | |
| 5847 | + | |
| 5805 | 5848 | "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| 5806 | 5849 | |
| 5850 | + "@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], | |
| 5851 | + | |
| 5852 | + "@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 5853 | + | |
| 5854 | + "@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 5855 | + | |
| 5856 | + "@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], | |
| 5857 | + | |
| 5858 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], | |
| 5859 | + | |
| 5860 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 5861 | + | |
| 5862 | + "@babel/parser/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 5863 | + | |
| 5864 | + "@babel/plugin-proposal-decorators/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], | |
| 5865 | + | |
| 5866 | + "@babel/plugin-syntax-decorators/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], | |
| 5867 | + | |
| 5868 | + "@babel/template/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 5869 | + | |
| 5870 | + "@babel/traverse/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 5871 | + | |
| 5807 | 5872 | "@better-auth/core/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], |
| 5808 | 5873 | |
| 5809 | 5874 | "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], |
| @@ -5814,6 +5879,8 @@ · 4 unmodified lines | ||
| 5814 | 5879 | |
| 5815 | 5880 | "@clerk/shared/js-cookie": ["js-cookie@3.0.7", "", {}, "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw=="], |
| 5816 | 5881 | |
| 5882 | + "@cloudflare/codemode/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], | |
| 5883 | + | |
| 5817 | 5884 | "@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="], |
| 5818 | 5885 | |
| 5819 | 5886 | "@cloudflare/vite-plugin/miniflare": ["miniflare@4.20260415.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260415.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng=="], |
| @@ -5916,6 +5983,8 @@ · 96 unmodified lines | ||
| 5916 | 5983 | |
| 5917 | 5984 | "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], |
| 5918 | 5985 | |
| 5986 | + "@jimp/core/file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], | |
| 5987 | + | |
| 5919 | 5988 | "@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], |
| 5920 | 5989 | |
| 5921 | 5990 | "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
| @@ -6246,6 +6315,8 @@ · 324 unmodified lines | ||
| 6246 | 6315 | |
| 6247 | 6316 | "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
| 6248 | 6317 | |
| 6318 | + "@tanstack/router-utils/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 6319 | + | |
| 6249 | 6320 | "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], |
| 6250 | 6321 | |
| 6251 | 6322 | "@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="], |
| @@ -6254,6 +6325,10 @@ · 2 unmodified lines | ||
| 6254 | 6325 | |
| 6255 | 6326 | "@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
| 6256 | 6327 | |
| 6328 | + "@types/babel__core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 6329 | + | |
| 6330 | + "@types/babel__template/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 6331 | + | |
| 6257 | 6332 | "@types/better-sqlite3/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], |
| 6258 | 6333 | |
| 6259 | 6334 | "@types/cacheable-request/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], |
| @@ -6280,6 +6355,10 @@ · 20 unmodified lines | ||
| 6280 | 6355 | |
| 6281 | 6356 | "@vercel/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], |
| 6282 | 6357 | |
| 6358 | + "agents/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], | |
| 6359 | + | |
| 6360 | + "agents/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], | |
| 6361 | + | |
| 6283 | 6362 | "agents/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], |
| 6284 | 6363 | |
| 6285 | 6364 | "ai/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], |
| @@ -6314,6 +6393,8 @@ · 28 unmodified lines | ||
| 6314 | 6393 | |
| 6315 | 6394 | "atmn/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 6316 | 6395 | |
| 6396 | + "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 6397 | + | |
| 6317 | 6398 | "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], |
| 6318 | 6399 | |
| 6319 | 6400 | "better-auth/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], |
| @@ -6434,6 +6515,8 @@ · 114 unmodified lines | ||
| 6434 | 6515 | |
| 6435 | 6516 | "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], |
| 6436 | 6517 | |
| 6518 | + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], | |
| 6519 | + | |
| 6437 | 6520 | "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], |
| 6438 | 6521 | |
| 6439 | 6522 | "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], |
| @@ -6456,6 +6539,10 @@ · 16 unmodified lines | ||
| 6456 | 6539 | |
| 6457 | 6540 | "jsprim/verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], |
| 6458 | 6541 | |
| 6542 | + "just-bash/quickjs-emscripten": ["quickjs-emscripten@0.32.0", "", { "dependencies": { "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-release-sync": "0.32.0", "quickjs-emscripten-core": "0.32.0" } }, "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA=="], | |
| 6543 | + | |
| 6544 | + "just-bash/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], | |
| 6545 | + | |
| 6459 | 6546 | "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], |
| 6460 | 6547 | |
| 6461 | 6548 | "kayvee/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], |
| @@ -6464,6 +6551,8 @@ · 2 unmodified lines | ||
| 6464 | 6551 | |
| 6465 | 6552 | "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], |
| 6466 | 6553 | |
| 6554 | + "magicast/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 6555 | + | |
| 6467 | 6556 | "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], |
| 6468 | 6557 | |
| 6469 | 6558 | "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], |
| @@ -6570,6 +6659,8 @@ · 100 unmodified lines | ||
| 6570 | 6659 | |
| 6571 | 6660 | "run-jxa/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], |
| 6572 | 6661 | |
| 6662 | + "seek-bzip/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="], | |
| 6663 | + | |
| 6573 | 6664 | "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], |
| 6574 | 6665 | |
| 6575 | 6666 | "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], |
| @@ -6644,8 +6735,64 @@ · 68 unmodified lines | ||
| 6644 | 6735 | |
| 6645 | 6736 | "@azure/identity/@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], |
| 6646 | 6737 | |
| 6738 | + "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 6739 | + | |
| 6740 | + "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 6741 | + | |
| 6647 | 6742 | "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |
| 6648 | 6743 | |
| 6744 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], | |
| 6745 | + | |
| 6746 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], | |
| 6747 | + | |
| 6748 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], | |
| 6749 | + | |
| 6750 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], | |
| 6751 | + | |
| 6752 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 6753 | + | |
| 6754 | + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], | |
| 6755 | + | |
| 6756 | + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], | |
| 6757 | + | |
| 6758 | + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], | |
| 6759 | + | |
| 6760 | + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], | |
| 6761 | + | |
| 6762 | + "@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 6763 | + | |
| 6764 | + "@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 6765 | + | |
| 6766 | + "@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 6767 | + | |
| 6768 | + "@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 6769 | + | |
| 6770 | + "@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], | |
| 6771 | + | |
| 6772 | + "@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], | |
| 6773 | + | |
| 6774 | + "@babel/helper-replace-supers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], | |
| 6775 | + | |
| 6776 | + "@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], | |
| 6777 | + | |
| 6778 | + "@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], | |
| 6779 | + | |
| 6780 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], | |
| 6781 | + | |
| 6782 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], | |
| 6783 | + | |
| 6784 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], | |
| 6785 | + | |
| 6786 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], | |
| 6787 | + | |
| 6788 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 6789 | + | |
| 6790 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 6791 | + | |
| 6792 | + "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 6793 | + | |
| 6794 | + "@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 6795 | + | |
| 6649 | 6796 | "@cloudflare/vite-plugin/miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], |
| 6650 | 6797 | |
| 6651 | 6798 | "@cloudflare/vite-plugin/miniflare/workerd": ["workerd@1.20260415.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260415.1", "@cloudflare/workerd-darwin-arm64": "1.20260415.1", "@cloudflare/workerd-linux-64": "1.20260415.1", "@cloudflare/workerd-linux-arm64": "1.20260415.1", "@cloudflare/workerd-windows-64": "1.20260415.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-phyPjRnx+mQDfkhN9ENPioL1L0SdhYs4S0YmJK/xF9Oga+ykNfdSy1MHnsOj8yqnOV96zcVQMx32dJ0r3pq0jQ=="], |
| @@ -6846,6 +6993,10 @@ · 194 unmodified lines | ||
| 6846 | 6993 | |
| 6847 | 6994 | "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], |
| 6848 | 6995 | |
| 6996 | + "@jimp/core/file-type/strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], | |
| 6997 | + | |
| 6998 | + "@jimp/core/file-type/token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], | |
| 6999 | + | |
| 6849 | 7000 | "@libsql/kysely-libsql/@libsql/client/@libsql/core": ["@libsql/core@0.8.1", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-u6nrj6HZMTPsgJ9EBhLzO2uhqhlHQJQmVHV+0yFLvfGf3oSP8w7TjZCNUgu1G8jHISx6KFi7bmcrdXW9lRt++A=="], |
| 6850 | 7001 | |
| 6851 | 7002 | "@libsql/kysely-libsql/@libsql/client/@libsql/hrana-client": ["@libsql/hrana-client@0.6.2", "", { "dependencies": { "@libsql/isomorphic-fetch": "^0.2.1", "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-MWxgD7mXLNf9FXXiM0bc90wCjZSpErWKr5mGza7ERy2FJNNMXd7JIOv+DepBA1FQTIfI8TFO4/QDYgaQC0goNw=="], |
| @@ -6940,6 +7091,8 @@ · 88 unmodified lines | ||
| 6940 | 7091 | |
| 6941 | 7092 | "@tanstack/router-generator/@tanstack/router-core/seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], |
| 6942 | 7093 | |
| 7094 | + "@tanstack/router-generator/@tanstack/router-utils/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | |
| 7095 | + | |
| 6943 | 7096 | "@tanstack/start-plugin-core/@tanstack/router-generator/@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], |
| 6944 | 7097 | |
| 6945 | 7098 | "@types/better-sqlite3/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], |
| @@ -6966,6 +7119,58 @@ · 20 unmodified lines | ||
| 6966 | 7119 | |
| 6967 | 7120 | "@types/yauzl/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], |
| 6968 | 7121 | |
| 7122 | + "agents/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], | |
| 7123 | + | |
| 7124 | + "agents/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], | |
| 7125 | + | |
| 7126 | + "agents/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], | |
| 7127 | + | |
| 7128 | + "agents/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], | |
| 7129 | + | |
| 7130 | + "agents/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], | |
| 7131 | + | |
| 7132 | + "agents/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], | |
| 7133 | + | |
| 7134 | + "agents/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], | |
| 7135 | + | |
| 7136 | + "agents/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], | |
| 7137 | + | |
| 7138 | + "agents/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], | |
| 7139 | + | |
| 7140 | + "agents/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], | |
| 7141 | + | |
| 7142 | + "agents/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], | |
| 7143 | + | |
| 7144 | + "agents/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], | |
| 7145 | + | |
| 7146 | + "agents/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], | |
| 7147 | + | |
| 7148 | + "agents/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], | |
| 7149 | + | |
| 7150 | + "agents/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], | |
| 7151 | + | |
| 7152 | + "agents/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], | |
| 7153 | + | |
| 7154 | + "agents/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], | |
| 7155 | + | |
| 7156 | + "agents/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], | |
| 7157 | + | |
| 7158 | + "agents/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], | |
| 7159 | + | |
| 7160 | + "agents/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], | |
| 7161 | + | |
| 7162 | + "agents/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], | |
| 7163 | + | |
| 7164 | + "agents/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], | |
| 7165 | + | |
| 7166 | + "agents/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], | |
| 7167 | + | |
| 7168 | + "agents/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], | |
| 7169 | + | |
| 7170 | + "agents/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], | |
| 7171 | + | |
| 7172 | + "agents/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], | |
| 7173 | + | |
| 6969 | 7174 | "agents/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], |
| 6970 | 7175 | |
| 6971 | 7176 | "agents/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], |
| @@ -7178,6 +7383,16 @@ · 206 unmodified lines | ||
| 7178 | 7383 | |
| 7179 | 7384 | "jsprim/verror/extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], |
| 7180 | 7385 | |
| 7386 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-asyncify": ["@jitl/quickjs-wasmfile-debug-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw=="], | |
| 7387 | + | |
| 7388 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-sync": ["@jitl/quickjs-wasmfile-debug-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q=="], | |
| 7389 | + | |
| 7390 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-asyncify": ["@jitl/quickjs-wasmfile-release-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q=="], | |
| 7391 | + | |
| 7392 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-sync": ["@jitl/quickjs-wasmfile-release-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg=="], | |
| 7393 | + | |
| 7394 | + "just-bash/quickjs-emscripten/quickjs-emscripten-core": ["quickjs-emscripten-core@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg=="], | |
| 7395 | + | |
| 7181 | 7396 | "kayvee/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], |
| 7182 | 7397 | |
| 7183 | 7398 | "mimetext/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], |
| @@ -7306,6 +7521,22 @@ · 122 unmodified lines | ||
| 7306 | 7521 | |
| 7307 | 7522 | "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], |
| 7308 | 7523 | |
| 7524 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 7525 | + | |
| 7526 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 7527 | + | |
| 7528 | + "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 7529 | + | |
| 7530 | + "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 7531 | + | |
| 7532 | + "@babel/helper-replace-supers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 7533 | + | |
| 7534 | + "@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 7535 | + | |
| 7536 | + "@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 7537 | + | |
| 7538 | + "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 7539 | + | |
| 7309 | 7540 | "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260415.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg=="], |
| 7310 | 7541 | |
| 7311 | 7542 | "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260415.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ=="], |
| @@ -7468,6 +7699,16 @@ · 156 unmodified lines | ||
| 7468 | 7699 | |
| 7469 | 7700 | "ink-confirm-input/ink-text-input/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], |
| 7470 | 7701 | |
| 7702 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-asyncify/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], | |
| 7703 | + | |
| 7704 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-debug-sync/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], | |
| 7705 | + | |
| 7706 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-asyncify/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], | |
| 7707 | + | |
| 7708 | + "just-bash/quickjs-emscripten/@jitl/quickjs-wasmfile-release-sync/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], | |
| 7709 | + | |
| 7710 | + "just-bash/quickjs-emscripten/quickjs-emscripten-core/@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], | |
| 7711 | + | |
| 7471 | 7712 | "kayvee/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], |
| 7472 | 7713 | |
| 7473 | 7714 | "ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], |
| @@ -49,6 +49,7 @@ import type { PluginsProvider } from "./scoped-executor"; · 48 unmodified lines | ||
| 49 | 49 | import { requestScopedMiddleware } from "./request-scoped"; |
| 50 | 50 | import { |
| 51 | 51 | McpServingRoutes, |
| 52 | + McpDiscoveryRoutes, | |
| 52 | 53 | McpErrorReporterNoop, |
| 53 | 54 | type McpAuthProvider, |
| 54 | 55 | type McpErrorReporter, |
| @@ -128,8 +129,12 @@ export interface EngineProviders<REngine = never> { · 73 unmodified lines | ||
| 128 | 129 | export interface McpProviders<RMcpAuth = never> { |
| 129 | 130 | /** Resolve a request to an MCP `AuthOutcome` + declare the discovery routes. */ |
| 130 | 131 | readonly auth: Layer.Layer<McpAuthProvider, never, RMcpAuth>; |
| 131 | − /** Owns the entire serving-session lifecycle (in-process Map vs DO). */ | |
| 132 | − readonly sessions: Layer.Layer<McpSessionStore>; | |
| 132 | + /** | |
| 133 | + * Owns the entire serving-session lifecycle (in-process Map vs DO). Optional: | |
| 134 | + * a host that serves `/mcp` transport outside this envelope (the Cloudflare | |
| 135 | + * Agent bridge) omits it, and only the discovery routes are mounted. | |
| 136 | + */ | |
| 137 | + readonly sessions?: Layer.Layer<McpSessionStore>; | |
| 133 | 138 | /** Forward an orchestration defect to the host's capture; default no-op. */ |
| 134 | 139 | readonly reporter?: Layer.Layer<McpErrorReporter>; |
| 135 | 140 | } |
| @@ -609,6 +614,12 @@ const buildMcpRoutes = <RMcpAuth>( · 473 unmodified lines | ||
| 609 | 614 | const mcpAuthLive = (mcp.auth as Layer.Layer<McpAuthProvider, never, IdentityProvider>).pipe( |
| 610 | 615 | Layer.provide(identity), |
| 611 | 616 | ); |
| 617 | + // No session store: the host serves `/mcp` transport elsewhere (the Cloudflare | |
| 618 | + // Agent bridge), so mount only the auth-declared discovery routes. The discovery | |
| 619 | + // handlers are captured from the auth seam at build, so no per-request seams. | |
| 620 | + if (!mcp.sessions) { | |
| 621 | + return McpDiscoveryRoutes.pipe(Layer.provide(mcpAuthLive)); | |
| 622 | + } | |
| 612 | 623 | const mcpSeams = Layer.mergeAll(mcpAuthLive, mcp.sessions, mcp.reporter ?? McpErrorReporterNoop); |
| 613 | 624 | return McpServingRoutes.pipe(HttpRouter.provideRequest(mcpSeams), Layer.provide(mcpAuthLive)); |
| 614 | 625 | }; |
| @@ -85,3 +85,18 @@ describe("executeWithPause failure propagation", () => { · 84 unmodified lines | ||
| 85 | 85 | }), |
| 86 | 86 | ); |
| 87 | 87 | }); |
| 88 | + | |
| 89 | +describe("pausedExecutionCount", () => { | |
| 90 | + it.effect("starts at zero", () => | |
| 91 | + Effect.gen(function* () { | |
| 92 | + const executor = yield* makeExecutor(); | |
| 93 | + const engine = createExecutionEngine({ | |
| 94 | + executor, | |
| 95 | + codeExecutor: succeedingExecutor, | |
| 96 | + }); | |
| 97 | + | |
| 98 | + expect(yield* engine.pausedExecutionCount()).toBe(0); | |
| 99 | + expect(yield* engine.hasPausedExecutions()).toBe(false); | |
| 100 | + }), | |
| 101 | + ); | |
| 102 | +}); | |
| @@ -401,6 +401,12 @@ export type ExecutionEngine<E extends Cause.YieldableError = CodeExecutionError> · 400 unmodified lines | ||
| 401 | 401 | */ |
| 402 | 402 | readonly getPausedExecution: (executionId: string) => Effect.Effect<PausedExecution | null>; |
| 403 | 403 | |
| 404 | + /** Count of executions currently paused awaiting resume. */ | |
| 405 | + readonly pausedExecutionCount: () => Effect.Effect<number>; | |
| 406 | + | |
| 407 | + /** Whether any executions are paused awaiting resume. */ | |
| 408 | + readonly hasPausedExecutions: () => Effect.Effect<boolean>; | |
| 409 | + | |
| 404 | 410 | /** |
| 405 | 411 | * Get the dynamic tool description (workflow + namespaces). |
| 406 | 412 | */ |
| @@ -633,6 +639,8 @@ export const createExecutionEngine = <E extends Cause.YieldableError = CodeExecu · 226 unmodified lines | ||
| 633 | 639 | resume: resumeExecution, |
| 634 | 640 | getPausedExecution: (executionId) => |
| 635 | 641 | Effect.sync(() => pausedExecutions.get(executionId) ?? null), |
| 642 | + pausedExecutionCount: () => Effect.sync(() => pausedExecutions.size), | |
| 643 | + hasPausedExecutions: () => Effect.sync(() => pausedExecutions.size > 0), | |
| 636 | 644 | getDescription: buildExecuteDescription(executor), |
| 637 | 645 | }; |
| 638 | 646 | }; |
| @@ -50,6 +50,8 @@ export type ExecutionEngine = { · 49 unmodified lines | ||
| 50 | 50 | response: ResumeResponse, |
| 51 | 51 | ) => Promise<ExecutionResult | null>; |
| 52 | 52 | readonly getPausedExecution: (executionId: string) => Promise<PausedExecution | null>; |
| 53 | + readonly pausedExecutionCount: () => Promise<number>; | |
| 54 | + readonly hasPausedExecutions: () => Promise<boolean>; | |
| 53 | 55 | readonly getDescription: () => Promise<string>; |
| 54 | 56 | }; |
| 55 | 57 | |
| @@ -155,6 +157,8 @@ export const toPromiseExecutionEngine = <E extends Cause.YieldableError>( · 99 unmodified lines | ||
| 155 | 157 | executeWithPause: (code, options) => Effect.runPromise(engine.executeWithPause(code, options)), |
| 156 | 158 | resume: (executionId, response) => Effect.runPromise(engine.resume(executionId, response)), |
| 157 | 159 | getPausedExecution: (executionId) => Effect.runPromise(engine.getPausedExecution(executionId)), |
| 160 | + pausedExecutionCount: () => Effect.runPromise(engine.pausedExecutionCount()), | |
| 161 | + hasPausedExecutions: () => Effect.runPromise(engine.hasPausedExecutions()), | |
| 158 | 162 | getDescription: () => Effect.runPromise(engine.getDescription), |
| 159 | 163 | }); |
| 160 | 164 | |
| @@ -8,29 +8,13 @@ · 7 unmodified lines | ||
| 8 | 8 | "types": "./src/blob-store.ts", |
| 9 | 9 | "default": "./src/blob-store.ts" |
| 10 | 10 | }, |
| 11 | − "./mcp/worker-transport": { | |
| 12 | − "types": "./src/mcp/worker-transport.ts", | |
| 13 | − "default": "./src/mcp/worker-transport.ts" | |
| 14 | − }, | |
| 15 | 11 | "./mcp/do-headers": { |
| 16 | 12 | "types": "./src/mcp/do-headers.ts", |
| 17 | 13 | "default": "./src/mcp/do-headers.ts" |
| 18 | 14 | }, |
| 19 | − "./mcp/response-peek": { | |
| 20 | − "types": "./src/mcp/response-peek.ts", | |
| 21 | − "default": "./src/mcp/response-peek.ts" | |
| 22 | − }, | |
| 23 | − "./mcp/session-store": { | |
| 24 | − "types": "./src/mcp/session-store.ts", | |
| 25 | − "default": "./src/mcp/session-store.ts" | |
| 26 | − }, | |
| 27 | − "./mcp/durable-object": { | |
| 28 | − "types": "./src/mcp/session-durable-object.ts", | |
| 29 | − "default": "./src/mcp/session-durable-object.ts" | |
| 30 | − }, | |
| 31 | − "./mcp/request-id-queue": { | |
| 32 | − "types": "./src/mcp/request-id-queue.ts", | |
| 33 | − "default": "./src/mcp/request-id-queue.ts" | |
| 15 | + "./mcp/agent-durable-object": { | |
| 16 | + "types": "./src/mcp/agent-session-durable-object.ts", | |
| 17 | + "default": "./src/mcp/agent-session-durable-object.ts" | |
| 34 | 18 | } |
| 35 | 19 | }, |
| 36 | 20 | "scripts": { |
| @@ -44,7 +28,7 @@ · 7 unmodified lines | ||
| 44 | 28 | "@executor-js/host-mcp": "workspace:*", |
| 45 | 29 | "@executor-js/sdk": "workspace:*", |
| 46 | 30 | "@modelcontextprotocol/sdk": "^1.29.0", |
| 47 | − "agents": "^0.10.0", | |
| 31 | + "agents": "^0.16.2", | |
| 48 | 32 | "effect": "catalog:" |
| 49 | 33 | }, |
| 50 | 34 | "devDependencies": { |
| @@ -0,0 +1,788 @@ | ||
| 1 | +import { Cause, Deferred, Effect, Exit, Option, Schema } from "effect"; | |
| 2 | +import type * as Tracer from "effect/Tracer"; | |
| 3 | +import type { Connection, ConnectionContext } from "agents"; | |
| 4 | +import { McpAgent } from "agents/mcp"; | |
| 5 | +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | |
| 6 | + | |
| 7 | +import { RequestOrgSlug, RequestWebOrigin } from "@executor-js/api/server"; | |
| 8 | +import { | |
| 9 | + formatPausedExecution, | |
| 10 | + type ExecutionEngine, | |
| 11 | + type ResumeResponse, | |
| 12 | +} from "@executor-js/execution"; | |
| 13 | +import { | |
| 14 | + PAUSED_APPROVAL_TIMEOUT_MS, | |
| 15 | + type PausedExecutionHooks, | |
| 16 | +} from "@executor-js/host-mcp/tool-server"; | |
| 17 | +import type { McpResource } from "@executor-js/host-mcp"; | |
| 18 | + | |
| 19 | +import type { IncomingPropagationHeaders, McpElicitationMode } from "./do-headers"; | |
| 20 | +import { | |
| 21 | + SESSION_TIMEOUT_MS, | |
| 22 | + decideSessionAlarm, | |
| 23 | + pausedLeaseExtensionLog, | |
| 24 | +} from "./session-alarm-policy"; | |
| 25 | + | |
| 26 | +export type IncomingTraceHeaders = IncomingPropagationHeaders; | |
| 27 | + | |
| 28 | +export interface McpSessionInit { | |
| 29 | + readonly organizationId: string; | |
| 30 | + readonly userId: string; | |
| 31 | + readonly elicitationMode: McpElicitationMode; | |
| 32 | + /** The MCP resource the session was minted against (`/mcp` default vs a | |
| 33 | + * `/mcp/toolkits/<slug>` toolkit), so the tool catalog is scoped to it. */ | |
| 34 | + readonly resource: McpResource; | |
| 35 | + readonly webOrigin?: string; | |
| 36 | +} | |
| 37 | + | |
| 38 | +export interface McpSessionProps extends Record<string, unknown> { | |
| 39 | + readonly session: McpSessionInit; | |
| 40 | + readonly propagation?: IncomingTraceHeaders; | |
| 41 | +} | |
| 42 | + | |
| 43 | +export type McpApprovalOwner = { | |
| 44 | + readonly accountId: string; | |
| 45 | + readonly organizationId: string; | |
| 46 | +}; | |
| 47 | + | |
| 48 | +type McpSessionApprovalErrorResult = | |
| 49 | + | { readonly status: "not_found" } | |
| 50 | + | { readonly status: "forbidden" }; | |
| 51 | + | |
| 52 | +type PendingApprovalLease = { | |
| 53 | + readonly disposeKeepAlive: () => void; | |
| 54 | + timeout: ReturnType<typeof setTimeout> | null; | |
| 55 | + expiring: boolean; | |
| 56 | +}; | |
| 57 | + | |
| 58 | +export type McpSessionApprovalResult = | |
| 59 | + | { | |
| 60 | + readonly status: "ok"; | |
| 61 | + readonly text: string; | |
| 62 | + readonly structured: Record<string, unknown>; | |
| 63 | + } | |
| 64 | + | McpSessionApprovalErrorResult; | |
| 65 | + | |
| 66 | +export type McpSessionResumeApprovalResult = | |
| 67 | + | { | |
| 68 | + readonly status: "ok"; | |
| 69 | + readonly executionStatus: "completed" | "paused"; | |
| 70 | + readonly text: string; | |
| 71 | + readonly structured: Record<string, unknown>; | |
| 72 | + readonly isError?: boolean; | |
| 73 | + } | |
| 74 | + | McpSessionApprovalErrorResult; | |
| 75 | + | |
| 76 | +export interface SessionDbHandle { | |
| 77 | + readonly end: () => Promise<void> | void; | |
| 78 | +} | |
| 79 | + | |
| 80 | +export interface SessionMeta { | |
| 81 | + readonly organizationId: string; | |
| 82 | + readonly organizationName: string; | |
| 83 | + /** The org's URL slug, when the host's `resolveSessionMeta` carried one. | |
| 84 | + * Pins browser-handoff URLs to the right org's console. */ | |
| 85 | + readonly organizationSlug?: string; | |
| 86 | + readonly userId: string; | |
| 87 | + readonly elicitationMode?: "browser" | "model" | "native"; | |
| 88 | + /** The MCP resource the session serves (carried from {@link McpSessionInit}); | |
| 89 | + * `buildMcpServer` scopes the tool catalog to it. */ | |
| 90 | + readonly resource: McpResource; | |
| 91 | + readonly webOrigin?: string; | |
| 92 | +} | |
| 93 | + | |
| 94 | +export interface BuiltMcpServer { | |
| 95 | + readonly mcpServer: McpServer; | |
| 96 | + readonly engine: ExecutionEngine<Cause.YieldableError>; | |
| 97 | +} | |
| 98 | + | |
| 99 | +export interface BrowserApprovalStore { | |
| 100 | + readonly takeResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>; | |
| 101 | + readonly waitForResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>; | |
| 102 | +} | |
| 103 | + | |
| 104 | +const SESSION_META_KEY = "session-meta"; | |
| 105 | +const LAST_ACTIVITY_KEY = "last-activity-ms"; | |
| 106 | +const PARTYSERVER_NAME_KEY = "__ps_name"; | |
| 107 | +const MCP_HTTP_METHOD_HEADER = "cf-mcp-method"; | |
| 108 | +const MCP_MESSAGE_HEADER = "cf-mcp-message"; | |
| 109 | +const ACTIVE_POST_RESPONSE_WAIT_POLL_MS = 25; | |
| 110 | +const ACTIVE_POST_RESPONSE_WAIT_MAX_MS = PAUSED_APPROVAL_TIMEOUT_MS + 30_000; | |
| 111 | +const approvalResponseKey = (executionId: string) => `approval-response:${executionId}`; | |
| 112 | + | |
| 113 | +type JsonRpcRequestId = string | number; | |
| 114 | +const JsonRpcRequestWithId = Schema.Struct({ | |
| 115 | + id: Schema.Union([Schema.String, Schema.Number]), | |
| 116 | + method: Schema.String, | |
| 117 | +}); | |
| 118 | +const JsonRpcPostPayload = Schema.fromJsonString(Schema.Unknown); | |
| 119 | +const decodeJsonRpcPostPayload = Schema.decodeUnknownOption(JsonRpcPostPayload); | |
| 120 | +const decodeJsonRpcRequestWithId = Schema.decodeUnknownOption(JsonRpcRequestWithId); | |
| 121 | + | |
| 122 | +const resumeApprovalResult = ( | |
| 123 | + executionId: string, | |
| 124 | + response: ResumeResponse, | |
| 125 | +): Extract<McpSessionResumeApprovalResult, { readonly status: "ok" }> => { | |
| 126 | + const textByAction = { | |
| 127 | + accept: "I've approved it", | |
| 128 | + decline: "I've denied it", | |
| 129 | + cancel: "I've canceled it", | |
| 130 | + } satisfies Record<ResumeResponse["action"], string>; | |
| 131 | + const statusByAction = { | |
| 132 | + accept: "approved", | |
| 133 | + decline: "denied", | |
| 134 | + cancel: "canceled", | |
| 135 | + } satisfies Record<ResumeResponse["action"], string>; | |
| 136 | + | |
| 137 | + return { | |
| 138 | + status: "ok", | |
| 139 | + executionStatus: "completed", | |
| 140 | + text: textByAction[response.action], | |
| 141 | + structured: { status: statusByAction[response.action], executionId }, | |
| 142 | + isError: false, | |
| 143 | + }; | |
| 144 | +}; | |
| 145 | + | |
| 146 | +const isSessionProps = (props: unknown): props is McpSessionProps => | |
| 147 | + typeof props === "object" && | |
| 148 | + props !== null && | |
| 149 | + "session" in props && | |
| 150 | + typeof (props as { readonly session?: unknown }).session === "object" && | |
| 151 | + (props as { readonly session?: unknown }).session !== null; | |
| 152 | + | |
| 153 | +const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)); | |
| 154 | + | |
| 155 | +const readActivePostRequestIds = (request: Request): readonly JsonRpcRequestId[] => { | |
| 156 | + if (request.headers.get(MCP_HTTP_METHOD_HEADER) !== "POST") return []; | |
| 157 | + const encoded = request.headers.get(MCP_MESSAGE_HEADER); | |
| 158 | + if (!encoded) return []; | |
| 159 | + const decoded = Effect.runSyncExit( | |
| 160 | + Effect.try({ | |
| 161 | + try: () => atob(encoded), | |
| 162 | + catch: () => "invalid_base64" as const, | |
| 163 | + }), | |
| 164 | + ); | |
| 165 | + if (Exit.isFailure(decoded)) { | |
| 166 | + console.warn( | |
| 167 | + JSON.stringify({ | |
| 168 | + event: "mcp_active_post_response_wait_parse_failed", | |
| 169 | + reason: "invalid_base64", | |
| 170 | + }), | |
| 171 | + ); | |
| 172 | + return []; | |
| 173 | + } | |
| 174 | + const parsed = decodeJsonRpcPostPayload(decoded.value); | |
| 175 | + if (Option.isNone(parsed)) { | |
| 176 | + console.warn( | |
| 177 | + JSON.stringify({ | |
| 178 | + event: "mcp_active_post_response_wait_parse_failed", | |
| 179 | + reason: "invalid_json", | |
| 180 | + }), | |
| 181 | + ); | |
| 182 | + return []; | |
| 183 | + } | |
| 184 | + const messages = Array.isArray(parsed.value) ? parsed.value : [parsed.value]; | |
| 185 | + const requestIds: JsonRpcRequestId[] = []; | |
| 186 | + for (const message of messages) { | |
| 187 | + const decoded = decodeJsonRpcRequestWithId(message); | |
| 188 | + if (Option.isSome(decoded)) requestIds.push(decoded.value.id); | |
| 189 | + } | |
| 190 | + return requestIds; | |
| 191 | +}; | |
| 192 | + | |
| 193 | +export abstract class McpAgentSessionDOBase< | |
| 194 | + Env extends Cloudflare.Env = Cloudflare.Env, | |
| 195 | + TDbHandle extends SessionDbHandle = SessionDbHandle, | |
| 196 | +> extends McpAgent<Env, unknown, McpSessionProps> { | |
| 197 | + server!: McpServer; | |
| 198 | + private engine: ExecutionEngine<Cause.YieldableError> | null = null; | |
| 199 | + private dbHandle: TDbHandle | null = null; | |
| 200 | + private sessionMeta: SessionMeta | null = null; | |
| 201 | + private initialized = false; | |
| 202 | + private lastActivityMs = 0; | |
| 203 | + private approvalResponses = new Map<string, ResumeResponse>(); | |
| 204 | + private approvalWaiters = new Map<string, Deferred.Deferred<ResumeResponse>>(); | |
| 205 | + private pendingApprovalLeases = new Map<string, PendingApprovalLease>(); | |
| 206 | + | |
| 207 | + protected abstract openSessionDb(): TDbHandle | Promise<TDbHandle>; | |
| 208 | + | |
| 209 | + protected abstract resolveSessionMeta(token: McpSessionInit): Effect.Effect<SessionMeta>; | |
| 210 | + | |
| 211 | + protected abstract buildMcpServer( | |
| 212 | + sessionMeta: SessionMeta, | |
| 213 | + dbHandle: TDbHandle, | |
| 214 | + ): Effect.Effect<BuiltMcpServer>; | |
| 215 | + | |
| 216 | + protected withTelemetry<A, E>( | |
| 217 | + effect: Effect.Effect<A, E>, | |
| 218 | + _incoming?: IncomingTraceHeaders, | |
| 219 | + ): Effect.Effect<A, E> { | |
| 220 | + return effect; | |
| 221 | + } | |
| 222 | + | |
| 223 | + protected captureCause(_cause: Cause.Cause<unknown>): void {} | |
| 224 | + | |
| 225 | + protected flushTelemetry(): Promise<void> { | |
| 226 | + return Promise.resolve(); | |
| 227 | + } | |
| 228 | + | |
| 229 | + protected get sessionId(): string { | |
| 230 | + return this.getSessionId(); | |
| 231 | + } | |
| 232 | + | |
| 233 | + protected currentParentSpan(): Tracer.AnySpan | undefined { | |
| 234 | + return undefined; | |
| 235 | + } | |
| 236 | + | |
| 237 | + protected readonly browserApprovalStore: BrowserApprovalStore = { | |
| 238 | + takeResponse: (executionId) => this.takeApprovalResponse(executionId), | |
| 239 | + waitForResponse: (executionId) => this.waitForApprovalResponse(executionId), | |
| 240 | + }; | |
| 241 | + | |
| 242 | + protected readonly pausedExecutionHooks: PausedExecutionHooks = { | |
| 243 | + onExecutionPaused: (executionId) => | |
| 244 | + Effect.sync(() => { | |
| 245 | + this.queuePendingApprovalLeaseStart(executionId); | |
| 246 | + }), | |
| 247 | + onResumeStarted: (executionId) => this.beginPendingApprovalResume(executionId), | |
| 248 | + onResumeSettled: (executionId) => this.finishPendingApprovalResume(executionId), | |
| 249 | + }; | |
| 250 | + | |
| 251 | + override async onConnect(conn: Connection, context: ConnectionContext): Promise<void> { | |
| 252 | + const requestIds = readActivePostRequestIds(context.request); | |
| 253 | + if (requestIds.length === 0) { | |
| 254 | + await super.onConnect(conn, context); | |
| 255 | + return; | |
| 256 | + } | |
| 257 | + | |
| 258 | + await this.keepAliveWhile(async () => { | |
| 259 | + await super.onConnect(conn, context); | |
| 260 | + await this.waitForActivePostResponseDrain(conn.id); | |
| 261 | + }); | |
| 262 | + } | |
| 263 | + | |
| 264 | + private openSessionDbHandle(): Effect.Effect<TDbHandle> { | |
| 265 | + return Effect.promise(() => Promise.resolve(this.openSessionDb())); | |
| 266 | + } | |
| 267 | + | |
| 268 | + private async waitForActivePostResponseDrain(streamId: string): Promise<void> { | |
| 269 | + const startedAt = Date.now(); | |
| 270 | + for (;;) { | |
| 271 | + const requestIds = await this.getStreamRequestIds(streamId); | |
| 272 | + if (!requestIds || requestIds.length === 0) return; | |
| 273 | + if (Date.now() - startedAt >= ACTIVE_POST_RESPONSE_WAIT_MAX_MS) { | |
| 274 | + console.warn( | |
| 275 | + JSON.stringify({ | |
| 276 | + event: "mcp_active_post_response_wait_timeout", | |
| 277 | + sessionId: this.sessionId, | |
| 278 | + streamId, | |
| 279 | + requestIds, | |
| 280 | + elapsedMs: Date.now() - startedAt, | |
| 281 | + }), | |
| 282 | + ); | |
| 283 | + return; | |
| 284 | + } | |
| 285 | + await sleep(ACTIVE_POST_RESPONSE_WAIT_POLL_MS); | |
| 286 | + } | |
| 287 | + } | |
| 288 | + | |
| 289 | + private loadSessionMeta(): Effect.Effect<SessionMeta | null> { | |
| 290 | + return Effect.promise(async () => { | |
| 291 | + if (this.sessionMeta) return this.sessionMeta; | |
| 292 | + const stored = await this.ctx.storage.get<SessionMeta>(SESSION_META_KEY); | |
| 293 | + this.sessionMeta = stored ?? null; | |
| 294 | + return this.sessionMeta; | |
| 295 | + }).pipe(Effect.withSpan("mcp.session.load_meta")); | |
| 296 | + } | |
| 297 | + | |
| 298 | + private async saveSessionMeta(sessionMeta: SessionMeta): Promise<void> { | |
| 299 | + this.sessionMeta = sessionMeta; | |
| 300 | + await this.ctx.storage.put(SESSION_META_KEY, sessionMeta); | |
| 301 | + } | |
| 302 | + | |
| 303 | + private async markActivity(now = Date.now()): Promise<void> { | |
| 304 | + this.lastActivityMs = now; | |
| 305 | + await Promise.all([ | |
| 306 | + this.ctx.storage.put(LAST_ACTIVITY_KEY, now), | |
| 307 | + this.ctx.storage.setAlarm(now + SESSION_TIMEOUT_MS), | |
| 308 | + ]); | |
| 309 | + } | |
| 310 | + | |
| 311 | + private async loadLastActivity(): Promise<number> { | |
| 312 | + if (this.lastActivityMs > 0) return this.lastActivityMs; | |
| 313 | + const stored = await this.ctx.storage.get<number>(LAST_ACTIVITY_KEY); | |
| 314 | + this.lastActivityMs = stored ?? 0; | |
| 315 | + return this.lastActivityMs; | |
| 316 | + } | |
| 317 | + | |
| 318 | + private async hasPartyServerName(): Promise<boolean> { | |
| 319 | + if (this.ctx.id.name) return true; | |
| 320 | + const stored = await this.ctx.storage.get<string>(PARTYSERVER_NAME_KEY); | |
| 321 | + return !!stored; | |
| 322 | + } | |
| 323 | + | |
| 324 | + private async cleanupUnaddressableSessionAlarm(): Promise<void> { | |
| 325 | + await Effect.runPromise(this.closeRuntime()); | |
| 326 | + await Effect.runPromise( | |
| 327 | + Effect.all([ | |
| 328 | + Effect.ignore(Effect.tryPromise(() => this.ctx.storage.deleteAlarm())), | |
| 329 | + Effect.ignore(Effect.tryPromise(() => this.ctx.storage.delete(LAST_ACTIVITY_KEY))), | |
| 330 | + ]), | |
| 331 | + ); | |
| 332 | + } | |
| 333 | + | |
| 334 | + private resolveAndStoreSessionMeta(token: McpSessionInit) { | |
| 335 | + const self = this; | |
| 336 | + return Effect.gen(function* () { | |
| 337 | + const resolved = yield* self.resolveSessionMeta(token); | |
| 338 | + const sessionMeta: SessionMeta = { | |
| 339 | + ...resolved, | |
| 340 | + ...(token.webOrigin ? { webOrigin: token.webOrigin } : {}), | |
| 341 | + }; | |
| 342 | + yield* Effect.promise(() => self.saveSessionMeta(sessionMeta)).pipe( | |
| 343 | + Effect.withSpan("mcp.session.save_meta"), | |
| 344 | + ); | |
| 345 | + return sessionMeta; | |
| 346 | + }).pipe(Effect.withSpan("mcp.session.resolve_and_store_meta")); | |
| 347 | + } | |
| 348 | + | |
| 349 | + private recordCauseOnSpan(cause: Cause.Cause<unknown>): Effect.Effect<void> { | |
| 350 | + const errors = Cause.prettyErrors(cause); | |
| 351 | + if (errors.length === 0) return Effect.void; | |
| 352 | + const first = errors[0]; | |
| 353 | + return Effect.annotateCurrentSpan({ | |
| 354 | + "exception.type": first?.name ?? "Error", | |
| 355 | + "exception.message": first?.message ?? "unknown", | |
| 356 | + "exception.stacktrace": Cause.pretty(cause), | |
| 357 | + }); | |
| 358 | + } | |
| 359 | + | |
| 360 | + private withSpanFlush<A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E> { | |
| 361 | + const self = this; | |
| 362 | + return effect.pipe(Effect.ensuring(Effect.promise(() => self.flushTelemetry()))); | |
| 363 | + } | |
| 364 | + | |
| 365 | + private buildRuntime(sessionMeta: SessionMeta, dbHandle: TDbHandle) { | |
| 366 | + const built = sessionMeta.organizationSlug | |
| 367 | + ? this.buildMcpServer(sessionMeta, dbHandle).pipe( | |
| 368 | + Effect.provideService(RequestOrgSlug, { slug: sessionMeta.organizationSlug }), | |
| 369 | + ) | |
| 370 | + : this.buildMcpServer(sessionMeta, dbHandle); | |
| 371 | + return sessionMeta.webOrigin | |
| 372 | + ? built.pipe(Effect.provideService(RequestWebOrigin, { origin: sessionMeta.webOrigin })) | |
| 373 | + : built; | |
| 374 | + } | |
| 375 | + | |
| 376 | + private closeRuntime(): Effect.Effect<void> { | |
| 377 | + const self = this; | |
| 378 | + return Effect.gen(function* () { | |
| 379 | + yield* self.releaseAllPendingApprovalLeases(); | |
| 380 | + if (self.server) { | |
| 381 | + const server = self.server; | |
| 382 | + yield* Effect.promise(() => server.close()).pipe(Effect.ignore); | |
| 383 | + } | |
| 384 | + self.engine = null; | |
| 385 | + if (self.dbHandle) { | |
| 386 | + const dbHandle = self.dbHandle; | |
| 387 | + self.dbHandle = null; | |
| 388 | + yield* Effect.promise(() => Promise.resolve(dbHandle.end())).pipe(Effect.ignore); | |
| 389 | + } | |
| 390 | + self.initialized = false; | |
| 391 | + }); | |
| 392 | + } | |
| 393 | + | |
| 394 | + private ensureRuntimeForApproval(): Effect.Effect<boolean> { | |
| 395 | + const self = this; | |
| 396 | + return Effect.gen(function* () { | |
| 397 | + if (self.initialized && self.engine) return true; | |
| 398 | + | |
| 399 | + const sessionMeta = yield* self.loadSessionMeta(); | |
| 400 | + if (!sessionMeta) return false; | |
| 401 | + | |
| 402 | + yield* self.closeRuntime(); | |
| 403 | + const dbHandle = yield* self.openSessionDbHandle(); | |
| 404 | + const { mcpServer, engine } = yield* self.buildRuntime(sessionMeta, dbHandle); | |
| 405 | + self.dbHandle = dbHandle; | |
| 406 | + self.server = mcpServer; | |
| 407 | + self.engine = engine; | |
| 408 | + self.initialized = true; | |
| 409 | + yield* Effect.promise(() => self.markActivity()).pipe( | |
| 410 | + Effect.withSpan("McpSessionDO.markActivity"), | |
| 411 | + ); | |
| 412 | + return true; | |
| 413 | + }).pipe(Effect.withSpan("McpSessionDO.ensure_runtime_for_approval")); | |
| 414 | + } | |
| 415 | + | |
| 416 | + async init(): Promise<void> { | |
| 417 | + if (this.initialized) return; | |
| 418 | + const props = isSessionProps(this.props) ? this.props : null; | |
| 419 | + if (!props) { | |
| 420 | + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: McpAgent.init is a Promise-only framework hook and props are required before any Effect runtime exists. | |
| 421 | + throw new Error("MCP session props are required"); | |
| 422 | + } | |
| 423 | + const self = this; | |
| 424 | + const program = Effect.gen(function* () { | |
| 425 | + const sessionMeta = yield* self.resolveAndStoreSessionMeta(props.session); | |
| 426 | + const dbHandle = yield* self.openSessionDbHandle(); | |
| 427 | + const { mcpServer, engine } = yield* self.buildRuntime(sessionMeta, dbHandle); | |
| 428 | + self.dbHandle = dbHandle; | |
| 429 | + self.server = mcpServer; | |
| 430 | + self.engine = engine; | |
| 431 | + self.initialized = true; | |
| 432 | + yield* Effect.promise(() => self.markActivity()).pipe( | |
| 433 | + Effect.withSpan("McpSessionDO.markActivity"), | |
| 434 | + ); | |
| 435 | + }).pipe( | |
| 436 | + Effect.tapCause((cause) => | |
| 437 | + Effect.gen(function* () { | |
| 438 | + console.error("[mcp-session] init failed:", Cause.pretty(cause)); | |
| 439 | + self.captureCause(cause); | |
| 440 | + yield* self.recordCauseOnSpan(cause); | |
| 441 | + }), | |
| 442 | + ), | |
| 443 | + Effect.catchCause((cause) => | |
| 444 | + Effect.gen(function* () { | |
| 445 | + yield* Effect.promise(() => self.cleanup()); | |
| 446 | + return yield* Effect.failCause(cause); | |
| 447 | + }), | |
| 448 | + ), | |
| 449 | + Effect.withSpan("McpSessionDO.init", { | |
| 450 | + attributes: { | |
| 451 | + "mcp.auth.organization_id": props?.session.organizationId ?? "", | |
| 452 | + }, | |
| 453 | + }), | |
| 454 | + ); | |
| 455 | + const traced = this.withTelemetry(program, props?.propagation); | |
| 456 | + return Effect.runPromise( | |
| 457 | + traced.pipe( | |
| 458 | + // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: Durable Object init method can only reject its Promise | |
| 459 | + Effect.orDie, | |
| 460 | + (effect) => self.withSpanFlush(effect), | |
| 461 | + ), | |
| 462 | + ); | |
| 463 | + } | |
| 464 | + | |
| 465 | + async validateMcpSessionOwner( | |
| 466 | + identity: McpApprovalOwner, | |
| 467 | + ): Promise<"ok" | "not_found" | "forbidden"> { | |
| 468 | + const self = this; | |
| 469 | + return Effect.runPromise( | |
| 470 | + Effect.gen(function* () { | |
| 471 | + const sessionMeta = yield* self.loadSessionMeta(); | |
| 472 | + if (!sessionMeta) return "not_found" as const; | |
| 473 | + yield* Effect.promise(() => self.markActivity()).pipe( | |
| 474 | + Effect.withSpan("McpSessionDO.markActivity"), | |
| 475 | + ); | |
| 476 | + return identity.accountId === sessionMeta.userId && | |
| 477 | + identity.organizationId === sessionMeta.organizationId | |
| 478 | + ? ("ok" as const) | |
| 479 | + : ("forbidden" as const); | |
| 480 | + }).pipe( | |
| 481 | + Effect.withSpan("McpSessionDO.validateMcpSessionOwner"), | |
| 482 | + // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO RPC exposes Promise results | |
| 483 | + Effect.orDie, | |
| 484 | + ), | |
| 485 | + ); | |
| 486 | + } | |
| 487 | + | |
| 488 | + async getPausedExecutionForApproval( | |
| 489 | + executionId: string, | |
| 490 | + identity: McpApprovalOwner, | |
| 491 | + incoming?: IncomingTraceHeaders, | |
| 492 | + ): Promise<McpSessionApprovalResult> { | |
| 493 | + const self = this; | |
| 494 | + return Effect.runPromise( | |
| 495 | + Effect.gen(function* () { | |
| 496 | + const owner = yield* self.validateApprovalIdentity(identity); | |
| 497 | + if (owner !== "ok") return { status: owner } as const; | |
| 498 | + | |
| 499 | + const restored = yield* self.ensureRuntimeForApproval(); | |
| 500 | + if (!restored || !self.engine) return { status: "not_found" } as const; | |
| 501 | + | |
| 502 | + const paused = yield* self.engine.getPausedExecution(executionId); | |
| 503 | + if (!paused) return { status: "not_found" } as const; | |
| 504 | + | |
| 505 | + const formatted = formatPausedExecution(paused); | |
| 506 | + return { | |
| 507 | + status: "ok" as const, | |
| 508 | + text: formatted.text, | |
| 509 | + structured: formatted.structured, | |
| 510 | + }; | |
| 511 | + }).pipe( | |
| 512 | + Effect.withSpan("McpSessionDO.getPausedExecutionForApproval", { | |
| 513 | + attributes: { "mcp.execution.id": executionId }, | |
| 514 | + }), | |
| 515 | + (eff) => this.withTelemetry(eff, incoming), | |
| 516 | + // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO RPC exposes Promise results | |
| 517 | + Effect.orDie, | |
| 518 | + (eff) => self.withSpanFlush(eff), | |
| 519 | + ), | |
| 520 | + ); | |
| 521 | + } | |
| 522 | + | |
| 523 | + async resumeExecutionForApproval( | |
| 524 | + executionId: string, | |
| 525 | + identity: McpApprovalOwner, | |
| 526 | + response: ResumeResponse, | |
| 527 | + incoming?: IncomingTraceHeaders, | |
| 528 | + ): Promise<McpSessionResumeApprovalResult> { | |
| 529 | + const self = this; | |
| 530 | + return Effect.runPromise( | |
| 531 | + Effect.gen(function* () { | |
| 532 | + const owner = yield* self.validateApprovalIdentity(identity); | |
| 533 | + if (owner !== "ok") return { status: owner } as const; | |
| 534 | + | |
| 535 | + const restored = yield* self.ensureRuntimeForApproval(); | |
| 536 | + if (!restored || !self.engine) return { status: "not_found" } as const; | |
| 537 | + | |
| 538 | + const paused = yield* self.engine.getPausedExecution(executionId); | |
| 539 | + if (!paused) return { status: "not_found" } as const; | |
| 540 | + | |
| 541 | + yield* self.recordApprovalResponse(executionId, response); | |
| 542 | + return resumeApprovalResult(executionId, response); | |
| 543 | + }).pipe( | |
| 544 | + Effect.withSpan("McpSessionDO.resumeExecutionForApproval", { | |
| 545 | + attributes: { "mcp.execution.id": executionId }, | |
| 546 | + }), | |
| 547 | + (eff) => this.withTelemetry(eff, incoming), | |
| 548 | + // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO RPC exposes Promise results | |
| 549 | + Effect.orDie, | |
| 550 | + (eff) => self.withSpanFlush(eff), | |
| 551 | + ), | |
| 552 | + ); | |
| 553 | + } | |
| 554 | + | |
| 555 | + override async destroy(): Promise<void> { | |
| 556 | + await this.cleanup(); | |
| 557 | + await super.destroy(); | |
| 558 | + } | |
| 559 | + | |
| 560 | + private async pausedExecutionCount(): Promise<number> { | |
| 561 | + if (!this.engine) return 0; | |
| 562 | + return Effect.runPromise(this.engine.pausedExecutionCount()); | |
| 563 | + } | |
| 564 | + | |
| 565 | + override async alarm(): Promise<void> { | |
| 566 | + if (!(await this.hasPartyServerName())) { | |
| 567 | + await this.cleanupUnaddressableSessionAlarm(); | |
| 568 | + return; | |
| 569 | + } | |
| 570 | + const lastActivityMs = await this.loadLastActivity(); | |
| 571 | + const idleMs = lastActivityMs > 0 ? Date.now() - lastActivityMs : 0; | |
| 572 | + const pausedExecutionCount = await this.pausedExecutionCount(); | |
| 573 | + const decision = decideSessionAlarm({ idleMs, pausedExecutionCount }); | |
| 574 | + | |
| 575 | + if (decision.kind === "idle_within_timeout") { | |
| 576 | + await super.alarm(); | |
| 577 | + return; | |
| 578 | + } | |
| 579 | + | |
| 580 | + if (decision.kind === "extend_paused_lease") { | |
| 581 | + console.info( | |
| 582 | + JSON.stringify( | |
| 583 | + pausedLeaseExtensionLog({ | |
| 584 | + sessionId: this.sessionId, | |
| 585 | + pausedExecutionCount, | |
| 586 | + idleMs, | |
| 587 | + leaseMs: decision.leaseMs, | |
| 588 | + }), | |
| 589 | + ), | |
| 590 | + ); | |
| 591 | + await this.ctx.storage.setAlarm(Date.now() + decision.leaseMs); | |
| 592 | + return; | |
| 593 | + } | |
| 594 | + | |
| 595 | + await this.destroy(); | |
| 596 | + } | |
| 597 | + | |
| 598 | + private validateApprovalIdentity( | |
| 599 | + identity: McpApprovalOwner, | |
| 600 | + ): Effect.Effect<"ok" | "not_found" | "forbidden"> { | |
| 601 | + const self = this; | |
| 602 | + return Effect.gen(function* () { | |
| 603 | + const sessionMeta = yield* self.loadSessionMeta(); | |
| 604 | + if (!sessionMeta) return "not_found" as const; | |
| 605 | + return identity.accountId === sessionMeta.userId && | |
| 606 | + identity.organizationId === sessionMeta.organizationId | |
| 607 | + ? ("ok" as const) | |
| 608 | + : ("forbidden" as const); | |
| 609 | + }).pipe(Effect.withSpan("mcp.session.validate_approval_identity")); | |
| 610 | + } | |
| 611 | + | |
| 612 | + private startPendingApprovalLease(executionId: string): Effect.Effect<void> { | |
| 613 | + const self = this; | |
| 614 | + return Effect.gen(function* () { | |
| 615 | + if (self.pendingApprovalLeases.has(executionId)) return; | |
| 616 | + | |
| 617 | + yield* Effect.promise(() => self.markActivity()).pipe( | |
| 618 | + Effect.withSpan("McpSessionDO.markActivity"), | |
| 619 | + ); | |
| 620 | + const disposeKeepAlive = yield* Effect.promise(() => self.keepAlive()); | |
| 621 | + const timeout = setTimeout(() => { | |
| 622 | + self.queuePendingApprovalLeaseExpiration(executionId); | |
| 623 | + }, PAUSED_APPROVAL_TIMEOUT_MS); | |
| 624 | + self.pendingApprovalLeases.set(executionId, { disposeKeepAlive, timeout, expiring: false }); | |
| 625 | + }).pipe( | |
| 626 | + Effect.withSpan("McpSessionDO.pending_approval_lease.start", { | |
| 627 | + attributes: { "mcp.execution.id": executionId }, | |
| 628 | + }), | |
| 629 | + Effect.tapCause((cause) => | |
| 630 | + Effect.sync(() => { | |
| 631 | + console.error("[mcp-session] pending approval lease start failed:", Cause.pretty(cause)); | |
| 632 | + self.captureCause(cause); | |
| 633 | + }), | |
| 634 | + ), | |
| 635 | + Effect.ignore, | |
| 636 | + ); | |
| 637 | + } | |
| 638 | + | |
| 639 | + private queuePendingApprovalLeaseStart(executionId: string): void { | |
| 640 | + this.ctx.waitUntil(Effect.runPromise(this.startPendingApprovalLease(executionId))); | |
| 641 | + } | |
| 642 | + | |
| 643 | + private queuePendingApprovalLeaseExpiration(executionId: string): void { | |
| 644 | + this.ctx.waitUntil( | |
| 645 | + Effect.runPromise( | |
| 646 | + this.expirePendingApproval(executionId).pipe( | |
| 647 | + Effect.tapCause((cause) => | |
| 648 | + Effect.sync(() => { | |
| 649 | + console.error( | |
| 650 | + "[mcp-session] pending approval lease expiration failed:", | |
| 651 | + Cause.pretty(cause), | |
| 652 | + ); | |
| 653 | + this.captureCause(cause); | |
| 654 | + }), | |
| 655 | + ), | |
| 656 | + Effect.ignore, | |
| 657 | + ), | |
| 658 | + ), | |
| 659 | + ); | |
| 660 | + } | |
| 661 | + | |
| 662 | + private beginPendingApprovalResume(executionId: string): Effect.Effect<void> { | |
| 663 | + return Effect.sync(() => { | |
| 664 | + const lease = this.pendingApprovalLeases.get(executionId); | |
| 665 | + if (!lease || lease.expiring) return; | |
| 666 | + if (lease.timeout) clearTimeout(lease.timeout); | |
| 667 | + lease.timeout = null; | |
| 668 | + }).pipe( | |
| 669 | + Effect.withSpan("McpSessionDO.pending_approval_lease.begin_resume", { | |
| 670 | + attributes: { "mcp.execution.id": executionId }, | |
| 671 | + }), | |
| 672 | + ); | |
| 673 | + } | |
| 674 | + | |
| 675 | + private finishPendingApprovalResume(executionId: string): Effect.Effect<void> { | |
| 676 | + return this.releasePendingApprovalLease(executionId).pipe( | |
| 677 | + Effect.withSpan("McpSessionDO.pending_approval_lease.finish", { | |
| 678 | + attributes: { "mcp.execution.id": executionId }, | |
| 679 | + }), | |
| 680 | + ); | |
| 681 | + } | |
| 682 | + | |
| 683 | + private releasePendingApprovalLease(executionId: string): Effect.Effect<void> { | |
| 684 | + return Effect.sync(() => { | |
| 685 | + const lease = this.pendingApprovalLeases.get(executionId); | |
| 686 | + if (!lease) return; | |
| 687 | + if (lease.timeout) clearTimeout(lease.timeout); | |
| 688 | + this.pendingApprovalLeases.delete(executionId); | |
| 689 | + lease.disposeKeepAlive(); | |
| 690 | + }); | |
| 691 | + } | |
| 692 | + | |
| 693 | + private releaseAllPendingApprovalLeases(): Effect.Effect<void> { | |
| 694 | + return Effect.sync(() => { | |
| 695 | + for (const executionId of this.pendingApprovalLeases.keys()) { | |
| 696 | + const lease = this.pendingApprovalLeases.get(executionId); | |
| 697 | + if (!lease) continue; | |
| 698 | + if (lease.timeout) clearTimeout(lease.timeout); | |
| 699 | + lease.disposeKeepAlive(); | |
| 700 | + } | |
| 701 | + this.pendingApprovalLeases.clear(); | |
| 702 | + }); | |
| 703 | + } | |
| 704 | + | |
| 705 | + private recordApprovalResponse( | |
| 706 | + executionId: string, | |
| 707 | + response: ResumeResponse, | |
| 708 | + ): Effect.Effect<void> { | |
| 709 | + const self = this; | |
| 710 | + return Effect.gen(function* () { | |
| 711 | + self.approvalResponses.set(executionId, response); | |
| 712 | + yield* Effect.promise(() => self.ctx.storage.put(approvalResponseKey(executionId), response)); | |
| 713 | + const waiter = self.approvalWaiters.get(executionId); | |
| 714 | + if (waiter) yield* Deferred.succeed(waiter, response); | |
| 715 | + }); | |
| 716 | + } | |
| 717 | + | |
| 718 | + private expirePendingApproval(executionId: string): Effect.Effect<void> { | |
| 719 | + const self = this; | |
| 720 | + return Effect.gen(function* () { | |
| 721 | + const lease = self.pendingApprovalLeases.get(executionId); | |
| 722 | + if (!lease || lease.expiring) return; | |
| 723 | + lease.expiring = true; | |
| 724 | + if (lease.timeout) clearTimeout(lease.timeout); | |
| 725 | + lease.timeout = null; | |
| 726 | + if (self.approvalResponses.has(executionId)) return; | |
| 727 | + | |
| 728 | + const response = { | |
| 729 | + action: "decline", | |
| 730 | + content: { reason: "approval_timeout" }, | |
| 731 | + } satisfies ResumeResponse; | |
| 732 | + yield* Effect.sync(() => { | |
| 733 | + console.info(JSON.stringify({ event: "mcp_pending_approval_lease_expire", executionId })); | |
| 734 | + }); | |
| 735 | + yield* self.recordApprovalResponse(executionId, response); | |
| 736 | + if (self.engine && !self.approvalWaiters.has(executionId)) { | |
| 737 | + yield* self.engine.resume(executionId, response).pipe(Effect.ignore); | |
| 738 | + } | |
| 739 | + }).pipe( | |
| 740 | + Effect.ensuring(self.releasePendingApprovalLease(executionId)), | |
| 741 | + Effect.withSpan("McpSessionDO.pending_approval_lease.expire", { | |
| 742 | + attributes: { "mcp.execution.id": executionId }, | |
| 743 | + }), | |
| 744 | + ); | |
| 745 | + } | |
| 746 | + | |
| 747 | + private takeApprovalResponse(executionId: string): Effect.Effect<ResumeResponse | null> { | |
| 748 | + const self = this; | |
| 749 | + return Effect.promise(async () => { | |
| 750 | + const memoryResponse = self.approvalResponses.get(executionId); | |
| 751 | + if (memoryResponse) { | |
| 752 | + self.approvalResponses.delete(executionId); | |
| 753 | + await self.ctx.storage.delete(approvalResponseKey(executionId)); | |
| 754 | + return memoryResponse; | |
| 755 | + } | |
| 756 | + const stored = await self.ctx.storage.get<ResumeResponse>(approvalResponseKey(executionId)); | |
| 757 | + if (!stored) return null; | |
| 758 | + await self.ctx.storage.delete(approvalResponseKey(executionId)); | |
| 759 | + return stored; | |
| 760 | + }); | |
| 761 | + } | |
| 762 | + | |
| 763 | + private waitForApprovalResponse(executionId: string): Effect.Effect<ResumeResponse | null> { | |
| 764 | + const self = this; | |
| 765 | + return Effect.gen(function* () { | |
| 766 | + const existing = yield* self.takeApprovalResponse(executionId); | |
| 767 | + if (existing) return existing; | |
| 768 | + | |
| 769 | + const waiter = | |
| 770 | + self.approvalWaiters.get(executionId) ?? (yield* Deferred.make<ResumeResponse>()); | |
| 771 | + self.approvalWaiters.set(executionId, waiter); | |
| 772 | + yield* Deferred.await(waiter).pipe( | |
| 773 | + Effect.ensuring( | |
| 774 | + Effect.sync(() => { | |
| 775 | + if (self.approvalWaiters.get(executionId) === waiter) { | |
| 776 | + self.approvalWaiters.delete(executionId); | |
| 777 | + } | |
| 778 | + }), | |
| 779 | + ), | |
| 780 | + ); | |
| 781 | + return yield* self.takeApprovalResponse(executionId); | |
| 782 | + }); | |
| 783 | + } | |
| 784 | + | |
| 785 | + private async cleanup(): Promise<void> { | |
| 786 | + await Effect.runPromise(this.closeRuntime()); | |
| 787 | + } | |
| 788 | +} | |
| @@ -1,125 +0,0 @@ | ||
| 1 | −// The same-id JSON-RPC request queue used by the MCP worker transport. | |
| 2 | −// Pure logic with no Cloudflare/agents dependency, in its own module so it | |
| 3 | −// is unit-testable outside workerd. | |
| 4 | −import { Effect, Exit, Match, Option, Predicate } from "effect"; | |
| 5 | − | |
| 6 | −type JsonRpcLike = { | |
| 7 | − readonly id?: unknown; | |
| 8 | − readonly method?: unknown; | |
| 9 | − readonly params?: unknown; | |
| 10 | −}; | |
| 11 | − | |
| 12 | −type ToolCallParamsLike = { | |
| 13 | − readonly name?: unknown; | |
| 14 | − readonly arguments?: unknown; | |
| 15 | −}; | |
| 16 | − | |
| 17 | −type ResumeArgumentsLike = { | |
| 18 | − readonly executionId?: unknown; | |
| 19 | −}; | |
| 20 | − | |
| 21 | −const jsonRpcRequestIdKey = (id: unknown): string | null => | |
| 22 | − Match.value(id).pipe( | |
| 23 | − Match.whenOr( | |
| 24 | − Match.string, | |
| 25 | − Match.number, | |
| 26 | − Match.boolean, | |
| 27 | − (value) => `${typeof value}:${String(value)}`, | |
| 28 | − ), | |
| 29 | − Match.option, | |
| 30 | − Option.getOrNull, | |
| 31 | − ); | |
| 32 | − | |
| 33 | −const jsonRpcRequestQueueKey = (message: JsonRpcLike): string | null => { | |
| 34 | − const idKey = jsonRpcRequestIdKey(message.id); | |
| 35 | − if (!idKey) return null; | |
| 36 | − | |
| 37 | − if (message.method !== "tools/call") return idKey; | |
| 38 | − if (!message.params || typeof message.params !== "object") return idKey; | |
| 39 | − | |
| 40 | − const params = message.params as ToolCallParamsLike; | |
| 41 | − if (params.name !== "resume") return idKey; | |
| 42 | − if (!params.arguments || typeof params.arguments !== "object") return idKey; | |
| 43 | − | |
| 44 | − const args = params.arguments as ResumeArgumentsLike; | |
| 45 | − if (typeof args.executionId !== "string" || args.executionId.length === 0) return idKey; | |
| 46 | − | |
| 47 | − return `${idKey}:tools/call:resume:${args.executionId}`; | |
| 48 | −}; | |
| 49 | − | |
| 50 | −const extractJsonRpcRequestQueueKeys = async (request: Request): Promise<ReadonlyArray<string>> => { | |
| 51 | − if (request.method !== "POST") return []; | |
| 52 | − const contentType = request.headers.get("content-type") ?? ""; | |
| 53 | − if (!contentType.includes("application/json")) return []; | |
| 54 | − | |
| 55 | − const parsed = await Effect.runPromiseExit(Effect.tryPromise(() => request.clone().json())); | |
| 56 | − if (Exit.isFailure(parsed)) { | |
| 57 | − return []; | |
| 58 | − } | |
| 59 | − const messages = Array.isArray(parsed.value) ? parsed.value : [parsed.value]; | |
| 60 | − return messages.flatMap((message) => { | |
| 61 | − if (!message || typeof message !== "object") return []; | |
| 62 | − const rpc = message as JsonRpcLike; | |
| 63 | − if (typeof rpc.method !== "string") return []; | |
| 64 | − const key = jsonRpcRequestQueueKey(rpc); | |
| 65 | − return key ? [key] : []; | |
| 66 | − }); | |
| 67 | −}; | |
| 68 | − | |
| 69 | −// Hard ceiling on how long a same-id JSON-RPC request will wait for an | |
| 70 | −// earlier in-flight one to finish. Stays well under the 180s upstream | |
| 71 | −// client timeout that Claude / Cowork enforce, so a poisoned queue slot | |
| 72 | −// can't block the next request long enough for the client to give up. | |
| 73 | −// If a previous request hasn't released within the budget, we proceed | |
| 74 | −// anyway — at worst the MCP SDK rejects the second reply for a duplicate | |
| 75 | −// id, which is recoverable; a perma-stuck queue is not. | |
| 76 | −export const PREVIOUS_REQUEST_TIMEOUT_MS = 60_000; | |
| 77 | − | |
| 78 | −export class JsonRpcRequestIdQueue { | |
| 79 | − private readonly inFlight = new Map<string, Promise<void>>(); | |
| 80 | − private readonly previousTimeoutMs: number; | |
| 81 | − | |
| 82 | − constructor(options: { readonly previousTimeoutMs?: number } = {}) { | |
| 83 | − this.previousTimeoutMs = options.previousTimeoutMs ?? PREVIOUS_REQUEST_TIMEOUT_MS; | |
| 84 | − } | |
| 85 | − | |
| 86 | − async run<A>(request: Request, run: () => Promise<A>): Promise<A> { | |
| 87 | − const ids = [...new Set(await extractJsonRpcRequestQueueKeys(request))]; | |
| 88 | − if (ids.length === 0) return await run(); | |
| 89 | − | |
| 90 | − const previous = ids.map((id) => this.inFlight.get(id)).filter(Predicate.isNotUndefined); | |
| 91 | − let release!: () => void; | |
| 92 | − const current = new Promise<void>((resolve) => { | |
| 93 | − release = resolve; | |
| 94 | − }); | |
| 95 | − for (const id of ids) { | |
| 96 | − this.inFlight.set(id, current); | |
| 97 | − } | |
| 98 | − | |
| 99 | − // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: promise queue must release in-flight ids after callback completion | |
| 100 | − try { | |
| 101 | − if (previous.length > 0) { | |
| 102 | − const settled = Promise.all( | |
| 103 | − previous.map((p) => Effect.runPromise(Effect.ignore(Effect.tryPromise(() => p)))), | |
| 104 | − ); | |
| 105 | − const timeout = new Promise<"timeout">((resolve) => | |
| 106 | − setTimeout(() => resolve("timeout"), this.previousTimeoutMs), | |
| 107 | − ); | |
| 108 | − const outcome = await Promise.race([settled.then(() => "settled" as const), timeout]); | |
| 109 | − if (outcome === "timeout") { | |
| 110 | − console.warn( | |
| 111 | − `[mcp-worker-transport] previous in-flight request for ids=${JSON.stringify(ids)} did not release within ${this.previousTimeoutMs}ms; proceeding anyway`, | |
| 112 | − ); | |
| 113 | − } | |
| 114 | − } | |
| 115 | − return await run(); | |
| 116 | − } finally { | |
| 117 | − for (const id of ids) { | |
| 118 | − if (this.inFlight.get(id) === current) { | |
| 119 | − this.inFlight.delete(id); | |
| 120 | − } | |
| 121 | − } | |
| 122 | − release(); | |
| 123 | − } | |
| 124 | − } | |
| 125 | −} | |
| @@ -1,263 +0,0 @@ | ||
| 1 | −import { Cause, Data, Effect, Exit, Option, Schema } from "effect"; | |
| 2 | − | |
| 3 | −import { jsonRpcErrorBody } from "@executor-js/host-mcp"; | |
| 4 | − | |
| 5 | −const DEFAULT_SSE_PEEK_TIMEOUT_MS = 10_000; | |
| 6 | − | |
| 7 | −/** Observe a JSON-RPC internal error (-32603) seen on a peeked response. The | |
| 8 | − * host injects this (cloud: Sentry capture; host-cloudflare: console / omit). */ | |
| 9 | −export type OnInternalJsonRpcError = (message: string) => void; | |
| 10 | − | |
| 11 | −class ResponseBodyTimeoutError extends Data.TaggedError("ResponseBodyTimeoutError")<{ | |
| 12 | − readonly timeoutMs: number; | |
| 13 | −}> {} | |
| 14 | − | |
| 15 | −class ResponseBodyReadError extends Data.TaggedError("ResponseBodyReadError") {} | |
| 16 | − | |
| 17 | −const ResponseBodyTimeoutErrorData = Schema.Struct({ | |
| 18 | − _tag: Schema.Literal("ResponseBodyTimeoutError"), | |
| 19 | − timeoutMs: Schema.Number, | |
| 20 | −}); | |
| 21 | −const decodeResponseBodyTimeoutError = Schema.decodeUnknownOption(ResponseBodyTimeoutErrorData); | |
| 22 | − | |
| 23 | −const SandboxOutcomeSchema = Schema.Struct({ | |
| 24 | − status: Schema.optional(Schema.String), | |
| 25 | − error: Schema.optional( | |
| 26 | − Schema.Struct({ | |
| 27 | − kind: Schema.optional(Schema.String), | |
| 28 | − message: Schema.optional(Schema.String), | |
| 29 | − }), | |
| 30 | − ), | |
| 31 | −}); | |
| 32 | − | |
| 33 | −const JsonRpcResponseBodySchema = Schema.Struct({ | |
| 34 | − jsonrpc: Schema.optional(Schema.String), | |
| 35 | − error: Schema.optional( | |
| 36 | − Schema.Struct({ | |
| 37 | − code: Schema.optional(Schema.Number), | |
| 38 | − message: Schema.optional(Schema.String), | |
| 39 | − }), | |
| 40 | − ), | |
| 41 | − result: Schema.optional( | |
| 42 | − Schema.Struct({ | |
| 43 | − isError: Schema.optional(Schema.Boolean), | |
| 44 | − structuredContent: Schema.optional(SandboxOutcomeSchema), | |
| 45 | − }), | |
| 46 | − ), | |
| 47 | −}); | |
| 48 | − | |
| 49 | −const decodeJsonRpcResponseBody = Schema.decodeUnknownOption( | |
| 50 | − Schema.fromJsonString(JsonRpcResponseBodySchema), | |
| 51 | −); | |
| 52 | − | |
| 53 | −type JsonRpcResponseBody = typeof JsonRpcResponseBodySchema.Type; | |
| 54 | − | |
| 55 | −const responseBodyShape = (body: string): string => { | |
| 56 | − const trimmed = body.trimStart(); | |
| 57 | − if (!trimmed) return "empty"; | |
| 58 | − if (trimmed.startsWith("{")) return "json-object"; | |
| 59 | − if (trimmed.startsWith("[")) return "json-array"; | |
| 60 | − if (trimmed.startsWith("event:") || trimmed.startsWith("data:")) return "sse"; | |
| 61 | − if (trimmed.startsWith("<")) return "html-or-xml"; | |
| 62 | − return "other"; | |
| 63 | −}; | |
| 64 | − | |
| 65 | −const parseFirstJsonRpc = (contentType: string, body: string): JsonRpcResponseBody | null => { | |
| 66 | − if (!body) return null; | |
| 67 | − if (contentType.includes("text/event-stream")) { | |
| 68 | − for (const line of body.split(/\r?\n/)) { | |
| 69 | − if (line.startsWith("data:")) { | |
| 70 | − return Option.getOrNull(decodeJsonRpcResponseBody(line.slice(5).trimStart())); | |
| 71 | − } | |
| 72 | − } | |
| 73 | − return null; | |
| 74 | − } | |
| 75 | − if (contentType.includes("application/json")) { | |
| 76 | − return Option.getOrNull(decodeJsonRpcResponseBody(body)); | |
| 77 | − } | |
| 78 | − return null; | |
| 79 | −}; | |
| 80 | − | |
| 81 | −const jsonRpcResponseAttrs = (payload: JsonRpcResponseBody | null): Record<string, unknown> => { | |
| 82 | − if (!payload || payload.jsonrpc !== "2.0") return {}; | |
| 83 | − const attrs: Record<string, unknown> = {}; | |
| 84 | − const err = payload.error; | |
| 85 | − if (err && typeof err === "object") { | |
| 86 | − attrs["mcp.rpc.is_error"] = true; | |
| 87 | − if (typeof err.code === "number") attrs["mcp.rpc.error.code"] = err.code; | |
| 88 | − // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: schema-decoded JSON-RPC error message is protocol telemetry | |
| 89 | − const { message } = err; | |
| 90 | − if (typeof message === "string") { | |
| 91 | − attrs["mcp.rpc.error.message"] = message.slice(0, 500); | |
| 92 | − } | |
| 93 | − } | |
| 94 | − if (payload.result?.isError === true) attrs["mcp.tool.result.is_error"] = true; | |
| 95 | − const structured = payload.result?.structuredContent; | |
| 96 | − if (structured && typeof structured.status === "string") { | |
| 97 | − attrs["mcp.tool.sandbox.status"] = structured.status; | |
| 98 | − if (structured.error?.kind) attrs["mcp.tool.sandbox.error.kind"] = structured.error.kind; | |
| 99 | − const message = structured.error?.["message"]; | |
| 100 | − if (typeof message === "string") { | |
| 101 | − attrs["mcp.tool.sandbox.error.message"] = message.slice(0, 500); | |
| 102 | − } | |
| 103 | − } | |
| 104 | − return attrs; | |
| 105 | −}; | |
| 106 | − | |
| 107 | −const readResponseText = async (response: Response, timeoutMs: number | null): Promise<string> => { | |
| 108 | − if (timeoutMs === null) return await response.text(); | |
| 109 | − | |
| 110 | − const reader = response.body?.getReader(); | |
| 111 | − if (!reader) return ""; | |
| 112 | − | |
| 113 | − const decoder = new TextDecoder(); | |
| 114 | − let timeout: ReturnType<typeof setTimeout> | undefined; | |
| 115 | − const timeoutPromise = new Promise<never>((_, reject) => { | |
| 116 | − timeout = setTimeout(() => { | |
| 117 | − // oxlint-disable-next-line executor/no-promise-catch -- boundary: best-effort stream cancellation inside timeout callback | |
| 118 | − void reader.cancel().catch(() => undefined); | |
| 119 | − // oxlint-disable-next-line executor/no-promise-reject -- boundary: Promise.race timeout adapter for Web ReadableStream | |
| 120 | − reject(new ResponseBodyTimeoutError({ timeoutMs })); | |
| 121 | − }, timeoutMs); | |
| 122 | − }); | |
| 123 | − const readPromise = (async () => { | |
| 124 | − // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Web stream reader cleanup must clear host timeout after success or failure | |
| 125 | − try { | |
| 126 | − let text = ""; | |
| 127 | − for (;;) { | |
| 128 | − const { done, value } = await reader.read(); | |
| 129 | − if (done) return text + decoder.decode(); | |
| 130 | − text += decoder.decode(value, { stream: true }); | |
| 131 | − } | |
| 132 | − } finally { | |
| 133 | − if (timeout) clearTimeout(timeout); | |
| 134 | − } | |
| 135 | − })(); | |
| 136 | − | |
| 137 | − return await Promise.race([readPromise, timeoutPromise]); | |
| 138 | −}; | |
| 139 | − | |
| 140 | −const annotateEmptyResponse = (response: Response, contentType: string) => | |
| 141 | − Effect.annotateCurrentSpan({ | |
| 142 | − "mcp.response.status_code": response.status, | |
| 143 | − "mcp.response.content_type": contentType, | |
| 144 | − "mcp.response.body.shape": "empty", | |
| 145 | − "mcp.response.body.length": 0, | |
| 146 | − "mcp.response.jsonrpc.detected": false, | |
| 147 | − }); | |
| 148 | − | |
| 149 | −const withoutBodyHeaders = (response: Response) => { | |
| 150 | − const headers = new Headers(response.headers); | |
| 151 | − headers.delete("content-type"); | |
| 152 | − headers.delete("content-length"); | |
| 153 | − return new Response(null, { | |
| 154 | − status: response.status, | |
| 155 | − statusText: response.statusText, | |
| 156 | − headers, | |
| 157 | − }); | |
| 158 | −}; | |
| 159 | − | |
| 160 | −const isResponseBodyTimeoutError = (error: unknown) => | |
| 161 | − Option.isSome(decodeResponseBodyTimeoutError(error)); | |
| 162 | − | |
| 163 | −const responsePeekError = (error: unknown): ResponseBodyTimeoutError | ResponseBodyReadError => | |
| 164 | − Option.match(decodeResponseBodyTimeoutError(error), { | |
| 165 | − onNone: () => new ResponseBodyReadError(), | |
| 166 | − onSome: ({ timeoutMs }) => new ResponseBodyTimeoutError({ timeoutMs }), | |
| 167 | − }); | |
| 168 | − | |
| 169 | −const responseReadFailure = (error: unknown) => | |
| 170 | − Effect.gen(function* () { | |
| 171 | − const timedOut = isResponseBodyTimeoutError(error); | |
| 172 | − yield* Effect.annotateCurrentSpan({ | |
| 173 | − "mcp.response.status_code": timedOut ? 504 : 500, | |
| 174 | − "mcp.response.content_type": "application/json", | |
| 175 | − "mcp.response.body.shape": "json-object", | |
| 176 | − "mcp.response.body.length": 0, | |
| 177 | − "mcp.response.jsonrpc.detected": true, | |
| 178 | − "mcp.peek_response.timed_out": timedOut, | |
| 179 | − "mcp.peek_response.error": timedOut ? "ResponseBodyTimeoutError" : "ResponseBodyReadError", | |
| 180 | − }); | |
| 181 | − return jsonRpcErrorBody( | |
| 182 | − timedOut ? 504 : 500, | |
| 183 | − -32001, | |
| 184 | − timedOut | |
| 185 | − ? "Timed out waiting for MCP response - please retry" | |
| 186 | − : "Failed to read MCP response", | |
| 187 | − ); | |
| 188 | − }); | |
| 189 | − | |
| 190 | −const reportInternalJsonRpcError = ( | |
| 191 | − payload: JsonRpcResponseBody | null, | |
| 192 | − onInternalError: OnInternalJsonRpcError | undefined, | |
| 193 | −) => | |
| 194 | − Effect.sync(() => { | |
| 195 | − if (payload?.error?.code !== -32603) return; | |
| 196 | − onInternalError?.(payload.error["message"] ?? "unknown"); | |
| 197 | − }); | |
| 198 | − | |
| 199 | −export interface PeekAndAnnotateOptions { | |
| 200 | − /** Observe a JSON-RPC -32603 internal error (cloud injects Sentry capture). */ | |
| 201 | − readonly onInternalError?: OnInternalJsonRpcError; | |
| 202 | − /** SSE body read timeout (defaults to 10s). */ | |
| 203 | − readonly sseTimeoutMs?: number; | |
| 204 | −} | |
| 205 | − | |
| 206 | −export const peekAndAnnotate = ( | |
| 207 | − response: Response, | |
| 208 | − options: PeekAndAnnotateOptions = {}, | |
| 209 | −): Effect.Effect<Response> => | |
| 210 | − Effect.gen(function* () { | |
| 211 | − const contentType = response.headers.get("content-type") ?? ""; | |
| 212 | − if (response.status === 202) { | |
| 213 | − yield* annotateEmptyResponse(response, contentType); | |
| 214 | − return withoutBodyHeaders(response); | |
| 215 | − } | |
| 216 | − if (!response.body) { | |
| 217 | − yield* annotateEmptyResponse(response, contentType); | |
| 218 | − return response; | |
| 219 | − } | |
| 220 | − | |
| 221 | − const isSseResponse = contentType.includes("text/event-stream"); | |
| 222 | − const timeoutMs = isSseResponse ? (options.sseTimeoutMs ?? DEFAULT_SSE_PEEK_TIMEOUT_MS) : null; | |
| 223 | − const textExit = yield* Effect.exit( | |
| 224 | − Effect.tryPromise({ | |
| 225 | − try: () => readResponseText(response, timeoutMs), | |
| 226 | − catch: responsePeekError, | |
| 227 | − }).pipe( | |
| 228 | − Effect.withSpan("mcp.peek_response", { | |
| 229 | − attributes: { | |
| 230 | − "http.response.content_type": contentType, | |
| 231 | − "http.response.status_code": response.status, | |
| 232 | − "mcp.peek_response.timeout_ms": timeoutMs ?? 0, | |
| 233 | − }, | |
| 234 | − }), | |
| 235 | − ), | |
| 236 | − ); | |
| 237 | − if (Exit.isFailure(textExit)) { | |
| 238 | − const error = Option.getOrElse( | |
| 239 | − Cause.findErrorOption(textExit.cause), | |
| 240 | − () => new ResponseBodyReadError(), | |
| 241 | − ); | |
| 242 | − return yield* responseReadFailure(error); | |
| 243 | − } | |
| 244 | − | |
| 245 | − const text = textExit.value; | |
| 246 | − const payload = parseFirstJsonRpc(contentType, text); | |
| 247 | − yield* Effect.annotateCurrentSpan({ | |
| 248 | − "mcp.response.status_code": response.status, | |
| 249 | − "mcp.response.content_type": contentType, | |
| 250 | − "mcp.response.body.length": text.length, | |
| 251 | − "mcp.response.body.shape": responseBodyShape(text), | |
| 252 | − "mcp.response.jsonrpc.detected": payload?.jsonrpc === "2.0", | |
| 253 | − }); | |
| 254 | − const attrs = jsonRpcResponseAttrs(payload); | |
| 255 | − if (Object.keys(attrs).length > 0) yield* Effect.annotateCurrentSpan(attrs); | |
| 256 | − yield* reportInternalJsonRpcError(payload, options.onInternalError); | |
| 257 | − | |
| 258 | − return new Response(text, { | |
| 259 | − status: response.status, | |
| 260 | − statusText: response.statusText, | |
| 261 | − headers: response.headers, | |
| 262 | − }); | |
| 263 | − }); | |
| @@ -1,31 +0,0 @@ | ||
| 1 | −import type { McpResource } from "@executor-js/host-mcp"; | |
| 2 | − | |
| 3 | −import type { IncomingPropagationHeaders, McpElicitationMode } from "./do-headers"; | |
| 4 | − | |
| 5 | −// --------------------------------------------------------------------------- | |
| 6 | −// The injection seams shared between the worker-side DO dispatcher and the | |
| 7 | −// DO-side base class. A host (cloud / host-cloudflare) supplies its own DO | |
| 8 | −// namespace + runtime builder; everything else is platform-generic. | |
| 9 | −// --------------------------------------------------------------------------- | |
| 10 | − | |
| 11 | −/** What the worker tells the session DO at creation (owner + elicitation mode). */ | |
| 12 | −export interface McpSessionInit { | |
| 13 | − readonly organizationId: string; | |
| 14 | − readonly userId: string; | |
| 15 | − readonly resource: McpResource; | |
| 16 | − readonly elicitationMode: McpElicitationMode; | |
| 17 | − /** Public origin of the create request (`https://host`), so the DO derives a | |
| 18 | − * web base URL zero-config when the host configures no static one. */ | |
| 19 | − readonly webOrigin?: string; | |
| 20 | −} | |
| 21 | − | |
| 22 | −/** | |
| 23 | − * The RPC surface the worker calls on a session-DO stub. The DO base class | |
| 24 | − * (McpSessionDOBase) implements these; a host's concrete DO subclass inherits | |
| 25 | − * them. The worker dispatcher only depends on this interface, not the class. | |
| 26 | − */ | |
| 27 | −export interface McpSessionDOStub { | |
| 28 | − init(meta: McpSessionInit, propagation: IncomingPropagationHeaders): Promise<void>; | |
| 29 | − handleRequest(request: Request): Promise<Response>; | |
| 30 | − clearSession(propagation: IncomingPropagationHeaders): Promise<void>; | |
| 31 | −} | |
| @@ -0,0 +1,58 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + MAX_PAUSED_SESSION_IDLE_MS, | |
| 5 | + PAUSED_EXECUTION_LEASE_MS, | |
| 6 | + SESSION_TIMEOUT_MS, | |
| 7 | + decideSessionAlarm, | |
| 8 | +} from "./session-alarm-policy"; | |
| 9 | + | |
| 10 | +describe("decideSessionAlarm", () => { | |
| 11 | + it("keeps the session within the idle timeout", () => { | |
| 12 | + expect( | |
| 13 | + decideSessionAlarm({ | |
| 14 | + idleMs: SESSION_TIMEOUT_MS - 1, | |
| 15 | + pausedExecutionCount: 0, | |
| 16 | + }), | |
| 17 | + ).toEqual({ kind: "idle_within_timeout" }); | |
| 18 | + }); | |
| 19 | + | |
| 20 | + it("stays within the idle timeout regardless of paused work", () => { | |
| 21 | + expect( | |
| 22 | + decideSessionAlarm({ | |
| 23 | + idleMs: SESSION_TIMEOUT_MS - 1, | |
| 24 | + pausedExecutionCount: 3, | |
| 25 | + }), | |
| 26 | + ).toEqual({ kind: "idle_within_timeout" }); | |
| 27 | + }); | |
| 28 | + | |
| 29 | + it("destroys an idle session with no paused work", () => { | |
| 30 | + expect( | |
| 31 | + decideSessionAlarm({ | |
| 32 | + idleMs: SESSION_TIMEOUT_MS, | |
| 33 | + pausedExecutionCount: 0, | |
| 34 | + }), | |
| 35 | + ).toEqual({ kind: "destroy_idle_session" }); | |
| 36 | + }); | |
| 37 | + | |
| 38 | + it("extends the lease when paused continuations exist", () => { | |
| 39 | + expect( | |
| 40 | + decideSessionAlarm({ | |
| 41 | + idleMs: SESSION_TIMEOUT_MS, | |
| 42 | + pausedExecutionCount: 1, | |
| 43 | + }), | |
| 44 | + ).toEqual({ | |
| 45 | + kind: "extend_paused_lease", | |
| 46 | + leaseMs: PAUSED_EXECUTION_LEASE_MS, | |
| 47 | + }); | |
| 48 | + }); | |
| 49 | + | |
| 50 | + it("destroys a paused session once the lease cap elapses", () => { | |
| 51 | + expect( | |
| 52 | + decideSessionAlarm({ | |
| 53 | + idleMs: MAX_PAUSED_SESSION_IDLE_MS, | |
| 54 | + pausedExecutionCount: 1, | |
| 55 | + }), | |
| 56 | + ).toEqual({ kind: "destroy_idle_session" }); | |
| 57 | + }); | |
| 58 | +}); | |
| @@ -0,0 +1,47 @@ | ||
| 1 | +import { PAUSED_APPROVAL_TIMEOUT_MS } from "@executor-js/host-mcp/tool-server"; | |
| 2 | + | |
| 3 | +/** Idle timeout for MCP sessions with no paused continuations. */ | |
| 4 | +export const SESSION_TIMEOUT_MS = 5 * 60 * 1000; | |
| 5 | + | |
| 6 | +/** Lease extension while paused executions block hibernation (matches browser approval wait). */ | |
| 7 | +export const PAUSED_EXECUTION_LEASE_MS = PAUSED_APPROVAL_TIMEOUT_MS; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * Hard upper bound on idle time before a paused session is torn down regardless | |
| 11 | + * of outstanding paused work. The lease grants a single approval window of grace | |
| 12 | + * past the idle timeout — once it elapses the browser approval wait has already | |
| 13 | + * timed out (see tool-server `waitForBrowserApprovalResponse`), so the paused | |
| 14 | + * execution is no longer resumable and the DO must not keep extending forever. | |
| 15 | + */ | |
| 16 | +export const MAX_PAUSED_SESSION_IDLE_MS = SESSION_TIMEOUT_MS + PAUSED_EXECUTION_LEASE_MS; | |
| 17 | + | |
| 18 | +export type SessionAlarmDecision = | |
| 19 | + | { readonly kind: "idle_within_timeout" } | |
| 20 | + | { readonly kind: "destroy_idle_session" } | |
| 21 | + | { readonly kind: "extend_paused_lease"; readonly leaseMs: number }; | |
| 22 | + | |
| 23 | +export const decideSessionAlarm = (input: { | |
| 24 | + readonly idleMs: number; | |
| 25 | + readonly pausedExecutionCount: number; | |
| 26 | +}): SessionAlarmDecision => { | |
| 27 | + if (input.idleMs < SESSION_TIMEOUT_MS) { | |
| 28 | + return { kind: "idle_within_timeout" }; | |
| 29 | + } | |
| 30 | + if (input.pausedExecutionCount > 0 && input.idleMs < MAX_PAUSED_SESSION_IDLE_MS) { | |
| 31 | + return { kind: "extend_paused_lease", leaseMs: PAUSED_EXECUTION_LEASE_MS }; | |
| 32 | + } | |
| 33 | + return { kind: "destroy_idle_session" }; | |
| 34 | +}; | |
| 35 | + | |
| 36 | +export const pausedLeaseExtensionLog = (input: { | |
| 37 | + readonly sessionId: string; | |
| 38 | + readonly pausedExecutionCount: number; | |
| 39 | + readonly idleMs: number; | |
| 40 | + readonly leaseMs: number; | |
| 41 | +}): Record<string, unknown> => ({ | |
| 42 | + event: "mcp_session_paused_lease_extension", | |
| 43 | + sessionId: input.sessionId, | |
| 44 | + pausedExecutionCount: input.pausedExecutionCount, | |
| 45 | + idleMs: input.idleMs, | |
| 46 | + leaseMs: input.leaseMs, | |
| 47 | +}); | |
| @@ -1,924 +0,0 @@ | ||
| 1 | −// --------------------------------------------------------------------------- | |
| 2 | −// Shared MCP Session Durable Object base — holds the MCP server + engine for ONE | |
| 3 | −// session in a single addressable isolate (the DO id IS the mcp-session-id), so | |
| 4 | −// every follow-up request routes back to the same isolate. Owns ALL the | |
| 5 | −// platform-generic lifecycle (cold-restore from ctx.storage, the inactivity | |
| 6 | −// alarm, owner validation, the JSON-response-mode transport upgrade, the | |
| 7 | −// per-request→per-session span bridge, the browser-approval store). A host | |
| 8 | −// supplies only the seams: openSessionDb / resolveSessionMeta / buildMcpServer, | |
| 9 | −// and optionally withTelemetry / captureCause. cloud and host-cloudflare each | |
| 10 | −// become a ~100-line subclass binding their injected dependencies. | |
| 11 | −// --------------------------------------------------------------------------- | |
| 12 | − | |
| 13 | −import { DurableObject } from "cloudflare:workers"; | |
| 14 | −import { Cause, Deferred, Effect } from "effect"; | |
| 15 | −import type * as Tracer from "effect/Tracer"; | |
| 16 | −import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | |
| 17 | −import type { TransportState } from "agents/mcp"; | |
| 18 | − | |
| 19 | −import { jsonRpcErrorBody } from "@executor-js/host-mcp"; | |
| 20 | −import { formatResumeAcknowledgement } from "@executor-js/host-mcp/browser-approval"; | |
| 21 | −import { RequestOrgSlug, RequestWebOrigin } from "@executor-js/api/server"; | |
| 22 | −import { | |
| 23 | − formatPausedExecution, | |
| 24 | − type ExecutionEngine, | |
| 25 | − type ResumeResponse, | |
| 26 | −} from "@executor-js/execution"; | |
| 27 | − | |
| 28 | −import { makeMcpWorkerTransport, type McpWorkerTransport } from "./worker-transport"; | |
| 29 | −import { | |
| 30 | − INTERNAL_ACCOUNT_ID_HEADER, | |
| 31 | − INTERNAL_ORGANIZATION_ID_HEADER, | |
| 32 | − INTERNAL_RESOURCE_KEY_HEADER, | |
| 33 | − type IncomingPropagationHeaders, | |
| 34 | −} from "./do-headers"; | |
| 35 | −import { mcpResourceKey } from "@executor-js/host-mcp"; | |
| 36 | −import type { McpSessionInit } from "./seams"; | |
| 37 | −import type { McpResource } from "@executor-js/host-mcp"; | |
| 38 | − | |
| 39 | −// --------------------------------------------------------------------------- | |
| 40 | −// Types | |
| 41 | −// --------------------------------------------------------------------------- | |
| 42 | − | |
| 43 | −export type { McpSessionInit } from "./seams"; | |
| 44 | − | |
| 45 | −/** The W3C trace headers the worker forwards to the DO (same shape as the | |
| 46 | − * dispatcher's propagation headers). */ | |
| 47 | −export type IncomingTraceHeaders = IncomingPropagationHeaders; | |
| 48 | − | |
| 49 | −export type McpApprovalOwner = { | |
| 50 | − readonly accountId: string; | |
| 51 | − readonly organizationId: string; | |
| 52 | −}; | |
| 53 | − | |
| 54 | −type McpSessionApprovalErrorResult = | |
| 55 | − | { readonly status: "not_found" } | |
| 56 | − | { readonly status: "forbidden" }; | |
| 57 | − | |
| 58 | −export type McpSessionApprovalResult = | |
| 59 | − | { | |
| 60 | − readonly status: "ok"; | |
| 61 | − readonly text: string; | |
| 62 | − readonly structured: Record<string, unknown>; | |
| 63 | − } | |
| 64 | − | McpSessionApprovalErrorResult; | |
| 65 | − | |
| 66 | −export type McpSessionResumeApprovalResult = | |
| 67 | − | { | |
| 68 | − readonly status: "ok"; | |
| 69 | − readonly executionStatus: "completed" | "paused"; | |
| 70 | − readonly text: string; | |
| 71 | − readonly structured: Record<string, unknown>; | |
| 72 | − readonly isError?: boolean; | |
| 73 | − } | |
| 74 | − | McpSessionApprovalErrorResult; | |
| 75 | − | |
| 76 | −const resumeApprovalResult = ( | |
| 77 | − executionId: string, | |
| 78 | − response: ResumeResponse, | |
| 79 | −): Extract<McpSessionResumeApprovalResult, { readonly status: "ok" }> => ({ | |
| 80 | − status: "ok", | |
| 81 | − executionStatus: "completed", | |
| 82 | − ...formatResumeAcknowledgement(executionId, response), | |
| 83 | − isError: false, | |
| 84 | −}); | |
| 85 | − | |
| 86 | −const HEARTBEAT_MS = 30 * 1000; | |
| 87 | −const SESSION_TIMEOUT_MS = 5 * 60 * 1000; | |
| 88 | −const TRANSPORT_STATE_KEY = "transport"; | |
| 89 | −const SESSION_META_KEY = "session-meta"; | |
| 90 | −const LAST_ACTIVITY_KEY = "last-activity-ms"; | |
| 91 | −const approvalResponseKey = (executionId: string) => `approval-response:${executionId}`; | |
| 92 | − | |
| 93 | −// --------------------------------------------------------------------------- | |
| 94 | −// Helpers | |
| 95 | −// --------------------------------------------------------------------------- | |
| 96 | − | |
| 97 | −// The DO's JSON-RPC error bodies are INNER responses (no CORS): the edge worker | |
| 98 | −// re-wraps them with CORS before they leave the origin, so the canonical | |
| 99 | −// renderer is called with `cors: false` to stay byte-identical to the prior | |
| 100 | −// hand-rolled copy (`content-type: application/json` only). | |
| 101 | −const jsonRpcError = (status: number, code: number, message: string) => | |
| 102 | − jsonRpcErrorBody(status, code, message, { cors: false }); | |
| 103 | − | |
| 104 | −const sessionOwnerMismatch = () => | |
| 105 | − jsonRpcError(403, -32003, "MCP session does not belong to the current bearer"); | |
| 106 | − | |
| 107 | −// --------------------------------------------------------------------------- | |
| 108 | −// Host seams | |
| 109 | −// --------------------------------------------------------------------------- | |
| 110 | − | |
| 111 | −/** | |
| 112 | − * A host's per-session DB handle. The base only disposes it during runtime | |
| 113 | − * teardown; the host's `buildMcpServer` reads its concrete shape (postgres.js | |
| 114 | − * for cloud, the D1 `ExecutorDbHandle` for host-cloudflare). | |
| 115 | − */ | |
| 116 | −export interface SessionDbHandle { | |
| 117 | − readonly end: () => Promise<void> | void; | |
| 118 | −} | |
| 119 | − | |
| 120 | −/** | |
| 121 | − * Resolved session identity + elicitation mode — the output of a host's | |
| 122 | − * `resolveSessionMeta`. Persisted to `ctx.storage` so a cold isolate can | |
| 123 | − * re-validate ownership and rebuild the runtime without re-resolving. | |
| 124 | − */ | |
| 125 | −export interface SessionMeta { | |
| 126 | − readonly organizationId: string; | |
| 127 | − readonly organizationName: string; | |
| 128 | − readonly resource: McpResource; | |
| 129 | − /** The org's URL slug, when the host's `resolveSessionMeta` carried one. | |
| 130 | − * Pins browser-handoff URLs to the right org's console. */ | |
| 131 | − readonly organizationSlug?: string; | |
| 132 | − readonly userId: string; | |
| 133 | − readonly elicitationMode?: "browser" | "model" | "native"; | |
| 134 | − /** Public origin captured at session create — used to derive the runtime's | |
| 135 | − * web base URL when the host configures no static one. */ | |
| 136 | − readonly webOrigin?: string; | |
| 137 | −} | |
| 138 | − | |
| 139 | −/** What a host's `buildMcpServer` seam returns: the connected MCP server plus | |
| 140 | − * the engine the base drives for paused-execution approval flows. */ | |
| 141 | −export interface BuiltMcpServer { | |
| 142 | − readonly mcpServer: McpServer; | |
| 143 | − readonly engine: ExecutionEngine<Cause.YieldableError>; | |
| 144 | −} | |
| 145 | − | |
| 146 | −/** The shared browser-approval store the base wires to its persisted approval | |
| 147 | − * responses; a host hands it to its MCP server when elicitation is "browser". */ | |
| 148 | −export interface BrowserApprovalStore { | |
| 149 | − readonly takeResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>; | |
| 150 | − readonly waitForResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>; | |
| 151 | −} | |
| 152 | − | |
| 153 | −// --------------------------------------------------------------------------- | |
| 154 | −// Durable Object base | |
| 155 | −// --------------------------------------------------------------------------- | |
| 156 | − | |
| 157 | −export abstract class McpSessionDOBase< | |
| 158 | − TDbHandle extends SessionDbHandle = SessionDbHandle, | |
| 159 | −> extends DurableObject { | |
| 160 | − private readonly instanceCreatedAt = Date.now(); | |
| 161 | − private mcpServer: McpServer | null = null; | |
| 162 | − private transport: McpWorkerTransport | null = null; | |
| 163 | − private engine: ExecutionEngine<Cause.YieldableError> | null = null; | |
| 164 | − private initialized = false; | |
| 165 | − private lastActivityMs = 0; | |
| 166 | − private dbHandle: TDbHandle | null = null; | |
| 167 | − private sessionMeta: SessionMeta | null = null; | |
| 168 | − private transportJsonResponseMode: boolean | null = null; | |
| 169 | − private approvalResponses = new Map<string, ResumeResponse>(); | |
| 170 | − private approvalWaiters = new Map<string, Deferred.Deferred<ResumeResponse>>(); | |
| 171 | − // Updated at the start of each `handleRequest` so the host-mcp server's | |
| 172 | − // `parentSpan` getter — invoked by the MCP SDK's deferred tool callbacks | |
| 173 | − // after `transport.handleRequest()` has already returned its streaming | |
| 174 | − // Response — can hand back the request-scoped span. The server is | |
| 175 | − // session-scoped (a fresh server-per-request would lose the elicitation | |
| 176 | − // request → reply correlation that the SDK keeps in-memory on the | |
| 177 | − // `Server` instance), so we have to bridge a per-request value through | |
| 178 | − // a per-session reference. | |
| 179 | − private currentRequestSpan: Tracer.AnySpan | null = null; | |
| 180 | − | |
| 181 | − // ------------------------------------------------------------------------- | |
| 182 | − // Host seams — the ONLY platform-specific surface. A host subclass binds its | |
| 183 | − // DB driver, organization lookup, and MCP-server/engine construction; cloud | |
| 184 | − // adds telemetry + Sentry by overriding the two optional hooks. Everything | |
| 185 | − // else in this class is platform-generic. | |
| 186 | − // ------------------------------------------------------------------------- | |
| 187 | − | |
| 188 | − /** Open the per-session DB handle the runtime holds for this session's | |
| 189 | − * lifetime (postgres.js for cloud, the D1 handle for host-cloudflare). May be | |
| 190 | − * async — host-cloudflare runs an idempotent schema bring-up when it opens. */ | |
| 191 | − protected abstract openSessionDb(): TDbHandle | Promise<TDbHandle>; | |
| 192 | − | |
| 193 | − /** Resolve `openSessionDb` (sync or async) into the Effect chain. */ | |
| 194 | − private openSessionDbHandle(): Effect.Effect<TDbHandle> { | |
| 195 | − return Effect.promise(() => Promise.resolve(this.openSessionDb())); | |
| 196 | − } | |
| 197 | − | |
| 198 | − /** Resolve + validate the session owner into the meta persisted to storage. | |
| 199 | − * Owns its own short-lived DB/services (it runs once per session create). */ | |
| 200 | − protected abstract resolveSessionMeta(token: McpSessionInit): Effect.Effect<SessionMeta>; | |
| 201 | − | |
| 202 | − /** Build the connected MCP server + engine for a resolved session. The host | |
| 203 | − * provides its execution stack + DB layers here; the base owns the transport | |
| 204 | − * and the per-request span / browser-approval wiring exposed below. */ | |
| 205 | − protected abstract buildMcpServer( | |
| 206 | − sessionMeta: SessionMeta, | |
| 207 | − dbHandle: TDbHandle, | |
| 208 | − ): Effect.Effect<BuiltMcpServer>; | |
| 209 | − | |
| 210 | − /** Optional telemetry seam: stitch the DO span under the worker's incoming | |
| 211 | − * trace and install the host's tracer. Default is identity (no telemetry). */ | |
| 212 | − protected withTelemetry<A, E>( | |
| 213 | − effect: Effect.Effect<A, E>, | |
| 214 | − _incoming?: IncomingTraceHeaders, | |
| 215 | − ): Effect.Effect<A, E> { | |
| 216 | − return effect; | |
| 217 | − } | |
| 218 | − | |
| 219 | − /** Optional error seam: report a fatal request cause (cloud → Sentry). */ | |
| 220 | − protected captureCause(_cause: Cause.Cause<unknown>): void {} | |
| 221 | − | |
| 222 | − /** Optional flush seam: force-export buffered spans before the DO RPC | |
| 223 | − * settles. Default is a no-op; cloud overrides it with the tracer | |
| 224 | − * provider's `forceFlush`. Without this, a DO whose Effect dies tears the | |
| 225 | − * isolate down with the SimpleSpanProcessor's in-flight export `fetch` | |
| 226 | − * still pending — so the failing method's OWN spans (and the exception + | |
| 227 | − * stack the Effect tracer records on them) never reach the collector, and | |
| 228 | − * only the worker-side `mcp.do.*` span (which carries just the RPC-boundary | |
| 229 | − * message, no real stack) survives. Flushing in an `ensuring` finalizer — | |
| 230 | − * placed OUTSIDE the span so the span has already ended and the | |
| 231 | − * SimpleSpanProcessor has fired its export — lets the flush await that | |
| 232 | − * export before the RPC rejects. */ | |
| 233 | − protected flushTelemetry(): Promise<void> { | |
| 234 | − return Promise.resolve(); | |
| 235 | − } | |
| 236 | − | |
| 237 | − /** Mirror an error cause onto the active span as top-level attributes. The | |
| 238 | − * Effect OTel tracer already records the cause as an `exception` span | |
| 239 | − * EVENT (`exception.stacktrace` = the full `Cause.pretty` rendering), but | |
| 240 | − * span events are awkward to query in Axiom and the worker-side spans never | |
| 241 | − * see them. Copying type/message/stack onto plain span attributes makes the | |
| 242 | − * failing frame a one-field APL lookup on the span row itself. */ | |
| 243 | − private recordCauseOnSpan(cause: Cause.Cause<unknown>): Effect.Effect<void> { | |
| 244 | − const errors = Cause.prettyErrors(cause); | |
| 245 | − // No `Error` reasons means a pure interruption (or empty cause) — nothing | |
| 246 | − // worth annotating, and we don't want to flag interrupts as failures. | |
| 247 | − if (errors.length === 0) return Effect.void; | |
| 248 | − const first = errors[0]; | |
| 249 | − return Effect.annotateCurrentSpan({ | |
| 250 | − "exception.type": first?.name ?? "Error", | |
| 251 | − "exception.message": first?.message ?? "unknown", | |
| 252 | − "exception.stacktrace": Cause.pretty(cause), | |
| 253 | − }); | |
| 254 | − } | |
| 255 | − | |
| 256 | − /** Flush DO-side spans once the wrapped method (incl. its span) has ended, | |
| 257 | − * whether it succeeded or died. Apply as the OUTERMOST pipe step. */ | |
| 258 | − private withSpanFlush<A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E> { | |
| 259 | − const self = this; | |
| 260 | − return effect.pipe(Effect.ensuring(Effect.promise(() => self.flushTelemetry()))); | |
| 261 | − } | |
| 262 | − | |
| 263 | − /** The session id — equal to this DO's id. */ | |
| 264 | − protected get sessionId(): string { | |
| 265 | − return this.ctx.id.toString(); | |
| 266 | − } | |
| 267 | − | |
| 268 | − /** The request-scoped span for the host-mcp `parentSpan` getter (read by | |
| 269 | − * deferred MCP SDK callbacks after the request Effect has returned). */ | |
| 270 | − protected currentParentSpan(): Tracer.AnySpan | undefined { | |
| 271 | − return this.currentRequestSpan ?? undefined; | |
| 272 | − } | |
| 273 | − | |
| 274 | − /** The browser-approval store wired to this session's persisted responses. */ | |
| 275 | − protected readonly browserApprovalStore: BrowserApprovalStore = { | |
| 276 | − takeResponse: (executionId) => this.takeApprovalResponse(executionId), | |
| 277 | − waitForResponse: (executionId) => this.waitForApprovalResponse(executionId), | |
| 278 | − }; | |
| 279 | − | |
| 280 | − private makeStorage() { | |
| 281 | − return { | |
| 282 | − get: async (): Promise<TransportState | undefined> => { | |
| 283 | − return await this.ctx.storage.get<TransportState>(TRANSPORT_STATE_KEY); | |
| 284 | − }, | |
| 285 | − set: async (state: TransportState): Promise<void> => { | |
| 286 | − await this.ctx.storage.put(TRANSPORT_STATE_KEY, state); | |
| 287 | − }, | |
| 288 | − }; | |
| 289 | − } | |
| 290 | − | |
| 291 | − private loadSessionMeta(): Effect.Effect<SessionMeta | null> { | |
| 292 | − return Effect.promise(async () => { | |
| 293 | − if (this.sessionMeta) return this.sessionMeta; | |
| 294 | − const stored = await this.ctx.storage.get<SessionMeta>(SESSION_META_KEY); | |
| 295 | − this.sessionMeta = stored ?? null; | |
| 296 | − return this.sessionMeta; | |
| 297 | − }).pipe(Effect.withSpan("mcp.session.load_meta")); | |
| 298 | − } | |
| 299 | − | |
| 300 | − private async saveSessionMeta(sessionMeta: SessionMeta): Promise<void> { | |
| 301 | − this.sessionMeta = sessionMeta; | |
| 302 | − await this.ctx.storage.put(SESSION_META_KEY, sessionMeta); | |
| 303 | − } | |
| 304 | − | |
| 305 | − private async markActivity(now = Date.now()): Promise<void> { | |
| 306 | − this.lastActivityMs = now; | |
| 307 | − await Promise.all([ | |
| 308 | − this.ctx.storage.put(LAST_ACTIVITY_KEY, now), | |
| 309 | − this.ctx.storage.setAlarm(now + HEARTBEAT_MS), | |
| 310 | − ]); | |
| 311 | − } | |
| 312 | − | |
| 313 | − private async loadLastActivity(): Promise<number> { | |
| 314 | − if (this.lastActivityMs > 0) return this.lastActivityMs; | |
| 315 | − const stored = await this.ctx.storage.get<number>(LAST_ACTIVITY_KEY); | |
| 316 | − this.lastActivityMs = stored ?? 0; | |
| 317 | − return this.lastActivityMs; | |
| 318 | − } | |
| 319 | − | |
| 320 | − private entryAttrs(methodEnteredAt: number): Record<string, unknown> { | |
| 321 | − const now = Date.now(); | |
| 322 | − return { | |
| 323 | − "mcp.do.instance_age_ms": now - this.instanceCreatedAt, | |
| 324 | − "mcp.do.method_entry_delay_ms": now - methodEnteredAt, | |
| 325 | − "mcp.session.session_id": this.ctx.id.toString(), | |
| 326 | − "mcp.session.initialized": this.initialized, | |
| 327 | − "mcp.session.has_transport": !!this.transport, | |
| 328 | − "mcp.session.has_meta_memory": !!this.sessionMeta, | |
| 329 | − }; | |
| 330 | − } | |
| 331 | − | |
| 332 | − private clearSessionState(): Effect.Effect<void> { | |
| 333 | − return Effect.promise(async () => { | |
| 334 | − this.sessionMeta = null; | |
| 335 | − this.initialized = false; | |
| 336 | − this.lastActivityMs = 0; | |
| 337 | − this.transportJsonResponseMode = null; | |
| 338 | − | |
| 339 | − await Promise.all([ | |
| 340 | − // oxlint-disable-next-line executor/no-promise-catch -- boundary: Durable Object storage cleanup is best-effort after session invalidation | |
| 341 | − this.ctx.storage.delete(TRANSPORT_STATE_KEY).catch(() => false), | |
| 342 | − // oxlint-disable-next-line executor/no-promise-catch -- boundary: Durable Object storage cleanup is best-effort after session invalidation | |
| 343 | − this.ctx.storage.delete(SESSION_META_KEY).catch(() => false), | |
| 344 | − // oxlint-disable-next-line executor/no-promise-catch -- boundary: Durable Object storage cleanup is best-effort after session invalidation | |
| 345 | − this.ctx.storage.delete(LAST_ACTIVITY_KEY).catch(() => false), | |
| 346 | − // oxlint-disable-next-line executor/no-promise-catch -- boundary: Durable Object alarm cleanup is best-effort after session invalidation | |
| 347 | − this.ctx.storage.deleteAlarm().catch(() => undefined), | |
| 348 | − ]); | |
| 349 | − }).pipe(Effect.withSpan("mcp.session.clear_state")); | |
| 350 | − } | |
| 351 | − | |
| 352 | − private createConnectedRuntime( | |
| 353 | − sessionMeta: SessionMeta, | |
| 354 | − options: { readonly dbHandle: TDbHandle; readonly enableJsonResponse?: boolean }, | |
| 355 | − ) { | |
| 356 | − const self = this; | |
| 357 | − return Effect.gen(function* () { | |
| 358 | − // The host builds its MCP server + engine (execution stack, DB layers, | |
| 359 | − // elicitation policy); the base owns the worker transport so JSON-response | |
| 360 | − // mode, the session-id generator, and storage stay identical everywhere. | |
| 361 | − // The session's captured origin is provided here so the host's execution | |
| 362 | − // stack derives a web base URL zero-config (a no-op when it configures one). | |
| 363 | − const built = self.buildMcpServer(sessionMeta, options.dbHandle); | |
| 364 | − const withOrigin = sessionMeta.webOrigin | |
| 365 | − ? built.pipe(Effect.provideService(RequestWebOrigin, { origin: sessionMeta.webOrigin })) | |
| 366 | − : built; | |
| 367 | − // Pin browser-handoff URLs to the session's org slug when captured at | |
| 368 | − // create; absent slug leaves the URL bare for client canonicalization. | |
| 369 | − const { mcpServer, engine } = yield* sessionMeta.organizationSlug | |
| 370 | − ? withOrigin.pipe( | |
| 371 | − Effect.provideService(RequestOrgSlug, { slug: sessionMeta.organizationSlug }), | |
| 372 | − ) | |
| 373 | − : withOrigin; | |
| 374 | − const transport = yield* makeMcpWorkerTransport({ | |
| 375 | − sessionIdGenerator: () => self.sessionId, | |
| 376 | − storage: self.makeStorage(), | |
| 377 | − enableJsonResponse: options.enableJsonResponse, | |
| 378 | − }); | |
| 379 | − self.transportJsonResponseMode = options.enableJsonResponse ?? false; | |
| 380 | − yield* transport.connect(mcpServer); | |
| 381 | − return { mcpServer, transport, engine }; | |
| 382 | − }).pipe(Effect.withSpan("McpSessionDO.createRuntime")); | |
| 383 | − } | |
| 384 | − | |
| 385 | − private closeRuntime(): Effect.Effect<void> { | |
| 386 | − const self = this; | |
| 387 | − return Effect.gen(function* () { | |
| 388 | − if (self.transport) { | |
| 389 | − yield* self.transport.close(); | |
| 390 | − self.transport = null; | |
| 391 | − } | |
| 392 | − if (self.mcpServer) { | |
| 393 | − const mcpServer = self.mcpServer; | |
| 394 | − // oxlint-disable-next-line executor/no-promise-catch -- boundary: MCP SDK close failure is ignored during best-effort runtime teardown | |
| 395 | − yield* Effect.promise(() => mcpServer.close().catch(() => undefined)); | |
| 396 | − self.mcpServer = null; | |
| 397 | − } | |
| 398 | − self.engine = null; | |
| 399 | − if (self.dbHandle) { | |
| 400 | − const dbHandle = self.dbHandle; | |
| 401 | − yield* Effect.promise(() => Promise.resolve(dbHandle.end())); | |
| 402 | − self.dbHandle = null; | |
| 403 | − } | |
| 404 | − self.initialized = false; | |
| 405 | − self.transportJsonResponseMode = null; | |
| 406 | − }).pipe( | |
| 407 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO cleanup has no typed failure surface | |
| 408 | − Effect.orDie, | |
| 409 | − ); | |
| 410 | − } | |
| 411 | − | |
| 412 | − private installRuntime( | |
| 413 | − sessionMeta: SessionMeta, | |
| 414 | − options: { | |
| 415 | − readonly dbHandle: TDbHandle; | |
| 416 | − readonly enableJsonResponse: boolean; | |
| 417 | − }, | |
| 418 | − ) { | |
| 419 | − const self = this; | |
| 420 | − return Effect.gen(function* () { | |
| 421 | − const runtime = yield* self.createConnectedRuntime(sessionMeta, options); | |
| 422 | − self.dbHandle = options.dbHandle; | |
| 423 | − self.mcpServer = runtime.mcpServer; | |
| 424 | − self.transport = runtime.transport; | |
| 425 | − self.engine = runtime.engine; | |
| 426 | − self.initialized = true; | |
| 427 | − }); | |
| 428 | − } | |
| 429 | − | |
| 430 | − private ensureRuntimeForApproval(): Effect.Effect<boolean> { | |
| 431 | − const self = this; | |
| 432 | − return Effect.gen(function* () { | |
| 433 | − if (self.initialized && self.engine) return true; | |
| 434 | − | |
| 435 | − const sessionMeta = yield* self.loadSessionMeta(); | |
| 436 | − if (!sessionMeta) return false; | |
| 437 | − | |
| 438 | − yield* self.closeRuntime(); | |
| 439 | − const dbHandle = yield* self.openSessionDbHandle(); | |
| 440 | − yield* self.installRuntime(sessionMeta, { | |
| 441 | − dbHandle, | |
| 442 | − enableJsonResponse: true, | |
| 443 | − }); | |
| 444 | − yield* Effect.promise(() => self.markActivity()).pipe( | |
| 445 | − Effect.withSpan("McpSessionDO.markActivity"), | |
| 446 | − ); | |
| 447 | − return true; | |
| 448 | − }).pipe( | |
| 449 | − Effect.withSpan("McpSessionDO.ensure_runtime_for_approval"), | |
| 450 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO RPC has no typed Effect channel | |
| 451 | − Effect.orDie, | |
| 452 | − ); | |
| 453 | − } | |
| 454 | − | |
| 455 | − private validateApprovalIdentity( | |
| 456 | − identity: McpApprovalOwner, | |
| 457 | − ): Effect.Effect<"ok" | "not_found" | "forbidden"> { | |
| 458 | − const self = this; | |
| 459 | − return Effect.gen(function* () { | |
| 460 | − const sessionMeta = yield* self.loadSessionMeta(); | |
| 461 | − if (!sessionMeta) return "not_found" as const; | |
| 462 | − | |
| 463 | − const matches = | |
| 464 | − identity.accountId === sessionMeta.userId && | |
| 465 | − identity.organizationId === sessionMeta.organizationId; | |
| 466 | − | |
| 467 | − yield* Effect.annotateCurrentSpan({ | |
| 468 | − "mcp.session.owner_match": matches, | |
| 469 | − }); | |
| 470 | − | |
| 471 | − return matches ? ("ok" as const) : ("forbidden" as const); | |
| 472 | − }).pipe(Effect.withSpan("mcp.session.validate_approval_identity")); | |
| 473 | − } | |
| 474 | − | |
| 475 | − private restoreRuntimeFromStorage(request: Request): Effect.Effect<"restored" | "missing_meta"> { | |
| 476 | − const self = this; | |
| 477 | − return Effect.gen(function* () { | |
| 478 | − if (self.initialized && self.transport) return "restored" as const; | |
| 479 | − | |
| 480 | − const sessionMeta = yield* self.loadSessionMeta(); | |
| 481 | − if (!sessionMeta) { | |
| 482 | − yield* Effect.annotateCurrentSpan({ | |
| 483 | − "mcp.session.restore.outcome": "missing_meta", | |
| 484 | − }); | |
| 485 | − return "missing_meta" as const; | |
| 486 | − } | |
| 487 | − | |
| 488 | − yield* self.closeRuntime(); | |
| 489 | − const dbHandle = yield* self.openSessionDbHandle(); | |
| 490 | − yield* self.installRuntime(sessionMeta, { | |
| 491 | − dbHandle, | |
| 492 | − // GET always returns an SSE stream regardless of this option, but the | |
| 493 | − // session-scoped transport is reused by later POSTs. Keep JSON mode on | |
| 494 | − // across cold restores so a GET reconnect cannot poison future POSTs. | |
| 495 | − enableJsonResponse: true, | |
| 496 | − }); | |
| 497 | − yield* Effect.promise(() => self.markActivity()).pipe( | |
| 498 | − Effect.withSpan("McpSessionDO.markActivity"), | |
| 499 | − ); | |
| 500 | − yield* Effect.annotateCurrentSpan({ | |
| 501 | − "mcp.session.restore.outcome": "restored", | |
| 502 | − }); | |
| 503 | − return "restored" as const; | |
| 504 | − }).pipe( | |
| 505 | − Effect.withSpan("McpSessionDO.restoreRuntime", { | |
| 506 | − attributes: { | |
| 507 | − "mcp.request.method": request.method, | |
| 508 | − "mcp.request.session_id_present": !!request.headers.get("mcp-session-id"), | |
| 509 | − }, | |
| 510 | − }), | |
| 511 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: cold DO restore is re-entered from Promise-only Durable Object method | |
| 512 | − Effect.orDie, | |
| 513 | − ); | |
| 514 | − } | |
| 515 | − | |
| 516 | − private ensureJsonResponseTransportForPost(request: Request): Effect.Effect<void> { | |
| 517 | − const self = this; | |
| 518 | − return Effect.gen(function* () { | |
| 519 | − if (request.method !== "POST" || self.transportJsonResponseMode === true) return; | |
| 520 | − | |
| 521 | − const sessionMeta = yield* self.loadSessionMeta(); | |
| 522 | − if (!sessionMeta) return; | |
| 523 | − | |
| 524 | − yield* self.closeRuntime(); | |
| 525 | − const dbHandle = yield* self.openSessionDbHandle(); | |
| 526 | − yield* self.installRuntime(sessionMeta, { | |
| 527 | − dbHandle, | |
| 528 | − enableJsonResponse: true, | |
| 529 | − }); | |
| 530 | − yield* Effect.annotateCurrentSpan({ | |
| 531 | − "mcp.session.transport_upgraded_json_response": true, | |
| 532 | − }); | |
| 533 | − }).pipe( | |
| 534 | − Effect.withSpan("McpSessionDO.ensureJsonResponseTransportForPost"), | |
| 535 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: transport rebuild is internal DO runtime state | |
| 536 | − Effect.orDie, | |
| 537 | − ); | |
| 538 | − } | |
| 539 | − | |
| 540 | − private validateSessionOwner(request: Request): Effect.Effect<Response | null> { | |
| 541 | − const self = this; | |
| 542 | − return Effect.gen(function* () { | |
| 543 | − const sessionMeta = yield* self.loadSessionMeta(); | |
| 544 | − if (!sessionMeta) return null; | |
| 545 | − | |
| 546 | − const accountId = request.headers.get(INTERNAL_ACCOUNT_ID_HEADER); | |
| 547 | − const organizationId = request.headers.get(INTERNAL_ORGANIZATION_ID_HEADER); | |
| 548 | − const resourceKey = request.headers.get(INTERNAL_RESOURCE_KEY_HEADER); | |
| 549 | − const matches = | |
| 550 | − accountId === sessionMeta.userId && | |
| 551 | − organizationId === sessionMeta.organizationId && | |
| 552 | − resourceKey === mcpResourceKey(sessionMeta.resource); | |
| 553 | − | |
| 554 | − yield* Effect.annotateCurrentSpan({ | |
| 555 | − "mcp.session.owner_match": matches, | |
| 556 | − }); | |
| 557 | − | |
| 558 | − return matches ? null : sessionOwnerMismatch(); | |
| 559 | − }).pipe(Effect.withSpan("mcp.session.validate_owner")); | |
| 560 | − } | |
| 561 | − | |
| 562 | − private resolveAndStoreSessionMeta(token: McpSessionInit) { | |
| 563 | − const self = this; | |
| 564 | − return Effect.gen(function* () { | |
| 565 | − const resolved = yield* self.resolveSessionMeta(token); | |
| 566 | − // Carry the create request's origin onto the persisted meta (the host's | |
| 567 | − // resolveSessionMeta is identity-only and doesn't see it), so a cold | |
| 568 | − // isolate rebuilds the runtime with the same web base URL. | |
| 569 | − const sessionMeta: SessionMeta = { | |
| 570 | − ...resolved, | |
| 571 | − resource: token.resource, | |
| 572 | − ...(token.webOrigin ? { webOrigin: token.webOrigin } : {}), | |
| 573 | − }; | |
| 574 | − yield* Effect.promise(() => self.saveSessionMeta(sessionMeta)).pipe( | |
| 575 | − Effect.withSpan("mcp.session.save_meta"), | |
| 576 | − ); | |
| 577 | − return sessionMeta; | |
| 578 | − }).pipe(Effect.withSpan("mcp.session.resolve_and_store_meta")); | |
| 579 | − } | |
| 580 | − | |
| 581 | − async init(token: McpSessionInit, incoming?: IncomingTraceHeaders): Promise<void> { | |
| 582 | − const methodEnteredAt = Date.now(); | |
| 583 | − if (this.initialized) return; | |
| 584 | − const self = this; | |
| 585 | − return Effect.runPromise( | |
| 586 | − Effect.gen(function* () { | |
| 587 | − yield* Effect.annotateCurrentSpan(self.entryAttrs(methodEnteredAt)); | |
| 588 | − yield* self.doInit(token); | |
| 589 | − }).pipe( | |
| 590 | − Effect.withSpan("McpSessionDO.init", { | |
| 591 | − attributes: { "mcp.auth.organization_id": token.organizationId }, | |
| 592 | − }), | |
| 593 | − (eff) => this.withTelemetry(eff, incoming), | |
| 594 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: Durable Object init method can only reject its Promise | |
| 595 | − Effect.orDie, | |
| 596 | − (eff) => self.withSpanFlush(eff), | |
| 597 | − ), | |
| 598 | − ); | |
| 599 | − } | |
| 600 | − | |
| 601 | − private doInit(token: McpSessionInit) { | |
| 602 | − const self = this; | |
| 603 | − // Single Effect chain so every sub-span (resolveSessionMeta, | |
| 604 | − // createRuntime, createScopedExecutor, createExecutorMcpServer, | |
| 605 | − // transport.connect, storage.setAlarm) lands as a child of | |
| 606 | − // `McpSessionDO.init`. The prior implementation called | |
| 607 | − // `Effect.runPromise` nested inside an async function, which orphaned | |
| 608 | − // each sub-span into its own root trace and made init opaque — | |
| 609 | − // dashboard saw one 2.77s span with nothing under it. | |
| 610 | − return Effect.gen(function* () { | |
| 611 | − const sessionMeta = yield* self.resolveAndStoreSessionMeta(token); | |
| 612 | − | |
| 613 | − self.dbHandle = yield* self.openSessionDbHandle(); | |
| 614 | − // POST responses go out as JSON so `transport.handleRequest()` awaits | |
| 615 | − // every MCP tool callback before resolving — keeps engine spans inside | |
| 616 | − // the outer `handleRequest` Effect's fiber so `currentRequestSpan` is | |
| 617 | − // still set when the host-mcp `parentSpan` getter reads it. With SSE | |
| 618 | − // POSTs the callback fires after `Effect.ensuring` clears the field | |
| 619 | − // and engine spans orphan into new root traces. GET still streams | |
| 620 | − // (the GET handler doesn't consult `enableJsonResponse`). | |
| 621 | − const runtime = yield* self.createConnectedRuntime(sessionMeta, { | |
| 622 | − dbHandle: self.dbHandle, | |
| 623 | − enableJsonResponse: true, | |
| 624 | − }); | |
| 625 | − self.mcpServer = runtime.mcpServer; | |
| 626 | − self.transport = runtime.transport; | |
| 627 | − self.engine = runtime.engine; | |
| 628 | − | |
| 629 | − self.initialized = true; | |
| 630 | − yield* Effect.promise(() => self.markActivity()).pipe( | |
| 631 | − Effect.withSpan("McpSessionDO.markActivity"), | |
| 632 | − ); | |
| 633 | − }).pipe( | |
| 634 | − Effect.tapCause((cause) => | |
| 635 | − Effect.gen(function* () { | |
| 636 | − console.error("[mcp-session] init failed:", Cause.pretty(cause)); | |
| 637 | − // Report to the host's error sink (cloud → Sentry). init() runs on | |
| 638 | − // the prototype, which Sentry's auto-wrap doesn't cover, so capture | |
| 639 | − // here explicitly rather than relying on prototype instrumentation. | |
| 640 | − self.captureCause(cause); | |
| 641 | − // Annotate `McpSessionDO.init` (the active span here — `doInit` opens | |
| 642 | − // none of its own) so the surviving, flushed span names the frame. | |
| 643 | − yield* self.recordCauseOnSpan(cause); | |
| 644 | − }), | |
| 645 | − ), | |
| 646 | − Effect.catchCause((cause) => | |
| 647 | − Effect.gen(function* () { | |
| 648 | − yield* Effect.promise(() => self.cleanup()); | |
| 649 | − return yield* Effect.failCause(cause); | |
| 650 | − }), | |
| 651 | − ), | |
| 652 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: doInit is called only from Promise-only Durable Object init | |
| 653 | − Effect.orDie, | |
| 654 | − ); | |
| 655 | − } | |
| 656 | − | |
| 657 | − async handleRequest(request: Request): Promise<Response> { | |
| 658 | − const methodEnteredAt = Date.now(); | |
| 659 | − // Wrap the dispatch in an Effect span so every DO request — not just | |
| 660 | − // the rare new-session `init()` — shows up in Axiom. Basic attributes | |
| 661 | − // only (method, session-id presence, response status); rich client | |
| 662 | − // fingerprint stays on the edge `mcp.request` span, which shares a | |
| 663 | − // trace_id with this one. | |
| 664 | − const incoming = { | |
| 665 | − traceparent: request.headers.get("traceparent") ?? undefined, | |
| 666 | − tracestate: request.headers.get("tracestate") ?? undefined, | |
| 667 | − baggage: request.headers.get("baggage") ?? undefined, | |
| 668 | − } satisfies IncomingTraceHeaders; | |
| 669 | − const self = this; | |
| 670 | − const program = Effect.gen(function* () { | |
| 671 | − yield* Effect.annotateCurrentSpan(self.entryAttrs(methodEnteredAt)); | |
| 672 | − // Capture the request-entry span so the host-mcp `parentSpan` getter | |
| 673 | − // — fired by deferred MCP SDK callbacks after this Effect has already | |
| 674 | − // returned — anchors engine spans under the same trace. Cleared in a | |
| 675 | − // finalizer so a future request that arrives without a fresh span | |
| 676 | − // doesn't accidentally inherit a stale one. | |
| 677 | − const span = yield* Effect.currentSpan; | |
| 678 | − self.currentRequestSpan = span; | |
| 679 | − | |
| 680 | − return yield* self.dispatchRequest(request).pipe( | |
| 681 | − Effect.tap((response) => | |
| 682 | − Effect.annotateCurrentSpan({ | |
| 683 | − "mcp.response.status_code": response.status, | |
| 684 | − "mcp.response.content_type": response.headers.get("content-type") ?? "", | |
| 685 | − "mcp.transport.enable_json_response": self.transportJsonResponseMode ?? false, | |
| 686 | − }), | |
| 687 | − ), | |
| 688 | − Effect.ensuring( | |
| 689 | − Effect.sync(() => { | |
| 690 | − self.currentRequestSpan = null; | |
| 691 | − }), | |
| 692 | − ), | |
| 693 | − ); | |
| 694 | − }).pipe( | |
| 695 | − // Cold-restore failures (`restoreRuntimeFromStorage` is `orDie`'d and is | |
| 696 | − // NOT under the handled `dispatchAuthorizedRequest` branch) die straight | |
| 697 | − // through here — annotate the handleRequest span with their stack before | |
| 698 | − // it ends so the flushed span names the frame. | |
| 699 | − Effect.tapCause((cause) => self.recordCauseOnSpan(cause)), | |
| 700 | − Effect.withSpan("McpSessionDO.handleRequest", { | |
| 701 | − attributes: { | |
| 702 | − "mcp.request.method": request.method, | |
| 703 | − "mcp.request.session_id_present": !!request.headers.get("mcp-session-id"), | |
| 704 | − }, | |
| 705 | − }), | |
| 706 | − (eff) => this.withTelemetry(eff, incoming), | |
| 707 | − (eff) => self.withSpanFlush(eff), | |
| 708 | − ); | |
| 709 | − return Effect.runPromise(program); | |
| 710 | − } | |
| 711 | − | |
| 712 | − async getPausedExecutionForApproval( | |
| 713 | − executionId: string, | |
| 714 | − identity: McpApprovalOwner, | |
| 715 | − incoming?: IncomingTraceHeaders, | |
| 716 | − ): Promise<McpSessionApprovalResult> { | |
| 717 | − const self = this; | |
| 718 | − return Effect.runPromise( | |
| 719 | − Effect.gen(function* () { | |
| 720 | − const owner = yield* self.validateApprovalIdentity(identity); | |
| 721 | − if (owner !== "ok") return { status: owner } as const; | |
| 722 | − | |
| 723 | − const restored = yield* self.ensureRuntimeForApproval(); | |
| 724 | − if (!restored || !self.engine) return { status: "not_found" } as const; | |
| 725 | − | |
| 726 | − const paused = yield* self.engine.getPausedExecution(executionId); | |
| 727 | − if (!paused) return { status: "not_found" } as const; | |
| 728 | − | |
| 729 | − const formatted = formatPausedExecution(paused); | |
| 730 | − return { | |
| 731 | − status: "ok" as const, | |
| 732 | − text: formatted.text, | |
| 733 | − structured: formatted.structured, | |
| 734 | − }; | |
| 735 | − }).pipe( | |
| 736 | − Effect.withSpan("McpSessionDO.getPausedExecutionForApproval", { | |
| 737 | − attributes: { "mcp.execution.id": executionId }, | |
| 738 | − }), | |
| 739 | − (eff) => this.withTelemetry(eff, incoming), | |
| 740 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO RPC exposes Promise results | |
| 741 | − Effect.orDie, | |
| 742 | − (eff) => self.withSpanFlush(eff), | |
| 743 | − ), | |
| 744 | − ); | |
| 745 | − } | |
| 746 | − | |
| 747 | − private takeApprovalResponse(executionId: string): Effect.Effect<ResumeResponse | null> { | |
| 748 | − const self = this; | |
| 749 | − return Effect.promise(async () => { | |
| 750 | − const memoryResponse = self.approvalResponses.get(executionId); | |
| 751 | − if (memoryResponse) { | |
| 752 | − self.approvalResponses.delete(executionId); | |
| 753 | − await self.ctx.storage.delete(approvalResponseKey(executionId)); | |
| 754 | − return memoryResponse; | |
| 755 | − } | |
| 756 | − const stored = await self.ctx.storage.get<ResumeResponse>(approvalResponseKey(executionId)); | |
| 757 | − if (!stored) return null; | |
| 758 | − await self.ctx.storage.delete(approvalResponseKey(executionId)); | |
| 759 | − return stored; | |
| 760 | − }); | |
| 761 | − } | |
| 762 | − | |
| 763 | − private waitForApprovalResponse(executionId: string): Effect.Effect<ResumeResponse | null> { | |
| 764 | − const self = this; | |
| 765 | − return Effect.gen(function* () { | |
| 766 | − const existing = yield* self.takeApprovalResponse(executionId); | |
| 767 | − if (existing) return existing; | |
| 768 | − | |
| 769 | − const waiter = | |
| 770 | − self.approvalWaiters.get(executionId) ?? (yield* Deferred.make<ResumeResponse>()); | |
| 771 | − self.approvalWaiters.set(executionId, waiter); | |
| 772 | − yield* Deferred.await(waiter).pipe( | |
| 773 | − Effect.ensuring( | |
| 774 | − Effect.sync(() => { | |
| 775 | − if (self.approvalWaiters.get(executionId) === waiter) { | |
| 776 | − self.approvalWaiters.delete(executionId); | |
| 777 | − } | |
| 778 | − }), | |
| 779 | − ), | |
| 780 | − ); | |
| 781 | − return yield* self.takeApprovalResponse(executionId); | |
| 782 | − }); | |
| 783 | − } | |
| 784 | − | |
| 785 | − async resumeExecutionForApproval( | |
| 786 | − executionId: string, | |
| 787 | − identity: McpApprovalOwner, | |
| 788 | − response: ResumeResponse, | |
| 789 | − incoming?: IncomingTraceHeaders, | |
| 790 | − ): Promise<McpSessionResumeApprovalResult> { | |
| 791 | − const self = this; | |
| 792 | − return Effect.runPromise( | |
| 793 | − Effect.gen(function* () { | |
| 794 | − const owner = yield* self.validateApprovalIdentity(identity); | |
| 795 | − if (owner !== "ok") return { status: owner } as const; | |
| 796 | − | |
| 797 | − const restored = yield* self.ensureRuntimeForApproval(); | |
| 798 | − if (!restored || !self.engine) return { status: "not_found" } as const; | |
| 799 | − | |
| 800 | − const paused = yield* self.engine.getPausedExecution(executionId); | |
| 801 | − if (!paused) return { status: "not_found" } as const; | |
| 802 | − | |
| 803 | − self.approvalResponses.set(executionId, response); | |
| 804 | − yield* Effect.promise(() => | |
| 805 | − self.ctx.storage.put(approvalResponseKey(executionId), response), | |
| 806 | − ); | |
| 807 | − const waiter = self.approvalWaiters.get(executionId); | |
| 808 | − if (waiter) yield* Deferred.succeed(waiter, response); | |
| 809 | − return resumeApprovalResult(executionId, response); | |
| 810 | − }).pipe( | |
| 811 | − Effect.withSpan("McpSessionDO.resumeExecutionForApproval", { | |
| 812 | − attributes: { "mcp.execution.id": executionId }, | |
| 813 | − }), | |
| 814 | − (eff) => this.withTelemetry(eff, incoming), | |
| 815 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: DO RPC exposes Promise results | |
| 816 | − Effect.orDie, | |
| 817 | − (eff) => self.withSpanFlush(eff), | |
| 818 | − ), | |
| 819 | − ); | |
| 820 | − } | |
| 821 | − | |
| 822 | − private dispatchRequest(request: Request): Effect.Effect<Response> { | |
| 823 | − const self = this; | |
| 824 | − return Effect.gen(function* () { | |
| 825 | − const ownerError = yield* self.validateSessionOwner(request); | |
| 826 | − if (ownerError) return ownerError; | |
| 827 | − return yield* self.dispatchAuthorizedRequest(request); | |
| 828 | − }); | |
| 829 | − } | |
| 830 | − | |
| 831 | − private dispatchAuthorizedRequest(request: Request): Effect.Effect<Response> { | |
| 832 | − if (!this.initialized || !this.transport) { | |
| 833 | − if (request.method === "DELETE") { | |
| 834 | − return this.clearSessionState().pipe( | |
| 835 | − Effect.as(new Response(null, { status: 204 })), | |
| 836 | − Effect.withSpan("mcp.session.stale_delete"), | |
| 837 | − ); | |
| 838 | − } | |
| 839 | − const self = this; | |
| 840 | − return Effect.gen(function* () { | |
| 841 | − const restored = yield* self.restoreRuntimeFromStorage(request); | |
| 842 | − if (restored === "restored") { | |
| 843 | − return yield* self.dispatchAuthorizedRequest(request); | |
| 844 | − } | |
| 845 | − return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect"); | |
| 846 | − }); | |
| 847 | − } | |
| 848 | − | |
| 849 | − const self = this; | |
| 850 | − return Effect.gen(function* () { | |
| 851 | − yield* self.ensureJsonResponseTransportForPost(request); | |
| 852 | − const transport = self.transport; | |
| 853 | − if (!transport) { | |
| 854 | − return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect"); | |
| 855 | − } | |
| 856 | − | |
| 857 | − yield* Effect.promise(() => self.markActivity()).pipe( | |
| 858 | − Effect.withSpan("McpSessionDO.markActivity"), | |
| 859 | − ); | |
| 860 | − const response = yield* transport.handleRequest(request).pipe( | |
| 861 | − Effect.withSpan("McpSessionDO.transport.handleRequest", { | |
| 862 | − attributes: { | |
| 863 | − "mcp.request.method": request.method, | |
| 864 | − "mcp.request.content_type": request.headers.get("content-type") ?? "", | |
| 865 | − "mcp.request.content_length": request.headers.get("content-length") ?? "", | |
| 866 | − }, | |
| 867 | − }), | |
| 868 | − ); | |
| 869 | − yield* Effect.annotateCurrentSpan({ | |
| 870 | − "mcp.response.status_code": response.status, | |
| 871 | − "mcp.response.content_type": response.headers.get("content-type") ?? "", | |
| 872 | − "mcp.transport.enable_json_response": self.transportJsonResponseMode ?? false, | |
| 873 | − }); | |
| 874 | − if (request.method === "DELETE") { | |
| 875 | − yield* Effect.promise(() => self.cleanup()).pipe(Effect.withSpan("mcp.session.cleanup")); | |
| 876 | − } | |
| 877 | − return response; | |
| 878 | − }).pipe( | |
| 879 | − Effect.catchCause((cause) => | |
| 880 | − Effect.gen(function* () { | |
| 881 | − console.error("[mcp-session] handleRequest error:", Cause.pretty(cause)); | |
| 882 | − self.captureCause(cause); | |
| 883 | − yield* self.recordCauseOnSpan(cause); | |
| 884 | − return jsonRpcError(500, -32603, "Internal error"); | |
| 885 | − }), | |
| 886 | − ), | |
| 887 | − ); | |
| 888 | − } | |
| 889 | − | |
| 890 | − override async alarm(): Promise<void> { | |
| 891 | − const program = Effect.promise(() => this.runAlarm()).pipe( | |
| 892 | − Effect.withSpan("McpSessionDO.alarm"), | |
| 893 | − (eff) => this.withTelemetry(eff), | |
| 894 | − (eff) => this.withSpanFlush(eff), | |
| 895 | − ); | |
| 896 | − return Effect.runPromise(program); | |
| 897 | − } | |
| 898 | − | |
| 899 | − async clearSession(incoming?: IncomingTraceHeaders): Promise<void> { | |
| 900 | − return Effect.runPromise( | |
| 901 | − Effect.promise(() => this.cleanup()).pipe( | |
| 902 | − Effect.withSpan("McpSessionDO.clearSession"), | |
| 903 | − (eff) => this.withTelemetry(eff, incoming), | |
| 904 | − (eff) => this.withSpanFlush(eff), | |
| 905 | − ), | |
| 906 | − ); | |
| 907 | − } | |
| 908 | − | |
| 909 | − private async runAlarm(): Promise<void> { | |
| 910 | − const lastActivityMs = await this.loadLastActivity(); | |
| 911 | − const idleMs = Date.now() - lastActivityMs; | |
| 912 | − if (idleMs >= SESSION_TIMEOUT_MS) { | |
| 913 | − await Effect.runPromise(this.closeRuntime()); | |
| 914 | − await this.ctx.storage.deleteAlarm(); | |
| 915 | − return; | |
| 916 | − } | |
| 917 | − await this.ctx.storage.setAlarm(Date.now() + HEARTBEAT_MS); | |
| 918 | − } | |
| 919 | − | |
| 920 | − private async cleanup(): Promise<void> { | |
| 921 | − await Effect.runPromise(this.closeRuntime()); | |
| 922 | − await Effect.runPromise(this.clearSessionState()); | |
| 923 | − } | |
| 924 | −} | |
| @@ -1,226 +0,0 @@ | ||
| 1 | −// --------------------------------------------------------------------------- | |
| 2 | −// Regression coverage for the transient DO-relocation retry in the worker-side | |
| 3 | −// session-store dispatcher. | |
| 4 | −// | |
| 5 | −// Cloudflare may relocate a live Durable Object between machines; an in-flight | |
| 6 | −// `init` then throws "cannot access storage because object has moved to a | |
| 7 | −// different machine". Before the retry, that rejection became an unrecoverable | |
| 8 | −// defect the envelope rendered as a -32603 — exactly the prod incident on | |
| 9 | −// 2026-06-15 (one `mcp.do.init` failure a reconnect cleared). These tests inject | |
| 10 | −// that rejection at the `McpSessionDOStub` seam and pin the recovery behavior. | |
| 11 | −// --------------------------------------------------------------------------- | |
| 12 | − | |
| 13 | −import { describe, expect, it } from "@effect/vitest"; | |
| 14 | −import { Cause, Effect, Exit } from "effect"; | |
| 15 | − | |
| 16 | −import { | |
| 17 | − McpSessionStore, | |
| 18 | − defaultMcpResource, | |
| 19 | − type McpDispatchResult, | |
| 20 | − type Principal, | |
| 21 | −} from "@executor-js/host-mcp"; | |
| 22 | − | |
| 23 | −import { | |
| 24 | − DO_RELOCATION_MAX_RETRIES, | |
| 25 | − makeDurableObjectMcpSessionStore, | |
| 26 | − type McpSessionInit, | |
| 27 | − type McpSessionDOStub, | |
| 28 | −} from "./session-store"; | |
| 29 | −import { INTERNAL_RESOURCE_KEY_HEADER } from "./do-headers"; | |
| 30 | − | |
| 31 | −const RELOCATION_ERROR = "cannot access storage because object has moved to a different machine"; | |
| 32 | − | |
| 33 | −const TEST_PRINCIPAL: Principal = { | |
| 34 | − accountId: "user_test", | |
| 35 | − organizationId: "org_test", | |
| 36 | − organizationName: "Test Org", | |
| 37 | − email: "test@example.com", | |
| 38 | − name: "Test", | |
| 39 | − avatarUrl: null, | |
| 40 | − roles: ["user"], | |
| 41 | −}; | |
| 42 | − | |
| 43 | −/** A POST with no session id — routes dispatch through the create/init path. */ | |
| 44 | −const initializeRequest = (): Request => | |
| 45 | − new Request("https://mcp.test/mcp", { | |
| 46 | − method: "POST", | |
| 47 | − headers: { authorization: "Bearer x", "content-type": "application/json" }, | |
| 48 | − body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize" }), | |
| 49 | − }); | |
| 50 | − | |
| 51 | −/** The JSON-RPC body the faked DO returns once `init` succeeds. */ | |
| 52 | −const okResponse = (): Response => | |
| 53 | − new Response(JSON.stringify({ jsonrpc: "2.0", id: 0, result: {} }), { | |
| 54 | − status: 200, | |
| 55 | − headers: { "content-type": "application/json" }, | |
| 56 | − }); | |
| 57 | − | |
| 58 | −/** Drive `dispatch` for a create over a store built from one fake stub. */ | |
| 59 | −const dispatchCreate = (stub: McpSessionDOStub): Effect.Effect<McpDispatchResult> => | |
| 60 | − Effect.gen(function* () { | |
| 61 | − const store = yield* McpSessionStore; | |
| 62 | − return yield* store.dispatch({ | |
| 63 | − request: initializeRequest(), | |
| 64 | − principal: TEST_PRINCIPAL, | |
| 65 | − resource: defaultMcpResource, | |
| 66 | − sessionId: null, | |
| 67 | − method: "POST", | |
| 68 | − }); | |
| 69 | − }).pipe( | |
| 70 | − Effect.provide(makeDurableObjectMcpSessionStore({ newStub: () => stub, getStub: () => stub })), | |
| 71 | − ); | |
| 72 | − | |
| 73 | −/** Rendered cause of a failed dispatch (empty for a success) — keeps the | |
| 74 | − * message assertion unconditional, off the `Exit.isFailure` branch. */ | |
| 75 | −const failureText = (exit: Exit.Exit<McpDispatchResult>): string => | |
| 76 | − Exit.isFailure(exit) ? Cause.pretty(exit.cause) : ""; | |
| 77 | − | |
| 78 | −describe("makeDurableObjectMcpSessionStore — DO-relocation retry", () => { | |
| 79 | − it.live("passes the requested MCP resource into session init", () => | |
| 80 | − Effect.gen(function* () { | |
| 81 | − let initMeta: McpSessionInit | undefined; | |
| 82 | − const stub: McpSessionDOStub = { | |
| 83 | − init: (meta) => { | |
| 84 | − initMeta = meta; | |
| 85 | − return Promise.resolve(); | |
| 86 | − }, | |
| 87 | − handleRequest: () => Promise.resolve(okResponse()), | |
| 88 | − clearSession: () => Promise.resolve(), | |
| 89 | − }; | |
| 90 | − | |
| 91 | − const result = yield* Effect.gen(function* () { | |
| 92 | − const store = yield* McpSessionStore; | |
| 93 | − return yield* store.dispatch({ | |
| 94 | − request: initializeRequest(), | |
| 95 | − principal: TEST_PRINCIPAL, | |
| 96 | − resource: { kind: "toolkit", slug: "deploy" }, | |
| 97 | − sessionId: null, | |
| 98 | − method: "POST", | |
| 99 | − }); | |
| 100 | − }).pipe( | |
| 101 | − Effect.provide( | |
| 102 | − makeDurableObjectMcpSessionStore({ newStub: () => stub, getStub: () => stub }), | |
| 103 | − ), | |
| 104 | − ); | |
| 105 | − | |
| 106 | − expect(result).toBeInstanceOf(Response); | |
| 107 | − expect(initMeta?.resource, "the DO session is keyed to the requested resource").toEqual({ | |
| 108 | − kind: "toolkit", | |
| 109 | − slug: "deploy", | |
| 110 | − }); | |
| 111 | − }), | |
| 112 | − ); | |
| 113 | − | |
| 114 | − it.live("stamps the requested MCP resource on forwarded session requests", () => | |
| 115 | − Effect.gen(function* () { | |
| 116 | − let forwardedResourceKey: string | null = null; | |
| 117 | − const stub: McpSessionDOStub = { | |
| 118 | − init: () => Promise.resolve(), | |
| 119 | − handleRequest: (request) => { | |
| 120 | − forwardedResourceKey = request.headers.get(INTERNAL_RESOURCE_KEY_HEADER); | |
| 121 | − return Promise.resolve(okResponse()); | |
| 122 | − }, | |
| 123 | − clearSession: () => Promise.resolve(), | |
| 124 | − }; | |
| 125 | − | |
| 126 | − const result = yield* Effect.gen(function* () { | |
| 127 | − const store = yield* McpSessionStore; | |
| 128 | − return yield* store.dispatch({ | |
| 129 | − request: initializeRequest(), | |
| 130 | − principal: TEST_PRINCIPAL, | |
| 131 | − resource: { kind: "toolkit", slug: "deploy" }, | |
| 132 | − sessionId: "existing-session", | |
| 133 | − method: "POST", | |
| 134 | − }); | |
| 135 | − }).pipe( | |
| 136 | − Effect.provide( | |
| 137 | − makeDurableObjectMcpSessionStore({ newStub: () => stub, getStub: () => stub }), | |
| 138 | − ), | |
| 139 | − ); | |
| 140 | − | |
| 141 | − expect(result).toBeInstanceOf(Response); | |
| 142 | − expect( | |
| 143 | − forwardedResourceKey, | |
| 144 | − "the DO can reject a session id reused on another resource", | |
| 145 | − ).toBe("toolkit:deploy"); | |
| 146 | − }), | |
| 147 | − ); | |
| 148 | − | |
| 149 | − it.live("retries mcp.do.init past a relocation, then returns the DO response", () => | |
| 150 | − Effect.gen(function* () { | |
| 151 | − let initCalls = 0; | |
| 152 | − let handleCalls = 0; | |
| 153 | − const stub: McpSessionDOStub = { | |
| 154 | − init: () => { | |
| 155 | − initCalls += 1; | |
| 156 | − if (initCalls === 1) { | |
| 157 | − // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fake DO stub rejects to simulate a Cloudflare relocation throw | |
| 158 | − return Promise.reject(new Error(RELOCATION_ERROR)); | |
| 159 | − } | |
| 160 | − return Promise.resolve(); | |
| 161 | − }, | |
| 162 | − handleRequest: () => { | |
| 163 | − handleCalls += 1; | |
| 164 | − return Promise.resolve(okResponse()); | |
| 165 | − }, | |
| 166 | − clearSession: () => Promise.resolve(), | |
| 167 | − }; | |
| 168 | − | |
| 169 | − const result = yield* dispatchCreate(stub); | |
| 170 | − | |
| 171 | − expect(initCalls, "init retried once after the relocation").toBe(2); | |
| 172 | − expect(handleCalls, "handleRequest runs once, after init recovers").toBe(1); | |
| 173 | − expect(result).toBeInstanceOf(Response); | |
| 174 | − expect((result as Response).status).toBe(200); | |
| 175 | − }), | |
| 176 | − ); | |
| 177 | − | |
| 178 | − it.live("gives up after the retry budget when relocation never clears", () => | |
| 179 | − Effect.gen(function* () { | |
| 180 | − let initCalls = 0; | |
| 181 | − let handleCalls = 0; | |
| 182 | − const stub: McpSessionDOStub = { | |
| 183 | − init: () => { | |
| 184 | − initCalls += 1; | |
| 185 | − // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fake DO stub rejects to simulate a Cloudflare relocation throw | |
| 186 | − return Promise.reject(new Error(RELOCATION_ERROR)); | |
| 187 | − }, | |
| 188 | − handleRequest: () => { | |
| 189 | − handleCalls += 1; | |
| 190 | − return Promise.resolve(okResponse()); | |
| 191 | − }, | |
| 192 | − clearSession: () => Promise.resolve(), | |
| 193 | − }; | |
| 194 | − | |
| 195 | − const exit = yield* Effect.exit(dispatchCreate(stub)); | |
| 196 | − | |
| 197 | − expect(Exit.isFailure(exit), "exhausted retries surface as a defect").toBe(true); | |
| 198 | − expect(initCalls, "one initial attempt plus the full retry budget").toBe( | |
| 199 | − 1 + DO_RELOCATION_MAX_RETRIES, | |
| 200 | − ); | |
| 201 | − expect(handleCalls, "handleRequest is never reached when init never succeeds").toBe(0); | |
| 202 | − expect(failureText(exit), "the relocation cause is preserved").toContain(RELOCATION_ERROR); | |
| 203 | − }), | |
| 204 | − ); | |
| 205 | − | |
| 206 | − it.live("does not retry a non-relocation failure — surfaces it immediately", () => | |
| 207 | − Effect.gen(function* () { | |
| 208 | − let initCalls = 0; | |
| 209 | − const stub: McpSessionDOStub = { | |
| 210 | − init: () => { | |
| 211 | − initCalls += 1; | |
| 212 | − // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fake DO stub rejects to simulate a non-transient init failure | |
| 213 | − return Promise.reject(new Error("schema spread bug — not transient")); | |
| 214 | − }, | |
| 215 | − handleRequest: () => Promise.resolve(okResponse()), | |
| 216 | − clearSession: () => Promise.resolve(), | |
| 217 | − }; | |
| 218 | − | |
| 219 | − const exit = yield* Effect.exit(dispatchCreate(stub)); | |
| 220 | − | |
| 221 | − expect(Exit.isFailure(exit), "a non-transient rejection still dies").toBe(true); | |
| 222 | − expect(initCalls, "non-relocation errors are not retried").toBe(1); | |
| 223 | − expect(failureText(exit), "the original failure is preserved").toContain("schema spread bug"); | |
| 224 | − }), | |
| 225 | − ); | |
| 226 | −}); | |
| @@ -1,261 +0,0 @@ | ||
| 1 | −// --------------------------------------------------------------------------- | |
| 2 | −// The Durable-Object-backed McpSessionStore — the cross-isolate variant of the | |
| 3 | −// shared host-mcp session seam. Shared by every Cloudflare host (cloud + | |
| 4 | −// host-cloudflare); a host supplies ONLY its DO namespace accessors (newStub | |
| 5 | −// for create, getStub for forward, addressed by the session-id == DO-id) and an | |
| 6 | −// optional internal-error reporter. Everything else — identity-header stamping, | |
| 7 | −// W3C trace propagation, response peeking, the verbatim DO error passthrough — | |
| 8 | −// is platform-generic. | |
| 9 | −// | |
| 10 | −// `dispatch` owns the worker-isolate orchestration: | |
| 11 | −// - sessionId null + POST initialize -> newStub() -> init(meta) + handleRequest | |
| 12 | −// - sessionId present -> getStub(id) -> handleRequest (the DO id routes back to | |
| 13 | −// the same isolate, which is the whole point — sessions survive across the | |
| 14 | −// worker's stateless isolates). | |
| 15 | −// | |
| 16 | −// IMPORTANT: the DO `Response` is returned VERBATIM (incl. its 403 -32003 / | |
| 17 | −// 404 -32001 error bodies) — the envelope's "forbidden"/"not-found" discriminants | |
| 18 | −// would emit different message bytes. The envelope short-circuits bare GET (400) | |
| 19 | −// and DELETE (204) before dispatch, so dispatch only sees create or forward. | |
| 20 | −// --------------------------------------------------------------------------- | |
| 21 | − | |
| 22 | −import { Data, Effect, Layer, Schedule } from "effect"; | |
| 23 | − | |
| 24 | −import { | |
| 25 | − McpSessionStore, | |
| 26 | − type McpDispatchInput, | |
| 27 | − type McpDispatchResult, | |
| 28 | −} from "@executor-js/host-mcp"; | |
| 29 | − | |
| 30 | −import { | |
| 31 | − currentPropagationHeaders, | |
| 32 | − readElicitationMode, | |
| 33 | − withMcpResponseHeaders, | |
| 34 | − withPropagationHeaders, | |
| 35 | − withVerifiedIdentityHeaders, | |
| 36 | − type VerifiedTokenHeaders, | |
| 37 | −} from "./do-headers"; | |
| 38 | −import { peekAndAnnotate, type OnInternalJsonRpcError } from "./response-peek"; | |
| 39 | −import type { McpSessionDOStub } from "./seams"; | |
| 40 | − | |
| 41 | −export type { McpSessionDOStub, McpSessionInit } from "./seams"; | |
| 42 | − | |
| 43 | −export interface DurableObjectStoreConfig { | |
| 44 | − /** Resolve the stub for an existing session id (the id IS the DO id). */ | |
| 45 | − readonly getStub: (sessionId: string) => McpSessionDOStub; | |
| 46 | − /** Mint a fresh session DO stub (a new unique id) for a create. */ | |
| 47 | − readonly newStub: () => McpSessionDOStub; | |
| 48 | − /** Observe a JSON-RPC -32603 the peeker surfaces (cloud: Sentry). */ | |
| 49 | − readonly onInternalError?: OnInternalJsonRpcError; | |
| 50 | −} | |
| 51 | − | |
| 52 | −// --------------------------------------------------------------------------- | |
| 53 | −// Transient DO-relocation retry. | |
| 54 | −// | |
| 55 | −// Cloudflare may relocate a live Durable Object between machines; an in-flight | |
| 56 | −// stub call against the now-stale instance throws "cannot access storage | |
| 57 | −// because object has moved to a different machine". The runtime throws this | |
| 58 | −// BEFORE the object touched storage, so no work and no writes happened on that | |
| 59 | −// instance — a re-issued stub call resolves the DO's new location and succeeds. | |
| 60 | −// (Observed in prod 2026-06-15: a single `mcp.do.init` failure surfaced to the | |
| 61 | −// client as a -32603 "Internal server error" that a reconnect cleared.) | |
| 62 | −// | |
| 63 | −// We retry ONLY this relocation class, and ONLY `init` — init takes a | |
| 64 | −// structured `meta` object with no request body, so replaying it is safe. The | |
| 65 | −// forward path (`handleRequest`) is deliberately excluded: its `Request` body is | |
| 66 | −// a one-shot stream that cannot be re-sent on a retry without buffering. | |
| 67 | −// --------------------------------------------------------------------------- | |
| 68 | − | |
| 69 | −const RELOCATED_DO_ERROR_FRAGMENT = "moved to a different machine"; | |
| 70 | − | |
| 71 | −/** A rejected DO stub call, normalized at the boundary: the original thrown | |
| 72 | − * value in `cause` (so the final `Effect.die` re-raises it verbatim) plus a | |
| 73 | − * pre-computed `relocated` flag so the retry gate never re-inspects `unknown`. */ | |
| 74 | −class DoStubError extends Data.TaggedError("DoStubError")<{ | |
| 75 | − readonly cause: unknown; | |
| 76 | − readonly relocated: boolean; | |
| 77 | −}> {} | |
| 78 | − | |
| 79 | −/** Normalize a rejected DO stub call, flagging the Cloudflare relocation class — | |
| 80 | − * the only safe-to-retry error, and detectable only via the thrown message. */ | |
| 81 | −const toDoStubError = (cause: unknown): DoStubError => | |
| 82 | − new DoStubError({ | |
| 83 | − cause, | |
| 84 | − // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: Cloudflare signals DO relocation only via the thrown Error's message | |
| 85 | − relocated: cause instanceof Error && cause.message.includes(RELOCATED_DO_ERROR_FRAGMENT), | |
| 86 | − }); | |
| 87 | − | |
| 88 | −/** Up to 4 attempts total (1 + 3 retries) at ~25/50/100ms jittered backoff. */ | |
| 89 | −export const DO_RELOCATION_MAX_RETRIES = 3; | |
| 90 | −const DO_RELOCATION_RETRY_SCHEDULE = Schedule.jittered(Schedule.exponential("25 millis")); | |
| 91 | − | |
| 92 | −/** | |
| 93 | − * Run a replayable DO stub call under one `withSpan`, retrying the DO-relocation | |
| 94 | − * error a bounded number of times. A call that still fails after retries (or any | |
| 95 | − * non-relocation rejection) becomes an unrecoverable defect — exactly the prior | |
| 96 | − * `Effect.promise` semantics, which the envelope's top-level `catchCause` renders | |
| 97 | − * as a -32603 and reports to the error reporter (cloud: Sentry). | |
| 98 | − */ | |
| 99 | −const withDoRelocationRetry = <A>( | |
| 100 | − spanName: string, | |
| 101 | − attributes: Record<string, string | number | boolean>, | |
| 102 | − call: () => Promise<A>, | |
| 103 | −): Effect.Effect<A> => | |
| 104 | − Effect.suspend(() => { | |
| 105 | − let attempts = 0; | |
| 106 | − return Effect.tryPromise({ | |
| 107 | − try: () => { | |
| 108 | − attempts += 1; | |
| 109 | − return call(); | |
| 110 | − }, | |
| 111 | − catch: toDoStubError, | |
| 112 | − }).pipe( | |
| 113 | − // Gate retries on BOTH the relocation class and a hard attempt budget. The | |
| 114 | − // `attempts` counter (bumped in `try`) bounds this directly rather than via | |
| 115 | − // `retry`'s `times`, whose interaction with `while` is unreliable here. | |
| 116 | − Effect.retry({ | |
| 117 | − schedule: DO_RELOCATION_RETRY_SCHEDULE, | |
| 118 | − while: (error) => error.relocated && attempts <= DO_RELOCATION_MAX_RETRIES, | |
| 119 | − }), | |
| 120 | − Effect.tap(() => | |
| 121 | − attempts > 1 | |
| 122 | − ? Effect.annotateCurrentSpan({ "mcp.do.relocation_retries": attempts - 1 }) | |
| 123 | − : Effect.void, | |
| 124 | − ), | |
| 125 | − // A call that still fails (or any non-relocation rejection) re-raises the | |
| 126 | − // original thrown value as an unrecoverable defect — the prior | |
| 127 | − // `Effect.promise` semantics the envelope renders as a -32603. | |
| 128 | − // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: dispatch's contract is E=never; an exhausted DO RPC re-raises as the defect the envelope renders as -32603 | |
| 129 | − Effect.catch((error) => Effect.die(error.cause)), | |
| 130 | − ); | |
| 131 | − }).pipe(Effect.withSpan(spanName, { attributes })); | |
| 132 | − | |
| 133 | −/** | |
| 134 | − * Forward a request to an existing session DO. `peek` tees the body for | |
| 135 | − * telemetry on POST/DELETE; GET (SSE) streams through untouched. Returns the DO | |
| 136 | − * `Response` verbatim (incl. its 403 -32003 / 404 -32001 error bodies). | |
| 137 | − */ | |
| 138 | −const forwardToExistingSession = ( | |
| 139 | − config: DurableObjectStoreConfig, | |
| 140 | − request: Request, | |
| 141 | − sessionId: string, | |
| 142 | − peek: boolean, | |
| 143 | − token: VerifiedTokenHeaders, | |
| 144 | − resource: McpDispatchInput["resource"], | |
| 145 | −): Effect.Effect<Response> => | |
| 146 | − Effect.gen(function* () { | |
| 147 | − const stub = config.getStub(sessionId); | |
| 148 | − const propagation = yield* currentPropagationHeaders(request); | |
| 149 | − const propagated = withPropagationHeaders( | |
| 150 | − withVerifiedIdentityHeaders(request, token, resource), | |
| 151 | − propagation, | |
| 152 | − ); | |
| 153 | − const raw = yield* Effect.promise(() => stub.handleRequest(propagated)).pipe( | |
| 154 | − Effect.withSpan("mcp.do.handle_request", { | |
| 155 | − attributes: { | |
| 156 | − "mcp.request.method": request.method, | |
| 157 | − "mcp.request.session_id_present": true, | |
| 158 | − }, | |
| 159 | − }), | |
| 160 | − ); | |
| 161 | − const annotated = peek | |
| 162 | − ? yield* peekAndAnnotate(raw, { onInternalError: config.onInternalError }) | |
| 163 | − : raw; | |
| 164 | − return withMcpResponseHeaders(annotated); | |
| 165 | − }); | |
| 166 | − | |
| 167 | −/** Open a new session DO (POST, no session-id): init then handleRequest. */ | |
| 168 | −const createSession = ( | |
| 169 | − config: DurableObjectStoreConfig, | |
| 170 | − request: Request, | |
| 171 | − token: VerifiedTokenHeaders, | |
| 172 | − resource: McpDispatchInput["resource"], | |
| 173 | −): Effect.Effect<Response> => | |
| 174 | − Effect.gen(function* () { | |
| 175 | − const stub = config.newStub(); | |
| 176 | − const propagation = yield* currentPropagationHeaders(request); | |
| 177 | − yield* withDoRelocationRetry("mcp.do.init", { "mcp.request.session_id_present": false }, () => | |
| 178 | − stub.init( | |
| 179 | − { | |
| 180 | − organizationId: token.organizationId, | |
| 181 | − userId: token.accountId, | |
| 182 | − resource, | |
| 183 | − elicitationMode: readElicitationMode(request), | |
| 184 | − // The public origin the client reached us at — lets the DO derive a web | |
| 185 | − // base URL with no static config (we read the real URL, not a spoofable | |
| 186 | − // forwarded host). | |
| 187 | − webOrigin: new URL(request.url).origin, | |
| 188 | − }, | |
| 189 | − propagation, | |
| 190 | − ), | |
| 191 | − ); | |
| 192 | − const propagated = withPropagationHeaders( | |
| 193 | − withVerifiedIdentityHeaders(request, token, resource), | |
| 194 | − propagation, | |
| 195 | − ); | |
| 196 | − const raw = yield* Effect.promise(() => stub.handleRequest(propagated)).pipe( | |
| 197 | − Effect.withSpan("mcp.do.handle_request", { | |
| 198 | − attributes: { | |
| 199 | − "mcp.request.method": request.method, | |
| 200 | − "mcp.request.session_id_present": false, | |
| 201 | − }, | |
| 202 | − }), | |
| 203 | − ); | |
| 204 | − const annotated = yield* peekAndAnnotate(raw, { onInternalError: config.onInternalError }); | |
| 205 | − return withMcpResponseHeaders(annotated); | |
| 206 | − }); | |
| 207 | − | |
| 208 | −const clearExistingSession = ( | |
| 209 | − config: DurableObjectStoreConfig, | |
| 210 | − sessionId: string, | |
| 211 | − request?: Request, | |
| 212 | −): Effect.Effect<void> => | |
| 213 | − Effect.gen(function* () { | |
| 214 | − const stub = config.getStub(sessionId); | |
| 215 | − // Disposal carries the active request's trace context (tracestate/baggage) | |
| 216 | − // when the envelope forwards the inbound request (the Forbidden-with-session | |
| 217 | − // teardown); otherwise a synthetic request, with traceparent still linking | |
| 218 | − // the span via the active Effect span. | |
| 219 | − const propagation = yield* currentPropagationHeaders( | |
| 220 | − request ?? new Request("https://mcp.invalid/mcp"), | |
| 221 | − ); | |
| 222 | − yield* Effect.promise(() => stub.clearSession(propagation)).pipe( | |
| 223 | − Effect.catchCause(() => Effect.void), | |
| 224 | − Effect.withSpan("mcp.do.clear_session", { | |
| 225 | − attributes: { "mcp.request.session_id_present": true }, | |
| 226 | − }), | |
| 227 | − ); | |
| 228 | − }); | |
| 229 | − | |
| 230 | −/** | |
| 231 | − * Build the `McpSessionStore` seam over a host's DO namespace. Cloud and | |
| 232 | − * host-cloudflare each pass their `getStub`/`newStub` (over `env.MCP_SESSION`); | |
| 233 | − * the dispatch logic is identical. | |
| 234 | − */ | |
| 235 | −export const makeDurableObjectMcpSessionStore = ( | |
| 236 | − config: DurableObjectStoreConfig, | |
| 237 | −): Layer.Layer<McpSessionStore> => | |
| 238 | − Layer.succeed(McpSessionStore)({ | |
| 239 | − dispatch: ({ | |
| 240 | − request, | |
| 241 | − principal, | |
| 242 | − resource, | |
| 243 | − sessionId, | |
| 244 | − }: McpDispatchInput): Effect.Effect<McpDispatchResult> => { | |
| 245 | − const token: VerifiedTokenHeaders = { | |
| 246 | − accountId: principal.accountId, | |
| 247 | − organizationId: principal.organizationId, | |
| 248 | − }; | |
| 249 | − return sessionId | |
| 250 | − ? forwardToExistingSession( | |
| 251 | − config, | |
| 252 | − request, | |
| 253 | − sessionId, | |
| 254 | − request.method !== "GET", | |
| 255 | − token, | |
| 256 | − resource, | |
| 257 | − ) | |
| 258 | − : createSession(config, request, token, resource); | |
| 259 | − }, | |
| 260 | − dispose: (sessionId, request) => clearExistingSession(config, sessionId, request), | |
| 261 | − }); | |
| @@ -1,106 +0,0 @@ | ||
| 1 | −import { WorkerTransport, type WorkerTransportOptions } from "agents/mcp"; | |
| 2 | −import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | |
| 3 | −import { Data, Effect } from "effect"; | |
| 4 | − | |
| 5 | −import { JsonRpcRequestIdQueue } from "./request-id-queue.js"; | |
| 6 | − | |
| 7 | −export { JsonRpcRequestIdQueue, PREVIOUS_REQUEST_TIMEOUT_MS } from "./request-id-queue.js"; | |
| 8 | − | |
| 9 | −export class McpWorkerTransportError extends Data.TaggedError("McpWorkerTransportError")<{ | |
| 10 | − readonly cause: unknown; | |
| 11 | −}> {} | |
| 12 | − | |
| 13 | −export type McpWorkerTransport = Readonly<{ | |
| 14 | − transport: WorkerTransport; | |
| 15 | − connect: (server: McpServer) => Effect.Effect<void, McpWorkerTransportError>; | |
| 16 | − handleRequest: (request: Request) => Effect.Effect<Response, McpWorkerTransportError>; | |
| 17 | − close: () => Effect.Effect<void>; | |
| 18 | −}>; | |
| 19 | − | |
| 20 | −type HandleRequestResult = { | |
| 21 | − readonly response: Response; | |
| 22 | − readonly replacedStandaloneSse: boolean; | |
| 23 | −}; | |
| 24 | − | |
| 25 | −const closeExistingStandaloneSse = (transport: WorkerTransport): boolean => { | |
| 26 | − const streamId = | |
| 27 | − typeof Reflect.get(transport, "standaloneSseStreamId") === "string" | |
| 28 | − ? Reflect.get(transport, "standaloneSseStreamId") | |
| 29 | − : "_GET_stream"; | |
| 30 | − const streamMapping = Reflect.get(transport, "streamMapping"); | |
| 31 | − if (!(streamMapping instanceof Map)) return false; | |
| 32 | − | |
| 33 | − const stream = streamMapping.get(streamId); | |
| 34 | − if (!stream) return false; | |
| 35 | − | |
| 36 | − if ( | |
| 37 | − typeof stream === "object" && | |
| 38 | − stream !== null && | |
| 39 | − typeof Reflect.get(stream, "cleanup") === "function" | |
| 40 | − ) { | |
| 41 | − Reflect.get(stream, "cleanup")(); | |
| 42 | − } | |
| 43 | − streamMapping.delete(streamId); | |
| 44 | − return true; | |
| 45 | −}; | |
| 46 | − | |
| 47 | −const isStandaloneSseGet = (request: Request): boolean => | |
| 48 | − request.method === "GET" && (request.headers.get("accept") ?? "").includes("text/event-stream"); | |
| 49 | − | |
| 50 | −export const makeMcpWorkerTransport = ( | |
| 51 | − options: WorkerTransportOptions, | |
| 52 | −): Effect.Effect<McpWorkerTransport> => | |
| 53 | − Effect.sync(() => { | |
| 54 | − const transport = new WorkerTransport(options); | |
| 55 | − const requestIdQueue = new JsonRpcRequestIdQueue(); | |
| 56 | − | |
| 57 | − const use = <A>(name: string, fn: () => Promise<A>) => | |
| 58 | − Effect.tryPromise({ | |
| 59 | − try: fn, | |
| 60 | − catch: (cause) => new McpWorkerTransportError({ cause }), | |
| 61 | − }).pipe(Effect.withSpan(`mcp.worker_transport.${name}`)); | |
| 62 | − | |
| 63 | − const handleWithStandaloneSseReplacement = async ( | |
| 64 | − request: Request, | |
| 65 | − ): Promise<HandleRequestResult> => { | |
| 66 | − if (!isStandaloneSseGet(request)) { | |
| 67 | − return { | |
| 68 | − response: await transport.handleRequest(request), | |
| 69 | − replacedStandaloneSse: false, | |
| 70 | − }; | |
| 71 | − } | |
| 72 | − | |
| 73 | − const initial = await transport.handleRequest(request); | |
| 74 | − if (initial.status !== 409) { | |
| 75 | − return { response: initial, replacedStandaloneSse: false }; | |
| 76 | − } | |
| 77 | − | |
| 78 | − const replacedStandaloneSse = closeExistingStandaloneSse(transport); | |
| 79 | − return { | |
| 80 | − response: replacedStandaloneSse ? await transport.handleRequest(request) : initial, | |
| 81 | − replacedStandaloneSse, | |
| 82 | − }; | |
| 83 | − }; | |
| 84 | − | |
| 85 | − return { | |
| 86 | − transport, | |
| 87 | − connect: (server: McpServer) => use("connect", () => server.connect(transport)), | |
| 88 | − handleRequest: (request: Request) => | |
| 89 | − Effect.gen(function* () { | |
| 90 | − const result = yield* use("handle_request", () => | |
| 91 | − requestIdQueue.run(request, () => handleWithStandaloneSseReplacement(request)), | |
| 92 | − ); | |
| 93 | − yield* Effect.annotateCurrentSpan({ | |
| 94 | − "mcp.transport.replaced_standalone_sse": result.replacedStandaloneSse, | |
| 95 | − }); | |
| 96 | − return result.response; | |
| 97 | − }), | |
| 98 | − close: () => | |
| 99 | − Effect.ignore( | |
| 100 | − Effect.tryPromise({ | |
| 101 | − try: () => transport.close(), | |
| 102 | − catch: (cause) => new McpWorkerTransportError({ cause }), | |
| 103 | − }), | |
| 104 | − ).pipe(Effect.withSpan("mcp.worker_transport.close")), | |
| 105 | − } satisfies McpWorkerTransport; | |
| 106 | − }).pipe(Effect.withSpan("mcp.worker_transport.make")); | |
| @@ -20,6 +20,7 @@ import { · 19 unmodified lines | ||
| 20 | 20 | McpErrorReporter, |
| 21 | 21 | McpErrorReporterNoop, |
| 22 | 22 | McpServingRoutes, |
| 23 | + McpDiscoveryRoutes, | |
| 23 | 24 | McpSessionStore, |
| 24 | 25 | type McpResource, |
| 25 | 26 | type McpDispatchResult, |
| @@ -195,3 +196,45 @@ it("dispatches toolkit MCP routes with the parsed toolkit resource", async () => · 169 unmodified lines | ||
| 195 | 196 | slug: "deploy", |
| 196 | 197 | }); |
| 197 | 198 | }); |
| 199 | + | |
| 200 | +describe("McpDiscoveryRoutes (discovery-only, no session store)", () => { | |
| 201 | + // Builds with the auth seam ALONE — no McpSessionStore. This is the cloud | |
| 202 | + // shape: the Agent bridge serves /mcp transport, the envelope only publishes | |
| 203 | + // the provider's OAuth discovery docs. If this required McpSessionStore it | |
| 204 | + // would not compile, so the build itself is part of the assertion. | |
| 205 | + const discoveryHandler = (): ((request: Request) => Promise<Response>) => | |
| 206 | + HttpRouter.toWebHandler( | |
| 207 | + McpDiscoveryRoutes.pipe( | |
| 208 | + Layer.provide(AuthProviderLive), | |
| 209 | + Layer.provideMerge(HttpServer.layerServices), | |
| 210 | + ), | |
| 211 | + ).handler; | |
| 212 | + | |
| 213 | + it("serves the provider discovery document on GET", async () => { | |
| 214 | + const handler = discoveryHandler(); | |
| 215 | + const response = await handler(new Request(`https://host.test${DISCOVERY_PATH}`)); | |
| 216 | + expect(response.status).toBe(200); | |
| 217 | + expect((await response.json()) as { ok: boolean }).toEqual({ ok: true }); | |
| 218 | + }); | |
| 219 | + | |
| 220 | + it("answers an OPTIONS preflight on a discovery path with 204 + CORS", async () => { | |
| 221 | + const handler = discoveryHandler(); | |
| 222 | + const response = await handler( | |
| 223 | + new Request(`https://host.test${DISCOVERY_PATH}`, { method: "OPTIONS" }), | |
| 224 | + ); | |
| 225 | + expect(response.status).toBe(204); | |
| 226 | + expect(response.headers.get("access-control-allow-origin")).toBe("*"); | |
| 227 | + }); | |
| 228 | + | |
| 229 | + it("does NOT mount the /mcp transport route", async () => { | |
| 230 | + const handler = discoveryHandler(); | |
| 231 | + const response = await handler( | |
| 232 | + new Request("https://host.test/mcp", { | |
| 233 | + method: "POST", | |
| 234 | + headers: { authorization: "Bearer x", "content-type": "application/json" }, | |
| 235 | + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), | |
| 236 | + }), | |
| 237 | + ); | |
| 238 | + expect(response.status).toBe(404); | |
| 239 | + }); | |
| 240 | +}); | |
| @@ -286,3 +286,27 @@ export const McpServingRoutes = HttpRouter.use((router) => · 285 unmodified lines | ||
| 286 | 286 | yield* router.add("*", TOOLKIT_MCP_PATH, toolkitMcpRoute); |
| 287 | 287 | }), |
| 288 | 288 | ); |
| 289 | + | |
| 290 | +/** | |
| 291 | + * The discovery-only subset of {@link McpServingRoutes}: the provider-declared | |
| 292 | + * `/.well-known/oauth-*` GET routes plus their CORS preflight, and NOTHING on | |
| 293 | + * `/mcp`. Requires only the {@link McpAuthProvider} seam, not {@link McpSessionStore}. | |
| 294 | + * | |
| 295 | + * For a host whose `/mcp` transport is served outside this envelope (the | |
| 296 | + * Cloudflare Agent bridge intercepts `/mcp` before the app handler) but that | |
| 297 | + * still needs to publish its OAuth metadata docs. Such a host wires the `auth` | |
| 298 | + * seam without a `sessions` seam. | |
| 299 | + */ | |
| 300 | +export const McpDiscoveryRoutes = HttpRouter.use((router) => | |
| 301 | + Effect.gen(function* () { | |
| 302 | + const auth = yield* McpAuthProvider; | |
| 303 | + for (const route of auth.discoveryRoutes) { | |
| 304 | + yield* router.add("GET", route.path, discoveryRoute(route.handler)); | |
| 305 | + yield* router.add( | |
| 306 | + "OPTIONS", | |
| 307 | + route.path, | |
| 308 | + Effect.sync(() => HttpServerResponse.raw(corsPreflightResponse())), | |
| 309 | + ); | |
| 310 | + } | |
| 311 | + }), | |
| 312 | +); | |
| @@ -36,4 +36,4 @@ export { · 35 unmodified lines | ||
| 36 | 36 | type McpResource, |
| 37 | 37 | } from "./seams"; |
| 38 | 38 | |
| 39 | −export { McpServingRoutes, jsonRpcErrorBody } from "./envelope"; | |
| 39 | +export { McpServingRoutes, McpDiscoveryRoutes, jsonRpcErrorBody } from "./envelope"; | |
| @@ -38,6 +38,8 @@ const makeStubEngine = <E extends Cause.YieldableError = never>(overrides: { · 37 unmodified lines | ||
| 38 | 38 | (() => Effect.succeed({ status: "completed", result: { result: "default" } })), |
| 39 | 39 | resume: overrides.resume ?? (() => Effect.succeed(null)), |
| 40 | 40 | getPausedExecution: () => Effect.succeed(null), |
| 41 | + pausedExecutionCount: () => Effect.succeed(0), | |
| 42 | + hasPausedExecutions: () => Effect.succeed(false), | |
| 41 | 43 | getDescription: Effect.succeed(overrides.description ?? "test executor"), |
| 42 | 44 | }); |
| 43 | 45 | |
| @@ -46,7 +48,10 @@ const withClient = async <E extends Cause.YieldableError>( · 2 unmodified lines | ||
| 46 | 48 | engine: ExecutionEngine<E>, |
| 47 | 49 | capabilities: ClientCapabilities, |
| 48 | 50 | fn: (client: Client) => Promise<void>, |
| 49 | − config?: Pick<ExecutorMcpServerConfig<E>, "debug" | "elicitationMode" | "browserApprovalStore">, | |
| 51 | + config?: Pick< | |
| 52 | + ExecutorMcpServerConfig<E>, | |
| 53 | + "debug" | "elicitationMode" | "browserApprovalStore" | "pausedExecutionHooks" | |
| 54 | + >, | |
| 50 | 55 | ) => { |
| 51 | 56 | const mcpServer = await Effect.runPromise(createExecutorMcpServer({ engine, ...config })); |
| 52 | 57 | const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| @@ -1119,6 +1124,49 @@ describe("MCP host server — client without elicitation (pause/resume)", () => · 1066 unmodified lines | ||
| 1119 | 1124 | ); |
| 1120 | 1125 | }); |
| 1121 | 1126 | |
| 1127 | + it("reports pause and resume lifecycle boundaries", async () => { | |
| 1128 | + const events: string[] = []; | |
| 1129 | + const approval = FormElicitation.make({ message: "Approve", requestedSchema: {} }); | |
| 1130 | + const engine = makeStubEngine({ | |
| 1131 | + executeWithPause: () => Effect.succeed(makePausedResult("exec_1", approval)), | |
| 1132 | + resume: () => Effect.succeed(makePausedResult("exec_2", approval)), | |
| 1133 | + }); | |
| 1134 | + | |
| 1135 | + await withClient( | |
| 1136 | + engine, | |
| 1137 | + NO_CAPS, | |
| 1138 | + async (client) => { | |
| 1139 | + await client.callTool({ | |
| 1140 | + name: "execute", | |
| 1141 | + arguments: { code: "pause" }, | |
| 1142 | + }); | |
| 1143 | + expect(events).toEqual(["pause:exec_1"]); | |
| 1144 | + | |
| 1145 | + await client.callTool({ | |
| 1146 | + name: "resume", | |
| 1147 | + arguments: { executionId: "exec_1", action: "accept", content: "{}" }, | |
| 1148 | + }); | |
| 1149 | + expect(events).toEqual([ | |
| 1150 | + "pause:exec_1", | |
| 1151 | + "resume-start:exec_1", | |
| 1152 | + "resume-settle:exec_1", | |
| 1153 | + "pause:exec_2", | |
| 1154 | + ]); | |
| 1155 | + }, | |
| 1156 | + { | |
| 1157 | + elicitationMode: { mode: "model" }, | |
| 1158 | + pausedExecutionHooks: { | |
| 1159 | + onExecutionPaused: (executionId) => | |
| 1160 | + Effect.sync(() => events.push(`pause:${executionId}`)), | |
| 1161 | + onResumeStarted: (executionId) => | |
| 1162 | + Effect.sync(() => events.push(`resume-start:${executionId}`)), | |
| 1163 | + onResumeSettled: (executionId) => | |
| 1164 | + Effect.sync(() => events.push(`resume-settle:${executionId}`)), | |
| 1165 | + }, | |
| 1166 | + }, | |
| 1167 | + ); | |
| 1168 | + }); | |
| 1169 | + | |
| 1122 | 1170 | it("resume tool passes parsed content to engine", async () => { |
| 1123 | 1171 | let receivedContent: Record<string, unknown> | undefined; |
| 1124 | 1172 | const engine = makeStubEngine({ |
| @@ -1,4 +1,4 @@ | ||
| 1 | −import { Effect, Match, Option, Schema } from "effect"; | |
| 1 | +import { Duration, Effect, Match, Option, Schema } from "effect"; | |
| 2 | 2 | import * as Cause from "effect/Cause"; |
| 3 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 4 | 4 | import { ContentBlockSchema, type ContentBlock } from "@modelcontextprotocol/sdk/types.js"; |
| @@ -91,6 +91,12 @@ type SharedMcpServerConfig = { · 86 unmodified lines | ||
| 91 | 91 | readonly mode: "native"; |
| 92 | 92 | }; |
| 93 | 93 | readonly browserApprovalStore?: BrowserApprovalStore; |
| 94 | + /** | |
| 95 | + * Host-owned lifecycle for paused executions. The MCP server reports pause | |
| 96 | + * boundaries; the host decides whether that means a keepAlive lease, browser | |
| 97 | + * wait, durable record, or no-op. | |
| 98 | + */ | |
| 99 | + readonly pausedExecutionHooks?: PausedExecutionHooks; | |
| 94 | 100 | }; |
| 95 | 101 | |
| 96 | 102 | export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> = |
| @@ -104,6 +110,15 @@ export type BrowserApprovalStore = { · 7 unmodified lines | ||
| 104 | 110 | readonly waitForResponse?: (executionId: string) => Effect.Effect<ResumeResponse | null>; |
| 105 | 111 | }; |
| 106 | 112 | |
| 113 | +export const PAUSED_APPROVAL_TIMEOUT_MS = 4 * 60 * 1000; | |
| 114 | +const BROWSER_APPROVAL_WAIT_TIMEOUT_MS = PAUSED_APPROVAL_TIMEOUT_MS + 1000; | |
| 115 | + | |
| 116 | +export type PausedExecutionHooks = { | |
| 117 | + readonly onExecutionPaused?: (executionId: string) => Effect.Effect<void>; | |
| 118 | + readonly onResumeStarted?: (executionId: string) => Effect.Effect<void>; | |
| 119 | + readonly onResumeSettled?: (executionId: string) => Effect.Effect<void>; | |
| 120 | +}; | |
| 121 | + | |
| 107 | 122 | // --------------------------------------------------------------------------- |
| 108 | 123 | // Elicitation bridge |
| 109 | 124 | // --------------------------------------------------------------------------- |
| @@ -544,6 +559,17 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 434 unmodified lines | ||
| 544 | 559 | ({ |
| 545 | 560 | mode: "model", |
| 546 | 561 | } as const); |
| 562 | + const onExecutionPaused = (executionId: string): Effect.Effect<void> => | |
| 563 | + config.pausedExecutionHooks?.onExecutionPaused?.(executionId) ?? Effect.void; | |
| 564 | + const onResumeStarted = (executionId: string): Effect.Effect<void> => | |
| 565 | + config.pausedExecutionHooks?.onResumeStarted?.(executionId) ?? Effect.void; | |
| 566 | + const onResumeSettled = (executionId: string): Effect.Effect<void> => | |
| 567 | + config.pausedExecutionHooks?.onResumeSettled?.(executionId) ?? Effect.void; | |
| 568 | + const resumeWithLifecycle = (executionId: string, response: ResumeResponse) => | |
| 569 | + Effect.gen(function* () { | |
| 570 | + yield* onResumeStarted(executionId); | |
| 571 | + return yield* engine.resume(executionId, response); | |
| 572 | + }).pipe(Effect.ensuring(onResumeSettled(executionId))); | |
| 547 | 573 | |
| 548 | 574 | const resolveParentSpan = (): Tracer.AnySpan | undefined => { |
| 549 | 575 | const ps = config.parentSpan; |
| @@ -594,6 +620,14 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 44 unmodified lines | ||
| 594 | 620 | ? pausedInteractionKind(outcome.execution.elicitationContext.request) |
| 595 | 621 | : undefined, |
| 596 | 622 | }); |
| 623 | + if (outcome.status === "paused") { | |
| 624 | + yield* Effect.annotateCurrentSpan({ | |
| 625 | + "mcp.execute.paused": true, | |
| 626 | + "mcp.execute.paused_execution_id": outcome.execution.id, | |
| 627 | + "mcp.execute.pause_source": "execute", | |
| 628 | + }); | |
| 629 | + yield* onExecutionPaused(outcome.execution.id); | |
| 630 | + } | |
| 597 | 631 | return outcome.status === "completed" |
| 598 | 632 | ? toMcpResult(outcome.result) |
| 599 | 633 | : elicitationMode.mode === "browser" |
| @@ -620,7 +654,7 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 20 unmodified lines | ||
| 620 | 654 | hasContent: content !== undefined, |
| 621 | 655 | clientCapabilities: server.server.getClientCapabilities() ?? null, |
| 622 | 656 | }); |
| 623 | − const outcome = yield* engine.resume(executionId, { action, content }); | |
| 657 | + const outcome = yield* resumeWithLifecycle(executionId, { action, content }); | |
| 624 | 658 | if (!outcome) { |
| 625 | 659 | debugLog("resume.missing_execution", { executionId }); |
| 626 | 660 | return missingExecutionResult(executionId); |
| @@ -634,6 +668,14 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines | ||
| 634 | 668 | ? pausedInteractionKind(outcome.execution.elicitationContext.request) |
| 635 | 669 | : undefined, |
| 636 | 670 | }); |
| 671 | + if (outcome.status === "paused") { | |
| 672 | + yield* Effect.annotateCurrentSpan({ | |
| 673 | + "mcp.execute.paused": true, | |
| 674 | + "mcp.execute.paused_execution_id": outcome.execution.id, | |
| 675 | + "mcp.execute.pause_source": "resume", | |
| 676 | + }); | |
| 677 | + yield* onExecutionPaused(outcome.execution.id); | |
| 678 | + } | |
| 637 | 679 | return outcome.status === "completed" |
| 638 | 680 | ? toMcpResult(outcome.result) |
| 639 | 681 | : toMcpPausedResult(formatPausedExecution(outcome.execution)); |
| @@ -682,7 +724,7 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 42 unmodified lines | ||
| 682 | 724 | |
| 683 | 725 | return waitForResponse(executionId).pipe( |
| 684 | 726 | Effect.timeoutOrElse({ |
| 685 | − duration: "10 minutes", | |
| 727 | + duration: Duration.millis(BROWSER_APPROVAL_WAIT_TIMEOUT_MS), | |
| 686 | 728 | orElse: () => Effect.succeed(null), |
| 687 | 729 | }), |
| 688 | 730 | ); |
| @@ -693,10 +735,18 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 4 unmodified lines | ||
| 693 | 735 | const response = yield* waitForBrowserApprovalResponse(executionId); |
| 694 | 736 | if (!response) return yield* requireUserResumeApproval(executionId); |
| 695 | 737 | |
| 696 | − const outcome = yield* engine.resume(executionId, response); | |
| 738 | + const outcome = yield* resumeWithLifecycle(executionId, response); | |
| 697 | 739 | if (!outcome) { |
| 698 | 740 | return missingExecutionResult(executionId); |
| 699 | 741 | } |
| 742 | + if (outcome.status === "paused") { | |
| 743 | + yield* Effect.annotateCurrentSpan({ | |
| 744 | + "mcp.execute.paused": true, | |
| 745 | + "mcp.execute.paused_execution_id": outcome.execution.id, | |
| 746 | + "mcp.execute.pause_source": "browser_resume", | |
| 747 | + }); | |
| 748 | + yield* onExecutionPaused(outcome.execution.id); | |
| 749 | + } | |
| 700 | 750 | return outcome.status === "completed" |
| 701 | 751 | ? toMcpResult(outcome.result) |
| 702 | 752 | : yield* requireUserResumeApproval(outcome.execution.id); |
| @@ -651,15 +651,14 @@ export const resolveOpenApiBackedAnnotations = (input: { · 650 unmodified lines | ||
| 651 | 651 | readonly toolRows: readonly { readonly name: string }[]; |
| 652 | 652 | }) => |
| 653 | 653 | Effect.gen(function* () { |
| 654 | − const ops = yield* input.ctx.storage.listOperations(String(input.integration)); | |
| 655 | − const byName = new Map<string, OperationBinding>(); | |
| 656 | − for (const op of ops) byName.set(op.toolName, op.binding); | |
| 657 | 654 | const out: Record<string, ReturnType<typeof annotationsForOperation>> = {}; |
| 658 | 655 | for (const row of input.toolRows) { |
| 659 | − const binding = byName.get(row.name); | |
| 660 | − if (binding) { | |
| 661 | − out[row.name] = annotationsForOperation(binding.method, binding.pathTemplate); | |
| 662 | − } | |
| 656 | + const operation = yield* input.ctx.storage.getOperation(input.integration, row.name); | |
| 657 | + if (!operation) continue; | |
| 658 | + out[row.name] = annotationsForOperation( | |
| 659 | + operation.binding.method, | |
| 660 | + operation.binding.pathTemplate, | |
| 661 | + ); | |
| 663 | 662 | } |
| 664 | 663 | return out; |
| 665 | 664 | }); |