@markusylisiurunen/tau 0.3.91 → 0.3.93

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 (75) hide show
  1. package/dist/core/cli.js +0 -3
  2. package/dist/core/cli.js.map +1 -1
  3. package/dist/core/code_mode/sdk.js +0 -1
  4. package/dist/core/code_mode/sdk.js.map +1 -1
  5. package/dist/core/config/content_loader.js +1 -3
  6. package/dist/core/config/content_loader.js.map +1 -1
  7. package/dist/core/config/index.js +1 -1
  8. package/dist/core/config/index.js.map +1 -1
  9. package/dist/core/config/schema.js +0 -67
  10. package/dist/core/config/schema.js.map +1 -1
  11. package/dist/core/index.js +0 -1
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/core/models/openrouter.js +6 -0
  14. package/dist/core/models/openrouter.js.map +1 -1
  15. package/dist/core/personas.js +30 -4
  16. package/dist/core/personas.js.map +1 -1
  17. package/dist/core/static/tau_docs/config-reference.md +0 -24
  18. package/dist/core/static/tau_docs/configuration.md +1 -1
  19. package/dist/core/static/tau_docs/credentials.md +3 -18
  20. package/dist/core/static/tau_docs/index.md +0 -1
  21. package/dist/core/static/tau_docs/manifest.json +0 -1
  22. package/dist/core/static/tau_docs/openrouter.md +1 -0
  23. package/dist/core/static/tau_docs/personas.md +4 -4
  24. package/dist/core/static/tau_docs/security.md +6 -18
  25. package/dist/core/static/tau_docs/subagents.md +0 -1
  26. package/dist/core/static/tau_docs/tools.md +6 -8
  27. package/dist/core/static/tau_docs/troubleshooting.md +10 -12
  28. package/dist/core/static/tau_docs/tui.md +1 -0
  29. package/dist/core/subagents/claude_code_runner.mjs +43 -43
  30. package/dist/core/subagents/registry.js +1 -2
  31. package/dist/core/subagents/registry.js.map +1 -1
  32. package/dist/core/subagents/types.js +1 -2
  33. package/dist/core/subagents/types.js.map +1 -1
  34. package/dist/core/telegram/workspace.js +1 -1
  35. package/dist/core/telegram/workspace.js.map +1 -1
  36. package/dist/core/tools/code.js +0 -3
  37. package/dist/core/tools/code.js.map +1 -1
  38. package/dist/core/tools/presentation.js +1 -2
  39. package/dist/core/tools/presentation.js.map +1 -1
  40. package/dist/core/tools/tool_names.js +1 -3
  41. package/dist/core/tools/tool_names.js.map +1 -1
  42. package/dist/core/utils/speech.js +6 -2
  43. package/dist/core/utils/speech.js.map +1 -1
  44. package/dist/core/version.js +1 -1
  45. package/dist/diff_tool/app/dist/assets/{core-DVIaxTZj.js → core-DaUcjNBm.js} +1 -1
  46. package/dist/diff_tool/app/dist/assets/{highlights-BKf96Xcr.js → highlights-ey_YWY7Q.js} +1 -1
  47. package/dist/diff_tool/app/dist/assets/{index-DKDNDuYd.js → index-B7CqEx6P.js} +19 -19
  48. package/dist/diff_tool/app/dist/assets/langs-BAlu0Oiy.js +1 -0
  49. package/dist/diff_tool/app/dist/assets/{shiki-DmA--L4w.js → shiki-BDtWTy6K.js} +2 -2
  50. package/dist/diff_tool/app/dist/assets/{stream-tokenizer-5ezp0vCr.js → stream-tokenizer-Dpnp6Te1.js} +1 -1
  51. package/dist/diff_tool/app/dist/assets/{themes-BNoMSXBX.js → themes-snQUZ0v5.js} +2 -2
  52. package/dist/diff_tool/app/dist/index.html +1 -1
  53. package/dist/main.js +0 -23
  54. package/dist/main.js.map +1 -1
  55. package/package.json +4 -5
  56. package/dist/core/code_mode/nook.js +0 -226
  57. package/dist/core/code_mode/nook.js.map +0 -1
  58. package/dist/core/nook/cli.js +0 -258
  59. package/dist/core/nook/cli.js.map +0 -1
  60. package/dist/core/nook/client.js +0 -372
  61. package/dist/core/nook/client.js.map +0 -1
  62. package/dist/core/nook/deploy.js +0 -152
  63. package/dist/core/nook/deploy.js.map +0 -1
  64. package/dist/core/nook/index.js +0 -5
  65. package/dist/core/nook/index.js.map +0 -1
  66. package/dist/core/nook/setup.js +0 -429
  67. package/dist/core/nook/setup.js.map +0 -1
  68. package/dist/core/nook/validation.js +0 -164
  69. package/dist/core/nook/validation.js.map +0 -1
  70. package/dist/core/static/code_mode/nook/documentation.md +0 -144
  71. package/dist/core/static/code_mode/nook/sandbox_runner.mjs +0 -1
  72. package/dist/core/static/tau_docs/nook.md +0 -270
  73. package/dist/diff_tool/app/dist/assets/langs-D3RgqfNW.js +0 -1
  74. package/dist/nook/worker/index.js +0 -1175
  75. package/dist/nook/worker/index.js.map +0 -1
@@ -1,1175 +0,0 @@
1
- import { createRemoteJWKSet, jwtVerify } from "jose";
2
- const RESERVED_PREFIX = "/__nook";
3
- const DEPLOY_SESSION_MS = 15 * 60 * 1000;
4
- const TEMPLATE_SAVE_SESSION_MS = 15 * 60 * 1000;
5
- const MAX_PENDING_DEPLOYMENTS = 3;
6
- const MAX_PENDING_TEMPLATE_SAVES = 3;
7
- const MAX_FILES = 1_000;
8
- const MAX_FILE_BYTES = 10 * 1024 * 1024;
9
- const MAX_TOTAL_BYTES = 100 * 1024 * 1024;
10
- const MAX_PATH_LENGTH = 512;
11
- const MAX_KV_KEY_LENGTH = 256;
12
- const MAX_KV_VALUE_BYTES = 64 * 1024;
13
- const MAX_KV_KEYS = 1_000;
14
- const MAX_KV_TOTAL_BYTES = 5 * 1024 * 1024;
15
- const DEPLOYED_ASSET_CACHE_CONTROL = "public, no-cache, must-revalidate";
16
- const RESERVED_SITE_SLUGS = new Set([
17
- "admin",
18
- "api",
19
- "assets",
20
- "login",
21
- "logout",
22
- "nook",
23
- "quick",
24
- "static",
25
- "www",
26
- ]);
27
- const accessJwksCache = new Map();
28
- const SKILL_MARKDOWN = `# Nook
29
-
30
- Nook deploys static mini-apps to path-based site URLs and gives each site same-origin JSON KV through window.nook. It also stores editable templates that can be copied to a working directory, modified, built, and deployed separately.
31
-
32
- Deploy a finished static directory only. The directory must contain /index.html, must not contain hidden files or symlinks, and must not contain files under /__nook/. Templates follow the same file safety rules but do not require /index.html. Copying a template requires an existing empty destination directory.
33
-
34
- Sites are served at https://<nook-domain>/<site>/.
35
-
36
- The Worker injects /<site>/__nook/client.js just after the opening HTML body tag.
37
-
38
- Code that runs from the document head should wait until DOMContentLoaded, or otherwise run after the page has loaded, before using window.nook:
39
-
40
- \`\`\`js
41
- window.addEventListener("DOMContentLoaded", async () => {
42
- await window.nook.kv.put("settings", { theme: "dark" });
43
- const settings = await window.nook.kv.get("settings");
44
- await window.nook.kv.delete("settings");
45
- const keys = await window.nook.kv.list({ prefix: "todos/" });
46
- });
47
- \`\`\`
48
-
49
- KV is per-site, JSON-only, and survives redeploys. Public deployments expose public writable per-site KV. Private deployments require Cloudflare Access identity.
50
- `;
51
- const CLIENT_JS = `(() => {
52
- const script = document.currentScript;
53
- const scriptUrl = script && script.src
54
- ? new URL(script.src)
55
- : new URL("/__nook/client.js", window.location.origin);
56
- const basePath = scriptUrl.pathname.replace(/\\/__nook\\/client\\.js$/, "");
57
- function apiPath(path) {
58
- return basePath + path;
59
- }
60
- async function request(path, options = {}) {
61
- const response = await fetch(path, {
62
- ...options,
63
- headers: {
64
- ...(options.body !== undefined ? { "content-type": "application/json" } : {}),
65
- ...(options.headers || {})
66
- }
67
- });
68
- if (!response.ok) {
69
- let message = response.statusText;
70
- try {
71
- const payload = await response.json();
72
- message = payload && payload.error && payload.error.message ? payload.error.message : message;
73
- } catch {}
74
- if (response.status === 401) {
75
- const authUrl = new URL("/__nook/auth", window.location.origin);
76
- authUrl.searchParams.set(
77
- "returnTo",
78
- window.location.pathname + window.location.search + window.location.hash
79
- );
80
- window.location.assign(authUrl.toString());
81
- }
82
- throw new Error(message);
83
- }
84
- if (response.status === 204) return null;
85
- return await response.json();
86
- }
87
- window.nook = {
88
- kv: {
89
- async get(key) {
90
- const payload = await request(apiPath("/__nook/kv/" + encodeURIComponent(key)));
91
- return payload.value;
92
- },
93
- async put(key, value) {
94
- await request(apiPath("/__nook/kv/" + encodeURIComponent(key)), {
95
- method: "PUT",
96
- body: JSON.stringify(value)
97
- });
98
- },
99
- async delete(key) {
100
- await request(apiPath("/__nook/kv/" + encodeURIComponent(key)), { method: "DELETE" });
101
- },
102
- async list(options = {}) {
103
- const url = new URL(apiPath("/__nook/kv"), window.location.origin);
104
- if (options.prefix) url.searchParams.set("prefix", options.prefix);
105
- const payload = await request(url.pathname + url.search);
106
- return payload.keys;
107
- }
108
- }
109
- };
110
- })();`;
111
- function json(data, status = 200) {
112
- return new Response(JSON.stringify(data), {
113
- status,
114
- headers: { "content-type": "application/json; charset=utf-8" },
115
- });
116
- }
117
- function error(code, message, status) {
118
- return json({ error: { code, message } }, status);
119
- }
120
- function now() {
121
- return new Date().toISOString();
122
- }
123
- function randomId(prefix) {
124
- const bytes = crypto.getRandomValues(new Uint8Array(12));
125
- return `${prefix}_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
126
- }
127
- function accessTokenFromRequest(request) {
128
- const headerToken = request.headers.get("Cf-Access-Jwt-Assertion");
129
- if (headerToken?.trim())
130
- return headerToken.trim();
131
- const cookie = request.headers.get("Cookie") ?? "";
132
- for (const segment of cookie.split(";")) {
133
- const [name, ...rest] = segment.trim().split("=");
134
- if (name === "CF_Authorization")
135
- return rest.join("=");
136
- }
137
- return undefined;
138
- }
139
- function accessJwks(teamDomain) {
140
- let jwks = accessJwksCache.get(teamDomain);
141
- if (!jwks) {
142
- jwks = createRemoteJWKSet(new URL(`${teamDomain}/cdn-cgi/access/certs`));
143
- accessJwksCache.set(teamDomain, jwks);
144
- }
145
- return jwks;
146
- }
147
- function stringClaim(payload, claim) {
148
- const value = payload[claim];
149
- return typeof value === "string" && value ? value : undefined;
150
- }
151
- function accessActor(payload) {
152
- return (stringClaim(payload, "email") ??
153
- stringClaim(payload, "common_name") ??
154
- payload.sub ??
155
- "access-user");
156
- }
157
- async function verifyAccessJwt(token, env) {
158
- const teamDomain = env.NOOK_ACCESS_TEAM_DOMAIN?.replace(/\/+$/, "");
159
- const audience = env.NOOK_ACCESS_AUD?.trim();
160
- if (!teamDomain || !audience) {
161
- throw new ErrorResponse("access_not_configured", "Cloudflare Access is not configured", 500);
162
- }
163
- try {
164
- const { payload } = await jwtVerify(token, accessJwks(teamDomain), {
165
- issuer: teamDomain,
166
- audience,
167
- algorithms: ["RS256"],
168
- });
169
- if (!payload.exp) {
170
- throw new ErrorResponse("unauthorized", "Cloudflare Access JWT has invalid claims", 401);
171
- }
172
- return { actor: accessActor(payload) };
173
- }
174
- catch (err) {
175
- if (err instanceof ErrorResponse)
176
- throw err;
177
- throw new ErrorResponse("unauthorized", "Invalid Cloudflare Access JWT", 401);
178
- }
179
- }
180
- async function requireAccessIdentity(request, env) {
181
- const token = accessTokenFromRequest(request);
182
- if (!token)
183
- throw new ErrorResponse("unauthorized", "Authentication required", 401);
184
- return await verifyAccessJwt(token, env);
185
- }
186
- async function optionalAccessIdentity(request, env) {
187
- const token = accessTokenFromRequest(request);
188
- if (!token)
189
- return { actor: "anonymous" };
190
- try {
191
- return await verifyAccessJwt(token, env);
192
- }
193
- catch {
194
- return { actor: "anonymous" };
195
- }
196
- }
197
- async function privateAccessIdentity(request, env) {
198
- try {
199
- return await requireAccessIdentity(request, env);
200
- }
201
- catch (err) {
202
- if (err instanceof ErrorResponse && err.status === 401)
203
- return undefined;
204
- throw err;
205
- }
206
- }
207
- function accessLoginUrl(request) {
208
- const requestUrl = new URL(request.url);
209
- const loginUrl = new URL("/__nook/auth", requestUrl.origin);
210
- loginUrl.searchParams.set("returnTo", `${requestUrl.pathname}${requestUrl.search}`);
211
- return loginUrl.toString();
212
- }
213
- function accessReturnUrl(request) {
214
- const requestUrl = new URL(request.url);
215
- const returnTo = requestUrl.searchParams.get("returnTo");
216
- if (!returnTo)
217
- return undefined;
218
- let returnUrl;
219
- try {
220
- returnUrl = new URL(returnTo, requestUrl.origin);
221
- }
222
- catch {
223
- return undefined;
224
- }
225
- if (returnUrl.origin !== requestUrl.origin || !parseSitePath(returnUrl.pathname)) {
226
- return undefined;
227
- }
228
- return returnUrl;
229
- }
230
- function validatePathLabel(value, label) {
231
- if (!/^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(value))
232
- return `invalid ${label}`;
233
- return undefined;
234
- }
235
- function validateSlug(slug) {
236
- return (validatePathLabel(slug, "slug") ?? (RESERVED_SITE_SLUGS.has(slug) ? "reserved slug" : undefined));
237
- }
238
- function validateTemplateName(name) {
239
- return validatePathLabel(name, "template name");
240
- }
241
- function normalizeAssetPath(pathname) {
242
- if (!pathname ||
243
- pathname.includes("\0") ||
244
- !pathname.startsWith("/") ||
245
- pathname.startsWith("//")) {
246
- return undefined;
247
- }
248
- const parts = pathname.split("/");
249
- const out = [];
250
- for (const part of parts) {
251
- if (!part || part === ".")
252
- continue;
253
- if (part === "..")
254
- return undefined;
255
- out.push(part);
256
- }
257
- const normalized = `/${out.join("/")}`;
258
- if (normalized === RESERVED_PREFIX || normalized.startsWith(`${RESERVED_PREFIX}/`))
259
- return undefined;
260
- if (normalized.length > MAX_PATH_LENGTH)
261
- return undefined;
262
- return normalized;
263
- }
264
- function validateManifest(files, options) {
265
- const label = options.artifactLabel;
266
- if (!Array.isArray(files) || files.length === 0)
267
- return "manifest must include files";
268
- if (files.length > MAX_FILES)
269
- return `${label} exceeds ${MAX_FILES} files`;
270
- const paths = new Set();
271
- let total = 0;
272
- let hasIndex = false;
273
- for (const file of files) {
274
- const normalized = normalizeAssetPath(file.path);
275
- if (!normalized || normalized !== file.path)
276
- return `invalid asset path ${file.path}`;
277
- if (paths.has(file.path))
278
- return `duplicate asset path ${file.path}`;
279
- if (file.path.split("/").some((segment) => segment.startsWith("."))) {
280
- return `hidden deploy path ${file.path} is not allowed`;
281
- }
282
- if (typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(file.sha256)) {
283
- return `invalid sha256 for ${file.path}`;
284
- }
285
- paths.add(file.path);
286
- if (file.path === "/index.html")
287
- hasIndex = true;
288
- if (!Number.isInteger(file.sizeBytes) || file.sizeBytes < 0)
289
- return `invalid size for ${file.path}`;
290
- if (file.sizeBytes > MAX_FILE_BYTES)
291
- return `${file.path} exceeds max file size`;
292
- total += file.sizeBytes;
293
- if (typeof file.contentType !== "string" || !file.contentType)
294
- return `invalid content type for ${file.path}`;
295
- }
296
- if (options.requireIndex && !hasIndex)
297
- return "root index.html is required";
298
- if (total > MAX_TOTAL_BYTES)
299
- return `${label} exceeds max total size`;
300
- return undefined;
301
- }
302
- function parseHost(url, env) {
303
- const domain = env.NOOK_DOMAIN.toLowerCase();
304
- const host = url.hostname.toLowerCase();
305
- if (host === domain)
306
- return { kind: "base" };
307
- return undefined;
308
- }
309
- function parseSitePath(pathname) {
310
- const match = pathname.match(/^\/([^/]+)(\/.*)?$/);
311
- if (!match)
312
- return undefined;
313
- let slug;
314
- try {
315
- slug = decodeURIComponent(match[1]);
316
- }
317
- catch {
318
- return undefined;
319
- }
320
- const slugError = validateSlug(slug);
321
- if (slugError)
322
- return undefined;
323
- const rest = match[2];
324
- return {
325
- slug,
326
- sitePath: rest && rest !== "/" ? rest : "/index.html",
327
- needsSlashRedirect: rest === undefined,
328
- };
329
- }
330
- async function readJson(request) {
331
- try {
332
- return await request.json();
333
- }
334
- catch {
335
- throw new ErrorResponse("invalid_json", "Invalid JSON body", 400);
336
- }
337
- }
338
- async function registryFetch(env, path, init) {
339
- return await env.REGISTRY_DO.get(env.REGISTRY_DO.idFromName("__nook_registry")).fetch(new Request(`https://registry.local${path}`, init));
340
- }
341
- async function siteFetch(env, site, path, init) {
342
- return await env.SITE_DO.get(env.SITE_DO.idFromName(site)).fetch(new Request(`https://site.local${path}`, init));
343
- }
344
- function assetKey(site, deploymentId, path) {
345
- return `sites/${site}/deployments/${deploymentId}${path}`;
346
- }
347
- function templateRevisionPrefix(name, revisionId) {
348
- return `templates/${name}/revisions/${revisionId}/`;
349
- }
350
- function templateFileKey(name, revisionId, path) {
351
- return `${templateRevisionPrefix(name, revisionId)}${path.replace(/^\/+/, "")}`;
352
- }
353
- function siteUrl(domain, site) {
354
- return `https://${domain}/${site}/`;
355
- }
356
- async function sha256Hex(bytes) {
357
- const digest = await crypto.subtle.digest("SHA-256", bytes);
358
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
359
- }
360
- async function deleteR2Prefix(bucket, prefix) {
361
- let cursor;
362
- let deleted = 0;
363
- do {
364
- const listed = await bucket.list({ prefix, cursor, limit: 1000 });
365
- if (listed.objects.length > 0) {
366
- await bucket.delete(listed.objects.map((object) => object.key));
367
- deleted += listed.objects.length;
368
- }
369
- cursor = listed.cursor;
370
- if (!listed.truncated)
371
- break;
372
- } while (cursor);
373
- return deleted;
374
- }
375
- async function handleTemplateApi(request, env, url, ctx) {
376
- const match = url.pathname.match(/^\/__nook\/api\/templates(?:\/([^/]+))?(?:\/save\/([^/]+)\/(file|finish)|\/save\/start|\/file)?$/);
377
- if (!match)
378
- return error("not_found", "Unknown Nook API route", 404);
379
- const name = match[1] ? decodeURIComponent(match[1]) : undefined;
380
- const saveId = match[2] ? decodeURIComponent(match[2]) : undefined;
381
- const saveAction = match[3];
382
- if (url.pathname === "/__nook/api/templates" && request.method === "GET") {
383
- return await registryFetch(env, "/templates");
384
- }
385
- if (!name)
386
- return error("template_not_found", "Template is required", 404);
387
- const nameError = validateTemplateName(name);
388
- if (nameError)
389
- return error("invalid_template", nameError, 400);
390
- if (request.method === "GET" && url.pathname === `/__nook/api/templates/${name}`) {
391
- return await registryFetch(env, `/templates/${encodeURIComponent(name)}/manifest`);
392
- }
393
- if (request.method === "GET" && url.pathname === `/__nook/api/templates/${name}/file`) {
394
- const revisionId = url.searchParams.get("revision");
395
- if (!revisionId || !/^tpl_[a-f0-9]{24}$/.test(revisionId)) {
396
- return error("invalid_revision", "Invalid template revision", 400);
397
- }
398
- const path = url.searchParams.get("path");
399
- const normalized = path ? normalizeAssetPath(path) : undefined;
400
- if (!normalized || normalized !== path)
401
- return error("invalid_path", "Invalid template file path", 400);
402
- const object = await env.ASSETS.get(templateFileKey(name, revisionId, normalized));
403
- if (!object)
404
- return error("not_found", "Template file not found", 404);
405
- return new Response(object.body, {
406
- headers: { "content-type": object.httpMetadata?.contentType ?? "application/octet-stream" },
407
- });
408
- }
409
- if (request.method === "DELETE" && url.pathname === `/__nook/api/templates/${name}`) {
410
- await deleteR2Prefix(env.ASSETS, `templates/${name}/`);
411
- await registryFetch(env, `/templates/${encodeURIComponent(name)}`, { method: "DELETE" });
412
- return json({ template: name, deleted: true });
413
- }
414
- if (request.method === "POST" && url.pathname.endsWith("/save/start")) {
415
- const body = (await readJson(request));
416
- const files = body.files ?? [];
417
- const validationError = validateManifest(files, {
418
- requireIndex: false,
419
- artifactLabel: "template",
420
- });
421
- if (validationError)
422
- return error("invalid_manifest", validationError, 400);
423
- const start = await registryFetch(env, `/templates/${encodeURIComponent(name)}/save/start`, {
424
- method: "POST",
425
- body: JSON.stringify({ files }),
426
- });
427
- if (!start.ok)
428
- return start;
429
- const result = (await start.json());
430
- for (const expiredSaveId of result.expiredSaveIds) {
431
- ctx.waitUntil(deleteR2Prefix(env.ASSETS, templateRevisionPrefix(name, expiredSaveId)));
432
- }
433
- const { expiredSaveIds: _expiredSaveIds, ...response } = result;
434
- return json(response);
435
- }
436
- if (!saveId)
437
- return error("save_not_found", "Template save is required", 404);
438
- const token = request.headers.get("x-nook-template-token");
439
- if (!token)
440
- return error("unauthorized", "Missing template save token", 401);
441
- if (request.method === "PUT" && saveAction === "file") {
442
- const path = url.searchParams.get("path");
443
- if (!path)
444
- return error("invalid_path", "Missing upload path", 400);
445
- const verify = await registryFetch(env, `/templates/${encodeURIComponent(name)}/save/${encodeURIComponent(saveId)}/verify-upload`, {
446
- method: "POST",
447
- body: JSON.stringify({ token, path }),
448
- });
449
- if (!verify.ok)
450
- return verify;
451
- const payload = (await verify.json());
452
- const bytes = await request.arrayBuffer();
453
- if (bytes.byteLength !== payload.file.sizeBytes) {
454
- return error("invalid_upload", "Uploaded file size does not match manifest", 400);
455
- }
456
- if ((await sha256Hex(bytes)) !== payload.file.sha256) {
457
- return error("invalid_upload", "Uploaded file hash does not match manifest", 400);
458
- }
459
- await env.ASSETS.put(templateFileKey(name, saveId, payload.file.path), bytes, {
460
- httpMetadata: { contentType: payload.file.contentType },
461
- });
462
- return await registryFetch(env, `/templates/${encodeURIComponent(name)}/save/${encodeURIComponent(saveId)}/mark-uploaded`, {
463
- method: "POST",
464
- body: JSON.stringify({ token, path }),
465
- });
466
- }
467
- if (request.method === "POST" && saveAction === "finish") {
468
- const finish = await registryFetch(env, `/templates/${encodeURIComponent(name)}/save/${encodeURIComponent(saveId)}/finish`, {
469
- method: "POST",
470
- body: JSON.stringify({ token }),
471
- });
472
- if (!finish.ok)
473
- return finish;
474
- const result = (await finish.json());
475
- if (result.previousRevisionId) {
476
- ctx.waitUntil(deleteR2Prefix(env.ASSETS, templateRevisionPrefix(name, result.previousRevisionId)));
477
- }
478
- const { previousRevisionId: _previousRevisionId, ...summary } = result;
479
- return json(summary);
480
- }
481
- return error("not_found", "Unknown Nook API route", 404);
482
- }
483
- async function handleBaseApi(request, env, url, identity, ctx) {
484
- if (url.pathname === "/__nook/api/templates" ||
485
- url.pathname.startsWith("/__nook/api/templates/")) {
486
- return await handleTemplateApi(request, env, url, ctx);
487
- }
488
- if (request.method === "POST" && url.pathname === "/__nook/api/destroy") {
489
- const sitesResponse = await registryFetch(env, `/sites?domain=${encodeURIComponent(env.NOOK_DOMAIN)}`);
490
- if (!sitesResponse.ok)
491
- return sitesResponse;
492
- const payload = (await sitesResponse.json());
493
- for (const site of payload.sites) {
494
- await deleteR2Prefix(env.ASSETS, `sites/${site.slug}/`);
495
- await siteFetch(env, site.slug, "/delete", { method: "POST" });
496
- }
497
- await registryFetch(env, "/delete-all", { method: "POST" });
498
- const objectsDeleted = await deleteR2Prefix(env.ASSETS, "");
499
- return json({
500
- deleted: true,
501
- sitesDeleted: payload.sites.length,
502
- objectsDeleted,
503
- actor: identity.actor,
504
- });
505
- }
506
- const kvMatch = url.pathname.match(/^\/__nook\/api\/sites\/([^/]+)\/kv(\/.*)?$/);
507
- if (kvMatch) {
508
- const site = decodeURIComponent(kvMatch[1]);
509
- const slugError = validateSlug(site);
510
- if (slugError)
511
- return error("invalid_slug", slugError, 400);
512
- const active = await siteFetch(env, site, "/active");
513
- if (!active.ok)
514
- return active;
515
- return await handleKv(request, env, site, identity, `/__nook/kv${kvMatch[2] ?? ""}`);
516
- }
517
- const match = url.pathname.match(/^\/__nook\/api\/sites(?:\/([^/]+))?(?:\/deploy\/([^/]+)\/(file|finish)|\/deploy\/start|\/file)?$/);
518
- if (!match)
519
- return error("not_found", "Unknown Nook API route", 404);
520
- const site = match[1] ? decodeURIComponent(match[1]) : undefined;
521
- const deploymentId = match[2] ? decodeURIComponent(match[2]) : undefined;
522
- const deployAction = match[3];
523
- if (url.pathname === "/__nook/api/sites" && request.method === "GET") {
524
- return await registryFetch(env, `/sites?domain=${encodeURIComponent(env.NOOK_DOMAIN)}`);
525
- }
526
- if (!site)
527
- return error("site_not_found", "Site is required", 404);
528
- const slugError = validateSlug(site);
529
- if (slugError)
530
- return error("invalid_slug", slugError, 400);
531
- if (request.method === "DELETE" && url.pathname === `/__nook/api/sites/${site}`) {
532
- await deleteR2Prefix(env.ASSETS, `sites/${site}/`);
533
- await siteFetch(env, site, "/delete", { method: "POST" });
534
- await registryFetch(env, `/sites/${encodeURIComponent(site)}`, { method: "DELETE" });
535
- return json({ site, deleted: true });
536
- }
537
- if (request.method === "GET" && url.pathname === `/__nook/api/sites/${site}`) {
538
- const active = await siteFetch(env, site, "/active");
539
- if (!active.ok)
540
- return active;
541
- const deployment = (await active.json());
542
- return json({
543
- site,
544
- deploymentId: deployment.deploymentId,
545
- visibility: deployment.public ? "public" : "private",
546
- fileCount: deployment.files.length,
547
- byteCount: deployment.files.reduce((total, file) => total + file.sizeBytes, 0),
548
- files: deployment.files,
549
- });
550
- }
551
- if (request.method === "GET" && url.pathname === `/__nook/api/sites/${site}/file`) {
552
- const deploymentId = url.searchParams.get("deployment");
553
- if (!deploymentId || !/^dep_[a-f0-9]{24}$/.test(deploymentId)) {
554
- return error("invalid_deployment", "Invalid deployment", 400);
555
- }
556
- const path = url.searchParams.get("path");
557
- const normalized = path ? normalizeAssetPath(path) : undefined;
558
- if (!normalized || normalized !== path)
559
- return error("invalid_path", "Invalid site file path", 400);
560
- const active = await siteFetch(env, site, "/active");
561
- if (!active.ok)
562
- return active;
563
- const deployment = (await active.json());
564
- if (deployment.deploymentId !== deploymentId) {
565
- return error("deployment_changed", "Active deployment changed during copy", 409);
566
- }
567
- if (!deployment.files.some((file) => file.path === normalized)) {
568
- return error("invalid_path", "Path is not in the active deployment manifest", 400);
569
- }
570
- const object = await env.ASSETS.get(assetKey(site, deploymentId, normalized));
571
- if (!object)
572
- return error("not_found", "Site file not found", 404);
573
- return new Response(object.body, {
574
- headers: { "content-type": object.httpMetadata?.contentType ?? "application/octet-stream" },
575
- });
576
- }
577
- if (request.method === "POST" && url.pathname.endsWith("/deploy/start")) {
578
- const body = (await readJson(request));
579
- const files = body.files ?? [];
580
- const validationError = validateManifest(files, {
581
- requireIndex: true,
582
- artifactLabel: "deployment",
583
- });
584
- if (validationError)
585
- return error("invalid_manifest", validationError, 400);
586
- await registryFetch(env, "/sites", {
587
- method: "POST",
588
- body: JSON.stringify({ slug: site, domain: env.NOOK_DOMAIN }),
589
- });
590
- return await siteFetch(env, site, "/deploy/start", {
591
- method: "POST",
592
- body: JSON.stringify({ files, public: body.public === true, actor: identity.actor }),
593
- });
594
- }
595
- if (!deploymentId)
596
- return error("deployment_not_found", "Deployment is required", 404);
597
- const token = request.headers.get("x-nook-deploy-token");
598
- if (!token)
599
- return error("unauthorized", "Missing deploy token", 401);
600
- if (request.method === "PUT" && deployAction === "file") {
601
- const path = url.searchParams.get("path");
602
- if (!path)
603
- return error("invalid_path", "Missing upload path", 400);
604
- const verify = await siteFetch(env, site, "/deploy/verify-upload", {
605
- method: "POST",
606
- body: JSON.stringify({ deploymentId, token, path }),
607
- });
608
- if (!verify.ok)
609
- return verify;
610
- const payload = (await verify.json());
611
- const bytes = await request.arrayBuffer();
612
- if (bytes.byteLength !== payload.file.sizeBytes) {
613
- return error("invalid_upload", "Uploaded file size does not match manifest", 400);
614
- }
615
- if ((await sha256Hex(bytes)) !== payload.file.sha256) {
616
- return error("invalid_upload", "Uploaded file hash does not match manifest", 400);
617
- }
618
- await env.ASSETS.put(assetKey(site, deploymentId, payload.file.path), bytes, {
619
- httpMetadata: { contentType: payload.file.contentType },
620
- });
621
- return await siteFetch(env, site, "/deploy/mark-uploaded", {
622
- method: "POST",
623
- body: JSON.stringify({ deploymentId, token, path }),
624
- });
625
- }
626
- if (request.method === "POST" && deployAction === "finish") {
627
- const finish = await siteFetch(env, site, "/deploy/finish", {
628
- method: "POST",
629
- body: JSON.stringify({ deploymentId, token }),
630
- });
631
- if (!finish.ok)
632
- return finish;
633
- const rawResult = (await finish.json());
634
- const result = {
635
- ...rawResult,
636
- site,
637
- url: siteUrl(env.NOOK_DOMAIN, site),
638
- };
639
- await registryFetch(env, `/sites/${encodeURIComponent(site)}/deploy`, {
640
- method: "POST",
641
- body: JSON.stringify({
642
- latestDeploymentId: result.deploymentId,
643
- visibility: result.visibility,
644
- }),
645
- });
646
- if (result.previousDeploymentId) {
647
- ctx.waitUntil(deleteR2Prefix(env.ASSETS, `sites/${site}/deployments/${result.previousDeploymentId}/`));
648
- }
649
- return json(result);
650
- }
651
- return error("not_found", "Unknown Nook API route", 404);
652
- }
653
- async function handleKv(request, env, site, identity, sitePath) {
654
- return await siteFetch(env, site, `/kv${sitePath.slice("/__nook/kv".length)}${new URL(request.url).search}`, {
655
- method: request.method,
656
- headers: { "x-nook-actor": identity.actor },
657
- body: request.body,
658
- });
659
- }
660
- function shouldSpaFallback(pathname) {
661
- const segment = pathname.split("/").pop() ?? "";
662
- return !segment.includes(".");
663
- }
664
- export function cacheControlForDeployedAsset() {
665
- return DEPLOYED_ASSET_CACHE_CONTROL;
666
- }
667
- function nookClientScriptTag(site) {
668
- return `<script src="/${site}/__nook/client.js"></script>`;
669
- }
670
- function injectNookClientScript(response, site) {
671
- const scriptTag = nookClientScriptTag(site);
672
- const injector = {
673
- element(element) {
674
- element.prepend(scriptTag, { html: true });
675
- delete injector.end;
676
- },
677
- end(end) {
678
- end.append(scriptTag, { html: true });
679
- },
680
- };
681
- return new HTMLRewriter().on("body", injector).onDocument(injector).transform(response);
682
- }
683
- async function serveAsset(request, env, site, sitePath) {
684
- const activeResponse = await siteFetch(env, site, "/active");
685
- if (!activeResponse.ok)
686
- return error("site_not_found", "Site has no active deployment", 404);
687
- const active = (await activeResponse.json());
688
- const identity = active.public
689
- ? await optionalAccessIdentity(request, env)
690
- : await privateAccessIdentity(request, env);
691
- if (!identity) {
692
- if (sitePath === "/__nook/kv" || sitePath.startsWith("/__nook/kv/")) {
693
- return error("unauthorized", "Authentication required", 401);
694
- }
695
- return Response.redirect(accessLoginUrl(request), 302);
696
- }
697
- if (sitePath === "/__nook/client.js") {
698
- return new Response(CLIENT_JS, {
699
- headers: { "content-type": "application/javascript; charset=utf-8" },
700
- });
701
- }
702
- if (sitePath === "/__nook/kv" || sitePath.startsWith("/__nook/kv/")) {
703
- return await handleKv(request, env, site, identity, sitePath);
704
- }
705
- if (sitePath === RESERVED_PREFIX || sitePath.startsWith(`${RESERVED_PREFIX}/`)) {
706
- return error("not_found", "Unknown Nook route", 404);
707
- }
708
- const normalized = normalizeAssetPath(sitePath);
709
- if (!normalized)
710
- return error("invalid_path", "Invalid path", 400);
711
- let object = await env.ASSETS.get(assetKey(site, active.deploymentId, normalized));
712
- let servedPath = normalized;
713
- if (!object && shouldSpaFallback(normalized)) {
714
- servedPath = "/index.html";
715
- object = await env.ASSETS.get(assetKey(site, active.deploymentId, servedPath));
716
- }
717
- if (!object)
718
- return new Response("Not found", { status: 404 });
719
- const file = active.files.find((entry) => entry.path === servedPath);
720
- const contentType = object.httpMetadata?.contentType ?? file?.contentType ?? "application/octet-stream";
721
- const headers = new Headers({
722
- "content-type": contentType,
723
- "cache-control": cacheControlForDeployedAsset(),
724
- });
725
- if (contentType.startsWith("text/html")) {
726
- return injectNookClientScript(new Response(object.body ?? "", { headers }), site);
727
- }
728
- return new Response(object.body, { headers });
729
- }
730
- export default {
731
- async fetch(request, env, ctx) {
732
- try {
733
- const url = new URL(request.url);
734
- const parsedHost = parseHost(url, env);
735
- if (!parsedHost)
736
- return error("not_found", "Unknown nook host", 404);
737
- if (url.pathname === "/__nook/auth") {
738
- if (request.method !== "GET") {
739
- return error("method_not_allowed", "Method not allowed", 405);
740
- }
741
- await requireAccessIdentity(request, env);
742
- const returnUrl = accessReturnUrl(request);
743
- if (!returnUrl)
744
- return error("invalid_return_url", "Invalid return URL", 400);
745
- return Response.redirect(returnUrl.toString(), 303);
746
- }
747
- if (url.pathname === "/__nook/skill") {
748
- await requireAccessIdentity(request, env);
749
- return new Response(SKILL_MARKDOWN, {
750
- headers: { "content-type": "text/markdown; charset=utf-8" },
751
- });
752
- }
753
- if (url.pathname.startsWith("/__nook/api/")) {
754
- const identity = await requireAccessIdentity(request, env);
755
- return await handleBaseApi(request, env, url, identity, ctx);
756
- }
757
- const sitePath = parseSitePath(url.pathname);
758
- if (!sitePath)
759
- return error("not_found", "Unknown Nook route", 404);
760
- if (sitePath.needsSlashRedirect) {
761
- const redirectUrl = new URL(request.url);
762
- redirectUrl.pathname = `${redirectUrl.pathname}/`;
763
- return Response.redirect(redirectUrl.toString(), 308);
764
- }
765
- return await serveAsset(request, env, sitePath.slug, sitePath.sitePath);
766
- }
767
- catch (err) {
768
- if (err instanceof ErrorResponse)
769
- return err.toResponse();
770
- return error("internal_error", err instanceof Error ? err.message : String(err), 500);
771
- }
772
- },
773
- };
774
- export class RegistryDO {
775
- state;
776
- constructor(state) {
777
- this.state = state;
778
- }
779
- async fetch(request) {
780
- const url = new URL(request.url);
781
- if (request.method === "POST" && url.pathname === "/delete-all") {
782
- await this.state.storage.deleteAll();
783
- return json({ deleted: true });
784
- }
785
- if (request.method === "GET" && url.pathname === "/templates") {
786
- const records = await this.state.storage.list({ prefix: "template:" });
787
- return json({ templates: [...records.values()] });
788
- }
789
- const templateManifestMatch = url.pathname.match(/^\/templates\/([^/]+)\/manifest$/);
790
- if (request.method === "GET" && templateManifestMatch) {
791
- const name = decodeURIComponent(templateManifestMatch[1]);
792
- const template = await this.state.storage.get(`template:${name}`);
793
- if (!template)
794
- return error("template_not_found", "Template not found", 404);
795
- const files = await this.state.storage.get(`template-files:${name}`);
796
- return json({ template, files: files ?? [] });
797
- }
798
- const templateSaveStartMatch = url.pathname.match(/^\/templates\/([^/]+)\/save\/start$/);
799
- if (request.method === "POST" && templateSaveStartMatch) {
800
- const name = decodeURIComponent(templateSaveStartMatch[1]);
801
- const body = (await readJson(request));
802
- const timestamp = now();
803
- const nowMs = Date.now();
804
- const saves = await this.state.storage.list({
805
- prefix: `template-save:${name}:`,
806
- });
807
- const expiredSaveIds = [];
808
- const expiredSaveKeys = [];
809
- let pendingSaves = 0;
810
- for (const [key, save] of saves.entries()) {
811
- if (Date.parse(save.expiresAt) < nowMs) {
812
- expiredSaveIds.push(save.saveId);
813
- expiredSaveKeys.push(key);
814
- }
815
- else {
816
- pendingSaves += 1;
817
- }
818
- }
819
- if (expiredSaveKeys.length > 0)
820
- await this.state.storage.delete(expiredSaveKeys);
821
- if (pendingSaves >= MAX_PENDING_TEMPLATE_SAVES) {
822
- return error("quota_exceeded", "Too many pending saves for this template", 429);
823
- }
824
- const save = {
825
- saveId: randomId("tpl"),
826
- token: randomId("tok"),
827
- createdAt: timestamp,
828
- expiresAt: new Date(Date.now() + TEMPLATE_SAVE_SESSION_MS).toISOString(),
829
- files: body.files,
830
- uploaded: [],
831
- };
832
- await this.state.storage.put(`template-save:${name}:${save.saveId}`, save);
833
- return json({
834
- saveId: save.saveId,
835
- upload: save.files.map((file) => file.path),
836
- token: save.token,
837
- expiredSaveIds,
838
- });
839
- }
840
- const templateSaveActionMatch = url.pathname.match(/^\/templates\/([^/]+)\/save\/([^/]+)\/(verify-upload|mark-uploaded|finish)$/);
841
- if (request.method === "POST" && templateSaveActionMatch) {
842
- const name = decodeURIComponent(templateSaveActionMatch[1]);
843
- const saveId = decodeURIComponent(templateSaveActionMatch[2]);
844
- const action = templateSaveActionMatch[3];
845
- const body = (await readJson(request));
846
- const save = await this.requireTemplateSave(name, saveId, body.token);
847
- if (save instanceof Response)
848
- return save;
849
- if (action === "verify-upload") {
850
- const file = save.files.find((entry) => entry.path === body.path);
851
- if (!file)
852
- return error("invalid_path", "Path is not in template manifest", 400);
853
- return json({ file });
854
- }
855
- if (action === "mark-uploaded") {
856
- if (!body.path)
857
- return error("invalid_path", "Missing upload path", 400);
858
- if (!save.uploaded.includes(body.path))
859
- save.uploaded.push(body.path);
860
- await this.state.storage.put(`template-save:${name}:${saveId}`, save);
861
- return json({ uploaded: true });
862
- }
863
- const uploaded = new Set(save.uploaded);
864
- const missing = save.files.filter((file) => !uploaded.has(file.path));
865
- if (missing.length > 0) {
866
- return error("template_incomplete", "Template save has missing files", 409);
867
- }
868
- const existing = await this.state.storage.get(`template:${name}`);
869
- const timestamp = now();
870
- const summary = {
871
- name,
872
- revisionId: save.saveId,
873
- createdAt: existing?.createdAt ?? save.createdAt,
874
- updatedAt: timestamp,
875
- fileCount: save.files.length,
876
- byteCount: save.files.reduce((total, file) => total + file.sizeBytes, 0),
877
- };
878
- await this.state.storage.put(`template:${name}`, summary);
879
- await this.state.storage.put(`template-files:${name}`, save.files);
880
- await this.state.storage.delete(`template-save:${name}:${saveId}`);
881
- return json({ ...summary, previousRevisionId: existing?.revisionId });
882
- }
883
- const templateDeleteMatch = url.pathname.match(/^\/templates\/([^/]+)$/);
884
- if (request.method === "DELETE" && templateDeleteMatch) {
885
- const name = decodeURIComponent(templateDeleteMatch[1]);
886
- const saves = await this.state.storage.list({
887
- prefix: `template-save:${name}:`,
888
- });
889
- await this.state.storage.delete([
890
- `template:${name}`,
891
- `template-files:${name}`,
892
- ...saves.keys(),
893
- ]);
894
- return json({ deleted: true });
895
- }
896
- if (request.method === "GET" && url.pathname === "/sites") {
897
- const domain = url.searchParams.get("domain") ?? "";
898
- const records = await this.state.storage.list({ prefix: "site:" });
899
- return json({
900
- sites: [...records.values()].map((site) => ({
901
- ...site,
902
- url: siteUrl(domain, site.slug),
903
- })),
904
- });
905
- }
906
- if (request.method === "POST" && url.pathname === "/sites") {
907
- const body = (await readJson(request));
908
- const key = `site:${body.slug}`;
909
- const existing = await this.state.storage.get(key);
910
- if (existing)
911
- return json(existing);
912
- const timestamp = now();
913
- const site = {
914
- slug: body.slug,
915
- url: siteUrl(body.domain, body.slug),
916
- createdAt: timestamp,
917
- updatedAt: timestamp,
918
- };
919
- await this.state.storage.put(key, site);
920
- return json(site, 201);
921
- }
922
- const deployMatch = url.pathname.match(/^\/sites\/([^/]+)\/deploy$/);
923
- if (request.method === "POST" && deployMatch) {
924
- const slug = decodeURIComponent(deployMatch[1]);
925
- const body = (await readJson(request));
926
- const key = `site:${slug}`;
927
- const existing = await this.state.storage.get(key);
928
- if (!existing)
929
- return error("site_not_found", "Site not found", 404);
930
- const updated = {
931
- ...existing,
932
- updatedAt: now(),
933
- latestDeploymentId: body.latestDeploymentId,
934
- visibility: body.visibility,
935
- };
936
- await this.state.storage.put(key, updated);
937
- return json(updated);
938
- }
939
- const deleteMatch = url.pathname.match(/^\/sites\/([^/]+)$/);
940
- if (request.method === "DELETE" && deleteMatch) {
941
- await this.state.storage.delete(`site:${decodeURIComponent(deleteMatch[1])}`);
942
- return json({ deleted: true });
943
- }
944
- return error("not_found", "Unknown registry route", 404);
945
- }
946
- async requireTemplateSave(name, saveId, token) {
947
- const save = await this.state.storage.get(`template-save:${name}:${saveId}`);
948
- if (!save)
949
- return error("save_not_found", "Template save not found", 404);
950
- if (save.token !== token)
951
- return error("unauthorized", "Invalid template save token", 401);
952
- if (Date.parse(save.expiresAt) < Date.now()) {
953
- return error("save_expired", "Template save session expired", 410);
954
- }
955
- return save;
956
- }
957
- }
958
- export class SiteDO {
959
- state;
960
- constructor(state) {
961
- this.state = state;
962
- }
963
- async fetch(request) {
964
- const url = new URL(request.url);
965
- if (request.method === "POST" && url.pathname === "/delete") {
966
- await this.state.storage.deleteAll();
967
- return json({ deleted: true });
968
- }
969
- if (request.method === "GET" && url.pathname === "/active") {
970
- const site = await this.state.storage.get("site");
971
- if (!site?.activeDeploymentId)
972
- return error("deployment_not_found", "No active deployment", 404);
973
- const deployment = await this.state.storage.get(`deployment:${site.activeDeploymentId}`);
974
- if (!deployment)
975
- return error("deployment_not_found", "Active deployment missing", 404);
976
- return json({
977
- deploymentId: deployment.deploymentId,
978
- public: deployment.public,
979
- files: deployment.files,
980
- });
981
- }
982
- if (request.method === "POST" && url.pathname === "/deploy/start") {
983
- const body = (await readJson(request));
984
- const timestamp = now();
985
- const nowMs = Date.now();
986
- const deployments = await this.state.storage.list({
987
- prefix: "deployment:",
988
- });
989
- let pendingDeployments = 0;
990
- const expiredPendingKeys = [];
991
- for (const [key, deployment] of deployments.entries()) {
992
- if (deployment.status !== "pending")
993
- continue;
994
- if (Date.parse(deployment.expiresAt) < nowMs) {
995
- expiredPendingKeys.push(key);
996
- }
997
- else {
998
- pendingDeployments += 1;
999
- }
1000
- }
1001
- if (expiredPendingKeys.length > 0)
1002
- await this.state.storage.delete(expiredPendingKeys);
1003
- if (pendingDeployments >= MAX_PENDING_DEPLOYMENTS) {
1004
- return error("quota_exceeded", "Too many pending deployments for this site", 429);
1005
- }
1006
- const deployment = {
1007
- deploymentId: randomId("dep"),
1008
- status: "pending",
1009
- public: body.public,
1010
- createdAt: timestamp,
1011
- token: randomId("tok"),
1012
- expiresAt: new Date(Date.now() + DEPLOY_SESSION_MS).toISOString(),
1013
- files: body.files,
1014
- uploaded: [],
1015
- };
1016
- const existingSite = await this.state.storage.get("site");
1017
- await this.state.storage.put("site", {
1018
- createdAt: existingSite?.createdAt ?? timestamp,
1019
- updatedAt: timestamp,
1020
- activeDeploymentId: existingSite?.activeDeploymentId,
1021
- activePublic: existingSite?.activePublic,
1022
- });
1023
- await this.state.storage.put(`deployment:${deployment.deploymentId}`, deployment);
1024
- return json({
1025
- deploymentId: deployment.deploymentId,
1026
- upload: deployment.files.map((file) => file.path),
1027
- token: deployment.token,
1028
- });
1029
- }
1030
- if (request.method === "POST" && url.pathname === "/deploy/verify-upload") {
1031
- const body = (await readJson(request));
1032
- const deploymentResult = await this.requirePendingDeployment(body.deploymentId, body.token);
1033
- if (deploymentResult instanceof Response)
1034
- return deploymentResult;
1035
- const deployment = deploymentResult;
1036
- const file = deployment.files.find((entry) => entry.path === body.path);
1037
- if (!file)
1038
- return error("invalid_path", "Path is not in deployment manifest", 400);
1039
- return json({ file });
1040
- }
1041
- if (request.method === "POST" && url.pathname === "/deploy/mark-uploaded") {
1042
- const body = (await readJson(request));
1043
- const deploymentResult = await this.requirePendingDeployment(body.deploymentId, body.token);
1044
- if (deploymentResult instanceof Response)
1045
- return deploymentResult;
1046
- const deployment = deploymentResult;
1047
- if (!deployment.uploaded.includes(body.path))
1048
- deployment.uploaded.push(body.path);
1049
- await this.state.storage.put(`deployment:${deployment.deploymentId}`, deployment);
1050
- return json({ uploaded: true });
1051
- }
1052
- if (request.method === "POST" && url.pathname === "/deploy/finish") {
1053
- const body = (await readJson(request));
1054
- const deploymentResult = await this.requirePendingDeployment(body.deploymentId, body.token);
1055
- if (deploymentResult instanceof Response)
1056
- return deploymentResult;
1057
- const deployment = deploymentResult;
1058
- const uploaded = new Set(deployment.uploaded);
1059
- const missing = deployment.files.filter((file) => !uploaded.has(file.path));
1060
- if (missing.length > 0)
1061
- return error("deployment_incomplete", "Deployment has missing files", 409);
1062
- const previous = await this.state.storage.get("site");
1063
- const timestamp = now();
1064
- deployment.status = "active";
1065
- deployment.finishedAt = timestamp;
1066
- await this.state.storage.put(`deployment:${deployment.deploymentId}`, deployment);
1067
- await this.state.storage.put("site", {
1068
- createdAt: previous?.createdAt ?? deployment.createdAt,
1069
- updatedAt: timestamp,
1070
- activeDeploymentId: deployment.deploymentId,
1071
- activePublic: deployment.public,
1072
- });
1073
- return json({
1074
- visibility: deployment.public ? "public" : "private",
1075
- deploymentId: deployment.deploymentId,
1076
- fileCount: deployment.files.length,
1077
- byteCount: deployment.files.reduce((total, file) => total + file.sizeBytes, 0),
1078
- previousDeploymentId: previous?.activeDeploymentId,
1079
- });
1080
- }
1081
- if (url.pathname === "/kv" || url.pathname.startsWith("/kv/")) {
1082
- return await this.handleKv(request, url);
1083
- }
1084
- return error("not_found", "Unknown site route", 404);
1085
- }
1086
- async requirePendingDeployment(deploymentId, token) {
1087
- const deployment = await this.state.storage.get(`deployment:${deploymentId}`);
1088
- if (!deployment)
1089
- return error("deployment_not_found", "Deployment not found", 404);
1090
- if (deployment.status !== "pending") {
1091
- return error("deployment_not_found", "Deployment is not pending", 409);
1092
- }
1093
- if (deployment.token !== token)
1094
- return error("unauthorized", "Invalid deploy token", 401);
1095
- if (Date.parse(deployment.expiresAt) < Date.now()) {
1096
- return error("deployment_expired", "Deployment session expired", 410);
1097
- }
1098
- return deployment;
1099
- }
1100
- async handleKv(request, url) {
1101
- try {
1102
- const key = decodeURIComponent(url.pathname.slice("/kv/".length));
1103
- if (request.method === "GET" && url.pathname === "/kv") {
1104
- const prefix = url.searchParams.get("prefix") ?? "";
1105
- const records = await this.state.storage.list({ prefix: "kv:" });
1106
- const keys = [...records.entries()]
1107
- .map(([storageKey, record]) => ({
1108
- key: storageKey.slice("kv:".length),
1109
- sizeBytes: record.sizeBytes,
1110
- updatedAt: record.updatedAt,
1111
- }))
1112
- .filter((entry) => entry.key.startsWith(prefix));
1113
- return json({ keys });
1114
- }
1115
- if (!key || key.length > MAX_KV_KEY_LENGTH)
1116
- return error("invalid_key", "Invalid KV key", 400);
1117
- const storageKey = `kv:${key}`;
1118
- if (request.method === "GET") {
1119
- const record = await this.state.storage.get(storageKey);
1120
- return json({ value: record?.value ?? null });
1121
- }
1122
- if (request.method === "DELETE") {
1123
- const deleted = await this.state.storage.delete(storageKey);
1124
- return json({ key, deleted });
1125
- }
1126
- if (request.method === "PUT") {
1127
- const value = await readJson(request);
1128
- const serialized = JSON.stringify(value);
1129
- const sizeBytes = new TextEncoder().encode(serialized).length;
1130
- if (sizeBytes > MAX_KV_VALUE_BYTES)
1131
- return error("quota_exceeded", "KV value is too large", 413);
1132
- const existing = await this.state.storage.get(storageKey);
1133
- const usage = await this.kvUsage();
1134
- const nextKeys = existing ? usage.keyCount : usage.keyCount + 1;
1135
- const nextBytes = usage.bytesUsed - (existing?.sizeBytes ?? 0) + sizeBytes;
1136
- if (nextKeys > MAX_KV_KEYS || nextBytes > MAX_KV_TOTAL_BYTES) {
1137
- return error("quota_exceeded", "KV quota exceeded", 413);
1138
- }
1139
- await this.state.storage.put(storageKey, {
1140
- value,
1141
- sizeBytes,
1142
- updatedAt: now(),
1143
- updatedBy: request.headers.get("x-nook-actor") ?? "unknown",
1144
- });
1145
- return json({ key });
1146
- }
1147
- return error("method_not_allowed", "Method not allowed", 405);
1148
- }
1149
- catch (err) {
1150
- if (err instanceof ErrorResponse)
1151
- return err.toResponse();
1152
- return error("internal_error", err instanceof Error ? err.message : String(err), 500);
1153
- }
1154
- }
1155
- async kvUsage() {
1156
- const records = await this.state.storage.list({ prefix: "kv:" });
1157
- let bytesUsed = 0;
1158
- for (const record of records.values())
1159
- bytesUsed += record.sizeBytes;
1160
- return { keyCount: records.size, bytesUsed };
1161
- }
1162
- }
1163
- class ErrorResponse extends Error {
1164
- code;
1165
- status;
1166
- constructor(code, message, status) {
1167
- super(message);
1168
- this.code = code;
1169
- this.status = status;
1170
- }
1171
- toResponse() {
1172
- return error(this.code, this.message, this.status);
1173
- }
1174
- }
1175
- //# sourceMappingURL=index.js.map