@agntcms/next 0.2.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.
@@ -0,0 +1,2525 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/handlers.ts
31
+ var handlers_exports = {};
32
+ __export(handlers_exports, {
33
+ AgentUnreachableError: () => AgentUnreachableError,
34
+ createAgentBridge: () => createAgentBridge,
35
+ createAssetsHandler: () => createAssetsHandler,
36
+ createDraftHandler: () => createDraftHandler,
37
+ createEventsHandler: () => createEventsHandler,
38
+ createFormsDeleteHandler: () => createFormsDeleteHandler,
39
+ createFormsListHandler: () => createFormsListHandler,
40
+ createFormsReadHandler: () => createFormsReadHandler,
41
+ createGlobalHandler: () => createGlobalHandler,
42
+ createMcpHandler: () => createMcpHandler,
43
+ createPageHandler: () => createPageHandler,
44
+ createPreviewHandler: () => createPreviewHandler,
45
+ createPreviewTokenStore: () => createPreviewTokenStore,
46
+ createSubmitFormHandler: () => createSubmitFormHandler,
47
+ createTaskStore: () => createTaskStore,
48
+ createTemplateHandler: () => createTemplateHandler
49
+ });
50
+ module.exports = __toCommonJS(handlers_exports);
51
+
52
+ // src/storage/fs/assets.ts
53
+ var import_node_crypto = require("crypto");
54
+ var fs2 = __toESM(require("fs/promises"), 1);
55
+ var path2 = __toESM(require("path"), 1);
56
+
57
+ // src/storage/fs/_helpers.ts
58
+ var fs = __toESM(require("fs/promises"), 1);
59
+ var path = __toESM(require("path"), 1);
60
+ var isEnoent = (err) => typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
61
+ var defaultRandomSuffix = () => Math.random().toString(36).slice(2);
62
+ var writeAtomic = async (target, data, randomSuffix = defaultRandomSuffix) => {
63
+ const dir = path.dirname(target);
64
+ await fs.mkdir(dir, { recursive: true });
65
+ const tmp = path.join(
66
+ dir,
67
+ `.${path.basename(target)}.${process.pid}.${randomSuffix()}.tmp`
68
+ );
69
+ try {
70
+ await fs.writeFile(tmp, data);
71
+ await fs.rename(tmp, target);
72
+ } catch (err) {
73
+ await fs.unlink(tmp).catch(() => {
74
+ });
75
+ throw err;
76
+ }
77
+ };
78
+ var resolveUnderBucket = (bucket, key, errorLabel, errorKey = key) => {
79
+ const resolved = path.resolve(bucket, key);
80
+ const bucketWithSep = bucket.endsWith(path.sep) ? bucket : bucket + path.sep;
81
+ if (!resolved.startsWith(bucketWithSep)) {
82
+ throw new Error(`${errorLabel}: ${JSON.stringify(errorKey)}`);
83
+ }
84
+ return resolved;
85
+ };
86
+
87
+ // src/storage/fs/assets.ts
88
+ var EXT_PATTERN = /^\.[a-z0-9_-]{1,16}$/;
89
+ var JSON_SUFFIX = ".json";
90
+ var extensionFrom = (filename) => {
91
+ const dot = filename.lastIndexOf(".");
92
+ if (dot < 0 || dot === filename.length - 1) return "";
93
+ const raw = filename.slice(dot).toLowerCase();
94
+ return EXT_PATTERN.test(raw) ? raw : "";
95
+ };
96
+ var CONTENT_TYPE_BY_EXT = {
97
+ ".png": "image/png",
98
+ ".jpg": "image/jpeg",
99
+ ".jpeg": "image/jpeg",
100
+ ".gif": "image/gif",
101
+ ".webp": "image/webp",
102
+ ".svg": "image/svg+xml",
103
+ ".avif": "image/avif"
104
+ };
105
+ var contentTypeFor = (filename) => {
106
+ const dot = filename.lastIndexOf(".");
107
+ if (dot < 0) return void 0;
108
+ const ext = filename.slice(dot).toLowerCase();
109
+ return CONTENT_TYPE_BY_EXT[ext];
110
+ };
111
+ var createFsAssetAdapter = (options) => {
112
+ const { assetsRoot, publicUrlBase } = options;
113
+ if (!path2.isAbsolute(assetsRoot)) {
114
+ throw new Error(
115
+ `assetsRoot must be an absolute path, got: ${JSON.stringify(assetsRoot)}`
116
+ );
117
+ }
118
+ if (publicUrlBase.endsWith("/")) {
119
+ throw new Error(
120
+ `publicUrlBase must not end with '/', got: ${JSON.stringify(publicUrlBase)}`
121
+ );
122
+ }
123
+ const rootResolved = path2.resolve(assetsRoot);
124
+ const rootWithSep = rootResolved.endsWith(path2.sep) ? rootResolved : rootResolved + path2.sep;
125
+ const upload = async (input) => {
126
+ const hash = (0, import_node_crypto.createHash)("sha256").update(input.bytes).digest("hex");
127
+ const ext = extensionFrom(input.filename);
128
+ const storedName = `${hash}${ext}`;
129
+ const target = path2.resolve(rootResolved, storedName);
130
+ if (!target.startsWith(rootWithSep)) {
131
+ throw new Error(`asset path escapes assetsRoot: ${JSON.stringify(target)}`);
132
+ }
133
+ let bytesExist = false;
134
+ try {
135
+ await fs2.stat(target);
136
+ bytesExist = true;
137
+ } catch (err) {
138
+ if (!isEnoent(err)) throw err;
139
+ }
140
+ if (!bytesExist) {
141
+ await writeAtomic(target, input.bytes);
142
+ }
143
+ return {
144
+ url: `${publicUrlBase}/${storedName}`,
145
+ filename: storedName
146
+ };
147
+ };
148
+ const list = async () => {
149
+ let names;
150
+ try {
151
+ names = await fs2.readdir(rootResolved);
152
+ } catch (err) {
153
+ if (isEnoent(err)) return [];
154
+ throw err;
155
+ }
156
+ const candidates = names.filter(
157
+ (n) => !n.startsWith(".") && !n.endsWith(JSON_SUFFIX)
158
+ );
159
+ const entries = [];
160
+ for (const filename of candidates) {
161
+ const full = path2.join(rootResolved, filename);
162
+ let stat3;
163
+ try {
164
+ stat3 = await fs2.stat(full);
165
+ } catch (err) {
166
+ if (isEnoent(err)) continue;
167
+ throw err;
168
+ }
169
+ if (!stat3.isFile()) continue;
170
+ entries.push({
171
+ filename,
172
+ url: `${publicUrlBase}/${filename}`,
173
+ contentType: contentTypeFor(filename),
174
+ modifiedAt: stat3.mtime
175
+ });
176
+ }
177
+ entries.sort((a, b) => {
178
+ const delta = b.modifiedAt.getTime() - a.modifiedAt.getTime();
179
+ if (delta !== 0) return delta;
180
+ return a.filename.localeCompare(b.filename);
181
+ });
182
+ return entries;
183
+ };
184
+ return { upload, list };
185
+ };
186
+
187
+ // src/storage/fs/content.ts
188
+ var import_node_fs = require("fs");
189
+ var fs3 = __toESM(require("fs/promises"), 1);
190
+ var path3 = __toESM(require("path"), 1);
191
+
192
+ // src/domain/page.ts
193
+ function assertValidPageMeta(obj) {
194
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
195
+ throw new Error("invalid page: missing or empty slug");
196
+ }
197
+ const slug = obj["slug"];
198
+ const rawSeo = obj["seo"];
199
+ if (rawSeo === null || typeof rawSeo !== "object") {
200
+ throw new Error(`invalid page "${slug}": seo must be an object`);
201
+ }
202
+ const seo = rawSeo;
203
+ if (typeof seo["title"] !== "string" || seo["title"].trim() === "") {
204
+ throw new Error(
205
+ `invalid page "${slug}": seo.title must be a non-empty string`
206
+ );
207
+ }
208
+ if (typeof seo["description"] !== "string" || seo["description"].trim() === "") {
209
+ throw new Error(
210
+ `invalid page "${slug}": seo.description must be a non-empty string`
211
+ );
212
+ }
213
+ if (seo["canonical"] !== void 0) {
214
+ if (typeof seo["canonical"] !== "string" || seo["canonical"].trim() === "") {
215
+ throw new Error(
216
+ `invalid page "${slug}": seo.canonical, when present, must be a non-empty string`
217
+ );
218
+ }
219
+ }
220
+ }
221
+ function assertValidPage(page) {
222
+ if (page === null || typeof page !== "object") {
223
+ throw new Error("invalid page: expected an object");
224
+ }
225
+ const obj = page;
226
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
227
+ throw new Error("invalid page: missing or empty slug");
228
+ }
229
+ const slug = obj["slug"];
230
+ if (!Array.isArray(obj["sections"])) {
231
+ throw new Error(`invalid page "${slug}": sections must be an array`);
232
+ }
233
+ assertValidPageMeta(obj);
234
+ }
235
+
236
+ // src/domain/form.ts
237
+ var SubmissionsNotReadableError = class extends Error {
238
+ constructor(message = "submission adapter does not support reading") {
239
+ super(message);
240
+ this.name = "SubmissionsNotReadableError";
241
+ }
242
+ };
243
+
244
+ // src/storage/fs/content.ts
245
+ var SLUG_PATTERN = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/;
246
+ var assertValidSlug = (slug) => {
247
+ if (!SLUG_PATTERN.test(slug)) {
248
+ throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
249
+ }
250
+ };
251
+ var deepEqual = (a, b) => {
252
+ if (a === b) return true;
253
+ if (typeof a !== typeof b) return false;
254
+ if (a === null || b === null) return false;
255
+ if (typeof a !== "object") return false;
256
+ const aIsArr = Array.isArray(a);
257
+ const bIsArr = Array.isArray(b);
258
+ if (aIsArr !== bIsArr) return false;
259
+ if (aIsArr) {
260
+ const arrA = a;
261
+ const arrB = b;
262
+ if (arrA.length !== arrB.length) return false;
263
+ for (let i = 0; i < arrA.length; i += 1) {
264
+ if (!deepEqual(arrA[i], arrB[i])) return false;
265
+ }
266
+ return true;
267
+ }
268
+ const objA = a;
269
+ const objB = b;
270
+ const keysA = Object.keys(objA);
271
+ const keysB = Object.keys(objB);
272
+ if (keysA.length !== keysB.length) return false;
273
+ for (const key of keysA) {
274
+ if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
275
+ if (!deepEqual(objA[key], objB[key])) return false;
276
+ }
277
+ return true;
278
+ };
279
+ var createFsContentAdapter = (options) => {
280
+ const { contentRoot } = options;
281
+ if (!path3.isAbsolute(contentRoot)) {
282
+ throw new Error(
283
+ `contentRoot must be an absolute path, got: ${JSON.stringify(contentRoot)}`
284
+ );
285
+ }
286
+ const pagesDir = path3.resolve(contentRoot, "pages");
287
+ const draftsDir = path3.resolve(contentRoot, "drafts");
288
+ const historyDir = path3.resolve(contentRoot, "history");
289
+ const globalsDir = path3.resolve(contentRoot, "globals");
290
+ const globalsHistoryDir = path3.resolve(contentRoot, "history-globals");
291
+ const resolveSlugPath = (bucket, slug, ext) => {
292
+ assertValidSlug(slug);
293
+ return resolveUnderBucket(bucket, `${slug}${ext}`, "slug escapes storage bucket", slug);
294
+ };
295
+ const readJsonOrNull = async (filePath) => {
296
+ try {
297
+ const raw = await fs3.readFile(filePath, { encoding: "utf8" });
298
+ return JSON.parse(raw);
299
+ } catch (err) {
300
+ if (isEnoent(err)) return null;
301
+ throw err;
302
+ }
303
+ };
304
+ const listJsonFiles2 = async (dir) => {
305
+ let entries;
306
+ try {
307
+ entries = await fs3.readdir(dir, { withFileTypes: true });
308
+ } catch (err) {
309
+ if (isEnoent(err)) return [];
310
+ throw err;
311
+ }
312
+ const results = [];
313
+ for (const entry of entries) {
314
+ if (entry.isFile() && entry.name.endsWith(".json")) {
315
+ results.push(entry.name);
316
+ } else if (entry.isDirectory()) {
317
+ const subDir = path3.join(dir, entry.name);
318
+ const subFiles = await listJsonFiles2(subDir);
319
+ for (const sub of subFiles) {
320
+ results.push(`${entry.name}/${sub}`);
321
+ }
322
+ }
323
+ }
324
+ return results;
325
+ };
326
+ const bucketFor = (mode) => mode === "published" ? pagesDir : draftsDir;
327
+ const uniqueTimestampedPath = async (baseDir, key, errorLabel) => {
328
+ const keyDir = resolveUnderBucket(baseDir, key, errorLabel);
329
+ await fs3.mkdir(keyDir, { recursive: true });
330
+ const base = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
331
+ let candidate = path3.join(keyDir, `${base}.json`);
332
+ let suffix = 0;
333
+ while (true) {
334
+ try {
335
+ await fs3.access(candidate, import_node_fs.constants.F_OK);
336
+ suffix += 1;
337
+ candidate = path3.join(keyDir, `${base}-${suffix}.json`);
338
+ } catch (err) {
339
+ if (isEnoent(err)) return candidate;
340
+ throw err;
341
+ }
342
+ }
343
+ };
344
+ const uniqueHistoryPath = async (slug) => {
345
+ assertValidSlug(slug);
346
+ return uniqueTimestampedPath(historyDir, slug, "slug escapes history bucket");
347
+ };
348
+ const readPage = async (slug, mode) => {
349
+ const filePath = resolveSlugPath(bucketFor(mode), slug, ".json");
350
+ return readJsonOrNull(filePath);
351
+ };
352
+ const saveDraft = async (page) => {
353
+ const filePath = resolveSlugPath(draftsDir, page.slug, ".json");
354
+ await writeAtomic(filePath, JSON.stringify(page, null, 2));
355
+ };
356
+ const listDrafts = async () => {
357
+ const files = await listJsonFiles2(draftsDir);
358
+ const results = [];
359
+ for (const relPath of files) {
360
+ const slug = relPath.slice(0, -".json".length);
361
+ const stat3 = await fs3.stat(path3.join(draftsDir, relPath));
362
+ results.push({ slug, updatedAt: stat3.mtime });
363
+ }
364
+ return results;
365
+ };
366
+ const listPages = async () => {
367
+ const files = await listJsonFiles2(pagesDir);
368
+ const results = [];
369
+ for (const relPath of files) {
370
+ const slug = relPath.slice(0, -".json".length);
371
+ const stat3 = await fs3.stat(path3.join(pagesDir, relPath));
372
+ results.push({ slug, updatedAt: stat3.mtime });
373
+ }
374
+ return results;
375
+ };
376
+ const listPageSummaries = async () => {
377
+ const files = await listJsonFiles2(pagesDir);
378
+ const results = [];
379
+ for (const relPath of files) {
380
+ const slug = relPath.slice(0, -".json".length);
381
+ const filePath = path3.join(pagesDir, relPath);
382
+ const page = await readJsonOrNull(filePath);
383
+ if (page === null) continue;
384
+ const stat3 = await fs3.stat(filePath).catch(() => null);
385
+ if (stat3 === null) continue;
386
+ const { sections: _sections, ...meta } = page;
387
+ void _sections;
388
+ results.push({ ...meta, slug, updatedAt: stat3.mtime });
389
+ }
390
+ return results;
391
+ };
392
+ const readLatestSnapshotIn = async (baseDir, key, errorLabel) => {
393
+ const keyDir = resolveUnderBucket(baseDir, key, errorLabel);
394
+ let entries;
395
+ try {
396
+ entries = await fs3.readdir(keyDir, { withFileTypes: true });
397
+ } catch (err) {
398
+ if (isEnoent(err)) return null;
399
+ throw err;
400
+ }
401
+ let latest = null;
402
+ for (const entry of entries) {
403
+ if (!entry.isFile()) continue;
404
+ if (!entry.name.endsWith(".json")) continue;
405
+ if (latest === null || entry.name > latest) latest = entry.name;
406
+ }
407
+ if (latest === null) return null;
408
+ return readJsonOrNull(path3.join(keyDir, latest));
409
+ };
410
+ const readLatestHistorySnapshot = async (slug) => {
411
+ assertValidSlug(slug);
412
+ return readLatestSnapshotIn(historyDir, slug, "slug escapes history bucket");
413
+ };
414
+ const publishDraft = async (slug) => {
415
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
416
+ const pagePath = resolveSlugPath(pagesDir, slug, ".json");
417
+ const page = await readJsonOrNull(draftPath);
418
+ if (page === null) {
419
+ throw new Error(`no draft to publish for slug: ${JSON.stringify(slug)}`);
420
+ }
421
+ assertValidPage(page);
422
+ const serialised = JSON.stringify(page, null, 2);
423
+ const latestSnapshot = await readLatestHistorySnapshot(slug);
424
+ const skipHistory = latestSnapshot !== null && deepEqual(latestSnapshot, page);
425
+ if (!skipHistory) {
426
+ const historyPath = await uniqueHistoryPath(slug);
427
+ await writeAtomic(historyPath, serialised);
428
+ }
429
+ await writeAtomic(pagePath, serialised);
430
+ try {
431
+ await fs3.unlink(draftPath);
432
+ } catch (err) {
433
+ if (!isEnoent(err)) throw err;
434
+ }
435
+ return page;
436
+ };
437
+ const deleteDraft = async (slug) => {
438
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
439
+ try {
440
+ await fs3.unlink(draftPath);
441
+ } catch (err) {
442
+ if (isEnoent(err)) {
443
+ throw new Error(`no draft to discard for slug: ${JSON.stringify(slug)}`);
444
+ }
445
+ throw err;
446
+ }
447
+ };
448
+ const deletePage = async (slug) => {
449
+ const publishedPath = resolveSlugPath(pagesDir, slug, ".json");
450
+ try {
451
+ await fs3.access(publishedPath);
452
+ } catch {
453
+ throw new Error(`page not found: ${slug}`);
454
+ }
455
+ await fs3.unlink(publishedPath);
456
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
457
+ try {
458
+ await fs3.unlink(draftPath);
459
+ } catch {
460
+ }
461
+ };
462
+ const unpublishPage = async (slug) => {
463
+ const publishedPath = resolveSlugPath(pagesDir, slug, ".json");
464
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
465
+ const published = await readJsonOrNull(publishedPath);
466
+ if (published === null) {
467
+ throw new Error(`page not found: ${slug}`);
468
+ }
469
+ const existingDraft = await readJsonOrNull(draftPath);
470
+ if (existingDraft === null) {
471
+ await writeAtomic(draftPath, JSON.stringify(published, null, 2));
472
+ }
473
+ await fs3.unlink(publishedPath);
474
+ };
475
+ const listHistory = async (slug) => {
476
+ assertValidSlug(slug);
477
+ const slugDir = resolveUnderBucket(historyDir, slug, "slug escapes history bucket");
478
+ let entries;
479
+ try {
480
+ entries = await fs3.readdir(slugDir, { withFileTypes: true });
481
+ } catch (err) {
482
+ if (isEnoent(err)) return [];
483
+ throw err;
484
+ }
485
+ const results = [];
486
+ for (const entry of entries) {
487
+ if (!entry.isFile()) continue;
488
+ if (!entry.name.endsWith(".json")) continue;
489
+ const timestamp = entry.name.slice(0, -".json".length);
490
+ const stat3 = await fs3.stat(path3.join(slugDir, entry.name));
491
+ results.push({ slug, timestamp, size: stat3.size });
492
+ }
493
+ results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
494
+ return results;
495
+ };
496
+ const readHistorySnapshot = async (slug, timestamp) => {
497
+ assertValidSlug(slug);
498
+ const slugDir = resolveUnderBucket(historyDir, slug, "slug escapes history bucket");
499
+ const filePath = resolveUnderBucket(
500
+ slugDir,
501
+ `${timestamp}.json`,
502
+ "timestamp escapes history directory",
503
+ timestamp
504
+ );
505
+ return readJsonOrNull(filePath);
506
+ };
507
+ const renamePage = async (fromSlug, toSlug) => {
508
+ assertValidSlug(fromSlug);
509
+ assertValidSlug(toSlug);
510
+ const fromPublished = resolveSlugPath(pagesDir, fromSlug, ".json");
511
+ const toPublished = resolveSlugPath(pagesDir, toSlug, ".json");
512
+ try {
513
+ await fs3.access(fromPublished);
514
+ } catch {
515
+ throw new Error(`page not found: ${fromSlug}`);
516
+ }
517
+ try {
518
+ await fs3.access(toPublished);
519
+ throw new Error(`target slug already exists: ${toSlug}`);
520
+ } catch (err) {
521
+ if (isEnoent(err)) {
522
+ } else {
523
+ throw err;
524
+ }
525
+ }
526
+ await fs3.rename(fromPublished, toPublished);
527
+ const fromDraft = resolveSlugPath(draftsDir, fromSlug, ".json");
528
+ const toDraft = resolveSlugPath(draftsDir, toSlug, ".json");
529
+ try {
530
+ await fs3.rename(fromDraft, toDraft);
531
+ } catch (err) {
532
+ if (!isEnoent(err)) throw err;
533
+ }
534
+ const fromHistory = path3.resolve(historyDir, fromSlug);
535
+ const toHistory = path3.resolve(historyDir, toSlug);
536
+ try {
537
+ await fs3.rename(fromHistory, toHistory);
538
+ } catch (err) {
539
+ if (!isEnoent(err)) throw err;
540
+ }
541
+ };
542
+ const readGlobal = async (name) => {
543
+ const filePath = resolveSlugPath(globalsDir, name, ".json");
544
+ return readJsonOrNull(filePath);
545
+ };
546
+ const uniqueGlobalHistoryPath = async (name) => {
547
+ assertValidSlug(name);
548
+ return uniqueTimestampedPath(
549
+ globalsHistoryDir,
550
+ name,
551
+ "name escapes globals history bucket"
552
+ );
553
+ };
554
+ const readLatestGlobalHistorySnapshot = async (name) => {
555
+ assertValidSlug(name);
556
+ return readLatestSnapshotIn(
557
+ globalsHistoryDir,
558
+ name,
559
+ "name escapes globals history bucket"
560
+ );
561
+ };
562
+ const saveGlobal = async (global) => {
563
+ const filePath = resolveSlugPath(globalsDir, global.name, ".json");
564
+ const serialised = JSON.stringify(global, null, 2);
565
+ const latestSnapshot = await readLatestGlobalHistorySnapshot(global.name);
566
+ const skipHistory = latestSnapshot !== null && deepEqual(latestSnapshot, global);
567
+ if (!skipHistory) {
568
+ const historyPath = await uniqueGlobalHistoryPath(global.name);
569
+ await writeAtomic(historyPath, serialised);
570
+ }
571
+ await writeAtomic(filePath, serialised);
572
+ };
573
+ const listGlobals = async () => {
574
+ let entries;
575
+ try {
576
+ entries = await fs3.readdir(globalsDir, { withFileTypes: true });
577
+ } catch (err) {
578
+ if (isEnoent(err)) return [];
579
+ throw err;
580
+ }
581
+ const results = [];
582
+ for (const entry of entries) {
583
+ if (!entry.isFile()) continue;
584
+ if (!entry.name.endsWith(".json")) continue;
585
+ const filePath = path3.join(globalsDir, entry.name);
586
+ const json = await readJsonOrNull(filePath);
587
+ if (json === null) continue;
588
+ const stat3 = await fs3.stat(filePath).catch(() => null);
589
+ if (stat3 === null) continue;
590
+ results.push({ name: json.name, type: json.type, updatedAt: stat3.mtime });
591
+ }
592
+ return results;
593
+ };
594
+ const deleteGlobal = async (name) => {
595
+ const filePath = resolveSlugPath(globalsDir, name, ".json");
596
+ try {
597
+ await fs3.unlink(filePath);
598
+ } catch (err) {
599
+ if (isEnoent(err)) {
600
+ throw new Error(`global not found: ${name}`);
601
+ }
602
+ throw err;
603
+ }
604
+ };
605
+ const listGlobalHistory = async (name) => {
606
+ assertValidSlug(name);
607
+ const nameDir = resolveUnderBucket(
608
+ globalsHistoryDir,
609
+ name,
610
+ "name escapes globals history bucket"
611
+ );
612
+ let entries;
613
+ try {
614
+ entries = await fs3.readdir(nameDir, { withFileTypes: true });
615
+ } catch (err) {
616
+ if (isEnoent(err)) return [];
617
+ throw err;
618
+ }
619
+ const results = [];
620
+ for (const entry of entries) {
621
+ if (!entry.isFile()) continue;
622
+ if (!entry.name.endsWith(".json")) continue;
623
+ const timestamp = entry.name.slice(0, -".json".length);
624
+ const stat3 = await fs3.stat(path3.join(nameDir, entry.name));
625
+ results.push({ name, timestamp, size: stat3.size });
626
+ }
627
+ results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
628
+ return results;
629
+ };
630
+ const readGlobalHistorySnapshot = async (name, timestamp) => {
631
+ assertValidSlug(name);
632
+ const nameDir = resolveUnderBucket(
633
+ globalsHistoryDir,
634
+ name,
635
+ "name escapes globals history bucket"
636
+ );
637
+ const filePath = resolveUnderBucket(
638
+ nameDir,
639
+ `${timestamp}.json`,
640
+ "timestamp escapes history directory",
641
+ timestamp
642
+ );
643
+ return readJsonOrNull(filePath);
644
+ };
645
+ const rollbackGlobal = async (name, timestamp) => {
646
+ const snapshot = await readGlobalHistorySnapshot(name, timestamp);
647
+ if (snapshot === null) {
648
+ throw new Error(`global history snapshot not found: ${name} @ ${timestamp}`);
649
+ }
650
+ await saveGlobal(snapshot);
651
+ };
652
+ return {
653
+ readPage,
654
+ saveDraft,
655
+ listDrafts,
656
+ listPages,
657
+ listPageSummaries,
658
+ publishDraft,
659
+ deleteDraft,
660
+ deletePage,
661
+ unpublishPage,
662
+ listHistory,
663
+ readHistorySnapshot,
664
+ renamePage,
665
+ readGlobal,
666
+ saveGlobal,
667
+ listGlobals,
668
+ deleteGlobal,
669
+ listGlobalHistory,
670
+ readGlobalHistorySnapshot,
671
+ rollbackGlobal
672
+ };
673
+ };
674
+
675
+ // src/storage/fs/submissions.ts
676
+ var import_node_crypto2 = require("crypto");
677
+ var fs4 = __toESM(require("fs/promises"), 1);
678
+ var path4 = __toESM(require("path"), 1);
679
+ var DEFAULT_MAX_LIST = 500;
680
+ var FORM_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
681
+ var parseSubmission = (raw) => {
682
+ let parsed;
683
+ try {
684
+ parsed = JSON.parse(raw);
685
+ } catch {
686
+ return null;
687
+ }
688
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
689
+ return null;
690
+ }
691
+ const obj = parsed;
692
+ if (typeof obj["formName"] !== "string") return null;
693
+ if (typeof obj["id"] !== "string") return null;
694
+ if (typeof obj["submittedAt"] !== "string") return null;
695
+ if (obj["payload"] === null || typeof obj["payload"] !== "object" || Array.isArray(obj["payload"])) {
696
+ return null;
697
+ }
698
+ return {
699
+ formName: obj["formName"],
700
+ id: obj["id"],
701
+ submittedAt: obj["submittedAt"],
702
+ payload: obj["payload"]
703
+ };
704
+ };
705
+ var assertValidFormName = (name) => {
706
+ if (typeof name !== "string" || !FORM_NAME_PATTERN.test(name)) {
707
+ throw new Error(`invalid form name: ${JSON.stringify(name)}`);
708
+ }
709
+ };
710
+ var submissionFilename = (submittedAt, id) => {
711
+ const ts = submittedAt.replace(/:/g, "-");
712
+ return `${ts}-${id}.json`;
713
+ };
714
+ var listJsonFiles = async (dir) => {
715
+ let entries;
716
+ try {
717
+ entries = await fs4.readdir(dir, { withFileTypes: true });
718
+ } catch (err) {
719
+ if (isEnoent(err)) return [];
720
+ throw err;
721
+ }
722
+ const results = [];
723
+ for (const entry of entries) {
724
+ if (entry.isFile() && entry.name.endsWith(".json")) {
725
+ results.push(entry.name);
726
+ }
727
+ }
728
+ return results;
729
+ };
730
+ var createFsSubmissionAdapter = (options) => {
731
+ const { submissionsRoot } = options;
732
+ if (!path4.isAbsolute(submissionsRoot)) {
733
+ throw new Error(
734
+ `submissionsRoot must be an absolute path, got: ${JSON.stringify(submissionsRoot)}`
735
+ );
736
+ }
737
+ const maxList = options.maxList ?? DEFAULT_MAX_LIST;
738
+ if (!Number.isFinite(maxList) || maxList <= 0 || !Number.isInteger(maxList)) {
739
+ throw new Error(
740
+ `maxList must be a positive integer, got: ${maxList}`
741
+ );
742
+ }
743
+ const rootResolved = path4.resolve(submissionsRoot);
744
+ const rootWithSep = rootResolved.endsWith(path4.sep) ? rootResolved : rootResolved + path4.sep;
745
+ const formDir = (formName) => {
746
+ assertValidFormName(formName);
747
+ const dir = path4.resolve(rootResolved, formName);
748
+ if (!dir.startsWith(rootWithSep)) {
749
+ throw new Error(`form name escapes submissions root: ${JSON.stringify(formName)}`);
750
+ }
751
+ return dir;
752
+ };
753
+ const cryptoSuffix = () => (0, import_node_crypto2.randomUUID)();
754
+ const store = async (submission) => {
755
+ const dir = formDir(submission.formName);
756
+ const filename = submissionFilename(submission.submittedAt, submission.id);
757
+ if (filename.includes("/") || filename.includes("\\")) {
758
+ throw new Error(`submission id contains path separator: ${JSON.stringify(submission.id)}`);
759
+ }
760
+ const target = path4.join(dir, filename);
761
+ await writeAtomic(target, JSON.stringify(submission, null, 2), cryptoSuffix);
762
+ };
763
+ const list = async (formName) => {
764
+ const dir = formDir(formName);
765
+ const files = await listJsonFiles(dir);
766
+ const summaries = [];
767
+ for (const filename of files) {
768
+ const stem = filename.slice(0, -".json".length);
769
+ const lastDash = stem.lastIndexOf("-");
770
+ if (lastDash < 0) continue;
771
+ const tsRaw = stem.slice(0, lastDash);
772
+ const id = stem.slice(lastDash + 1);
773
+ if (id === "") continue;
774
+ const tIndex = tsRaw.indexOf("T");
775
+ if (tIndex < 0) continue;
776
+ const datePart = tsRaw.slice(0, tIndex);
777
+ const timePart = tsRaw.slice(tIndex + 1).replace(/-/g, ":");
778
+ const submittedAt = `${datePart}T${timePart}`;
779
+ summaries.push({ id, submittedAt });
780
+ }
781
+ summaries.sort((a, b) => {
782
+ if (a.submittedAt !== b.submittedAt) {
783
+ return a.submittedAt < b.submittedAt ? 1 : -1;
784
+ }
785
+ if (a.id !== b.id) {
786
+ return a.id < b.id ? 1 : -1;
787
+ }
788
+ return 0;
789
+ });
790
+ return summaries.length > maxList ? summaries.slice(0, maxList) : summaries;
791
+ };
792
+ const read = async (formName, id) => {
793
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) {
794
+ return null;
795
+ }
796
+ const dir = formDir(formName);
797
+ let entries;
798
+ try {
799
+ entries = (await fs4.readdir(dir)).filter((n) => n.endsWith(".json"));
800
+ } catch (err) {
801
+ if (isEnoent(err)) return null;
802
+ throw err;
803
+ }
804
+ const suffix = `-${id}.json`;
805
+ const found = entries.find((n) => n.endsWith(suffix));
806
+ if (!found) return null;
807
+ const target = path4.join(dir, found);
808
+ let raw;
809
+ try {
810
+ raw = await fs4.readFile(target, { encoding: "utf8" });
811
+ } catch (err) {
812
+ if (isEnoent(err)) return null;
813
+ throw err;
814
+ }
815
+ return parseSubmission(raw);
816
+ };
817
+ return { info: { kind: "fs" }, store, list, read };
818
+ };
819
+
820
+ // src/config/defaults-registry.ts
821
+ var SLOT = /* @__PURE__ */ Symbol.for("@agntcms/next/default-adapter-factories");
822
+ function holder() {
823
+ return globalThis;
824
+ }
825
+ function registerDefaultAdapterFactories(factories) {
826
+ holder()[SLOT] = factories;
827
+ }
828
+
829
+ // src/config/defaults.ts
830
+ var path5 = __toESM(require("path"), 1);
831
+ function createDefaultContentAdapter(options) {
832
+ const root = options?.projectRoot ?? process.cwd();
833
+ return createFsContentAdapter({
834
+ contentRoot: path5.resolve(root, "content")
835
+ });
836
+ }
837
+ function createDefaultAssetAdapter(options) {
838
+ const root = options?.projectRoot ?? process.cwd();
839
+ return createFsAssetAdapter({
840
+ assetsRoot: path5.resolve(root, "public/assets"),
841
+ publicUrlBase: "/assets"
842
+ });
843
+ }
844
+ function createDefaultSubmissionAdapter(options) {
845
+ const root = options?.projectRoot ?? process.cwd();
846
+ return createFsSubmissionAdapter({
847
+ submissionsRoot: path5.resolve(root, "content/submissions")
848
+ });
849
+ }
850
+ function installDefaultAdapterFactories() {
851
+ const factories = {
852
+ content: createDefaultContentAdapter,
853
+ asset: createDefaultAssetAdapter,
854
+ submission: createDefaultSubmissionAdapter
855
+ };
856
+ registerDefaultAdapterFactories(factories);
857
+ }
858
+
859
+ // src/mcp/bridge.ts
860
+ var AgentUnreachableError = class extends Error {
861
+ name = "AgentUnreachableError";
862
+ status;
863
+ constructor(message, status) {
864
+ super(message);
865
+ this.status = status;
866
+ }
867
+ };
868
+ var TASK_ACK_TIMEOUT_MESSAGE = "Agent received the task but did not respond. Open an interactive Claude Code session in the project directory and launch it with channel notifications enabled: `claude --dangerously-load-development-channels server:agntcms`. Requirements: Claude Code v2.1.80+, signed in with claude.ai (not API key / Console), and an interactive session \u2014 `claude -p` does not deliver channel events. The flag is undocumented in `--help` because channels are still in research preview; it is still the supported mechanism (see code.claude.com/docs/en/channels-reference).";
869
+ var createAgentBridge = (options) => {
870
+ const {
871
+ taskStore,
872
+ channelPort = 4819,
873
+ channelHost = "127.0.0.1",
874
+ callbackUrl = "http://localhost:3000/api/agntcms/mcp",
875
+ taskAckTimeoutMs = 15e3
876
+ } = options;
877
+ const fetchFn = options.fetch ?? globalThis.fetch;
878
+ const setTimeoutFn = options.setTimeoutFn ?? ((cb, ms) => setTimeout(cb, ms));
879
+ const clearTimeoutFn = options.clearTimeoutFn ?? ((handle) => {
880
+ clearTimeout(handle);
881
+ });
882
+ const baseUrl = `http://${channelHost}:${channelPort}`;
883
+ let status = "stopped";
884
+ const inflightTaskIds = /* @__PURE__ */ new Set();
885
+ let startPromise = null;
886
+ const failAllInflight = (reason) => {
887
+ const ids = Array.from(inflightTaskIds);
888
+ inflightTaskIds.clear();
889
+ for (const id of ids) {
890
+ try {
891
+ taskStore.fail(id, reason);
892
+ } catch {
893
+ }
894
+ }
895
+ };
896
+ const markFailed = (reason) => {
897
+ status = "failed";
898
+ startPromise = null;
899
+ failAllInflight(reason);
900
+ };
901
+ const doStart = async () => {
902
+ status = "starting";
903
+ try {
904
+ const res = await fetchFn(`${baseUrl}/health`, { method: "GET" });
905
+ if (!res.ok) {
906
+ throw new Error(`health check returned ${String(res.status)}`);
907
+ }
908
+ status = "running";
909
+ } catch (err) {
910
+ const reason = err instanceof Error ? `failed to connect to channel server: ${err.message}` : `failed to connect to channel server: ${String(err)}`;
911
+ markFailed(reason);
912
+ throw new AgentUnreachableError(reason, "failed");
913
+ }
914
+ };
915
+ const start = () => {
916
+ if (status === "running") return Promise.resolve();
917
+ if (status === "starting" && startPromise !== null) return startPromise;
918
+ const p = doStart();
919
+ startPromise = p;
920
+ return p;
921
+ };
922
+ const pushTask = (task) => {
923
+ if (status !== "running") {
924
+ return Promise.reject(
925
+ new AgentUnreachableError(
926
+ `cannot push task: agent bridge is ${status}`,
927
+ status
928
+ )
929
+ );
930
+ }
931
+ let resultPromise;
932
+ try {
933
+ resultPromise = taskStore.create(task);
934
+ } catch (err) {
935
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
936
+ }
937
+ inflightTaskIds.add(task.id);
938
+ let ackTimer = null;
939
+ let unsubscribe = null;
940
+ const clearWatchdog = () => {
941
+ if (ackTimer !== null) {
942
+ clearTimeoutFn(ackTimer);
943
+ ackTimer = null;
944
+ }
945
+ if (unsubscribe !== null) {
946
+ unsubscribe();
947
+ unsubscribe = null;
948
+ }
949
+ };
950
+ const cleanup = () => {
951
+ inflightTaskIds.delete(task.id);
952
+ clearWatchdog();
953
+ };
954
+ resultPromise.then(cleanup, cleanup);
955
+ if (taskAckTimeoutMs > 0) {
956
+ unsubscribe = taskStore.subscribe(task.id, () => {
957
+ clearWatchdog();
958
+ });
959
+ }
960
+ fetchFn(`${baseUrl}/task`, {
961
+ method: "POST",
962
+ headers: { "Content-Type": "application/json" },
963
+ body: JSON.stringify({
964
+ task_id: task.id,
965
+ type: task.type,
966
+ payload: task.payload,
967
+ callback_url: callbackUrl
968
+ })
969
+ }).then((res) => {
970
+ if (!res.ok) {
971
+ throw new Error(`channel /task returned ${String(res.status)}`);
972
+ }
973
+ if (taskAckTimeoutMs > 0 && ackTimer === null && unsubscribe !== null) {
974
+ ackTimer = setTimeoutFn(() => {
975
+ try {
976
+ taskStore.fail(task.id, TASK_ACK_TIMEOUT_MESSAGE);
977
+ } catch {
978
+ }
979
+ }, taskAckTimeoutMs);
980
+ }
981
+ }).catch((err) => {
982
+ const reason = err instanceof Error ? `channel POST failed: ${err.message}` : `channel POST failed: ${String(err)}`;
983
+ try {
984
+ taskStore.fail(task.id, reason);
985
+ } catch {
986
+ }
987
+ });
988
+ return resultPromise;
989
+ };
990
+ const stop = async () => {
991
+ if (status === "stopped") return;
992
+ const previousStatus = status;
993
+ status = "stopped";
994
+ startPromise = null;
995
+ if (previousStatus === "running" || previousStatus === "starting") {
996
+ failAllInflight("agent bridge stopped");
997
+ }
998
+ };
999
+ const statusGetter = () => status;
1000
+ return { start, pushTask, stop, status: statusGetter };
1001
+ };
1002
+
1003
+ // src/handlers/utils.ts
1004
+ var jsonResponse = (body, status, extraHeaders) => new Response(JSON.stringify(body), {
1005
+ status,
1006
+ headers: { "Content-Type": "application/json", ...extraHeaders }
1007
+ });
1008
+ var safeErrorMessage = (err) => {
1009
+ if (err instanceof Error) return err.message;
1010
+ if (typeof err === "string") return err;
1011
+ return "unknown error";
1012
+ };
1013
+
1014
+ // src/handlers/mcp/mcp-handler.ts
1015
+ var parseAction = (body) => {
1016
+ if (body === null || typeof body !== "object") {
1017
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
1018
+ }
1019
+ const obj = body;
1020
+ if (typeof obj["action"] !== "string") {
1021
+ return jsonResponse({ error: "missing_field", message: "Missing required field: action" }, 400);
1022
+ }
1023
+ const action = obj["action"];
1024
+ switch (action) {
1025
+ case "start":
1026
+ case "stop":
1027
+ case "status":
1028
+ return { action };
1029
+ case "push_task": {
1030
+ if (typeof obj["type"] !== "string" || obj["type"] === "") {
1031
+ return jsonResponse(
1032
+ { error: "missing_field", message: 'push_task requires a non-empty "type" string' },
1033
+ 400
1034
+ );
1035
+ }
1036
+ if (obj["payload"] === void 0 || obj["payload"] === null || typeof obj["payload"] !== "object" || Array.isArray(obj["payload"])) {
1037
+ return jsonResponse(
1038
+ { error: "missing_field", message: 'push_task requires a "payload" object' },
1039
+ 400
1040
+ );
1041
+ }
1042
+ return {
1043
+ action: "push_task",
1044
+ type: obj["type"],
1045
+ payload: obj["payload"]
1046
+ };
1047
+ }
1048
+ case "task_callback": {
1049
+ const event = obj["event"];
1050
+ if (event !== "completed" && event !== "progress" && event !== "failed") {
1051
+ return jsonResponse(
1052
+ { error: "missing_field", message: 'task_callback requires "event" to be "completed", "progress", or "failed"' },
1053
+ 400
1054
+ );
1055
+ }
1056
+ if (typeof obj["task_id"] !== "string" || obj["task_id"] === "") {
1057
+ return jsonResponse(
1058
+ { error: "missing_field", message: 'task_callback requires a non-empty "task_id" string' },
1059
+ 400
1060
+ );
1061
+ }
1062
+ if (event === "completed" && typeof obj["result"] !== "string") {
1063
+ return jsonResponse(
1064
+ { error: "missing_field", message: 'task_callback with event "completed" requires a "result" string' },
1065
+ 400
1066
+ );
1067
+ }
1068
+ if (event === "progress" && typeof obj["message"] !== "string") {
1069
+ return jsonResponse(
1070
+ { error: "missing_field", message: 'task_callback with event "progress" requires a "message" string' },
1071
+ 400
1072
+ );
1073
+ }
1074
+ if (event === "failed" && typeof obj["error"] !== "string") {
1075
+ return jsonResponse(
1076
+ { error: "missing_field", message: 'task_callback with event "failed" requires an "error" string' },
1077
+ 400
1078
+ );
1079
+ }
1080
+ const taskId = obj["task_id"];
1081
+ switch (event) {
1082
+ case "completed":
1083
+ return { action: "task_callback", event, task_id: taskId, result: obj["result"] };
1084
+ case "progress":
1085
+ return { action: "task_callback", event, task_id: taskId, message: obj["message"] };
1086
+ case "failed":
1087
+ return { action: "task_callback", event, task_id: taskId, error: obj["error"] };
1088
+ }
1089
+ }
1090
+ default:
1091
+ return jsonResponse({ error: "unknown_action", message: `Unknown action: ${action}` }, 400);
1092
+ }
1093
+ };
1094
+ var createMcpHandler = (deps) => {
1095
+ const { bridge, taskStore } = deps;
1096
+ const POST = async (req) => {
1097
+ let body;
1098
+ try {
1099
+ body = await req.json();
1100
+ } catch {
1101
+ return jsonResponse({ error: "invalid_body", message: "Invalid JSON" }, 400);
1102
+ }
1103
+ const actionOrError = parseAction(body);
1104
+ if (actionOrError instanceof Response) return actionOrError;
1105
+ const action = actionOrError;
1106
+ try {
1107
+ switch (action.action) {
1108
+ case "start": {
1109
+ await bridge.start();
1110
+ return jsonResponse({ status: bridge.status() }, 200);
1111
+ }
1112
+ case "stop": {
1113
+ await bridge.stop();
1114
+ return jsonResponse({ status: bridge.status() }, 200);
1115
+ }
1116
+ case "status": {
1117
+ return jsonResponse({ status: bridge.status() }, 200);
1118
+ }
1119
+ case "push_task": {
1120
+ if (bridge.status() !== "running") {
1121
+ return jsonResponse(
1122
+ {
1123
+ error: "bridge_not_running",
1124
+ message: `Agent bridge is ${bridge.status()}. Start it before pushing tasks.`
1125
+ },
1126
+ 409
1127
+ );
1128
+ }
1129
+ const taskId = crypto.randomUUID();
1130
+ const task = { id: taskId, type: action.type, payload: action.payload };
1131
+ bridge.pushTask(task).catch(() => {
1132
+ });
1133
+ return jsonResponse({ taskId }, 200);
1134
+ }
1135
+ case "task_callback": {
1136
+ try {
1137
+ switch (action.event) {
1138
+ case "completed":
1139
+ taskStore.complete(action.task_id, action.result);
1140
+ break;
1141
+ case "progress":
1142
+ taskStore.progress(action.task_id, action.message);
1143
+ break;
1144
+ case "failed":
1145
+ taskStore.fail(action.task_id, action.error);
1146
+ break;
1147
+ }
1148
+ return jsonResponse({ ok: true }, 200);
1149
+ } catch (err) {
1150
+ return jsonResponse(
1151
+ { error: "task_store_error", message: safeErrorMessage(err) },
1152
+ 400
1153
+ );
1154
+ }
1155
+ }
1156
+ }
1157
+ } catch (err) {
1158
+ if (err instanceof AgentUnreachableError) {
1159
+ return jsonResponse({ error: "agent_unreachable", message: err.message }, 503);
1160
+ }
1161
+ return jsonResponse({ error: "internal" }, 500);
1162
+ }
1163
+ };
1164
+ return { POST };
1165
+ };
1166
+
1167
+ // src/handlers/events/events-handler.ts
1168
+ var formatSseFrame = (taskId, event) => {
1169
+ let data;
1170
+ switch (event.kind) {
1171
+ case "progress":
1172
+ data = { taskId, message: event.message };
1173
+ break;
1174
+ case "completed":
1175
+ data = { taskId, result: event.result };
1176
+ break;
1177
+ case "failed":
1178
+ data = { taskId, error: event.error };
1179
+ break;
1180
+ }
1181
+ return `event: ${event.kind}
1182
+ data: ${JSON.stringify(data)}
1183
+
1184
+ `;
1185
+ };
1186
+ var isTerminal = (event) => event.kind === "completed" || event.kind === "failed";
1187
+ var createEventsHandler = (deps) => {
1188
+ const { taskStore } = deps;
1189
+ const GET = (req) => {
1190
+ const url = new URL(req.url);
1191
+ const taskId = url.searchParams.get("taskId");
1192
+ if (taskId === null || taskId === "") {
1193
+ return new Response("Missing required query parameter: taskId", { status: 400 });
1194
+ }
1195
+ const snapshot = taskStore.get(taskId);
1196
+ if (snapshot === void 0) {
1197
+ return new Response(`Task not found: ${taskId}`, { status: 404 });
1198
+ }
1199
+ const encoder = new TextEncoder();
1200
+ const stream = new ReadableStream({
1201
+ start(controller) {
1202
+ let closed = false;
1203
+ const close = () => {
1204
+ if (closed) return;
1205
+ closed = true;
1206
+ try {
1207
+ controller.close();
1208
+ } catch {
1209
+ }
1210
+ };
1211
+ const unsubscribe = taskStore.subscribe(taskId, (event) => {
1212
+ if (closed) return;
1213
+ try {
1214
+ controller.enqueue(encoder.encode(formatSseFrame(taskId, event)));
1215
+ } catch {
1216
+ return;
1217
+ }
1218
+ if (isTerminal(event)) {
1219
+ close();
1220
+ }
1221
+ });
1222
+ if (req.signal) {
1223
+ const onAbort = () => {
1224
+ unsubscribe();
1225
+ close();
1226
+ };
1227
+ if (req.signal.aborted) {
1228
+ onAbort();
1229
+ } else {
1230
+ req.signal.addEventListener("abort", onAbort, { once: true });
1231
+ }
1232
+ }
1233
+ }
1234
+ });
1235
+ return new Response(stream, {
1236
+ status: 200,
1237
+ headers: {
1238
+ "Content-Type": "text/event-stream",
1239
+ "Cache-Control": "no-cache",
1240
+ "Connection": "keep-alive"
1241
+ }
1242
+ });
1243
+ };
1244
+ return { GET };
1245
+ };
1246
+
1247
+ // src/handlers/preview/preview-handler.ts
1248
+ var DEFAULT_COOKIE_NAME = "__agntcms_preview";
1249
+ function createPreviewHandler(deps) {
1250
+ const cookieName = deps?.cookieName ?? DEFAULT_COOKIE_NAME;
1251
+ const tokenStore = deps?.tokenStore;
1252
+ const enter = (_req) => {
1253
+ const cookie = `${cookieName}=1; Path=/; HttpOnly; SameSite=Lax`;
1254
+ return jsonResponse({ ok: true }, 200, { "Set-Cookie": cookie });
1255
+ };
1256
+ const exit = (_req) => {
1257
+ const cookie = `${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
1258
+ return jsonResponse({ ok: true }, 200, { "Set-Cookie": cookie });
1259
+ };
1260
+ const enterWithToken = (req) => {
1261
+ const url = new URL(req.url);
1262
+ const token = url.searchParams.get("token");
1263
+ if (!tokenStore) {
1264
+ return jsonResponse({ error: "token_store_not_configured" }, 501);
1265
+ }
1266
+ if (!token) {
1267
+ return jsonResponse({ error: "missing_token" }, 400);
1268
+ }
1269
+ const slug = tokenStore.consume(token);
1270
+ if (slug === null) {
1271
+ return jsonResponse({ error: "invalid_or_expired_token" }, 403);
1272
+ }
1273
+ const cookie = `${cookieName}=1; Path=/; HttpOnly; SameSite=Lax`;
1274
+ const redirectUrl = `/${slug}`;
1275
+ return new Response(null, {
1276
+ status: 302,
1277
+ headers: {
1278
+ "Location": redirectUrl,
1279
+ "Set-Cookie": cookie
1280
+ }
1281
+ });
1282
+ };
1283
+ const issueToken = async (req) => {
1284
+ if (!tokenStore) {
1285
+ return jsonResponse({ error: "token_store_not_configured" }, 501);
1286
+ }
1287
+ let body;
1288
+ try {
1289
+ body = await req.json();
1290
+ } catch {
1291
+ return jsonResponse({ error: "invalid_json" }, 400);
1292
+ }
1293
+ if (!body || typeof body !== "object" || !("slug" in body) || typeof body.slug !== "string") {
1294
+ return jsonResponse({ error: "missing_slug" }, 400);
1295
+ }
1296
+ const slug = body.slug;
1297
+ const previewToken = tokenStore.issue(slug);
1298
+ return jsonResponse({
1299
+ token: previewToken.token,
1300
+ previewUrl: `/api/agntcms/preview/enter?token=${previewToken.token}`
1301
+ }, 200);
1302
+ };
1303
+ return { enter, exit, enterWithToken, issueToken };
1304
+ }
1305
+
1306
+ // src/runtime/systemPages.ts
1307
+ var NOT_FOUND_PAGE_SLUG = "404";
1308
+ var SERVER_ERROR_PAGE_SLUG = "500";
1309
+ var SYSTEM_PAGE_ALIAS_TO_CANONICAL = /* @__PURE__ */ new Map([
1310
+ ["not-found", NOT_FOUND_PAGE_SLUG],
1311
+ ["_not-found", NOT_FOUND_PAGE_SLUG],
1312
+ ["error-404", NOT_FOUND_PAGE_SLUG],
1313
+ ["404-page", NOT_FOUND_PAGE_SLUG],
1314
+ ["page-not-found", NOT_FOUND_PAGE_SLUG],
1315
+ ["error-500", SERVER_ERROR_PAGE_SLUG],
1316
+ ["500-page", SERVER_ERROR_PAGE_SLUG],
1317
+ ["server-error", SERVER_ERROR_PAGE_SLUG]
1318
+ ]);
1319
+ var getReservedPageSlugViolation = (slug) => {
1320
+ const canonicalSlug = SYSTEM_PAGE_ALIAS_TO_CANONICAL.get(slug);
1321
+ if (!canonicalSlug) return null;
1322
+ const pageLabel = canonicalSlug === NOT_FOUND_PAGE_SLUG ? "404 page" : "500 page";
1323
+ return {
1324
+ slug,
1325
+ canonicalSlug,
1326
+ message: `Slug "${slug}" is reserved as an alias for the ${pageLabel}. Edit the canonical "${canonicalSlug}" page instead.`
1327
+ };
1328
+ };
1329
+
1330
+ // src/handlers/draft/draft-handler.ts
1331
+ var parseSaveBody = (body) => {
1332
+ if (body === null || typeof body !== "object") {
1333
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
1334
+ }
1335
+ try {
1336
+ assertValidPage(body);
1337
+ } catch (err) {
1338
+ return jsonResponse(
1339
+ { error: "missing_field", message: safeErrorMessage(err) },
1340
+ 400
1341
+ );
1342
+ }
1343
+ const reservedSlug = getReservedPageSlugViolation(body.slug);
1344
+ if (reservedSlug) {
1345
+ return jsonResponse(
1346
+ { error: "reserved_slug_alias", message: reservedSlug.message },
1347
+ 400
1348
+ );
1349
+ }
1350
+ const page = {
1351
+ slug: body.slug,
1352
+ seo: body.seo,
1353
+ sections: body.sections,
1354
+ ...body.tags !== void 0 ? { tags: body.tags } : {},
1355
+ ...body.excerpt !== void 0 ? { excerpt: body.excerpt } : {},
1356
+ ...body.coverImage !== void 0 ? { coverImage: body.coverImage } : {},
1357
+ ...body.publishedAt !== void 0 ? { publishedAt: body.publishedAt } : {}
1358
+ };
1359
+ return page;
1360
+ };
1361
+ var parsePublishBody = (body) => {
1362
+ if (body === null || typeof body !== "object") {
1363
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
1364
+ }
1365
+ const obj = body;
1366
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
1367
+ return jsonResponse(
1368
+ { error: "missing_field", message: "Missing or empty required field: slug" },
1369
+ 400
1370
+ );
1371
+ }
1372
+ return obj["slug"];
1373
+ };
1374
+ function createDraftHandler(deps) {
1375
+ const { contentAdapter, runtime } = deps;
1376
+ const save = async (req) => {
1377
+ let body;
1378
+ try {
1379
+ body = await req.json();
1380
+ } catch {
1381
+ return jsonResponse({ error: "invalid_body", message: "Invalid JSON" }, 400);
1382
+ }
1383
+ const pageOrError = parseSaveBody(body);
1384
+ if (pageOrError instanceof Response) return pageOrError;
1385
+ const page = pageOrError;
1386
+ try {
1387
+ await contentAdapter.saveDraft(page);
1388
+ } catch (err) {
1389
+ return jsonResponse(
1390
+ { error: "save_failed", message: safeErrorMessage(err) },
1391
+ 500
1392
+ );
1393
+ }
1394
+ return jsonResponse({ ok: true }, 200);
1395
+ };
1396
+ const list = async (_req) => {
1397
+ let drafts;
1398
+ try {
1399
+ drafts = await contentAdapter.listDrafts();
1400
+ } catch (err) {
1401
+ return jsonResponse(
1402
+ { error: "list_failed", message: safeErrorMessage(err) },
1403
+ 500
1404
+ );
1405
+ }
1406
+ const serialized = drafts.map((d) => ({
1407
+ slug: d.slug,
1408
+ updatedAt: d.updatedAt.toISOString()
1409
+ }));
1410
+ return jsonResponse({ drafts: serialized }, 200);
1411
+ };
1412
+ const publish = async (req) => {
1413
+ let body;
1414
+ try {
1415
+ body = await req.json();
1416
+ } catch {
1417
+ return jsonResponse({ error: "invalid_body", message: "Invalid JSON" }, 400);
1418
+ }
1419
+ const slugOrError = parsePublishBody(body);
1420
+ if (slugOrError instanceof Response) return slugOrError;
1421
+ const slug = slugOrError;
1422
+ let page;
1423
+ try {
1424
+ page = await runtime.publishDraft(slug);
1425
+ } catch (err) {
1426
+ return jsonResponse(
1427
+ { error: "not_found", message: safeErrorMessage(err) },
1428
+ 404
1429
+ );
1430
+ }
1431
+ return jsonResponse({ ok: true, page }, 200);
1432
+ };
1433
+ const reorder = async (req) => {
1434
+ let body;
1435
+ try {
1436
+ body = await req.json();
1437
+ } catch {
1438
+ return jsonResponse({ error: "invalid_body", message: "Invalid JSON" }, 400);
1439
+ }
1440
+ if (body === null || typeof body !== "object") {
1441
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
1442
+ }
1443
+ const obj = body;
1444
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
1445
+ return jsonResponse(
1446
+ { error: "missing_field", message: "Missing or empty required field: slug" },
1447
+ 400
1448
+ );
1449
+ }
1450
+ if (!Array.isArray(obj["order"])) {
1451
+ return jsonResponse(
1452
+ { error: "missing_field", message: "Missing or non-array required field: order" },
1453
+ 400
1454
+ );
1455
+ }
1456
+ const slug = obj["slug"];
1457
+ const order = obj["order"];
1458
+ if (!order.every((id) => typeof id === "string")) {
1459
+ return jsonResponse(
1460
+ { error: "invalid_field", message: "order must be an array of string section IDs" },
1461
+ 400
1462
+ );
1463
+ }
1464
+ let page = await contentAdapter.readPage(slug, "draft");
1465
+ if (page === null) {
1466
+ page = await contentAdapter.readPage(slug, "published");
1467
+ }
1468
+ if (page === null) {
1469
+ return jsonResponse({ error: "not_found", message: `No page found for slug: ${slug}` }, 404);
1470
+ }
1471
+ const currentIds = page.sections.map((s) => s.id);
1472
+ const sortedCurrent = [...currentIds].sort();
1473
+ const sortedOrder = [...order].sort();
1474
+ if (sortedCurrent.length !== sortedOrder.length) {
1475
+ return jsonResponse(
1476
+ { error: "invalid_order", message: "order must contain exactly the same section IDs as the page" },
1477
+ 400
1478
+ );
1479
+ }
1480
+ for (let i = 0; i < sortedCurrent.length; i++) {
1481
+ if (sortedCurrent[i] !== sortedOrder[i]) {
1482
+ return jsonResponse(
1483
+ { error: "invalid_order", message: "order must contain exactly the same section IDs as the page" },
1484
+ 400
1485
+ );
1486
+ }
1487
+ }
1488
+ const sectionById = new Map(page.sections.map((s) => [s.id, s]));
1489
+ const reordered = order.map((id) => sectionById.get(id));
1490
+ const reorderedPage = { ...page, sections: reordered };
1491
+ try {
1492
+ await contentAdapter.saveDraft(reorderedPage);
1493
+ } catch (err) {
1494
+ return jsonResponse(
1495
+ { error: "save_failed", message: safeErrorMessage(err) },
1496
+ 500
1497
+ );
1498
+ }
1499
+ return jsonResponse({ ok: true }, 200);
1500
+ };
1501
+ const discard = async (req) => {
1502
+ let body;
1503
+ try {
1504
+ body = await req.json();
1505
+ } catch {
1506
+ return jsonResponse({ error: "invalid_body", message: "Invalid JSON" }, 400);
1507
+ }
1508
+ const slugOrError = parsePublishBody(body);
1509
+ if (slugOrError instanceof Response) return slugOrError;
1510
+ const slug = slugOrError;
1511
+ const published = await contentAdapter.readPage(slug, "published");
1512
+ if (published === null) {
1513
+ return jsonResponse(
1514
+ { error: "no_published_version", message: `No published version exists for slug: ${slug}. Discarding the draft would lose all data.` },
1515
+ 400
1516
+ );
1517
+ }
1518
+ try {
1519
+ await contentAdapter.deleteDraft(slug);
1520
+ } catch (err) {
1521
+ const message = safeErrorMessage(err);
1522
+ if (message.includes("no draft to discard")) {
1523
+ return jsonResponse({ error: "not_found", message }, 404);
1524
+ }
1525
+ return jsonResponse({ error: "discard_failed", message }, 500);
1526
+ }
1527
+ return jsonResponse({ ok: true }, 200);
1528
+ };
1529
+ return { save, list, publish, reorder, discard };
1530
+ }
1531
+
1532
+ // src/handlers/assets/assets-handler.ts
1533
+ var serialiseEntry = (e) => ({
1534
+ filename: e.filename,
1535
+ url: e.url,
1536
+ contentType: e.contentType ?? null,
1537
+ modifiedAt: e.modifiedAt.toISOString()
1538
+ });
1539
+ function createAssetsHandler(deps) {
1540
+ const { assetAdapter } = deps;
1541
+ const list = async (req) => {
1542
+ if (req.method !== "GET") {
1543
+ return jsonResponse(
1544
+ { error: "method_not_allowed", message: `Expected GET, got ${req.method}` },
1545
+ 405
1546
+ );
1547
+ }
1548
+ let entries;
1549
+ try {
1550
+ entries = await assetAdapter.list();
1551
+ } catch (err) {
1552
+ return jsonResponse(
1553
+ { error: "list_failed", message: safeErrorMessage(err) },
1554
+ 500
1555
+ );
1556
+ }
1557
+ return jsonResponse(
1558
+ { assets: entries.map(serialiseEntry) },
1559
+ 200
1560
+ );
1561
+ };
1562
+ const upload = async (req) => {
1563
+ let form;
1564
+ try {
1565
+ form = await req.formData();
1566
+ } catch (err) {
1567
+ return jsonResponse(
1568
+ { error: "invalid_body", message: safeErrorMessage(err) },
1569
+ 400
1570
+ );
1571
+ }
1572
+ const fileField = form.get("file");
1573
+ if (!(fileField instanceof Blob)) {
1574
+ return jsonResponse(
1575
+ { error: "missing_file", message: "Missing required field: file" },
1576
+ 400
1577
+ );
1578
+ }
1579
+ const contentType = fileField.type;
1580
+ if (!contentType.startsWith("image/")) {
1581
+ return jsonResponse(
1582
+ {
1583
+ error: "invalid_content_type",
1584
+ message: `Expected image/*, got ${contentType || "unknown"}`
1585
+ },
1586
+ 400
1587
+ );
1588
+ }
1589
+ const filename = "name" in fileField && typeof fileField.name === "string" ? fileField.name : "";
1590
+ const arrayBuffer = await fileField.arrayBuffer();
1591
+ const bytes = new Uint8Array(arrayBuffer);
1592
+ let result;
1593
+ try {
1594
+ result = await assetAdapter.upload({
1595
+ bytes,
1596
+ filename,
1597
+ contentType
1598
+ });
1599
+ } catch (err) {
1600
+ return jsonResponse(
1601
+ { error: "upload_failed", message: safeErrorMessage(err) },
1602
+ 500
1603
+ );
1604
+ }
1605
+ const asset = {
1606
+ filename: result.filename,
1607
+ url: result.url,
1608
+ contentType,
1609
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1610
+ };
1611
+ return jsonResponse({ asset }, 200);
1612
+ };
1613
+ return { list, upload };
1614
+ }
1615
+
1616
+ // src/tasks/store.ts
1617
+ var isTerminal2 = (entry) => entry.state === "completed" || entry.state === "failed";
1618
+ var toSnapshot = (entry) => {
1619
+ const base = { id: entry.task.id, type: entry.task.type, state: entry.state };
1620
+ switch (entry.state) {
1621
+ case "pending":
1622
+ case "in_progress":
1623
+ return base;
1624
+ case "completed":
1625
+ return { ...base, result: entry.result };
1626
+ case "failed":
1627
+ return { ...base, error: entry.error };
1628
+ }
1629
+ };
1630
+ var require_ = (entries, id) => {
1631
+ const entry = entries.get(id);
1632
+ if (entry === void 0) {
1633
+ throw new Error(`unknown task id: ${id}`);
1634
+ }
1635
+ return entry;
1636
+ };
1637
+ var requirePending = (entries, id) => {
1638
+ const entry = require_(entries, id);
1639
+ if (isTerminal2(entry)) {
1640
+ throw new Error(`task ${id} is already ${entry.state}`);
1641
+ }
1642
+ return entry;
1643
+ };
1644
+ var createTaskStore = () => {
1645
+ const entries = /* @__PURE__ */ new Map();
1646
+ const emit = (listeners, event) => {
1647
+ for (const listener of Array.from(listeners)) {
1648
+ listener(event);
1649
+ }
1650
+ };
1651
+ const create = (task) => {
1652
+ if (entries.has(task.id)) {
1653
+ throw new Error(`duplicate task id: ${task.id}`);
1654
+ }
1655
+ return new Promise((resolve6, reject) => {
1656
+ const entry = {
1657
+ state: "pending",
1658
+ task,
1659
+ resolve: resolve6,
1660
+ reject,
1661
+ listeners: /* @__PURE__ */ new Set()
1662
+ };
1663
+ entries.set(task.id, entry);
1664
+ });
1665
+ };
1666
+ const get = (id) => {
1667
+ const entry = entries.get(id);
1668
+ return entry === void 0 ? void 0 : toSnapshot(entry);
1669
+ };
1670
+ const progress = (id, message) => {
1671
+ const pending = requirePending(entries, id);
1672
+ if (pending.state === "pending") {
1673
+ const next = {
1674
+ ...pending,
1675
+ state: "in_progress"
1676
+ };
1677
+ entries.set(id, next);
1678
+ emit(next.listeners, { kind: "progress", message });
1679
+ return;
1680
+ }
1681
+ emit(pending.listeners, { kind: "progress", message });
1682
+ };
1683
+ const complete = (id, result) => {
1684
+ const pending = requirePending(entries, id);
1685
+ const terminal = {
1686
+ state: "completed",
1687
+ task: pending.task,
1688
+ result
1689
+ };
1690
+ entries.set(id, terminal);
1691
+ pending.resolve(result);
1692
+ emit(pending.listeners, { kind: "completed", result });
1693
+ };
1694
+ const fail = (id, error) => {
1695
+ const pending = requirePending(entries, id);
1696
+ const terminal = {
1697
+ state: "failed",
1698
+ task: pending.task,
1699
+ error
1700
+ };
1701
+ entries.set(id, terminal);
1702
+ pending.reject(new Error(error));
1703
+ emit(pending.listeners, { kind: "failed", error });
1704
+ };
1705
+ const subscribe = (id, listener) => {
1706
+ const entry = require_(entries, id);
1707
+ if (isTerminal2(entry)) {
1708
+ const replayEvent = entry.state === "completed" ? { kind: "completed", result: entry.result } : { kind: "failed", error: entry.error };
1709
+ listener(replayEvent);
1710
+ const noop = () => {
1711
+ };
1712
+ return noop;
1713
+ }
1714
+ entry.listeners.add(listener);
1715
+ return () => {
1716
+ entry.listeners.delete(listener);
1717
+ };
1718
+ };
1719
+ return { create, get, progress, complete, fail, subscribe };
1720
+ };
1721
+
1722
+ // src/preview-tokens/store.ts
1723
+ var import_node_crypto3 = require("crypto");
1724
+ var DEFAULT_TTL_MS = 6e5;
1725
+ var createPreviewTokenStore = (options) => {
1726
+ const ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
1727
+ const entries = /* @__PURE__ */ new Map();
1728
+ const sweep = (now) => {
1729
+ for (const [token, entry] of entries) {
1730
+ if (entry.createdAt + ttlMs <= now) {
1731
+ entries.delete(token);
1732
+ }
1733
+ }
1734
+ };
1735
+ const issue = (slug) => {
1736
+ const now = Date.now();
1737
+ sweep(now);
1738
+ const token = (0, import_node_crypto3.randomUUID)();
1739
+ const entry = { slug, createdAt: now };
1740
+ entries.set(token, entry);
1741
+ return { token, slug, createdAt: now };
1742
+ };
1743
+ const consume = (token) => {
1744
+ const entry = entries.get(token);
1745
+ if (entry === void 0) {
1746
+ return null;
1747
+ }
1748
+ entries.delete(token);
1749
+ const now = Date.now();
1750
+ if (entry.createdAt + ttlMs <= now) {
1751
+ return null;
1752
+ }
1753
+ return entry.slug;
1754
+ };
1755
+ return { issue, consume };
1756
+ };
1757
+
1758
+ // src/handlers/page/page-handler.ts
1759
+ var import_node_crypto4 = require("crypto");
1760
+ var SLUG_PATTERN2 = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/;
1761
+ var reservedSlugResponse = (slug, error) => {
1762
+ const violation = getReservedPageSlugViolation(slug);
1763
+ if (!violation) return null;
1764
+ return jsonResponse({ error, message: violation.message }, 400);
1765
+ };
1766
+ var cloneSection = (section) => {
1767
+ const fresh = {
1768
+ id: `sec_${(0, import_node_crypto4.randomUUID)()}`,
1769
+ type: section.type,
1770
+ data: section.data,
1771
+ ...section.globalRef !== void 0 ? { globalRef: section.globalRef } : {}
1772
+ };
1773
+ return fresh;
1774
+ };
1775
+ function createPageHandler(deps) {
1776
+ const { contentAdapter, runtime } = deps;
1777
+ const parseLimitParam = (raw) => {
1778
+ if (raw === null || raw === "") return { ok: true, value: void 0 };
1779
+ const n = Number(raw);
1780
+ if (!Number.isFinite(n)) return { ok: false };
1781
+ if (!Number.isInteger(n)) return { ok: false };
1782
+ if (n < 0) return { ok: false };
1783
+ return { ok: true, value: n };
1784
+ };
1785
+ const parseSortParam = (raw) => {
1786
+ if (raw === null || raw === "") return { ok: true, value: void 0 };
1787
+ if (raw === "newest" || raw === "oldest") return { ok: true, value: raw };
1788
+ return { ok: false };
1789
+ };
1790
+ const listSummaries = async (req) => {
1791
+ const url = new URL(req.url);
1792
+ const tagRaw = url.searchParams.get("tag");
1793
+ const limitParsed = parseLimitParam(url.searchParams.get("limit"));
1794
+ const sortParsed = parseSortParam(url.searchParams.get("sort"));
1795
+ if (!limitParsed.ok) {
1796
+ return jsonResponse(
1797
+ { error: "invalid_limit", message: "limit must be a non-negative integer" },
1798
+ 400
1799
+ );
1800
+ }
1801
+ if (!sortParsed.ok) {
1802
+ return jsonResponse(
1803
+ { error: "invalid_sort", message: "sort must be 'newest' or 'oldest'" },
1804
+ 400
1805
+ );
1806
+ }
1807
+ const tag = tagRaw === null || tagRaw === "" ? void 0 : tagRaw;
1808
+ const mutableInput = {};
1809
+ if (tag !== void 0) mutableInput.tag = tag;
1810
+ if (limitParsed.value !== void 0) mutableInput.limit = limitParsed.value;
1811
+ if (sortParsed.value !== void 0) mutableInput.sort = sortParsed.value;
1812
+ const input = mutableInput;
1813
+ try {
1814
+ const pages = await runtime.listPages(input);
1815
+ return jsonResponse({ pages }, 200);
1816
+ } catch (err) {
1817
+ return jsonResponse(
1818
+ { error: "list_pages_failed", message: safeErrorMessage(err) },
1819
+ 500
1820
+ );
1821
+ }
1822
+ };
1823
+ const list = async (req) => {
1824
+ const url = new URL(req.url);
1825
+ const hasListPagesParams = url.searchParams.has("tag") || url.searchParams.has("limit") || url.searchParams.has("sort");
1826
+ if (hasListPagesParams) {
1827
+ return listSummaries(req);
1828
+ }
1829
+ try {
1830
+ const [pages, drafts] = await Promise.all([
1831
+ contentAdapter.listPages(),
1832
+ contentAdapter.listDrafts()
1833
+ ]);
1834
+ const map = /* @__PURE__ */ new Map();
1835
+ for (const p of pages) {
1836
+ map.set(p.slug, {
1837
+ slug: p.slug,
1838
+ hasPublished: true,
1839
+ hasDraft: false,
1840
+ updatedAt: p.updatedAt.toISOString()
1841
+ });
1842
+ }
1843
+ for (const d of drafts) {
1844
+ const existing = map.get(d.slug);
1845
+ if (existing) {
1846
+ existing.hasDraft = true;
1847
+ const existingDate = new Date(existing.updatedAt);
1848
+ if (d.updatedAt > existingDate) {
1849
+ existing.updatedAt = d.updatedAt.toISOString();
1850
+ }
1851
+ } else {
1852
+ map.set(d.slug, {
1853
+ slug: d.slug,
1854
+ hasPublished: false,
1855
+ hasDraft: true,
1856
+ updatedAt: d.updatedAt.toISOString()
1857
+ });
1858
+ }
1859
+ }
1860
+ const combined = Array.from(map.values()).sort(
1861
+ (a, b) => a.slug.localeCompare(b.slug)
1862
+ );
1863
+ return jsonResponse({ pages: combined }, 200);
1864
+ } catch (err) {
1865
+ return jsonResponse(
1866
+ { error: "list_pages_failed", message: safeErrorMessage(err) },
1867
+ 500
1868
+ );
1869
+ }
1870
+ };
1871
+ const read = async (req) => {
1872
+ const url = new URL(req.url);
1873
+ const slug = url.searchParams.get("slug");
1874
+ if (!slug || slug === "") {
1875
+ return jsonResponse({ error: "missing_slug" }, 400);
1876
+ }
1877
+ try {
1878
+ const page = await contentAdapter.readPage(slug, "draft") ?? await contentAdapter.readPage(slug, "published");
1879
+ if (!page) {
1880
+ return jsonResponse({ error: "page_not_found" }, 404);
1881
+ }
1882
+ return jsonResponse({ page }, 200);
1883
+ } catch (err) {
1884
+ return jsonResponse(
1885
+ { error: "read_page_failed", message: safeErrorMessage(err) },
1886
+ 500
1887
+ );
1888
+ }
1889
+ };
1890
+ const deletePage = async (req) => {
1891
+ let body;
1892
+ try {
1893
+ body = await req.json();
1894
+ } catch {
1895
+ return jsonResponse({ error: "invalid_json" }, 400);
1896
+ }
1897
+ if (!body || typeof body !== "object" || !("slug" in body) || typeof body.slug !== "string") {
1898
+ return jsonResponse({ error: "missing_slug" }, 400);
1899
+ }
1900
+ const slug = body.slug;
1901
+ try {
1902
+ await contentAdapter.deletePage(slug);
1903
+ } catch (err) {
1904
+ const message = err instanceof Error ? err.message : String(err);
1905
+ if (message.includes("page not found")) {
1906
+ return jsonResponse({ error: message }, 404);
1907
+ }
1908
+ return jsonResponse({ error: message }, 500);
1909
+ }
1910
+ return jsonResponse({ ok: true }, 200);
1911
+ };
1912
+ const unpublish = async (req) => {
1913
+ let body;
1914
+ try {
1915
+ body = await req.json();
1916
+ } catch {
1917
+ return jsonResponse({ error: "invalid_json" }, 400);
1918
+ }
1919
+ if (!body || typeof body !== "object" || !("slug" in body) || typeof body.slug !== "string") {
1920
+ return jsonResponse({ error: "missing_slug" }, 400);
1921
+ }
1922
+ const slug = body.slug;
1923
+ try {
1924
+ await contentAdapter.unpublishPage(slug);
1925
+ } catch (err) {
1926
+ const message = err instanceof Error ? err.message : String(err);
1927
+ if (message.includes("page not found")) {
1928
+ return jsonResponse({ error: message }, 404);
1929
+ }
1930
+ return jsonResponse({ error: message }, 500);
1931
+ }
1932
+ return jsonResponse({ ok: true }, 200);
1933
+ };
1934
+ const listHistory = async (req) => {
1935
+ const url = new URL(req.url);
1936
+ const slug = url.searchParams.get("slug");
1937
+ const timestamp = url.searchParams.get("ts");
1938
+ if (!slug || slug === "") {
1939
+ return jsonResponse({ error: "missing_slug" }, 400);
1940
+ }
1941
+ if (timestamp !== null) {
1942
+ if (timestamp === "") {
1943
+ return jsonResponse({ error: "missing_timestamp" }, 400);
1944
+ }
1945
+ try {
1946
+ const snapshot = await contentAdapter.readHistorySnapshot(slug, timestamp);
1947
+ if (snapshot === null) {
1948
+ return jsonResponse({ error: "snapshot_not_found" }, 404);
1949
+ }
1950
+ return jsonResponse({ page: snapshot }, 200);
1951
+ } catch (err) {
1952
+ return jsonResponse(
1953
+ { error: "read_history_snapshot_failed", message: safeErrorMessage(err) },
1954
+ 500
1955
+ );
1956
+ }
1957
+ }
1958
+ try {
1959
+ const entries = await contentAdapter.listHistory(slug);
1960
+ return jsonResponse({ entries }, 200);
1961
+ } catch (err) {
1962
+ return jsonResponse(
1963
+ { error: "list_history_failed", message: safeErrorMessage(err) },
1964
+ 500
1965
+ );
1966
+ }
1967
+ };
1968
+ const rollback = async (req) => {
1969
+ let body;
1970
+ try {
1971
+ body = await req.json();
1972
+ } catch {
1973
+ return jsonResponse({ error: "invalid_json" }, 400);
1974
+ }
1975
+ if (body === null || typeof body !== "object") {
1976
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
1977
+ }
1978
+ const obj = body;
1979
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
1980
+ return jsonResponse({ error: "missing_slug" }, 400);
1981
+ }
1982
+ if (typeof obj["timestamp"] !== "string" || obj["timestamp"] === "") {
1983
+ return jsonResponse({ error: "missing_timestamp" }, 400);
1984
+ }
1985
+ const slug = obj["slug"];
1986
+ const timestamp = obj["timestamp"];
1987
+ let snapshot;
1988
+ try {
1989
+ snapshot = await contentAdapter.readHistorySnapshot(slug, timestamp);
1990
+ } catch (err) {
1991
+ return jsonResponse(
1992
+ { error: "read_history_snapshot_failed", message: safeErrorMessage(err) },
1993
+ 500
1994
+ );
1995
+ }
1996
+ if (snapshot === null) {
1997
+ return jsonResponse({ error: "snapshot_not_found" }, 404);
1998
+ }
1999
+ try {
2000
+ await contentAdapter.saveDraft(snapshot);
2001
+ await runtime.publishDraft(slug);
2002
+ } catch (err) {
2003
+ return jsonResponse(
2004
+ { error: "rollback_failed", message: safeErrorMessage(err) },
2005
+ 500
2006
+ );
2007
+ }
2008
+ return jsonResponse({ ok: true }, 200);
2009
+ };
2010
+ const rename3 = async (req) => {
2011
+ let body;
2012
+ try {
2013
+ body = await req.json();
2014
+ } catch {
2015
+ return jsonResponse({ error: "invalid_json" }, 400);
2016
+ }
2017
+ if (body === null || typeof body !== "object") {
2018
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
2019
+ }
2020
+ const obj = body;
2021
+ if (typeof obj["fromSlug"] !== "string" || obj["fromSlug"] === "") {
2022
+ return jsonResponse({ error: "missing_from_slug" }, 400);
2023
+ }
2024
+ if (typeof obj["toSlug"] !== "string" || obj["toSlug"] === "") {
2025
+ return jsonResponse({ error: "missing_to_slug" }, 400);
2026
+ }
2027
+ const fromSlug = obj["fromSlug"];
2028
+ const toSlug = obj["toSlug"];
2029
+ const reservedToSlug = reservedSlugResponse(toSlug, "reserved_to_slug");
2030
+ if (reservedToSlug) return reservedToSlug;
2031
+ try {
2032
+ await contentAdapter.renamePage(fromSlug, toSlug);
2033
+ } catch (err) {
2034
+ const message = safeErrorMessage(err);
2035
+ if (message.includes("page not found")) {
2036
+ return jsonResponse({ error: message }, 404);
2037
+ }
2038
+ if (message.includes("target slug already exists")) {
2039
+ return jsonResponse({ error: message }, 409);
2040
+ }
2041
+ return jsonResponse({ error: message }, 500);
2042
+ }
2043
+ return jsonResponse({ ok: true }, 200);
2044
+ };
2045
+ const duplicate = async (req) => {
2046
+ let body;
2047
+ try {
2048
+ body = await req.json();
2049
+ } catch {
2050
+ return jsonResponse({ error: "invalid_json" }, 400);
2051
+ }
2052
+ if (body === null || typeof body !== "object") {
2053
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
2054
+ }
2055
+ const obj = body;
2056
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
2057
+ return jsonResponse({ error: "missing_slug" }, 400);
2058
+ }
2059
+ if (typeof obj["newSlug"] !== "string" || obj["newSlug"] === "") {
2060
+ return jsonResponse({ error: "missing_new_slug" }, 400);
2061
+ }
2062
+ const slug = obj["slug"];
2063
+ const newSlug = obj["newSlug"];
2064
+ if (!SLUG_PATTERN2.test(newSlug)) {
2065
+ return jsonResponse({ error: "invalid_new_slug", message: "Slug must contain only letters, numbers, hyphens, underscores, and forward slashes." }, 400);
2066
+ }
2067
+ const reservedNewSlug = reservedSlugResponse(newSlug, "reserved_new_slug");
2068
+ if (reservedNewSlug) return reservedNewSlug;
2069
+ if (newSlug === slug) {
2070
+ return jsonResponse({ error: "same_slug", message: "newSlug must differ from slug" }, 400);
2071
+ }
2072
+ const existingPublished = await contentAdapter.readPage(newSlug, "published");
2073
+ const existingDraft = await contentAdapter.readPage(newSlug, "draft");
2074
+ if (existingPublished !== null || existingDraft !== null) {
2075
+ return jsonResponse({ error: "slug_exists", message: `Slug "${newSlug}" is already in use.` }, 409);
2076
+ }
2077
+ const source = await contentAdapter.readPage(slug, "published") ?? await contentAdapter.readPage(slug, "draft");
2078
+ if (source === null) {
2079
+ return jsonResponse({ error: "source_not_found", message: `No page found for slug: ${slug}` }, 404);
2080
+ }
2081
+ const clonedSections = source.sections.map(cloneSection);
2082
+ const { sections: _srcSections, slug: _srcSlug, ...metaFromSource } = source;
2083
+ void _srcSections;
2084
+ void _srcSlug;
2085
+ const clone = {
2086
+ ...metaFromSource,
2087
+ slug: newSlug,
2088
+ sections: clonedSections
2089
+ };
2090
+ try {
2091
+ await contentAdapter.saveDraft(clone);
2092
+ } catch (err) {
2093
+ return jsonResponse(
2094
+ { error: "duplicate_failed", message: safeErrorMessage(err) },
2095
+ 500
2096
+ );
2097
+ }
2098
+ return jsonResponse({ ok: true, slug: newSlug }, 200);
2099
+ };
2100
+ return { list, read, deletePage, unpublish, listHistory, rollback, rename: rename3, duplicate };
2101
+ }
2102
+
2103
+ // src/handlers/globals/global-handler.ts
2104
+ var GLOBAL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
2105
+ function createGlobalHandler(deps) {
2106
+ const { contentAdapter } = deps;
2107
+ const list = async (_req) => {
2108
+ try {
2109
+ const globals = await contentAdapter.listGlobals();
2110
+ const serialized = globals.map((g) => ({
2111
+ name: g.name,
2112
+ type: g.type,
2113
+ updatedAt: g.updatedAt.toISOString()
2114
+ }));
2115
+ const body = { globals: serialized };
2116
+ if (deps.allowedTypes) {
2117
+ body["types"] = Array.from(deps.allowedTypes).sort();
2118
+ }
2119
+ return jsonResponse(body, 200);
2120
+ } catch (err) {
2121
+ return jsonResponse(
2122
+ { error: "list_globals_failed", message: safeErrorMessage(err) },
2123
+ 500
2124
+ );
2125
+ }
2126
+ };
2127
+ const read = async (req) => {
2128
+ const url = new URL(req.url);
2129
+ const name = url.searchParams.get("name");
2130
+ if (!name) {
2131
+ return jsonResponse({ error: "missing_name" }, 400);
2132
+ }
2133
+ if (!GLOBAL_NAME_PATTERN.test(name)) {
2134
+ return jsonResponse(
2135
+ { error: "invalid_name", message: "Global name must contain only letters, numbers, hyphens, and underscores" },
2136
+ 400
2137
+ );
2138
+ }
2139
+ try {
2140
+ const global = await contentAdapter.readGlobal(name);
2141
+ if (!global) {
2142
+ return jsonResponse({ error: "not_found", message: `Global "${name}" not found` }, 404);
2143
+ }
2144
+ return jsonResponse({ global: { name: global.name, type: global.type, data: global.data } }, 200);
2145
+ } catch (err) {
2146
+ return jsonResponse(
2147
+ { error: "read_global_failed", message: safeErrorMessage(err) },
2148
+ 500
2149
+ );
2150
+ }
2151
+ };
2152
+ const save = async (req) => {
2153
+ let body;
2154
+ try {
2155
+ body = await req.json();
2156
+ } catch {
2157
+ return jsonResponse({ error: "invalid_json" }, 400);
2158
+ }
2159
+ if (body === null || typeof body !== "object") {
2160
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
2161
+ }
2162
+ const obj = body;
2163
+ if (typeof obj["name"] !== "string" || obj["name"] === "") {
2164
+ return jsonResponse({ error: "missing_name" }, 400);
2165
+ }
2166
+ if (!GLOBAL_NAME_PATTERN.test(obj["name"])) {
2167
+ return jsonResponse({ error: "invalid_name", message: "Global name must contain only letters, numbers, hyphens, and underscores" }, 400);
2168
+ }
2169
+ if (typeof obj["type"] !== "string" || obj["type"] === "") {
2170
+ return jsonResponse({ error: "missing_type" }, 400);
2171
+ }
2172
+ if (deps.allowedTypes && !deps.allowedTypes.has(obj["type"])) {
2173
+ return jsonResponse(
2174
+ { error: "unknown_type", message: `Type "${obj["type"]}" is not registered` },
2175
+ 400
2176
+ );
2177
+ }
2178
+ if (obj["data"] === void 0 || obj["data"] === null || typeof obj["data"] !== "object" || Array.isArray(obj["data"])) {
2179
+ return jsonResponse({ error: "missing_data" }, 400);
2180
+ }
2181
+ let mergedData = obj["data"];
2182
+ if (deps.sectionDefaults) {
2183
+ const defaults = deps.sectionDefaults.get(obj["type"]);
2184
+ if (defaults) {
2185
+ const result = { ...defaults };
2186
+ for (const key of Object.keys(mergedData)) {
2187
+ result[key] = mergedData[key];
2188
+ }
2189
+ mergedData = result;
2190
+ }
2191
+ }
2192
+ try {
2193
+ await contentAdapter.saveGlobal({
2194
+ name: obj["name"],
2195
+ type: obj["type"],
2196
+ data: mergedData
2197
+ });
2198
+ return jsonResponse({ ok: true }, 200);
2199
+ } catch (err) {
2200
+ return jsonResponse(
2201
+ { error: "save_global_failed", message: safeErrorMessage(err) },
2202
+ 500
2203
+ );
2204
+ }
2205
+ };
2206
+ const deleteHandler = async (req) => {
2207
+ let body;
2208
+ try {
2209
+ body = await req.json();
2210
+ } catch {
2211
+ return jsonResponse({ error: "invalid_json" }, 400);
2212
+ }
2213
+ if (body === null || typeof body !== "object") {
2214
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
2215
+ }
2216
+ const obj = body;
2217
+ if (typeof obj["name"] !== "string" || obj["name"] === "") {
2218
+ return jsonResponse({ error: "missing_name" }, 400);
2219
+ }
2220
+ if (!GLOBAL_NAME_PATTERN.test(obj["name"])) {
2221
+ return jsonResponse({ error: "invalid_name", message: "Global name must contain only letters, numbers, hyphens, and underscores" }, 400);
2222
+ }
2223
+ try {
2224
+ await contentAdapter.deleteGlobal(obj["name"]);
2225
+ return jsonResponse({ ok: true }, 200);
2226
+ } catch (err) {
2227
+ const message = safeErrorMessage(err);
2228
+ if (message.includes("global not found")) {
2229
+ return jsonResponse({ error: message }, 404);
2230
+ }
2231
+ return jsonResponse(
2232
+ { error: "delete_global_failed", message },
2233
+ 500
2234
+ );
2235
+ }
2236
+ };
2237
+ const listHistory = async (req) => {
2238
+ const url = new URL(req.url);
2239
+ const name = url.searchParams.get("name");
2240
+ const timestamp = url.searchParams.get("ts");
2241
+ if (!name || name === "") {
2242
+ return jsonResponse({ error: "missing_name" }, 400);
2243
+ }
2244
+ if (!GLOBAL_NAME_PATTERN.test(name)) {
2245
+ return jsonResponse(
2246
+ { error: "invalid_name", message: "Global name must contain only letters, numbers, hyphens, and underscores" },
2247
+ 400
2248
+ );
2249
+ }
2250
+ if (timestamp !== null) {
2251
+ if (timestamp === "") {
2252
+ return jsonResponse({ error: "missing_timestamp" }, 400);
2253
+ }
2254
+ try {
2255
+ const snapshot = await contentAdapter.readGlobalHistorySnapshot(name, timestamp);
2256
+ if (snapshot === null) {
2257
+ return jsonResponse({ error: "not_found" }, 404);
2258
+ }
2259
+ return jsonResponse({ global: snapshot }, 200);
2260
+ } catch (err) {
2261
+ return jsonResponse(
2262
+ { error: "read_history_snapshot_failed", message: safeErrorMessage(err) },
2263
+ 500
2264
+ );
2265
+ }
2266
+ }
2267
+ try {
2268
+ const entries = await contentAdapter.listGlobalHistory(name);
2269
+ const projected = entries.map((e) => ({ timestamp: e.timestamp }));
2270
+ return jsonResponse({ entries: projected }, 200);
2271
+ } catch (err) {
2272
+ return jsonResponse(
2273
+ { error: "list_history_failed", message: safeErrorMessage(err) },
2274
+ 500
2275
+ );
2276
+ }
2277
+ };
2278
+ const rollback = async (req) => {
2279
+ let body;
2280
+ try {
2281
+ body = await req.json();
2282
+ } catch {
2283
+ return jsonResponse({ error: "invalid_json" }, 400);
2284
+ }
2285
+ if (body === null || typeof body !== "object") {
2286
+ return jsonResponse({ error: "invalid_body", message: "Expected a JSON object" }, 400);
2287
+ }
2288
+ const obj = body;
2289
+ if (typeof obj["name"] !== "string" || obj["name"] === "") {
2290
+ return jsonResponse({ error: "missing_name" }, 400);
2291
+ }
2292
+ if (!GLOBAL_NAME_PATTERN.test(obj["name"])) {
2293
+ return jsonResponse(
2294
+ { error: "invalid_name", message: "Global name must contain only letters, numbers, hyphens, and underscores" },
2295
+ 400
2296
+ );
2297
+ }
2298
+ if (typeof obj["timestamp"] !== "string" || obj["timestamp"] === "") {
2299
+ return jsonResponse({ error: "missing_timestamp" }, 400);
2300
+ }
2301
+ const name = obj["name"];
2302
+ const timestamp = obj["timestamp"];
2303
+ try {
2304
+ await contentAdapter.rollbackGlobal(name, timestamp);
2305
+ } catch (err) {
2306
+ const message = safeErrorMessage(err);
2307
+ if (message.includes("global history snapshot not found")) {
2308
+ return jsonResponse({ error: "not_found" }, 404);
2309
+ }
2310
+ return jsonResponse(
2311
+ { error: "rollback_failed", message },
2312
+ 500
2313
+ );
2314
+ }
2315
+ return jsonResponse({ ok: true }, 200);
2316
+ };
2317
+ return { list, read, save, delete: deleteHandler, listHistory, rollback };
2318
+ }
2319
+
2320
+ // src/handlers/template/template-handler.ts
2321
+ function createTemplateHandler(deps) {
2322
+ const { sectionDefaults, templates } = deps;
2323
+ const list = async (_req) => {
2324
+ const resolved = [];
2325
+ for (const template of templates) {
2326
+ const sections = [];
2327
+ for (const typeName of template.sectionTypes) {
2328
+ const defaults = sectionDefaults.get(typeName);
2329
+ if (!defaults) {
2330
+ console.warn(
2331
+ `[agntcms] Template "${template.name}": section type "${typeName}" not found in registered sections, skipping.`
2332
+ );
2333
+ continue;
2334
+ }
2335
+ sections.push({ type: typeName, data: defaults });
2336
+ }
2337
+ resolved.push({
2338
+ name: template.name,
2339
+ ...template.description !== void 0 ? { description: template.description } : {},
2340
+ sections
2341
+ });
2342
+ }
2343
+ return jsonResponse({ templates: resolved }, 200);
2344
+ };
2345
+ return { list };
2346
+ }
2347
+
2348
+ // src/handlers/forms/submit-handler.ts
2349
+ var extractIp = (req) => {
2350
+ const xff = req.headers.get("x-forwarded-for");
2351
+ if (xff) {
2352
+ const first = xff.split(",")[0]?.trim();
2353
+ if (first && first.length > 0) return first;
2354
+ }
2355
+ const xri = req.headers.get("x-real-ip");
2356
+ if (xri && xri.trim().length > 0) return xri.trim();
2357
+ return "unknown";
2358
+ };
2359
+ function createSubmitFormHandler(deps) {
2360
+ const { runtime, rateLimit } = deps;
2361
+ return async function handle(req) {
2362
+ let body;
2363
+ try {
2364
+ body = await req.json();
2365
+ } catch {
2366
+ return jsonResponse({ ok: false, error: "invalid_json" }, 400);
2367
+ }
2368
+ if (body === null || typeof body !== "object") {
2369
+ return jsonResponse({ ok: false, error: "invalid_body" }, 400);
2370
+ }
2371
+ const obj = body;
2372
+ if (typeof obj["formName"] !== "string" || obj["formName"] === "") {
2373
+ return jsonResponse({ ok: false, error: "missing_form_name" }, 400);
2374
+ }
2375
+ const payload = obj["payload"];
2376
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
2377
+ return jsonResponse({ ok: false, error: "invalid_payload" }, 400);
2378
+ }
2379
+ const formName = obj["formName"];
2380
+ const ip = extractIp(req);
2381
+ const rl = rateLimit.check(ip, formName);
2382
+ if (!rl.allowed) {
2383
+ return jsonResponse({ ok: false, error: "rate_limit" }, 429, {
2384
+ // Inform polite clients when they may retry. Spec-compliant for 429.
2385
+ "Retry-After": Math.max(1, Math.ceil((rl.resetAt - Date.now()) / 1e3)).toString()
2386
+ });
2387
+ }
2388
+ let result;
2389
+ try {
2390
+ result = await runtime.submitForm({
2391
+ formName,
2392
+ payload
2393
+ });
2394
+ } catch (err) {
2395
+ void safeErrorMessage(err);
2396
+ return jsonResponse({ ok: false, error: "storage_unavailable" }, 502);
2397
+ }
2398
+ if (!result.ok) {
2399
+ switch (result.error) {
2400
+ case "unknown_form":
2401
+ return jsonResponse({ ok: false, error: "unknown_form" }, 404);
2402
+ case "validation_failed":
2403
+ return jsonResponse(
2404
+ { ok: false, error: "validation_failed", errors: result.errors },
2405
+ 400
2406
+ );
2407
+ default: {
2408
+ const _exhaustive = result;
2409
+ void _exhaustive;
2410
+ return jsonResponse({ ok: false, error: "internal_error" }, 500);
2411
+ }
2412
+ }
2413
+ }
2414
+ if (!result.stored) {
2415
+ return jsonResponse({ ok: true, stored: false }, 200);
2416
+ }
2417
+ return jsonResponse({ ok: true, stored: true, id: result.id }, 200);
2418
+ };
2419
+ }
2420
+
2421
+ // src/handlers/forms/list-handler.ts
2422
+ function createFormsListHandler(deps) {
2423
+ const adapter = deps.adapter ?? { kind: "fs" };
2424
+ return async function handle(_req) {
2425
+ try {
2426
+ const forms = deps.forms.map((f) => {
2427
+ const schema = {};
2428
+ for (const [name, descriptor] of Object.entries(f.schema)) {
2429
+ schema[name] = descriptor;
2430
+ }
2431
+ return { name: f.name, schema };
2432
+ });
2433
+ return jsonResponse({ forms, adapter }, 200);
2434
+ } catch (err) {
2435
+ console.error("[agntcms] list_forms_failed:", err);
2436
+ return jsonResponse({ error: "list_forms_failed", message: "internal error" }, 500);
2437
+ }
2438
+ };
2439
+ }
2440
+
2441
+ // src/handlers/forms/read-handler.ts
2442
+ var FORM_NAME_PATTERN2 = /^[a-zA-Z0-9_-]+$/;
2443
+ var ID_PATTERN = /^[0-9A-HJKMNPQRSTVWXYZ]{16}$/;
2444
+ function createFormsReadHandler(deps) {
2445
+ return async function handle(req) {
2446
+ const url = new URL(req.url);
2447
+ const form = url.searchParams.get("form");
2448
+ const id = url.searchParams.get("id");
2449
+ if (!form) return jsonResponse({ error: "missing_form" }, 400);
2450
+ if (!FORM_NAME_PATTERN2.test(form)) {
2451
+ return jsonResponse({ error: "invalid_form" }, 400);
2452
+ }
2453
+ if (!deps.knownForms.has(form)) {
2454
+ return jsonResponse({ error: "unknown_form" }, 404);
2455
+ }
2456
+ if (id !== null) {
2457
+ if (!ID_PATTERN.test(id)) {
2458
+ return jsonResponse({ error: "invalid_id" }, 400);
2459
+ }
2460
+ try {
2461
+ const sub = await deps.submissionAdapter.read(form, id);
2462
+ if (!sub) return jsonResponse({ error: "not_found" }, 404);
2463
+ return jsonResponse({ submission: sub }, 200);
2464
+ } catch (err) {
2465
+ if (err instanceof SubmissionsNotReadableError) {
2466
+ return jsonResponse(
2467
+ { error: "not_supported", message: err.message },
2468
+ 501
2469
+ );
2470
+ }
2471
+ console.error("[agntcms] read_failed:", err);
2472
+ return jsonResponse({ error: "read_failed", message: "internal error" }, 500);
2473
+ }
2474
+ }
2475
+ try {
2476
+ const summaries = await deps.submissionAdapter.list(form);
2477
+ return jsonResponse({ submissions: summaries }, 200);
2478
+ } catch (err) {
2479
+ if (err instanceof SubmissionsNotReadableError) {
2480
+ return jsonResponse(
2481
+ { error: "not_supported", message: err.message },
2482
+ 501
2483
+ );
2484
+ }
2485
+ console.error("[agntcms] list_failed:", err);
2486
+ return jsonResponse({ error: "list_failed", message: "internal error" }, 500);
2487
+ }
2488
+ };
2489
+ }
2490
+
2491
+ // src/handlers/forms/delete-handler.ts
2492
+ function createFormsDeleteHandler() {
2493
+ return async function handle(_req) {
2494
+ return jsonResponse(
2495
+ {
2496
+ ok: false,
2497
+ error: "not_implemented",
2498
+ message: "Submission delete is reserved but not implemented in v1. See ARCHITECTURE.md \xA76.5 / \xA712."
2499
+ },
2500
+ 501
2501
+ );
2502
+ };
2503
+ }
2504
+
2505
+ // src/handlers.ts
2506
+ installDefaultAdapterFactories();
2507
+ // Annotate the CommonJS export names for ESM import in node:
2508
+ 0 && (module.exports = {
2509
+ AgentUnreachableError,
2510
+ createAgentBridge,
2511
+ createAssetsHandler,
2512
+ createDraftHandler,
2513
+ createEventsHandler,
2514
+ createFormsDeleteHandler,
2515
+ createFormsListHandler,
2516
+ createFormsReadHandler,
2517
+ createGlobalHandler,
2518
+ createMcpHandler,
2519
+ createPageHandler,
2520
+ createPreviewHandler,
2521
+ createPreviewTokenStore,
2522
+ createSubmitFormHandler,
2523
+ createTaskStore,
2524
+ createTemplateHandler
2525
+ });