@opendatalabs/personal-server-ts-lite 0.0.1-canary.9d54d31

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 (44) 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 +29 -0
  6. package/dist/browser-runtime.d.ts.map +1 -0
  7. package/dist/browser-runtime.js +81 -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/index.d.ts +9 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +9 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/relay-tls.d.ts +19 -0
  17. package/dist/relay-tls.d.ts.map +1 -0
  18. package/dist/relay-tls.js +226 -0
  19. package/dist/relay-tls.js.map +1 -0
  20. package/dist/relay.d.ts +68 -0
  21. package/dist/relay.d.ts.map +1 -0
  22. package/dist/relay.js +361 -0
  23. package/dist/relay.js.map +1 -0
  24. package/dist/runtime.d.ts +90 -0
  25. package/dist/runtime.d.ts.map +1 -0
  26. package/dist/runtime.js +846 -0
  27. package/dist/runtime.js.map +1 -0
  28. package/dist/state.d.ts +59 -0
  29. package/dist/state.d.ts.map +1 -0
  30. package/dist/state.js +298 -0
  31. package/dist/state.js.map +1 -0
  32. package/dist/storage-utils.d.ts +7 -0
  33. package/dist/storage-utils.d.ts.map +1 -0
  34. package/dist/storage-utils.js +66 -0
  35. package/dist/storage-utils.js.map +1 -0
  36. package/dist/storage.d.ts +39 -0
  37. package/dist/storage.d.ts.map +1 -0
  38. package/dist/storage.js +312 -0
  39. package/dist/storage.js.map +1 -0
  40. package/dist/sync.d.ts +25 -0
  41. package/dist/sync.d.ts.map +1 -0
  42. package/dist/sync.js +62 -0
  43. package/dist/sync.js.map +1 -0
  44. package/package.json +40 -0
@@ -0,0 +1,846 @@
1
+ import { ExpiredTokenError as SdkExpiredTokenError, InvalidSignatureError as SdkInvalidSignatureError, MissingAuthError as SdkMissingAuthError, verifyWeb3Signed as sdkVerifyWeb3Signed, } from "@opendatalabs/vana-sdk/browser";
2
+ import { ExpiredTokenError, GrantRequiredError, InvalidSignatureError, MissingAuthError, NotOwnerError, ProtocolError, PsUnavailableError, UnregisteredBuilderError, } from "@opendatalabs/personal-server-ts-core/errors";
3
+ import { approveDeviceSessionContract, createGrantContract, createMemoryDeviceSessionStore, deleteDataScopeContract, getSyncStatusContract, ingestDataContract, initiateDeviceSessionContract, listAccessLogsContract, listDataScopesContract, listDataVersionsContract, listGrantsContract, oauthTokenContract, parseDataScopeContract, parseJsonObjectBody, pollDeviceSessionContract, provisionDeviceTokenContract, readDataContract, revokeDeviceTokenContract, syncFileContract, triggerSyncContract, validateServerConfigContract, verifyGrantContract, } from "@opendatalabs/personal-server-ts-core/contracts";
4
+ import { verifyDataReadPolicy, } from "@opendatalabs/personal-server-ts-core/policy";
5
+ import { createStorageReadMethods, readEnvelopeFromMap, sortEntries, } from "./storage-utils.js";
6
+ import { createIndexedDbPsLiteAccessLogStore, createIndexedDbPsLiteStateStore, createIndexedDbPsLiteTokenStore, savePsLiteConfig, } from "./state.js";
7
+ function jsonResponse(body, init) {
8
+ const headers = new Headers(init?.headers);
9
+ headers.set("Content-Type", "application/json");
10
+ return new Response(JSON.stringify(body), { ...init, headers });
11
+ }
12
+ function protocolErrorResponse(err) {
13
+ return jsonResponse(err.toJSON(), { status: err.code });
14
+ }
15
+ function errorResponse(status, errorCode, message) {
16
+ return jsonResponse({
17
+ error: {
18
+ code: status,
19
+ errorCode,
20
+ message,
21
+ },
22
+ }, { status });
23
+ }
24
+ function contractErrorResponse(err) {
25
+ return errorResponse(err.status, err.body.error, err.body.message);
26
+ }
27
+ function unavailableResponse() {
28
+ const err = new PsUnavailableError({
29
+ runtime: "ps-lite",
30
+ reason: "Browser runtime is inactive",
31
+ });
32
+ return protocolErrorResponse(err);
33
+ }
34
+ function parseBearerToken(request) {
35
+ const authorization = request.headers.get("authorization");
36
+ if (!authorization)
37
+ return null;
38
+ const match = /^Bearer\s+(.+)$/i.exec(authorization);
39
+ return match?.[1] ?? null;
40
+ }
41
+ function assertBearerToken(request, expectedToken, ownerOnly = false) {
42
+ const token = parseBearerToken(request);
43
+ if (!token) {
44
+ throw new MissingAuthError();
45
+ }
46
+ if (token !== expectedToken) {
47
+ throw ownerOnly ? new NotOwnerError() : new InvalidSignatureError();
48
+ }
49
+ }
50
+ function createMissingAuthAdapter() {
51
+ return {
52
+ async authorizeOwner() {
53
+ throw new MissingAuthError();
54
+ },
55
+ async authorizeBuilderList() {
56
+ throw new MissingAuthError();
57
+ },
58
+ async authorizeBuilderRead() {
59
+ throw new MissingAuthError();
60
+ },
61
+ };
62
+ }
63
+ export function createBearerTokenPsLiteAuth(options) {
64
+ return {
65
+ async authorizeOwner(request) {
66
+ assertBearerToken(request, options.ownerToken, true);
67
+ },
68
+ async authorizeBuilderList(request) {
69
+ assertBearerToken(request, options.builderToken);
70
+ },
71
+ async authorizeBuilderRead(input) {
72
+ assertBearerToken(input.request, options.builderToken);
73
+ if (!input.grantId) {
74
+ throw new GrantRequiredError({
75
+ reason: "No grantId in request",
76
+ });
77
+ }
78
+ return { grantId: input.grantId };
79
+ },
80
+ };
81
+ }
82
+ function resolveOrigin(origin) {
83
+ return typeof origin === "function" ? origin() : origin;
84
+ }
85
+ async function verifyWeb3SignedRequest(request, options) {
86
+ const url = new URL(request.url);
87
+ try {
88
+ return await sdkVerifyWeb3Signed({
89
+ headerValue: request.headers.get("authorization") ?? undefined,
90
+ expectedOrigin: resolveOrigin(options.origin),
91
+ expectedMethod: request.method,
92
+ expectedPath: url.pathname,
93
+ now: options.now?.(),
94
+ });
95
+ }
96
+ catch (err) {
97
+ if (err instanceof SdkMissingAuthError) {
98
+ throw new MissingAuthError(getErrorDetails(err));
99
+ }
100
+ if (err instanceof SdkInvalidSignatureError) {
101
+ throw new InvalidSignatureError(getErrorDetails(err));
102
+ }
103
+ if (err instanceof SdkExpiredTokenError) {
104
+ throw new ExpiredTokenError(getErrorDetails(err));
105
+ }
106
+ throw err;
107
+ }
108
+ }
109
+ function getErrorDetails(err) {
110
+ if (err && typeof err === "object" && "details" in err) {
111
+ const details = err.details;
112
+ if (details && typeof details === "object" && !Array.isArray(details)) {
113
+ return details;
114
+ }
115
+ }
116
+ return undefined;
117
+ }
118
+ export function createWeb3SignedPsLiteAuth(options) {
119
+ return {
120
+ async authorizeOwner(request) {
121
+ const verified = await verifyWeb3SignedRequest(request, options);
122
+ if (verified.signer.toLowerCase() !== options.ownerAddress.toLowerCase()) {
123
+ throw new NotOwnerError({
124
+ expected: options.ownerAddress,
125
+ actual: verified.signer,
126
+ });
127
+ }
128
+ },
129
+ async authorizeBuilderList(request) {
130
+ const verified = await verifyWeb3SignedRequest(request, options);
131
+ const builder = await options.dataReadPolicyPorts?.authSessionVerifier.getBuilder(verified.signer);
132
+ if (options.dataReadPolicyPorts && !builder) {
133
+ throw new UnregisteredBuilderError();
134
+ }
135
+ },
136
+ async authorizeBuilderRead(input) {
137
+ const verified = await verifyWeb3SignedRequest(input.request, options);
138
+ if (!options.dataReadPolicyPorts) {
139
+ throw new ProtocolError(500, "SERVER_NOT_CONFIGURED", "Server is not configured", {
140
+ reason: "PS Lite read policy ports are not configured",
141
+ });
142
+ }
143
+ const grant = await verifyDataReadPolicy({
144
+ signer: verified.signer,
145
+ grantId: verified.payload.grantId ?? input.grantId,
146
+ requestedScope: input.scope,
147
+ fileId: input.fileId,
148
+ }, options.dataReadPolicyPorts);
149
+ return { builder: verified.signer, grantId: grant.id };
150
+ },
151
+ };
152
+ }
153
+ function normalizeLimit(value, fallback) {
154
+ if (value === null)
155
+ return fallback;
156
+ const parsed = Number(value);
157
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
158
+ }
159
+ function toDataStoragePort(storage) {
160
+ if ("listScopes" in storage) {
161
+ return storage;
162
+ }
163
+ throw new Error("PS Lite runtime requires a persistent DataStoragePort. Use createIndexedDbPsLiteRuntime() or createPersistentPsLiteStorage().");
164
+ }
165
+ export function createMemoryPsLiteAccessLogStore() {
166
+ const logs = [];
167
+ return {
168
+ capabilities: { accessLogs: "memory" },
169
+ async write(entry) {
170
+ logs.push(entry);
171
+ },
172
+ async read(options) {
173
+ const limit = options?.limit ?? 50;
174
+ const offset = options?.offset ?? 0;
175
+ const sorted = [...logs].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
176
+ return {
177
+ logs: sorted.slice(offset, offset + limit),
178
+ total: sorted.length,
179
+ limit,
180
+ offset,
181
+ };
182
+ },
183
+ };
184
+ }
185
+ function indexedDbAvailable() {
186
+ return typeof indexedDB !== "undefined";
187
+ }
188
+ function createDefaultAccessLogStore() {
189
+ if (!indexedDbAvailable()) {
190
+ throw new Error("IndexedDB is required for default PS Lite access log persistence.");
191
+ }
192
+ return createIndexedDbPsLiteAccessLogStore();
193
+ }
194
+ function createLogId() {
195
+ return globalThis.crypto?.randomUUID?.() ?? `log-${Date.now()}`;
196
+ }
197
+ function randomHex(byteLength) {
198
+ const bytes = new Uint8Array(byteLength);
199
+ globalThis.crypto.getRandomValues(bytes);
200
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
201
+ }
202
+ export function createMemoryPsLiteTokenStore() {
203
+ const tokens = new Map();
204
+ function normalizeExpiresAt(value) {
205
+ if (value == null)
206
+ return null;
207
+ const date = value instanceof Date ? value : new Date(value);
208
+ if (Number.isNaN(date.getTime())) {
209
+ throw new Error("Invalid token expiry");
210
+ }
211
+ return date.toISOString();
212
+ }
213
+ function isExpired(expiresAt) {
214
+ return expiresAt ? new Date(expiresAt).getTime() <= Date.now() : false;
215
+ }
216
+ return {
217
+ capabilities: { tokens: "memory" },
218
+ async getTokens() {
219
+ return Array.from(tokens.entries())
220
+ .filter(([, expiresAt]) => !isExpired(expiresAt))
221
+ .map(([token]) => token);
222
+ },
223
+ async isValid(token) {
224
+ const expiresAt = tokens.get(token);
225
+ if (expiresAt === undefined)
226
+ return false;
227
+ if (isExpired(expiresAt)) {
228
+ tokens.delete(token);
229
+ return false;
230
+ }
231
+ return true;
232
+ },
233
+ async addToken(token, options) {
234
+ tokens.set(token, normalizeExpiresAt(options?.expiresAt));
235
+ },
236
+ async removeToken(token) {
237
+ tokens.delete(token);
238
+ },
239
+ };
240
+ }
241
+ function createDefaultTokenStore() {
242
+ if (!indexedDbAvailable()) {
243
+ throw new Error("IndexedDB is required for default PS Lite token storage.");
244
+ }
245
+ return createIndexedDbPsLiteTokenStore();
246
+ }
247
+ function createDefaultSaveConfig() {
248
+ if (!indexedDbAvailable()) {
249
+ throw new Error("IndexedDB is required for default PS Lite config persistence.");
250
+ }
251
+ const stateStore = createIndexedDbPsLiteStateStore();
252
+ return async (nextConfig) => {
253
+ await savePsLiteConfig(stateStore, nextConfig);
254
+ };
255
+ }
256
+ function bearerToken(request) {
257
+ const authorization = request.headers.get("authorization");
258
+ if (!authorization?.startsWith("Bearer "))
259
+ return null;
260
+ return authorization.slice(7);
261
+ }
262
+ async function readForm(request) {
263
+ return new URLSearchParams(await request.text());
264
+ }
265
+ export function createMemoryPsLiteStorage(adapter = { kind: "indexeddb" }) {
266
+ const entries = new Map();
267
+ const envelopes = new Map();
268
+ let nextId = 1;
269
+ function envelopeKey(scope, collectedAt) {
270
+ return `${scope}\n${collectedAt}`;
271
+ }
272
+ function entriesForScope(scope) {
273
+ return sortEntries(Array.from(entries.values()).filter((entry) => entry.scope === scope));
274
+ }
275
+ const storagePort = {
276
+ kind: adapter.kind === "custom" ? "custom" : "browser-indexeddb-opfs",
277
+ capabilities: {
278
+ metadata: "memory",
279
+ files: "memory",
280
+ opfsAvailable: false,
281
+ },
282
+ ...createStorageReadMethods(() => entries.values(), entriesForScope),
283
+ findByFileId(fileId) {
284
+ return Array.from(entries.values()).find((entry) => entry.fileId === fileId);
285
+ },
286
+ findUnsynced(options) {
287
+ const unsynced = Array.from(entries.values())
288
+ .filter((entry) => entry.fileId === null)
289
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
290
+ return options?.limit === undefined
291
+ ? unsynced
292
+ : unsynced.slice(0, options.limit);
293
+ },
294
+ async readEnvelope(scope, collectedAt) {
295
+ return readEnvelopeFromMap(envelopes, envelopeKey(scope, collectedAt));
296
+ },
297
+ async writeEnvelope(envelope) {
298
+ envelopes.set(envelopeKey(envelope.scope, envelope.collectedAt), envelope);
299
+ return {
300
+ path: `${envelope.scope}/${envelope.collectedAt}.json`,
301
+ relativePath: `${envelope.scope}/${envelope.collectedAt}.json`,
302
+ sizeBytes: new TextEncoder().encode(JSON.stringify(envelope)).length,
303
+ };
304
+ },
305
+ insertEntry(entry) {
306
+ const indexed = {
307
+ ...entry,
308
+ schemaId: entry.schemaId ?? null,
309
+ id: nextId,
310
+ createdAt: new Date().toISOString(),
311
+ };
312
+ nextId += 1;
313
+ entries.set(entry.path, indexed);
314
+ return indexed;
315
+ },
316
+ updateFileId(path, fileId) {
317
+ const entry = entries.get(path);
318
+ if (!entry)
319
+ return false;
320
+ entries.set(path, { ...entry, fileId });
321
+ return true;
322
+ },
323
+ async deleteScope(scope) {
324
+ let deleted = 0;
325
+ for (const [path, entry] of entries.entries()) {
326
+ if (entry.scope === scope) {
327
+ entries.delete(path);
328
+ envelopes.delete(envelopeKey(entry.scope, entry.collectedAt));
329
+ deleted += 1;
330
+ }
331
+ }
332
+ return deleted;
333
+ },
334
+ };
335
+ return storagePort;
336
+ }
337
+ function parseScope(pathPart) {
338
+ const scopeResult = parseDataScopeContract(decodeURIComponent(pathPart));
339
+ if (!scopeResult.ok)
340
+ return contractErrorResponse(scopeResult);
341
+ return scopeResult.scope;
342
+ }
343
+ function isRecord(value) {
344
+ return value !== null && typeof value === "object" && !Array.isArray(value);
345
+ }
346
+ async function parseJsonObject(request) {
347
+ try {
348
+ const body = (await request.json());
349
+ if (!isRecord(body)) {
350
+ return {
351
+ ok: false,
352
+ response: errorResponse(400, "INVALID_BODY", "Request body must be a JSON object"),
353
+ };
354
+ }
355
+ return { ok: true, body };
356
+ }
357
+ catch {
358
+ return {
359
+ ok: false,
360
+ response: errorResponse(400, "INVALID_BODY", "Request body must be JSON"),
361
+ };
362
+ }
363
+ }
364
+ export function createPsLiteRuntime(options) {
365
+ let active = options.active ?? false;
366
+ const now = options.now ?? (() => new Date());
367
+ const auth = options.auth ?? createMissingAuthAdapter();
368
+ const dataStorage = toDataStoragePort(options.storage);
369
+ let accessLogReader = options.accessLogReader;
370
+ let accessLogWriter = options.accessLogWriter;
371
+ if (!accessLogReader || !accessLogWriter) {
372
+ const accessLogStore = createDefaultAccessLogStore();
373
+ accessLogReader ??= accessLogStore;
374
+ accessLogWriter ??= accessLogStore;
375
+ }
376
+ const tokenStore = options.tokenStore ?? createDefaultTokenStore();
377
+ const saveConfig = options.saveConfig ?? createDefaultSaveConfig();
378
+ const deviceSessions = createMemoryDeviceSessionStore();
379
+ async function withProtocolErrors(handler) {
380
+ try {
381
+ return await handler();
382
+ }
383
+ catch (err) {
384
+ if (err instanceof ProtocolError) {
385
+ return protocolErrorResponse(err);
386
+ }
387
+ return errorResponse(500, "INTERNAL_ERROR", "Internal server error");
388
+ }
389
+ }
390
+ function sendContractResult(result) {
391
+ return jsonResponse(result.body, { status: result.status });
392
+ }
393
+ function gatewayRequiredResponse() {
394
+ return errorResponse(500, "SERVER_NOT_CONFIGURED", "Gateway is not configured");
395
+ }
396
+ function ownerAddress() {
397
+ return options.serverOwner ?? options.identity?.address;
398
+ }
399
+ async function handleAuthDevice(request, url) {
400
+ if (url.pathname === "/auth/device") {
401
+ if (request.method !== "POST") {
402
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
403
+ }
404
+ return sendContractResult(initiateDeviceSessionContract({
405
+ sessionStore: deviceSessions,
406
+ serverOwner: ownerAddress(),
407
+ requestOrigin: url.origin,
408
+ approvalOrigin: url.origin,
409
+ sessionId: randomHex(32),
410
+ pollToken: randomHex(32),
411
+ now: now().getTime(),
412
+ }));
413
+ }
414
+ if (url.pathname === "/auth/device/poll") {
415
+ if (request.method !== "GET") {
416
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
417
+ }
418
+ return sendContractResult(pollDeviceSessionContract({
419
+ sessionStore: deviceSessions,
420
+ pollToken: url.searchParams.get("token"),
421
+ serverOwner: ownerAddress(),
422
+ now: now().getTime(),
423
+ }));
424
+ }
425
+ if (url.pathname === "/auth/device/approve") {
426
+ const sessionId = url.searchParams.get("session");
427
+ if (!sessionId) {
428
+ return request.method === "GET"
429
+ ? new Response("Missing session parameter", { status: 400 })
430
+ : jsonResponse({ error: { code: 400, message: "Missing session parameter" } }, { status: 400 });
431
+ }
432
+ const session = deviceSessions.get(sessionId);
433
+ if (!session) {
434
+ return request.method === "GET"
435
+ ? new Response("Session expired or invalid", { status: 404 })
436
+ : jsonResponse({ error: { code: 404, message: "Session expired or invalid" } }, { status: 404 });
437
+ }
438
+ if (request.method === "GET") {
439
+ return new Response("Device authorization pending", {
440
+ headers: { "Content-Type": "text/html; charset=utf-8" },
441
+ });
442
+ }
443
+ if (request.method !== "POST") {
444
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
445
+ }
446
+ if (session.status === "approved") {
447
+ return jsonResponse({ status: "already_approved" });
448
+ }
449
+ await auth.authorizeOwner(request);
450
+ return sendContractResult(await approveDeviceSessionContract({
451
+ sessionStore: deviceSessions,
452
+ tokenStore,
453
+ sessionId,
454
+ serverOwner: ownerAddress(),
455
+ accessToken: `vana_ps_${randomHex(32)}`,
456
+ now: now().getTime(),
457
+ }));
458
+ }
459
+ if (url.pathname === "/auth/device/token") {
460
+ if (request.method === "DELETE") {
461
+ const token = bearerToken(request);
462
+ if (!token) {
463
+ return jsonResponse({ error: { code: 401, message: "Missing Bearer token" } }, { status: 401 });
464
+ }
465
+ return sendContractResult(await revokeDeviceTokenContract({
466
+ tokenStore,
467
+ bearerToken: token,
468
+ }));
469
+ }
470
+ if (request.method !== "POST") {
471
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
472
+ }
473
+ const token = bearerToken(request);
474
+ if (!options.accessToken || token !== options.accessToken) {
475
+ return jsonResponse({
476
+ error: {
477
+ code: 403,
478
+ message: "Only control-plane tokens can provision Personal Server session tokens",
479
+ },
480
+ }, { status: 403 });
481
+ }
482
+ let body;
483
+ try {
484
+ body = (await request.json());
485
+ }
486
+ catch {
487
+ return jsonResponse({ error: { code: 400, message: "Request body must be valid JSON" } }, { status: 400 });
488
+ }
489
+ if (!body.token || typeof body.token !== "string") {
490
+ return jsonResponse({ error: { code: 400, message: "Missing token" } }, { status: 400 });
491
+ }
492
+ return sendContractResult(await provisionDeviceTokenContract({
493
+ tokenStore,
494
+ body,
495
+ now: now().getTime(),
496
+ }));
497
+ }
498
+ return undefined;
499
+ }
500
+ async function handleOauthToken(request) {
501
+ if (request.method !== "POST") {
502
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
503
+ }
504
+ const contentType = request.headers.get("content-type") ?? "";
505
+ if (!contentType.includes("application/x-www-form-urlencoded")) {
506
+ return jsonResponse({
507
+ error: "invalid_request",
508
+ error_description: "Content-Type must be application/x-www-form-urlencoded",
509
+ }, { status: 400 });
510
+ }
511
+ const form = await readForm(request);
512
+ const result = await oauthTokenContract({
513
+ body: form,
514
+ authorizationHeader: request.headers.get("authorization"),
515
+ tokenStore,
516
+ controlPlaneSecret: options.accessToken,
517
+ randomToken: () => `vana_ps_${randomHex(32)}`,
518
+ now,
519
+ deviceSessions: {
520
+ findByDeviceCode(deviceCode) {
521
+ const session = deviceSessions.findByPollToken(deviceCode);
522
+ if (!session)
523
+ return null;
524
+ return {
525
+ status: session.status,
526
+ accessToken: session.accessToken,
527
+ accessTokenExpiresAt: session.accessTokenExpiresAt,
528
+ sessionId: session.sessionId,
529
+ };
530
+ },
531
+ consume(sessionId) {
532
+ deviceSessions.delete(sessionId);
533
+ },
534
+ },
535
+ });
536
+ return jsonResponse(result.body, {
537
+ status: result.status,
538
+ headers: result.headers,
539
+ });
540
+ }
541
+ return {
542
+ kind: "ps-lite",
543
+ storage: options.storage,
544
+ activate() {
545
+ active = true;
546
+ },
547
+ deactivate() {
548
+ active = false;
549
+ },
550
+ isAvailable() {
551
+ return active;
552
+ },
553
+ async fetch(request) {
554
+ return withProtocolErrors(async () => {
555
+ const url = new URL(request.url);
556
+ if (url.pathname === "/health") {
557
+ const apiOrigin = url.origin;
558
+ const identity = options.identity ?? null;
559
+ let serverId = null;
560
+ if (identity && options.gateway) {
561
+ try {
562
+ const server = await options.gateway.getServer(identity.address);
563
+ serverId = server?.id ?? null;
564
+ }
565
+ catch {
566
+ serverId = null;
567
+ }
568
+ }
569
+ const registration = options.serverOwner && identity
570
+ ? {
571
+ ownerAddress: options.serverOwner,
572
+ serverAddress: identity.address,
573
+ publicKey: identity.publicKey,
574
+ serverUrl: apiOrigin,
575
+ serverId,
576
+ registered: Boolean(serverId),
577
+ }
578
+ : null;
579
+ const capabilities = dataStorage.capabilities;
580
+ const stateCapabilities = {
581
+ tokens: tokenStore
582
+ .capabilities?.tokens ?? "custom",
583
+ accessLogs: accessLogReader.capabilities?.accessLogs ?? "custom",
584
+ config: options.stateCapabilities?.config ??
585
+ (options.saveConfig ? "custom" : "indexeddb"),
586
+ };
587
+ return jsonResponse({
588
+ status: active ? "healthy" : "unavailable",
589
+ runtime: "ps-lite",
590
+ storage: options.storage.kind,
591
+ capabilities: capabilities ?? null,
592
+ stateCapabilities,
593
+ owner: options.serverOwner ?? null,
594
+ apiOrigin,
595
+ gatewayUrl: options.config?.gateway?.url ?? null,
596
+ gatewayConfig: options.config?.gateway ?? null,
597
+ identity,
598
+ registration,
599
+ active,
600
+ checkedAt: now().toISOString(),
601
+ });
602
+ }
603
+ if (!active) {
604
+ return unavailableResponse();
605
+ }
606
+ const dataPrefix = "/v1/data";
607
+ if (url.pathname === dataPrefix || url.pathname === `${dataPrefix}/`) {
608
+ if (request.method !== "GET") {
609
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
610
+ }
611
+ await auth.authorizeBuilderList(request);
612
+ const limit = normalizeLimit(url.searchParams.get("limit"), 20);
613
+ const offset = normalizeLimit(url.searchParams.get("offset"), 0);
614
+ const result = listDataScopesContract({
615
+ storage: dataStorage,
616
+ scopePrefix: url.searchParams.get("scopePrefix") ?? undefined,
617
+ limit,
618
+ offset,
619
+ });
620
+ return jsonResponse(result.response);
621
+ }
622
+ if (!url.pathname.startsWith(`${dataPrefix}/`)) {
623
+ if (url.pathname.startsWith("/auth/device")) {
624
+ const response = await handleAuthDevice(request, url);
625
+ if (response)
626
+ return response;
627
+ }
628
+ if (url.pathname === "/oauth/token") {
629
+ return handleOauthToken(request);
630
+ }
631
+ const grantsPrefix = "/v1/grants";
632
+ if (url.pathname === grantsPrefix ||
633
+ url.pathname === `${grantsPrefix}/`) {
634
+ if (request.method === "GET") {
635
+ await auth.authorizeOwner(request);
636
+ if (!options.gateway)
637
+ return gatewayRequiredResponse();
638
+ return sendContractResult(await listGrantsContract({
639
+ gateway: options.gateway,
640
+ serverOwner: options.serverOwner ?? options.identity?.address,
641
+ }));
642
+ }
643
+ if (request.method === "POST") {
644
+ await auth.authorizeOwner(request);
645
+ if (!options.gateway)
646
+ return gatewayRequiredResponse();
647
+ let body;
648
+ try {
649
+ body = await request.json();
650
+ }
651
+ catch {
652
+ return errorResponse(400, "INVALID_BODY", "Invalid JSON body");
653
+ }
654
+ return sendContractResult(await createGrantContract({
655
+ gateway: options.gateway,
656
+ serverOwner: options.serverOwner ?? options.identity?.address,
657
+ serverSigner: options.serverSigner,
658
+ body,
659
+ now: () => now().getTime(),
660
+ }));
661
+ }
662
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
663
+ }
664
+ if (url.pathname === `${grantsPrefix}/verify`) {
665
+ if (request.method !== "POST") {
666
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
667
+ }
668
+ let body;
669
+ try {
670
+ body = await request.json();
671
+ }
672
+ catch {
673
+ return errorResponse(400, "INVALID_BODY", "Invalid JSON body");
674
+ }
675
+ return sendContractResult(await verifyGrantContract(body));
676
+ }
677
+ const accessLogsPrefix = "/v1/access-logs";
678
+ if (url.pathname === accessLogsPrefix ||
679
+ url.pathname === `${accessLogsPrefix}/`) {
680
+ if (request.method !== "GET") {
681
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
682
+ }
683
+ await auth.authorizeOwner(request);
684
+ return sendContractResult(await listAccessLogsContract({
685
+ accessLogReader,
686
+ limit: url.searchParams.get("limit"),
687
+ offset: url.searchParams.get("offset"),
688
+ }));
689
+ }
690
+ const syncPrefix = "/v1/sync";
691
+ if (url.pathname === `${syncPrefix}/trigger`) {
692
+ if (request.method !== "POST") {
693
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
694
+ }
695
+ await auth.authorizeOwner(request);
696
+ return sendContractResult(await triggerSyncContract(options.syncManager ?? null));
697
+ }
698
+ if (url.pathname === `${syncPrefix}/status`) {
699
+ if (request.method !== "GET") {
700
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
701
+ }
702
+ await auth.authorizeOwner(request);
703
+ return sendContractResult(getSyncStatusContract(options.syncManager ?? null));
704
+ }
705
+ if (url.pathname.startsWith(`${syncPrefix}/file/`)) {
706
+ if (request.method !== "POST") {
707
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
708
+ }
709
+ await auth.authorizeOwner(request);
710
+ const fileId = decodeURIComponent(url.pathname.slice(`${syncPrefix}/file/`.length));
711
+ return sendContractResult(await syncFileContract({
712
+ fileId,
713
+ syncManager: options.syncManager ?? null,
714
+ }));
715
+ }
716
+ if (url.pathname === "/ui/api/config") {
717
+ if (request.method === "GET") {
718
+ await auth.authorizeOwner(request);
719
+ return jsonResponse(options.config ?? {});
720
+ }
721
+ if (request.method === "PUT") {
722
+ await auth.authorizeOwner(request);
723
+ const parsed = await parseJsonObjectBody(request);
724
+ if (!parsed.ok)
725
+ return sendContractResult(parsed.result);
726
+ const result = validateServerConfigContract(parsed.body);
727
+ if (!result.ok)
728
+ return sendContractResult(result);
729
+ await saveConfig?.(result.body.config);
730
+ return sendContractResult(result);
731
+ }
732
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
733
+ }
734
+ return errorResponse(404, "NOT_FOUND", "Not found");
735
+ }
736
+ const parts = url.pathname.slice(dataPrefix.length + 1).split("/");
737
+ const scope = parseScope(parts[0] ?? "");
738
+ if (scope instanceof Response) {
739
+ return scope;
740
+ }
741
+ if (parts.length === 2 && parts[1] === "versions") {
742
+ if (request.method !== "GET") {
743
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
744
+ }
745
+ await auth.authorizeBuilderList(request);
746
+ const limit = normalizeLimit(url.searchParams.get("limit"), 20);
747
+ const offset = normalizeLimit(url.searchParams.get("offset"), 0);
748
+ const result = listDataVersionsContract({
749
+ storage: dataStorage,
750
+ scopeParam: scope,
751
+ limit,
752
+ offset,
753
+ });
754
+ if (!result.ok)
755
+ return contractErrorResponse(result);
756
+ return jsonResponse(result.response);
757
+ }
758
+ if (parts.length !== 1) {
759
+ return errorResponse(404, "NOT_FOUND", "Not found");
760
+ }
761
+ if (request.method === "GET") {
762
+ const grantId = url.searchParams.get("grantId") ??
763
+ request.headers.get("x-ps-grant-id") ??
764
+ undefined;
765
+ const selectedEntry = dataStorage.findEntry({
766
+ scope,
767
+ fileId: url.searchParams.get("fileId") ?? undefined,
768
+ at: url.searchParams.get("at") ?? undefined,
769
+ });
770
+ const authResult = await auth.authorizeBuilderRead({
771
+ request,
772
+ scope,
773
+ grantId,
774
+ fileId: url.searchParams.get("fileId") ??
775
+ selectedEntry?.fileId ??
776
+ undefined,
777
+ });
778
+ const result = await readDataContract({
779
+ storage: dataStorage,
780
+ scopeParam: scope,
781
+ fileId: url.searchParams.get("fileId") ?? undefined,
782
+ at: url.searchParams.get("at") ?? undefined,
783
+ });
784
+ if (!result.ok)
785
+ return contractErrorResponse(result);
786
+ await accessLogWriter.write({
787
+ logId: createLogId(),
788
+ grantId: authResult?.grantId ?? grantId ?? "unknown",
789
+ builder: authResult?.builder ?? "unknown",
790
+ action: "read",
791
+ scope,
792
+ timestamp: now().toISOString(),
793
+ ipAddress: request.headers.get("x-forwarded-for") ??
794
+ request.headers.get("x-real-ip") ??
795
+ "unknown",
796
+ userAgent: request.headers.get("user-agent") ?? "unknown",
797
+ });
798
+ return jsonResponse(result.envelope);
799
+ }
800
+ if (request.method === "POST") {
801
+ await auth.authorizeOwner(request);
802
+ const parsed = await parseJsonObject(request);
803
+ if (!parsed.ok) {
804
+ return parsed.response;
805
+ }
806
+ let schemaUrl;
807
+ let schemaId;
808
+ if (options.gateway) {
809
+ const schema = await options.gateway.getSchemaForScope(scope);
810
+ if (!schema) {
811
+ return errorResponse(400, "NO_SCHEMA", `No schema registered for scope: ${scope}`);
812
+ }
813
+ schemaUrl = schema.definitionUrl;
814
+ schemaId = schema.id;
815
+ }
816
+ const collectedAt = now().toISOString();
817
+ const result = await ingestDataContract({
818
+ storage: dataStorage,
819
+ scopeParam: scope,
820
+ body: parsed.body,
821
+ collectedAt,
822
+ status: options.syncManager ? "syncing" : "stored",
823
+ schemaUrl,
824
+ schemaId,
825
+ });
826
+ if (!result.ok)
827
+ return contractErrorResponse(result);
828
+ options.syncManager?.trigger().catch(() => undefined);
829
+ return jsonResponse(result.response, { status: 201 });
830
+ }
831
+ if (request.method === "DELETE") {
832
+ await auth.authorizeOwner(request);
833
+ const result = await deleteDataScopeContract({
834
+ storage: dataStorage,
835
+ scopeParam: scope,
836
+ });
837
+ if (!result.ok)
838
+ return contractErrorResponse(result);
839
+ return new Response(null, { status: 204 });
840
+ }
841
+ return errorResponse(405, "METHOD_NOT_ALLOWED", "Method not allowed");
842
+ });
843
+ },
844
+ };
845
+ }
846
+ //# sourceMappingURL=runtime.js.map