@marktiderman/genesis-forms 0.8.0 → 0.9.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.
- package/README.md +299 -10
- package/dist/destinations.cjs +202 -39
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +67 -21
- package/dist/destinations.d.ts +67 -21
- package/dist/destinations.js +202 -40
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-Bj23YJlB.d.cts → engine-BaHPC2-1.d.cts} +2 -2
- package/dist/{engine-CzinTJph.d.ts → engine-Da99Tv-E.d.ts} +2 -2
- package/dist/index.cjs +996 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +288 -11
- package/dist/index.d.ts +288 -11
- package/dist/index.js +975 -168
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +563 -173
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -474
- package/dist/server.d.ts +3 -474
- package/dist/server.js +558 -174
- package/dist/server.js.map +1 -1
- package/dist/submit-protocol-CFi-0Qd0.d.ts +840 -0
- package/dist/submit-protocol-D0cOckmK.d.cts +840 -0
- package/dist/testing.cjs +250 -154
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +250 -154
- package/dist/testing.js.map +1 -1
- package/dist/{types-jqDKcmP4.d.cts → types-C0pUfew3.d.cts} +7 -1
- package/dist/{types-jqDKcmP4.d.ts → types-C0pUfew3.d.ts} +7 -1
- package/dist/{validate-DvfQHYnu.d.ts → validate-DpmBssFk.d.ts} +258 -140
- package/dist/{validate-irIPubxc.d.cts → validate-U5LoW3eR.d.cts} +258 -140
- package/package.json +2 -2
- package/dist/upload-protocol-BOe018kL.d.cts +0 -122
- package/dist/upload-protocol-BUO74WkS.d.ts +0 -122
package/dist/destinations.cjs
CHANGED
|
@@ -6,9 +6,12 @@ var DeliveryError = class extends Error {
|
|
|
6
6
|
retryable;
|
|
7
7
|
retryAfterSeconds;
|
|
8
8
|
status;
|
|
9
|
+
/** The receiver asked to be switched off (`DeliveryFailure.disable`); never retryable. */
|
|
10
|
+
disable;
|
|
9
11
|
constructor(message, options = {}) {
|
|
10
12
|
super(message);
|
|
11
|
-
this.
|
|
13
|
+
this.disable = options.disable ?? false;
|
|
14
|
+
this.retryable = !this.disable && (options.retryable ?? false);
|
|
12
15
|
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
13
16
|
this.status = options.status;
|
|
14
17
|
}
|
|
@@ -49,7 +52,8 @@ function deliveryFailure(error) {
|
|
|
49
52
|
retryable: error.retryable,
|
|
50
53
|
error: error.message.slice(0, 500),
|
|
51
54
|
...error.retryAfterSeconds !== void 0 ? { retryAfterSeconds: error.retryAfterSeconds } : {},
|
|
52
|
-
...error.status !== void 0 ? { status: error.status } : {}
|
|
55
|
+
...error.status !== void 0 ? { status: error.status } : {},
|
|
56
|
+
...error.disable ? { disable: true } : {}
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
const name = error instanceof Error ? error.name : "";
|
|
@@ -114,6 +118,12 @@ async function sha256Hex(text) {
|
|
|
114
118
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// src/props-check.ts
|
|
122
|
+
new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
123
|
+
var isPlainRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
124
|
+
var isWholeNumber = (v) => typeof v === "number" && Number.isInteger(v);
|
|
125
|
+
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
126
|
+
|
|
117
127
|
// src/calculated.ts
|
|
118
128
|
var CALCULATED_TYPE = "calculated";
|
|
119
129
|
var MAX_CALCULATED_DECIMALS = 10;
|
|
@@ -142,6 +152,17 @@ var calculatedFieldType = {
|
|
|
142
152
|
value: { json: true },
|
|
143
153
|
operators: numberOps,
|
|
144
154
|
a11y: { semantics: "control" },
|
|
155
|
+
// A number, unless the format says text; without a format it can hold either, so it isn't known.
|
|
156
|
+
literal: (props) => props.format === "text" ? "string" : props.format === "number" || props.format === "percent" ? "number" : void 0,
|
|
157
|
+
// The formula itself is checked once every key is known (`parseForm`).
|
|
158
|
+
checkProps: (props, check) => {
|
|
159
|
+
if (props.format !== void 0 && props.format !== "number" && props.format !== "percent" && props.format !== "text") {
|
|
160
|
+
check.issue("format", "Must be number, percent or text.");
|
|
161
|
+
}
|
|
162
|
+
if (props.decimals !== void 0 && !(isWholeNumber(props.decimals) && props.decimals >= 0 && props.decimals <= MAX_CALCULATED_DECIMALS)) {
|
|
163
|
+
check.issue("decimals", `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
145
166
|
// The engine works the value out; any number, text or true / false it made is valid.
|
|
146
167
|
validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
|
|
147
168
|
summarize: (value, field) => formatCalculated(value, field)
|
|
@@ -165,6 +186,7 @@ function listLimits(field) {
|
|
|
165
186
|
return { min: optionalIntProp(field, "min") ?? 0, max: intProp(field, "max", 10), itemMaxLength: intProp(field, "itemMaxLength", 500) };
|
|
166
187
|
}
|
|
167
188
|
var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
189
|
+
var MAX_TABLE_COLUMNS = 6;
|
|
168
190
|
var MAX_TABLE_CELL_LENGTH = 2e3;
|
|
169
191
|
function tableColumns(field) {
|
|
170
192
|
const raw = field.props?.columns;
|
|
@@ -198,12 +220,42 @@ var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
|
|
|
198
220
|
var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
199
221
|
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
200
222
|
var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
|
|
223
|
+
var listProps = (props, check) => {
|
|
224
|
+
check.optionalInt(props.min, "min", 0);
|
|
225
|
+
check.optionalInt(props.max, "max", 1);
|
|
226
|
+
check.optionalInt(props.itemMaxLength, "itemMaxLength", 1);
|
|
227
|
+
check.optionalText(props.placeholder, "placeholder");
|
|
228
|
+
check.ordered(props.min, props.max ?? 10, "min", "most items");
|
|
229
|
+
};
|
|
230
|
+
var tableProps = (props, check) => {
|
|
231
|
+
const columns = props.columns;
|
|
232
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
233
|
+
check.issue("columns", `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
234
|
+
} else {
|
|
235
|
+
const seen = /* @__PURE__ */ new Set();
|
|
236
|
+
columns.forEach((c, j) => {
|
|
237
|
+
const at = `columns[${j}]`;
|
|
238
|
+
if (!isPlainRecord(c)) return check.issue(at, "Each column needs a key and a label.");
|
|
239
|
+
if (check.name(c.key, `${at}.key`, "A column key")) {
|
|
240
|
+
if (seen.has(c.key)) check.issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
241
|
+
seen.add(c.key);
|
|
242
|
+
}
|
|
243
|
+
check.text(c.label, `${at}.label`);
|
|
244
|
+
check.optionalBoolean(c.long, `${at}.long`);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
check.optionalInt(props.minRows, "minRows", 0);
|
|
248
|
+
check.optionalInt(props.maxRows, "maxRows", 1);
|
|
249
|
+
check.ordered(props.minRows, props.maxRows ?? 3, "minRows", "most rows");
|
|
250
|
+
};
|
|
201
251
|
var structuredFieldTypes = [
|
|
202
252
|
{
|
|
203
253
|
type: "list",
|
|
204
254
|
label: "List",
|
|
205
255
|
category: "text",
|
|
206
256
|
value: { json: true },
|
|
257
|
+
literal: "string",
|
|
258
|
+
checkProps: listProps,
|
|
207
259
|
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
208
260
|
operators: ["contains", "answered", "empty"],
|
|
209
261
|
a11y: { semantics: "group" },
|
|
@@ -229,6 +281,7 @@ var structuredFieldTypes = [
|
|
|
229
281
|
label: "Table",
|
|
230
282
|
category: "advanced",
|
|
231
283
|
value: { json: true },
|
|
284
|
+
checkProps: tableProps,
|
|
232
285
|
operators: ["answered", "empty"],
|
|
233
286
|
a11y: { semantics: "group" },
|
|
234
287
|
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
@@ -272,6 +325,10 @@ var structuredFieldTypes = [
|
|
|
272
325
|
label: "Notes at moments",
|
|
273
326
|
category: "media",
|
|
274
327
|
value: { json: true },
|
|
328
|
+
checkProps: (props, check) => {
|
|
329
|
+
check.optionalInt(props.maxNotes, "maxNotes", 1);
|
|
330
|
+
check.optionalInt(props.textMaxLength, "textMaxLength", 1);
|
|
331
|
+
},
|
|
275
332
|
operators: ["answered", "empty"],
|
|
276
333
|
a11y: { semantics: "group" },
|
|
277
334
|
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
@@ -295,6 +352,10 @@ var structuredFieldTypes = [
|
|
|
295
352
|
label: "Highlights",
|
|
296
353
|
category: "media",
|
|
297
354
|
value: { json: true },
|
|
355
|
+
checkProps: (props, check) => {
|
|
356
|
+
check.optionalText(props.text, "text");
|
|
357
|
+
check.optionalInt(props.maxHighlights, "maxHighlights", 1);
|
|
358
|
+
},
|
|
298
359
|
operators: ["answered", "empty"],
|
|
299
360
|
a11y: { semantics: "group" },
|
|
300
361
|
isEmpty: (value) => emptyList(value, () => false),
|
|
@@ -344,6 +405,7 @@ var DEFAULT_PHOTO_SIZE_MB = 10;
|
|
|
344
405
|
var MAX_PHOTO_SIZE_MB = 25;
|
|
345
406
|
var MAX_PHOTO_ID_LENGTH = 200;
|
|
346
407
|
var MAX_PHOTO_NAME_LENGTH = 255;
|
|
408
|
+
var isPhotoFormat = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(PHOTO_FORMATS, v);
|
|
347
409
|
var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
348
410
|
function photoLimits(field) {
|
|
349
411
|
const maxFiles = wholeIn(field.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field.props.maxFiles : DEFAULT_PHOTO_FILES;
|
|
@@ -385,6 +447,22 @@ var photoFieldType = {
|
|
|
385
447
|
value: { json: true },
|
|
386
448
|
operators: ["answered", "empty"],
|
|
387
449
|
a11y: { semantics: "group" },
|
|
450
|
+
checkProps: (props, check) => {
|
|
451
|
+
if (props.maxFiles !== void 0 && !(isWholeNumber(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
|
|
452
|
+
check.issue("maxFiles", `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
453
|
+
}
|
|
454
|
+
if (props.maxSizeMb !== void 0 && !(isWholeNumber(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
455
|
+
check.issue("maxSizeMb", `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
456
|
+
}
|
|
457
|
+
if (props.formats !== void 0) {
|
|
458
|
+
const formats = props.formats;
|
|
459
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
460
|
+
check.issue("formats", "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
461
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
462
|
+
check.issue("formats", "Name each format once.");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
388
466
|
isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
|
|
389
467
|
validate: (value, field) => {
|
|
390
468
|
if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
|
|
@@ -402,22 +480,37 @@ var photoFieldType = {
|
|
|
402
480
|
};
|
|
403
481
|
|
|
404
482
|
// src/fields.ts
|
|
405
|
-
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
483
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types@2");
|
|
484
|
+
var BUILT_IN = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/built-in-field-types@2");
|
|
406
485
|
var globalScope = globalThis;
|
|
407
486
|
var registry = globalScope[REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
);
|
|
487
|
+
var builtIn = globalScope[BUILT_IN] ??= /* @__PURE__ */ new Set();
|
|
488
|
+
var freezeRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.freeze({ ...v }) : v;
|
|
489
|
+
function frozen(manifest) {
|
|
490
|
+
const copy = { ...manifest };
|
|
491
|
+
for (const key of ["choice", "a11y", "value"]) if (key in copy) copy[key] = freezeRecord(copy[key]);
|
|
492
|
+
if (Array.isArray(copy.operators)) {
|
|
493
|
+
const operators = Object.freeze([...copy.operators]);
|
|
494
|
+
Object.defineProperty(copy, "operators", { get: () => [...operators], enumerable: true });
|
|
415
495
|
}
|
|
416
|
-
|
|
496
|
+
return Object.freeze(copy);
|
|
497
|
+
}
|
|
498
|
+
function registerBuiltIn(manifest) {
|
|
499
|
+
builtIn.add(manifest.type);
|
|
500
|
+
if (!registry.has(manifest.type)) registry.set(manifest.type, frozen(manifest));
|
|
417
501
|
}
|
|
418
502
|
function getFieldType(type) {
|
|
419
503
|
return registry.get(type);
|
|
420
504
|
}
|
|
505
|
+
var CONTENT_REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/content-types@1");
|
|
506
|
+
var contentScope = globalThis;
|
|
507
|
+
var contentRegistry = contentScope[CONTENT_REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
508
|
+
for (const manifest of [
|
|
509
|
+
{ type: "heading", label: "Heading" },
|
|
510
|
+
{ type: "text", label: "Text" }
|
|
511
|
+
]) {
|
|
512
|
+
if (!contentRegistry.has(manifest.type)) contentRegistry.set(manifest.type, frozen(manifest));
|
|
513
|
+
}
|
|
421
514
|
function isEmptyValue(value, field) {
|
|
422
515
|
const manifest = field ? registry.get(field.type) : void 0;
|
|
423
516
|
if (manifest?.isEmpty) return manifest.isEmpty(value);
|
|
@@ -550,6 +643,18 @@ function isPhoneNumber(value) {
|
|
|
550
643
|
}
|
|
551
644
|
return digits >= 7 && digits <= 15;
|
|
552
645
|
}
|
|
646
|
+
var oneOptionValidate = (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.";
|
|
647
|
+
var oneOptionSummary = (value, field) => optionLabel(field, String(value));
|
|
648
|
+
var optionsValidate = (value, field) => {
|
|
649
|
+
const allowed = new Set(choiceOptions(field).map((o) => o.value));
|
|
650
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
651
|
+
return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field, others[0])) ? null : "Choose from the options.";
|
|
652
|
+
};
|
|
653
|
+
var optionsSummary = (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value);
|
|
654
|
+
var scaleProps = (props, check) => {
|
|
655
|
+
check.optionalText(props.lowLabel, "lowLabel");
|
|
656
|
+
check.optionalText(props.highLabel, "highLabel");
|
|
657
|
+
};
|
|
553
658
|
var builtins = [
|
|
554
659
|
{
|
|
555
660
|
type: "short-text",
|
|
@@ -558,6 +663,7 @@ var builtins = [
|
|
|
558
663
|
value: { json: true },
|
|
559
664
|
operators: textOps,
|
|
560
665
|
a11y: { semantics: "control" },
|
|
666
|
+
literal: "string",
|
|
561
667
|
validate: textValidate(500)
|
|
562
668
|
},
|
|
563
669
|
{
|
|
@@ -567,6 +673,7 @@ var builtins = [
|
|
|
567
673
|
value: { json: true },
|
|
568
674
|
operators: textOps,
|
|
569
675
|
a11y: { semantics: "control" },
|
|
676
|
+
literal: "string",
|
|
570
677
|
validate: textValidate(5e3)
|
|
571
678
|
},
|
|
572
679
|
{
|
|
@@ -576,6 +683,7 @@ var builtins = [
|
|
|
576
683
|
value: { json: true },
|
|
577
684
|
operators: textOps,
|
|
578
685
|
a11y: { semantics: "control", autocomplete: "email", inputMode: "email" },
|
|
686
|
+
literal: "string",
|
|
579
687
|
validate: (value) => typeof value === "string" && isEmailAddress(value.trim()) ? null : "Enter an email address like name@example.com.",
|
|
580
688
|
normalize: (value) => typeof value === "string" ? value.trim() : value
|
|
581
689
|
},
|
|
@@ -586,6 +694,7 @@ var builtins = [
|
|
|
586
694
|
value: { json: true },
|
|
587
695
|
operators: ["eq", "neq", "answered", "empty"],
|
|
588
696
|
a11y: { semantics: "control", autocomplete: "tel", inputMode: "tel" },
|
|
697
|
+
literal: "string",
|
|
589
698
|
validate: (value) => typeof value === "string" && isPhoneNumber(value.trim()) ? null : "Enter a phone number with 7 to 15 digits.",
|
|
590
699
|
normalize: (value) => typeof value === "string" ? value.trim() : value,
|
|
591
700
|
summarize: (value) => String(value)
|
|
@@ -597,6 +706,13 @@ var builtins = [
|
|
|
597
706
|
value: { json: true },
|
|
598
707
|
operators: numberOps2,
|
|
599
708
|
a11y: { semantics: "group" },
|
|
709
|
+
literal: "number",
|
|
710
|
+
checkProps: (props, check) => {
|
|
711
|
+
scaleProps(props, check);
|
|
712
|
+
if (props.display !== void 0 && props.display !== "numbers" && props.display !== "stars") {
|
|
713
|
+
check.issue("display", "Must be numbers or stars.");
|
|
714
|
+
}
|
|
715
|
+
},
|
|
600
716
|
validate: (value, field) => {
|
|
601
717
|
const { min, max } = intRange(field, 1, 5);
|
|
602
718
|
return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
|
|
@@ -614,6 +730,8 @@ var builtins = [
|
|
|
614
730
|
value: { json: true },
|
|
615
731
|
operators: numberOps2,
|
|
616
732
|
a11y: { semantics: "group" },
|
|
733
|
+
literal: "number",
|
|
734
|
+
checkProps: scaleProps,
|
|
617
735
|
validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
|
|
618
736
|
summarize: (value, field) => {
|
|
619
737
|
const end = scaleEndLabel(field, value, 0, 10);
|
|
@@ -627,8 +745,10 @@ var builtins = [
|
|
|
627
745
|
value: { json: true },
|
|
628
746
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
629
747
|
a11y: { semantics: "group" },
|
|
630
|
-
|
|
631
|
-
|
|
748
|
+
literal: "string",
|
|
749
|
+
choice: { multiple: false },
|
|
750
|
+
validate: oneOptionValidate,
|
|
751
|
+
summarize: oneOptionSummary
|
|
632
752
|
},
|
|
633
753
|
{
|
|
634
754
|
type: "multi-choice",
|
|
@@ -637,12 +757,10 @@ var builtins = [
|
|
|
637
757
|
value: { json: true },
|
|
638
758
|
operators: ["contains", "answered", "empty"],
|
|
639
759
|
a11y: { semantics: "group" },
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
},
|
|
645
|
-
summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
|
|
760
|
+
literal: "string",
|
|
761
|
+
choice: { multiple: true },
|
|
762
|
+
validate: optionsValidate,
|
|
763
|
+
summarize: optionsSummary
|
|
646
764
|
},
|
|
647
765
|
{
|
|
648
766
|
type: "yes-no",
|
|
@@ -651,6 +769,7 @@ var builtins = [
|
|
|
651
769
|
value: { json: true },
|
|
652
770
|
operators: ["eq", "neq", "answered", "empty"],
|
|
653
771
|
a11y: { semantics: "group" },
|
|
772
|
+
literal: "boolean",
|
|
654
773
|
validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
|
|
655
774
|
summarize: (value) => value ? "Yes" : "No"
|
|
656
775
|
},
|
|
@@ -661,8 +780,10 @@ var builtins = [
|
|
|
661
780
|
value: { json: true },
|
|
662
781
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
663
782
|
a11y: { semantics: "control" },
|
|
664
|
-
|
|
665
|
-
|
|
783
|
+
literal: "string",
|
|
784
|
+
choice: { multiple: false },
|
|
785
|
+
validate: oneOptionValidate,
|
|
786
|
+
summarize: oneOptionSummary
|
|
666
787
|
},
|
|
667
788
|
{
|
|
668
789
|
type: "number",
|
|
@@ -671,6 +792,16 @@ var builtins = [
|
|
|
671
792
|
value: { json: true },
|
|
672
793
|
operators: numberOps2,
|
|
673
794
|
a11y: { semantics: "control", inputMode: "decimal" },
|
|
795
|
+
literal: "number",
|
|
796
|
+
checkProps: (props, check) => {
|
|
797
|
+
for (const k of ["min", "max", "step"]) {
|
|
798
|
+
if (props[k] !== void 0 && !isFiniteNumber(props[k])) check.issue(k, "Must be a number.");
|
|
799
|
+
}
|
|
800
|
+
if (isFiniteNumber(props.step) && props.step <= 0) check.issue("step", "Must be above 0.");
|
|
801
|
+
if (isFiniteNumber(props.min) && isFiniteNumber(props.max) && props.min > props.max) {
|
|
802
|
+
check.issue("min", "Can't be more than the largest number.");
|
|
803
|
+
}
|
|
804
|
+
},
|
|
674
805
|
validate: (value, field) => {
|
|
675
806
|
if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
|
|
676
807
|
const { min, max, step } = numberLimits(field);
|
|
@@ -697,6 +828,17 @@ var builtins = [
|
|
|
697
828
|
value: { json: true },
|
|
698
829
|
operators: ["eq", "neq", "answered", "empty"],
|
|
699
830
|
a11y: { semantics: "control" },
|
|
831
|
+
literal: "string",
|
|
832
|
+
checkProps: (props, check) => {
|
|
833
|
+
for (const k of ["min", "max"]) {
|
|
834
|
+
if (props[k] !== void 0 && !(typeof props[k] === "string" && isIsoDate(props[k]))) {
|
|
835
|
+
check.issue(k, "Must be a date like 2026-10-22.");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (typeof props.min === "string" && typeof props.max === "string" && isIsoDate(props.min) && isIsoDate(props.max) && props.min > props.max) {
|
|
839
|
+
check.issue("min", "Can't be after the latest date.");
|
|
840
|
+
}
|
|
841
|
+
},
|
|
700
842
|
validate: (value, field) => {
|
|
701
843
|
if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
|
|
702
844
|
const date = value.trim();
|
|
@@ -719,6 +861,8 @@ var builtins = [
|
|
|
719
861
|
value: { json: true },
|
|
720
862
|
operators: ["answered", "empty"],
|
|
721
863
|
a11y: { semantics: "group" },
|
|
864
|
+
literal: "boolean",
|
|
865
|
+
checkProps: (props, check) => check.optionalText(props.text, "text"),
|
|
722
866
|
// Unchecked is unanswered, so a required consent must be checked.
|
|
723
867
|
isEmpty: (value) => value === void 0 || value === null || value === false,
|
|
724
868
|
validate: (value) => value === true ? null : "Check the box to agree.",
|
|
@@ -768,7 +912,7 @@ function dateLimits(field) {
|
|
|
768
912
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
769
913
|
return out;
|
|
770
914
|
}
|
|
771
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType])
|
|
915
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerBuiltIn(manifest);
|
|
772
916
|
|
|
773
917
|
// src/logic.ts
|
|
774
918
|
function own(record, key) {
|
|
@@ -1529,7 +1673,7 @@ function hookHost(url) {
|
|
|
1529
1673
|
}
|
|
1530
1674
|
|
|
1531
1675
|
// src/destinations/webhook.ts
|
|
1532
|
-
var
|
|
1676
|
+
var MIN_WEBHOOK_SECRET_BYTES = 24;
|
|
1533
1677
|
var STRICT_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
1534
1678
|
function secretKey(secret) {
|
|
1535
1679
|
const encoded = secret.startsWith("whsec_") ? secret.slice(6) : secret;
|
|
@@ -1539,7 +1683,9 @@ function secretKey(secret) {
|
|
|
1539
1683
|
);
|
|
1540
1684
|
}
|
|
1541
1685
|
const bytes = fromBase64(encoded);
|
|
1542
|
-
if (bytes.length <
|
|
1686
|
+
if (bytes.length < MIN_WEBHOOK_SECRET_BYTES) {
|
|
1687
|
+
throw new DeliveryError(`The webhook secret is shorter than ${MIN_WEBHOOK_SECRET_BYTES} bytes`);
|
|
1688
|
+
}
|
|
1543
1689
|
return bytes;
|
|
1544
1690
|
}
|
|
1545
1691
|
async function hmacKey(secret, usage) {
|
|
@@ -1549,9 +1695,15 @@ function newWebhookSecret() {
|
|
|
1549
1695
|
return `whsec_${base64(crypto.getRandomValues(new Uint8Array(32)))}`;
|
|
1550
1696
|
}
|
|
1551
1697
|
async function signWebhook(secret, id, timestamp, body) {
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1698
|
+
const secrets = typeof secret === "string" ? [secret] : secret;
|
|
1699
|
+
if (secrets.length === 0) throw new DeliveryError("The webhook secret is not set");
|
|
1700
|
+
const data = new TextEncoder().encode(`${id}.${timestamp}.${body}`);
|
|
1701
|
+
const signatures = [];
|
|
1702
|
+
for (const one of secrets) {
|
|
1703
|
+
const key = await hmacKey(one, "sign");
|
|
1704
|
+
signatures.push(`v1,${base64(new Uint8Array(await crypto.subtle.sign("HMAC", key, data)))}`);
|
|
1705
|
+
}
|
|
1706
|
+
return signatures.join(" ");
|
|
1555
1707
|
}
|
|
1556
1708
|
async function verifyWebhook(input) {
|
|
1557
1709
|
const header = (name) => {
|
|
@@ -1565,12 +1717,14 @@ async function verifyWebhook(input) {
|
|
|
1565
1717
|
if (!id || !/^\d{1,12}$/.test(timestamp) || !signatures) return false;
|
|
1566
1718
|
const now = Math.floor((input.now?.() ?? Date.now()) / 1e3);
|
|
1567
1719
|
if (Math.abs(now - Number(timestamp)) > (input.toleranceSeconds ?? 300)) return false;
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1720
|
+
const keys = [];
|
|
1721
|
+
for (const secret of typeof input.secret === "string" ? [input.secret] : input.secret) {
|
|
1722
|
+
try {
|
|
1723
|
+
keys.push(await hmacKey(secret, "verify"));
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1573
1726
|
}
|
|
1727
|
+
if (keys.length === 0) return false;
|
|
1574
1728
|
const data = new TextEncoder().encode(`${id}.${timestamp}.${input.body}`);
|
|
1575
1729
|
for (const part of signatures.split(" ")) {
|
|
1576
1730
|
const [version, value] = part.split(",", 2);
|
|
@@ -1581,7 +1735,7 @@ async function verifyWebhook(input) {
|
|
|
1581
1735
|
} catch {
|
|
1582
1736
|
continue;
|
|
1583
1737
|
}
|
|
1584
|
-
if (await crypto.subtle.verify("HMAC", key, mac, data)) return true;
|
|
1738
|
+
for (const key of keys) if (await crypto.subtle.verify("HMAC", key, mac, data)) return true;
|
|
1585
1739
|
}
|
|
1586
1740
|
return false;
|
|
1587
1741
|
}
|
|
@@ -1646,9 +1800,10 @@ function webhookPayload(submission, definition, options = {}) {
|
|
|
1646
1800
|
for (const [name, value] of Object.entries(source ?? {})) if (own(allowed, name) !== void 0) out[name] = value;
|
|
1647
1801
|
return out;
|
|
1648
1802
|
};
|
|
1803
|
+
const happened = submission.submittedAt ? Date.parse(submission.submittedAt) : Number.NaN;
|
|
1649
1804
|
return {
|
|
1650
1805
|
type: options.type ?? "form.submitted",
|
|
1651
|
-
timestamp: new Date(options.now ?? Date.now()).toISOString(),
|
|
1806
|
+
timestamp: new Date(Number.isNaN(happened) ? options.now ?? Date.now() : happened).toISOString(),
|
|
1652
1807
|
data: {
|
|
1653
1808
|
form: { id: definition.id, title: definition.title },
|
|
1654
1809
|
submission: {
|
|
@@ -1675,7 +1830,7 @@ function webhookDestination(options) {
|
|
|
1675
1830
|
const url = checkWebhookUrl(typeof raw === "string" ? raw.trim() : "", options);
|
|
1676
1831
|
if (options.resolveHost && !options.allowPrivateHosts) await checkHookHost(url, options.resolveHost);
|
|
1677
1832
|
const secret = await resolveOption("The webhook secret", options.secret, config, context);
|
|
1678
|
-
if (!secret) throw new DeliveryError("The webhook secret is not set");
|
|
1833
|
+
if (!secret || secret.length === 0) throw new DeliveryError("The webhook secret is not set");
|
|
1679
1834
|
const now = options.now?.() ?? Date.now();
|
|
1680
1835
|
const id = deliveryOptions.deliveryId ?? submission.id;
|
|
1681
1836
|
const timestamp = Math.floor(now / 1e3);
|
|
@@ -1700,6 +1855,9 @@ function webhookDestination(options) {
|
|
|
1700
1855
|
);
|
|
1701
1856
|
discard(response);
|
|
1702
1857
|
if (response.status >= 200 && response.status < 300) return { ok: true };
|
|
1858
|
+
if (response.status === 410) {
|
|
1859
|
+
throw new DeliveryError("Webhook answered 410: the endpoint is gone; disable the destination", { status: 410, disable: true });
|
|
1860
|
+
}
|
|
1703
1861
|
if (response.status === 0 || response.status >= 300 && response.status < 400) {
|
|
1704
1862
|
throw new DeliveryError(`Webhook answered ${response.status || "a redirect"}: redirects are not followed`, {
|
|
1705
1863
|
...response.status ? { status: response.status } : {}
|
|
@@ -1902,11 +2060,12 @@ function secretKeyBytes(key) {
|
|
|
1902
2060
|
if (bytes.length !== 32) throw new DeliveryError("The encryption key must be 32 bytes");
|
|
1903
2061
|
return bytes;
|
|
1904
2062
|
}
|
|
1905
|
-
async function deriveSecretKey(key, info) {
|
|
2063
|
+
async function deriveSecretKey(key, info, options = {}) {
|
|
1906
2064
|
if (!info) throw new DeliveryError("A sealing key needs its purpose (info)");
|
|
2065
|
+
const salt = typeof options.salt === "string" ? enc.encode(options.salt) : new Uint8Array(options.salt ?? new Uint8Array(0));
|
|
1907
2066
|
const base = await crypto.subtle.importKey("raw", secretKeyBytes(key), "HKDF", false, ["deriveKey"]);
|
|
1908
2067
|
return crypto.subtle.deriveKey(
|
|
1909
|
-
{ name: "HKDF", hash: "SHA-256", salt
|
|
2068
|
+
{ name: "HKDF", hash: "SHA-256", salt, info: enc.encode(info) },
|
|
1910
2069
|
base,
|
|
1911
2070
|
{ name: "AES-GCM", length: 256 },
|
|
1912
2071
|
false,
|
|
@@ -1940,12 +2099,15 @@ function padded(text) {
|
|
|
1940
2099
|
var DEFAULT_MAX_ATTEMPTS = 8;
|
|
1941
2100
|
var DEFAULT_MAX_DEFERRALS = 24;
|
|
1942
2101
|
var DEFAULT_BATCH_SIZE = 25;
|
|
1943
|
-
function defaultBackoff(attempt) {
|
|
1944
|
-
|
|
2102
|
+
function defaultBackoff(attempt, random) {
|
|
2103
|
+
const nominal = Math.min(24 * 3600, 300 * 4 ** Math.max(0, attempt - 1));
|
|
2104
|
+
const r = typeof random === "function" ? random() : Math.random();
|
|
2105
|
+
return Math.round(nominal * (0.8 + 0.4 * Math.min(1, Math.max(0, r))));
|
|
1945
2106
|
}
|
|
1946
2107
|
function deliveryOutcome(result, attempt, options = {}) {
|
|
1947
2108
|
if (result.ok) return result.notSent ? { status: "skipped", reason: result.notSent.slice(0, 500) } : { status: "sent" };
|
|
1948
2109
|
const http = result.status !== void 0 ? { httpStatus: result.status } : {};
|
|
2110
|
+
if (result.disable) return { status: "failed", error: result.error, ...http, disable: true };
|
|
1949
2111
|
if (!result.retryable) return { status: "failed", error: result.error, ...http };
|
|
1950
2112
|
const now = options.now ?? Date.now();
|
|
1951
2113
|
if (result.retryAfterSeconds !== void 0 && (options.deferrals ?? 0) < (options.maxDeferrals ?? DEFAULT_MAX_DEFERRALS)) {
|
|
@@ -2106,6 +2268,7 @@ exports.HookUrlError = HookUrlError;
|
|
|
2106
2268
|
exports.MAX_CONDITION_DEPTH = MAX_CONDITION_DEPTH;
|
|
2107
2269
|
exports.MAX_HOOK_URL_LENGTH = MAX_HOOK_URL_LENGTH;
|
|
2108
2270
|
exports.MAX_RECIPIENTS = MAX_RECIPIENTS;
|
|
2271
|
+
exports.MIN_WEBHOOK_SECRET_BYTES = MIN_WEBHOOK_SECRET_BYTES;
|
|
2109
2272
|
exports.REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS;
|
|
2110
2273
|
exports.RESEND_API = RESEND_API;
|
|
2111
2274
|
exports.SENSITIVE_MASK = SENSITIVE_MASK;
|