@marktiderman/genesis-forms 0.4.2 → 0.6.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/dist/server.js CHANGED
@@ -719,6 +719,80 @@ var structuredFieldTypes = [
719
719
  }
720
720
  ];
721
721
 
722
+ // src/photo.ts
723
+ var PHOTO_TYPE = "photo";
724
+ var PHOTO_FORMATS = {
725
+ jpeg: ["image/jpeg"],
726
+ png: ["image/png"],
727
+ heic: ["image/heic", "image/heif"],
728
+ webp: ["image/webp"]
729
+ };
730
+ var FORMAT_NAMES = { jpeg: "JPEG", png: "PNG", heic: "HEIC", webp: "WebP" };
731
+ var ALL_FORMATS = Object.keys(PHOTO_FORMATS);
732
+ var DEFAULT_PHOTO_FILES = 3;
733
+ var MAX_PHOTO_FILES = 5;
734
+ var DEFAULT_PHOTO_SIZE_MB = 10;
735
+ var MAX_PHOTO_SIZE_MB = 25;
736
+ var MAX_PHOTO_ID_LENGTH = 200;
737
+ var MAX_PHOTO_NAME_LENGTH = 255;
738
+ var isPhotoFormat = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(PHOTO_FORMATS, v);
739
+ var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
740
+ function photoLimits(field) {
741
+ const maxFiles = wholeIn(field.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field.props.maxFiles : DEFAULT_PHOTO_FILES;
742
+ const maxSizeMb = wholeIn(field.props?.maxSizeMb, 1, MAX_PHOTO_SIZE_MB) ? field.props.maxSizeMb : DEFAULT_PHOTO_SIZE_MB;
743
+ const raw = field.props?.formats;
744
+ const picked = Array.isArray(raw) ? ALL_FORMATS.filter((f) => raw.includes(f)) : [];
745
+ const formats = picked.length ? picked : [...ALL_FORMATS];
746
+ return {
747
+ maxFiles,
748
+ maxSizeMb,
749
+ maxBytes: maxSizeMb * 1024 * 1024,
750
+ formats,
751
+ mimeTypes: formats.flatMap((f) => PHOTO_FORMATS[f])
752
+ };
753
+ }
754
+ function isPhotoRef(v) {
755
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
756
+ const r = v;
757
+ return typeof r.id === "string" && r.id.length > 0 && r.id.length <= MAX_PHOTO_ID_LENGTH && typeof r.name === "string" && r.name.length <= MAX_PHOTO_NAME_LENGTH && typeof r.type === "string" && typeof r.size === "number" && Number.isInteger(r.size) && r.size > 0;
758
+ }
759
+ function photoRefs(value) {
760
+ if (!Array.isArray(value)) return [];
761
+ const refs = [];
762
+ for (const r of value) if (isPhotoRef(r)) refs.push({ id: r.id, name: r.name, type: r.type, size: r.size });
763
+ return refs;
764
+ }
765
+ var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
766
+ function photoFormatList(formats) {
767
+ const names = formats.map((f) => FORMAT_NAMES[f]);
768
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
769
+ }
770
+ var photoTypeMessage = (field) => `Use a ${photoFormatList(photoLimits(field).formats)} photo.`;
771
+ var photoSizeMessage = (field) => `Each photo must be ${photoLimits(field).maxSizeMb} MB or smaller.`;
772
+ var photoCountMessage = (field) => `Add at most ${plural2(photoLimits(field).maxFiles, "photo")}.`;
773
+ var photoFieldType = {
774
+ type: PHOTO_TYPE,
775
+ label: "Photo",
776
+ category: "media",
777
+ value: { json: true },
778
+ operators: ["answered", "empty"],
779
+ a11y: { semantics: "group" },
780
+ isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
781
+ validate: (value, field) => {
782
+ if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
783
+ const limits = photoLimits(field);
784
+ if (value.length > limits.maxFiles) return photoCountMessage(field);
785
+ const refs = value;
786
+ if (new Set(refs.map((r) => r.id)).size !== refs.length) return "Each photo can be added once.";
787
+ if (refs.some((r) => !limits.mimeTypes.includes(r.type.toLowerCase()))) return photoTypeMessage(field);
788
+ if (refs.some((r) => r.size > limits.maxBytes)) return photoSizeMessage(field);
789
+ return null;
790
+ },
791
+ normalize: (value) => Array.isArray(value) && value.every(isPhotoRef) ? photoRefs(value) : value,
792
+ summarize: (value) => photoRefs(value).map((r) => r.name || "Photo").join(", "),
793
+ samples: () => [[{ id: "sample-photo", name: "photo.jpg", type: "image/jpeg", size: 1024 }]]
794
+ };
795
+
722
796
  // src/fields.ts
723
797
  var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
724
798
  var globalScope = globalThis;
@@ -755,12 +829,127 @@ function summarizeValue(value, field) {
755
829
  if (Array.isArray(value)) return value.join(", ");
756
830
  return String(value);
757
831
  }
832
+ var OPTION_LIMITS = {
833
+ /** Characters in a description. */
834
+ description: 500,
835
+ /** Characters in an image URL. */
836
+ image: 2048,
837
+ /** Attributes on one option. */
838
+ attributes: 20,
839
+ /** Characters in an attribute's name. */
840
+ attributeName: 64,
841
+ /** Characters in one attribute value. */
842
+ attributeValue: 200,
843
+ /** Values in one attribute's list. */
844
+ attributeItems: 50
845
+ };
846
+ function isHttpsUrl(url) {
847
+ return typeof url === "string" && url.length <= OPTION_LIMITS.image && /^https:\/\/[^\s/?#@\\]+(?:[/?#][^\s]*)?$/i.test(url) && // No control characters anywhere (a line break would end a CSV row or an HTML attribute).
848
+ ![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
849
+ }
850
+ function attributeName(name) {
851
+ return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
852
+ }
853
+ function readAttributes(raw) {
854
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
855
+ const out = {};
856
+ let count = 0;
857
+ for (const name of Object.keys(raw)) {
858
+ if (count >= OPTION_LIMITS.attributes) break;
859
+ if (!attributeName(name)) continue;
860
+ const v = raw[name];
861
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
862
+ if (fits(v)) out[name] = v;
863
+ else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
864
+ else continue;
865
+ count++;
866
+ }
867
+ return count ? out : void 0;
868
+ }
869
+ function readOption(raw) {
870
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
871
+ const o = raw;
872
+ if (typeof o.value !== "string") return null;
873
+ const out = { value: o.value };
874
+ if (typeof o.label === "string") out.label = o.label;
875
+ if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
876
+ out.description = o.description;
877
+ }
878
+ if (isHttpsUrl(o.image)) out.image = o.image;
879
+ const attributes = readAttributes(o.attributes);
880
+ if (attributes) out.attributes = attributes;
881
+ return out;
882
+ }
883
+ function optionJson(option) {
884
+ const out = { value: option.value };
885
+ if (option.label !== void 0) out.label = option.label;
886
+ if (option.description !== void 0) out.description = option.description;
887
+ if (option.image !== void 0) out.image = option.image;
888
+ if (option.attributes !== void 0) out.attributes = { ...option.attributes };
889
+ return out;
890
+ }
891
+ var OTHER_MAX_LENGTH = 200;
892
+ function otherOption(field) {
893
+ const other = field.props?.other;
894
+ if (other === true) return { label: "Other" };
895
+ if (other && typeof other === "object" && !Array.isArray(other)) {
896
+ const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
897
+ return { label };
898
+ }
899
+ return null;
900
+ }
901
+ function isOtherValue(field, value) {
902
+ return otherOption(field) !== null && typeof value === "string" && !choiceOptions(field).some((o) => o.value === value);
903
+ }
904
+ var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
758
905
  function choiceOptions(field) {
759
906
  const raw = field.props?.options;
760
907
  if (!Array.isArray(raw)) return [];
761
- return raw.flatMap(
762
- (o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
763
- );
908
+ return raw.flatMap((o) => {
909
+ const option = readOption(o);
910
+ return option ? [option] : [];
911
+ });
912
+ }
913
+ function optionsSource(field) {
914
+ const raw = field.props?.optionsFrom;
915
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
916
+ if (raw.source !== "host" || typeof raw.key !== "string" || !raw.key) return void 0;
917
+ const out = { source: "host", key: raw.key };
918
+ const filter = raw.filter;
919
+ if (filter && typeof filter === "object" && !Array.isArray(filter) && typeof filter.attribute === "string" && filter.attribute && typeof filter.field === "string" && filter.field) {
920
+ out.filter = { attribute: filter.attribute, field: filter.field };
921
+ }
922
+ return out;
923
+ }
924
+ function hostList(hostOptions, key) {
925
+ if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
926
+ const list = hostOptions[key];
927
+ if (!Array.isArray(list)) return [];
928
+ const seen = /* @__PURE__ */ new Set();
929
+ const out = [];
930
+ for (const o of list) {
931
+ const option = readOption(o);
932
+ if (!option || !option.value || seen.has(option.value)) continue;
933
+ seen.add(option.value);
934
+ out.push(option);
935
+ }
936
+ return out;
937
+ }
938
+ function withHostOptions(definition, hostOptions) {
939
+ let changed = false;
940
+ const blocks = definition.blocks.map((block) => {
941
+ if (block.kind !== "field") return block;
942
+ const from = optionsSource(block);
943
+ if (!from) return block;
944
+ const own2 = choiceOptions(block);
945
+ const taken = new Set(own2.map((o) => o.value));
946
+ const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
947
+ if (!added.length) return block;
948
+ changed = true;
949
+ const options = [...own2, ...added].map(optionJson);
950
+ return { ...block, props: { ...block.props, options } };
951
+ });
952
+ return changed ? { ...definition, blocks } : definition;
764
953
  }
765
954
  function optionLabel(field, value) {
766
955
  const option = choiceOptions(field).find((o) => o.value === value);
@@ -880,7 +1069,7 @@ var builtins = [
880
1069
  value: { json: true },
881
1070
  operators: ["eq", "neq", "in", "answered", "empty"],
882
1071
  a11y: { semantics: "group" },
883
- validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
1072
+ validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
884
1073
  summarize: (value, field) => optionLabel(field, String(value))
885
1074
  },
886
1075
  {
@@ -892,7 +1081,8 @@ var builtins = [
892
1081
  a11y: { semantics: "group" },
893
1082
  validate: (value, field) => {
894
1083
  const allowed = new Set(choiceOptions(field).map((o) => o.value));
895
- return Array.isArray(value) && value.length <= allowed.size && new Set(value).size === value.length && value.every((v) => typeof v === "string" && allowed.has(v)) ? null : "Choose from the options.";
1084
+ const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
1085
+ 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.";
896
1086
  },
897
1087
  summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
898
1088
  },
@@ -913,7 +1103,7 @@ var builtins = [
913
1103
  value: { json: true },
914
1104
  operators: ["eq", "neq", "in", "answered", "empty"],
915
1105
  a11y: { semantics: "control" },
916
- validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
1106
+ validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
917
1107
  summarize: (value, field) => optionLabel(field, String(value))
918
1108
  },
919
1109
  {
@@ -1020,7 +1210,7 @@ function dateLimits(field) {
1020
1210
  if (typeof max === "string" && isIsoDate(max)) out.max = max;
1021
1211
  return out;
1022
1212
  }
1023
- for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
1213
+ for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
1024
1214
 
1025
1215
  // src/events.ts
1026
1216
  function isPlainObject(value) {
@@ -1236,6 +1426,205 @@ function fieldMap(def) {
1236
1426
  return map;
1237
1427
  }
1238
1428
 
1429
+ // src/options.ts
1430
+ var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
1431
+ var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
1432
+ function put(record, key, value) {
1433
+ Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
1434
+ }
1435
+ function optionFilter(field) {
1436
+ return optionsSource(field)?.filter;
1437
+ }
1438
+ function answerValues(answer) {
1439
+ const one = (v) => typeof v === "string" ? [v] : typeof v === "number" || typeof v === "boolean" ? [String(v)] : [];
1440
+ if (answer === void 0 || answer === null) return [];
1441
+ return Array.isArray(answer) ? answer.flatMap(one) : one(answer);
1442
+ }
1443
+ function optionMatches(option, attribute, answer) {
1444
+ const have = option.attributes && Object.prototype.hasOwnProperty.call(option.attributes, attribute) ? option.attributes[attribute] : void 0;
1445
+ if (have === void 0) return false;
1446
+ const wanted = answerValues(answer);
1447
+ return (Array.isArray(have) ? have : [have]).some((v) => wanted.includes(v));
1448
+ }
1449
+ function filterSource(definition, key) {
1450
+ return definition.blocks.find((b) => b.kind === "field" && b.key === key);
1451
+ }
1452
+ function visibleOptions(definition, field, values) {
1453
+ const options = choiceOptions(field);
1454
+ const filter = optionFilter(field);
1455
+ if (!filter) return options;
1456
+ const source = filterSource(definition, filter.field);
1457
+ if (!source || source.id === field.id) return options;
1458
+ const answer = own(values, source.id);
1459
+ if (isEmptyValue(answer, source)) return options;
1460
+ return options.filter((o) => optionMatches(o, filter.attribute, answer));
1461
+ }
1462
+ function answersOnPath(state) {
1463
+ const out = /* @__PURE__ */ Object.create(null);
1464
+ for (const id of state.path) out[id] = own(state.values, id);
1465
+ return out;
1466
+ }
1467
+ var OUTSIDE_FILTER = "Choose one of the options.";
1468
+ function filteredChoiceError(definition, field, value, values) {
1469
+ if (!optionFilter(field) || isEmptyValue(value, field)) return null;
1470
+ const all = new Set(choiceOptions(field).map((o) => o.value));
1471
+ const shown2 = new Set(visibleOptions(definition, field, values).map((o) => o.value));
1472
+ const picked = answerValues(value);
1473
+ return picked.some((v) => all.has(v) && !shown2.has(v)) ? OUTSIDE_FILTER : null;
1474
+ }
1475
+ function hasOptionFilters(definition) {
1476
+ return definition.blocks.some((b) => isChoice(b) && optionFilter(b) !== void 0);
1477
+ }
1478
+ function clearFilteredAnswers(definition, values, changedId) {
1479
+ const changed = definition.blocks.find((b) => b.kind === "field" && b.id === changedId);
1480
+ if (!changed) return values;
1481
+ const keys = /* @__PURE__ */ new Set([changed.key]);
1482
+ let out = values;
1483
+ for (const block of definition.blocks) {
1484
+ if (!isChoice(block)) continue;
1485
+ const filter = optionFilter(block);
1486
+ if (!filter || !keys.has(filter.field)) continue;
1487
+ const value = own(out, block.id);
1488
+ if (value === void 0 || isEmptyValue(value, block)) continue;
1489
+ const all = new Set(choiceOptions(block).map((o) => o.value));
1490
+ const shown2 = new Set(visibleOptions(definition, block, out).map((o) => o.value));
1491
+ const keep = (v) => typeof v !== "string" || !all.has(v) || shown2.has(v);
1492
+ let next = value;
1493
+ if (Array.isArray(value)) {
1494
+ const kept = value.filter(keep);
1495
+ if (kept.length !== value.length) next = kept.length ? kept : void 0;
1496
+ } else if (!keep(value)) next = void 0;
1497
+ if (next === value) continue;
1498
+ if (out === values) out = { ...values };
1499
+ put(out, block.id, next);
1500
+ keys.add(block.key);
1501
+ }
1502
+ return out;
1503
+ }
1504
+ function fold(text) {
1505
+ return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
1506
+ }
1507
+ var MAX_PREFILL_PARAM_LENGTH = 64;
1508
+ function isPrefillParam(param) {
1509
+ return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
1510
+ }
1511
+ function prefillSetting(field) {
1512
+ if (!CHOICE_FIELD_TYPES.has(field.type)) return void 0;
1513
+ const raw = field.props?.prefill;
1514
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
1515
+ return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
1516
+ }
1517
+ function paramValues(params, name) {
1518
+ let raw;
1519
+ if (typeof params.getAll === "function") {
1520
+ raw = params.getAll(name);
1521
+ } else if (Object.prototype.hasOwnProperty.call(params, name)) {
1522
+ raw = params[name];
1523
+ }
1524
+ const list = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
1525
+ return list.filter((v) => typeof v === "string" && v !== "" && v.length <= MAX_HIDDEN_STRING_LENGTH);
1526
+ }
1527
+ function prefillFromLink(definition, params, values = {}) {
1528
+ const out = {};
1529
+ if (!params || typeof params !== "object") return out;
1530
+ const seen = {};
1531
+ for (const id of Object.keys(values)) put(seen, id, values[id]);
1532
+ for (const block of definition.blocks) {
1533
+ if (!isChoice(block)) continue;
1534
+ const setting = prefillSetting(block);
1535
+ if (!setting || own(seen, block.id) !== void 0) continue;
1536
+ const raw = paramValues(params, setting.param);
1537
+ if (!raw.length) continue;
1538
+ const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
1539
+ let value;
1540
+ if (block.type === "multi-choice") {
1541
+ const picked = [];
1542
+ for (const r of raw) {
1543
+ for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
1544
+ if (shown2.has(v) && !picked.includes(v)) picked.push(v);
1545
+ }
1546
+ }
1547
+ if (picked.length) value = picked;
1548
+ } else value = raw.find((v) => shown2.has(v));
1549
+ if (value === void 0) continue;
1550
+ put(out, block.id, value);
1551
+ put(seen, block.id, value);
1552
+ }
1553
+ return out;
1554
+ }
1555
+ function hiddenByPrefill(definition, state) {
1556
+ const hideable = /* @__PURE__ */ new Set();
1557
+ for (const b of definition.blocks) {
1558
+ if (isChoice(b) && prefillSetting(b)?.hideWhenPrefilled) hideable.add(b.id);
1559
+ }
1560
+ if (!hideable.size) return [];
1561
+ const prefilled = /* @__PURE__ */ new Set();
1562
+ for (const e of state.events) {
1563
+ if (e.type !== "answer" || !hideable.has(e.field)) continue;
1564
+ if (e.source === "prefill") prefilled.add(e.field);
1565
+ else prefilled.delete(e.field);
1566
+ }
1567
+ if (!prefilled.size) return [];
1568
+ const fields = new Map(definition.blocks.map((b) => [b.id, b]));
1569
+ return state.path.filter((id) => {
1570
+ if (!prefilled.has(id) || own(state.errors, id) !== void 0) return false;
1571
+ const field = fields.get(id);
1572
+ return !isEmptyValue(own(state.values, id), field);
1573
+ });
1574
+ }
1575
+ function snapshotOf(byValue, value) {
1576
+ const option = byValue.get(value);
1577
+ if (option) return { value, label: option.label ?? value };
1578
+ return { value, label: value, other: true };
1579
+ }
1580
+ function answerSnapshots(definition, answers) {
1581
+ const out = {};
1582
+ for (const block of definition.blocks) {
1583
+ if (!isChoice(block) || !optionsSource(block)) continue;
1584
+ const value = own(answers, block.id);
1585
+ if (isEmptyValue(value, block)) continue;
1586
+ const byValue = new Map(choiceOptions(block).map((o) => [o.value, o]));
1587
+ let snapshot;
1588
+ if (Array.isArray(value)) {
1589
+ const items = value.filter((v) => typeof v === "string").map((v) => snapshotOf(byValue, v));
1590
+ if (items.length) snapshot = items;
1591
+ } else if (typeof value === "string") snapshot = snapshotOf(byValue, value);
1592
+ if (snapshot) put(out, block.id, snapshot);
1593
+ }
1594
+ return out;
1595
+ }
1596
+ function answersFromSnapshots(definition, snapshots) {
1597
+ const answers = {};
1598
+ const missing = {};
1599
+ for (const block of definition.blocks) {
1600
+ if (!isChoice(block)) continue;
1601
+ const raw = own(snapshots, block.id);
1602
+ const list = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).flatMap((s) => {
1603
+ const o = readOption(s);
1604
+ if (!o) return [];
1605
+ const label = o.label ?? o.value;
1606
+ return [s.other === true ? { value: o.value, label, other: true } : { value: o.value, label }];
1607
+ });
1608
+ if (!list.length) continue;
1609
+ const options = choiceOptions(block);
1610
+ const byValue = new Set(options.map((o) => o.value));
1611
+ const allowsOther = block.props?.other !== void 0 && block.props?.other !== false;
1612
+ const found = [];
1613
+ const lost = [];
1614
+ for (const s of list) {
1615
+ let value;
1616
+ if (byValue.has(s.value)) value = s.value;
1617
+ else if (s.other) value = allowsOther ? s.value : void 0;
1618
+ else value = options.find((o) => fold(o.label ?? o.value) === fold(s.label))?.value;
1619
+ if (value === void 0) lost.push(s);
1620
+ else if (!found.includes(value)) found.push(value);
1621
+ }
1622
+ if (found.length) put(answers, block.id, block.type === "multi-choice" ? found : found[0]);
1623
+ if (lost.length) put(missing, block.id, lost);
1624
+ }
1625
+ return { answers, missing };
1626
+ }
1627
+
1239
1628
  // src/recall.ts
1240
1629
  var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
1241
1630
  function recall(text, scope) {
@@ -1289,7 +1678,8 @@ function readsAny(node, ids) {
1289
1678
  function createForm(definition, options = {}) {
1290
1679
  return build(definition, options).engine;
1291
1680
  }
1292
- function build(definition, options, replaying = false, budget) {
1681
+ function build(given, options, replaying = false, budget) {
1682
+ const definition = withHostOptions(given, options.hostOptions);
1293
1683
  const fields = fieldMap(definition);
1294
1684
  const byKey = new Map([...fields.values()].map((f) => [f.key, f]));
1295
1685
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -1335,6 +1725,7 @@ function build(definition, options, replaying = false, budget) {
1335
1725
  path: first.path,
1336
1726
  current: first.path[0] ?? null,
1337
1727
  history: [],
1728
+ furthest: first.path[0] ?? null,
1338
1729
  errors: {},
1339
1730
  status: "in_progress",
1340
1731
  ending: null,
@@ -1355,16 +1746,34 @@ function build(definition, options, replaying = false, budget) {
1355
1746
  return at === void 0 ? event : { ...event, at };
1356
1747
  };
1357
1748
  const isVisibleIn = (s, id) => s.path.includes(id);
1749
+ const filters = hasOptionFilters(definition);
1750
+ const clearFiltered = (values, id) => filters ? clearFilteredAnswers(definition, values, id) : values;
1358
1751
  const errorsFor = (s, ids) => {
1359
1752
  const errors = {};
1753
+ let shown2;
1360
1754
  for (const id of ids) {
1361
1755
  const field = fields.get(id);
1362
1756
  if (!field || !isVisibleIn(s, id)) continue;
1363
- const message = validateField(field, own(s.values, id), { custom: options.validators?.[id] });
1757
+ const value = own(s.values, id);
1758
+ let message = validateField(field, value, { custom: options.validators?.[id] });
1759
+ if (!message && filters) message = filteredChoiceError(definition, field, value, shown2 ??= answersOnPath(s));
1364
1760
  if (message) errors[id] = message;
1365
1761
  }
1366
1762
  return errors;
1367
1763
  };
1764
+ const skipped = (s) => new Set(hiddenByPrefill(definition, s));
1765
+ const shownFrom = (s, from, skip) => {
1766
+ for (let i = from; i < s.path.length; i++) if (!skip.has(s.path[i])) return s.path[i];
1767
+ return void 0;
1768
+ };
1769
+ const showCurrent = () => {
1770
+ if (!state.current) return;
1771
+ const skip = skipped(state);
1772
+ if (!skip.has(state.current)) return;
1773
+ const at = state.path.indexOf(state.current);
1774
+ const shown2 = at < 0 ? void 0 : shownFrom(state, at, skip);
1775
+ if (shown2) state = { ...state, current: shown2, furthest: state.furthest === state.current ? shown2 : state.furthest };
1776
+ };
1368
1777
  let unsettled = false;
1369
1778
  let formulasStale = false;
1370
1779
  const settleNow = (needValues = true) => {
@@ -1393,13 +1802,13 @@ function build(definition, options, replaying = false, budget) {
1393
1802
  const errors = { ...state.errors };
1394
1803
  delete errors[id];
1395
1804
  if (batch) {
1396
- state.values = { ...state.values, [id]: value };
1805
+ state.values = clearFiltered({ ...state.values, [id]: value }, id);
1397
1806
  state.errors = errors;
1398
1807
  log(event);
1399
1808
  unsettled = true;
1400
1809
  return;
1401
1810
  }
1402
- const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
1811
+ const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
1403
1812
  emit({ ...state, values, path, errors, events: log(event) });
1404
1813
  };
1405
1814
  const engine = {
@@ -1430,19 +1839,21 @@ function build(definition, options, replaying = false, budget) {
1430
1839
  return false;
1431
1840
  }
1432
1841
  const at = state.path.indexOf(state.current);
1433
- const target = state.path[at + 1];
1842
+ const target = shownFrom(state, at + 1, skipped(state));
1434
1843
  if (!target) {
1435
1844
  emit({ ...state, events });
1436
1845
  return true;
1437
1846
  }
1438
- emit({ ...state, current: target, history: [...state.history, state.current], events });
1847
+ const furthest = state.furthest && state.path.indexOf(state.furthest) >= state.path.indexOf(target) ? state.furthest : target;
1848
+ emit({ ...state, current: target, history: [...state.history, state.current], furthest, events });
1439
1849
  return true;
1440
1850
  },
1441
1851
  back() {
1442
1852
  if (state.status === "completed") return;
1443
1853
  const history = [...state.history];
1854
+ const skip = skipped(state);
1444
1855
  let previous = history.pop();
1445
- while (previous && !state.path.includes(previous)) previous = history.pop();
1856
+ while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
1446
1857
  if (!previous) return;
1447
1858
  emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
1448
1859
  },
@@ -1470,15 +1881,27 @@ function build(definition, options, replaying = false, budget) {
1470
1881
  return out;
1471
1882
  }
1472
1883
  };
1473
- if (options.initialValues) {
1884
+ const fromLink = options.linkParams ? prefillFromLink(definition, options.linkParams, options.initialValues) : {};
1885
+ const initialValues = Object.keys(fromLink).length ? { ...fromLink, ...options.initialValues } : options.initialValues;
1886
+ if (initialValues) {
1474
1887
  const wasBatch = batch;
1475
1888
  batch = true;
1476
- for (const [id, value] of Object.entries(options.initialValues)) {
1889
+ for (const [id, value] of Object.entries(initialValues)) {
1477
1890
  if (!fields.has(id) || value === void 0) continue;
1478
1891
  answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
1479
1892
  }
1480
1893
  settleNow();
1481
1894
  batch = wasBatch;
1895
+ showCurrent();
1896
+ }
1897
+ const resumeAt = options.resumeAt;
1898
+ if (resumeAt && !replaying && state.path.includes(resumeAt)) {
1899
+ for (let i = 0; i < state.path.length && state.current !== resumeAt; i++) {
1900
+ if (!engine.next()) {
1901
+ state = { ...state, errors: {} };
1902
+ break;
1903
+ }
1904
+ }
1482
1905
  }
1483
1906
  const apply = (event) => {
1484
1907
  if (event.type === "answer") {
@@ -1488,18 +1911,22 @@ function build(definition, options, replaying = false, budget) {
1488
1911
  }
1489
1912
  } else if (event.type === "next") {
1490
1913
  settleNow(false);
1914
+ showCurrent();
1491
1915
  if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
1492
1916
  engine.next();
1493
1917
  } else if (event.type === "back") {
1494
1918
  settleNow(false);
1919
+ showCurrent();
1495
1920
  engine.back();
1496
1921
  } else if (event.type === "submit") {
1497
1922
  settleNow();
1923
+ showCurrent();
1498
1924
  engine.submit();
1499
1925
  }
1500
1926
  };
1501
1927
  const finish = () => {
1502
1928
  settleNow();
1929
+ showCurrent();
1503
1930
  batch = false;
1504
1931
  state = { ...state, events: [...state.events] };
1505
1932
  };
@@ -1507,7 +1934,12 @@ function build(definition, options, replaying = false, budget) {
1507
1934
  }
1508
1935
  function replayWithin(definition, events, options, maxWork) {
1509
1936
  const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
1510
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
1937
+ const { engine, apply, finish } = build(
1938
+ definition,
1939
+ { ...options, initialValues: void 0, linkParams: void 0, clock: () => void 0 },
1940
+ true,
1941
+ budget
1942
+ );
1511
1943
  try {
1512
1944
  for (const event of events) apply(event);
1513
1945
  finish();
@@ -1579,6 +2011,31 @@ function parseForm(input) {
1579
2011
  const optionalBoolean = (v, path) => {
1580
2012
  if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
1581
2013
  };
2014
+ const optionDetails = (o, at) => {
2015
+ if (o.description !== void 0 && (typeof o.description !== "string" || o.description.length > OPTION_LIMITS.description)) {
2016
+ issue(`${at}.description`, `Must be text of at most ${OPTION_LIMITS.description} characters.`);
2017
+ }
2018
+ if (o.image !== void 0 && !isHttpsUrl(o.image)) {
2019
+ issue(`${at}.image`, `Must be an https:// link of at most ${OPTION_LIMITS.image} characters.`);
2020
+ }
2021
+ if (o.attributes === void 0) return;
2022
+ const attributes = o.attributes;
2023
+ if (!isObject2(attributes) || Object.keys(attributes).length > OPTION_LIMITS.attributes) {
2024
+ issue(`${at}.attributes`, `Must be an object of at most ${OPTION_LIMITS.attributes} attributes.`);
2025
+ return;
2026
+ }
2027
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
2028
+ for (const [name2, v] of Object.entries(attributes)) {
2029
+ if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
2030
+ issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
2031
+ } else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
2032
+ issue(
2033
+ `${at}.attributes.${name2}`,
2034
+ `Must be text, or a list of at most ${OPTION_LIMITS.attributeItems} texts, each at most ${OPTION_LIMITS.attributeValue} characters.`
2035
+ );
2036
+ }
2037
+ }
2038
+ };
1582
2039
  const name = (v, path, what) => {
1583
2040
  if (typeof v !== "string" || !v) {
1584
2041
  issue(path, `${what} must be a non-empty string.`);
@@ -1667,6 +2124,22 @@ function parseForm(input) {
1667
2124
  }
1668
2125
  }
1669
2126
  if (type === "consent") optionalText(props.text, `${path}.text`);
2127
+ if (type === PHOTO_TYPE) {
2128
+ if (props.maxFiles !== void 0 && !(isInt(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
2129
+ issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
2130
+ }
2131
+ if (props.maxSizeMb !== void 0 && !(isInt(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
2132
+ issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
2133
+ }
2134
+ if (props.formats !== void 0) {
2135
+ const formats = props.formats;
2136
+ if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
2137
+ issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
2138
+ } else if (new Set(formats).size !== formats.length) {
2139
+ issue(`${path}.formats`, "Name each format once.");
2140
+ }
2141
+ }
2142
+ }
1670
2143
  };
1671
2144
  if (!isObject2(input)) {
1672
2145
  issue("", "A form definition must be an object.");
@@ -1687,6 +2160,7 @@ function parseForm(input) {
1687
2160
  else {
1688
2161
  optionalBoolean(input.settings.anonymous, "settings.anonymous");
1689
2162
  optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
2163
+ optionalBoolean(input.settings.saveProgress, "settings.saveProgress");
1690
2164
  }
1691
2165
  }
1692
2166
  if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
@@ -1727,6 +2201,38 @@ function parseForm(input) {
1727
2201
  issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
1728
2202
  }
1729
2203
  if (block.props !== void 0) {
2204
+ const from = isObject2(block.props) ? block.props.optionsFrom : void 0;
2205
+ if (from !== void 0) {
2206
+ if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
2207
+ issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
2208
+ } else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2209
+ warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
2210
+ }
2211
+ const filter = isObject2(from) ? from.filter : void 0;
2212
+ if (filter !== void 0) {
2213
+ const at = `${path}.props.optionsFrom.filter`;
2214
+ if (!isObject2(filter) || typeof filter.attribute !== "string" || !filter.attribute || filter.attribute.length > OPTION_LIMITS.attributeName || typeof filter.field !== "string" || !filter.field) {
2215
+ issue(at, `Must be { attribute, field }: an attribute name (at most ${OPTION_LIMITS.attributeName} characters) and an earlier question's key.`);
2216
+ } else if (!keys.has(filter.field)) {
2217
+ issue(`${at}.field`, `"${filter.field}" must be the key of an earlier question.`);
2218
+ }
2219
+ }
2220
+ }
2221
+ const prefill = isObject2(block.props) ? block.props.prefill : void 0;
2222
+ if (prefill !== void 0) {
2223
+ if (!isObject2(prefill) || !isPrefillParam(prefill.param)) {
2224
+ issue(
2225
+ `${path}.props.prefill`,
2226
+ `Must be { param, hideWhenPrefilled? }: a link parameter of letters, digits, "_", "-" or "." (at most ${MAX_PREFILL_PARAM_LENGTH} characters).`
2227
+ );
2228
+ } else {
2229
+ optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
2230
+ if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2231
+ warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
2232
+ }
2233
+ }
2234
+ }
2235
+ if (isObject2(block.props)) optionalBoolean(block.props.searchable, `${path}.props.searchable`);
1730
2236
  if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
1731
2237
  else if (block.props.options !== void 0) {
1732
2238
  const options = block.props.options;
@@ -1735,12 +2241,25 @@ function parseForm(input) {
1735
2241
  options.forEach((o, j) => {
1736
2242
  const at = `${path}.props.options[${j}]`;
1737
2243
  if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
1738
- else optionalText(o.label, `${at}.label`);
2244
+ else {
2245
+ optionalText(o.label, `${at}.label`);
2246
+ optionDetails(o, at);
2247
+ }
1739
2248
  });
1740
- choices.set(
1741
- block.id,
1742
- new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
1743
- );
2249
+ if (from === void 0) {
2250
+ choices.set(
2251
+ block.id,
2252
+ new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
2253
+ );
2254
+ }
2255
+ }
2256
+ }
2257
+ const other = isObject2(block.props) ? block.props.other : void 0;
2258
+ if (other !== void 0) {
2259
+ if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
2260
+ issue(`${path}.props.other`, "Must be true, false or { label }.");
2261
+ } else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2262
+ warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
1744
2263
  }
1745
2264
  }
1746
2265
  const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
@@ -1815,6 +2334,8 @@ function parseForm(input) {
1815
2334
  } else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
1816
2335
  else if (targetType === "media-notes" || targetType === "text-highlights") {
1817
2336
  warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
2337
+ } else if (targetType === PHOTO_TYPE) {
2338
+ warnings.push({ path, message: `"${ref.key}" holds photos, not numbers; the formula reads it as blank.` });
1818
2339
  }
1819
2340
  }
1820
2341
  });
@@ -2016,6 +2537,158 @@ function parseForm(input) {
2016
2537
  return issues.length ? fail() : { ok: true, form: input, warnings };
2017
2538
  }
2018
2539
 
2540
+ // src/edit-link.ts
2541
+ var EDIT_LINK_TTL_SECONDS = 30 * 24 * 60 * 60;
2542
+ var MIN_EDIT_SECRET_BYTES = 32;
2543
+ var EDIT_LINK_PARAM = "edit";
2544
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2545
+ var NONCE = /^[A-Za-z0-9_-]{16,64}$/;
2546
+ var EXPIRES = /^[1-9][0-9]{0,11}$/;
2547
+ var SIG = /^[A-Za-z0-9_-]{43}$/;
2548
+ var DOMAIN = "edit-link:v2:";
2549
+ var MAX_TOKEN_LENGTH = 300;
2550
+ var encoder = new TextEncoder();
2551
+ function toBase64Url(bytes) {
2552
+ let binary = "";
2553
+ for (const b of bytes) binary += String.fromCharCode(b);
2554
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2555
+ }
2556
+ function fromBase64Url(text) {
2557
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
2558
+ const bytes = new Uint8Array(binary.length);
2559
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2560
+ return bytes;
2561
+ }
2562
+ function isUsableEditSecret(secret) {
2563
+ return typeof secret === "string" && secret.trim().length > 0 && encoder.encode(secret).length >= MIN_EDIT_SECRET_BYTES;
2564
+ }
2565
+ function hmacKey(secret, usage) {
2566
+ return crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [usage]);
2567
+ }
2568
+ var payload = (id, nonce, expiresAt) => encoder.encode(`${DOMAIN}${id}.${nonce}.${expiresAt}`);
2569
+ async function signEditToken(link, secret, options = {}) {
2570
+ if (!isUsableEditSecret(secret) || !UUID.test(link.id) || !NONCE.test(link.nonce)) return null;
2571
+ const ttl = options.ttlSeconds ?? EDIT_LINK_TTL_SECONDS;
2572
+ if (!Number.isFinite(ttl) || ttl <= 0) return null;
2573
+ const id = link.id.toLowerCase();
2574
+ const expiresAt = Math.floor((options.now ?? Date.now()) / 1e3) + Math.floor(ttl);
2575
+ const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret, "sign"), payload(id, link.nonce, expiresAt));
2576
+ return `${id}.${link.nonce}.${expiresAt}.${toBase64Url(new Uint8Array(sig))}`;
2577
+ }
2578
+ async function verifyEditToken(token, secret, options = {}) {
2579
+ if (!isUsableEditSecret(secret) || typeof token !== "string" || token.length > MAX_TOKEN_LENGTH) return null;
2580
+ const parts = token.split(".");
2581
+ if (parts.length !== 4) return null;
2582
+ const [id, nonce, expires, sig] = parts;
2583
+ if (!UUID.test(id) || id !== id.toLowerCase() || !NONCE.test(nonce) || !EXPIRES.test(expires)) return null;
2584
+ if (!SIG.test(sig)) return null;
2585
+ const expiresAt = Number(expires);
2586
+ const now = Math.floor((options.now ?? Date.now()) / 1e3);
2587
+ const ttl = options.ttlSeconds ?? EDIT_LINK_TTL_SECONDS;
2588
+ if (expiresAt <= now || expiresAt > now + ttl) return null;
2589
+ let bytes;
2590
+ try {
2591
+ bytes = fromBase64Url(sig);
2592
+ } catch {
2593
+ return null;
2594
+ }
2595
+ const ok = await crypto.subtle.verify("HMAC", await hmacKey(secret, "verify"), bytes, payload(id, nonce, expiresAt));
2596
+ return ok ? { id, nonce, expiresAt } : null;
2597
+ }
2598
+ function withEditToken(formUrl, token, param = EDIT_LINK_PARAM) {
2599
+ return `${formUrl}${formUrl.includes("?") ? "&" : "?"}${param}=${encodeURIComponent(token)}`;
2600
+ }
2601
+ async function editLinkUrl(formUrl, link, secret, options = {}) {
2602
+ const token = await signEditToken(link, secret, options);
2603
+ return token ? withEditToken(formUrl, token, options.param) : null;
2604
+ }
2605
+ function createEditSecretReader(options) {
2606
+ const { variable } = options;
2607
+ let warned = false;
2608
+ const read = (env, log) => {
2609
+ const secret = env[variable];
2610
+ if (isUsableEditSecret(secret)) return secret;
2611
+ if (!warned) {
2612
+ warned = true;
2613
+ (log ?? options.log ?? ((m) => console.warn(m)))(
2614
+ `${variable} is ${secret ? `shorter than ${MIN_EDIT_SECRET_BYTES} bytes` : "not set"}: edit links are off.`
2615
+ );
2616
+ }
2617
+ return null;
2618
+ };
2619
+ return Object.assign(read, {
2620
+ reset: () => {
2621
+ warned = false;
2622
+ }
2623
+ });
2624
+ }
2625
+ function isEditLinkRequest(url, options = {}) {
2626
+ const isFormPage = options.isFormPage ?? (() => true);
2627
+ return isFormPage(url) && url.searchParams.has(options.param ?? EDIT_LINK_PARAM);
2628
+ }
2629
+ function withEditLinkHeaders(request, response, options = {}) {
2630
+ let url;
2631
+ try {
2632
+ url = new URL(request.url);
2633
+ } catch {
2634
+ return response;
2635
+ }
2636
+ if (!isEditLinkRequest(url, options)) return response;
2637
+ const headers = new Headers(response.headers);
2638
+ headers.set("referrer-policy", "no-referrer");
2639
+ headers.set("cache-control", "private, no-store");
2640
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
2641
+ }
2642
+
2643
+ // src/rate-limit.ts
2644
+ var DEFAULT_TRUSTED_IP_HEADERS = ["x-vercel-forwarded-for", "x-forwarded-for", "x-real-ip"];
2645
+ var RATE_LIMIT_DEFAULTS = {
2646
+ submit: { limit: 60, windowSeconds: 600 },
2647
+ progress: { limit: 900, windowSeconds: 600 },
2648
+ upload: { limit: 120, windowSeconds: 600 }
2649
+ };
2650
+ function clientIp(headers, trusted = DEFAULT_TRUSTED_IP_HEADERS) {
2651
+ for (const name of trusted) {
2652
+ const first = headers.get(name)?.split(",")[0]?.trim();
2653
+ if (first) return first;
2654
+ }
2655
+ return "unknown";
2656
+ }
2657
+ function addressPrefix(ip) {
2658
+ const bare = ip.replace(/^\[|\](:\d+)?$/g, "").split("%")[0].toLowerCase();
2659
+ if (!bare.includes(":")) return bare;
2660
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(bare);
2661
+ if (mapped) return mapped[1];
2662
+ const [head = "", tail = ""] = bare.split("::");
2663
+ const left = head ? head.split(":") : [];
2664
+ const right = bare.includes("::") && tail ? tail.split(":") : [];
2665
+ const groups = bare.includes("::") ? [...left, ...Array(Math.max(0, 8 - left.length - right.length)).fill("0"), ...right] : left;
2666
+ return `${groups.slice(0, 4).map((g) => (g || "0").replace(/^0+(?=.)/, "")).join(":")}::/64`;
2667
+ }
2668
+ async function rateLimitKey(input) {
2669
+ const encoder2 = new TextEncoder();
2670
+ const key = await crypto.subtle.importKey("raw", encoder2.encode(input.secret), { name: "HMAC", hash: "SHA-256" }, false, [
2671
+ "sign"
2672
+ ]);
2673
+ const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder2.encode(addressPrefix(input.ip))));
2674
+ let hex = "";
2675
+ for (const b of mac) hex += b.toString(16).padStart(2, "0");
2676
+ return `${input.kind}:${input.scope}:${hex}`;
2677
+ }
2678
+ async function allow(store, check, options = {}) {
2679
+ let allowed;
2680
+ try {
2681
+ allowed = await store.hit(check.key, check.limit, check.windowSeconds);
2682
+ } catch {
2683
+ allowed = null;
2684
+ }
2685
+ if (allowed !== null) return allowed;
2686
+ (options.log ?? ((m) => console.warn(m)))(
2687
+ options.message ?? "The rate limit could not be checked; letting the request through."
2688
+ );
2689
+ return true;
2690
+ }
2691
+
2019
2692
  // src/server.ts
2020
2693
  var MAX_EVENTS = 1e4;
2021
2694
  var MAX_SUBMISSION_VALUES = 2e5;
@@ -2079,9 +2752,10 @@ function wellFormed(input) {
2079
2752
  }
2080
2753
  return { answers, events: known, hidden };
2081
2754
  }
2082
- function validateSubmission(definition, submission, options = {}) {
2083
- const invalid = checkDefinition(definition);
2755
+ function validateSubmission(published, submission, options = {}) {
2756
+ const invalid = checkDefinition(published);
2084
2757
  if (invalid) return { ok: false, errors: { "": invalid } };
2758
+ const definition = withHostOptions(published, options.hostOptions);
2085
2759
  if (!isPlainObject(submission)) return MALFORMED;
2086
2760
  const maxEvents = options.maxEvents ?? MAX_EVENTS;
2087
2761
  let sent = submission;
@@ -2152,11 +2826,12 @@ function validateSubmission(definition, submission, options = {}) {
2152
2826
  }
2153
2827
  } else {
2154
2828
  const errors = {};
2155
- for (const [id, value] of Object.entries(engine.answers())) {
2829
+ const kept = engine.answers();
2830
+ for (const [id, value] of Object.entries(kept)) {
2156
2831
  const message = validateField(fields.get(id), value, {
2157
2832
  requireAnswers: false,
2158
2833
  custom: options.validators?.[id]
2159
- });
2834
+ }) ?? filteredChoiceError(definition, fields.get(id), value, kept);
2160
2835
  if (message) errors[id] = message;
2161
2836
  }
2162
2837
  if (Object.keys(errors).length) return { ok: false, errors };
@@ -2198,6 +2873,6 @@ function publicDefinition(definition) {
2198
2873
  return JSON.parse(JSON.stringify(definition));
2199
2874
  }
2200
2875
 
2201
- export { MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, publicDefinition, validateSubmission };
2876
+ export { DEFAULT_TRUSTED_IP_HEADERS, EDIT_LINK_PARAM, EDIT_LINK_TTL_SECONDS, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, MIN_EDIT_SECRET_BYTES, RATE_LIMIT_DEFAULTS, RAW_EVENTS_PER_EVENT, addressPrefix, allow, answerSnapshots, answersFromSnapshots, clientIp, createEditSecretReader, editLinkUrl, isEditLinkRequest, isUsableEditSecret, publicDefinition, rateLimitKey, signEditToken, validateSubmission, verifyEditToken, withEditLinkHeaders, withEditToken, withHostOptions };
2202
2877
  //# sourceMappingURL=server.js.map
2203
2878
  //# sourceMappingURL=server.js.map