@opendatalabs/personal-server-ts-lite 0.0.1-canary.01ca694
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.
- package/dist/bridge.d.ts +23 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +53 -0
- package/dist/bridge.js.map +1 -0
- package/dist/browser-runtime.d.ts +31 -0
- package/dist/browser-runtime.d.ts.map +1 -0
- package/dist/browser-runtime.js +109 -0
- package/dist/browser-runtime.js.map +1 -0
- package/dist/browser-tls-rustls/browser_tls_rustls.d.ts +49 -0
- package/dist/browser-tls-rustls/browser_tls_rustls.js +401 -0
- package/dist/browser-tls-rustls/browser_tls_rustls_bg.wasm +0 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +376 -0
- package/dist/client.js.map +1 -0
- package/dist/diagnostics.d.ts +166 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +265 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-store.d.ts +35 -0
- package/dist/mcp-store.d.ts.map +1 -0
- package/dist/mcp-store.js +177 -0
- package/dist/mcp-store.js.map +1 -0
- package/dist/owner-binding.d.ts +7 -0
- package/dist/owner-binding.d.ts.map +1 -0
- package/dist/owner-binding.js +18 -0
- package/dist/owner-binding.js.map +1 -0
- package/dist/relay-tls.d.ts +19 -0
- package/dist/relay-tls.d.ts.map +1 -0
- package/dist/relay-tls.js +226 -0
- package/dist/relay-tls.js.map +1 -0
- package/dist/relay.d.ts +70 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +368 -0
- package/dist/relay.js.map +1 -0
- package/dist/runtime.d.ts +95 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +991 -0
- package/dist/runtime.js.map +1 -0
- package/dist/state.d.ts +62 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +296 -0
- package/dist/state.js.map +1 -0
- package/dist/storage-utils.d.ts +8 -0
- package/dist/storage-utils.d.ts.map +1 -0
- package/dist/storage-utils.js +67 -0
- package/dist/storage-utils.js.map +1 -0
- package/dist/storage.d.ts +57 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +575 -0
- package/dist/storage.js.map +1 -0
- package/dist/sync.d.ts +28 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +158 -0
- package/dist/sync.js.map +1 -0
- package/package.json +40 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
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 { verifyDataReadPolicy, } from "@opendatalabs/personal-server-ts-core/policy";
|
|
6
|
+
import { approveMcpOAuthAuthorization, approveMcpOAuthAuthorizationWithScopes, approveMcpConnection, buildMcpProtectedResourceMetadataUrl, buildStableMcpUrl, createInMemoryMcpOAuthAuthorizationStore, createMcpConnection, createMcpOAuthAuthorization, createMcpDataReadClient, handleMcpStreamableHttpRequest, hashConnectionToken, listMcpConnectionViews, loadMcpGranteeAccount, McpConnectionNotFoundError, McpConnectionStateError, McpOAuthAuthorizationError, redeemMcpOAuthAuthorizationCode, toMcpOAuthAuthorizationView, revokeMcpConnection, toMcpConnectionView, McpActivityRecorder, } from "@opendatalabs/personal-server-ts-core/mcp";
|
|
7
|
+
import { createIndexedDbPsLiteAccessLogStore, createIndexedDbPsLiteStateStore, createIndexedDbPsLiteTokenStore, savePsLiteConfig, } from "./state.js";
|
|
8
|
+
import { collectDiagnosticsWithTimeout, DiagnosticsRecorder, } from "./diagnostics.js";
|
|
9
|
+
function jsonResponse(body, init) {
|
|
10
|
+
const headers = new Headers(init?.headers);
|
|
11
|
+
headers.set("Content-Type", "application/json");
|
|
12
|
+
return new Response(JSON.stringify(body), { ...init, headers });
|
|
13
|
+
}
|
|
14
|
+
function protocolErrorResponse(err) {
|
|
15
|
+
return jsonResponse(err.toJSON(), { status: err.code });
|
|
16
|
+
}
|
|
17
|
+
function errorResponse(status, errorCode, message) {
|
|
18
|
+
return jsonResponse({
|
|
19
|
+
error: {
|
|
20
|
+
code: status,
|
|
21
|
+
errorCode,
|
|
22
|
+
message,
|
|
23
|
+
},
|
|
24
|
+
}, { status });
|
|
25
|
+
}
|
|
26
|
+
function mcpUnauthorized(origin, message = "MCP authorization required") {
|
|
27
|
+
return jsonResponse({
|
|
28
|
+
error: {
|
|
29
|
+
code: 401,
|
|
30
|
+
errorCode: "MCP_AUTH_REQUIRED",
|
|
31
|
+
message,
|
|
32
|
+
},
|
|
33
|
+
}, {
|
|
34
|
+
status: 401,
|
|
35
|
+
headers: {
|
|
36
|
+
"WWW-Authenticate": `Bearer resource_metadata="${buildMcpProtectedResourceMetadataUrl(origin)}", scope="vana:read"`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function authorizationServerMetadata(origin) {
|
|
41
|
+
return {
|
|
42
|
+
issuer: origin,
|
|
43
|
+
authorization_endpoint: `${origin}/mcp/oauth/authorize`,
|
|
44
|
+
token_endpoint: `${origin}/mcp/oauth/token`,
|
|
45
|
+
registration_endpoint: `${origin}/mcp/oauth/register`,
|
|
46
|
+
response_types_supported: ["code"],
|
|
47
|
+
grant_types_supported: ["authorization_code"],
|
|
48
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
49
|
+
code_challenge_methods_supported: ["S256"],
|
|
50
|
+
scopes_supported: ["vana:read"],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function protectedResourceMetadata(origin) {
|
|
54
|
+
return {
|
|
55
|
+
resource: buildStableMcpUrl(origin),
|
|
56
|
+
authorization_servers: [origin],
|
|
57
|
+
bearer_methods_supported: ["header"],
|
|
58
|
+
scopes_supported: ["vana:read"],
|
|
59
|
+
resource_name: "Vana Personal Server MCP",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function resolveMcpApprovalUrl(value) {
|
|
63
|
+
if (!value)
|
|
64
|
+
return null;
|
|
65
|
+
return typeof value === "function" ? value() : value;
|
|
66
|
+
}
|
|
67
|
+
function redirectWithOAuthError(redirectUri, error, description, state) {
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(redirectUri);
|
|
70
|
+
url.searchParams.set("error", error);
|
|
71
|
+
url.searchParams.set("error_description", description);
|
|
72
|
+
if (state)
|
|
73
|
+
url.searchParams.set("state", state);
|
|
74
|
+
return Response.redirect(url.toString(), 302);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return jsonResponse({
|
|
78
|
+
error,
|
|
79
|
+
error_description: description,
|
|
80
|
+
...(state ? { state } : {}),
|
|
81
|
+
}, { status: 400 });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function parseFormBody(request) {
|
|
85
|
+
return new URLSearchParams(await request.text());
|
|
86
|
+
}
|
|
87
|
+
function unavailableResponse() {
|
|
88
|
+
const err = new PsUnavailableError({
|
|
89
|
+
runtime: "ps-lite",
|
|
90
|
+
reason: "Browser runtime is inactive",
|
|
91
|
+
});
|
|
92
|
+
return protocolErrorResponse(err);
|
|
93
|
+
}
|
|
94
|
+
function parseBearerToken(request) {
|
|
95
|
+
const authorization = request.headers.get("authorization");
|
|
96
|
+
if (!authorization)
|
|
97
|
+
return null;
|
|
98
|
+
const match = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
99
|
+
return match?.[1] ?? null;
|
|
100
|
+
}
|
|
101
|
+
function assertBearerToken(request, expectedToken, ownerOnly = false) {
|
|
102
|
+
const token = parseBearerToken(request);
|
|
103
|
+
if (!token) {
|
|
104
|
+
throw new MissingAuthError();
|
|
105
|
+
}
|
|
106
|
+
if (token !== expectedToken) {
|
|
107
|
+
throw ownerOnly ? new NotOwnerError() : new InvalidSignatureError();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function createMissingAuthAdapter() {
|
|
111
|
+
return {
|
|
112
|
+
async authorizeOwner() {
|
|
113
|
+
throw new MissingAuthError();
|
|
114
|
+
},
|
|
115
|
+
async authorizeBuilderList() {
|
|
116
|
+
throw new MissingAuthError();
|
|
117
|
+
},
|
|
118
|
+
async authorizeBuilderRead() {
|
|
119
|
+
throw new MissingAuthError();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function createBearerTokenPsLiteAuth(options) {
|
|
124
|
+
return {
|
|
125
|
+
async authorizeOwner(request) {
|
|
126
|
+
assertBearerToken(request, options.ownerToken, true);
|
|
127
|
+
},
|
|
128
|
+
async authorizeBuilderList(request) {
|
|
129
|
+
if (parseBearerToken(request) === options.ownerToken)
|
|
130
|
+
return;
|
|
131
|
+
assertBearerToken(request, options.builderToken);
|
|
132
|
+
},
|
|
133
|
+
async authorizeBuilderRead(input) {
|
|
134
|
+
if (parseBearerToken(input.request) === options.ownerToken) {
|
|
135
|
+
return { builder: "owner", grantId: "owner" };
|
|
136
|
+
}
|
|
137
|
+
assertBearerToken(input.request, options.builderToken);
|
|
138
|
+
if (!input.grantId) {
|
|
139
|
+
throw new GrantRequiredError({
|
|
140
|
+
reason: "No grantId in request",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return { grantId: input.grantId };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function authenticatePsLiteRequest(request, options) {
|
|
148
|
+
return authenticateRequest({
|
|
149
|
+
request,
|
|
150
|
+
serverOrigin: options.origin,
|
|
151
|
+
serverOwner: options.ownerAddress,
|
|
152
|
+
accessToken: options.accessToken,
|
|
153
|
+
sessionTokenVerifier: options.tokenStore,
|
|
154
|
+
now: options.now,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function isOwnerSigner(auth, ownerAddress) {
|
|
158
|
+
return auth.auth.signer.toLowerCase() === ownerAddress.toLowerCase();
|
|
159
|
+
}
|
|
160
|
+
function dataReadPolicyPortsRequired() {
|
|
161
|
+
return new ProtocolError(500, "SERVER_NOT_CONFIGURED", "Server is not configured", {
|
|
162
|
+
reason: "PS Lite read policy ports are not configured",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export function createWeb3SignedPsLiteAuth(options) {
|
|
166
|
+
return {
|
|
167
|
+
async authorizeOwner(request) {
|
|
168
|
+
const auth = await authenticatePsLiteRequest(request, options);
|
|
169
|
+
if (!isOwnerSigner(auth, options.ownerAddress)) {
|
|
170
|
+
throw new NotOwnerError({
|
|
171
|
+
expected: options.ownerAddress,
|
|
172
|
+
actual: auth.auth.signer,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async authorizeBuilderList(request) {
|
|
177
|
+
const auth = await authenticatePsLiteRequest(request, options);
|
|
178
|
+
if (auth.isPolicyBypass || isOwnerSigner(auth, options.ownerAddress)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!options.dataReadPolicyPorts) {
|
|
182
|
+
throw dataReadPolicyPortsRequired();
|
|
183
|
+
}
|
|
184
|
+
const builder = await options.dataReadPolicyPorts.authSessionVerifier.getBuilder(auth.auth.signer);
|
|
185
|
+
if (!builder) {
|
|
186
|
+
throw new UnregisteredBuilderError();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
async authorizeBuilderRead(input) {
|
|
190
|
+
const auth = await authenticatePsLiteRequest(input.request, options);
|
|
191
|
+
if (auth.isPolicyBypass) {
|
|
192
|
+
return { builder: auth.auth.signer, grantId: "policy-bypass" };
|
|
193
|
+
}
|
|
194
|
+
if (isOwnerSigner(auth, options.ownerAddress)) {
|
|
195
|
+
return { builder: auth.auth.signer, grantId: "owner" };
|
|
196
|
+
}
|
|
197
|
+
if (!options.dataReadPolicyPorts) {
|
|
198
|
+
throw dataReadPolicyPortsRequired();
|
|
199
|
+
}
|
|
200
|
+
const grant = await verifyDataReadPolicy({
|
|
201
|
+
signer: auth.auth.signer,
|
|
202
|
+
grantId: auth.auth.payload.grantId ?? input.grantId,
|
|
203
|
+
requestedScope: input.scope,
|
|
204
|
+
fileId: input.fileId,
|
|
205
|
+
}, options.dataReadPolicyPorts);
|
|
206
|
+
return { builder: auth.auth.signer, grantId: grant.id };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function toDataStoragePort(storage) {
|
|
211
|
+
if ("listScopes" in storage) {
|
|
212
|
+
return storage;
|
|
213
|
+
}
|
|
214
|
+
throw new Error("PS Lite runtime requires a persistent DataStoragePort. Use createIndexedDbPsLiteRuntime() or createPersistentPsLiteStorage().");
|
|
215
|
+
}
|
|
216
|
+
function indexedDbAvailable() {
|
|
217
|
+
return typeof indexedDB !== "undefined";
|
|
218
|
+
}
|
|
219
|
+
function createDefaultAccessLogStore() {
|
|
220
|
+
if (!indexedDbAvailable()) {
|
|
221
|
+
throw new Error("IndexedDB is required for default PS Lite access log persistence.");
|
|
222
|
+
}
|
|
223
|
+
return createIndexedDbPsLiteAccessLogStore();
|
|
224
|
+
}
|
|
225
|
+
function createLogId() {
|
|
226
|
+
return globalThis.crypto?.randomUUID?.() ?? `log-${Date.now()}`;
|
|
227
|
+
}
|
|
228
|
+
function randomHex(byteLength) {
|
|
229
|
+
const bytes = new Uint8Array(byteLength);
|
|
230
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
231
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
232
|
+
}
|
|
233
|
+
function createDefaultTokenStore() {
|
|
234
|
+
if (!indexedDbAvailable()) {
|
|
235
|
+
throw new Error("IndexedDB is required for default PS Lite token storage.");
|
|
236
|
+
}
|
|
237
|
+
return createIndexedDbPsLiteTokenStore();
|
|
238
|
+
}
|
|
239
|
+
function createDefaultSaveConfig() {
|
|
240
|
+
if (!indexedDbAvailable()) {
|
|
241
|
+
throw new Error("IndexedDB is required for default PS Lite config persistence.");
|
|
242
|
+
}
|
|
243
|
+
const stateStore = createIndexedDbPsLiteStateStore();
|
|
244
|
+
return async (nextConfig) => {
|
|
245
|
+
await savePsLiteConfig(stateStore, nextConfig);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function bearerToken(request) {
|
|
249
|
+
const authorization = request.headers.get("authorization");
|
|
250
|
+
if (!authorization?.startsWith("Bearer "))
|
|
251
|
+
return null;
|
|
252
|
+
return authorization.slice(7);
|
|
253
|
+
}
|
|
254
|
+
export function createPsLiteRuntime(options) {
|
|
255
|
+
let active = options.active ?? false;
|
|
256
|
+
const now = options.now ?? (() => new Date());
|
|
257
|
+
const auth = options.auth ?? createMissingAuthAdapter();
|
|
258
|
+
const dataStorage = toDataStoragePort(options.storage);
|
|
259
|
+
// Wire diagnostics by default so GET /v1/diagnostics is always available.
|
|
260
|
+
const diagnostics = options.diagnostics ?? new DiagnosticsRecorder();
|
|
261
|
+
options = { ...options, diagnostics };
|
|
262
|
+
diagnostics.push({ phase: "booting", detail: "runtime created" });
|
|
263
|
+
let accessLogReader = options.accessLogReader;
|
|
264
|
+
let accessLogWriter = options.accessLogWriter;
|
|
265
|
+
if (!accessLogReader || !accessLogWriter) {
|
|
266
|
+
const accessLogStore = createDefaultAccessLogStore();
|
|
267
|
+
accessLogReader ??= accessLogStore;
|
|
268
|
+
accessLogWriter ??= accessLogStore;
|
|
269
|
+
}
|
|
270
|
+
const tokenStore = options.tokenStore ?? createDefaultTokenStore();
|
|
271
|
+
const saveConfig = options.saveConfig ?? createDefaultSaveConfig();
|
|
272
|
+
const deviceSessions = createMemoryDeviceSessionStore();
|
|
273
|
+
const mcpOAuthAuthorizationStore = options.mcpOAuthAuthorizationStore ??
|
|
274
|
+
createInMemoryMcpOAuthAuthorizationStore();
|
|
275
|
+
const activityRecorder = options.mcpConnectionStore
|
|
276
|
+
? new McpActivityRecorder()
|
|
277
|
+
: undefined;
|
|
278
|
+
async function withProtocolErrors(handler) {
|
|
279
|
+
try {
|
|
280
|
+
return await handler();
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (err instanceof ProtocolError) {
|
|
284
|
+
return protocolErrorResponse(err);
|
|
285
|
+
}
|
|
286
|
+
return errorResponse(500, "INTERNAL_ERROR", "Internal server error");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function sendContractResult(result) {
|
|
290
|
+
return jsonResponse(result.body, { status: result.status });
|
|
291
|
+
}
|
|
292
|
+
function ownerAddress() {
|
|
293
|
+
return options.serverOwner ?? options.identity?.address;
|
|
294
|
+
}
|
|
295
|
+
async function handleAuthDevice(request, url) {
|
|
296
|
+
if (url.pathname === "/auth/device") {
|
|
297
|
+
if (request.method !== "POST") {
|
|
298
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
299
|
+
}
|
|
300
|
+
return sendContractResult(initiateDeviceSessionContract({
|
|
301
|
+
sessionStore: deviceSessions,
|
|
302
|
+
serverOwner: ownerAddress(),
|
|
303
|
+
requestOrigin: url.origin,
|
|
304
|
+
approvalOrigin: url.origin,
|
|
305
|
+
sessionId: randomHex(32),
|
|
306
|
+
pollToken: randomHex(32),
|
|
307
|
+
now: now().getTime(),
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
if (url.pathname === "/auth/device/poll") {
|
|
311
|
+
if (request.method !== "GET") {
|
|
312
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
313
|
+
}
|
|
314
|
+
return sendContractResult(pollDeviceSessionContract({
|
|
315
|
+
sessionStore: deviceSessions,
|
|
316
|
+
pollToken: url.searchParams.get("token"),
|
|
317
|
+
serverOwner: ownerAddress(),
|
|
318
|
+
now: now().getTime(),
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
if (url.pathname === "/auth/device/approve") {
|
|
322
|
+
const sessionId = url.searchParams.get("session");
|
|
323
|
+
if (!sessionId) {
|
|
324
|
+
return request.method === "GET"
|
|
325
|
+
? new Response("Missing session parameter", { status: 400 })
|
|
326
|
+
: jsonResponse({ error: { code: 400, message: "Missing session parameter" } }, { status: 400 });
|
|
327
|
+
}
|
|
328
|
+
const session = deviceSessions.get(sessionId);
|
|
329
|
+
if (!session) {
|
|
330
|
+
return request.method === "GET"
|
|
331
|
+
? new Response("Session expired or invalid", { status: 404 })
|
|
332
|
+
: jsonResponse({ error: { code: 404, message: "Session expired or invalid" } }, { status: 404 });
|
|
333
|
+
}
|
|
334
|
+
if (request.method === "GET") {
|
|
335
|
+
return new Response("Device authorization pending", {
|
|
336
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (request.method !== "POST") {
|
|
340
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
341
|
+
}
|
|
342
|
+
if (session.status === "approved") {
|
|
343
|
+
return jsonResponse({ status: "already_approved" });
|
|
344
|
+
}
|
|
345
|
+
await auth.authorizeOwner(request);
|
|
346
|
+
return sendContractResult(await approveDeviceSessionContract({
|
|
347
|
+
sessionStore: deviceSessions,
|
|
348
|
+
tokenStore,
|
|
349
|
+
sessionId,
|
|
350
|
+
serverOwner: ownerAddress(),
|
|
351
|
+
accessToken: `vana_ps_${randomHex(32)}`,
|
|
352
|
+
now: now().getTime(),
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
if (url.pathname === "/auth/device/token") {
|
|
356
|
+
if (request.method === "DELETE") {
|
|
357
|
+
const token = bearerToken(request);
|
|
358
|
+
if (!token) {
|
|
359
|
+
return jsonResponse({ error: { code: 401, message: "Missing Bearer token" } }, { status: 401 });
|
|
360
|
+
}
|
|
361
|
+
return sendContractResult(await revokeDeviceTokenContract({
|
|
362
|
+
tokenStore,
|
|
363
|
+
bearerToken: token,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
if (request.method !== "POST") {
|
|
367
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
368
|
+
}
|
|
369
|
+
const token = bearerToken(request);
|
|
370
|
+
if (!options.accessToken || token !== options.accessToken) {
|
|
371
|
+
return jsonResponse({
|
|
372
|
+
error: {
|
|
373
|
+
code: 403,
|
|
374
|
+
message: "Only control-plane tokens can provision Personal Server session tokens",
|
|
375
|
+
},
|
|
376
|
+
}, { status: 403 });
|
|
377
|
+
}
|
|
378
|
+
let body;
|
|
379
|
+
try {
|
|
380
|
+
body = (await request.json());
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return jsonResponse({ error: { code: 400, message: "Request body must be valid JSON" } }, { status: 400 });
|
|
384
|
+
}
|
|
385
|
+
if (!body.token || typeof body.token !== "string") {
|
|
386
|
+
return jsonResponse({ error: { code: 400, message: "Missing token" } }, { status: 400 });
|
|
387
|
+
}
|
|
388
|
+
return sendContractResult(await provisionDeviceTokenContract({
|
|
389
|
+
tokenStore,
|
|
390
|
+
body,
|
|
391
|
+
now: now().getTime(),
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
kind: "ps-lite",
|
|
398
|
+
storage: options.storage,
|
|
399
|
+
activate() {
|
|
400
|
+
active = true;
|
|
401
|
+
options.syncManager?.start?.();
|
|
402
|
+
},
|
|
403
|
+
deactivate() {
|
|
404
|
+
active = false;
|
|
405
|
+
void options.syncManager?.stop?.();
|
|
406
|
+
},
|
|
407
|
+
isAvailable() {
|
|
408
|
+
return active;
|
|
409
|
+
},
|
|
410
|
+
async fetch(request) {
|
|
411
|
+
return withProtocolErrors(async () => {
|
|
412
|
+
const url = new URL(request.url);
|
|
413
|
+
if (url.pathname === "/health") {
|
|
414
|
+
const apiOrigin = url.origin;
|
|
415
|
+
const identity = options.identity ?? null;
|
|
416
|
+
let serverId = null;
|
|
417
|
+
if (identity && options.gateway) {
|
|
418
|
+
try {
|
|
419
|
+
const server = await options.gateway.getServer(identity.address);
|
|
420
|
+
serverId = server?.id ?? null;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
serverId = null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const registration = options.serverOwner && identity
|
|
427
|
+
? {
|
|
428
|
+
ownerAddress: options.serverOwner,
|
|
429
|
+
serverAddress: identity.address,
|
|
430
|
+
publicKey: identity.publicKey,
|
|
431
|
+
serverUrl: apiOrigin,
|
|
432
|
+
serverId,
|
|
433
|
+
registered: Boolean(serverId),
|
|
434
|
+
}
|
|
435
|
+
: null;
|
|
436
|
+
const capabilities = dataStorage.capabilities;
|
|
437
|
+
const stateCapabilities = {
|
|
438
|
+
tokens: tokenStore
|
|
439
|
+
.capabilities?.tokens ?? "custom",
|
|
440
|
+
accessLogs: accessLogReader.capabilities?.accessLogs ?? "custom",
|
|
441
|
+
config: options.stateCapabilities?.config ??
|
|
442
|
+
(options.saveConfig ? "custom" : "indexeddb"),
|
|
443
|
+
};
|
|
444
|
+
return jsonResponse({
|
|
445
|
+
status: active ? "healthy" : "unavailable",
|
|
446
|
+
runtime: "ps-lite",
|
|
447
|
+
storage: options.storage.kind,
|
|
448
|
+
capabilities: capabilities ?? null,
|
|
449
|
+
stateCapabilities,
|
|
450
|
+
owner: options.serverOwner ?? null,
|
|
451
|
+
apiOrigin,
|
|
452
|
+
gatewayUrl: options.config?.gateway?.url ?? null,
|
|
453
|
+
gatewayConfig: options.config?.gateway ?? null,
|
|
454
|
+
identity,
|
|
455
|
+
registration,
|
|
456
|
+
active,
|
|
457
|
+
checkedAt: now().toISOString(),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (url.pathname === "/v1/diagnostics") {
|
|
461
|
+
if (request.method !== "GET") {
|
|
462
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
await auth.authorizeOwner(request);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
if (err instanceof ProtocolError) {
|
|
469
|
+
return protocolErrorResponse(err);
|
|
470
|
+
}
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
if (!options.diagnostics) {
|
|
474
|
+
return jsonResponse({
|
|
475
|
+
error: {
|
|
476
|
+
code: 404,
|
|
477
|
+
errorCode: "DIAGNOSTICS_NOT_CONFIGURED",
|
|
478
|
+
message: "Diagnostics recorder not configured for this runtime",
|
|
479
|
+
},
|
|
480
|
+
}, { status: 404 });
|
|
481
|
+
}
|
|
482
|
+
const syncStatus = options.syncManager?.getStatus() ?? null;
|
|
483
|
+
const snapshot = await collectDiagnosticsWithTimeout(options.diagnostics, {
|
|
484
|
+
runtimeActive: active,
|
|
485
|
+
syncStatus,
|
|
486
|
+
storage: dataStorage,
|
|
487
|
+
});
|
|
488
|
+
return jsonResponse(snapshot);
|
|
489
|
+
}
|
|
490
|
+
if (!active) {
|
|
491
|
+
return unavailableResponse();
|
|
492
|
+
}
|
|
493
|
+
const dataPrefix = "/v1/data";
|
|
494
|
+
if (url.pathname === dataPrefix ||
|
|
495
|
+
url.pathname.startsWith(`${dataPrefix}/`)) {
|
|
496
|
+
return handlePersonalServerDataRequest(request, {
|
|
497
|
+
storage: dataStorage,
|
|
498
|
+
auth,
|
|
499
|
+
schemaResolver: options.gateway,
|
|
500
|
+
accessLogWriter,
|
|
501
|
+
syncManager: options.syncManager ?? null,
|
|
502
|
+
now,
|
|
503
|
+
createLogId,
|
|
504
|
+
}, { basePath: dataPrefix });
|
|
505
|
+
}
|
|
506
|
+
if (url.pathname.startsWith("/auth/device")) {
|
|
507
|
+
const response = await handleAuthDevice(request, url);
|
|
508
|
+
if (response)
|
|
509
|
+
return response;
|
|
510
|
+
}
|
|
511
|
+
if (url.pathname === "/oauth/token") {
|
|
512
|
+
return handlePersonalServerOauthTokenRequest(request, {
|
|
513
|
+
tokenStore,
|
|
514
|
+
controlPlaneSecret: options.accessToken,
|
|
515
|
+
randomToken: () => `vana_ps_${randomHex(32)}`,
|
|
516
|
+
now,
|
|
517
|
+
deviceSessions: {
|
|
518
|
+
findByDeviceCode(deviceCode) {
|
|
519
|
+
const session = deviceSessions.findByPollToken(deviceCode);
|
|
520
|
+
if (!session)
|
|
521
|
+
return null;
|
|
522
|
+
return {
|
|
523
|
+
status: session.status,
|
|
524
|
+
accessToken: session.accessToken,
|
|
525
|
+
accessTokenExpiresAt: session.accessTokenExpiresAt,
|
|
526
|
+
sessionId: session.sessionId,
|
|
527
|
+
};
|
|
528
|
+
},
|
|
529
|
+
consume(sessionId) {
|
|
530
|
+
deviceSessions.delete(sessionId);
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const grantsPrefix = "/v1/grants";
|
|
536
|
+
if (url.pathname === grantsPrefix ||
|
|
537
|
+
url.pathname === `${grantsPrefix}/` ||
|
|
538
|
+
url.pathname.startsWith(`${grantsPrefix}/`)) {
|
|
539
|
+
return handlePersonalServerGrantsRequest(request, {
|
|
540
|
+
auth,
|
|
541
|
+
gateway: options.gateway,
|
|
542
|
+
gatewayConfig: options.config?.gateway,
|
|
543
|
+
serverOwner: options.serverOwner ?? options.identity?.address,
|
|
544
|
+
serverSigner: options.serverSigner,
|
|
545
|
+
now,
|
|
546
|
+
}, { basePath: grantsPrefix });
|
|
547
|
+
}
|
|
548
|
+
const accessLogsPrefix = "/v1/access-logs";
|
|
549
|
+
if (url.pathname === accessLogsPrefix ||
|
|
550
|
+
url.pathname === `${accessLogsPrefix}/`) {
|
|
551
|
+
return handlePersonalServerAccessLogsRequest(request, { auth, accessLogReader }, { basePath: accessLogsPrefix });
|
|
552
|
+
}
|
|
553
|
+
const syncPrefix = "/v1/sync";
|
|
554
|
+
if (url.pathname === `${syncPrefix}/trigger` ||
|
|
555
|
+
url.pathname === `${syncPrefix}/status` ||
|
|
556
|
+
url.pathname.startsWith(`${syncPrefix}/file/`)) {
|
|
557
|
+
return handlePersonalServerSyncRequest(request, { auth, syncManager: options.syncManager ?? null }, { basePath: syncPrefix });
|
|
558
|
+
}
|
|
559
|
+
if (url.pathname === "/ui/api/config") {
|
|
560
|
+
return handlePersonalServerConfigRequest(request, {
|
|
561
|
+
auth,
|
|
562
|
+
async readConfig() {
|
|
563
|
+
return options.config ?? {};
|
|
564
|
+
},
|
|
565
|
+
async writeConfig(config) {
|
|
566
|
+
await saveConfig(config);
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (options.mcpConnectionStore) {
|
|
571
|
+
const mcpResponse = await handleMcpRoute({
|
|
572
|
+
request,
|
|
573
|
+
url,
|
|
574
|
+
store: options.mcpConnectionStore,
|
|
575
|
+
authorizationStore: mcpOAuthAuthorizationStore,
|
|
576
|
+
approvalUrl: options.mcpOAuthApprovalUrl,
|
|
577
|
+
auth,
|
|
578
|
+
dataStorage,
|
|
579
|
+
schemaResolver: options.gateway,
|
|
580
|
+
accessLogWriter,
|
|
581
|
+
now,
|
|
582
|
+
runtimeAvailability: { isAvailable: () => active },
|
|
583
|
+
serverOrigin: url.origin,
|
|
584
|
+
gateway: options.gateway,
|
|
585
|
+
gatewayConfig: options.config?.gateway,
|
|
586
|
+
serverOwner: options.serverOwner ?? options.identity?.address,
|
|
587
|
+
serverSigner: options.serverSigner,
|
|
588
|
+
activityRecorder,
|
|
589
|
+
});
|
|
590
|
+
if (mcpResponse)
|
|
591
|
+
return mcpResponse;
|
|
592
|
+
}
|
|
593
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* MCP route dispatcher used inside `createPsLiteRuntime.fetch`. Handles both
|
|
600
|
+
* the owner-only `/v1/mcp/connections` management endpoints and the public
|
|
601
|
+
* `/mcp/:connectionToken` Streamable HTTP endpoint that Claude dials.
|
|
602
|
+
*
|
|
603
|
+
* Returns `null` for paths that don't match — the caller continues to the
|
|
604
|
+
* 404. Errors are passed through; the caller is already inside
|
|
605
|
+
* `withProtocolErrors`.
|
|
606
|
+
*/
|
|
607
|
+
async function handleMcpRoute(input) {
|
|
608
|
+
const pathname = input.url.pathname;
|
|
609
|
+
const ownerPrefix = "/v1/mcp/connections";
|
|
610
|
+
const ownerAuthorizationPrefix = "/v1/mcp/oauth/authorizations";
|
|
611
|
+
if (pathname === "/.well-known/oauth-protected-resource/mcp") {
|
|
612
|
+
if (!resolveMcpApprovalUrl(input.approvalUrl)) {
|
|
613
|
+
return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
|
|
614
|
+
}
|
|
615
|
+
return jsonResponse(protectedResourceMetadata(input.serverOrigin));
|
|
616
|
+
}
|
|
617
|
+
if (pathname === "/.well-known/oauth-authorization-server") {
|
|
618
|
+
if (!resolveMcpApprovalUrl(input.approvalUrl)) {
|
|
619
|
+
return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
|
|
620
|
+
}
|
|
621
|
+
return jsonResponse(authorizationServerMetadata(input.serverOrigin));
|
|
622
|
+
}
|
|
623
|
+
if (pathname === "/mcp/oauth/register") {
|
|
624
|
+
if (!resolveMcpApprovalUrl(input.approvalUrl)) {
|
|
625
|
+
return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
|
|
626
|
+
}
|
|
627
|
+
if (input.request.method !== "POST") {
|
|
628
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
629
|
+
}
|
|
630
|
+
let body = {};
|
|
631
|
+
try {
|
|
632
|
+
body = (await input.request.json());
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
body = {};
|
|
636
|
+
}
|
|
637
|
+
return jsonResponse({
|
|
638
|
+
client_id: `mcp-client-${crypto.randomUUID()}`,
|
|
639
|
+
client_name: body.client_name ?? "Claude",
|
|
640
|
+
redirect_uris: Array.isArray(body.redirect_uris)
|
|
641
|
+
? body.redirect_uris
|
|
642
|
+
: [],
|
|
643
|
+
token_endpoint_auth_method: "none",
|
|
644
|
+
grant_types: ["authorization_code"],
|
|
645
|
+
response_types: ["code"],
|
|
646
|
+
}, { status: 201 });
|
|
647
|
+
}
|
|
648
|
+
if (pathname === "/mcp/oauth/authorize") {
|
|
649
|
+
if (!resolveMcpApprovalUrl(input.approvalUrl)) {
|
|
650
|
+
return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
|
|
651
|
+
}
|
|
652
|
+
if (input.request.method !== "GET") {
|
|
653
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
654
|
+
}
|
|
655
|
+
const responseType = input.url.searchParams.get("response_type");
|
|
656
|
+
const clientId = input.url.searchParams.get("client_id") ?? "";
|
|
657
|
+
const redirectUri = input.url.searchParams.get("redirect_uri") ?? "";
|
|
658
|
+
const codeChallenge = input.url.searchParams.get("code_challenge") ?? "";
|
|
659
|
+
const codeChallengeMethod = input.url.searchParams.get("code_challenge_method") ?? "";
|
|
660
|
+
const state = input.url.searchParams.get("state");
|
|
661
|
+
const scope = input.url.searchParams.get("scope") ?? "vana:read";
|
|
662
|
+
if (responseType !== "code") {
|
|
663
|
+
return redirectWithOAuthError(redirectUri, "unsupported_response_type", "Only response_type=code is supported", state);
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const created = await createMcpOAuthAuthorization({
|
|
667
|
+
clientId,
|
|
668
|
+
redirectUri,
|
|
669
|
+
codeChallenge,
|
|
670
|
+
codeChallengeMethod,
|
|
671
|
+
scope,
|
|
672
|
+
...(state ? { state } : {}),
|
|
673
|
+
}, {
|
|
674
|
+
connectionStore: input.store,
|
|
675
|
+
authorizationStore: input.authorizationStore,
|
|
676
|
+
publicOrigin: input.serverOrigin,
|
|
677
|
+
now: input.now,
|
|
678
|
+
});
|
|
679
|
+
const approvalUrl = resolveMcpApprovalUrl(input.approvalUrl);
|
|
680
|
+
if (!approvalUrl) {
|
|
681
|
+
return errorResponse(500, "MCP_APPROVAL_URL_MISSING", "MCP OAuth approval URL is not configured");
|
|
682
|
+
}
|
|
683
|
+
const approve = new URL(approvalUrl);
|
|
684
|
+
approve.searchParams.set("mcp_authorization", created.authorizationId);
|
|
685
|
+
approve.searchParams.set("ps_origin", input.serverOrigin);
|
|
686
|
+
return Response.redirect(approve.toString(), 302);
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
690
|
+
return redirectWithOAuthError(redirectUri, err instanceof McpOAuthAuthorizationError
|
|
691
|
+
? err.code
|
|
692
|
+
: "invalid_request", message, state);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (pathname === "/mcp/oauth/token") {
|
|
696
|
+
if (!resolveMcpApprovalUrl(input.approvalUrl)) {
|
|
697
|
+
return errorResponse(404, "MCP_OAUTH_NOT_CONFIGURED", "MCP OAuth is not configured");
|
|
698
|
+
}
|
|
699
|
+
if (input.request.method !== "POST") {
|
|
700
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
701
|
+
}
|
|
702
|
+
const body = await parseFormBody(input.request);
|
|
703
|
+
if (body.get("grant_type") !== "authorization_code") {
|
|
704
|
+
return jsonResponse({
|
|
705
|
+
error: "unsupported_grant_type",
|
|
706
|
+
error_description: "Only authorization_code is supported",
|
|
707
|
+
}, { status: 400 });
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const token = await redeemMcpOAuthAuthorizationCode({
|
|
711
|
+
authorizationCode: body.get("code") ?? "",
|
|
712
|
+
codeVerifier: body.get("code_verifier") ?? "",
|
|
713
|
+
clientId: body.get("client_id") ?? "",
|
|
714
|
+
redirectUri: body.get("redirect_uri") ?? "",
|
|
715
|
+
}, {
|
|
716
|
+
authorizationStore: input.authorizationStore,
|
|
717
|
+
connectionStore: input.store,
|
|
718
|
+
now: input.now,
|
|
719
|
+
});
|
|
720
|
+
return jsonResponse({
|
|
721
|
+
access_token: token.accessToken,
|
|
722
|
+
token_type: "Bearer",
|
|
723
|
+
...(token.scope ? { scope: token.scope } : {}),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
return jsonResponse({
|
|
728
|
+
error: err instanceof McpOAuthAuthorizationError
|
|
729
|
+
? err.code
|
|
730
|
+
: "invalid_grant",
|
|
731
|
+
error_description: err instanceof Error ? err.message : String(err),
|
|
732
|
+
}, { status: 400 });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (pathname === ownerAuthorizationPrefix ||
|
|
736
|
+
pathname.startsWith(`${ownerAuthorizationPrefix}/`)) {
|
|
737
|
+
try {
|
|
738
|
+
await input.auth.authorizeOwner(input.request);
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
if (err instanceof ProtocolError) {
|
|
742
|
+
return protocolErrorResponse(err);
|
|
743
|
+
}
|
|
744
|
+
throw err;
|
|
745
|
+
}
|
|
746
|
+
const tail = pathname.slice(ownerAuthorizationPrefix.length + 1);
|
|
747
|
+
const [id, action] = tail.split("/");
|
|
748
|
+
if (!id) {
|
|
749
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
750
|
+
}
|
|
751
|
+
if (!action && input.request.method === "GET") {
|
|
752
|
+
const record = await input.authorizationStore.getById(id);
|
|
753
|
+
if (!record) {
|
|
754
|
+
return errorResponse(404, "NOT_FOUND", "Authorization not found");
|
|
755
|
+
}
|
|
756
|
+
return jsonResponse(toMcpOAuthAuthorizationView(record));
|
|
757
|
+
}
|
|
758
|
+
if (action === "approve") {
|
|
759
|
+
if (input.request.method !== "POST") {
|
|
760
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
761
|
+
}
|
|
762
|
+
let body = {};
|
|
763
|
+
try {
|
|
764
|
+
body = (await input.request.json());
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
return errorResponse(400, "INVALID_BODY", "Body must be JSON");
|
|
768
|
+
}
|
|
769
|
+
if (Array.isArray(body.scopes) && body.scopes.length > 0) {
|
|
770
|
+
if (!input.gateway || !input.gatewayConfig?.url) {
|
|
771
|
+
return errorResponse(500, "SERVER_NOT_CONFIGURED", "Gateway config is not configured");
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const approved = await approveMcpOAuthAuthorizationWithScopes({
|
|
775
|
+
authorizationId: id,
|
|
776
|
+
scopes: body.scopes,
|
|
777
|
+
...(body.expiresAt !== undefined
|
|
778
|
+
? { expiresAt: body.expiresAt }
|
|
779
|
+
: {}),
|
|
780
|
+
...(body.nonce !== undefined ? { nonce: body.nonce } : {}),
|
|
781
|
+
}, {
|
|
782
|
+
connectionStore: input.store,
|
|
783
|
+
authorizationStore: input.authorizationStore,
|
|
784
|
+
gateway: input.gateway,
|
|
785
|
+
gatewayConfig: input.gatewayConfig,
|
|
786
|
+
gatewayUrl: input.gatewayConfig.url,
|
|
787
|
+
serverOwner: input.serverOwner,
|
|
788
|
+
serverSigner: input.serverSigner,
|
|
789
|
+
now: input.now,
|
|
790
|
+
});
|
|
791
|
+
return jsonResponse({ redirectTo: approved.redirectTo });
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
if (err instanceof McpOAuthAuthorizationError) {
|
|
795
|
+
return errorResponse(err.status, err.code, err.message);
|
|
796
|
+
}
|
|
797
|
+
throw err;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (!Array.isArray(body.grants) || body.grants.length === 0) {
|
|
801
|
+
return errorResponse(400, "GRANTS_REQUIRED", "Approve requires grants or scopes");
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const approved = await approveMcpOAuthAuthorization({ authorizationId: id, grants: body.grants }, {
|
|
805
|
+
connectionStore: input.store,
|
|
806
|
+
authorizationStore: input.authorizationStore,
|
|
807
|
+
now: input.now,
|
|
808
|
+
});
|
|
809
|
+
return jsonResponse({ redirectTo: approved.redirectTo });
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
if (err instanceof McpOAuthAuthorizationError) {
|
|
813
|
+
return errorResponse(err.status, err.code, err.message);
|
|
814
|
+
}
|
|
815
|
+
throw err;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
819
|
+
}
|
|
820
|
+
// Owner management endpoints
|
|
821
|
+
if (pathname === ownerPrefix || pathname.startsWith(`${ownerPrefix}/`)) {
|
|
822
|
+
try {
|
|
823
|
+
await input.auth.authorizeOwner(input.request);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
if (err instanceof ProtocolError) {
|
|
827
|
+
return protocolErrorResponse(err);
|
|
828
|
+
}
|
|
829
|
+
throw err;
|
|
830
|
+
}
|
|
831
|
+
// POST /v1/mcp/connections — create
|
|
832
|
+
if (pathname === ownerPrefix) {
|
|
833
|
+
if (input.request.method === "POST") {
|
|
834
|
+
let body = {};
|
|
835
|
+
try {
|
|
836
|
+
body = (await input.request.json());
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
body = {};
|
|
840
|
+
}
|
|
841
|
+
const created = await createMcpConnection({ displayName: body.displayName }, {
|
|
842
|
+
store: input.store,
|
|
843
|
+
publicOrigin: input.serverOrigin,
|
|
844
|
+
now: input.now,
|
|
845
|
+
});
|
|
846
|
+
return jsonResponse(created, { status: 201 });
|
|
847
|
+
}
|
|
848
|
+
if (input.request.method === "GET") {
|
|
849
|
+
const records = await listMcpConnectionViews(input.store);
|
|
850
|
+
return jsonResponse({ connections: records });
|
|
851
|
+
}
|
|
852
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
853
|
+
}
|
|
854
|
+
// /v1/mcp/connections/:id[/approve]
|
|
855
|
+
const tail = pathname.slice(ownerPrefix.length + 1);
|
|
856
|
+
const [id, action] = tail.split("/");
|
|
857
|
+
if (!id) {
|
|
858
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
859
|
+
}
|
|
860
|
+
if (action === "approve") {
|
|
861
|
+
if (input.request.method !== "POST") {
|
|
862
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
863
|
+
}
|
|
864
|
+
let body = {};
|
|
865
|
+
try {
|
|
866
|
+
body = (await input.request.json());
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
return errorResponse(400, "INVALID_BODY", "Body must be JSON");
|
|
870
|
+
}
|
|
871
|
+
if (!Array.isArray(body.grants) || body.grants.length === 0) {
|
|
872
|
+
return errorResponse(400, "GRANTS_REQUIRED", "Approve requires at least one grant — mint grants in the consent flow first");
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
const updated = await approveMcpConnection({ connectionId: id, grants: body.grants }, { store: input.store, now: input.now });
|
|
876
|
+
return jsonResponse(toMcpConnectionView(updated));
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
if (err instanceof McpConnectionNotFoundError) {
|
|
880
|
+
return errorResponse(404, "NOT_FOUND", err.message);
|
|
881
|
+
}
|
|
882
|
+
if (err instanceof McpConnectionStateError) {
|
|
883
|
+
return errorResponse(409, "INVALID_STATE", err.message);
|
|
884
|
+
}
|
|
885
|
+
throw err;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (!action) {
|
|
889
|
+
if (input.request.method === "DELETE") {
|
|
890
|
+
try {
|
|
891
|
+
const updated = await revokeMcpConnection(id, {
|
|
892
|
+
store: input.store,
|
|
893
|
+
now: input.now,
|
|
894
|
+
});
|
|
895
|
+
return jsonResponse(toMcpConnectionView(updated));
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
if (err instanceof McpConnectionNotFoundError) {
|
|
899
|
+
return errorResponse(404, "NOT_FOUND", err.message);
|
|
900
|
+
}
|
|
901
|
+
throw err;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
905
|
+
}
|
|
906
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
907
|
+
}
|
|
908
|
+
async function handleMcpToken(rawToken, options) {
|
|
909
|
+
if (!rawToken) {
|
|
910
|
+
if (!options.oauthChallenge) {
|
|
911
|
+
return errorResponse(401, "INVALID_TOKEN", "Missing MCP connection token");
|
|
912
|
+
}
|
|
913
|
+
return mcpUnauthorized(input.serverOrigin);
|
|
914
|
+
}
|
|
915
|
+
const tokenHash = await hashConnectionToken(rawToken);
|
|
916
|
+
const record = await input.store.getByTokenHash(tokenHash);
|
|
917
|
+
if (!record) {
|
|
918
|
+
if (!options.oauthChallenge) {
|
|
919
|
+
return errorResponse(401, "INVALID_TOKEN", "Unknown or revoked MCP connection");
|
|
920
|
+
}
|
|
921
|
+
return mcpUnauthorized(input.serverOrigin, "Unknown or revoked MCP connection");
|
|
922
|
+
}
|
|
923
|
+
const granteeAccount = loadMcpGranteeAccount({
|
|
924
|
+
address: record.granteeAddress,
|
|
925
|
+
publicKey: record.granteePublicKey,
|
|
926
|
+
encryptedPrivateKey: record.encryptedGranteePrivateKey,
|
|
927
|
+
});
|
|
928
|
+
const readClient = createMcpDataReadClient({
|
|
929
|
+
serverOrigin: input.serverOrigin,
|
|
930
|
+
granteeAccount,
|
|
931
|
+
dataApiDeps: {
|
|
932
|
+
storage: input.dataStorage,
|
|
933
|
+
auth: input.auth,
|
|
934
|
+
schemaResolver: input.schemaResolver,
|
|
935
|
+
accessLogWriter: input.accessLogWriter,
|
|
936
|
+
runtimeAvailability: input.runtimeAvailability,
|
|
937
|
+
now: input.now,
|
|
938
|
+
createLogId,
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
const response = await handleMcpStreamableHttpRequest(input.request, {
|
|
942
|
+
connection: record,
|
|
943
|
+
readClient,
|
|
944
|
+
activityRecorder: input.activityRecorder,
|
|
945
|
+
});
|
|
946
|
+
void input.store
|
|
947
|
+
.update(record.id, {
|
|
948
|
+
lastUsedAt: input.now().toISOString(),
|
|
949
|
+
})
|
|
950
|
+
.catch((err) => {
|
|
951
|
+
console.warn("[mcp] failed to update lastUsedAt", err);
|
|
952
|
+
});
|
|
953
|
+
return response;
|
|
954
|
+
}
|
|
955
|
+
// Owner-only activity feed endpoint.
|
|
956
|
+
if (pathname === "/v1/mcp/activity") {
|
|
957
|
+
try {
|
|
958
|
+
await input.auth.authorizeOwner(input.request);
|
|
959
|
+
}
|
|
960
|
+
catch (err) {
|
|
961
|
+
if (err instanceof ProtocolError) {
|
|
962
|
+
return protocolErrorResponse(err);
|
|
963
|
+
}
|
|
964
|
+
throw err;
|
|
965
|
+
}
|
|
966
|
+
if (input.request.method !== "GET") {
|
|
967
|
+
return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
|
|
968
|
+
}
|
|
969
|
+
const snapshot = input.activityRecorder
|
|
970
|
+
? input.activityRecorder.snapshot()
|
|
971
|
+
: { events: [], running: 0, total: 0 };
|
|
972
|
+
return jsonResponse(snapshot);
|
|
973
|
+
}
|
|
974
|
+
// Claude-facing stable endpoint: /mcp with Bearer token.
|
|
975
|
+
if (pathname === "/mcp") {
|
|
976
|
+
return handleMcpToken(bearerToken(input.request), {
|
|
977
|
+
oauthChallenge: Boolean(resolveMcpApprovalUrl(input.approvalUrl)),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
// Legacy Claude-facing endpoint: /mcp/:token
|
|
981
|
+
const mcpPrefix = "/mcp/";
|
|
982
|
+
if (pathname.startsWith(mcpPrefix)) {
|
|
983
|
+
const rawToken = decodeURIComponent(pathname.slice(mcpPrefix.length));
|
|
984
|
+
if (!rawToken || rawToken.includes("/")) {
|
|
985
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
986
|
+
}
|
|
987
|
+
return handleMcpToken(rawToken, { oauthChallenge: false });
|
|
988
|
+
}
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
//# sourceMappingURL=runtime.js.map
|