@chat-de-hp/site 0.1.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 (55) hide show
  1. package/README.md +50 -0
  2. package/bin/chat-de-hp.js +11 -0
  3. package/dist/astro.d.ts +12 -0
  4. package/dist/astro.d.ts.map +1 -0
  5. package/dist/astro.js +77 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +36 -0
  9. package/dist/config.d.ts +6 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +40 -0
  12. package/dist/contracts/forms.d.ts +135 -0
  13. package/dist/contracts/forms.d.ts.map +1 -0
  14. package/dist/contracts/forms.js +89 -0
  15. package/dist/control-plane.d.ts +51 -0
  16. package/dist/control-plane.d.ts.map +1 -0
  17. package/dist/control-plane.js +50 -0
  18. package/dist/generator.d.ts +17 -0
  19. package/dist/generator.d.ts.map +1 -0
  20. package/dist/generator.js +207 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/internal/astro-plugins.d.ts +9 -0
  25. package/dist/internal/astro-plugins.d.ts.map +1 -0
  26. package/dist/internal/astro-plugins.js +22 -0
  27. package/dist/internal/forms-plugin.d.ts +3 -0
  28. package/dist/internal/forms-plugin.d.ts.map +1 -0
  29. package/dist/internal/forms-plugin.js +29 -0
  30. package/dist/internal/primitive-names.d.ts +3 -0
  31. package/dist/internal/primitive-names.d.ts.map +1 -0
  32. package/dist/internal/primitive-names.js +5 -0
  33. package/dist/internal/registry.d.ts +32 -0
  34. package/dist/internal/registry.d.ts.map +1 -0
  35. package/dist/internal/registry.js +88 -0
  36. package/dist/primitives/forms-astro.d.ts +2 -0
  37. package/dist/primitives/forms-astro.d.ts.map +1 -0
  38. package/dist/primitives/forms-astro.js +1 -0
  39. package/dist/primitives/forms-styles.css +1 -0
  40. package/dist/primitives/forms.d.ts +5 -0
  41. package/dist/primitives/forms.d.ts.map +1 -0
  42. package/dist/primitives/forms.js +4 -0
  43. package/dist/runtime/contracts.d.ts +60 -0
  44. package/dist/runtime/contracts.d.ts.map +1 -0
  45. package/dist/runtime/contracts.js +1 -0
  46. package/dist/runtime/index.d.ts +4 -0
  47. package/dist/runtime/index.d.ts.map +1 -0
  48. package/dist/runtime/index.js +2 -0
  49. package/dist/runtime/worker-core.d.ts +7 -0
  50. package/dist/runtime/worker-core.d.ts.map +1 -0
  51. package/dist/runtime/worker-core.js +619 -0
  52. package/dist/runtime/worker.d.ts +5 -0
  53. package/dist/runtime/worker.d.ts.map +1 -0
  54. package/dist/runtime/worker.js +5 -0
  55. package/package.json +102 -0
@@ -0,0 +1,619 @@
1
+ const CHAT_DE_HP_BOOTSTRAP_PATH = "/__chat_de_hp/bootstrap";
2
+ const CHAT_DE_HP_ADMIN_LOGIN_LINK_PATH = "/__chat_de_hp/admin-login-link";
3
+ const CHAT_DE_HP_MEDIA_PATH = "/__chat_de_hp/media";
4
+ const CHAT_DE_HP_AGENT_EMAIL_DOMAIN = "chat-de-hp.internal";
5
+ const CHAT_DE_HP_AGENT_TOKEN_NAME = "Chat de HP Agent";
6
+ const CHAT_DE_HP_API_TOKEN_SCOPES = ["admin"];
7
+ const ADMIN_LOGIN_LINK_EXPIRES_MS = 15 * 60 * 1000;
8
+ const DEFAULT_FORM_EMAIL_FROM = "forms@chat-de-hp.com";
9
+ const MAX_MEDIA_BYTES = 8 * 1024 * 1024;
10
+ const SUPPORTED_MEDIA_TYPES = new Set([
11
+ "image/jpeg",
12
+ "image/png",
13
+ "image/webp",
14
+ ]);
15
+ function isCmsBootstrapPath(pathname) {
16
+ return (pathname === CHAT_DE_HP_BOOTSTRAP_PATH ||
17
+ pathname === CHAT_DE_HP_BOOTSTRAP_PATH + "/");
18
+ }
19
+ function isAdminLoginLinkPath(pathname) {
20
+ return (pathname === CHAT_DE_HP_ADMIN_LOGIN_LINK_PATH ||
21
+ pathname === CHAT_DE_HP_ADMIN_LOGIN_LINK_PATH + "/");
22
+ }
23
+ function isChatDeHpMediaPath(pathname) {
24
+ return (pathname === CHAT_DE_HP_MEDIA_PATH ||
25
+ pathname === CHAT_DE_HP_MEDIA_PATH + "/" ||
26
+ pathname.startsWith(CHAT_DE_HP_MEDIA_PATH + "/"));
27
+ }
28
+ function contentTypeForAssetPath(pathname) {
29
+ if (pathname.endsWith(".css")) {
30
+ return "text/css; charset=utf-8";
31
+ }
32
+ if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) {
33
+ return "application/javascript; charset=utf-8";
34
+ }
35
+ if (pathname.endsWith(".json")) {
36
+ return "application/json; charset=utf-8";
37
+ }
38
+ if (pathname.endsWith(".svg")) {
39
+ return "image/svg+xml";
40
+ }
41
+ if (pathname.endsWith(".wasm")) {
42
+ return "application/wasm";
43
+ }
44
+ if (pathname.endsWith(".woff2")) {
45
+ return "font/woff2";
46
+ }
47
+ if (pathname.endsWith(".woff")) {
48
+ return "font/woff";
49
+ }
50
+ if (pathname.endsWith(".png")) {
51
+ return "image/png";
52
+ }
53
+ if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) {
54
+ return "image/jpeg";
55
+ }
56
+ if (pathname.endsWith(".webp")) {
57
+ return "image/webp";
58
+ }
59
+ if (pathname.endsWith(".gif")) {
60
+ return "image/gif";
61
+ }
62
+ if (pathname.endsWith(".ico")) {
63
+ return "image/x-icon";
64
+ }
65
+ return "application/octet-stream";
66
+ }
67
+ function hasStaticAssetExtension(pathname) {
68
+ return /\.[a-z0-9][a-z0-9-]*$/iu.test(pathname);
69
+ }
70
+ function withAssetContentType(response, pathname) {
71
+ const headers = new Headers(response.headers);
72
+ if (!headers.get("content-type")) {
73
+ headers.set("content-type", contentTypeForAssetPath(pathname));
74
+ }
75
+ return new Response(response.body, {
76
+ headers,
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ });
80
+ }
81
+ async function handleCmsBootstrap(request, env, context, astroHandler) {
82
+ if (request.method !== "POST") {
83
+ return jsonResponse({ error: "Method not allowed.", success: false }, 405);
84
+ }
85
+ if (!env.CHAT_DE_HP_BOOTSTRAP_SECRET ||
86
+ request.headers.get("x-chat-de-hp-bootstrap-secret") !==
87
+ env.CHAT_DE_HP_BOOTSTRAP_SECRET) {
88
+ return jsonResponse({ error: "Unauthorized.", success: false }, 401);
89
+ }
90
+ if (!env.DB) {
91
+ return jsonResponse({ error: "D1 DB binding is missing.", success: false }, 500);
92
+ }
93
+ const body = await readCmsBootstrapBody(request);
94
+ const siteName = body.siteName ?? env.SITE_NAME ?? "Chat de HP Site";
95
+ const alreadySetUp = (await getOption(env.DB, "emdash:setup_complete")) === true;
96
+ const setup = await ensureEmDashSetup(request, env, context, siteName, astroHandler);
97
+ if (!setup.ok) {
98
+ return jsonResponse({ error: setup.error, success: false }, 500);
99
+ }
100
+ const tablesReady = await requiredEmDashTablesExist(env.DB);
101
+ if (!tablesReady) {
102
+ return jsonResponse({
103
+ error: "EmDash setup did not create the required tables.",
104
+ success: false,
105
+ }, 500);
106
+ }
107
+ const adminUserId = await upsertChatDeHpAgentUser(env.DB, env);
108
+ await finalizeChatDeHpSetup(env.DB, request, env, siteName, alreadySetUp);
109
+ const token = body.createToken
110
+ ? await createChatDeHpApiToken(env.DB, adminUserId)
111
+ : null;
112
+ return jsonResponse({
113
+ adminUserId,
114
+ scopes: CHAT_DE_HP_API_TOKEN_SCOPES,
115
+ success: true,
116
+ token: token?.raw ?? null,
117
+ tokenCreated: Boolean(token),
118
+ tokenPrefix: token?.prefix ?? null,
119
+ });
120
+ }
121
+ async function handleChatDeHpMedia(request, env) {
122
+ const url = new URL(request.url);
123
+ if (request.method === "GET") {
124
+ return serveChatDeHpMedia(url, env);
125
+ }
126
+ if (request.method !== "POST") {
127
+ return jsonResponse({ error: "Method not allowed.", success: false }, 405);
128
+ }
129
+ if (!env.CHAT_DE_HP_BOOTSTRAP_SECRET ||
130
+ request.headers.get("x-chat-de-hp-bootstrap-secret") !==
131
+ env.CHAT_DE_HP_BOOTSTRAP_SECRET) {
132
+ return jsonResponse({ error: "Unauthorized.", success: false }, 401);
133
+ }
134
+ if (!env.MEDIA) {
135
+ return jsonResponse({ error: "MEDIA binding is missing.", success: false }, 500);
136
+ }
137
+ if (!env.DB) {
138
+ return jsonResponse({ error: "D1 DB binding is missing.", success: false }, 500);
139
+ }
140
+ if (!(await tableExists(env.DB, "media"))) {
141
+ return jsonResponse({ error: "EmDash media table is missing.", success: false }, 500);
142
+ }
143
+ const formData = await request.formData();
144
+ const file = formData.get("file");
145
+ if (!(file instanceof File)) {
146
+ return jsonResponse({ error: "File is required.", success: false }, 400);
147
+ }
148
+ if (!SUPPORTED_MEDIA_TYPES.has(file.type)) {
149
+ return jsonResponse({ error: "Unsupported media type.", success: false }, 415);
150
+ }
151
+ if (file.size <= 0 || file.size > MAX_MEDIA_BYTES) {
152
+ return jsonResponse({ error: "File size is not allowed.", success: false }, 413);
153
+ }
154
+ const extension = extensionForMediaType(file.type);
155
+ const filename = sanitizeFilename(file.name || "image" + extension);
156
+ const key = "chat-de-hp/uploads/" +
157
+ new Date().toISOString().slice(0, 10) +
158
+ "/" +
159
+ crypto.randomUUID() +
160
+ "-" +
161
+ filename;
162
+ const bytes = await file.arrayBuffer();
163
+ await env.MEDIA.put(key, bytes, {
164
+ customMetadata: {
165
+ originalFilename: filename,
166
+ siteId: env.SITE_ID ?? "",
167
+ },
168
+ httpMetadata: {
169
+ contentDisposition: 'inline; filename="' + filename + '"',
170
+ contentType: file.type,
171
+ },
172
+ });
173
+ let media;
174
+ try {
175
+ media = await registerChatDeHpMedia(env.DB, env, {
176
+ contentHash: await sha256BytesBase64Url(new Uint8Array(bytes)),
177
+ filename,
178
+ height: positiveIntegerFormValue(formData, "height"),
179
+ mimeType: file.type,
180
+ size: file.size,
181
+ storageKey: key,
182
+ width: positiveIntegerFormValue(formData, "width"),
183
+ });
184
+ }
185
+ catch (error) {
186
+ await env.MEDIA.delete(key);
187
+ throw error;
188
+ }
189
+ const publicUrl = new URL(CHAT_DE_HP_MEDIA_PATH + "/" + encodeMediaKey(key), url.origin);
190
+ return jsonResponse({
191
+ emdashMediaId: media.id,
192
+ filename,
193
+ key,
194
+ mediaId: media.id,
195
+ mimeType: file.type,
196
+ size: file.size,
197
+ storageKey: key,
198
+ success: true,
199
+ url: publicUrl.toString(),
200
+ width: media.width,
201
+ height: media.height,
202
+ });
203
+ }
204
+ async function handleAdminLoginLink(request, env) {
205
+ if (request.method !== "POST") {
206
+ return jsonResponse({ error: "Method not allowed.", success: false }, 405);
207
+ }
208
+ if (!env.CHAT_DE_HP_BOOTSTRAP_SECRET ||
209
+ request.headers.get("x-chat-de-hp-bootstrap-secret") !==
210
+ env.CHAT_DE_HP_BOOTSTRAP_SECRET) {
211
+ return jsonResponse({ error: "Unauthorized.", success: false }, 401);
212
+ }
213
+ if (!env.DB) {
214
+ return jsonResponse({ error: "D1 DB binding is missing.", success: false }, 500);
215
+ }
216
+ if (!(await tableExists(env.DB, "auth_tokens"))) {
217
+ return jsonResponse({ error: "EmDash auth tables are missing.", success: false }, 500);
218
+ }
219
+ const adminUserId = await upsertChatDeHpAgentUser(env.DB, env);
220
+ const adminUser = chatDeHpAgentUser(env);
221
+ const body = await readAdminLoginLinkBody(request);
222
+ const token = randomBase64Url(32);
223
+ const hash = await hashMagicLinkToken(token);
224
+ const now = new Date();
225
+ const expiresAt = new Date(now.getTime() + ADMIN_LOGIN_LINK_EXPIRES_MS).toISOString();
226
+ await env.DB.prepare("DELETE FROM auth_tokens WHERE user_id = ? AND type = ?")
227
+ .bind(adminUserId, "magic_link")
228
+ .run();
229
+ await env.DB.prepare("INSERT INTO auth_tokens " +
230
+ "(hash, user_id, email, type, role, invited_by, expires_at, created_at) " +
231
+ "VALUES (?, ?, ?, 'magic_link', NULL, NULL, ?, ?)")
232
+ .bind(hash, adminUserId, adminUser.email, expiresAt, now.toISOString())
233
+ .run();
234
+ const loginUrl = new URL("/_emdash/api/auth/magic-link/verify", new URL(request.url).origin);
235
+ loginUrl.searchParams.set("token", token);
236
+ loginUrl.searchParams.set("redirect", body.redirect);
237
+ return jsonResponse({
238
+ adminUserId,
239
+ expiresAt,
240
+ loginUrl: loginUrl.toString(),
241
+ redirect: body.redirect,
242
+ success: true,
243
+ });
244
+ }
245
+ async function serveChatDeHpMedia(url, env) {
246
+ if (!env.MEDIA) {
247
+ return new Response("MEDIA binding is missing.", { status: 500 });
248
+ }
249
+ const prefix = CHAT_DE_HP_MEDIA_PATH + "/";
250
+ if (!url.pathname.startsWith(prefix)) {
251
+ return new Response("Not found.", { status: 404 });
252
+ }
253
+ const key = decodeMediaKey(url.pathname.slice(prefix.length));
254
+ if (!key.startsWith("chat-de-hp/uploads/")) {
255
+ return new Response("Not found.", { status: 404 });
256
+ }
257
+ const object = await env.MEDIA.get(key);
258
+ if (!object) {
259
+ return new Response("Not found.", { status: 404 });
260
+ }
261
+ return new Response(object.body, {
262
+ headers: {
263
+ "cache-control": "public, max-age=31536000, immutable",
264
+ "content-disposition": object.httpMetadata?.contentDisposition ?? "inline",
265
+ "content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
266
+ },
267
+ });
268
+ }
269
+ async function ensureEmDashSetup(request, env, context, siteName, astroHandler) {
270
+ if (!env.DB) {
271
+ return { error: "D1 DB binding is missing.", ok: false };
272
+ }
273
+ if ((await getOption(env.DB, "emdash:setup_complete")) === true) {
274
+ return { ok: true };
275
+ }
276
+ const setupUrl = new URL("/_emdash/api/setup", request.url);
277
+ const setupRequest = new Request(setupUrl, {
278
+ body: JSON.stringify({
279
+ includeContent: true,
280
+ tagline: "",
281
+ title: siteName,
282
+ }),
283
+ headers: {
284
+ "content-type": "application/json",
285
+ },
286
+ method: "POST",
287
+ });
288
+ const response = await astroHandler.fetch(setupRequest, env, context);
289
+ if (response.ok || response.status === 409) {
290
+ return { ok: true };
291
+ }
292
+ const responseText = await response.text();
293
+ return {
294
+ error: "EmDash setup failed. HTTP " +
295
+ response.status +
296
+ ": " +
297
+ responseText.slice(0, 500),
298
+ ok: false,
299
+ };
300
+ }
301
+ async function requiredEmDashTablesExist(db) {
302
+ return ((await tableExists(db, "options")) &&
303
+ (await tableExists(db, "users")) &&
304
+ (await tableExists(db, "_emdash_api_tokens")));
305
+ }
306
+ async function tableExists(db, tableName) {
307
+ const row = await db
308
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
309
+ .bind(tableName)
310
+ .first();
311
+ return Boolean(row);
312
+ }
313
+ async function upsertChatDeHpAgentUser(db, env) {
314
+ const now = new Date().toISOString();
315
+ const user = chatDeHpAgentUser(env);
316
+ await db
317
+ .prepare("INSERT INTO users (id, email, name, role, email_verified, disabled, data, created_at, updated_at) " +
318
+ "VALUES (?, ?, ?, 50, 1, 0, '{}', ?, ?) " +
319
+ "ON CONFLICT(email) DO UPDATE SET " +
320
+ "name = excluded.name, " +
321
+ "role = 50, " +
322
+ "email_verified = 1, " +
323
+ "disabled = 0, " +
324
+ "updated_at = excluded.updated_at")
325
+ .bind(user.id, user.email, "Chat de HP Agent", now, now)
326
+ .run();
327
+ const row = await db
328
+ .prepare("SELECT id FROM users WHERE email = ?")
329
+ .bind(user.email)
330
+ .first();
331
+ if (!row?.id) {
332
+ throw new Error("Failed to create the Chat de HP agent user.");
333
+ }
334
+ return row.id;
335
+ }
336
+ async function registerChatDeHpMedia(db, env, input) {
337
+ const now = new Date().toISOString();
338
+ const id = createMediaId();
339
+ const author = chatDeHpAgentUser(env);
340
+ await db
341
+ .prepare("INSERT INTO media " +
342
+ "(id, filename, mime_type, size, width, height, alt, caption, storage_key, content_hash, blurhash, dominant_color, status, created_at, author_id) " +
343
+ "VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, NULL, NULL, 'ready', ?, ?)")
344
+ .bind(id, input.filename, input.mimeType, input.size, input.width, input.height, input.storageKey, input.contentHash, now, author.id)
345
+ .run();
346
+ return {
347
+ height: input.height,
348
+ id,
349
+ width: input.width,
350
+ };
351
+ }
352
+ function chatDeHpAgentUser(env) {
353
+ const siteId = env.SITE_ID ?? "unknown-site";
354
+ const stableSiteId = stableIdentifier(siteId);
355
+ return {
356
+ email: "chat-de-hp-agent+" + stableSiteId + "@" + CHAT_DE_HP_AGENT_EMAIL_DOMAIN,
357
+ id: "chat_de_hp_agent_" + stableSiteId,
358
+ };
359
+ }
360
+ function createMediaId() {
361
+ return "chatdehp_" + crypto.randomUUID().replaceAll("-", "");
362
+ }
363
+ function positiveIntegerFormValue(formData, name) {
364
+ const value = formData.get(name);
365
+ if (typeof value !== "string" || value.trim() === "") {
366
+ return null;
367
+ }
368
+ const number = Number.parseInt(value, 10);
369
+ return Number.isInteger(number) && number > 0 ? number : null;
370
+ }
371
+ async function finalizeChatDeHpSetup(db, request, env, siteName, alreadySetUp) {
372
+ // First bootstrap: the name entered on the platform wins over any
373
+ // seed-provided default title.
374
+ await (alreadySetUp
375
+ ? setOptionIfAbsent(db, "emdash:site_title", siteName)
376
+ : setOption(db, "emdash:site_title", siteName));
377
+ await setOptionIfAbsent(db, "emdash:site_url", env.PUBLIC_URL ?? new URL(request.url).origin);
378
+ await setOptionIfAbsent(db, "plugin:cloudflare-email:settings:from", env.FORM_EMAIL_FROM ?? DEFAULT_FORM_EMAIL_FROM);
379
+ await setOption(db, "emdash:setup_complete", true);
380
+ await deleteOption(db, "emdash:setup_state");
381
+ }
382
+ async function createChatDeHpApiToken(db, userId) {
383
+ const raw = "ec_pat_" + randomBase64Url(32);
384
+ const hash = await sha256Base64Url(raw);
385
+ const prefix = raw.slice(0, "ec_pat_".length + 4);
386
+ await db
387
+ .prepare("DELETE FROM _emdash_api_tokens WHERE user_id = ? AND name = ?")
388
+ .bind(userId, CHAT_DE_HP_AGENT_TOKEN_NAME)
389
+ .run();
390
+ await db
391
+ .prepare("INSERT INTO _emdash_api_tokens " +
392
+ "(id, name, token_hash, prefix, user_id, scopes, expires_at, last_used_at) " +
393
+ "VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)")
394
+ .bind(crypto.randomUUID(), CHAT_DE_HP_AGENT_TOKEN_NAME, hash, prefix, userId, JSON.stringify(CHAT_DE_HP_API_TOKEN_SCOPES))
395
+ .run();
396
+ return { prefix, raw };
397
+ }
398
+ async function getOption(db, name) {
399
+ if (!(await tableExists(db, "options"))) {
400
+ return null;
401
+ }
402
+ const row = await db
403
+ .prepare("SELECT value FROM options WHERE name = ?")
404
+ .bind(name)
405
+ .first();
406
+ if (!row) {
407
+ return null;
408
+ }
409
+ try {
410
+ return JSON.parse(row.value);
411
+ }
412
+ catch {
413
+ return null;
414
+ }
415
+ }
416
+ async function setOption(db, name, value) {
417
+ await db
418
+ .prepare("INSERT INTO options (name, value) VALUES (?, ?) " +
419
+ "ON CONFLICT(name) DO UPDATE SET value = excluded.value")
420
+ .bind(name, JSON.stringify(value))
421
+ .run();
422
+ }
423
+ async function setOptionIfAbsent(db, name, value) {
424
+ await db
425
+ .prepare("INSERT INTO options (name, value) VALUES (?, ?) " +
426
+ "ON CONFLICT(name) DO NOTHING")
427
+ .bind(name, JSON.stringify(value))
428
+ .run();
429
+ }
430
+ async function deleteOption(db, name) {
431
+ await db.prepare("DELETE FROM options WHERE name = ?").bind(name).run();
432
+ }
433
+ async function readCmsBootstrapBody(request) {
434
+ try {
435
+ const value = (await request.json());
436
+ if (!value || typeof value !== "object") {
437
+ return { createToken: true, siteName: null };
438
+ }
439
+ const candidate = value;
440
+ return {
441
+ createToken: candidate.createToken !== false,
442
+ siteName: typeof candidate.siteName === "string" && candidate.siteName.trim()
443
+ ? candidate.siteName.trim()
444
+ : null,
445
+ };
446
+ }
447
+ catch {
448
+ return { createToken: true, siteName: null };
449
+ }
450
+ }
451
+ async function readAdminLoginLinkBody(request) {
452
+ try {
453
+ const value = (await request.json());
454
+ const candidate = value && typeof value === "object"
455
+ ? value
456
+ : {};
457
+ const redirect = typeof candidate.redirect === "string" &&
458
+ isSafeLocalRedirect(candidate.redirect)
459
+ ? candidate.redirect
460
+ : "/_emdash/admin";
461
+ return { redirect };
462
+ }
463
+ catch {
464
+ return {
465
+ redirect: "/_emdash/admin",
466
+ };
467
+ }
468
+ }
469
+ function isSafeLocalRedirect(value) {
470
+ return (value.startsWith("/") && !value.startsWith("//") && !value.includes("\\"));
471
+ }
472
+ async function sha256Base64Url(value) {
473
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
474
+ return bytesToBase64Url(new Uint8Array(digest));
475
+ }
476
+ async function sha256BytesBase64Url(value) {
477
+ const bytes = new Uint8Array(value.byteLength);
478
+ bytes.set(value);
479
+ const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
480
+ return bytesToBase64Url(new Uint8Array(digest));
481
+ }
482
+ async function hashMagicLinkToken(token) {
483
+ return sha256BytesBase64Url(base64UrlToBytes(token));
484
+ }
485
+ function randomBase64Url(byteLength) {
486
+ const bytes = new Uint8Array(byteLength);
487
+ crypto.getRandomValues(bytes);
488
+ return bytesToBase64Url(bytes);
489
+ }
490
+ function bytesToBase64Url(bytes) {
491
+ let binary = "";
492
+ const chunkSize = 0x80_00;
493
+ for (let index = 0; index < bytes.length; index += chunkSize) {
494
+ binary += String.fromCodePoint(...bytes.subarray(index, index + chunkSize));
495
+ }
496
+ return btoa(binary)
497
+ .replaceAll("+", "-")
498
+ .replaceAll("/", "_")
499
+ .replaceAll("=", "");
500
+ }
501
+ function base64UrlToBytes(value) {
502
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
503
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
504
+ const binary = atob(padded);
505
+ const bytes = new Uint8Array(binary.length);
506
+ for (let index = 0; index < binary.length; index += 1) {
507
+ bytes[index] = binary.codePointAt(index) ?? 0;
508
+ }
509
+ return bytes;
510
+ }
511
+ function stableIdentifier(value) {
512
+ return value.toLowerCase().replaceAll(/[^a-z0-9_-]/gu, "_");
513
+ }
514
+ function jsonResponse(body, status = 200) {
515
+ // oxlint-disable-next-line unicorn/prefer-response-static-json -- preserve the established charset-bearing content type
516
+ return new Response(JSON.stringify(body), {
517
+ headers: {
518
+ "cache-control": "no-store",
519
+ "content-type": "application/json; charset=utf-8",
520
+ },
521
+ status,
522
+ });
523
+ }
524
+ function extensionForMediaType(mediaType) {
525
+ switch (mediaType) {
526
+ case "image/jpeg": {
527
+ return ".jpg";
528
+ }
529
+ case "image/png": {
530
+ return ".png";
531
+ }
532
+ case "image/webp": {
533
+ return ".webp";
534
+ }
535
+ default: {
536
+ return "";
537
+ }
538
+ }
539
+ }
540
+ function sanitizeFilename(value) {
541
+ const sanitized = value
542
+ .trim()
543
+ .replaceAll(/[^a-z0-9._-]+/giu, "-")
544
+ .replaceAll(/^-+|-+$/gu, "")
545
+ .slice(0, 96);
546
+ return sanitized || "image";
547
+ }
548
+ function encodeMediaKey(key) {
549
+ return key.split("/").map(encodeURIComponent).join("/");
550
+ }
551
+ function decodeMediaKey(key) {
552
+ return key.split("/").map(decodeURIComponent).join("/");
553
+ }
554
+ export function createChatDeHpSiteWorkerWithHandler(runtime, astroHandler) {
555
+ const routes = validateRuntime(runtime);
556
+ return {
557
+ async fetch(request, env, context) {
558
+ const url = new URL(request.url);
559
+ if (isCmsBootstrapPath(url.pathname)) {
560
+ return handleCmsBootstrap(request, env, context, astroHandler);
561
+ }
562
+ if (isAdminLoginLinkPath(url.pathname)) {
563
+ return handleAdminLoginLink(request, env);
564
+ }
565
+ if (isChatDeHpMediaPath(url.pathname)) {
566
+ return handleChatDeHpMedia(request, env);
567
+ }
568
+ const primitiveRoute = routes.get(runtimeRouteKey(request.method, url.pathname)) ??
569
+ routes.get(runtimeRouteKey("*", url.pathname));
570
+ if (primitiveRoute) {
571
+ return primitiveRoute.handle(request, env, context);
572
+ }
573
+ if (env.ASSETS && hasStaticAssetExtension(url.pathname)) {
574
+ const assetResponse = await env.ASSETS.fetch(request);
575
+ if (assetResponse.status !== 404) {
576
+ return withAssetContentType(assetResponse, url.pathname);
577
+ }
578
+ }
579
+ return astroHandler.fetch(request, env, context);
580
+ },
581
+ };
582
+ }
583
+ function validateRuntime(runtime) {
584
+ if (!runtime || !Array.isArray(runtime.primitives)) {
585
+ throw new Error("Generated Chat de HP runtime is malformed.");
586
+ }
587
+ const primitiveIds = new Set();
588
+ const routes = new Map();
589
+ for (const primitive of runtime.primitives) {
590
+ if (!/^[a-z][a-z0-9-]*$/u.test(primitive.id)) {
591
+ throw new Error(`Runtime primitive id ${JSON.stringify(primitive.id)} is malformed.`);
592
+ }
593
+ if (primitiveIds.has(primitive.id)) {
594
+ throw new Error(`Runtime primitive ${primitive.id} is registered more than once.`);
595
+ }
596
+ primitiveIds.add(primitive.id);
597
+ for (const route of primitive.routes) {
598
+ validateRuntimeRoute(primitive.id, route);
599
+ const key = runtimeRouteKey(route.method ?? "*", route.pathname);
600
+ if (routes.has(key)) {
601
+ throw new Error(`Runtime route ${route.method ?? "*"} ${route.pathname} is registered more than once.`);
602
+ }
603
+ routes.set(key, route);
604
+ }
605
+ }
606
+ return routes;
607
+ }
608
+ function validateRuntimeRoute(primitiveId, route) {
609
+ if (!route.pathname.startsWith("/")) {
610
+ throw new Error(`Runtime primitive ${primitiveId} route must start with "/": ${route.pathname}.`);
611
+ }
612
+ if (route.pathname === "/__chat_de_hp" ||
613
+ route.pathname.startsWith("/__chat_de_hp/")) {
614
+ throw new Error(`Runtime primitive ${primitiveId} cannot claim reserved route ${route.pathname}.`);
615
+ }
616
+ }
617
+ function runtimeRouteKey(method, pathname) {
618
+ return `${method.toUpperCase()} ${pathname}`;
619
+ }
@@ -0,0 +1,5 @@
1
+ import type { GeneratedSiteRuntime } from "./contracts.js";
2
+ export declare function createChatDeHpSiteWorker(runtime: GeneratedSiteRuntime): {
3
+ fetch(request: Request, env: import("./contracts.js").ChatDeHpSiteEnv, context: import("./contracts.js").ChatDeHpExecutionContext): Promise<Response>;
4
+ };
5
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/runtime/worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,oBAAoB;;EAErE"}
@@ -0,0 +1,5 @@
1
+ import handler from "@astrojs/cloudflare/entrypoints/server";
2
+ import { createChatDeHpSiteWorkerWithHandler } from "./worker-core.js";
3
+ export function createChatDeHpSiteWorker(runtime) {
4
+ return createChatDeHpSiteWorkerWithHandler(runtime, handler);
5
+ }