@agntcms/next 0.3.2 → 0.3.4
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.
- package/dist/{assets-Cyt9upqW.d.cts → assets-B3oNeLdj.d.cts} +1 -1
- package/dist/{assets-P8OCigDG.d.ts → assets-DHumg-X7.d.ts} +1 -1
- package/dist/client.cjs +2831 -4747
- package/dist/client.d.cts +11 -153
- package/dist/client.d.ts +11 -153
- package/dist/client.mjs +4750 -6675
- package/dist/config.cjs +2 -123
- package/dist/config.d.cts +4 -59
- package/dist/config.d.ts +4 -59
- package/dist/config.mjs +2 -116
- package/dist/{defineSection-Kr0pWqMY.d.ts → defineSection-ByG5uwiR.d.cts} +5 -24
- package/dist/{defineSection-9qQ5ulAH.d.cts → defineSection-ChkZCQyQ.d.ts} +5 -24
- package/dist/{rateLimit-CXptRM_K.d.ts → getContent-DAgAn095.d.ts} +3 -132
- package/dist/{rateLimit-CiROGTLE.d.cts → getContent-yK-sARoc.d.cts} +3 -132
- package/dist/handlers.cjs +19 -382
- package/dist/handlers.d.cts +4 -73
- package/dist/handlers.d.ts +4 -73
- package/dist/handlers.mjs +19 -377
- package/dist/index.cjs +1 -109
- package/dist/index.d.cts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.mjs +1 -103
- package/dist/{form-BqY0H1V5.d.cts → page-DXF0_SrY.d.cts} +3 -293
- package/dist/{form-BqY0H1V5.d.ts → page-DXF0_SrY.d.ts} +3 -293
- package/dist/server.cjs +15 -635
- package/dist/server.d.cts +8 -75
- package/dist/server.d.ts +8 -75
- package/dist/server.mjs +11 -618
- package/package.json +1 -1
- package/dist/defineForm-Bp9vzW56.d.ts +0 -71
- package/dist/defineForm-CJ8KZC93.d.cts +0 -71
- package/dist/registry-CraTTwT7.d.cts +0 -29
- package/dist/registry-DMujGqt0.d.ts +0 -29
package/dist/server.mjs
CHANGED
|
@@ -335,37 +335,6 @@ function normalizeLinkValue(raw) {
|
|
|
335
335
|
return { type: "internal", slug: "", label: "" };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
// src/domain/form.ts
|
|
339
|
-
var FORM_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
|
|
340
|
-
"image",
|
|
341
|
-
"video",
|
|
342
|
-
"reference",
|
|
343
|
-
"list",
|
|
344
|
-
// `formOverrides` is a section-only descriptor (it overrides another
|
|
345
|
-
// form schema instance). Putting it inside a form would mean a form's
|
|
346
|
-
// payload could carry overrides for itself or another form — a
|
|
347
|
-
// recursive shape with no ergonomic editor UI. Section-only by design.
|
|
348
|
-
"formOverrides",
|
|
349
|
-
// `button` is a section-only descriptor: a styled CTA with an
|
|
350
|
-
// optional link. Public-form payloads collect user input — a button
|
|
351
|
-
// value is authored content, not a submitted answer. Section-only
|
|
352
|
-
// by design (mirrors `formOverrides`).
|
|
353
|
-
"button"
|
|
354
|
-
]);
|
|
355
|
-
var SubmissionsNotReadableError = class extends Error {
|
|
356
|
-
constructor(message = "submission adapter does not support reading") {
|
|
357
|
-
super(message);
|
|
358
|
-
this.name = "SubmissionsNotReadableError";
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
// src/domain/formOverrides.ts
|
|
363
|
-
var FormOverridesField = (formName, opts) => ({
|
|
364
|
-
kind: "formOverrides",
|
|
365
|
-
formName,
|
|
366
|
-
...opts?.default !== void 0 ? { default: opts.default } : {}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
338
|
// src/storage/fs/content.ts
|
|
370
339
|
var SLUG_PATTERN2 = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/;
|
|
371
340
|
var assertValidSlug = (slug) => {
|
|
@@ -426,7 +395,7 @@ var createFsContentAdapter = (options) => {
|
|
|
426
395
|
throw err;
|
|
427
396
|
}
|
|
428
397
|
};
|
|
429
|
-
const
|
|
398
|
+
const listJsonFiles = async (dir) => {
|
|
430
399
|
let entries;
|
|
431
400
|
try {
|
|
432
401
|
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
@@ -440,7 +409,7 @@ var createFsContentAdapter = (options) => {
|
|
|
440
409
|
results.push(entry.name);
|
|
441
410
|
} else if (entry.isDirectory()) {
|
|
442
411
|
const subDir = path3.join(dir, entry.name);
|
|
443
|
-
const subFiles = await
|
|
412
|
+
const subFiles = await listJsonFiles(subDir);
|
|
444
413
|
for (const sub of subFiles) {
|
|
445
414
|
results.push(`${entry.name}/${sub}`);
|
|
446
415
|
}
|
|
@@ -479,7 +448,7 @@ var createFsContentAdapter = (options) => {
|
|
|
479
448
|
await writeAtomic(filePath, JSON.stringify(page, null, 2));
|
|
480
449
|
};
|
|
481
450
|
const listDrafts = async () => {
|
|
482
|
-
const files = await
|
|
451
|
+
const files = await listJsonFiles(draftsDir);
|
|
483
452
|
const results = [];
|
|
484
453
|
for (const relPath of files) {
|
|
485
454
|
const slug = relPath.slice(0, -".json".length);
|
|
@@ -489,7 +458,7 @@ var createFsContentAdapter = (options) => {
|
|
|
489
458
|
return results;
|
|
490
459
|
};
|
|
491
460
|
const listPages = async () => {
|
|
492
|
-
const files = await
|
|
461
|
+
const files = await listJsonFiles(pagesDir);
|
|
493
462
|
const results = [];
|
|
494
463
|
for (const relPath of files) {
|
|
495
464
|
const slug = relPath.slice(0, -".json".length);
|
|
@@ -499,7 +468,7 @@ var createFsContentAdapter = (options) => {
|
|
|
499
468
|
return results;
|
|
500
469
|
};
|
|
501
470
|
const listPageSummaries = async () => {
|
|
502
|
-
const files = await
|
|
471
|
+
const files = await listJsonFiles(pagesDir);
|
|
503
472
|
const results = [];
|
|
504
473
|
for (const relPath of files) {
|
|
505
474
|
const slug = relPath.slice(0, -".json".length);
|
|
@@ -797,151 +766,6 @@ var createFsContentAdapter = (options) => {
|
|
|
797
766
|
};
|
|
798
767
|
};
|
|
799
768
|
|
|
800
|
-
// src/storage/fs/submissions.ts
|
|
801
|
-
import { randomUUID } from "crypto";
|
|
802
|
-
import * as fs4 from "fs/promises";
|
|
803
|
-
import * as path4 from "path";
|
|
804
|
-
var DEFAULT_MAX_LIST = 500;
|
|
805
|
-
var FORM_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
806
|
-
var parseSubmission = (raw) => {
|
|
807
|
-
let parsed;
|
|
808
|
-
try {
|
|
809
|
-
parsed = JSON.parse(raw);
|
|
810
|
-
} catch {
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
814
|
-
return null;
|
|
815
|
-
}
|
|
816
|
-
const obj = parsed;
|
|
817
|
-
if (typeof obj["formName"] !== "string") return null;
|
|
818
|
-
if (typeof obj["id"] !== "string") return null;
|
|
819
|
-
if (typeof obj["submittedAt"] !== "string") return null;
|
|
820
|
-
if (obj["payload"] === null || typeof obj["payload"] !== "object" || Array.isArray(obj["payload"])) {
|
|
821
|
-
return null;
|
|
822
|
-
}
|
|
823
|
-
return {
|
|
824
|
-
formName: obj["formName"],
|
|
825
|
-
id: obj["id"],
|
|
826
|
-
submittedAt: obj["submittedAt"],
|
|
827
|
-
payload: obj["payload"]
|
|
828
|
-
};
|
|
829
|
-
};
|
|
830
|
-
var assertValidFormName = (name) => {
|
|
831
|
-
if (typeof name !== "string" || !FORM_NAME_PATTERN.test(name)) {
|
|
832
|
-
throw new Error(`invalid form name: ${JSON.stringify(name)}`);
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
var submissionFilename = (submittedAt, id) => {
|
|
836
|
-
const ts = submittedAt.replace(/:/g, "-");
|
|
837
|
-
return `${ts}-${id}.json`;
|
|
838
|
-
};
|
|
839
|
-
var listJsonFiles = async (dir) => {
|
|
840
|
-
let entries;
|
|
841
|
-
try {
|
|
842
|
-
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
843
|
-
} catch (err) {
|
|
844
|
-
if (isEnoent(err)) return [];
|
|
845
|
-
throw err;
|
|
846
|
-
}
|
|
847
|
-
const results = [];
|
|
848
|
-
for (const entry of entries) {
|
|
849
|
-
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
850
|
-
results.push(entry.name);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return results;
|
|
854
|
-
};
|
|
855
|
-
var createFsSubmissionAdapter = (options) => {
|
|
856
|
-
const { submissionsRoot } = options;
|
|
857
|
-
if (!path4.isAbsolute(submissionsRoot)) {
|
|
858
|
-
throw new Error(
|
|
859
|
-
`submissionsRoot must be an absolute path, got: ${JSON.stringify(submissionsRoot)}`
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
const maxList = options.maxList ?? DEFAULT_MAX_LIST;
|
|
863
|
-
if (!Number.isFinite(maxList) || maxList <= 0 || !Number.isInteger(maxList)) {
|
|
864
|
-
throw new Error(
|
|
865
|
-
`maxList must be a positive integer, got: ${maxList}`
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
const rootResolved = path4.resolve(submissionsRoot);
|
|
869
|
-
const rootWithSep = rootResolved.endsWith(path4.sep) ? rootResolved : rootResolved + path4.sep;
|
|
870
|
-
const formDir = (formName) => {
|
|
871
|
-
assertValidFormName(formName);
|
|
872
|
-
const dir = path4.resolve(rootResolved, formName);
|
|
873
|
-
if (!dir.startsWith(rootWithSep)) {
|
|
874
|
-
throw new Error(`form name escapes submissions root: ${JSON.stringify(formName)}`);
|
|
875
|
-
}
|
|
876
|
-
return dir;
|
|
877
|
-
};
|
|
878
|
-
const cryptoSuffix = () => randomUUID();
|
|
879
|
-
const store = async (submission) => {
|
|
880
|
-
const dir = formDir(submission.formName);
|
|
881
|
-
const filename = submissionFilename(submission.submittedAt, submission.id);
|
|
882
|
-
if (filename.includes("/") || filename.includes("\\")) {
|
|
883
|
-
throw new Error(`submission id contains path separator: ${JSON.stringify(submission.id)}`);
|
|
884
|
-
}
|
|
885
|
-
const target = path4.join(dir, filename);
|
|
886
|
-
await writeAtomic(target, JSON.stringify(submission, null, 2), cryptoSuffix);
|
|
887
|
-
};
|
|
888
|
-
const list = async (formName) => {
|
|
889
|
-
const dir = formDir(formName);
|
|
890
|
-
const files = await listJsonFiles(dir);
|
|
891
|
-
const summaries = [];
|
|
892
|
-
for (const filename of files) {
|
|
893
|
-
const stem = filename.slice(0, -".json".length);
|
|
894
|
-
const lastDash = stem.lastIndexOf("-");
|
|
895
|
-
if (lastDash < 0) continue;
|
|
896
|
-
const tsRaw = stem.slice(0, lastDash);
|
|
897
|
-
const id = stem.slice(lastDash + 1);
|
|
898
|
-
if (id === "") continue;
|
|
899
|
-
const tIndex = tsRaw.indexOf("T");
|
|
900
|
-
if (tIndex < 0) continue;
|
|
901
|
-
const datePart = tsRaw.slice(0, tIndex);
|
|
902
|
-
const timePart = tsRaw.slice(tIndex + 1).replace(/-/g, ":");
|
|
903
|
-
const submittedAt = `${datePart}T${timePart}`;
|
|
904
|
-
summaries.push({ id, submittedAt });
|
|
905
|
-
}
|
|
906
|
-
summaries.sort((a, b) => {
|
|
907
|
-
if (a.submittedAt !== b.submittedAt) {
|
|
908
|
-
return a.submittedAt < b.submittedAt ? 1 : -1;
|
|
909
|
-
}
|
|
910
|
-
if (a.id !== b.id) {
|
|
911
|
-
return a.id < b.id ? 1 : -1;
|
|
912
|
-
}
|
|
913
|
-
return 0;
|
|
914
|
-
});
|
|
915
|
-
return summaries.length > maxList ? summaries.slice(0, maxList) : summaries;
|
|
916
|
-
};
|
|
917
|
-
const read = async (formName, id) => {
|
|
918
|
-
if (id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
const dir = formDir(formName);
|
|
922
|
-
let entries;
|
|
923
|
-
try {
|
|
924
|
-
entries = (await fs4.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
925
|
-
} catch (err) {
|
|
926
|
-
if (isEnoent(err)) return null;
|
|
927
|
-
throw err;
|
|
928
|
-
}
|
|
929
|
-
const suffix = `-${id}.json`;
|
|
930
|
-
const found = entries.find((n) => n.endsWith(suffix));
|
|
931
|
-
if (!found) return null;
|
|
932
|
-
const target = path4.join(dir, found);
|
|
933
|
-
let raw;
|
|
934
|
-
try {
|
|
935
|
-
raw = await fs4.readFile(target, { encoding: "utf8" });
|
|
936
|
-
} catch (err) {
|
|
937
|
-
if (isEnoent(err)) return null;
|
|
938
|
-
throw err;
|
|
939
|
-
}
|
|
940
|
-
return parseSubmission(raw);
|
|
941
|
-
};
|
|
942
|
-
return { info: { kind: "fs" }, store, list, read };
|
|
943
|
-
};
|
|
944
|
-
|
|
945
769
|
// src/config/defaults-registry.ts
|
|
946
770
|
var SLOT = /* @__PURE__ */ Symbol.for("@agntcms/next/default-adapter-factories");
|
|
947
771
|
function holder() {
|
|
@@ -952,31 +776,24 @@ function registerDefaultAdapterFactories(factories) {
|
|
|
952
776
|
}
|
|
953
777
|
|
|
954
778
|
// src/config/defaults.ts
|
|
955
|
-
import * as
|
|
779
|
+
import * as path4 from "path";
|
|
956
780
|
function createDefaultContentAdapter(options) {
|
|
957
781
|
const root = options?.projectRoot ?? process.cwd();
|
|
958
782
|
return createFsContentAdapter({
|
|
959
|
-
contentRoot:
|
|
783
|
+
contentRoot: path4.resolve(root, "content")
|
|
960
784
|
});
|
|
961
785
|
}
|
|
962
786
|
function createDefaultAssetAdapter(options) {
|
|
963
787
|
const root = options?.projectRoot ?? process.cwd();
|
|
964
788
|
return createFsAssetAdapter({
|
|
965
|
-
assetsRoot:
|
|
789
|
+
assetsRoot: path4.resolve(root, "public/assets"),
|
|
966
790
|
publicUrlBase: "/assets"
|
|
967
791
|
});
|
|
968
792
|
}
|
|
969
|
-
function createDefaultSubmissionAdapter(options) {
|
|
970
|
-
const root = options?.projectRoot ?? process.cwd();
|
|
971
|
-
return createFsSubmissionAdapter({
|
|
972
|
-
submissionsRoot: path5.resolve(root, "content/submissions")
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
793
|
function installDefaultAdapterFactories() {
|
|
976
794
|
const factories = {
|
|
977
795
|
content: createDefaultContentAdapter,
|
|
978
|
-
asset: createDefaultAssetAdapter
|
|
979
|
-
submission: createDefaultSubmissionAdapter
|
|
796
|
+
asset: createDefaultAssetAdapter
|
|
980
797
|
};
|
|
981
798
|
registerDefaultAdapterFactories(factories);
|
|
982
799
|
}
|
|
@@ -1085,160 +902,6 @@ var createListPages = ({
|
|
|
1085
902
|
};
|
|
1086
903
|
};
|
|
1087
904
|
|
|
1088
|
-
// src/runtime/submitForm.ts
|
|
1089
|
-
import { randomBytes } from "crypto";
|
|
1090
|
-
var CROCKFORD32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1091
|
-
var defaultGenerateId = () => {
|
|
1092
|
-
const bytes = randomBytes(16);
|
|
1093
|
-
let out = "";
|
|
1094
|
-
for (let i = 0; i < 16; i++) {
|
|
1095
|
-
out += CROCKFORD32[bytes[i] & 31];
|
|
1096
|
-
}
|
|
1097
|
-
return out;
|
|
1098
|
-
};
|
|
1099
|
-
var validateNonEmptyString = (value) => {
|
|
1100
|
-
if (typeof value !== "string") return "must be a string";
|
|
1101
|
-
if (value.trim() === "") return "must not be empty";
|
|
1102
|
-
return null;
|
|
1103
|
-
};
|
|
1104
|
-
var validateNumber = (value, descriptor) => {
|
|
1105
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1106
|
-
return "must be a finite number";
|
|
1107
|
-
}
|
|
1108
|
-
if (descriptor.min !== void 0 && value < descriptor.min) {
|
|
1109
|
-
return `must be >= ${descriptor.min}`;
|
|
1110
|
-
}
|
|
1111
|
-
if (descriptor.max !== void 0 && value > descriptor.max) {
|
|
1112
|
-
return `must be <= ${descriptor.max}`;
|
|
1113
|
-
}
|
|
1114
|
-
return null;
|
|
1115
|
-
};
|
|
1116
|
-
var validateBoolean = (value) => {
|
|
1117
|
-
if (typeof value !== "boolean") return "must be true or false";
|
|
1118
|
-
return null;
|
|
1119
|
-
};
|
|
1120
|
-
var validateSelect = (value, descriptor) => {
|
|
1121
|
-
if (typeof value !== "string") return "must be a string";
|
|
1122
|
-
if (!descriptor.options.some((opt) => opt.value === value)) {
|
|
1123
|
-
return "must be one of the declared options";
|
|
1124
|
-
}
|
|
1125
|
-
return null;
|
|
1126
|
-
};
|
|
1127
|
-
var validateLinkPayload = (value) => {
|
|
1128
|
-
if (value === null || typeof value !== "object") return "must be a link object";
|
|
1129
|
-
const obj = value;
|
|
1130
|
-
if (typeof obj["label"] !== "string") return "label must be a string";
|
|
1131
|
-
if (obj["type"] === "internal") {
|
|
1132
|
-
const slug = obj["slug"];
|
|
1133
|
-
if (typeof slug !== "string") return "slug must be a string";
|
|
1134
|
-
if (slug.trim() === "") return "slug must be a non-empty string";
|
|
1135
|
-
return validateInternalSlug(slug);
|
|
1136
|
-
}
|
|
1137
|
-
if (obj["type"] === "external") {
|
|
1138
|
-
const url = obj["url"];
|
|
1139
|
-
if (typeof url !== "string") return "url must be a string";
|
|
1140
|
-
if (url.trim() === "") return "url must be a non-empty string";
|
|
1141
|
-
return validateExternalUrl(url);
|
|
1142
|
-
}
|
|
1143
|
-
if (obj["type"] === "email") {
|
|
1144
|
-
const email = obj["email"];
|
|
1145
|
-
if (typeof email !== "string") return "email must be a string";
|
|
1146
|
-
if (email.trim() === "") return "email must be a non-empty string";
|
|
1147
|
-
return validateEmail(email);
|
|
1148
|
-
}
|
|
1149
|
-
if (obj["type"] === "phone") {
|
|
1150
|
-
const phone = obj["phone"];
|
|
1151
|
-
if (typeof phone !== "string") return "phone must be a string";
|
|
1152
|
-
if (phone.trim() === "") return "phone must be a non-empty string";
|
|
1153
|
-
return validatePhone(phone);
|
|
1154
|
-
}
|
|
1155
|
-
return 'type must be "internal", "external", "email", or "phone"';
|
|
1156
|
-
};
|
|
1157
|
-
var validateAndNormalisePayload = (schema, payload) => {
|
|
1158
|
-
const errors = {};
|
|
1159
|
-
const normalised = {};
|
|
1160
|
-
for (const [fieldName, descriptor] of Object.entries(schema)) {
|
|
1161
|
-
if (!Object.prototype.hasOwnProperty.call(payload, fieldName)) {
|
|
1162
|
-
errors[fieldName] = "is required";
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1165
|
-
const value = payload[fieldName];
|
|
1166
|
-
let err = null;
|
|
1167
|
-
switch (descriptor.kind) {
|
|
1168
|
-
case "text":
|
|
1169
|
-
case "richText": {
|
|
1170
|
-
err = validateNonEmptyString(value);
|
|
1171
|
-
if (!err) {
|
|
1172
|
-
normalised[fieldName] = value.trim();
|
|
1173
|
-
}
|
|
1174
|
-
break;
|
|
1175
|
-
}
|
|
1176
|
-
case "number": {
|
|
1177
|
-
err = validateNumber(value, descriptor);
|
|
1178
|
-
if (!err) normalised[fieldName] = value;
|
|
1179
|
-
break;
|
|
1180
|
-
}
|
|
1181
|
-
case "boolean": {
|
|
1182
|
-
err = validateBoolean(value);
|
|
1183
|
-
if (!err) normalised[fieldName] = value;
|
|
1184
|
-
break;
|
|
1185
|
-
}
|
|
1186
|
-
case "select": {
|
|
1187
|
-
err = validateSelect(value, descriptor);
|
|
1188
|
-
if (!err) normalised[fieldName] = value;
|
|
1189
|
-
break;
|
|
1190
|
-
}
|
|
1191
|
-
case "link": {
|
|
1192
|
-
err = validateLinkPayload(value);
|
|
1193
|
-
if (!err) {
|
|
1194
|
-
const linkObj = value;
|
|
1195
|
-
const label = typeof linkObj["label"] === "string" ? linkObj["label"] : "";
|
|
1196
|
-
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 };
|
|
1197
|
-
normalised[fieldName] = normalisedLink;
|
|
1198
|
-
}
|
|
1199
|
-
break;
|
|
1200
|
-
}
|
|
1201
|
-
default: {
|
|
1202
|
-
const _exhaustive = descriptor;
|
|
1203
|
-
void _exhaustive;
|
|
1204
|
-
err = "unsupported field type";
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
if (err) errors[fieldName] = err;
|
|
1208
|
-
}
|
|
1209
|
-
if (Object.keys(errors).length > 0) return { ok: false, errors };
|
|
1210
|
-
return { ok: true, payload: normalised };
|
|
1211
|
-
};
|
|
1212
|
-
function createSubmitForm(deps) {
|
|
1213
|
-
const { forms, submissionAdapter } = deps;
|
|
1214
|
-
const generateId = deps.generateId ?? defaultGenerateId;
|
|
1215
|
-
const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1216
|
-
return async function submitForm(input) {
|
|
1217
|
-
const def = forms.get(input.formName);
|
|
1218
|
-
if (!def) {
|
|
1219
|
-
return { ok: false, error: "unknown_form" };
|
|
1220
|
-
}
|
|
1221
|
-
if (def.honeypot !== void 0) {
|
|
1222
|
-
const trapValue = input.payload[def.honeypot];
|
|
1223
|
-
if (typeof trapValue === "string" && trapValue.length > 0) {
|
|
1224
|
-
return { ok: true, stored: false, suppressed: "honeypot" };
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
const validated = validateAndNormalisePayload(def.schema, input.payload);
|
|
1228
|
-
if (!validated.ok) {
|
|
1229
|
-
return { ok: false, error: "validation_failed", errors: validated.errors };
|
|
1230
|
-
}
|
|
1231
|
-
const submission = {
|
|
1232
|
-
formName: def.name,
|
|
1233
|
-
payload: validated.payload,
|
|
1234
|
-
submittedAt: now(),
|
|
1235
|
-
id: generateId()
|
|
1236
|
-
};
|
|
1237
|
-
await submissionAdapter.store(submission);
|
|
1238
|
-
return { ok: true, stored: true, id: submission.id };
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
905
|
// src/runtime/getContent.ts
|
|
1243
906
|
var isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
1244
907
|
var looksLikeLink = (obj) => {
|
|
@@ -1398,83 +1061,7 @@ function createRuntime(options) {
|
|
|
1398
1061
|
};
|
|
1399
1062
|
const getGlobal = createGetGlobal(contentAdapter);
|
|
1400
1063
|
const listPages = createListPages({ contentAdapter });
|
|
1401
|
-
|
|
1402
|
-
info: { kind: "fs" },
|
|
1403
|
-
store: async () => {
|
|
1404
|
-
throw new Error("submissionAdapter not configured; pass one to createRuntime");
|
|
1405
|
-
},
|
|
1406
|
-
list: async () => [],
|
|
1407
|
-
read: async () => null
|
|
1408
|
-
};
|
|
1409
|
-
const submitForm = createSubmitForm({
|
|
1410
|
-
forms: options.forms ?? emptyFormRegistry,
|
|
1411
|
-
submissionAdapter: options.submissionAdapter ?? noopAdapter
|
|
1412
|
-
});
|
|
1413
|
-
return { getContent, publishDraft, getGlobal, submitForm, listPages };
|
|
1414
|
-
}
|
|
1415
|
-
var emptyFormRegistry = Object.freeze({
|
|
1416
|
-
definitions: [],
|
|
1417
|
-
get: () => void 0,
|
|
1418
|
-
has: () => false
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
// src/runtime/rateLimit.ts
|
|
1422
|
-
var DEFAULT_MAX_BUCKETS = 1e4;
|
|
1423
|
-
function createRateLimit(options) {
|
|
1424
|
-
const { perWindow, windowMs } = options;
|
|
1425
|
-
if (!Number.isFinite(perWindow) || perWindow <= 0) {
|
|
1426
|
-
throw new Error(`perWindow must be a positive number, got: ${perWindow}`);
|
|
1427
|
-
}
|
|
1428
|
-
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
1429
|
-
throw new Error(`windowMs must be a positive number, got: ${windowMs}`);
|
|
1430
|
-
}
|
|
1431
|
-
const maxBuckets = options.maxBuckets ?? DEFAULT_MAX_BUCKETS;
|
|
1432
|
-
if (!Number.isFinite(maxBuckets) || maxBuckets <= 0 || !Number.isInteger(maxBuckets)) {
|
|
1433
|
-
throw new Error(`maxBuckets must be a positive integer, got: ${maxBuckets}`);
|
|
1434
|
-
}
|
|
1435
|
-
const now = options.now ?? Date.now;
|
|
1436
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
1437
|
-
const keyOf = (ip, formName) => `${ip}:${formName}`;
|
|
1438
|
-
const sweepExpired = (t) => {
|
|
1439
|
-
for (const [k, b] of buckets) {
|
|
1440
|
-
if (t >= b.resetAt) buckets.delete(k);
|
|
1441
|
-
}
|
|
1442
|
-
};
|
|
1443
|
-
const evictOldest = () => {
|
|
1444
|
-
let oldestKey = null;
|
|
1445
|
-
let oldestResetAt = Number.POSITIVE_INFINITY;
|
|
1446
|
-
for (const [k, b] of buckets) {
|
|
1447
|
-
if (b.resetAt < oldestResetAt) {
|
|
1448
|
-
oldestResetAt = b.resetAt;
|
|
1449
|
-
oldestKey = k;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
if (oldestKey !== null) buckets.delete(oldestKey);
|
|
1453
|
-
};
|
|
1454
|
-
return {
|
|
1455
|
-
check: (ip, formName) => {
|
|
1456
|
-
const key = keyOf(ip, formName);
|
|
1457
|
-
const t = now();
|
|
1458
|
-
const existing = buckets.get(key);
|
|
1459
|
-
if (!existing || t >= existing.resetAt) {
|
|
1460
|
-
if (!existing && buckets.size >= maxBuckets) {
|
|
1461
|
-
sweepExpired(t);
|
|
1462
|
-
while (buckets.size >= maxBuckets) {
|
|
1463
|
-
evictOldest();
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
const bucket = { count: 1, resetAt: t + windowMs };
|
|
1467
|
-
buckets.set(key, bucket);
|
|
1468
|
-
return { allowed: true, count: 1, resetAt: bucket.resetAt };
|
|
1469
|
-
}
|
|
1470
|
-
existing.count += 1;
|
|
1471
|
-
const allowed = existing.count <= perWindow;
|
|
1472
|
-
return { allowed, count: existing.count, resetAt: existing.resetAt };
|
|
1473
|
-
},
|
|
1474
|
-
reset: () => {
|
|
1475
|
-
buckets.clear();
|
|
1476
|
-
}
|
|
1477
|
-
};
|
|
1064
|
+
return { getContent, publishDraft, getGlobal, listPages };
|
|
1478
1065
|
}
|
|
1479
1066
|
|
|
1480
1067
|
// src/runtime/systemPages.ts
|
|
@@ -1512,95 +1099,6 @@ var isSitemapEligibleSlug = (slug) => {
|
|
|
1512
1099
|
return !SITEMAP_EXCLUDED_TERMINAL_SLUGS.has(terminal);
|
|
1513
1100
|
};
|
|
1514
1101
|
|
|
1515
|
-
// src/storage/webhook/submissions.ts
|
|
1516
|
-
var URL_PATTERN = /^https?:\/\//;
|
|
1517
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1518
|
-
var createWebhookSubmissionAdapter = (options) => {
|
|
1519
|
-
if (typeof options.url !== "string" || !URL_PATTERN.test(options.url)) {
|
|
1520
|
-
throw new Error(
|
|
1521
|
-
`webhook url must start with http:// or https://, got: ${JSON.stringify(options.url)}`
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
let parsedHost;
|
|
1525
|
-
try {
|
|
1526
|
-
parsedHost = new URL(options.url).host;
|
|
1527
|
-
} catch {
|
|
1528
|
-
throw new Error(
|
|
1529
|
-
`webhook url is not a valid URL: ${JSON.stringify(options.url)}`
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
if (options.url.startsWith("http://") && options.headers !== void 0 && Object.keys(options.headers).length > 0) {
|
|
1533
|
-
console.warn(
|
|
1534
|
-
"[agntcms] webhook url uses http:// with custom headers \u2014 secrets will be transmitted in plaintext"
|
|
1535
|
-
);
|
|
1536
|
-
}
|
|
1537
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1538
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
1539
|
-
throw new Error(
|
|
1540
|
-
`webhook timeoutMs must be a positive integer (ms), got: ${timeoutMs}`
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
const doFetch = options.fetch ?? ((url, init) => (
|
|
1544
|
-
// The DOM/Node fetch types differ; cast to the minimal shape we declared.
|
|
1545
|
-
// Using `globalThis.fetch` keeps this neutral across Node, Edge, and
|
|
1546
|
-
// jsdom test environments.
|
|
1547
|
-
globalThis.fetch(url, init)
|
|
1548
|
-
));
|
|
1549
|
-
const baseHeaders = {
|
|
1550
|
-
"Content-Type": "application/json",
|
|
1551
|
-
...options.headers
|
|
1552
|
-
};
|
|
1553
|
-
const store = async (submission) => {
|
|
1554
|
-
const controller = new AbortController();
|
|
1555
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1556
|
-
let res;
|
|
1557
|
-
try {
|
|
1558
|
-
res = await doFetch(options.url, {
|
|
1559
|
-
method: "POST",
|
|
1560
|
-
headers: baseHeaders,
|
|
1561
|
-
body: JSON.stringify(submission),
|
|
1562
|
-
signal: controller.signal
|
|
1563
|
-
});
|
|
1564
|
-
} catch (err) {
|
|
1565
|
-
const isAbort = controller.signal.aborted || err instanceof Error && err.name === "AbortError";
|
|
1566
|
-
if (isAbort) {
|
|
1567
|
-
console.error(
|
|
1568
|
-
`[agntcms] webhook submission timed out after ${timeoutMs}ms`
|
|
1569
|
-
);
|
|
1570
|
-
throw new Error(`webhook request timed out after ${timeoutMs}ms`);
|
|
1571
|
-
}
|
|
1572
|
-
console.error(
|
|
1573
|
-
`[agntcms] webhook submission failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1574
|
-
);
|
|
1575
|
-
throw err;
|
|
1576
|
-
} finally {
|
|
1577
|
-
clearTimeout(timer);
|
|
1578
|
-
}
|
|
1579
|
-
if (!res.ok) {
|
|
1580
|
-
let detail = "";
|
|
1581
|
-
try {
|
|
1582
|
-
detail = await res.text();
|
|
1583
|
-
} catch {
|
|
1584
|
-
}
|
|
1585
|
-
console.error(
|
|
1586
|
-
`[agntcms] webhook returned ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
1587
|
-
);
|
|
1588
|
-
throw new Error(`webhook responded with status ${res.status}`);
|
|
1589
|
-
}
|
|
1590
|
-
};
|
|
1591
|
-
const list = async (_formName) => {
|
|
1592
|
-
throw new SubmissionsNotReadableError(
|
|
1593
|
-
"webhook submission adapter has no local copy; configure an FS adapter to enable listing"
|
|
1594
|
-
);
|
|
1595
|
-
};
|
|
1596
|
-
const read = async (_formName, _id) => {
|
|
1597
|
-
throw new SubmissionsNotReadableError(
|
|
1598
|
-
"webhook submission adapter has no local copy; configure an FS adapter to enable reading"
|
|
1599
|
-
);
|
|
1600
|
-
};
|
|
1601
|
-
return { info: { kind: "webhook", host: parsedHost }, store, list, read };
|
|
1602
|
-
};
|
|
1603
|
-
|
|
1604
1102
|
// src/sections/defineSection.ts
|
|
1605
1103
|
function builtInDefault(fieldName, descriptor) {
|
|
1606
1104
|
switch (descriptor.kind) {
|
|
@@ -1628,8 +1126,6 @@ function builtInDefault(fieldName, descriptor) {
|
|
|
1628
1126
|
return descriptor.options[0]?.value ?? "";
|
|
1629
1127
|
case "list":
|
|
1630
1128
|
return [];
|
|
1631
|
-
case "formOverrides":
|
|
1632
|
-
return {};
|
|
1633
1129
|
default: {
|
|
1634
1130
|
const _exhaustive = descriptor;
|
|
1635
1131
|
void _exhaustive;
|
|
@@ -1647,11 +1143,7 @@ function defineSection(input) {
|
|
|
1647
1143
|
...input.category !== void 0 ? { category: input.category } : {},
|
|
1648
1144
|
schema: input.schema,
|
|
1649
1145
|
component: input.component,
|
|
1650
|
-
defaults
|
|
1651
|
-
// Conditional spread mirrors the `category` pattern — required so
|
|
1652
|
-
// `exactOptionalPropertyTypes` does not see `layouts: undefined` being
|
|
1653
|
-
// assigned to an optional-only property.
|
|
1654
|
-
...input.layouts !== void 0 ? { layouts: input.layouts } : {}
|
|
1146
|
+
defaults
|
|
1655
1147
|
};
|
|
1656
1148
|
}
|
|
1657
1149
|
|
|
@@ -1752,104 +1244,13 @@ function renderFallback(fallback) {
|
|
|
1752
1244
|
return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
1753
1245
|
}
|
|
1754
1246
|
|
|
1755
|
-
// src/forms/defineForm.ts
|
|
1756
|
-
var InvalidFormFieldError = class extends Error {
|
|
1757
|
-
formName;
|
|
1758
|
-
fieldName;
|
|
1759
|
-
fieldKind;
|
|
1760
|
-
constructor(formName, fieldName, fieldKind) {
|
|
1761
|
-
super(
|
|
1762
|
-
`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.`
|
|
1763
|
-
);
|
|
1764
|
-
this.name = "InvalidFormFieldError";
|
|
1765
|
-
this.formName = formName;
|
|
1766
|
-
this.fieldName = fieldName;
|
|
1767
|
-
this.fieldKind = fieldKind;
|
|
1768
|
-
}
|
|
1769
|
-
};
|
|
1770
|
-
var HoneypotCollisionError = class extends Error {
|
|
1771
|
-
formName;
|
|
1772
|
-
fieldName;
|
|
1773
|
-
reason;
|
|
1774
|
-
constructor(formName, fieldName, reason = "collision") {
|
|
1775
|
-
super(
|
|
1776
|
-
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.`
|
|
1777
|
-
);
|
|
1778
|
-
this.name = "HoneypotCollisionError";
|
|
1779
|
-
this.formName = formName;
|
|
1780
|
-
this.fieldName = fieldName;
|
|
1781
|
-
this.reason = reason;
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
1784
|
-
var InvalidFormNameError = class extends Error {
|
|
1785
|
-
constructor(name) {
|
|
1786
|
-
super(
|
|
1787
|
-
`Invalid form name ${JSON.stringify(name)}. Use letters, digits, hyphen, and underscore only (no path separators).`
|
|
1788
|
-
);
|
|
1789
|
-
this.name = "InvalidFormNameError";
|
|
1790
|
-
}
|
|
1791
|
-
};
|
|
1792
|
-
var FORM_NAME_PATTERN2 = /^[a-zA-Z0-9_-]+$/;
|
|
1793
|
-
function defineForm(input) {
|
|
1794
|
-
if (typeof input.name !== "string" || !FORM_NAME_PATTERN2.test(input.name)) {
|
|
1795
|
-
throw new InvalidFormNameError(input.name);
|
|
1796
|
-
}
|
|
1797
|
-
for (const [fieldName, descriptor] of Object.entries(input.schema)) {
|
|
1798
|
-
const kind = descriptor.kind;
|
|
1799
|
-
if (FORM_FORBIDDEN_KINDS.has(kind)) {
|
|
1800
|
-
throw new InvalidFormFieldError(input.name, fieldName, kind);
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
if (input.honeypot !== void 0) {
|
|
1804
|
-
if (typeof input.honeypot !== "string" || input.honeypot === "") {
|
|
1805
|
-
throw new HoneypotCollisionError(input.name, input.honeypot, "empty");
|
|
1806
|
-
}
|
|
1807
|
-
if (Object.prototype.hasOwnProperty.call(input.schema, input.honeypot)) {
|
|
1808
|
-
throw new HoneypotCollisionError(input.name, input.honeypot, "collision");
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
return input.honeypot !== void 0 ? { name: input.name, schema: input.schema, honeypot: input.honeypot } : { name: input.name, schema: input.schema };
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
// src/forms/registry.ts
|
|
1815
|
-
var DuplicateFormNameError = class extends Error {
|
|
1816
|
-
formName;
|
|
1817
|
-
constructor(name) {
|
|
1818
|
-
super(
|
|
1819
|
-
`Form name "${name}" is registered more than once. Form names must be unique within a config.`
|
|
1820
|
-
);
|
|
1821
|
-
this.name = "DuplicateFormNameError";
|
|
1822
|
-
this.formName = name;
|
|
1823
|
-
}
|
|
1824
|
-
};
|
|
1825
|
-
function buildFormRegistry(definitions) {
|
|
1826
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1827
|
-
for (const def of definitions) {
|
|
1828
|
-
if (byName.has(def.name)) {
|
|
1829
|
-
throw new DuplicateFormNameError(def.name);
|
|
1830
|
-
}
|
|
1831
|
-
byName.set(def.name, def);
|
|
1832
|
-
}
|
|
1833
|
-
const registry = {
|
|
1834
|
-
definitions,
|
|
1835
|
-
get: (name) => byName.get(name),
|
|
1836
|
-
has: (name) => byName.has(name)
|
|
1837
|
-
};
|
|
1838
|
-
return Object.freeze(registry);
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
1247
|
// src/server.ts
|
|
1842
1248
|
installDefaultAdapterFactories();
|
|
1843
1249
|
export {
|
|
1844
1250
|
BooleanField,
|
|
1845
1251
|
ButtonField,
|
|
1846
|
-
DuplicateFormNameError,
|
|
1847
|
-
FormOverridesField,
|
|
1848
1252
|
GlobalSlot,
|
|
1849
|
-
HoneypotCollisionError,
|
|
1850
1253
|
ImageField,
|
|
1851
|
-
InvalidFormFieldError,
|
|
1852
|
-
InvalidFormNameError,
|
|
1853
1254
|
LinkField,
|
|
1854
1255
|
ListField,
|
|
1855
1256
|
NOT_FOUND_PAGE_SLUG,
|
|
@@ -1858,22 +1259,14 @@ export {
|
|
|
1858
1259
|
RichTextField,
|
|
1859
1260
|
SERVER_ERROR_PAGE_SLUG,
|
|
1860
1261
|
SelectField,
|
|
1861
|
-
SubmissionsNotReadableError,
|
|
1862
1262
|
TextField,
|
|
1863
1263
|
VideoField,
|
|
1864
|
-
buildFormRegistry,
|
|
1865
1264
|
createDefaultAssetAdapter,
|
|
1866
1265
|
createDefaultContentAdapter,
|
|
1867
|
-
createDefaultSubmissionAdapter,
|
|
1868
1266
|
createFsAssetAdapter,
|
|
1869
1267
|
createFsContentAdapter,
|
|
1870
|
-
createFsSubmissionAdapter,
|
|
1871
1268
|
createListPages,
|
|
1872
|
-
createRateLimit,
|
|
1873
1269
|
createRuntime,
|
|
1874
|
-
createSubmitForm,
|
|
1875
|
-
createWebhookSubmissionAdapter,
|
|
1876
|
-
defineForm,
|
|
1877
1270
|
defineSection,
|
|
1878
1271
|
getReservedPageSlugViolation,
|
|
1879
1272
|
hasUniqueSectionIds,
|