solid-objects 0.12.1 → 0.13.0

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 (85) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +228 -953
  3. package/dist/actor.d.ts +12 -0
  4. package/dist/actor.d.ts.map +1 -1
  5. package/dist/actor.js +25 -2
  6. package/dist/actor.js.map +1 -1
  7. package/dist/browser/components.d.ts.map +1 -1
  8. package/dist/browser/components.js +4 -1
  9. package/dist/browser/components.js.map +1 -1
  10. package/dist/browser/index.d.ts +1 -0
  11. package/dist/browser/index.d.ts.map +1 -1
  12. package/dist/browser/index.js +8 -0
  13. package/dist/browser/index.js.map +1 -1
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +13 -1
  16. package/dist/cli.js.map +1 -1
  17. package/dist/configuration.d.ts +1 -0
  18. package/dist/configuration.d.ts.map +1 -1
  19. package/dist/configuration.js.map +1 -1
  20. package/dist/doctor.d.ts.map +1 -1
  21. package/dist/doctor.js +12 -3
  22. package/dist/doctor.js.map +1 -1
  23. package/dist/examples/sqlite-quickstart.js +83 -0
  24. package/dist/examples/sqlite-quickstart.js.map +1 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/records.d.ts +1 -0
  30. package/dist/records.d.ts.map +1 -1
  31. package/dist/repository.d.ts +4 -1
  32. package/dist/repository.d.ts.map +1 -1
  33. package/dist/repository.js +5 -4
  34. package/dist/repository.js.map +1 -1
  35. package/dist/runtime.d.ts.map +1 -1
  36. package/dist/runtime.js +37 -6
  37. package/dist/runtime.js.map +1 -1
  38. package/dist/schema.d.ts.map +1 -1
  39. package/dist/schema.js +15 -4
  40. package/dist/schema.js.map +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/dist/web/assets.d.ts +4 -0
  44. package/dist/web/assets.d.ts.map +1 -0
  45. package/dist/web/assets.js +11 -0
  46. package/dist/web/assets.js.map +1 -0
  47. package/dist/web/index.d.ts +28 -0
  48. package/dist/web/index.d.ts.map +1 -0
  49. package/dist/web/index.js +642 -0
  50. package/dist/web/index.js.map +1 -0
  51. package/dist/web/node.d.ts +3 -0
  52. package/dist/web/node.d.ts.map +1 -0
  53. package/dist/web/node.js +109 -0
  54. package/dist/web/node.js.map +1 -0
  55. package/dist/web/render.d.ts +64 -0
  56. package/dist/web/render.d.ts.map +1 -0
  57. package/dist/web/render.js +331 -0
  58. package/dist/web/render.js.map +1 -0
  59. package/dist/web/store.d.ts +65 -0
  60. package/dist/web/store.d.ts.map +1 -0
  61. package/dist/web/store.js +303 -0
  62. package/dist/web/store.js.map +1 -0
  63. package/dist/web/types.d.ts +82 -0
  64. package/dist/web/types.d.ts.map +1 -0
  65. package/dist/web/types.js +2 -0
  66. package/dist/web/types.js.map +1 -0
  67. package/docs/api.md +63 -0
  68. package/docs/architecture.md +16 -10
  69. package/docs/authorization.md +12 -3
  70. package/docs/benchmarks.md +123 -0
  71. package/docs/browser-protocol.md +15 -8
  72. package/docs/comparisons.md +36 -0
  73. package/docs/configuration.md +3 -1
  74. package/docs/correctness.md +23 -0
  75. package/docs/dashboard.md +196 -0
  76. package/docs/fit.md +58 -0
  77. package/docs/parity.md +37 -34
  78. package/docs/releasing.md +6 -4
  79. package/docs/state-and-lifecycle.md +10 -0
  80. package/docs/support.md +39 -0
  81. package/examples/failure-recovery/actor.ts +51 -0
  82. package/examples/failure-recovery/demo.ts +233 -0
  83. package/examples/failure-recovery/worker.ts +46 -0
  84. package/examples/sqlite-quickstart.ts +109 -0
  85. package/package.json +25 -4
@@ -0,0 +1,642 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { Unauthorized, SolidObjectsError } from "../errors.js";
3
+ import { DASHBOARD_CHARTS_JAVASCRIPT, DASHBOARD_JAVASCRIPT, DASHBOARD_STYLESHEET, } from "./assets.js";
4
+ import { DASHBOARD_COLUMNS, DashboardView, escapeHtml } from "./render.js";
5
+ import { DashboardStore } from "./store.js";
6
+ export { createNodeDashboardHandler } from "./node.js";
7
+ const CHART_LIBRARY_URL = "https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js";
8
+ const CHART_LIBRARY_INTEGRITY = "sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y";
9
+ const CSRF_SESSION_KEY = "solid_objects.dashboard.csrf";
10
+ const TOKEN_BYTES = 32;
11
+ const MAXIMUM_FORM_BYTES = 65_536;
12
+ const DEFAULT_TABS = [
13
+ { label: "Dashboard", path: "/" },
14
+ { label: "Instances", path: "/instances" },
15
+ { label: "Mailbox", path: "/mailbox" },
16
+ { label: "Reminders", path: "/reminders" },
17
+ { label: "Effects", path: "/effects" },
18
+ { label: "Broadcasts", path: "/broadcasts" },
19
+ { label: "Dead letters", path: "/dead-letters" },
20
+ { label: "Processes", path: "/processes" },
21
+ ];
22
+ export class SolidObjectsDashboard {
23
+ options;
24
+ mountPath;
25
+ access;
26
+ store;
27
+ routes;
28
+ tabs;
29
+ renderers;
30
+ middleware;
31
+ chartLibrary;
32
+ constructor(options) {
33
+ this.options = options;
34
+ this.mountPath = normalizeMountPath(options.mountPath ?? "/solid-objects/dashboard");
35
+ this.access = dashboardAccess(options.access);
36
+ this.store = new DashboardStore(options.runtime);
37
+ const extensions = Object.freeze([...(options.extensions ?? [])]);
38
+ this.routes = validateRoutes([...this.builtInRoutes(), ...extensions.flatMap(extensionRoutes)]);
39
+ this.tabs = Object.freeze([...DEFAULT_TABS, ...extensions.flatMap(extensionTabs)]);
40
+ this.renderers = Object.freeze(Object.assign({}, ...extensions.map((extension) => extension.renderers ?? {})));
41
+ this.middleware = Object.freeze([
42
+ ...(options.middleware ?? []),
43
+ ...extensions.flatMap((extension) => extension.middleware ?? []),
44
+ ]);
45
+ this.chartLibrary = Object.freeze(options.chartLibrary ?? {
46
+ url: CHART_LIBRARY_URL,
47
+ integrity: CHART_LIBRARY_INTEGRITY,
48
+ });
49
+ }
50
+ async fetch(request, requestContext) {
51
+ const input = Object.freeze({ request, requestContext });
52
+ const dispatch = this.middleware.reduceRight((next, middleware) => () => middleware(input, next), () => this.dispatch(request, requestContext));
53
+ try {
54
+ return await dispatch();
55
+ }
56
+ catch (error) {
57
+ this.options.runtime.settings.logger.error({
58
+ event: "solid_objects.dashboard.error",
59
+ error: error instanceof Error ? error.name : "UnknownError",
60
+ });
61
+ return textResponse("Internal Server Error", { status: 500 });
62
+ }
63
+ }
64
+ async dispatch(request, requestContext) {
65
+ const relativePath = this.relativePath(new URL(request.url).pathname);
66
+ if (relativePath === undefined)
67
+ return cascadeResponse();
68
+ const asset = this.asset(relativePath);
69
+ if (asset)
70
+ return asset;
71
+ const matched = matchRoute({
72
+ routes: this.routes,
73
+ requestMethod: request.method,
74
+ requestPath: relativePath,
75
+ });
76
+ if (!matched)
77
+ return cascadeResponse();
78
+ if (this.readOnly() && request.method === "POST")
79
+ return methodNotAllowedResponse();
80
+ const policy = matched.route.policy;
81
+ const resourceId = matched.params.id;
82
+ if (this.access !== "public-read-only") {
83
+ const authorized = await this.options.runtime.settings.authorizeAdministration({
84
+ action: policy.action,
85
+ resource: policy.resource,
86
+ ...(resourceId === undefined ? {} : { resourceId }),
87
+ authorizationContext: requestContext.authorizationContext,
88
+ });
89
+ if (!authorized)
90
+ return textResponse("Forbidden", { status: 403 });
91
+ }
92
+ if (request.method === "POST" && !(await validAuthenticityToken(request, requestContext))) {
93
+ return textResponse("Forbidden", { status: 403 });
94
+ }
95
+ const pageResources = request.method === "HEAD" || relativePath === "/stats"
96
+ ? undefined
97
+ : await this.pageResources({ request, requestContext, relativePath });
98
+ const routeContext = Object.freeze({
99
+ request,
100
+ requestContext,
101
+ params: matched.params,
102
+ runtime: this.options.runtime,
103
+ view: pageResources?.view,
104
+ path: (path) => this.path(path),
105
+ render: (page) => {
106
+ if (!pageResources)
107
+ throw new TypeError("this route cannot render an HTML page");
108
+ return this.htmlResponse(pageResources.view, page);
109
+ },
110
+ escape: escapeHtml,
111
+ });
112
+ try {
113
+ const result = await matched.route.handle(routeContext);
114
+ if (result instanceof Response)
115
+ return result;
116
+ if (!pageResources)
117
+ throw new TypeError("this route cannot return an HTML page");
118
+ return this.htmlResponse(pageResources.view, result);
119
+ }
120
+ catch (error) {
121
+ if (error instanceof Unauthorized)
122
+ return textResponse("Forbidden", { status: 403 });
123
+ throw error;
124
+ }
125
+ }
126
+ async pageResources(options) {
127
+ const statistics = await this.store.statistics();
128
+ const csrfToken = this.readOnly()
129
+ ? undefined
130
+ : await maskedAuthenticityToken(options.requestContext);
131
+ return {
132
+ view: new DashboardView({
133
+ mountPath: this.mountPath,
134
+ currentPath: options.relativePath,
135
+ nonce: randomBytes(16).toString("base64"),
136
+ ...(csrfToken === undefined ? {} : { csrfToken }),
137
+ readOnly: this.readOnly(),
138
+ tabs: this.tabs,
139
+ renderers: this.renderers,
140
+ chartLibrary: this.chartLibrary,
141
+ statistics,
142
+ }),
143
+ };
144
+ }
145
+ htmlResponse(view, page) {
146
+ return new Response(view.page({ title: page.title, content: page.content }), {
147
+ status: page.status ?? 200,
148
+ headers: this.pageHeaders(view.nonce),
149
+ });
150
+ }
151
+ pageHeaders(nonce) {
152
+ const headers = new Headers({
153
+ "cache-control": "private, no-store",
154
+ "content-type": "text/html; charset=utf-8",
155
+ "referrer-policy": "same-origin",
156
+ "x-content-type-options": "nosniff",
157
+ "x-frame-options": "DENY",
158
+ });
159
+ headers.set("content-security-policy", contentSecurityPolicy({ library: this.chartLibrary, nonce }));
160
+ return headers;
161
+ }
162
+ builtInRoutes() {
163
+ return [
164
+ route({
165
+ method: "HEAD",
166
+ path: "/",
167
+ policy: policy("index", "dashboard"),
168
+ handle: async () => {
169
+ await this.store.healthy();
170
+ return new Response(null, {
171
+ status: 200,
172
+ headers: { "cache-control": "private, no-store" },
173
+ });
174
+ },
175
+ }),
176
+ route({
177
+ method: "GET",
178
+ path: "/",
179
+ policy: policy("index", "dashboard"),
180
+ handle: async (context) => {
181
+ const { render, view } = internalContext(context);
182
+ const model = await this.store.dashboard();
183
+ return render({ title: "Dashboard", content: view.dashboard(model) });
184
+ },
185
+ }),
186
+ route({
187
+ method: "GET",
188
+ path: "/stats",
189
+ policy: policy("index", "dashboard"),
190
+ handle: async () => jsonResponse(await this.store.statistics()),
191
+ }),
192
+ route({
193
+ method: "GET",
194
+ path: "/instances",
195
+ policy: policy("index", "instances"),
196
+ handle: async (context) => {
197
+ const { request, view } = internalContext(context);
198
+ const search = new URL(request.url).searchParams;
199
+ const page = await this.store.instances(pageOptions(search));
200
+ return { title: "Instances", content: view.instances(page, search) };
201
+ },
202
+ }),
203
+ route({
204
+ method: "GET",
205
+ path: "/instances/:id",
206
+ policy: policy("show", "instances"),
207
+ handle: async (context) => {
208
+ const { params, view } = internalContext(context);
209
+ const detail = await this.store.instance(requiredParam(params, "id"));
210
+ if (!detail)
211
+ return notFoundResponse();
212
+ return {
213
+ title: `${text(detail.instance.actor_type)} / ${text(detail.instance.actor_id)}`,
214
+ content: view.instance(detail),
215
+ };
216
+ },
217
+ }),
218
+ route({
219
+ method: "POST",
220
+ path: "/instances/:id/pause",
221
+ policy: policy("pause", "instances"),
222
+ handle: async ({ params }) => {
223
+ const id = requiredParam(params, "id");
224
+ if (!(await this.store.setPaused({ id, paused: true })))
225
+ return notFoundResponse();
226
+ return redirectResponse(this.path(`/instances/${id}`));
227
+ },
228
+ }),
229
+ route({
230
+ method: "POST",
231
+ path: "/instances/:id/resume",
232
+ policy: policy("resume", "instances"),
233
+ handle: async ({ params }) => {
234
+ const id = requiredParam(params, "id");
235
+ if (!(await this.store.setPaused({ id, paused: false })))
236
+ return notFoundResponse();
237
+ return redirectResponse(this.path(`/instances/${id}`));
238
+ },
239
+ }),
240
+ route({
241
+ method: "GET",
242
+ path: "/mailbox",
243
+ policy: policy("index", "messages"),
244
+ handle: async (context) => {
245
+ const { request, view } = internalContext(context);
246
+ const search = new URL(request.url).searchParams;
247
+ const membership = search.get("membership") === "claimed" ? "claimed" : "ready";
248
+ const page = await this.store.mailbox({ ...pageOptions(search), membership });
249
+ const filter = `<form class="filters" method="get"><select name="membership"><option value="ready"${membership === "ready" ? " selected" : ""}>Ready</option><option value="claimed"${membership === "claimed" ? " selected" : ""}>Claimed</option></select><button type="submit">Filter</button></form>`;
250
+ return {
251
+ title: "Mailbox",
252
+ content: `${filter}${view.recordsPage({ title: "Mailbox", page, columns: DASHBOARD_COLUMNS.messages, extraQuery: { membership } })}`,
253
+ };
254
+ },
255
+ }),
256
+ route({
257
+ method: "GET",
258
+ path: "/messages/:id",
259
+ policy: policy("show", "messages"),
260
+ handle: async (context) => {
261
+ const { params, view } = internalContext(context);
262
+ const record = await this.store.message(requiredParam(params, "id"));
263
+ if (!record)
264
+ return notFoundResponse();
265
+ return { title: "Message", content: view.message(record) };
266
+ },
267
+ }),
268
+ route({
269
+ method: "GET",
270
+ path: "/reminders",
271
+ policy: policy("index", "reminders"),
272
+ handle: async (context) => this.statusPage({
273
+ context: internalContext(context),
274
+ title: "Reminders",
275
+ statuses: ["scheduled", "paused", "completed"],
276
+ load: (search) => this.store.reminders(pageOptions(search)),
277
+ columns: DASHBOARD_COLUMNS.reminders,
278
+ }),
279
+ }),
280
+ route({
281
+ method: "GET",
282
+ path: "/effects",
283
+ policy: policy("index", "effects"),
284
+ handle: async (context) => this.statusPage({
285
+ context: internalContext(context),
286
+ title: "Effects",
287
+ statuses: ["pending", "processing", "completed", "dead"],
288
+ load: (search) => this.store.effects(pageOptions(search)),
289
+ columns: DASHBOARD_COLUMNS.effects,
290
+ }),
291
+ }),
292
+ route({
293
+ method: "GET",
294
+ path: "/broadcasts",
295
+ policy: policy("index", "broadcasts"),
296
+ handle: async (context) => this.statusPage({
297
+ context: internalContext(context),
298
+ title: "Broadcasts",
299
+ statuses: ["pending", "processing", "delivered", "dead"],
300
+ load: (search) => this.store.broadcasts(pageOptions(search)),
301
+ columns: DASHBOARD_COLUMNS.broadcasts,
302
+ }),
303
+ }),
304
+ route({
305
+ method: "GET",
306
+ path: "/dead-letters",
307
+ policy: policy("index", "dead_letters"),
308
+ handle: async (context) => {
309
+ const { request, view } = internalContext(context);
310
+ const search = new URL(request.url).searchParams;
311
+ const page = await this.store.deadLetters(pageOptions(search));
312
+ return {
313
+ title: "Dead letters",
314
+ content: view.recordsPage({
315
+ title: "Dead letters",
316
+ page,
317
+ columns: DASHBOARD_COLUMNS.deadLetters,
318
+ }),
319
+ };
320
+ },
321
+ }),
322
+ route({
323
+ method: "GET",
324
+ path: "/dead-letters/:id",
325
+ policy: policy("show", "dead_letters"),
326
+ handle: async (context) => {
327
+ const { params, view } = internalContext(context);
328
+ const record = await this.store.deadLetter(requiredParam(params, "id"));
329
+ if (!record)
330
+ return notFoundResponse();
331
+ return { title: "Dead letter", content: view.deadLetter(record) };
332
+ },
333
+ }),
334
+ route({
335
+ method: "POST",
336
+ path: "/dead-letters/:id/retry",
337
+ policy: policy("retry", "dead_letters"),
338
+ handle: async (context) => {
339
+ const { params, requestContext, view } = internalContext(context);
340
+ const id = requiredParam(params, "id");
341
+ try {
342
+ await this.options.runtime.deadLetters.retry(id, {
343
+ authorizationContext: requestContext.authorizationContext,
344
+ });
345
+ return redirectResponse(this.path("/dead-letters"));
346
+ }
347
+ catch (error) {
348
+ if (!(error instanceof SolidObjectsError))
349
+ throw error;
350
+ const record = await this.store.deadLetter(id);
351
+ if (!record)
352
+ return notFoundResponse();
353
+ return {
354
+ title: "Dead letter",
355
+ status: 422,
356
+ content: view.deadLetter(record, `${error.name}: ${error.message}`),
357
+ };
358
+ }
359
+ },
360
+ }),
361
+ route({
362
+ method: "GET",
363
+ path: "/processes",
364
+ policy: policy("index", "processes"),
365
+ handle: async (context) => this.statusPage({
366
+ context: internalContext(context),
367
+ title: "Processes",
368
+ statuses: ["running", "draining", "stopped"],
369
+ load: (search) => this.store.processes(pageOptions(search)),
370
+ columns: DASHBOARD_COLUMNS.processes,
371
+ }),
372
+ }),
373
+ ];
374
+ }
375
+ async statusPage(options) {
376
+ const search = new URL(options.context.request.url).searchParams;
377
+ const status = options.statuses.includes(search.get("status") ?? "")
378
+ ? search.get("status")
379
+ : null;
380
+ const page = await options.load(search);
381
+ const view = options.context.view;
382
+ return {
383
+ title: options.title,
384
+ content: view.recordsPage({
385
+ title: options.title,
386
+ page,
387
+ columns: options.columns,
388
+ status,
389
+ statuses: options.statuses,
390
+ }),
391
+ };
392
+ }
393
+ asset(relativePath) {
394
+ if (relativePath === "/assets/application.css") {
395
+ return assetResponse(DASHBOARD_STYLESHEET, "text/css; charset=utf-8");
396
+ }
397
+ if (relativePath === "/assets/application.js") {
398
+ return assetResponse(DASHBOARD_JAVASCRIPT, "text/javascript; charset=utf-8");
399
+ }
400
+ if (relativePath === "/assets/charts.js") {
401
+ return assetResponse(DASHBOARD_CHARTS_JAVASCRIPT, "text/javascript; charset=utf-8");
402
+ }
403
+ return undefined;
404
+ }
405
+ relativePath(pathname) {
406
+ if (this.mountPath === "")
407
+ return pathname || "/";
408
+ if (pathname === this.mountPath)
409
+ return "/";
410
+ if (!pathname.startsWith(`${this.mountPath}/`))
411
+ return undefined;
412
+ return pathname.slice(this.mountPath.length) || "/";
413
+ }
414
+ path(path) {
415
+ if (path === "/")
416
+ return this.mountPath || "/";
417
+ return `${this.mountPath}${path}`;
418
+ }
419
+ readOnly() {
420
+ return this.access !== "authorized";
421
+ }
422
+ }
423
+ export function createDashboard(options) {
424
+ return new SolidObjectsDashboard(options);
425
+ }
426
+ function route(options) {
427
+ return Object.freeze(options);
428
+ }
429
+ function policy(action, resource) {
430
+ return Object.freeze({ action, resource });
431
+ }
432
+ function validateRoutes(routes) {
433
+ const identities = new Set();
434
+ return Object.freeze(routes.map((item) => {
435
+ if (!item.policy?.action || !item.policy.resource) {
436
+ throw new TypeError(`dashboard route ${item.path} requires an authorization policy`);
437
+ }
438
+ if (!item.path.startsWith("/"))
439
+ throw new TypeError(`dashboard route ${item.path} must start with /`);
440
+ const identity = `${item.method} ${item.path}`;
441
+ if (identities.has(identity))
442
+ throw new TypeError(`duplicate dashboard route ${identity}`);
443
+ identities.add(identity);
444
+ return Object.freeze(item);
445
+ }));
446
+ }
447
+ function matchRoute(options) {
448
+ const { routes, requestMethod, requestPath } = options;
449
+ for (const item of routes) {
450
+ if (item.method !== requestMethod)
451
+ continue;
452
+ const patternParts = item.path.split("/");
453
+ const pathParts = requestPath.split("/");
454
+ if (patternParts.length !== pathParts.length)
455
+ continue;
456
+ const params = {};
457
+ let matched = true;
458
+ for (let index = 0; index < patternParts.length; index += 1) {
459
+ const patternPart = patternParts[index] ?? "";
460
+ const pathPart = pathParts[index] ?? "";
461
+ if (patternPart.startsWith(":")) {
462
+ try {
463
+ params[patternPart.slice(1)] = decodeURIComponent(pathPart);
464
+ }
465
+ catch {
466
+ matched = false;
467
+ }
468
+ }
469
+ else if (patternPart !== pathPart) {
470
+ matched = false;
471
+ }
472
+ if (!matched)
473
+ break;
474
+ }
475
+ if (matched)
476
+ return { route: item, params: Object.freeze(params) };
477
+ }
478
+ return undefined;
479
+ }
480
+ function extensionRoutes(extension) {
481
+ return extension.routes ?? [];
482
+ }
483
+ function extensionTabs(extension) {
484
+ return extension.tab ? [Object.freeze(extension.tab)] : [];
485
+ }
486
+ async function maskedAuthenticityToken(context) {
487
+ const session = context.session;
488
+ if (!session)
489
+ throw new TypeError("read-write dashboard access requires a session");
490
+ let raw = await session.read(CSRF_SESSION_KEY);
491
+ if (!raw || !validRawToken(raw)) {
492
+ raw = randomBytes(TOKEN_BYTES).toString("base64url");
493
+ await session.write(CSRF_SESSION_KEY, raw);
494
+ }
495
+ const token = Buffer.from(raw, "base64url");
496
+ const mask = randomBytes(TOKEN_BYTES);
497
+ const masked = Buffer.alloc(TOKEN_BYTES);
498
+ for (let index = 0; index < TOKEN_BYTES; index += 1)
499
+ masked[index] = mask[index] ^ token[index];
500
+ return Buffer.concat([mask, masked]).toString("base64url");
501
+ }
502
+ async function validAuthenticityToken(request, context) {
503
+ const contentLength = Number(request.headers.get("content-length") ?? 0);
504
+ if (contentLength > MAXIMUM_FORM_BYTES)
505
+ return false;
506
+ const body = await request.text();
507
+ if (Buffer.byteLength(body) > MAXIMUM_FORM_BYTES)
508
+ return false;
509
+ const submitted = new URLSearchParams(body).get("authenticity_token");
510
+ const raw = await context.session?.read(CSRF_SESSION_KEY);
511
+ if (!submitted || !raw || !validRawToken(raw))
512
+ return false;
513
+ try {
514
+ const encoded = Buffer.from(submitted, "base64url");
515
+ if (encoded.length !== TOKEN_BYTES * 2)
516
+ return false;
517
+ const mask = encoded.subarray(0, TOKEN_BYTES);
518
+ const masked = encoded.subarray(TOKEN_BYTES);
519
+ const token = Buffer.alloc(TOKEN_BYTES);
520
+ for (let index = 0; index < TOKEN_BYTES; index += 1)
521
+ token[index] = mask[index] ^ masked[index];
522
+ return timingSafeEqual(token, Buffer.from(raw, "base64url"));
523
+ }
524
+ catch {
525
+ return false;
526
+ }
527
+ }
528
+ function validRawToken(value) {
529
+ try {
530
+ return Buffer.from(value, "base64url").length === TOKEN_BYTES;
531
+ }
532
+ catch {
533
+ return false;
534
+ }
535
+ }
536
+ function normalizeMountPath(value) {
537
+ if (value === "/")
538
+ return "";
539
+ if (!value.startsWith("/"))
540
+ throw new TypeError("dashboard mountPath must start with /");
541
+ return value.replace(/\/+$/, "");
542
+ }
543
+ function dashboardAccess(value) {
544
+ if (value === undefined)
545
+ return "authorized";
546
+ if (["authorized", "authorized-read-only", "public-read-only"].includes(value))
547
+ return value;
548
+ throw new TypeError(`unsupported dashboard access mode ${String(value)}`);
549
+ }
550
+ function pageOptions(search) {
551
+ return {
552
+ page: search.get("page"),
553
+ perPage: search.get("per_page"),
554
+ status: search.get("status"),
555
+ actorType: search.get("actor_type"),
556
+ actorId: search.get("actor_id"),
557
+ };
558
+ }
559
+ function contentSecurityPolicy(options) {
560
+ const sources = ["'self'", `'nonce-${options.nonce}'`];
561
+ if (options.library.url?.includes("//")) {
562
+ try {
563
+ sources.push(new URL(options.library.url).origin);
564
+ }
565
+ catch { }
566
+ }
567
+ return [
568
+ "default-src 'self'",
569
+ "base-uri 'self'",
570
+ "form-action 'self'",
571
+ "frame-ancestors 'none'",
572
+ "img-src 'self' data:",
573
+ "style-src 'self'",
574
+ `script-src ${sources.join(" ")}`,
575
+ "connect-src 'self'",
576
+ "object-src 'none'",
577
+ ].join("; ");
578
+ }
579
+ function internalContext(context) {
580
+ return context;
581
+ }
582
+ function requiredParam(params, name) {
583
+ const value = params[name];
584
+ if (!value)
585
+ throw new TypeError(`missing route parameter ${name}`);
586
+ return value;
587
+ }
588
+ function jsonResponse(value) {
589
+ return new Response(JSON.stringify(value, (_key, item) => typeof item === "bigint" ? String(item) : item), {
590
+ headers: {
591
+ "cache-control": "private, no-store",
592
+ "content-type": "application/json",
593
+ "x-content-type-options": "nosniff",
594
+ },
595
+ });
596
+ }
597
+ function textResponse(body, options) {
598
+ return new Response(body, {
599
+ status: options.status,
600
+ headers: { "cache-control": "private, no-store", "content-type": "text/plain; charset=utf-8" },
601
+ });
602
+ }
603
+ function assetResponse(body, contentType) {
604
+ return new Response(body, {
605
+ headers: {
606
+ "cache-control": "private, max-age=86400",
607
+ "content-type": contentType,
608
+ "x-content-type-options": "nosniff",
609
+ },
610
+ });
611
+ }
612
+ function cascadeResponse() {
613
+ return new Response("Not Found", {
614
+ status: 404,
615
+ headers: { "content-type": "text/plain; charset=utf-8", "x-cascade": "pass" },
616
+ });
617
+ }
618
+ function notFoundResponse() {
619
+ return new Response("Not Found", {
620
+ status: 404,
621
+ headers: { "content-type": "text/plain; charset=utf-8" },
622
+ });
623
+ }
624
+ function redirectResponse(location) {
625
+ return new Response(null, { status: 303, headers: { location } });
626
+ }
627
+ function methodNotAllowedResponse() {
628
+ return new Response("Method Not Allowed", {
629
+ status: 405,
630
+ headers: {
631
+ allow: "GET, HEAD",
632
+ "cache-control": "private, no-store",
633
+ "content-type": "text/plain; charset=utf-8",
634
+ },
635
+ });
636
+ }
637
+ function text(value) {
638
+ if (value === null || value === undefined)
639
+ return "";
640
+ return String(value);
641
+ }
642
+ //# sourceMappingURL=index.js.map