@opendatalabs/personal-server-ts-lite 0.0.1-canary.00328bf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/bridge.d.ts +23 -0
  2. package/dist/bridge.d.ts.map +1 -0
  3. package/dist/bridge.js +53 -0
  4. package/dist/bridge.js.map +1 -0
  5. package/dist/browser-runtime.d.ts +58 -0
  6. package/dist/browser-runtime.d.ts.map +1 -0
  7. package/dist/browser-runtime.js +214 -0
  8. package/dist/browser-runtime.js.map +1 -0
  9. package/dist/browser-tls-rustls/browser_tls_rustls.d.ts +49 -0
  10. package/dist/browser-tls-rustls/browser_tls_rustls.js +401 -0
  11. package/dist/browser-tls-rustls/browser_tls_rustls_bg.wasm +0 -0
  12. package/dist/client.d.ts +22 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +408 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/derivatives.d.ts +51 -0
  17. package/dist/derivatives.d.ts.map +1 -0
  18. package/dist/derivatives.js +87 -0
  19. package/dist/derivatives.js.map +1 -0
  20. package/dist/diagnostics.d.ts +166 -0
  21. package/dist/diagnostics.d.ts.map +1 -0
  22. package/dist/diagnostics.js +265 -0
  23. package/dist/diagnostics.js.map +1 -0
  24. package/dist/index.d.ts +18 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +19 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/mcp-store.d.ts +35 -0
  29. package/dist/mcp-store.d.ts.map +1 -0
  30. package/dist/mcp-store.js +208 -0
  31. package/dist/mcp-store.js.map +1 -0
  32. package/dist/owner-binding.d.ts +7 -0
  33. package/dist/owner-binding.d.ts.map +1 -0
  34. package/dist/owner-binding.js +18 -0
  35. package/dist/owner-binding.js.map +1 -0
  36. package/dist/persistence.d.ts +69 -0
  37. package/dist/persistence.d.ts.map +1 -0
  38. package/dist/persistence.js +58 -0
  39. package/dist/persistence.js.map +1 -0
  40. package/dist/relay-tls.d.ts +61 -0
  41. package/dist/relay-tls.d.ts.map +1 -0
  42. package/dist/relay-tls.js +316 -0
  43. package/dist/relay-tls.js.map +1 -0
  44. package/dist/relay.d.ts +149 -0
  45. package/dist/relay.d.ts.map +1 -0
  46. package/dist/relay.js +646 -0
  47. package/dist/relay.js.map +1 -0
  48. package/dist/runtime.d.ts +188 -0
  49. package/dist/runtime.d.ts.map +1 -0
  50. package/dist/runtime.js +1178 -0
  51. package/dist/runtime.js.map +1 -0
  52. package/dist/state.d.ts +72 -0
  53. package/dist/state.d.ts.map +1 -0
  54. package/dist/state.js +366 -0
  55. package/dist/state.js.map +1 -0
  56. package/dist/storage-utils.d.ts +8 -0
  57. package/dist/storage-utils.d.ts.map +1 -0
  58. package/dist/storage-utils.js +67 -0
  59. package/dist/storage-utils.js.map +1 -0
  60. package/dist/storage.d.ts +61 -0
  61. package/dist/storage.d.ts.map +1 -0
  62. package/dist/storage.js +718 -0
  63. package/dist/storage.js.map +1 -0
  64. package/dist/sync.d.ts +58 -0
  65. package/dist/sync.d.ts.map +1 -0
  66. package/dist/sync.js +216 -0
  67. package/dist/sync.js.map +1 -0
  68. package/package.json +40 -0
@@ -0,0 +1,1178 @@
1
+ import { GrantRequiredError, InvalidSignatureError, MissingAuthError, NotOwnerError, ProtocolError, PsUnavailableError, UnregisteredBuilderError, } from "@opendatalabs/personal-server-ts-core/errors";
2
+ import { authenticateRequest, } from "@opendatalabs/personal-server-ts-core/auth";
3
+ import { approveDeviceSessionContract, createMemoryDeviceSessionStore, initiateDeviceSessionContract, pollDeviceSessionContract, provisionDeviceTokenContract, revokeDeviceTokenContract, } from "@opendatalabs/personal-server-ts-core/contracts";
4
+ import { handlePersonalServerAccessLogsRequest, handlePersonalServerConfigRequest, handlePersonalServerDataRequest, handlePersonalServerGrantsRequest, handlePersonalServerOauthTokenRequest, handlePersonalServerSyncRequest, } from "@opendatalabs/personal-server-ts-core/api";
5
+ import { handlePersonalServerDerivativesRequest, } from "@opendatalabs/personal-server-ts-core/derivatives";
6
+ import { verifyDataReadPolicy, } from "@opendatalabs/personal-server-ts-core/policy";
7
+ import { createInMemoryWriteProofReplayStore, createWriteSessionAuthorization, handleWriteSessionRequest, } from "@opendatalabs/personal-server-ts-core/write";
8
+ import { approveMcpOAuthAuthorization, approveMcpOAuthAuthorizationWithScopes, approveMcpConnection, buildMcpProtectedResourceMetadataUrl, buildStableMcpUrl, createInMemoryMcpOAuthAuthorizationStore, createMcpConnection, createMcpOAuthAuthorization, createMcpDataReadClient, handleMcpStreamableHttpRequest, hashConnectionToken, listMcpConnectionViews, loadMcpGranteeAccount, McpConnectionNotFoundError, McpConnectionStateError, McpOAuthAuthorizationError, redeemMcpOAuthAuthorizationCode, refreshMcpOAuthToken, requestMcpScopeAccess, toMcpOAuthAuthorizationView, revokeMcpConnection, toMcpConnectionView, McpActivityRecorder, } from "@opendatalabs/personal-server-ts-core/mcp";
9
+ import { createDefaultPsLiteAccessLogStore, createDefaultPsLiteSaveConfig, createDefaultPsLiteTokenStore, } from "./state.js";
10
+ import { collectDiagnosticsWithTimeout, DiagnosticsRecorder, } from "./diagnostics.js";
11
+ function jsonResponse(body, init) {
12
+ const headers = new Headers(init?.headers);
13
+ headers.set("Content-Type", "application/json");
14
+ return new Response(JSON.stringify(body), { ...init, headers });
15
+ }
16
+ function protocolErrorResponse(err) {
17
+ return jsonResponse(err.toJSON(), { status: err.code });
18
+ }
19
+ function errorResponse(status, errorCode, message) {
20
+ return jsonResponse({
21
+ error: {
22
+ code: status,
23
+ errorCode,
24
+ message,
25
+ },
26
+ }, { status });
27
+ }
28
+ function mcpUnauthorized(origin, message = "MCP authorization required") {
29
+ return jsonResponse({
30
+ error: {
31
+ code: 401,
32
+ errorCode: "MCP_AUTH_REQUIRED",
33
+ message,
34
+ },
35
+ }, {
36
+ status: 401,
37
+ headers: {
38
+ "WWW-Authenticate": `Bearer resource_metadata="${buildMcpProtectedResourceMetadataUrl(origin)}", scope="vana:read"`,
39
+ },
40
+ });
41
+ }
42
+ /** Grant types this OAuth server implements — advertised and enforced here. */
43
+ const AUTHORIZATION_CODE_GRANT = "authorization_code";
44
+ const REFRESH_TOKEN_GRANT = "refresh_token";
45
+ const SUPPORTED_GRANT_TYPES = [
46
+ AUTHORIZATION_CODE_GRANT,
47
+ REFRESH_TOKEN_GRANT,
48
+ ];
49
+ function isSupportedGrantType(value) {
50
+ return SUPPORTED_GRANT_TYPES.includes(value);
51
+ }
52
+ function authorizationServerMetadata(origin) {
53
+ return {
54
+ issuer: origin,
55
+ authorization_endpoint: `${origin}/mcp/oauth/authorize`,
56
+ token_endpoint: `${origin}/mcp/oauth/token`,
57
+ registration_endpoint: `${origin}/mcp/oauth/register`,
58
+ response_types_supported: ["code"],
59
+ grant_types_supported: [...SUPPORTED_GRANT_TYPES],
60
+ token_endpoint_auth_methods_supported: ["none"],
61
+ code_challenge_methods_supported: ["S256"],
62
+ scopes_supported: ["vana:read"],
63
+ };
64
+ }
65
+ function protectedResourceMetadata(origin) {
66
+ return {
67
+ resource: buildStableMcpUrl(origin),
68
+ authorization_servers: [origin],
69
+ bearer_methods_supported: ["header"],
70
+ scopes_supported: ["vana:read"],
71
+ resource_name: "Vana Personal Server MCP",
72
+ };
73
+ }
74
+ function resolveMcpApprovalUrl(value) {
75
+ if (!value)
76
+ return null;
77
+ return typeof value === "function" ? value() : value;
78
+ }
79
+ function redirectWithOAuthError(redirectUri, error, description, state) {
80
+ try {
81
+ const url = new URL(redirectUri);
82
+ url.searchParams.set("error", error);
83
+ url.searchParams.set("error_description", description);
84
+ if (state)
85
+ url.searchParams.set("state", state);
86
+ return Response.redirect(url.toString(), 302);
87
+ }
88
+ catch {
89
+ return jsonResponse({
90
+ error,
91
+ error_description: description,
92
+ ...(state ? { state } : {}),
93
+ }, { status: 400 });
94
+ }
95
+ }
96
+ async function parseFormBody(request) {
97
+ return new URLSearchParams(await request.text());
98
+ }
99
+ function unavailableResponse() {
100
+ const err = new PsUnavailableError({
101
+ runtime: "ps-lite",
102
+ reason: "Browser runtime is inactive",
103
+ });
104
+ return protocolErrorResponse(err);
105
+ }
106
+ function parseBearerToken(request) {
107
+ const authorization = request.headers.get("authorization");
108
+ if (!authorization)
109
+ return null;
110
+ const match = /^Bearer\s+(.+)$/i.exec(authorization);
111
+ return match?.[1] ?? null;
112
+ }
113
+ function assertBearerToken(request, expectedToken, ownerOnly = false) {
114
+ const token = parseBearerToken(request);
115
+ if (!token) {
116
+ throw new MissingAuthError();
117
+ }
118
+ if (token !== expectedToken) {
119
+ throw ownerOnly ? new NotOwnerError() : new InvalidSignatureError();
120
+ }
121
+ }
122
+ function createMissingAuthAdapter() {
123
+ return {
124
+ async authorizeOwner() {
125
+ throw new MissingAuthError();
126
+ },
127
+ async authorizeBuilderList() {
128
+ throw new MissingAuthError();
129
+ },
130
+ async authorizeBuilderRead() {
131
+ throw new MissingAuthError();
132
+ },
133
+ };
134
+ }
135
+ /**
136
+ * The two write-session methods of the auth port, built on the SHARED core
137
+ * authorizer (`createWriteSessionAuthorization`) — the same code the Node
138
+ * server's adapter runs. `authorizeWrite` falls back to the adapter's owner
139
+ * gate for any request that carries no live session, so owner ingest behaves
140
+ * exactly as it did before write sessions existed.
141
+ */
142
+ function writeSessionAuthMethods(writeSessions, authorizeOwner) {
143
+ const authorization = createWriteSessionAuthorization({
144
+ serverOrigin: writeSessions.serverOrigin,
145
+ serverOwner: writeSessions.serverOwner,
146
+ sessionStore: writeSessions.store,
147
+ replayStore: writeSessions.replayStore ?? createInMemoryWriteProofReplayStore(),
148
+ policyPorts: writeSessions.policyPorts,
149
+ });
150
+ return {
151
+ async authorizeWrite(input) {
152
+ const delegated = await authorization.authorizeSessionWrite(input);
153
+ if (delegated)
154
+ return delegated;
155
+ await authorizeOwner(input.request);
156
+ },
157
+ async authorizeWriteSession(request) {
158
+ return authorization.recognizeWriteSession(request);
159
+ },
160
+ };
161
+ }
162
+ export function createBearerTokenPsLiteAuth(options) {
163
+ const authorizeOwner = async (request) => {
164
+ assertBearerToken(request, options.ownerToken, true);
165
+ };
166
+ return {
167
+ authorizeOwner,
168
+ ...(options.writeSessions
169
+ ? writeSessionAuthMethods(options.writeSessions, authorizeOwner)
170
+ : {}),
171
+ async authorizeBuilderList(request) {
172
+ if (parseBearerToken(request) === options.ownerToken)
173
+ return;
174
+ assertBearerToken(request, options.builderToken);
175
+ },
176
+ async authorizeBuilderRead(input) {
177
+ if (parseBearerToken(input.request) === options.ownerToken) {
178
+ return { builder: "owner", grantId: "owner" };
179
+ }
180
+ assertBearerToken(input.request, options.builderToken);
181
+ if (!input.grantId) {
182
+ throw new GrantRequiredError({
183
+ reason: "No grantId in request",
184
+ });
185
+ }
186
+ // This adapter echoes the caller-supplied grantId into the auth
187
+ // result, so it must never mint the server-internal sentinels that
188
+ // confer the unredacted owner view and the x402 payment exemption.
189
+ if (input.grantId === "owner" || input.grantId === "policy-bypass") {
190
+ throw new GrantRequiredError({
191
+ reason: "Reserved grantId",
192
+ grantId: input.grantId,
193
+ });
194
+ }
195
+ return { grantId: input.grantId };
196
+ },
197
+ };
198
+ }
199
+ async function authenticatePsLiteRequest(request, options) {
200
+ return authenticateRequest({
201
+ request,
202
+ serverOrigin: options.origin,
203
+ serverOwner: options.ownerAddress,
204
+ accessToken: options.accessToken,
205
+ sessionTokenVerifier: options.tokenStore,
206
+ now: options.now,
207
+ });
208
+ }
209
+ function isOwnerSigner(auth, ownerAddress) {
210
+ return auth.auth.signer.toLowerCase() === ownerAddress.toLowerCase();
211
+ }
212
+ function dataReadPolicyPortsRequired() {
213
+ return new ProtocolError(500, "SERVER_NOT_CONFIGURED", "Server is not configured", {
214
+ reason: "PS Lite read policy ports are not configured",
215
+ });
216
+ }
217
+ export function createWeb3SignedPsLiteAuth(options) {
218
+ const authorizeOwner = async (request) => {
219
+ const auth = await authenticatePsLiteRequest(request, options);
220
+ if (!isOwnerSigner(auth, options.ownerAddress)) {
221
+ throw new NotOwnerError({
222
+ expected: options.ownerAddress,
223
+ actual: auth.auth.signer,
224
+ });
225
+ }
226
+ };
227
+ return {
228
+ authorizeOwner,
229
+ ...(options.writeSessions
230
+ ? writeSessionAuthMethods({
231
+ ...options.writeSessions,
232
+ serverOrigin: options.origin,
233
+ serverOwner: options.ownerAddress,
234
+ }, authorizeOwner)
235
+ : {}),
236
+ async authorizeBuilderList(request) {
237
+ const auth = await authenticatePsLiteRequest(request, options);
238
+ if (auth.isPolicyBypass || isOwnerSigner(auth, options.ownerAddress)) {
239
+ return;
240
+ }
241
+ if (!options.dataReadPolicyPorts) {
242
+ throw dataReadPolicyPortsRequired();
243
+ }
244
+ const builder = await options.dataReadPolicyPorts.authSessionVerifier.getBuilder(auth.auth.signer);
245
+ if (!builder) {
246
+ throw new UnregisteredBuilderError();
247
+ }
248
+ },
249
+ async authorizeBuilderRead(input) {
250
+ const auth = await authenticatePsLiteRequest(input.request, options);
251
+ if (auth.isPolicyBypass) {
252
+ return { builder: auth.auth.signer, grantId: "policy-bypass" };
253
+ }
254
+ if (isOwnerSigner(auth, options.ownerAddress)) {
255
+ return { builder: auth.auth.signer, grantId: "owner" };
256
+ }
257
+ if (!options.dataReadPolicyPorts) {
258
+ throw dataReadPolicyPortsRequired();
259
+ }
260
+ const grant = await verifyDataReadPolicy({
261
+ signer: auth.auth.signer,
262
+ grantId: auth.auth.payload.grantId ?? input.grantId,
263
+ requestedScope: input.scope,
264
+ fileId: input.fileId,
265
+ // Bind the read to this PS's owner — the grant must have been issued
266
+ // by us. Required now that the policy fails closed on a missing owner.
267
+ serverOwner: options.ownerAddress,
268
+ }, options.dataReadPolicyPorts);
269
+ return { builder: auth.auth.signer, grantId: grant.id };
270
+ },
271
+ };
272
+ }
273
+ function toDataStoragePort(storage) {
274
+ if ("listScopes" in storage) {
275
+ return storage;
276
+ }
277
+ throw new Error("PS Lite runtime requires a persistent DataStoragePort. Use createIndexedDbPsLiteRuntime() or createPersistentPsLiteStorage().");
278
+ }
279
+ function createLogId() {
280
+ return globalThis.crypto?.randomUUID?.() ?? `log-${Date.now()}`;
281
+ }
282
+ function randomHex(byteLength) {
283
+ const bytes = new Uint8Array(byteLength);
284
+ globalThis.crypto.getRandomValues(bytes);
285
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
286
+ }
287
+ function bearerToken(request) {
288
+ const authorization = request.headers.get("authorization");
289
+ if (!authorization?.startsWith("Bearer "))
290
+ return null;
291
+ return authorization.slice(7);
292
+ }
293
+ /**
294
+ * Detached copy of the gateway block the caller built the gateway client and
295
+ * the server signer from, so a later live config write cannot mutate it.
296
+ */
297
+ function snapshotGatewayConfig(config) {
298
+ const gateway = config?.gateway;
299
+ if (!gateway)
300
+ return undefined;
301
+ return {
302
+ ...gateway,
303
+ ...(gateway.contracts ? { contracts: { ...gateway.contracts } } : {}),
304
+ };
305
+ }
306
+ function resolveX402Settings(config, gateway = config?.gateway) {
307
+ // Paid reads need the complete EIP-712 gateway domain. A partial gateway is
308
+ // valid while payment is disabled, but must never enter the x402 path.
309
+ const paymentEnabled = config?.payment?.enabled ?? false;
310
+ const gatewayConfigComplete = Boolean(gateway &&
311
+ typeof gateway.url === "string" &&
312
+ typeof gateway.chainId === "number" &&
313
+ gateway.contracts &&
314
+ typeof gateway.contracts.dataPortabilityEscrow === "string" &&
315
+ typeof gateway.contracts.dataRegistry === "string");
316
+ if (paymentEnabled && !gatewayConfigComplete) {
317
+ throw new Error("PS-Lite x402 payment is enabled (config.payment.enabled) but the gateway " +
318
+ "config is incomplete. A complete gateway config — url, chainId, and " +
319
+ "contracts (dataPortabilityEscrow, dataRegistry) — is required to charge " +
320
+ "for reads. Provide the full config or disable payment.");
321
+ }
322
+ return {
323
+ paymentEnabled,
324
+ gatewayConfig: gatewayConfigComplete
325
+ ? gateway
326
+ : undefined,
327
+ };
328
+ }
329
+ export function createPsLiteRuntime(options) {
330
+ let active = options.active ?? false;
331
+ const now = options.now ?? (() => new Date());
332
+ const auth = options.auth ?? createMissingAuthAdapter();
333
+ const dataStorage = toDataStoragePort(options.storage);
334
+ // Wire diagnostics by default so GET /v1/diagnostics is always available.
335
+ const diagnostics = options.diagnostics ?? new DiagnosticsRecorder();
336
+ options = { ...options, diagnostics };
337
+ diagnostics.push({ phase: "booting", detail: "runtime created" });
338
+ let accessLogReader = options.accessLogReader;
339
+ let accessLogWriter = options.accessLogWriter;
340
+ if (!accessLogReader || !accessLogWriter) {
341
+ const accessLogStore = createDefaultPsLiteAccessLogStore();
342
+ accessLogReader ??= accessLogStore;
343
+ accessLogWriter ??= accessLogStore;
344
+ }
345
+ const tokenStore = options.tokenStore ?? createDefaultPsLiteTokenStore();
346
+ const saveConfig = options.saveConfig ?? createDefaultPsLiteSaveConfig();
347
+ // The gateway client (options.gateway) and the server signer are built ONCE
348
+ // by the caller, from the startup gateway block: the gateway URL and the
349
+ // EIP-712 domain (chainId + contracts) are baked into them. A live config
350
+ // write persists a new gateway block but cannot rebuild those dependencies,
351
+ // so every gateway-derived request setting stays pinned to this snapshot —
352
+ // otherwise a config save (the UI PUTs a schema-defaulted config, so an
353
+ // omitted gateway block silently becomes the schema default) would issue
354
+ // x402 challenges, and query a gateway, for a domain the accessRecord
355
+ // signature was never made against. Gateway changes take effect on restart.
356
+ const boundGatewayConfig = snapshotGatewayConfig(options.config);
357
+ // Validate initial x402 readiness. The same derivation runs for every read
358
+ // and before every config write so payment policy cannot drift from the live
359
+ // config after a same-runtime update.
360
+ resolveX402Settings(options.config, boundGatewayConfig);
361
+ // The data handler's x402 path only needs signRecordDataAccess; narrow the
362
+ // (optional) signer so a grant-only signer doesn't masquerade as one that can
363
+ // sign access records.
364
+ const x402ServerSigner = options.serverSigner?.signRecordDataAccess
365
+ ? { signRecordDataAccess: options.serverSigner.signRecordDataAccess }
366
+ : undefined;
367
+ // One replay guard for the handshake proof and (when the caller shares it
368
+ // with the auth adapter) the per-write proofs, matching createApp's single
369
+ // writeProofReplayStore in the Node build.
370
+ const writeProofReplayStore = options.writeProofReplayStore ?? createInMemoryWriteProofReplayStore();
371
+ const deviceSessions = createMemoryDeviceSessionStore();
372
+ const mcpOAuthAuthorizationStore = options.mcpOAuthAuthorizationStore ??
373
+ createInMemoryMcpOAuthAuthorizationStore();
374
+ const activityRecorder = options.mcpConnectionStore
375
+ ? new McpActivityRecorder()
376
+ : undefined;
377
+ async function withProtocolErrors(handler) {
378
+ try {
379
+ return await handler();
380
+ }
381
+ catch (err) {
382
+ if (err instanceof ProtocolError) {
383
+ return protocolErrorResponse(err);
384
+ }
385
+ return errorResponse(500, "INTERNAL_ERROR", "Internal server error");
386
+ }
387
+ }
388
+ function sendContractResult(result) {
389
+ return jsonResponse(result.body, { status: result.status });
390
+ }
391
+ function ownerAddress() {
392
+ return options.serverOwner ?? options.identity?.address;
393
+ }
394
+ async function handleAuthDevice(request, url) {
395
+ if (url.pathname === "/auth/device") {
396
+ if (request.method !== "POST") {
397
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
398
+ }
399
+ return sendContractResult(initiateDeviceSessionContract({
400
+ sessionStore: deviceSessions,
401
+ serverOwner: ownerAddress(),
402
+ requestOrigin: url.origin,
403
+ approvalOrigin: url.origin,
404
+ sessionId: randomHex(32),
405
+ pollToken: randomHex(32),
406
+ now: now().getTime(),
407
+ }));
408
+ }
409
+ if (url.pathname === "/auth/device/poll") {
410
+ if (request.method !== "GET") {
411
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
412
+ }
413
+ return sendContractResult(pollDeviceSessionContract({
414
+ sessionStore: deviceSessions,
415
+ pollToken: url.searchParams.get("token"),
416
+ serverOwner: ownerAddress(),
417
+ now: now().getTime(),
418
+ }));
419
+ }
420
+ if (url.pathname === "/auth/device/approve") {
421
+ const sessionId = url.searchParams.get("session");
422
+ if (!sessionId) {
423
+ return request.method === "GET"
424
+ ? new Response("Missing session parameter", { status: 400 })
425
+ : jsonResponse({ error: { code: 400, message: "Missing session parameter" } }, { status: 400 });
426
+ }
427
+ const session = deviceSessions.get(sessionId);
428
+ if (!session) {
429
+ return request.method === "GET"
430
+ ? new Response("Session expired or invalid", { status: 404 })
431
+ : jsonResponse({ error: { code: 404, message: "Session expired or invalid" } }, { status: 404 });
432
+ }
433
+ if (request.method === "GET") {
434
+ return new Response("Device authorization pending", {
435
+ headers: { "Content-Type": "text/html; charset=utf-8" },
436
+ });
437
+ }
438
+ if (request.method !== "POST") {
439
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
440
+ }
441
+ if (session.status === "approved") {
442
+ return jsonResponse({ status: "already_approved" });
443
+ }
444
+ await auth.authorizeOwner(request);
445
+ return sendContractResult(await approveDeviceSessionContract({
446
+ sessionStore: deviceSessions,
447
+ tokenStore,
448
+ sessionId,
449
+ serverOwner: ownerAddress(),
450
+ accessToken: `vana_ps_${randomHex(32)}`,
451
+ now: now().getTime(),
452
+ }));
453
+ }
454
+ if (url.pathname === "/auth/device/token") {
455
+ if (request.method === "DELETE") {
456
+ const token = bearerToken(request);
457
+ if (!token) {
458
+ return jsonResponse({ error: { code: 401, message: "Missing Bearer token" } }, { status: 401 });
459
+ }
460
+ return sendContractResult(await revokeDeviceTokenContract({
461
+ tokenStore,
462
+ bearerToken: token,
463
+ }));
464
+ }
465
+ if (request.method !== "POST") {
466
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
467
+ }
468
+ const token = bearerToken(request);
469
+ if (!options.accessToken || token !== options.accessToken) {
470
+ return jsonResponse({
471
+ error: {
472
+ code: 403,
473
+ message: "Only control-plane tokens can provision Personal Server session tokens",
474
+ },
475
+ }, { status: 403 });
476
+ }
477
+ let body;
478
+ try {
479
+ body = (await request.json());
480
+ }
481
+ catch {
482
+ return jsonResponse({ error: { code: 400, message: "Request body must be valid JSON" } }, { status: 400 });
483
+ }
484
+ if (!body.token || typeof body.token !== "string") {
485
+ return jsonResponse({ error: { code: 400, message: "Missing token" } }, { status: 400 });
486
+ }
487
+ return sendContractResult(await provisionDeviceTokenContract({
488
+ tokenStore,
489
+ body,
490
+ now: now().getTime(),
491
+ }));
492
+ }
493
+ return undefined;
494
+ }
495
+ return {
496
+ kind: "ps-lite",
497
+ storage: options.storage,
498
+ activate() {
499
+ active = true;
500
+ options.syncManager?.start?.();
501
+ options.derivatives?.scheduler.start();
502
+ },
503
+ deactivate() {
504
+ active = false;
505
+ void options.syncManager?.stop?.();
506
+ options.derivatives?.scheduler.stop();
507
+ },
508
+ isAvailable() {
509
+ return active;
510
+ },
511
+ async fetch(request) {
512
+ return withProtocolErrors(async () => {
513
+ const url = new URL(request.url);
514
+ if (url.pathname === "/health") {
515
+ const apiOrigin = url.origin;
516
+ const identity = options.identity ?? null;
517
+ let serverId = null;
518
+ if (identity && options.gateway) {
519
+ try {
520
+ const server = await options.gateway.getServer(identity.address);
521
+ serverId = server?.id ?? null;
522
+ }
523
+ catch {
524
+ serverId = null;
525
+ }
526
+ }
527
+ const registration = options.serverOwner && identity
528
+ ? {
529
+ ownerAddress: options.serverOwner,
530
+ serverAddress: identity.address,
531
+ publicKey: identity.publicKey,
532
+ serverUrl: apiOrigin,
533
+ serverId,
534
+ registered: Boolean(serverId),
535
+ }
536
+ : null;
537
+ const capabilities = dataStorage.capabilities;
538
+ const stateCapabilities = {
539
+ tokens: tokenStore
540
+ .capabilities?.tokens ?? "custom",
541
+ accessLogs: accessLogReader.capabilities?.accessLogs ?? "custom",
542
+ config: options.stateCapabilities?.config ??
543
+ (options.saveConfig ? "custom" : "indexeddb"),
544
+ };
545
+ return jsonResponse({
546
+ status: active ? "healthy" : "unavailable",
547
+ runtime: "ps-lite",
548
+ storage: options.storage.kind,
549
+ capabilities: capabilities ?? null,
550
+ stateCapabilities,
551
+ owner: options.serverOwner ?? null,
552
+ apiOrigin,
553
+ gatewayUrl: options.config?.gateway?.url ?? null,
554
+ gatewayConfig: options.config?.gateway ?? null,
555
+ identity,
556
+ registration,
557
+ active,
558
+ checkedAt: now().toISOString(),
559
+ });
560
+ }
561
+ if (url.pathname === "/v1/diagnostics") {
562
+ if (request.method !== "GET") {
563
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
564
+ }
565
+ try {
566
+ await auth.authorizeOwner(request);
567
+ }
568
+ catch (err) {
569
+ if (err instanceof ProtocolError) {
570
+ return protocolErrorResponse(err);
571
+ }
572
+ throw err;
573
+ }
574
+ if (!options.diagnostics) {
575
+ return jsonResponse({
576
+ error: {
577
+ code: 404,
578
+ errorCode: "DIAGNOSTICS_NOT_CONFIGURED",
579
+ message: "Diagnostics recorder not configured for this runtime",
580
+ },
581
+ }, { status: 404 });
582
+ }
583
+ const syncStatus = options.syncManager?.getStatus() ?? null;
584
+ const snapshot = await collectDiagnosticsWithTimeout(options.diagnostics, {
585
+ runtimeActive: active,
586
+ syncStatus,
587
+ storage: dataStorage,
588
+ });
589
+ return jsonResponse(snapshot);
590
+ }
591
+ if (!active) {
592
+ return unavailableResponse();
593
+ }
594
+ const dataPrefix = "/v1/data";
595
+ if (url.pathname === dataPrefix ||
596
+ url.pathname.startsWith(`${dataPrefix}/`)) {
597
+ const x402 = resolveX402Settings(options.config, boundGatewayConfig);
598
+ return handlePersonalServerDataRequest(request, {
599
+ storage: dataStorage,
600
+ auth,
601
+ accessLogWriter,
602
+ readFulfillmentReporter: options.readFulfillmentReporter,
603
+ syncManager: options.syncManager ?? null,
604
+ scopeDeletions: options.scopeDeletions,
605
+ now,
606
+ createLogId,
607
+ // x402 payment enforcement for builder reads. Only engages with a
608
+ // complete gateway config (validated at construction); a partial
609
+ // config leaves these undefined so reads are served free instead
610
+ // of yielding a malformed challenge.
611
+ paymentEnabled: x402.paymentEnabled,
612
+ gateway: options.gateway,
613
+ gatewayConfig: x402.gatewayConfig,
614
+ gatewayUrl: x402.gatewayConfig?.url,
615
+ serverAddress: options.identity?.address,
616
+ // The data owner signed into the RecordDataAccess attestation.
617
+ // Distinct from serverAddress (the server keypair): the core only
618
+ // embeds the x402 accessRecord when BOTH serverSigner and
619
+ // serverOwner are present (see x402.ts buildChallenge). We pass
620
+ // the real owner — not identity.address — so the on-chain
621
+ // recordDataAccess attributes the read to the correct owner; if
622
+ // serverOwner is absent the accessRecord is safely omitted.
623
+ serverOwner: options.serverOwner,
624
+ serverSigner: x402ServerSigner,
625
+ lineageGateway: options.lineageGateway,
626
+ // A new local version marks every question that reads the
627
+ // scope stale; the recompute waits for a reader.
628
+ onDataWritten: options.derivatives
629
+ ? (event) => options.derivatives?.scheduler.markSourceChanged(event.scope, { lineageSources: event.lineageSources })
630
+ : undefined,
631
+ // And an authorized read of a derived scope is that reader.
632
+ onDataRead: options.derivatives
633
+ ? (event) => options.derivatives?.scheduler.markDemand(event.scope)
634
+ : undefined,
635
+ }, { basePath: dataPrefix });
636
+ }
637
+ // Write API handshake. Mounted only when a session store is wired,
638
+ // so a runtime without delegated writes has no endpoint to probe.
639
+ if (url.pathname === "/v1/write/session") {
640
+ if (!options.writeSessionStore) {
641
+ return errorResponse(404, "NOT_FOUND", "Not found");
642
+ }
643
+ if (request.method !== "POST") {
644
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
645
+ }
646
+ if (!options.gateway) {
647
+ return errorResponse(500, "SERVER_NOT_CONFIGURED", "Gateway client is not configured");
648
+ }
649
+ return handleWriteSessionRequest(request, {
650
+ serverOrigin: options.serverOrigin ?? url.origin,
651
+ // The real data owner only — never the identity.address fallback
652
+ // the other routes accept. The grant's grantor must equal what
653
+ // the auth adapter binds delegated writes to, and a session
654
+ // minted against the SERVER key would authorize nothing. Absent
655
+ // = the handshake answers 500 rather than guessing.
656
+ serverOwner: options.serverOwner,
657
+ sessionStore: options.writeSessionStore,
658
+ replayStore: writeProofReplayStore,
659
+ authSessionVerifier: options.gateway,
660
+ grantVerifier: options.gateway,
661
+ accessToken: options.accessToken,
662
+ tokenStore,
663
+ });
664
+ }
665
+ const derivativesPrefix = "/v1/derivatives";
666
+ if (url.pathname.startsWith(`${derivativesPrefix}/`)) {
667
+ return handlePersonalServerDerivativesRequest(request, { auth, compute: options.derivatives ?? null, now }, { basePath: derivativesPrefix });
668
+ }
669
+ if (url.pathname.startsWith("/auth/device")) {
670
+ const response = await handleAuthDevice(request, url);
671
+ if (response)
672
+ return response;
673
+ }
674
+ if (url.pathname === "/oauth/token") {
675
+ return handlePersonalServerOauthTokenRequest(request, {
676
+ tokenStore,
677
+ controlPlaneSecret: options.accessToken,
678
+ randomToken: () => `vana_ps_${randomHex(32)}`,
679
+ now,
680
+ deviceSessions: {
681
+ findByDeviceCode(deviceCode) {
682
+ const session = deviceSessions.findByPollToken(deviceCode);
683
+ if (!session)
684
+ return null;
685
+ return {
686
+ status: session.status,
687
+ accessToken: session.accessToken,
688
+ accessTokenExpiresAt: session.accessTokenExpiresAt,
689
+ sessionId: session.sessionId,
690
+ };
691
+ },
692
+ consume(sessionId) {
693
+ deviceSessions.delete(sessionId);
694
+ },
695
+ },
696
+ });
697
+ }
698
+ const grantsPrefix = "/v1/grants";
699
+ if (url.pathname === grantsPrefix ||
700
+ url.pathname === `${grantsPrefix}/` ||
701
+ url.pathname.startsWith(`${grantsPrefix}/`)) {
702
+ return handlePersonalServerGrantsRequest(request, {
703
+ auth,
704
+ gateway: options.gateway,
705
+ gatewayConfig: options.config?.gateway,
706
+ serverOwner: options.serverOwner ?? options.identity?.address,
707
+ serverSigner: options.serverSigner,
708
+ now,
709
+ }, { basePath: grantsPrefix });
710
+ }
711
+ const accessLogsPrefix = "/v1/access-logs";
712
+ if (url.pathname === accessLogsPrefix ||
713
+ url.pathname === `${accessLogsPrefix}/`) {
714
+ return handlePersonalServerAccessLogsRequest(request, { auth, accessLogReader }, { basePath: accessLogsPrefix });
715
+ }
716
+ const syncPrefix = "/v1/sync";
717
+ if (url.pathname === `${syncPrefix}/trigger` ||
718
+ url.pathname === `${syncPrefix}/status` ||
719
+ url.pathname.startsWith(`${syncPrefix}/file/`)) {
720
+ return handlePersonalServerSyncRequest(request, { auth, syncManager: options.syncManager ?? null }, { basePath: syncPrefix });
721
+ }
722
+ if (url.pathname === "/ui/api/config") {
723
+ return handlePersonalServerConfigRequest(request, {
724
+ auth,
725
+ async readConfig() {
726
+ return options.config ?? {};
727
+ },
728
+ async writeConfig(config) {
729
+ // Validate the policy that will actually run: the incoming
730
+ // payment toggle against the pinned gateway, so a persisted
731
+ // config can never leave live reads without a usable domain.
732
+ resolveX402Settings(config, boundGatewayConfig);
733
+ await saveConfig(config);
734
+ // Reflect the persisted config in memory so reads (and
735
+ // GET /ui/api/config) stop serving the pre-write snapshot. The
736
+ // gateway block is reported as persisted but stays pinned for
737
+ // behavior until the next restart rebuilds gateway + signer.
738
+ if (options.config &&
739
+ typeof config === "object" &&
740
+ config !== null) {
741
+ Object.assign(options.config, config);
742
+ }
743
+ },
744
+ });
745
+ }
746
+ if (options.mcpConnectionStore) {
747
+ const mcpResponse = await handleMcpRoute({
748
+ request,
749
+ url,
750
+ store: options.mcpConnectionStore,
751
+ authorizationStore: mcpOAuthAuthorizationStore,
752
+ approvalUrl: options.mcpOAuthApprovalUrl,
753
+ auth,
754
+ dataStorage,
755
+ accessLogWriter,
756
+ readFulfillmentReporter: options.readFulfillmentReporter,
757
+ now,
758
+ runtimeAvailability: { isAvailable: () => active },
759
+ serverOrigin: url.origin,
760
+ gateway: options.gateway,
761
+ // Pinned like the x402 path: grant registration is signed with the
762
+ // startup EIP-712 domain, so the MCP route must see the same one.
763
+ gatewayConfig: boundGatewayConfig,
764
+ serverOwner: options.serverOwner ?? options.identity?.address,
765
+ serverSigner: options.serverSigner,
766
+ activityRecorder,
767
+ });
768
+ if (mcpResponse)
769
+ return mcpResponse;
770
+ }
771
+ return errorResponse(404, "NOT_FOUND", "Not found");
772
+ });
773
+ },
774
+ };
775
+ }
776
+ /**
777
+ * MCP route dispatcher used inside `createPsLiteRuntime.fetch`. Handles both
778
+ * the owner-only `/v1/mcp/connections` management endpoints and the public
779
+ * `/mcp/:connectionToken` Streamable HTTP endpoint that Claude dials.
780
+ *
781
+ * Returns `null` for paths that don't match — the caller continues to the
782
+ * 404. Errors are passed through; the caller is already inside
783
+ * `withProtocolErrors`.
784
+ */
785
+ async function handleMcpRoute(input) {
786
+ const pathname = input.url.pathname;
787
+ const ownerPrefix = "/v1/mcp/connections";
788
+ const ownerAuthorizationPrefix = "/v1/mcp/oauth/authorizations";
789
+ if (pathname === "/.well-known/oauth-protected-resource/mcp") {
790
+ if (!resolveMcpApprovalUrl(input.approvalUrl)) {
791
+ return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
792
+ }
793
+ return jsonResponse(protectedResourceMetadata(input.serverOrigin));
794
+ }
795
+ if (pathname === "/.well-known/oauth-authorization-server") {
796
+ if (!resolveMcpApprovalUrl(input.approvalUrl)) {
797
+ return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
798
+ }
799
+ return jsonResponse(authorizationServerMetadata(input.serverOrigin));
800
+ }
801
+ if (pathname === "/mcp/oauth/register") {
802
+ if (!resolveMcpApprovalUrl(input.approvalUrl)) {
803
+ return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
804
+ }
805
+ if (input.request.method !== "POST") {
806
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
807
+ }
808
+ let body = {};
809
+ try {
810
+ body = (await input.request.json());
811
+ }
812
+ catch {
813
+ body = {};
814
+ }
815
+ return jsonResponse({
816
+ client_id: `mcp-client-${crypto.randomUUID()}`,
817
+ client_name: body.client_name ?? "Claude",
818
+ redirect_uris: Array.isArray(body.redirect_uris)
819
+ ? body.redirect_uris
820
+ : [],
821
+ token_endpoint_auth_method: "none",
822
+ grant_types: [...SUPPORTED_GRANT_TYPES],
823
+ response_types: ["code"],
824
+ }, { status: 201 });
825
+ }
826
+ if (pathname === "/mcp/oauth/authorize") {
827
+ if (!resolveMcpApprovalUrl(input.approvalUrl)) {
828
+ return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
829
+ }
830
+ if (input.request.method !== "GET") {
831
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
832
+ }
833
+ const responseType = input.url.searchParams.get("response_type");
834
+ const clientId = input.url.searchParams.get("client_id") ?? "";
835
+ const redirectUri = input.url.searchParams.get("redirect_uri") ?? "";
836
+ const codeChallenge = input.url.searchParams.get("code_challenge") ?? "";
837
+ const codeChallengeMethod = input.url.searchParams.get("code_challenge_method") ?? "";
838
+ const state = input.url.searchParams.get("state");
839
+ const scope = input.url.searchParams.get("scope") ?? "vana:read";
840
+ if (responseType !== "code") {
841
+ return redirectWithOAuthError(redirectUri, "unsupported_response_type", "Only response_type=code is supported", state);
842
+ }
843
+ try {
844
+ const created = await createMcpOAuthAuthorization({
845
+ clientId,
846
+ redirectUri,
847
+ codeChallenge,
848
+ codeChallengeMethod,
849
+ scope,
850
+ ...(state ? { state } : {}),
851
+ }, {
852
+ connectionStore: input.store,
853
+ authorizationStore: input.authorizationStore,
854
+ publicOrigin: input.serverOrigin,
855
+ now: input.now,
856
+ });
857
+ const approvalUrl = resolveMcpApprovalUrl(input.approvalUrl);
858
+ if (!approvalUrl) {
859
+ return errorResponse(500, "MCP_APPROVAL_URL_MISSING", "MCP OAuth approval URL is not configured");
860
+ }
861
+ const approve = new URL(approvalUrl);
862
+ approve.searchParams.set("mcp_authorization", created.authorizationId);
863
+ approve.searchParams.set("ps_origin", input.serverOrigin);
864
+ return Response.redirect(approve.toString(), 302);
865
+ }
866
+ catch (err) {
867
+ const message = err instanceof Error ? err.message : String(err);
868
+ return redirectWithOAuthError(redirectUri, err instanceof McpOAuthAuthorizationError
869
+ ? err.code
870
+ : "invalid_request", message, state);
871
+ }
872
+ }
873
+ if (pathname === "/mcp/oauth/token") {
874
+ if (!resolveMcpApprovalUrl(input.approvalUrl)) {
875
+ return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
876
+ }
877
+ if (input.request.method !== "POST") {
878
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
879
+ }
880
+ const body = await parseFormBody(input.request);
881
+ const grantType = body.get("grant_type") ?? "";
882
+ if (!isSupportedGrantType(grantType)) {
883
+ return jsonResponse({
884
+ error: "unsupported_grant_type",
885
+ error_description: `Supported grant types: ${SUPPORTED_GRANT_TYPES.join(", ")}`,
886
+ }, { status: 400 });
887
+ }
888
+ const clientId = body.get("client_id") ?? "";
889
+ try {
890
+ const token = grantType === REFRESH_TOKEN_GRANT
891
+ ? await refreshMcpOAuthToken({ refreshToken: body.get("refresh_token") ?? "", clientId }, { connectionStore: input.store, now: input.now })
892
+ : await redeemMcpOAuthAuthorizationCode({
893
+ authorizationCode: body.get("code") ?? "",
894
+ codeVerifier: body.get("code_verifier") ?? "",
895
+ clientId,
896
+ redirectUri: body.get("redirect_uri") ?? "",
897
+ }, {
898
+ authorizationStore: input.authorizationStore,
899
+ connectionStore: input.store,
900
+ now: input.now,
901
+ });
902
+ return jsonResponse({
903
+ access_token: token.accessToken,
904
+ token_type: "Bearer",
905
+ expires_in: token.expiresIn,
906
+ refresh_token: token.refreshToken,
907
+ ...(token.scope ? { scope: token.scope } : {}),
908
+ });
909
+ }
910
+ catch (err) {
911
+ return jsonResponse({
912
+ error: err instanceof McpOAuthAuthorizationError
913
+ ? err.code
914
+ : "invalid_grant",
915
+ error_description: err instanceof Error ? err.message : String(err),
916
+ }, { status: 400 });
917
+ }
918
+ }
919
+ if (pathname === ownerAuthorizationPrefix ||
920
+ pathname.startsWith(`${ownerAuthorizationPrefix}/`)) {
921
+ try {
922
+ await input.auth.authorizeOwner(input.request);
923
+ }
924
+ catch (err) {
925
+ if (err instanceof ProtocolError) {
926
+ return protocolErrorResponse(err);
927
+ }
928
+ throw err;
929
+ }
930
+ const tail = pathname.slice(ownerAuthorizationPrefix.length + 1);
931
+ const [id, action] = tail.split("/");
932
+ if (!id) {
933
+ return errorResponse(404, "NOT_FOUND", "Not found");
934
+ }
935
+ if (!action && input.request.method === "GET") {
936
+ const record = await input.authorizationStore.getById(id);
937
+ if (!record) {
938
+ return errorResponse(404, "NOT_FOUND", "Authorization not found");
939
+ }
940
+ return jsonResponse(toMcpOAuthAuthorizationView(record));
941
+ }
942
+ if (action === "approve") {
943
+ if (input.request.method !== "POST") {
944
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
945
+ }
946
+ let body = {};
947
+ try {
948
+ body = (await input.request.json());
949
+ }
950
+ catch {
951
+ return errorResponse(400, "INVALID_BODY", "Body must be JSON");
952
+ }
953
+ if (Array.isArray(body.scopes) && body.scopes.length > 0) {
954
+ if (!input.gateway || !input.gatewayConfig?.url) {
955
+ return errorResponse(500, "SERVER_NOT_CONFIGURED", "Gateway config is not configured");
956
+ }
957
+ try {
958
+ const approved = await approveMcpOAuthAuthorizationWithScopes({
959
+ authorizationId: id,
960
+ scopes: body.scopes,
961
+ ...(body.expiresAt !== undefined
962
+ ? { expiresAt: body.expiresAt }
963
+ : {}),
964
+ ...(body.nonce !== undefined ? { nonce: body.nonce } : {}),
965
+ }, {
966
+ connectionStore: input.store,
967
+ authorizationStore: input.authorizationStore,
968
+ gateway: input.gateway,
969
+ gatewayConfig: input.gatewayConfig,
970
+ gatewayUrl: input.gatewayConfig.url,
971
+ serverOwner: input.serverOwner,
972
+ serverSigner: input.serverSigner,
973
+ now: input.now,
974
+ });
975
+ return jsonResponse({ redirectTo: approved.redirectTo });
976
+ }
977
+ catch (err) {
978
+ if (err instanceof McpOAuthAuthorizationError) {
979
+ return errorResponse(err.status, err.code, err.message);
980
+ }
981
+ throw err;
982
+ }
983
+ }
984
+ if (!Array.isArray(body.grants) || body.grants.length === 0) {
985
+ return errorResponse(400, "GRANTS_REQUIRED", "Approve requires grants or scopes");
986
+ }
987
+ try {
988
+ const approved = await approveMcpOAuthAuthorization({ authorizationId: id, grants: body.grants }, {
989
+ connectionStore: input.store,
990
+ authorizationStore: input.authorizationStore,
991
+ now: input.now,
992
+ });
993
+ return jsonResponse({ redirectTo: approved.redirectTo });
994
+ }
995
+ catch (err) {
996
+ if (err instanceof McpOAuthAuthorizationError) {
997
+ return errorResponse(err.status, err.code, err.message);
998
+ }
999
+ throw err;
1000
+ }
1001
+ }
1002
+ return errorResponse(404, "NOT_FOUND", "Not found");
1003
+ }
1004
+ // Owner management endpoints
1005
+ if (pathname === ownerPrefix || pathname.startsWith(`${ownerPrefix}/`)) {
1006
+ try {
1007
+ await input.auth.authorizeOwner(input.request);
1008
+ }
1009
+ catch (err) {
1010
+ if (err instanceof ProtocolError) {
1011
+ return protocolErrorResponse(err);
1012
+ }
1013
+ throw err;
1014
+ }
1015
+ // POST /v1/mcp/connections — create
1016
+ if (pathname === ownerPrefix) {
1017
+ if (input.request.method === "POST") {
1018
+ let body = {};
1019
+ try {
1020
+ body = (await input.request.json());
1021
+ }
1022
+ catch {
1023
+ body = {};
1024
+ }
1025
+ const created = await createMcpConnection({ displayName: body.displayName }, {
1026
+ store: input.store,
1027
+ publicOrigin: input.serverOrigin,
1028
+ now: input.now,
1029
+ });
1030
+ return jsonResponse(created, { status: 201 });
1031
+ }
1032
+ if (input.request.method === "GET") {
1033
+ const records = await listMcpConnectionViews(input.store);
1034
+ return jsonResponse({ connections: records });
1035
+ }
1036
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
1037
+ }
1038
+ // /v1/mcp/connections/:id[/approve]
1039
+ const tail = pathname.slice(ownerPrefix.length + 1);
1040
+ const [id, action] = tail.split("/");
1041
+ if (!id) {
1042
+ return errorResponse(404, "NOT_FOUND", "Not found");
1043
+ }
1044
+ if (action === "approve") {
1045
+ if (input.request.method !== "POST") {
1046
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
1047
+ }
1048
+ let body = {};
1049
+ try {
1050
+ body = (await input.request.json());
1051
+ }
1052
+ catch {
1053
+ return errorResponse(400, "INVALID_BODY", "Body must be JSON");
1054
+ }
1055
+ if (!Array.isArray(body.grants) || body.grants.length === 0) {
1056
+ return errorResponse(400, "GRANTS_REQUIRED", "Approve requires at least one grant — mint grants in the consent flow first");
1057
+ }
1058
+ try {
1059
+ const updated = await approveMcpConnection({ connectionId: id, grants: body.grants }, { store: input.store, now: input.now });
1060
+ return jsonResponse(toMcpConnectionView(updated));
1061
+ }
1062
+ catch (err) {
1063
+ if (err instanceof McpConnectionNotFoundError) {
1064
+ return errorResponse(404, "NOT_FOUND", err.message);
1065
+ }
1066
+ if (err instanceof McpConnectionStateError) {
1067
+ return errorResponse(409, "INVALID_STATE", err.message);
1068
+ }
1069
+ throw err;
1070
+ }
1071
+ }
1072
+ if (!action) {
1073
+ if (input.request.method === "DELETE") {
1074
+ try {
1075
+ const updated = await revokeMcpConnection(id, {
1076
+ store: input.store,
1077
+ now: input.now,
1078
+ });
1079
+ return jsonResponse(toMcpConnectionView(updated));
1080
+ }
1081
+ catch (err) {
1082
+ if (err instanceof McpConnectionNotFoundError) {
1083
+ return errorResponse(404, "NOT_FOUND", err.message);
1084
+ }
1085
+ throw err;
1086
+ }
1087
+ }
1088
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
1089
+ }
1090
+ return errorResponse(404, "NOT_FOUND", "Not found");
1091
+ }
1092
+ async function handleMcpToken(rawToken, options) {
1093
+ if (!rawToken) {
1094
+ if (!options.oauthChallenge) {
1095
+ return errorResponse(401, "INVALID_TOKEN", "Missing MCP connection token");
1096
+ }
1097
+ return mcpUnauthorized(input.serverOrigin);
1098
+ }
1099
+ const tokenHash = await hashConnectionToken(rawToken);
1100
+ const record = await input.store.getByTokenHash(tokenHash);
1101
+ if (!record) {
1102
+ if (!options.oauthChallenge) {
1103
+ return errorResponse(401, "INVALID_TOKEN", "Unknown or revoked MCP connection");
1104
+ }
1105
+ return mcpUnauthorized(input.serverOrigin, "Unknown or revoked MCP connection");
1106
+ }
1107
+ const granteeAccount = loadMcpGranteeAccount({
1108
+ address: record.granteeAddress,
1109
+ publicKey: record.granteePublicKey,
1110
+ encryptedPrivateKey: record.encryptedGranteePrivateKey,
1111
+ });
1112
+ const readClient = createMcpDataReadClient({
1113
+ serverOrigin: input.serverOrigin,
1114
+ granteeAccount,
1115
+ dataApiDeps: {
1116
+ storage: input.dataStorage,
1117
+ auth: input.auth,
1118
+ accessLogWriter: input.accessLogWriter,
1119
+ readFulfillmentReporter: input.readFulfillmentReporter,
1120
+ runtimeAvailability: input.runtimeAvailability,
1121
+ now: input.now,
1122
+ createLogId,
1123
+ },
1124
+ });
1125
+ const response = await handleMcpStreamableHttpRequest(input.request, {
1126
+ connection: record,
1127
+ readClient,
1128
+ activityRecorder: input.activityRecorder,
1129
+ requestScopeAccess: async (request) => {
1130
+ return requestMcpScopeAccess({ connectionId: record.id, ...request }, { store: input.store, now: input.now });
1131
+ },
1132
+ });
1133
+ void input.store
1134
+ .update(record.id, {
1135
+ lastUsedAt: input.now().toISOString(),
1136
+ })
1137
+ .catch((err) => {
1138
+ console.warn("[mcp] failed to update lastUsedAt", err);
1139
+ });
1140
+ return response;
1141
+ }
1142
+ // Owner-only activity feed endpoint.
1143
+ if (pathname === "/v1/mcp/activity") {
1144
+ try {
1145
+ await input.auth.authorizeOwner(input.request);
1146
+ }
1147
+ catch (err) {
1148
+ if (err instanceof ProtocolError) {
1149
+ return protocolErrorResponse(err);
1150
+ }
1151
+ throw err;
1152
+ }
1153
+ if (input.request.method !== "GET") {
1154
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
1155
+ }
1156
+ const snapshot = input.activityRecorder
1157
+ ? input.activityRecorder.snapshot()
1158
+ : { events: [], running: 0, total: 0 };
1159
+ return jsonResponse(snapshot);
1160
+ }
1161
+ // Claude-facing stable endpoint: /mcp with Bearer token.
1162
+ if (pathname === "/mcp") {
1163
+ return handleMcpToken(bearerToken(input.request), {
1164
+ oauthChallenge: Boolean(resolveMcpApprovalUrl(input.approvalUrl)),
1165
+ });
1166
+ }
1167
+ // Legacy Claude-facing endpoint: /mcp/:token
1168
+ const mcpPrefix = "/mcp/";
1169
+ if (pathname.startsWith(mcpPrefix)) {
1170
+ const rawToken = decodeURIComponent(pathname.slice(mcpPrefix.length));
1171
+ if (!rawToken || rawToken.includes("/")) {
1172
+ return errorResponse(404, "NOT_FOUND", "Not found");
1173
+ }
1174
+ return handleMcpToken(rawToken, { oauthChallenge: false });
1175
+ }
1176
+ return null;
1177
+ }
1178
+ //# sourceMappingURL=runtime.js.map