@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,1970 @@
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/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ BooleanField: () => BooleanField,
34
+ ButtonField: () => ButtonField,
35
+ DuplicateFormNameError: () => DuplicateFormNameError,
36
+ FormOverridesField: () => FormOverridesField,
37
+ GlobalSlot: () => GlobalSlot,
38
+ HoneypotCollisionError: () => HoneypotCollisionError,
39
+ ImageField: () => ImageField,
40
+ InvalidFormFieldError: () => InvalidFormFieldError,
41
+ InvalidFormNameError: () => InvalidFormNameError,
42
+ LinkField: () => LinkField,
43
+ ListField: () => ListField,
44
+ NOT_FOUND_PAGE_SLUG: () => NOT_FOUND_PAGE_SLUG,
45
+ NumberField: () => NumberField,
46
+ ReferenceField: () => ReferenceField,
47
+ RichTextField: () => RichTextField,
48
+ SERVER_ERROR_PAGE_SLUG: () => SERVER_ERROR_PAGE_SLUG,
49
+ SelectField: () => SelectField,
50
+ SubmissionsNotReadableError: () => SubmissionsNotReadableError,
51
+ TextField: () => TextField,
52
+ VideoField: () => VideoField,
53
+ buildFormRegistry: () => buildFormRegistry,
54
+ createDefaultAssetAdapter: () => createDefaultAssetAdapter,
55
+ createDefaultContentAdapter: () => createDefaultContentAdapter,
56
+ createDefaultSubmissionAdapter: () => createDefaultSubmissionAdapter,
57
+ createFsAssetAdapter: () => createFsAssetAdapter,
58
+ createFsContentAdapter: () => createFsContentAdapter,
59
+ createFsSubmissionAdapter: () => createFsSubmissionAdapter,
60
+ createListPages: () => createListPages,
61
+ createRateLimit: () => createRateLimit,
62
+ createRuntime: () => createRuntime,
63
+ createSubmitForm: () => createSubmitForm,
64
+ createWebhookSubmissionAdapter: () => createWebhookSubmissionAdapter,
65
+ defineForm: () => defineForm,
66
+ defineSection: () => defineSection,
67
+ getReservedPageSlugViolation: () => getReservedPageSlugViolation,
68
+ hasUniqueSectionIds: () => hasUniqueSectionIds,
69
+ hrefOf: () => hrefOf,
70
+ isExternalLink: () => isExternalLink,
71
+ isSitemapEligibleSlug: () => isSitemapEligibleSlug,
72
+ linkAnchorAttrs: () => linkAnchorAttrs,
73
+ normalizeLinkValue: () => normalizeLinkValue,
74
+ validateEmail: () => validateEmail,
75
+ validateExternalUrl: () => validateExternalUrl,
76
+ validateInternalSlug: () => validateInternalSlug,
77
+ validatePhone: () => validatePhone
78
+ });
79
+ module.exports = __toCommonJS(server_exports);
80
+
81
+ // src/storage/fs/assets.ts
82
+ var import_node_crypto = require("crypto");
83
+ var fs2 = __toESM(require("fs/promises"), 1);
84
+ var path2 = __toESM(require("path"), 1);
85
+
86
+ // src/storage/fs/_helpers.ts
87
+ var fs = __toESM(require("fs/promises"), 1);
88
+ var path = __toESM(require("path"), 1);
89
+ var isEnoent = (err) => typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
90
+ var defaultRandomSuffix = () => Math.random().toString(36).slice(2);
91
+ var writeAtomic = async (target, data, randomSuffix = defaultRandomSuffix) => {
92
+ const dir = path.dirname(target);
93
+ await fs.mkdir(dir, { recursive: true });
94
+ const tmp = path.join(
95
+ dir,
96
+ `.${path.basename(target)}.${process.pid}.${randomSuffix()}.tmp`
97
+ );
98
+ try {
99
+ await fs.writeFile(tmp, data);
100
+ await fs.rename(tmp, target);
101
+ } catch (err) {
102
+ await fs.unlink(tmp).catch(() => {
103
+ });
104
+ throw err;
105
+ }
106
+ };
107
+ var resolveUnderBucket = (bucket, key, errorLabel, errorKey = key) => {
108
+ const resolved = path.resolve(bucket, key);
109
+ const bucketWithSep = bucket.endsWith(path.sep) ? bucket : bucket + path.sep;
110
+ if (!resolved.startsWith(bucketWithSep)) {
111
+ throw new Error(`${errorLabel}: ${JSON.stringify(errorKey)}`);
112
+ }
113
+ return resolved;
114
+ };
115
+
116
+ // src/storage/fs/assets.ts
117
+ var EXT_PATTERN = /^\.[a-z0-9_-]{1,16}$/;
118
+ var JSON_SUFFIX = ".json";
119
+ var extensionFrom = (filename) => {
120
+ const dot = filename.lastIndexOf(".");
121
+ if (dot < 0 || dot === filename.length - 1) return "";
122
+ const raw = filename.slice(dot).toLowerCase();
123
+ return EXT_PATTERN.test(raw) ? raw : "";
124
+ };
125
+ var CONTENT_TYPE_BY_EXT = {
126
+ ".png": "image/png",
127
+ ".jpg": "image/jpeg",
128
+ ".jpeg": "image/jpeg",
129
+ ".gif": "image/gif",
130
+ ".webp": "image/webp",
131
+ ".svg": "image/svg+xml",
132
+ ".avif": "image/avif"
133
+ };
134
+ var contentTypeFor = (filename) => {
135
+ const dot = filename.lastIndexOf(".");
136
+ if (dot < 0) return void 0;
137
+ const ext = filename.slice(dot).toLowerCase();
138
+ return CONTENT_TYPE_BY_EXT[ext];
139
+ };
140
+ var createFsAssetAdapter = (options) => {
141
+ const { assetsRoot, publicUrlBase } = options;
142
+ if (!path2.isAbsolute(assetsRoot)) {
143
+ throw new Error(
144
+ `assetsRoot must be an absolute path, got: ${JSON.stringify(assetsRoot)}`
145
+ );
146
+ }
147
+ if (publicUrlBase.endsWith("/")) {
148
+ throw new Error(
149
+ `publicUrlBase must not end with '/', got: ${JSON.stringify(publicUrlBase)}`
150
+ );
151
+ }
152
+ const rootResolved = path2.resolve(assetsRoot);
153
+ const rootWithSep = rootResolved.endsWith(path2.sep) ? rootResolved : rootResolved + path2.sep;
154
+ const upload = async (input) => {
155
+ const hash = (0, import_node_crypto.createHash)("sha256").update(input.bytes).digest("hex");
156
+ const ext = extensionFrom(input.filename);
157
+ const storedName = `${hash}${ext}`;
158
+ const target = path2.resolve(rootResolved, storedName);
159
+ if (!target.startsWith(rootWithSep)) {
160
+ throw new Error(`asset path escapes assetsRoot: ${JSON.stringify(target)}`);
161
+ }
162
+ let bytesExist = false;
163
+ try {
164
+ await fs2.stat(target);
165
+ bytesExist = true;
166
+ } catch (err) {
167
+ if (!isEnoent(err)) throw err;
168
+ }
169
+ if (!bytesExist) {
170
+ await writeAtomic(target, input.bytes);
171
+ }
172
+ return {
173
+ url: `${publicUrlBase}/${storedName}`,
174
+ filename: storedName
175
+ };
176
+ };
177
+ const list = async () => {
178
+ let names;
179
+ try {
180
+ names = await fs2.readdir(rootResolved);
181
+ } catch (err) {
182
+ if (isEnoent(err)) return [];
183
+ throw err;
184
+ }
185
+ const candidates = names.filter(
186
+ (n) => !n.startsWith(".") && !n.endsWith(JSON_SUFFIX)
187
+ );
188
+ const entries = [];
189
+ for (const filename of candidates) {
190
+ const full = path2.join(rootResolved, filename);
191
+ let stat3;
192
+ try {
193
+ stat3 = await fs2.stat(full);
194
+ } catch (err) {
195
+ if (isEnoent(err)) continue;
196
+ throw err;
197
+ }
198
+ if (!stat3.isFile()) continue;
199
+ entries.push({
200
+ filename,
201
+ url: `${publicUrlBase}/${filename}`,
202
+ contentType: contentTypeFor(filename),
203
+ modifiedAt: stat3.mtime
204
+ });
205
+ }
206
+ entries.sort((a, b) => {
207
+ const delta = b.modifiedAt.getTime() - a.modifiedAt.getTime();
208
+ if (delta !== 0) return delta;
209
+ return a.filename.localeCompare(b.filename);
210
+ });
211
+ return entries;
212
+ };
213
+ return { upload, list };
214
+ };
215
+
216
+ // src/storage/fs/content.ts
217
+ var import_node_fs = require("fs");
218
+ var fs3 = __toESM(require("fs/promises"), 1);
219
+ var path3 = __toESM(require("path"), 1);
220
+
221
+ // src/domain/page.ts
222
+ function assertValidPageMeta(obj) {
223
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
224
+ throw new Error("invalid page: missing or empty slug");
225
+ }
226
+ const slug = obj["slug"];
227
+ const rawSeo = obj["seo"];
228
+ if (rawSeo === null || typeof rawSeo !== "object") {
229
+ throw new Error(`invalid page "${slug}": seo must be an object`);
230
+ }
231
+ const seo = rawSeo;
232
+ if (typeof seo["title"] !== "string" || seo["title"].trim() === "") {
233
+ throw new Error(
234
+ `invalid page "${slug}": seo.title must be a non-empty string`
235
+ );
236
+ }
237
+ if (typeof seo["description"] !== "string" || seo["description"].trim() === "") {
238
+ throw new Error(
239
+ `invalid page "${slug}": seo.description must be a non-empty string`
240
+ );
241
+ }
242
+ if (seo["canonical"] !== void 0) {
243
+ if (typeof seo["canonical"] !== "string" || seo["canonical"].trim() === "") {
244
+ throw new Error(
245
+ `invalid page "${slug}": seo.canonical, when present, must be a non-empty string`
246
+ );
247
+ }
248
+ }
249
+ }
250
+ function assertValidPage(page) {
251
+ if (page === null || typeof page !== "object") {
252
+ throw new Error("invalid page: expected an object");
253
+ }
254
+ const obj = page;
255
+ if (typeof obj["slug"] !== "string" || obj["slug"] === "") {
256
+ throw new Error("invalid page: missing or empty slug");
257
+ }
258
+ const slug = obj["slug"];
259
+ if (!Array.isArray(obj["sections"])) {
260
+ throw new Error(`invalid page "${slug}": sections must be an array`);
261
+ }
262
+ assertValidPageMeta(obj);
263
+ }
264
+ function assertValidPageSummary(summary) {
265
+ if (summary === null || typeof summary !== "object") {
266
+ throw new Error("invalid page summary: expected an object");
267
+ }
268
+ assertValidPageMeta(summary);
269
+ }
270
+
271
+ // src/domain/fields.ts
272
+ var TextField = { kind: "text" };
273
+ var RichTextField = { kind: "richText" };
274
+ var ImageField = { kind: "image" };
275
+ var VideoField = { kind: "video" };
276
+ var ReferenceField = { kind: "reference" };
277
+ var LinkField = { kind: "link" };
278
+ var NumberField = { kind: "number" };
279
+ var BooleanField = { kind: "boolean" };
280
+ var SelectField = (options, opts) => ({
281
+ kind: "select",
282
+ options,
283
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
284
+ });
285
+ var ButtonField = (variants, opts) => ({
286
+ kind: "button",
287
+ variants,
288
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
289
+ });
290
+ var ListField = (itemSchema, opts) => ({
291
+ kind: "list",
292
+ itemSchema,
293
+ ...opts?.min !== void 0 ? { min: opts.min } : {},
294
+ ...opts?.max !== void 0 ? { max: opts.max } : {},
295
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
296
+ });
297
+
298
+ // src/domain/invariants.ts
299
+ function hasUniqueSectionIds(page) {
300
+ const seen = /* @__PURE__ */ new Set();
301
+ for (const section of page.sections) {
302
+ if (seen.has(section.id)) return false;
303
+ seen.add(section.id);
304
+ }
305
+ return true;
306
+ }
307
+
308
+ // src/domain/linkValidation.ts
309
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-/]*[a-z0-9])?$/;
310
+ var validateInternalSlug = (slug) => {
311
+ if (slug === "") return null;
312
+ if (/^https?:\/\//i.test(slug)) return "slug must not include a protocol";
313
+ if (slug.startsWith("//")) return "slug must not start with //";
314
+ if (slug.startsWith("/")) return "slug must not start with /";
315
+ if (slug.endsWith("/")) return "slug must not end with /";
316
+ if (slug.includes("//")) return "slug must not contain //";
317
+ if (slug.includes("..")) return "slug must not contain ..";
318
+ if (!SLUG_PATTERN.test(slug)) {
319
+ return "slug must use lowercase letters, digits, hyphens, and slashes only";
320
+ }
321
+ return null;
322
+ };
323
+ var validateExternalUrl = (url) => {
324
+ if (url === "") return null;
325
+ if (typeof url !== "string") return "URL must be a string";
326
+ if (!/^https?:\/\//i.test(url)) {
327
+ return "External link must start with http:// or https://";
328
+ }
329
+ try {
330
+ const parsed = new globalThis.URL(url);
331
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
332
+ return "External link must use http or https";
333
+ }
334
+ } catch {
335
+ return "External link is not a valid URL";
336
+ }
337
+ return null;
338
+ };
339
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
340
+ var validateEmail = (email) => {
341
+ if (email === "") return null;
342
+ if (typeof email !== "string") return "Invalid email address";
343
+ if (!EMAIL_PATTERN.test(email)) return "Invalid email address";
344
+ return null;
345
+ };
346
+ var validatePhone = (phone) => {
347
+ if (phone === "") return null;
348
+ if (typeof phone !== "string") return "Phone number is too short";
349
+ const digits = phone.replace(/[^\d+]/g, "").replace(/\+/g, "");
350
+ if (digits.length < 7) return "Phone number is too short";
351
+ return null;
352
+ };
353
+
354
+ // src/domain/link.ts
355
+ function hrefOf(link) {
356
+ if (link.type === "external") return link.url;
357
+ if (link.type === "email") {
358
+ if (link.email === "") return "";
359
+ return "mailto:" + link.email;
360
+ }
361
+ if (link.type === "phone") {
362
+ if (link.phone === "") return "";
363
+ return "tel:" + link.phone.replace(/[^\d+]/g, "");
364
+ }
365
+ const slug = link.slug.startsWith("/") ? link.slug.slice(1) : link.slug;
366
+ if (slug === "" || slug === "home") return "/";
367
+ return "/" + slug;
368
+ }
369
+ function isExternalLink(link) {
370
+ return link.type === "external";
371
+ }
372
+ function linkAnchorAttrs(link) {
373
+ const href = hrefOf(link);
374
+ if (link.type === "external") {
375
+ return { href, target: "_blank", rel: "noreferrer" };
376
+ }
377
+ return { href };
378
+ }
379
+ function normalizeLinkValue(raw) {
380
+ if (raw === null || typeof raw !== "object") {
381
+ return { type: "internal", slug: "", label: "" };
382
+ }
383
+ const obj = raw;
384
+ const label = typeof obj["label"] === "string" ? obj["label"] : "";
385
+ if (obj["type"] === "internal") {
386
+ const slug = typeof obj["slug"] === "string" ? obj["slug"] : "";
387
+ return { type: "internal", slug, label };
388
+ }
389
+ if (obj["type"] === "external") {
390
+ const url = typeof obj["url"] === "string" ? obj["url"] : "";
391
+ return { type: "external", url, label };
392
+ }
393
+ if (obj["type"] === "email") {
394
+ const email = typeof obj["email"] === "string" ? obj["email"] : "";
395
+ return { type: "email", email, label };
396
+ }
397
+ if (obj["type"] === "phone") {
398
+ const phone = typeof obj["phone"] === "string" ? obj["phone"] : "";
399
+ return { type: "phone", phone, label };
400
+ }
401
+ if (typeof obj["href"] === "string") {
402
+ const href = obj["href"];
403
+ if (/^mailto:/i.test(href)) {
404
+ return { type: "email", email: href.slice("mailto:".length), label };
405
+ }
406
+ if (/^tel:/i.test(href)) {
407
+ return { type: "phone", phone: href.slice("tel:".length), label };
408
+ }
409
+ if (/^https?:\/\//i.test(href)) {
410
+ return { type: "external", url: href, label };
411
+ }
412
+ const slug = href.startsWith("/") ? href.slice(1) : href;
413
+ return { type: "internal", slug, label };
414
+ }
415
+ return { type: "internal", slug: "", label: "" };
416
+ }
417
+
418
+ // src/domain/form.ts
419
+ var FORM_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
420
+ "image",
421
+ "video",
422
+ "reference",
423
+ "list",
424
+ // `formOverrides` is a section-only descriptor (it overrides another
425
+ // form schema instance). Putting it inside a form would mean a form's
426
+ // payload could carry overrides for itself or another form — a
427
+ // recursive shape with no ergonomic editor UI. Section-only by design.
428
+ "formOverrides",
429
+ // `button` is a section-only descriptor: a styled CTA with an
430
+ // optional link. Public-form payloads collect user input — a button
431
+ // value is authored content, not a submitted answer. Section-only
432
+ // by design (mirrors `formOverrides`).
433
+ "button"
434
+ ]);
435
+ var SubmissionsNotReadableError = class extends Error {
436
+ constructor(message = "submission adapter does not support reading") {
437
+ super(message);
438
+ this.name = "SubmissionsNotReadableError";
439
+ }
440
+ };
441
+
442
+ // src/domain/formOverrides.ts
443
+ var FormOverridesField = (formName, opts) => ({
444
+ kind: "formOverrides",
445
+ formName,
446
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
447
+ });
448
+
449
+ // src/storage/fs/content.ts
450
+ var SLUG_PATTERN2 = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/;
451
+ var assertValidSlug = (slug) => {
452
+ if (!SLUG_PATTERN2.test(slug)) {
453
+ throw new Error(`invalid slug: ${JSON.stringify(slug)}`);
454
+ }
455
+ };
456
+ var deepEqual = (a, b) => {
457
+ if (a === b) return true;
458
+ if (typeof a !== typeof b) return false;
459
+ if (a === null || b === null) return false;
460
+ if (typeof a !== "object") return false;
461
+ const aIsArr = Array.isArray(a);
462
+ const bIsArr = Array.isArray(b);
463
+ if (aIsArr !== bIsArr) return false;
464
+ if (aIsArr) {
465
+ const arrA = a;
466
+ const arrB = b;
467
+ if (arrA.length !== arrB.length) return false;
468
+ for (let i = 0; i < arrA.length; i += 1) {
469
+ if (!deepEqual(arrA[i], arrB[i])) return false;
470
+ }
471
+ return true;
472
+ }
473
+ const objA = a;
474
+ const objB = b;
475
+ const keysA = Object.keys(objA);
476
+ const keysB = Object.keys(objB);
477
+ if (keysA.length !== keysB.length) return false;
478
+ for (const key of keysA) {
479
+ if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
480
+ if (!deepEqual(objA[key], objB[key])) return false;
481
+ }
482
+ return true;
483
+ };
484
+ var createFsContentAdapter = (options) => {
485
+ const { contentRoot } = options;
486
+ if (!path3.isAbsolute(contentRoot)) {
487
+ throw new Error(
488
+ `contentRoot must be an absolute path, got: ${JSON.stringify(contentRoot)}`
489
+ );
490
+ }
491
+ const pagesDir = path3.resolve(contentRoot, "pages");
492
+ const draftsDir = path3.resolve(contentRoot, "drafts");
493
+ const historyDir = path3.resolve(contentRoot, "history");
494
+ const globalsDir = path3.resolve(contentRoot, "globals");
495
+ const globalsHistoryDir = path3.resolve(contentRoot, "history-globals");
496
+ const resolveSlugPath = (bucket, slug, ext) => {
497
+ assertValidSlug(slug);
498
+ return resolveUnderBucket(bucket, `${slug}${ext}`, "slug escapes storage bucket", slug);
499
+ };
500
+ const readJsonOrNull = async (filePath) => {
501
+ try {
502
+ const raw = await fs3.readFile(filePath, { encoding: "utf8" });
503
+ return JSON.parse(raw);
504
+ } catch (err) {
505
+ if (isEnoent(err)) return null;
506
+ throw err;
507
+ }
508
+ };
509
+ const listJsonFiles2 = async (dir) => {
510
+ let entries;
511
+ try {
512
+ entries = await fs3.readdir(dir, { withFileTypes: true });
513
+ } catch (err) {
514
+ if (isEnoent(err)) return [];
515
+ throw err;
516
+ }
517
+ const results = [];
518
+ for (const entry of entries) {
519
+ if (entry.isFile() && entry.name.endsWith(".json")) {
520
+ results.push(entry.name);
521
+ } else if (entry.isDirectory()) {
522
+ const subDir = path3.join(dir, entry.name);
523
+ const subFiles = await listJsonFiles2(subDir);
524
+ for (const sub of subFiles) {
525
+ results.push(`${entry.name}/${sub}`);
526
+ }
527
+ }
528
+ }
529
+ return results;
530
+ };
531
+ const bucketFor = (mode) => mode === "published" ? pagesDir : draftsDir;
532
+ const uniqueTimestampedPath = async (baseDir, key, errorLabel) => {
533
+ const keyDir = resolveUnderBucket(baseDir, key, errorLabel);
534
+ await fs3.mkdir(keyDir, { recursive: true });
535
+ const base = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
536
+ let candidate = path3.join(keyDir, `${base}.json`);
537
+ let suffix = 0;
538
+ while (true) {
539
+ try {
540
+ await fs3.access(candidate, import_node_fs.constants.F_OK);
541
+ suffix += 1;
542
+ candidate = path3.join(keyDir, `${base}-${suffix}.json`);
543
+ } catch (err) {
544
+ if (isEnoent(err)) return candidate;
545
+ throw err;
546
+ }
547
+ }
548
+ };
549
+ const uniqueHistoryPath = async (slug) => {
550
+ assertValidSlug(slug);
551
+ return uniqueTimestampedPath(historyDir, slug, "slug escapes history bucket");
552
+ };
553
+ const readPage = async (slug, mode) => {
554
+ const filePath = resolveSlugPath(bucketFor(mode), slug, ".json");
555
+ return readJsonOrNull(filePath);
556
+ };
557
+ const saveDraft = async (page) => {
558
+ const filePath = resolveSlugPath(draftsDir, page.slug, ".json");
559
+ await writeAtomic(filePath, JSON.stringify(page, null, 2));
560
+ };
561
+ const listDrafts = async () => {
562
+ const files = await listJsonFiles2(draftsDir);
563
+ const results = [];
564
+ for (const relPath of files) {
565
+ const slug = relPath.slice(0, -".json".length);
566
+ const stat3 = await fs3.stat(path3.join(draftsDir, relPath));
567
+ results.push({ slug, updatedAt: stat3.mtime });
568
+ }
569
+ return results;
570
+ };
571
+ const listPages = async () => {
572
+ const files = await listJsonFiles2(pagesDir);
573
+ const results = [];
574
+ for (const relPath of files) {
575
+ const slug = relPath.slice(0, -".json".length);
576
+ const stat3 = await fs3.stat(path3.join(pagesDir, relPath));
577
+ results.push({ slug, updatedAt: stat3.mtime });
578
+ }
579
+ return results;
580
+ };
581
+ const listPageSummaries = async () => {
582
+ const files = await listJsonFiles2(pagesDir);
583
+ const results = [];
584
+ for (const relPath of files) {
585
+ const slug = relPath.slice(0, -".json".length);
586
+ const filePath = path3.join(pagesDir, relPath);
587
+ const page = await readJsonOrNull(filePath);
588
+ if (page === null) continue;
589
+ const stat3 = await fs3.stat(filePath).catch(() => null);
590
+ if (stat3 === null) continue;
591
+ const { sections: _sections, ...meta } = page;
592
+ void _sections;
593
+ results.push({ ...meta, slug, updatedAt: stat3.mtime });
594
+ }
595
+ return results;
596
+ };
597
+ const readLatestSnapshotIn = async (baseDir, key, errorLabel) => {
598
+ const keyDir = resolveUnderBucket(baseDir, key, errorLabel);
599
+ let entries;
600
+ try {
601
+ entries = await fs3.readdir(keyDir, { withFileTypes: true });
602
+ } catch (err) {
603
+ if (isEnoent(err)) return null;
604
+ throw err;
605
+ }
606
+ let latest = null;
607
+ for (const entry of entries) {
608
+ if (!entry.isFile()) continue;
609
+ if (!entry.name.endsWith(".json")) continue;
610
+ if (latest === null || entry.name > latest) latest = entry.name;
611
+ }
612
+ if (latest === null) return null;
613
+ return readJsonOrNull(path3.join(keyDir, latest));
614
+ };
615
+ const readLatestHistorySnapshot = async (slug) => {
616
+ assertValidSlug(slug);
617
+ return readLatestSnapshotIn(historyDir, slug, "slug escapes history bucket");
618
+ };
619
+ const publishDraft = async (slug) => {
620
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
621
+ const pagePath = resolveSlugPath(pagesDir, slug, ".json");
622
+ const page = await readJsonOrNull(draftPath);
623
+ if (page === null) {
624
+ throw new Error(`no draft to publish for slug: ${JSON.stringify(slug)}`);
625
+ }
626
+ assertValidPage(page);
627
+ const serialised = JSON.stringify(page, null, 2);
628
+ const latestSnapshot = await readLatestHistorySnapshot(slug);
629
+ const skipHistory = latestSnapshot !== null && deepEqual(latestSnapshot, page);
630
+ if (!skipHistory) {
631
+ const historyPath = await uniqueHistoryPath(slug);
632
+ await writeAtomic(historyPath, serialised);
633
+ }
634
+ await writeAtomic(pagePath, serialised);
635
+ try {
636
+ await fs3.unlink(draftPath);
637
+ } catch (err) {
638
+ if (!isEnoent(err)) throw err;
639
+ }
640
+ return page;
641
+ };
642
+ const deleteDraft = async (slug) => {
643
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
644
+ try {
645
+ await fs3.unlink(draftPath);
646
+ } catch (err) {
647
+ if (isEnoent(err)) {
648
+ throw new Error(`no draft to discard for slug: ${JSON.stringify(slug)}`);
649
+ }
650
+ throw err;
651
+ }
652
+ };
653
+ const deletePage = async (slug) => {
654
+ const publishedPath = resolveSlugPath(pagesDir, slug, ".json");
655
+ try {
656
+ await fs3.access(publishedPath);
657
+ } catch {
658
+ throw new Error(`page not found: ${slug}`);
659
+ }
660
+ await fs3.unlink(publishedPath);
661
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
662
+ try {
663
+ await fs3.unlink(draftPath);
664
+ } catch {
665
+ }
666
+ };
667
+ const unpublishPage = async (slug) => {
668
+ const publishedPath = resolveSlugPath(pagesDir, slug, ".json");
669
+ const draftPath = resolveSlugPath(draftsDir, slug, ".json");
670
+ const published = await readJsonOrNull(publishedPath);
671
+ if (published === null) {
672
+ throw new Error(`page not found: ${slug}`);
673
+ }
674
+ const existingDraft = await readJsonOrNull(draftPath);
675
+ if (existingDraft === null) {
676
+ await writeAtomic(draftPath, JSON.stringify(published, null, 2));
677
+ }
678
+ await fs3.unlink(publishedPath);
679
+ };
680
+ const listHistory = async (slug) => {
681
+ assertValidSlug(slug);
682
+ const slugDir = resolveUnderBucket(historyDir, slug, "slug escapes history bucket");
683
+ let entries;
684
+ try {
685
+ entries = await fs3.readdir(slugDir, { withFileTypes: true });
686
+ } catch (err) {
687
+ if (isEnoent(err)) return [];
688
+ throw err;
689
+ }
690
+ const results = [];
691
+ for (const entry of entries) {
692
+ if (!entry.isFile()) continue;
693
+ if (!entry.name.endsWith(".json")) continue;
694
+ const timestamp = entry.name.slice(0, -".json".length);
695
+ const stat3 = await fs3.stat(path3.join(slugDir, entry.name));
696
+ results.push({ slug, timestamp, size: stat3.size });
697
+ }
698
+ results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
699
+ return results;
700
+ };
701
+ const readHistorySnapshot = async (slug, timestamp) => {
702
+ assertValidSlug(slug);
703
+ const slugDir = resolveUnderBucket(historyDir, slug, "slug escapes history bucket");
704
+ const filePath = resolveUnderBucket(
705
+ slugDir,
706
+ `${timestamp}.json`,
707
+ "timestamp escapes history directory",
708
+ timestamp
709
+ );
710
+ return readJsonOrNull(filePath);
711
+ };
712
+ const renamePage = async (fromSlug, toSlug) => {
713
+ assertValidSlug(fromSlug);
714
+ assertValidSlug(toSlug);
715
+ const fromPublished = resolveSlugPath(pagesDir, fromSlug, ".json");
716
+ const toPublished = resolveSlugPath(pagesDir, toSlug, ".json");
717
+ try {
718
+ await fs3.access(fromPublished);
719
+ } catch {
720
+ throw new Error(`page not found: ${fromSlug}`);
721
+ }
722
+ try {
723
+ await fs3.access(toPublished);
724
+ throw new Error(`target slug already exists: ${toSlug}`);
725
+ } catch (err) {
726
+ if (isEnoent(err)) {
727
+ } else {
728
+ throw err;
729
+ }
730
+ }
731
+ await fs3.rename(fromPublished, toPublished);
732
+ const fromDraft = resolveSlugPath(draftsDir, fromSlug, ".json");
733
+ const toDraft = resolveSlugPath(draftsDir, toSlug, ".json");
734
+ try {
735
+ await fs3.rename(fromDraft, toDraft);
736
+ } catch (err) {
737
+ if (!isEnoent(err)) throw err;
738
+ }
739
+ const fromHistory = path3.resolve(historyDir, fromSlug);
740
+ const toHistory = path3.resolve(historyDir, toSlug);
741
+ try {
742
+ await fs3.rename(fromHistory, toHistory);
743
+ } catch (err) {
744
+ if (!isEnoent(err)) throw err;
745
+ }
746
+ };
747
+ const readGlobal = async (name) => {
748
+ const filePath = resolveSlugPath(globalsDir, name, ".json");
749
+ return readJsonOrNull(filePath);
750
+ };
751
+ const uniqueGlobalHistoryPath = async (name) => {
752
+ assertValidSlug(name);
753
+ return uniqueTimestampedPath(
754
+ globalsHistoryDir,
755
+ name,
756
+ "name escapes globals history bucket"
757
+ );
758
+ };
759
+ const readLatestGlobalHistorySnapshot = async (name) => {
760
+ assertValidSlug(name);
761
+ return readLatestSnapshotIn(
762
+ globalsHistoryDir,
763
+ name,
764
+ "name escapes globals history bucket"
765
+ );
766
+ };
767
+ const saveGlobal = async (global) => {
768
+ const filePath = resolveSlugPath(globalsDir, global.name, ".json");
769
+ const serialised = JSON.stringify(global, null, 2);
770
+ const latestSnapshot = await readLatestGlobalHistorySnapshot(global.name);
771
+ const skipHistory = latestSnapshot !== null && deepEqual(latestSnapshot, global);
772
+ if (!skipHistory) {
773
+ const historyPath = await uniqueGlobalHistoryPath(global.name);
774
+ await writeAtomic(historyPath, serialised);
775
+ }
776
+ await writeAtomic(filePath, serialised);
777
+ };
778
+ const listGlobals = async () => {
779
+ let entries;
780
+ try {
781
+ entries = await fs3.readdir(globalsDir, { withFileTypes: true });
782
+ } catch (err) {
783
+ if (isEnoent(err)) return [];
784
+ throw err;
785
+ }
786
+ const results = [];
787
+ for (const entry of entries) {
788
+ if (!entry.isFile()) continue;
789
+ if (!entry.name.endsWith(".json")) continue;
790
+ const filePath = path3.join(globalsDir, entry.name);
791
+ const json = await readJsonOrNull(filePath);
792
+ if (json === null) continue;
793
+ const stat3 = await fs3.stat(filePath).catch(() => null);
794
+ if (stat3 === null) continue;
795
+ results.push({ name: json.name, type: json.type, updatedAt: stat3.mtime });
796
+ }
797
+ return results;
798
+ };
799
+ const deleteGlobal = async (name) => {
800
+ const filePath = resolveSlugPath(globalsDir, name, ".json");
801
+ try {
802
+ await fs3.unlink(filePath);
803
+ } catch (err) {
804
+ if (isEnoent(err)) {
805
+ throw new Error(`global not found: ${name}`);
806
+ }
807
+ throw err;
808
+ }
809
+ };
810
+ const listGlobalHistory = async (name) => {
811
+ assertValidSlug(name);
812
+ const nameDir = resolveUnderBucket(
813
+ globalsHistoryDir,
814
+ name,
815
+ "name escapes globals history bucket"
816
+ );
817
+ let entries;
818
+ try {
819
+ entries = await fs3.readdir(nameDir, { withFileTypes: true });
820
+ } catch (err) {
821
+ if (isEnoent(err)) return [];
822
+ throw err;
823
+ }
824
+ const results = [];
825
+ for (const entry of entries) {
826
+ if (!entry.isFile()) continue;
827
+ if (!entry.name.endsWith(".json")) continue;
828
+ const timestamp = entry.name.slice(0, -".json".length);
829
+ const stat3 = await fs3.stat(path3.join(nameDir, entry.name));
830
+ results.push({ name, timestamp, size: stat3.size });
831
+ }
832
+ results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
833
+ return results;
834
+ };
835
+ const readGlobalHistorySnapshot = async (name, timestamp) => {
836
+ assertValidSlug(name);
837
+ const nameDir = resolveUnderBucket(
838
+ globalsHistoryDir,
839
+ name,
840
+ "name escapes globals history bucket"
841
+ );
842
+ const filePath = resolveUnderBucket(
843
+ nameDir,
844
+ `${timestamp}.json`,
845
+ "timestamp escapes history directory",
846
+ timestamp
847
+ );
848
+ return readJsonOrNull(filePath);
849
+ };
850
+ const rollbackGlobal = async (name, timestamp) => {
851
+ const snapshot = await readGlobalHistorySnapshot(name, timestamp);
852
+ if (snapshot === null) {
853
+ throw new Error(`global history snapshot not found: ${name} @ ${timestamp}`);
854
+ }
855
+ await saveGlobal(snapshot);
856
+ };
857
+ return {
858
+ readPage,
859
+ saveDraft,
860
+ listDrafts,
861
+ listPages,
862
+ listPageSummaries,
863
+ publishDraft,
864
+ deleteDraft,
865
+ deletePage,
866
+ unpublishPage,
867
+ listHistory,
868
+ readHistorySnapshot,
869
+ renamePage,
870
+ readGlobal,
871
+ saveGlobal,
872
+ listGlobals,
873
+ deleteGlobal,
874
+ listGlobalHistory,
875
+ readGlobalHistorySnapshot,
876
+ rollbackGlobal
877
+ };
878
+ };
879
+
880
+ // src/storage/fs/submissions.ts
881
+ var import_node_crypto2 = require("crypto");
882
+ var fs4 = __toESM(require("fs/promises"), 1);
883
+ var path4 = __toESM(require("path"), 1);
884
+ var DEFAULT_MAX_LIST = 500;
885
+ var FORM_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
886
+ var parseSubmission = (raw) => {
887
+ let parsed;
888
+ try {
889
+ parsed = JSON.parse(raw);
890
+ } catch {
891
+ return null;
892
+ }
893
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
894
+ return null;
895
+ }
896
+ const obj = parsed;
897
+ if (typeof obj["formName"] !== "string") return null;
898
+ if (typeof obj["id"] !== "string") return null;
899
+ if (typeof obj["submittedAt"] !== "string") return null;
900
+ if (obj["payload"] === null || typeof obj["payload"] !== "object" || Array.isArray(obj["payload"])) {
901
+ return null;
902
+ }
903
+ return {
904
+ formName: obj["formName"],
905
+ id: obj["id"],
906
+ submittedAt: obj["submittedAt"],
907
+ payload: obj["payload"]
908
+ };
909
+ };
910
+ var assertValidFormName = (name) => {
911
+ if (typeof name !== "string" || !FORM_NAME_PATTERN.test(name)) {
912
+ throw new Error(`invalid form name: ${JSON.stringify(name)}`);
913
+ }
914
+ };
915
+ var submissionFilename = (submittedAt, id) => {
916
+ const ts = submittedAt.replace(/:/g, "-");
917
+ return `${ts}-${id}.json`;
918
+ };
919
+ var listJsonFiles = async (dir) => {
920
+ let entries;
921
+ try {
922
+ entries = await fs4.readdir(dir, { withFileTypes: true });
923
+ } catch (err) {
924
+ if (isEnoent(err)) return [];
925
+ throw err;
926
+ }
927
+ const results = [];
928
+ for (const entry of entries) {
929
+ if (entry.isFile() && entry.name.endsWith(".json")) {
930
+ results.push(entry.name);
931
+ }
932
+ }
933
+ return results;
934
+ };
935
+ var createFsSubmissionAdapter = (options) => {
936
+ const { submissionsRoot } = options;
937
+ if (!path4.isAbsolute(submissionsRoot)) {
938
+ throw new Error(
939
+ `submissionsRoot must be an absolute path, got: ${JSON.stringify(submissionsRoot)}`
940
+ );
941
+ }
942
+ const maxList = options.maxList ?? DEFAULT_MAX_LIST;
943
+ if (!Number.isFinite(maxList) || maxList <= 0 || !Number.isInteger(maxList)) {
944
+ throw new Error(
945
+ `maxList must be a positive integer, got: ${maxList}`
946
+ );
947
+ }
948
+ const rootResolved = path4.resolve(submissionsRoot);
949
+ const rootWithSep = rootResolved.endsWith(path4.sep) ? rootResolved : rootResolved + path4.sep;
950
+ const formDir = (formName) => {
951
+ assertValidFormName(formName);
952
+ const dir = path4.resolve(rootResolved, formName);
953
+ if (!dir.startsWith(rootWithSep)) {
954
+ throw new Error(`form name escapes submissions root: ${JSON.stringify(formName)}`);
955
+ }
956
+ return dir;
957
+ };
958
+ const cryptoSuffix = () => (0, import_node_crypto2.randomUUID)();
959
+ const store = async (submission) => {
960
+ const dir = formDir(submission.formName);
961
+ const filename = submissionFilename(submission.submittedAt, submission.id);
962
+ if (filename.includes("/") || filename.includes("\\")) {
963
+ throw new Error(`submission id contains path separator: ${JSON.stringify(submission.id)}`);
964
+ }
965
+ const target = path4.join(dir, filename);
966
+ await writeAtomic(target, JSON.stringify(submission, null, 2), cryptoSuffix);
967
+ };
968
+ const list = async (formName) => {
969
+ const dir = formDir(formName);
970
+ const files = await listJsonFiles(dir);
971
+ const summaries = [];
972
+ for (const filename of files) {
973
+ const stem = filename.slice(0, -".json".length);
974
+ const lastDash = stem.lastIndexOf("-");
975
+ if (lastDash < 0) continue;
976
+ const tsRaw = stem.slice(0, lastDash);
977
+ const id = stem.slice(lastDash + 1);
978
+ if (id === "") continue;
979
+ const tIndex = tsRaw.indexOf("T");
980
+ if (tIndex < 0) continue;
981
+ const datePart = tsRaw.slice(0, tIndex);
982
+ const timePart = tsRaw.slice(tIndex + 1).replace(/-/g, ":");
983
+ const submittedAt = `${datePart}T${timePart}`;
984
+ summaries.push({ id, submittedAt });
985
+ }
986
+ summaries.sort((a, b) => {
987
+ if (a.submittedAt !== b.submittedAt) {
988
+ return a.submittedAt < b.submittedAt ? 1 : -1;
989
+ }
990
+ if (a.id !== b.id) {
991
+ return a.id < b.id ? 1 : -1;
992
+ }
993
+ return 0;
994
+ });
995
+ return summaries.length > maxList ? summaries.slice(0, maxList) : summaries;
996
+ };
997
+ const read = async (formName, id) => {
998
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) {
999
+ return null;
1000
+ }
1001
+ const dir = formDir(formName);
1002
+ let entries;
1003
+ try {
1004
+ entries = (await fs4.readdir(dir)).filter((n) => n.endsWith(".json"));
1005
+ } catch (err) {
1006
+ if (isEnoent(err)) return null;
1007
+ throw err;
1008
+ }
1009
+ const suffix = `-${id}.json`;
1010
+ const found = entries.find((n) => n.endsWith(suffix));
1011
+ if (!found) return null;
1012
+ const target = path4.join(dir, found);
1013
+ let raw;
1014
+ try {
1015
+ raw = await fs4.readFile(target, { encoding: "utf8" });
1016
+ } catch (err) {
1017
+ if (isEnoent(err)) return null;
1018
+ throw err;
1019
+ }
1020
+ return parseSubmission(raw);
1021
+ };
1022
+ return { info: { kind: "fs" }, store, list, read };
1023
+ };
1024
+
1025
+ // src/config/defaults-registry.ts
1026
+ var SLOT = /* @__PURE__ */ Symbol.for("@agntcms/next/default-adapter-factories");
1027
+ function holder() {
1028
+ return globalThis;
1029
+ }
1030
+ function registerDefaultAdapterFactories(factories) {
1031
+ holder()[SLOT] = factories;
1032
+ }
1033
+
1034
+ // src/config/defaults.ts
1035
+ var path5 = __toESM(require("path"), 1);
1036
+ function createDefaultContentAdapter(options) {
1037
+ const root = options?.projectRoot ?? process.cwd();
1038
+ return createFsContentAdapter({
1039
+ contentRoot: path5.resolve(root, "content")
1040
+ });
1041
+ }
1042
+ function createDefaultAssetAdapter(options) {
1043
+ const root = options?.projectRoot ?? process.cwd();
1044
+ return createFsAssetAdapter({
1045
+ assetsRoot: path5.resolve(root, "public/assets"),
1046
+ publicUrlBase: "/assets"
1047
+ });
1048
+ }
1049
+ function createDefaultSubmissionAdapter(options) {
1050
+ const root = options?.projectRoot ?? process.cwd();
1051
+ return createFsSubmissionAdapter({
1052
+ submissionsRoot: path5.resolve(root, "content/submissions")
1053
+ });
1054
+ }
1055
+ function installDefaultAdapterFactories() {
1056
+ const factories = {
1057
+ content: createDefaultContentAdapter,
1058
+ asset: createDefaultAssetAdapter,
1059
+ submission: createDefaultSubmissionAdapter
1060
+ };
1061
+ registerDefaultAdapterFactories(factories);
1062
+ }
1063
+
1064
+ // src/runtime/getContent.ts
1065
+ var import_node_crypto5 = require("crypto");
1066
+
1067
+ // src/runtime/getGlobal.ts
1068
+ var import_node_crypto3 = require("crypto");
1069
+ var computeRevision = (global) => {
1070
+ const serialized = JSON.stringify(global);
1071
+ return (0, import_node_crypto3.createHash)("sha256").update(serialized).digest("hex");
1072
+ };
1073
+ var wrapGlobalData = (global, revision) => {
1074
+ const data = global.data;
1075
+ const wrapped = {};
1076
+ for (const key of Object.keys(data)) {
1077
+ const origin = {
1078
+ kind: "global",
1079
+ globalName: global.name,
1080
+ // Sentinel values: keep the shape stable for editable widgets that
1081
+ // already read these props. The `__global__:` prefix surfaces the
1082
+ // origin in any debug logging without ambiguity vs. real slugs.
1083
+ pageSlug: `__global__:${global.name}`,
1084
+ sectionId: global.name,
1085
+ fieldPath: key,
1086
+ // Globals have no draft/publish cycle in v1. Treat every preview
1087
+ // read as a draft view — saves overwrite the live global directly.
1088
+ source: "draft",
1089
+ revision
1090
+ };
1091
+ wrapped[key] = {
1092
+ __agntcmsPreview: true,
1093
+ value: data[key],
1094
+ origin
1095
+ };
1096
+ }
1097
+ return wrapped;
1098
+ };
1099
+ function createGetGlobal(contentAdapter) {
1100
+ return async (input) => {
1101
+ const { name, mode } = input;
1102
+ if (mode === "published") {
1103
+ return contentAdapter.readGlobal(name);
1104
+ }
1105
+ const global = await contentAdapter.readGlobal(name);
1106
+ if (global === null) return null;
1107
+ const revision = computeRevision(global);
1108
+ const wrappedData = wrapGlobalData(global, revision);
1109
+ return {
1110
+ name: global.name,
1111
+ type: global.type,
1112
+ data: wrappedData
1113
+ };
1114
+ };
1115
+ }
1116
+
1117
+ // src/runtime/listPages.ts
1118
+ var toPublicSummary = (entry) => {
1119
+ const result = { slug: entry.slug, seo: entry.seo };
1120
+ if (entry.tags !== void 0) result.tags = entry.tags;
1121
+ if (entry.excerpt !== void 0) result.excerpt = entry.excerpt;
1122
+ if (entry.coverImage !== void 0) result.coverImage = entry.coverImage;
1123
+ if (entry.publishedAt !== void 0) result.publishedAt = entry.publishedAt;
1124
+ return result;
1125
+ };
1126
+ var compareEntries = (a, b, direction) => {
1127
+ const aHasPublished = a.publishedAt !== void 0;
1128
+ const bHasPublished = b.publishedAt !== void 0;
1129
+ let primary;
1130
+ if (aHasPublished && bHasPublished) {
1131
+ primary = a.publishedAt.localeCompare(b.publishedAt);
1132
+ } else if (aHasPublished && !bHasPublished) {
1133
+ primary = 1;
1134
+ } else if (!aHasPublished && bHasPublished) {
1135
+ primary = -1;
1136
+ } else {
1137
+ primary = a.updatedAt.getTime() - b.updatedAt.getTime();
1138
+ }
1139
+ if (primary !== 0) {
1140
+ return direction === "newest" ? -primary : primary;
1141
+ }
1142
+ return a.slug.localeCompare(b.slug);
1143
+ };
1144
+ var createListPages = ({
1145
+ contentAdapter
1146
+ }) => {
1147
+ return async (input) => {
1148
+ const tag = input?.tag;
1149
+ const limit = input?.limit;
1150
+ const sort = input?.sort ?? "newest";
1151
+ if (limit !== void 0 && limit <= 0) return [];
1152
+ const all = await contentAdapter.listPageSummaries();
1153
+ for (const entry of all) {
1154
+ assertValidPageSummary(entry);
1155
+ }
1156
+ const filtered = tag === void 0 ? all : all.filter(
1157
+ (entry) => (
1158
+ // Tag matching is case-sensitive exact (ARCHITECTURE.md §4).
1159
+ entry.tags !== void 0 && entry.tags.includes(tag)
1160
+ )
1161
+ );
1162
+ const sorted = [...filtered].sort((a, b) => compareEntries(a, b, sort));
1163
+ const capped = limit === void 0 ? sorted : sorted.slice(0, limit);
1164
+ return capped.map(toPublicSummary);
1165
+ };
1166
+ };
1167
+
1168
+ // src/runtime/submitForm.ts
1169
+ var import_node_crypto4 = require("crypto");
1170
+ var CROCKFORD32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1171
+ var defaultGenerateId = () => {
1172
+ const bytes = (0, import_node_crypto4.randomBytes)(16);
1173
+ let out = "";
1174
+ for (let i = 0; i < 16; i++) {
1175
+ out += CROCKFORD32[bytes[i] & 31];
1176
+ }
1177
+ return out;
1178
+ };
1179
+ var validateNonEmptyString = (value) => {
1180
+ if (typeof value !== "string") return "must be a string";
1181
+ if (value.trim() === "") return "must not be empty";
1182
+ return null;
1183
+ };
1184
+ var validateNumber = (value, descriptor) => {
1185
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1186
+ return "must be a finite number";
1187
+ }
1188
+ if (descriptor.min !== void 0 && value < descriptor.min) {
1189
+ return `must be >= ${descriptor.min}`;
1190
+ }
1191
+ if (descriptor.max !== void 0 && value > descriptor.max) {
1192
+ return `must be <= ${descriptor.max}`;
1193
+ }
1194
+ return null;
1195
+ };
1196
+ var validateBoolean = (value) => {
1197
+ if (typeof value !== "boolean") return "must be true or false";
1198
+ return null;
1199
+ };
1200
+ var validateSelect = (value, descriptor) => {
1201
+ if (typeof value !== "string") return "must be a string";
1202
+ if (!descriptor.options.some((opt) => opt.value === value)) {
1203
+ return "must be one of the declared options";
1204
+ }
1205
+ return null;
1206
+ };
1207
+ var validateLinkPayload = (value) => {
1208
+ if (value === null || typeof value !== "object") return "must be a link object";
1209
+ const obj = value;
1210
+ if (typeof obj["label"] !== "string") return "label must be a string";
1211
+ if (obj["type"] === "internal") {
1212
+ const slug = obj["slug"];
1213
+ if (typeof slug !== "string") return "slug must be a string";
1214
+ if (slug.trim() === "") return "slug must be a non-empty string";
1215
+ return validateInternalSlug(slug);
1216
+ }
1217
+ if (obj["type"] === "external") {
1218
+ const url = obj["url"];
1219
+ if (typeof url !== "string") return "url must be a string";
1220
+ if (url.trim() === "") return "url must be a non-empty string";
1221
+ return validateExternalUrl(url);
1222
+ }
1223
+ if (obj["type"] === "email") {
1224
+ const email = obj["email"];
1225
+ if (typeof email !== "string") return "email must be a string";
1226
+ if (email.trim() === "") return "email must be a non-empty string";
1227
+ return validateEmail(email);
1228
+ }
1229
+ if (obj["type"] === "phone") {
1230
+ const phone = obj["phone"];
1231
+ if (typeof phone !== "string") return "phone must be a string";
1232
+ if (phone.trim() === "") return "phone must be a non-empty string";
1233
+ return validatePhone(phone);
1234
+ }
1235
+ return 'type must be "internal", "external", "email", or "phone"';
1236
+ };
1237
+ var validateAndNormalisePayload = (schema, payload) => {
1238
+ const errors = {};
1239
+ const normalised = {};
1240
+ for (const [fieldName, descriptor] of Object.entries(schema)) {
1241
+ if (!Object.prototype.hasOwnProperty.call(payload, fieldName)) {
1242
+ errors[fieldName] = "is required";
1243
+ continue;
1244
+ }
1245
+ const value = payload[fieldName];
1246
+ let err = null;
1247
+ switch (descriptor.kind) {
1248
+ case "text":
1249
+ case "richText": {
1250
+ err = validateNonEmptyString(value);
1251
+ if (!err) {
1252
+ normalised[fieldName] = value.trim();
1253
+ }
1254
+ break;
1255
+ }
1256
+ case "number": {
1257
+ err = validateNumber(value, descriptor);
1258
+ if (!err) normalised[fieldName] = value;
1259
+ break;
1260
+ }
1261
+ case "boolean": {
1262
+ err = validateBoolean(value);
1263
+ if (!err) normalised[fieldName] = value;
1264
+ break;
1265
+ }
1266
+ case "select": {
1267
+ err = validateSelect(value, descriptor);
1268
+ if (!err) normalised[fieldName] = value;
1269
+ break;
1270
+ }
1271
+ case "link": {
1272
+ err = validateLinkPayload(value);
1273
+ if (!err) {
1274
+ const linkObj = value;
1275
+ const label = typeof linkObj["label"] === "string" ? linkObj["label"] : "";
1276
+ const normalisedLink = linkObj["type"] === "external" ? { type: "external", url: linkObj["url"], label } : linkObj["type"] === "email" ? { type: "email", email: linkObj["email"], label } : linkObj["type"] === "phone" ? { type: "phone", phone: linkObj["phone"], label } : { type: "internal", slug: linkObj["slug"], label };
1277
+ normalised[fieldName] = normalisedLink;
1278
+ }
1279
+ break;
1280
+ }
1281
+ default: {
1282
+ const _exhaustive = descriptor;
1283
+ void _exhaustive;
1284
+ err = "unsupported field type";
1285
+ }
1286
+ }
1287
+ if (err) errors[fieldName] = err;
1288
+ }
1289
+ if (Object.keys(errors).length > 0) return { ok: false, errors };
1290
+ return { ok: true, payload: normalised };
1291
+ };
1292
+ function createSubmitForm(deps) {
1293
+ const { forms, submissionAdapter } = deps;
1294
+ const generateId = deps.generateId ?? defaultGenerateId;
1295
+ const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1296
+ return async function submitForm(input) {
1297
+ const def = forms.get(input.formName);
1298
+ if (!def) {
1299
+ return { ok: false, error: "unknown_form" };
1300
+ }
1301
+ if (def.honeypot !== void 0) {
1302
+ const trapValue = input.payload[def.honeypot];
1303
+ if (typeof trapValue === "string" && trapValue.length > 0) {
1304
+ return { ok: true, stored: false, suppressed: "honeypot" };
1305
+ }
1306
+ }
1307
+ const validated = validateAndNormalisePayload(def.schema, input.payload);
1308
+ if (!validated.ok) {
1309
+ return { ok: false, error: "validation_failed", errors: validated.errors };
1310
+ }
1311
+ const submission = {
1312
+ formName: def.name,
1313
+ payload: validated.payload,
1314
+ submittedAt: now(),
1315
+ id: generateId()
1316
+ };
1317
+ await submissionAdapter.store(submission);
1318
+ return { ok: true, stored: true, id: submission.id };
1319
+ };
1320
+ }
1321
+
1322
+ // src/runtime/getContent.ts
1323
+ var isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
1324
+ var looksLikeLink = (obj) => {
1325
+ if (obj["type"] === "internal" || obj["type"] === "external") return true;
1326
+ return typeof obj["href"] === "string" && typeof obj["label"] === "string" && obj["_id"] === void 0;
1327
+ };
1328
+ var normalizeLinks = (value) => {
1329
+ if (Array.isArray(value)) {
1330
+ let changed = false;
1331
+ const next = value.map((entry) => {
1332
+ const normalised = normalizeLinks(entry);
1333
+ if (normalised !== entry) changed = true;
1334
+ return normalised;
1335
+ });
1336
+ return changed ? next : value;
1337
+ }
1338
+ if (isPlainObject(value)) {
1339
+ if (looksLikeLink(value)) {
1340
+ return normalizeLinkValue(value);
1341
+ }
1342
+ let changed = false;
1343
+ const next = {};
1344
+ for (const key of Object.keys(value)) {
1345
+ const normalised = normalizeLinks(value[key]);
1346
+ if (normalised !== value[key]) changed = true;
1347
+ next[key] = normalised;
1348
+ }
1349
+ return changed ? next : value;
1350
+ }
1351
+ return value;
1352
+ };
1353
+ var normalizeLinksInSections = (sections) => {
1354
+ let changed = false;
1355
+ const next = sections.map((section) => {
1356
+ const normalisedData = normalizeLinks(section.data);
1357
+ if (normalisedData === section.data) return section;
1358
+ changed = true;
1359
+ return {
1360
+ id: section.id,
1361
+ type: section.type,
1362
+ data: normalisedData,
1363
+ ...section.globalRef !== void 0 ? { globalRef: section.globalRef } : {}
1364
+ };
1365
+ });
1366
+ return changed ? next : sections;
1367
+ };
1368
+ var computeRevision2 = (page) => {
1369
+ const serialized = JSON.stringify(page);
1370
+ return (0, import_node_crypto5.createHash)("sha256").update(serialized).digest("hex");
1371
+ };
1372
+ var withSections = (page, sections) => ({
1373
+ ...page,
1374
+ sections
1375
+ });
1376
+ var wrapSectionData = (section, pageSlug, source, revision) => {
1377
+ const data = section.data;
1378
+ const wrapped = {};
1379
+ for (const key of Object.keys(data)) {
1380
+ const origin = {
1381
+ pageSlug,
1382
+ sectionId: section.id,
1383
+ fieldPath: key,
1384
+ source,
1385
+ revision
1386
+ };
1387
+ wrapped[key] = {
1388
+ __agntcmsPreview: true,
1389
+ value: data[key],
1390
+ origin
1391
+ };
1392
+ }
1393
+ return wrapped;
1394
+ };
1395
+ var resolveGlobalRefs = async (sections, contentAdapter) => {
1396
+ if (!sections.some((s) => s.globalRef !== void 0)) return sections;
1397
+ const resolved = await Promise.all(
1398
+ sections.map(async (section) => {
1399
+ if (section.globalRef === void 0) return section;
1400
+ const global = await contentAdapter.readGlobal(section.globalRef);
1401
+ if (global === null) {
1402
+ console.warn(
1403
+ `[agntcms] global "${section.globalRef}" referenced by section "${section.id}" not found \u2014 keeping section as-is`
1404
+ );
1405
+ return section;
1406
+ }
1407
+ return {
1408
+ id: section.id,
1409
+ type: global.type,
1410
+ data: global.data,
1411
+ globalRef: section.globalRef
1412
+ };
1413
+ })
1414
+ );
1415
+ return resolved;
1416
+ };
1417
+ var isInvalidSlugError = (err) => err instanceof Error && err.message.startsWith("invalid slug:");
1418
+ function createRuntime(options) {
1419
+ const { contentAdapter } = options;
1420
+ const getContent = async (input) => {
1421
+ const { slug, mode } = input;
1422
+ if (mode === "published") {
1423
+ let page2;
1424
+ try {
1425
+ page2 = await contentAdapter.readPage(slug, "published");
1426
+ } catch (err) {
1427
+ if (isInvalidSlugError(err)) return null;
1428
+ throw err;
1429
+ }
1430
+ if (page2 === null) return null;
1431
+ assertValidPage(page2);
1432
+ const resolved = await resolveGlobalRefs(page2.sections, contentAdapter);
1433
+ const sections = normalizeLinksInSections(resolved);
1434
+ if (sections === page2.sections) return page2;
1435
+ return withSections(page2, sections);
1436
+ }
1437
+ let page;
1438
+ let source = "draft";
1439
+ try {
1440
+ page = await contentAdapter.readPage(slug, "draft");
1441
+ } catch (err) {
1442
+ if (isInvalidSlugError(err)) return null;
1443
+ throw err;
1444
+ }
1445
+ if (page === null) {
1446
+ try {
1447
+ page = await contentAdapter.readPage(slug, "published");
1448
+ } catch (err) {
1449
+ if (isInvalidSlugError(err)) return null;
1450
+ throw err;
1451
+ }
1452
+ source = "published";
1453
+ }
1454
+ if (page === null) {
1455
+ return null;
1456
+ }
1457
+ assertValidPage(page);
1458
+ const resolvedSections = await resolveGlobalRefs(page.sections, contentAdapter);
1459
+ const normalisedSections = normalizeLinksInSections(resolvedSections);
1460
+ const revision = computeRevision2(withSections(page, normalisedSections));
1461
+ const wrappedSections = normalisedSections.map((section) => ({
1462
+ id: section.id,
1463
+ type: section.type,
1464
+ data: wrapSectionData(section, slug, source, revision),
1465
+ // Preserve globalRef so the UI knows this section is backed by a global
1466
+ // and can redirect saves to the global endpoint.
1467
+ ...section.globalRef !== void 0 ? { globalRef: section.globalRef } : {}
1468
+ }));
1469
+ return withSections(page, wrappedSections);
1470
+ };
1471
+ const publishDraft = async (slug) => {
1472
+ const draft = await contentAdapter.readPage(slug, "draft");
1473
+ if (draft === null) {
1474
+ throw new Error(`no draft to publish for slug: ${JSON.stringify(slug)}`);
1475
+ }
1476
+ assertValidPage(draft);
1477
+ return contentAdapter.publishDraft(slug);
1478
+ };
1479
+ const getGlobal = createGetGlobal(contentAdapter);
1480
+ const listPages = createListPages({ contentAdapter });
1481
+ const noopAdapter = {
1482
+ info: { kind: "fs" },
1483
+ store: async () => {
1484
+ throw new Error("submissionAdapter not configured; pass one to createRuntime");
1485
+ },
1486
+ list: async () => [],
1487
+ read: async () => null
1488
+ };
1489
+ const submitForm = createSubmitForm({
1490
+ forms: options.forms ?? emptyFormRegistry,
1491
+ submissionAdapter: options.submissionAdapter ?? noopAdapter
1492
+ });
1493
+ return { getContent, publishDraft, getGlobal, submitForm, listPages };
1494
+ }
1495
+ var emptyFormRegistry = Object.freeze({
1496
+ definitions: [],
1497
+ get: () => void 0,
1498
+ has: () => false
1499
+ });
1500
+
1501
+ // src/runtime/rateLimit.ts
1502
+ var DEFAULT_MAX_BUCKETS = 1e4;
1503
+ function createRateLimit(options) {
1504
+ const { perWindow, windowMs } = options;
1505
+ if (!Number.isFinite(perWindow) || perWindow <= 0) {
1506
+ throw new Error(`perWindow must be a positive number, got: ${perWindow}`);
1507
+ }
1508
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
1509
+ throw new Error(`windowMs must be a positive number, got: ${windowMs}`);
1510
+ }
1511
+ const maxBuckets = options.maxBuckets ?? DEFAULT_MAX_BUCKETS;
1512
+ if (!Number.isFinite(maxBuckets) || maxBuckets <= 0 || !Number.isInteger(maxBuckets)) {
1513
+ throw new Error(`maxBuckets must be a positive integer, got: ${maxBuckets}`);
1514
+ }
1515
+ const now = options.now ?? Date.now;
1516
+ const buckets = /* @__PURE__ */ new Map();
1517
+ const keyOf = (ip, formName) => `${ip}:${formName}`;
1518
+ const sweepExpired = (t) => {
1519
+ for (const [k, b] of buckets) {
1520
+ if (t >= b.resetAt) buckets.delete(k);
1521
+ }
1522
+ };
1523
+ const evictOldest = () => {
1524
+ let oldestKey = null;
1525
+ let oldestResetAt = Number.POSITIVE_INFINITY;
1526
+ for (const [k, b] of buckets) {
1527
+ if (b.resetAt < oldestResetAt) {
1528
+ oldestResetAt = b.resetAt;
1529
+ oldestKey = k;
1530
+ }
1531
+ }
1532
+ if (oldestKey !== null) buckets.delete(oldestKey);
1533
+ };
1534
+ return {
1535
+ check: (ip, formName) => {
1536
+ const key = keyOf(ip, formName);
1537
+ const t = now();
1538
+ const existing = buckets.get(key);
1539
+ if (!existing || t >= existing.resetAt) {
1540
+ if (!existing && buckets.size >= maxBuckets) {
1541
+ sweepExpired(t);
1542
+ while (buckets.size >= maxBuckets) {
1543
+ evictOldest();
1544
+ }
1545
+ }
1546
+ const bucket = { count: 1, resetAt: t + windowMs };
1547
+ buckets.set(key, bucket);
1548
+ return { allowed: true, count: 1, resetAt: bucket.resetAt };
1549
+ }
1550
+ existing.count += 1;
1551
+ const allowed = existing.count <= perWindow;
1552
+ return { allowed, count: existing.count, resetAt: existing.resetAt };
1553
+ },
1554
+ reset: () => {
1555
+ buckets.clear();
1556
+ }
1557
+ };
1558
+ }
1559
+
1560
+ // src/runtime/systemPages.ts
1561
+ var NOT_FOUND_PAGE_SLUG = "404";
1562
+ var SERVER_ERROR_PAGE_SLUG = "500";
1563
+ var SITEMAP_EXCLUDED_TERMINAL_SLUGS = /* @__PURE__ */ new Set([
1564
+ NOT_FOUND_PAGE_SLUG,
1565
+ SERVER_ERROR_PAGE_SLUG,
1566
+ "not-found",
1567
+ "_not-found"
1568
+ ]);
1569
+ var SYSTEM_PAGE_ALIAS_TO_CANONICAL = /* @__PURE__ */ new Map([
1570
+ ["not-found", NOT_FOUND_PAGE_SLUG],
1571
+ ["_not-found", NOT_FOUND_PAGE_SLUG],
1572
+ ["error-404", NOT_FOUND_PAGE_SLUG],
1573
+ ["404-page", NOT_FOUND_PAGE_SLUG],
1574
+ ["page-not-found", NOT_FOUND_PAGE_SLUG],
1575
+ ["error-500", SERVER_ERROR_PAGE_SLUG],
1576
+ ["500-page", SERVER_ERROR_PAGE_SLUG],
1577
+ ["server-error", SERVER_ERROR_PAGE_SLUG]
1578
+ ]);
1579
+ var getReservedPageSlugViolation = (slug) => {
1580
+ const canonicalSlug = SYSTEM_PAGE_ALIAS_TO_CANONICAL.get(slug);
1581
+ if (!canonicalSlug) return null;
1582
+ const pageLabel = canonicalSlug === NOT_FOUND_PAGE_SLUG ? "404 page" : "500 page";
1583
+ return {
1584
+ slug,
1585
+ canonicalSlug,
1586
+ message: `Slug "${slug}" is reserved as an alias for the ${pageLabel}. Edit the canonical "${canonicalSlug}" page instead.`
1587
+ };
1588
+ };
1589
+ var isSitemapEligibleSlug = (slug) => {
1590
+ const terminal = slug.split("/").at(-1);
1591
+ if (!terminal) return false;
1592
+ return !SITEMAP_EXCLUDED_TERMINAL_SLUGS.has(terminal);
1593
+ };
1594
+
1595
+ // src/storage/webhook/submissions.ts
1596
+ var URL_PATTERN = /^https?:\/\//;
1597
+ var DEFAULT_TIMEOUT_MS = 1e4;
1598
+ var createWebhookSubmissionAdapter = (options) => {
1599
+ if (typeof options.url !== "string" || !URL_PATTERN.test(options.url)) {
1600
+ throw new Error(
1601
+ `webhook url must start with http:// or https://, got: ${JSON.stringify(options.url)}`
1602
+ );
1603
+ }
1604
+ let parsedHost;
1605
+ try {
1606
+ parsedHost = new URL(options.url).host;
1607
+ } catch {
1608
+ throw new Error(
1609
+ `webhook url is not a valid URL: ${JSON.stringify(options.url)}`
1610
+ );
1611
+ }
1612
+ if (options.url.startsWith("http://") && options.headers !== void 0 && Object.keys(options.headers).length > 0) {
1613
+ console.warn(
1614
+ "[agntcms] webhook url uses http:// with custom headers \u2014 secrets will be transmitted in plaintext"
1615
+ );
1616
+ }
1617
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1618
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
1619
+ throw new Error(
1620
+ `webhook timeoutMs must be a positive integer (ms), got: ${timeoutMs}`
1621
+ );
1622
+ }
1623
+ const doFetch = options.fetch ?? ((url, init) => (
1624
+ // The DOM/Node fetch types differ; cast to the minimal shape we declared.
1625
+ // Using `globalThis.fetch` keeps this neutral across Node, Edge, and
1626
+ // jsdom test environments.
1627
+ globalThis.fetch(url, init)
1628
+ ));
1629
+ const baseHeaders = {
1630
+ "Content-Type": "application/json",
1631
+ ...options.headers
1632
+ };
1633
+ const store = async (submission) => {
1634
+ const controller = new AbortController();
1635
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1636
+ let res;
1637
+ try {
1638
+ res = await doFetch(options.url, {
1639
+ method: "POST",
1640
+ headers: baseHeaders,
1641
+ body: JSON.stringify(submission),
1642
+ signal: controller.signal
1643
+ });
1644
+ } catch (err) {
1645
+ const isAbort = controller.signal.aborted || err instanceof Error && err.name === "AbortError";
1646
+ if (isAbort) {
1647
+ console.error(
1648
+ `[agntcms] webhook submission timed out after ${timeoutMs}ms`
1649
+ );
1650
+ throw new Error(`webhook request timed out after ${timeoutMs}ms`);
1651
+ }
1652
+ console.error(
1653
+ `[agntcms] webhook submission failed: ${err instanceof Error ? err.message : String(err)}`
1654
+ );
1655
+ throw err;
1656
+ } finally {
1657
+ clearTimeout(timer);
1658
+ }
1659
+ if (!res.ok) {
1660
+ let detail = "";
1661
+ try {
1662
+ detail = await res.text();
1663
+ } catch {
1664
+ }
1665
+ console.error(
1666
+ `[agntcms] webhook returned ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
1667
+ );
1668
+ throw new Error(`webhook responded with status ${res.status}`);
1669
+ }
1670
+ };
1671
+ const list = async (_formName) => {
1672
+ throw new SubmissionsNotReadableError(
1673
+ "webhook submission adapter has no local copy; configure an FS adapter to enable listing"
1674
+ );
1675
+ };
1676
+ const read = async (_formName, _id) => {
1677
+ throw new SubmissionsNotReadableError(
1678
+ "webhook submission adapter has no local copy; configure an FS adapter to enable reading"
1679
+ );
1680
+ };
1681
+ return { info: { kind: "webhook", host: parsedHost }, store, list, read };
1682
+ };
1683
+
1684
+ // src/sections/defineSection.ts
1685
+ function builtInDefault(fieldName, descriptor) {
1686
+ switch (descriptor.kind) {
1687
+ case "image":
1688
+ return { filename: "placeholder.png", alt: "Placeholder image" };
1689
+ case "video":
1690
+ return { url: "" };
1691
+ case "richText":
1692
+ return "Start writing here...";
1693
+ case "text":
1694
+ return fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
1695
+ case "reference":
1696
+ return { slug: "" };
1697
+ case "link":
1698
+ return { type: "internal", slug: "", label: "Learn more" };
1699
+ case "button": {
1700
+ const firstVariant = descriptor.variants[0]?.value ?? "";
1701
+ return { label: "Get started", variant: firstVariant };
1702
+ }
1703
+ case "number":
1704
+ return 0;
1705
+ case "boolean":
1706
+ return false;
1707
+ case "select":
1708
+ return descriptor.options[0]?.value ?? "";
1709
+ case "list":
1710
+ return [];
1711
+ case "formOverrides":
1712
+ return {};
1713
+ default: {
1714
+ const _exhaustive = descriptor;
1715
+ void _exhaustive;
1716
+ return "";
1717
+ }
1718
+ }
1719
+ }
1720
+ function defineSection(input) {
1721
+ const defaults = {};
1722
+ for (const [key, descriptor] of Object.entries(input.schema)) {
1723
+ defaults[key] = descriptor.default ?? builtInDefault(key, descriptor);
1724
+ }
1725
+ return {
1726
+ name: input.name,
1727
+ ...input.category !== void 0 ? { category: input.category } : {},
1728
+ schema: input.schema,
1729
+ component: input.component,
1730
+ defaults,
1731
+ // Conditional spread mirrors the `category` pattern — required so
1732
+ // `exactOptionalPropertyTypes` does not see `layouts: undefined` being
1733
+ // assigned to an optional-only property.
1734
+ ...input.layouts !== void 0 ? { layouts: input.layouts } : {}
1735
+ };
1736
+ }
1737
+
1738
+ // src/sections/wrapAsSlot.ts
1739
+ function wrapAsSlot(_kind, value) {
1740
+ return { value };
1741
+ }
1742
+
1743
+ // src/sections/wrapSectionProps.ts
1744
+ var SLOT_KINDS = /* @__PURE__ */ new Set([
1745
+ "text",
1746
+ "richText",
1747
+ "image",
1748
+ "video",
1749
+ "link",
1750
+ "button",
1751
+ "number",
1752
+ "boolean",
1753
+ "select",
1754
+ "list"
1755
+ ]);
1756
+ function wrapSectionProps(data, schema) {
1757
+ if (schema === void 0) {
1758
+ return { ...data };
1759
+ }
1760
+ const out = {};
1761
+ for (const [key, value] of Object.entries(data)) {
1762
+ const descriptor = schema[key];
1763
+ if (descriptor === void 0) {
1764
+ out[key] = value;
1765
+ continue;
1766
+ }
1767
+ if (!SLOT_KINDS.has(descriptor.kind)) {
1768
+ out[key] = value;
1769
+ continue;
1770
+ }
1771
+ out[key] = wrapAsSlot(descriptor.kind, value);
1772
+ }
1773
+ return out;
1774
+ }
1775
+
1776
+ // src/react-server/GlobalSlot.tsx
1777
+ var import_client = require("@agntcms/next/client");
1778
+ var import_jsx_runtime = require("react/jsx-runtime");
1779
+ var isPreviewBrand = (v) => v !== null && typeof v === "object" && "__agntcmsPreview" in v;
1780
+ async function GlobalSlot(props) {
1781
+ const { name, getGlobal, definitions, mode, fallback } = props;
1782
+ const global = await getGlobal({ name, mode }).catch(() => null);
1783
+ if (global === null) {
1784
+ return renderFallback(fallback);
1785
+ }
1786
+ const definition = definitions.find((d) => d.name === global.type);
1787
+ if (!definition) {
1788
+ return renderFallback(fallback);
1789
+ }
1790
+ const Component = definition.component;
1791
+ const dataRecord = global.data;
1792
+ const inPreview = Object.values(dataRecord).some(isPreviewBrand);
1793
+ const rawDefaults = definition.defaults ?? {};
1794
+ const wrappedDefaults = inPreview ? Object.fromEntries(
1795
+ Object.entries(rawDefaults).map(([key, value]) => [
1796
+ key,
1797
+ {
1798
+ __agntcmsPreview: true,
1799
+ value,
1800
+ origin: {
1801
+ kind: "global",
1802
+ globalName: name,
1803
+ pageSlug: `__global__:${name}`,
1804
+ sectionId: name,
1805
+ fieldPath: key,
1806
+ source: "draft",
1807
+ revision: ""
1808
+ }
1809
+ }
1810
+ ])
1811
+ ) : rawDefaults;
1812
+ const mergedData = {
1813
+ ...wrappedDefaults,
1814
+ ...dataRecord
1815
+ };
1816
+ const slotProps = wrapSectionProps(mergedData, definition.schema);
1817
+ if (mode === "published") {
1818
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { ...slotProps });
1819
+ }
1820
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1821
+ import_client.GlobalSaveProvider,
1822
+ {
1823
+ globalName: name,
1824
+ globalType: global.type,
1825
+ initialData: global.data,
1826
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { ...slotProps })
1827
+ }
1828
+ );
1829
+ }
1830
+ function renderFallback(fallback) {
1831
+ if (fallback === void 0 || fallback === null) return null;
1832
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
1833
+ }
1834
+
1835
+ // src/forms/defineForm.ts
1836
+ var InvalidFormFieldError = class extends Error {
1837
+ formName;
1838
+ fieldName;
1839
+ fieldKind;
1840
+ constructor(formName, fieldName, fieldKind) {
1841
+ super(
1842
+ `Form "${formName}": field "${fieldName}" uses kind "${fieldKind}", which is not allowed in a form schema in v1. Allowed kinds: text, richText, number, boolean, select, link. Forbidden kinds: image, reference, list, formOverrides. See ARCHITECTURE.md \xA76.5.`
1843
+ );
1844
+ this.name = "InvalidFormFieldError";
1845
+ this.formName = formName;
1846
+ this.fieldName = fieldName;
1847
+ this.fieldKind = fieldKind;
1848
+ }
1849
+ };
1850
+ var HoneypotCollisionError = class extends Error {
1851
+ formName;
1852
+ fieldName;
1853
+ reason;
1854
+ constructor(formName, fieldName, reason = "collision") {
1855
+ super(
1856
+ reason === "empty" ? `Form "${formName}": honeypot name must be a non-empty string.` : `Form "${formName}": honeypot name "${fieldName}" collides with a real field. Choose a honeypot name that does not appear in the schema.`
1857
+ );
1858
+ this.name = "HoneypotCollisionError";
1859
+ this.formName = formName;
1860
+ this.fieldName = fieldName;
1861
+ this.reason = reason;
1862
+ }
1863
+ };
1864
+ var InvalidFormNameError = class extends Error {
1865
+ constructor(name) {
1866
+ super(
1867
+ `Invalid form name ${JSON.stringify(name)}. Use letters, digits, hyphen, and underscore only (no path separators).`
1868
+ );
1869
+ this.name = "InvalidFormNameError";
1870
+ }
1871
+ };
1872
+ var FORM_NAME_PATTERN2 = /^[a-zA-Z0-9_-]+$/;
1873
+ function defineForm(input) {
1874
+ if (typeof input.name !== "string" || !FORM_NAME_PATTERN2.test(input.name)) {
1875
+ throw new InvalidFormNameError(input.name);
1876
+ }
1877
+ for (const [fieldName, descriptor] of Object.entries(input.schema)) {
1878
+ const kind = descriptor.kind;
1879
+ if (FORM_FORBIDDEN_KINDS.has(kind)) {
1880
+ throw new InvalidFormFieldError(input.name, fieldName, kind);
1881
+ }
1882
+ }
1883
+ if (input.honeypot !== void 0) {
1884
+ if (typeof input.honeypot !== "string" || input.honeypot === "") {
1885
+ throw new HoneypotCollisionError(input.name, input.honeypot, "empty");
1886
+ }
1887
+ if (Object.prototype.hasOwnProperty.call(input.schema, input.honeypot)) {
1888
+ throw new HoneypotCollisionError(input.name, input.honeypot, "collision");
1889
+ }
1890
+ }
1891
+ return input.honeypot !== void 0 ? { name: input.name, schema: input.schema, honeypot: input.honeypot } : { name: input.name, schema: input.schema };
1892
+ }
1893
+
1894
+ // src/forms/registry.ts
1895
+ var DuplicateFormNameError = class extends Error {
1896
+ formName;
1897
+ constructor(name) {
1898
+ super(
1899
+ `Form name "${name}" is registered more than once. Form names must be unique within a config.`
1900
+ );
1901
+ this.name = "DuplicateFormNameError";
1902
+ this.formName = name;
1903
+ }
1904
+ };
1905
+ function buildFormRegistry(definitions) {
1906
+ const byName = /* @__PURE__ */ new Map();
1907
+ for (const def of definitions) {
1908
+ if (byName.has(def.name)) {
1909
+ throw new DuplicateFormNameError(def.name);
1910
+ }
1911
+ byName.set(def.name, def);
1912
+ }
1913
+ const registry = {
1914
+ definitions,
1915
+ get: (name) => byName.get(name),
1916
+ has: (name) => byName.has(name)
1917
+ };
1918
+ return Object.freeze(registry);
1919
+ }
1920
+
1921
+ // src/server.ts
1922
+ installDefaultAdapterFactories();
1923
+ // Annotate the CommonJS export names for ESM import in node:
1924
+ 0 && (module.exports = {
1925
+ BooleanField,
1926
+ ButtonField,
1927
+ DuplicateFormNameError,
1928
+ FormOverridesField,
1929
+ GlobalSlot,
1930
+ HoneypotCollisionError,
1931
+ ImageField,
1932
+ InvalidFormFieldError,
1933
+ InvalidFormNameError,
1934
+ LinkField,
1935
+ ListField,
1936
+ NOT_FOUND_PAGE_SLUG,
1937
+ NumberField,
1938
+ ReferenceField,
1939
+ RichTextField,
1940
+ SERVER_ERROR_PAGE_SLUG,
1941
+ SelectField,
1942
+ SubmissionsNotReadableError,
1943
+ TextField,
1944
+ VideoField,
1945
+ buildFormRegistry,
1946
+ createDefaultAssetAdapter,
1947
+ createDefaultContentAdapter,
1948
+ createDefaultSubmissionAdapter,
1949
+ createFsAssetAdapter,
1950
+ createFsContentAdapter,
1951
+ createFsSubmissionAdapter,
1952
+ createListPages,
1953
+ createRateLimit,
1954
+ createRuntime,
1955
+ createSubmitForm,
1956
+ createWebhookSubmissionAdapter,
1957
+ defineForm,
1958
+ defineSection,
1959
+ getReservedPageSlugViolation,
1960
+ hasUniqueSectionIds,
1961
+ hrefOf,
1962
+ isExternalLink,
1963
+ isSitemapEligibleSlug,
1964
+ linkAnchorAttrs,
1965
+ normalizeLinkValue,
1966
+ validateEmail,
1967
+ validateExternalUrl,
1968
+ validateInternalSlug,
1969
+ validatePhone
1970
+ });