@marktiderman/genesis-forms 0.5.0 → 0.7.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.cjs CHANGED
@@ -723,6 +723,7 @@ var structuredFieldTypes = [
723
723
 
724
724
  // src/photo.ts
725
725
  var PHOTO_TYPE = "photo";
726
+ var UPLOAD_FIELD_TYPES = [PHOTO_TYPE];
726
727
  var PHOTO_FORMATS = {
727
728
  jpeg: ["image/jpeg"],
728
729
  png: ["image/png"],
@@ -831,6 +832,65 @@ function summarizeValue(value, field) {
831
832
  if (Array.isArray(value)) return value.join(", ");
832
833
  return String(value);
833
834
  }
835
+ var OPTION_LIMITS = {
836
+ /** Characters in a description. */
837
+ description: 500,
838
+ /** Characters in an image URL. */
839
+ image: 2048,
840
+ /** Attributes on one option. */
841
+ attributes: 20,
842
+ /** Characters in an attribute's name. */
843
+ attributeName: 64,
844
+ /** Characters in one attribute value. */
845
+ attributeValue: 200,
846
+ /** Values in one attribute's list. */
847
+ attributeItems: 50
848
+ };
849
+ function isHttpsUrl(url) {
850
+ 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).
851
+ ![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
852
+ }
853
+ function attributeName(name) {
854
+ return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
855
+ }
856
+ function readAttributes(raw) {
857
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
858
+ const out = {};
859
+ let count = 0;
860
+ for (const name of Object.keys(raw)) {
861
+ if (count >= OPTION_LIMITS.attributes) break;
862
+ if (!attributeName(name)) continue;
863
+ const v = raw[name];
864
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
865
+ if (fits(v)) out[name] = v;
866
+ else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
867
+ else continue;
868
+ count++;
869
+ }
870
+ return count ? out : void 0;
871
+ }
872
+ function readOption(raw) {
873
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
874
+ const o = raw;
875
+ if (typeof o.value !== "string") return null;
876
+ const out = { value: o.value };
877
+ if (typeof o.label === "string") out.label = o.label;
878
+ if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
879
+ out.description = o.description;
880
+ }
881
+ if (isHttpsUrl(o.image)) out.image = o.image;
882
+ const attributes = readAttributes(o.attributes);
883
+ if (attributes) out.attributes = attributes;
884
+ return out;
885
+ }
886
+ function optionJson(option) {
887
+ const out = { value: option.value };
888
+ if (option.label !== void 0) out.label = option.label;
889
+ if (option.description !== void 0) out.description = option.description;
890
+ if (option.image !== void 0) out.image = option.image;
891
+ if (option.attributes !== void 0) out.attributes = { ...option.attributes };
892
+ return out;
893
+ }
834
894
  var OTHER_MAX_LENGTH = 200;
835
895
  function otherOption(field) {
836
896
  const other = field.props?.other;
@@ -848,14 +908,21 @@ var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !=
848
908
  function choiceOptions(field) {
849
909
  const raw = field.props?.options;
850
910
  if (!Array.isArray(raw)) return [];
851
- return raw.flatMap(
852
- (o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
853
- );
911
+ return raw.flatMap((o) => {
912
+ const option = readOption(o);
913
+ return option ? [option] : [];
914
+ });
854
915
  }
855
916
  function optionsSource(field) {
856
917
  const raw = field.props?.optionsFrom;
857
918
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
858
- return raw.source === "host" && typeof raw.key === "string" && raw.key ? { source: "host", key: raw.key } : void 0;
919
+ if (raw.source !== "host" || typeof raw.key !== "string" || !raw.key) return void 0;
920
+ const out = { source: "host", key: raw.key };
921
+ const filter = raw.filter;
922
+ if (filter && typeof filter === "object" && !Array.isArray(filter) && typeof filter.attribute === "string" && filter.attribute && typeof filter.field === "string" && filter.field) {
923
+ out.filter = { attribute: filter.attribute, field: filter.field };
924
+ }
925
+ return out;
859
926
  }
860
927
  function hostList(hostOptions, key) {
861
928
  if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
@@ -864,11 +931,10 @@ function hostList(hostOptions, key) {
864
931
  const seen = /* @__PURE__ */ new Set();
865
932
  const out = [];
866
933
  for (const o of list) {
867
- if (!o || typeof o !== "object" || Array.isArray(o)) continue;
868
- const { value, label } = o;
869
- if (typeof value !== "string" || !value || seen.has(value)) continue;
870
- seen.add(value);
871
- out.push(typeof label === "string" ? { value, label } : { value });
934
+ const option = readOption(o);
935
+ if (!option || !option.value || seen.has(option.value)) continue;
936
+ seen.add(option.value);
937
+ out.push(option);
872
938
  }
873
939
  return out;
874
940
  }
@@ -883,7 +949,7 @@ function withHostOptions(definition, hostOptions) {
883
949
  const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
884
950
  if (!added.length) return block;
885
951
  changed = true;
886
- const options = [...own2, ...added].map((o) => o.label === void 0 ? { value: o.value } : { value: o.value, label: o.label });
952
+ const options = [...own2, ...added].map(optionJson);
887
953
  return { ...block, props: { ...block.props, options } };
888
954
  });
889
955
  return changed ? { ...definition, blocks } : definition;
@@ -1363,6 +1429,205 @@ function fieldMap(def) {
1363
1429
  return map;
1364
1430
  }
1365
1431
 
1432
+ // src/options.ts
1433
+ var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
1434
+ var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
1435
+ function put(record, key, value) {
1436
+ Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
1437
+ }
1438
+ function optionFilter(field) {
1439
+ return optionsSource(field)?.filter;
1440
+ }
1441
+ function answerValues(answer) {
1442
+ const one = (v) => typeof v === "string" ? [v] : typeof v === "number" || typeof v === "boolean" ? [String(v)] : [];
1443
+ if (answer === void 0 || answer === null) return [];
1444
+ return Array.isArray(answer) ? answer.flatMap(one) : one(answer);
1445
+ }
1446
+ function optionMatches(option, attribute, answer) {
1447
+ const have = option.attributes && Object.prototype.hasOwnProperty.call(option.attributes, attribute) ? option.attributes[attribute] : void 0;
1448
+ if (have === void 0) return false;
1449
+ const wanted = answerValues(answer);
1450
+ return (Array.isArray(have) ? have : [have]).some((v) => wanted.includes(v));
1451
+ }
1452
+ function filterSource(definition, key) {
1453
+ return definition.blocks.find((b) => b.kind === "field" && b.key === key);
1454
+ }
1455
+ function visibleOptions(definition, field, values) {
1456
+ const options = choiceOptions(field);
1457
+ const filter = optionFilter(field);
1458
+ if (!filter) return options;
1459
+ const source = filterSource(definition, filter.field);
1460
+ if (!source || source.id === field.id) return options;
1461
+ const answer = own(values, source.id);
1462
+ if (isEmptyValue(answer, source)) return options;
1463
+ return options.filter((o) => optionMatches(o, filter.attribute, answer));
1464
+ }
1465
+ function answersOnPath(state) {
1466
+ const out = /* @__PURE__ */ Object.create(null);
1467
+ for (const id of state.path) out[id] = own(state.values, id);
1468
+ return out;
1469
+ }
1470
+ var OUTSIDE_FILTER = "Choose one of the options.";
1471
+ function filteredChoiceError(definition, field, value, values) {
1472
+ if (!optionFilter(field) || isEmptyValue(value, field)) return null;
1473
+ const all = new Set(choiceOptions(field).map((o) => o.value));
1474
+ const shown2 = new Set(visibleOptions(definition, field, values).map((o) => o.value));
1475
+ const picked = answerValues(value);
1476
+ return picked.some((v) => all.has(v) && !shown2.has(v)) ? OUTSIDE_FILTER : null;
1477
+ }
1478
+ function hasOptionFilters(definition) {
1479
+ return definition.blocks.some((b) => isChoice(b) && optionFilter(b) !== void 0);
1480
+ }
1481
+ function clearFilteredAnswers(definition, values, changedId) {
1482
+ const changed = definition.blocks.find((b) => b.kind === "field" && b.id === changedId);
1483
+ if (!changed) return values;
1484
+ const keys = /* @__PURE__ */ new Set([changed.key]);
1485
+ let out = values;
1486
+ for (const block of definition.blocks) {
1487
+ if (!isChoice(block)) continue;
1488
+ const filter = optionFilter(block);
1489
+ if (!filter || !keys.has(filter.field)) continue;
1490
+ const value = own(out, block.id);
1491
+ if (value === void 0 || isEmptyValue(value, block)) continue;
1492
+ const all = new Set(choiceOptions(block).map((o) => o.value));
1493
+ const shown2 = new Set(visibleOptions(definition, block, out).map((o) => o.value));
1494
+ const keep = (v) => typeof v !== "string" || !all.has(v) || shown2.has(v);
1495
+ let next = value;
1496
+ if (Array.isArray(value)) {
1497
+ const kept = value.filter(keep);
1498
+ if (kept.length !== value.length) next = kept.length ? kept : void 0;
1499
+ } else if (!keep(value)) next = void 0;
1500
+ if (next === value) continue;
1501
+ if (out === values) out = { ...values };
1502
+ put(out, block.id, next);
1503
+ keys.add(block.key);
1504
+ }
1505
+ return out;
1506
+ }
1507
+ function fold(text) {
1508
+ return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
1509
+ }
1510
+ var MAX_PREFILL_PARAM_LENGTH = 64;
1511
+ function isPrefillParam(param) {
1512
+ return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
1513
+ }
1514
+ function prefillSetting(field) {
1515
+ if (!CHOICE_FIELD_TYPES.has(field.type)) return void 0;
1516
+ const raw = field.props?.prefill;
1517
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
1518
+ return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
1519
+ }
1520
+ function paramValues(params, name) {
1521
+ let raw;
1522
+ if (typeof params.getAll === "function") {
1523
+ raw = params.getAll(name);
1524
+ } else if (Object.prototype.hasOwnProperty.call(params, name)) {
1525
+ raw = params[name];
1526
+ }
1527
+ const list = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
1528
+ return list.filter((v) => typeof v === "string" && v !== "" && v.length <= MAX_HIDDEN_STRING_LENGTH);
1529
+ }
1530
+ function prefillFromLink(definition, params, values = {}) {
1531
+ const out = {};
1532
+ if (!params || typeof params !== "object") return out;
1533
+ const seen = {};
1534
+ for (const id of Object.keys(values)) put(seen, id, values[id]);
1535
+ for (const block of definition.blocks) {
1536
+ if (!isChoice(block)) continue;
1537
+ const setting = prefillSetting(block);
1538
+ if (!setting || own(seen, block.id) !== void 0) continue;
1539
+ const raw = paramValues(params, setting.param);
1540
+ if (!raw.length) continue;
1541
+ const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
1542
+ let value;
1543
+ if (block.type === "multi-choice") {
1544
+ const picked = [];
1545
+ for (const r of raw) {
1546
+ for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
1547
+ if (shown2.has(v) && !picked.includes(v)) picked.push(v);
1548
+ }
1549
+ }
1550
+ if (picked.length) value = picked;
1551
+ } else value = raw.find((v) => shown2.has(v));
1552
+ if (value === void 0) continue;
1553
+ put(out, block.id, value);
1554
+ put(seen, block.id, value);
1555
+ }
1556
+ return out;
1557
+ }
1558
+ function hiddenByPrefill(definition, state) {
1559
+ const hideable = /* @__PURE__ */ new Set();
1560
+ for (const b of definition.blocks) {
1561
+ if (isChoice(b) && prefillSetting(b)?.hideWhenPrefilled) hideable.add(b.id);
1562
+ }
1563
+ if (!hideable.size) return [];
1564
+ const prefilled = /* @__PURE__ */ new Set();
1565
+ for (const e of state.events) {
1566
+ if (e.type !== "answer" || !hideable.has(e.field)) continue;
1567
+ if (e.source === "prefill") prefilled.add(e.field);
1568
+ else prefilled.delete(e.field);
1569
+ }
1570
+ if (!prefilled.size) return [];
1571
+ const fields = new Map(definition.blocks.map((b) => [b.id, b]));
1572
+ return state.path.filter((id) => {
1573
+ if (!prefilled.has(id) || own(state.errors, id) !== void 0) return false;
1574
+ const field = fields.get(id);
1575
+ return !isEmptyValue(own(state.values, id), field);
1576
+ });
1577
+ }
1578
+ function snapshotOf(byValue, value) {
1579
+ const option = byValue.get(value);
1580
+ if (option) return { value, label: option.label ?? value };
1581
+ return { value, label: value, other: true };
1582
+ }
1583
+ function answerSnapshots(definition, answers) {
1584
+ const out = {};
1585
+ for (const block of definition.blocks) {
1586
+ if (!isChoice(block) || !optionsSource(block)) continue;
1587
+ const value = own(answers, block.id);
1588
+ if (isEmptyValue(value, block)) continue;
1589
+ const byValue = new Map(choiceOptions(block).map((o) => [o.value, o]));
1590
+ let snapshot;
1591
+ if (Array.isArray(value)) {
1592
+ const items = value.filter((v) => typeof v === "string").map((v) => snapshotOf(byValue, v));
1593
+ if (items.length) snapshot = items;
1594
+ } else if (typeof value === "string") snapshot = snapshotOf(byValue, value);
1595
+ if (snapshot) put(out, block.id, snapshot);
1596
+ }
1597
+ return out;
1598
+ }
1599
+ function answersFromSnapshots(definition, snapshots) {
1600
+ const answers = {};
1601
+ const missing = {};
1602
+ for (const block of definition.blocks) {
1603
+ if (!isChoice(block)) continue;
1604
+ const raw = own(snapshots, block.id);
1605
+ const list = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).flatMap((s) => {
1606
+ const o = readOption(s);
1607
+ if (!o) return [];
1608
+ const label = o.label ?? o.value;
1609
+ return [s.other === true ? { value: o.value, label, other: true } : { value: o.value, label }];
1610
+ });
1611
+ if (!list.length) continue;
1612
+ const options = choiceOptions(block);
1613
+ const byValue = new Set(options.map((o) => o.value));
1614
+ const allowsOther = block.props?.other !== void 0 && block.props?.other !== false;
1615
+ const found = [];
1616
+ const lost = [];
1617
+ for (const s of list) {
1618
+ let value;
1619
+ if (byValue.has(s.value)) value = s.value;
1620
+ else if (s.other) value = allowsOther ? s.value : void 0;
1621
+ else value = options.find((o) => fold(o.label ?? o.value) === fold(s.label))?.value;
1622
+ if (value === void 0) lost.push(s);
1623
+ else if (!found.includes(value)) found.push(value);
1624
+ }
1625
+ if (found.length) put(answers, block.id, block.type === "multi-choice" ? found : found[0]);
1626
+ if (lost.length) put(missing, block.id, lost);
1627
+ }
1628
+ return { answers, missing };
1629
+ }
1630
+
1366
1631
  // src/recall.ts
1367
1632
  var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
1368
1633
  function recall(text, scope) {
@@ -1484,16 +1749,34 @@ function build(given, options, replaying = false, budget) {
1484
1749
  return at === void 0 ? event : { ...event, at };
1485
1750
  };
1486
1751
  const isVisibleIn = (s, id) => s.path.includes(id);
1752
+ const filters = hasOptionFilters(definition);
1753
+ const clearFiltered = (values, id) => filters ? clearFilteredAnswers(definition, values, id) : values;
1487
1754
  const errorsFor = (s, ids) => {
1488
1755
  const errors = {};
1756
+ let shown2;
1489
1757
  for (const id of ids) {
1490
1758
  const field = fields.get(id);
1491
1759
  if (!field || !isVisibleIn(s, id)) continue;
1492
- const message = validateField(field, own(s.values, id), { custom: options.validators?.[id] });
1760
+ const value = own(s.values, id);
1761
+ let message = validateField(field, value, { custom: options.validators?.[id] });
1762
+ if (!message && filters) message = filteredChoiceError(definition, field, value, shown2 ??= answersOnPath(s));
1493
1763
  if (message) errors[id] = message;
1494
1764
  }
1495
1765
  return errors;
1496
1766
  };
1767
+ const skipped = (s) => new Set(hiddenByPrefill(definition, s));
1768
+ const shownFrom = (s, from, skip) => {
1769
+ for (let i = from; i < s.path.length; i++) if (!skip.has(s.path[i])) return s.path[i];
1770
+ return void 0;
1771
+ };
1772
+ const showCurrent = () => {
1773
+ if (!state.current) return;
1774
+ const skip = skipped(state);
1775
+ if (!skip.has(state.current)) return;
1776
+ const at = state.path.indexOf(state.current);
1777
+ const shown2 = at < 0 ? void 0 : shownFrom(state, at, skip);
1778
+ if (shown2) state = { ...state, current: shown2, furthest: state.furthest === state.current ? shown2 : state.furthest };
1779
+ };
1497
1780
  let unsettled = false;
1498
1781
  let formulasStale = false;
1499
1782
  const settleNow = (needValues = true) => {
@@ -1522,13 +1805,13 @@ function build(given, options, replaying = false, budget) {
1522
1805
  const errors = { ...state.errors };
1523
1806
  delete errors[id];
1524
1807
  if (batch) {
1525
- state.values = { ...state.values, [id]: value };
1808
+ state.values = clearFiltered({ ...state.values, [id]: value }, id);
1526
1809
  state.errors = errors;
1527
1810
  log(event);
1528
1811
  unsettled = true;
1529
1812
  return;
1530
1813
  }
1531
- const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
1814
+ const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
1532
1815
  emit({ ...state, values, path, errors, events: log(event) });
1533
1816
  };
1534
1817
  const engine = {
@@ -1559,7 +1842,7 @@ function build(given, options, replaying = false, budget) {
1559
1842
  return false;
1560
1843
  }
1561
1844
  const at = state.path.indexOf(state.current);
1562
- const target = state.path[at + 1];
1845
+ const target = shownFrom(state, at + 1, skipped(state));
1563
1846
  if (!target) {
1564
1847
  emit({ ...state, events });
1565
1848
  return true;
@@ -1571,8 +1854,9 @@ function build(given, options, replaying = false, budget) {
1571
1854
  back() {
1572
1855
  if (state.status === "completed") return;
1573
1856
  const history = [...state.history];
1857
+ const skip = skipped(state);
1574
1858
  let previous = history.pop();
1575
- while (previous && !state.path.includes(previous)) previous = history.pop();
1859
+ while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
1576
1860
  if (!previous) return;
1577
1861
  emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
1578
1862
  },
@@ -1600,15 +1884,18 @@ function build(given, options, replaying = false, budget) {
1600
1884
  return out;
1601
1885
  }
1602
1886
  };
1603
- if (options.initialValues) {
1887
+ const fromLink = options.linkParams ? prefillFromLink(definition, options.linkParams, options.initialValues) : {};
1888
+ const initialValues = Object.keys(fromLink).length ? { ...fromLink, ...options.initialValues } : options.initialValues;
1889
+ if (initialValues) {
1604
1890
  const wasBatch = batch;
1605
1891
  batch = true;
1606
- for (const [id, value] of Object.entries(options.initialValues)) {
1892
+ for (const [id, value] of Object.entries(initialValues)) {
1607
1893
  if (!fields.has(id) || value === void 0) continue;
1608
1894
  answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
1609
1895
  }
1610
1896
  settleNow();
1611
1897
  batch = wasBatch;
1898
+ showCurrent();
1612
1899
  }
1613
1900
  const resumeAt = options.resumeAt;
1614
1901
  if (resumeAt && !replaying && state.path.includes(resumeAt)) {
@@ -1627,18 +1914,22 @@ function build(given, options, replaying = false, budget) {
1627
1914
  }
1628
1915
  } else if (event.type === "next") {
1629
1916
  settleNow(false);
1917
+ showCurrent();
1630
1918
  if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
1631
1919
  engine.next();
1632
1920
  } else if (event.type === "back") {
1633
1921
  settleNow(false);
1922
+ showCurrent();
1634
1923
  engine.back();
1635
1924
  } else if (event.type === "submit") {
1636
1925
  settleNow();
1926
+ showCurrent();
1637
1927
  engine.submit();
1638
1928
  }
1639
1929
  };
1640
1930
  const finish = () => {
1641
1931
  settleNow();
1932
+ showCurrent();
1642
1933
  batch = false;
1643
1934
  state = { ...state, events: [...state.events] };
1644
1935
  };
@@ -1646,7 +1937,12 @@ function build(given, options, replaying = false, budget) {
1646
1937
  }
1647
1938
  function replayWithin(definition, events, options, maxWork) {
1648
1939
  const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
1649
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
1940
+ const { engine, apply, finish } = build(
1941
+ definition,
1942
+ { ...options, initialValues: void 0, linkParams: void 0, clock: () => void 0 },
1943
+ true,
1944
+ budget
1945
+ );
1650
1946
  try {
1651
1947
  for (const event of events) apply(event);
1652
1948
  finish();
@@ -1718,6 +2014,31 @@ function parseForm(input) {
1718
2014
  const optionalBoolean = (v, path) => {
1719
2015
  if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
1720
2016
  };
2017
+ const optionDetails = (o, at) => {
2018
+ if (o.description !== void 0 && (typeof o.description !== "string" || o.description.length > OPTION_LIMITS.description)) {
2019
+ issue(`${at}.description`, `Must be text of at most ${OPTION_LIMITS.description} characters.`);
2020
+ }
2021
+ if (o.image !== void 0 && !isHttpsUrl(o.image)) {
2022
+ issue(`${at}.image`, `Must be an https:// link of at most ${OPTION_LIMITS.image} characters.`);
2023
+ }
2024
+ if (o.attributes === void 0) return;
2025
+ const attributes = o.attributes;
2026
+ if (!isObject2(attributes) || Object.keys(attributes).length > OPTION_LIMITS.attributes) {
2027
+ issue(`${at}.attributes`, `Must be an object of at most ${OPTION_LIMITS.attributes} attributes.`);
2028
+ return;
2029
+ }
2030
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
2031
+ for (const [name2, v] of Object.entries(attributes)) {
2032
+ if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
2033
+ issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
2034
+ } else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
2035
+ issue(
2036
+ `${at}.attributes.${name2}`,
2037
+ `Must be text, or a list of at most ${OPTION_LIMITS.attributeItems} texts, each at most ${OPTION_LIMITS.attributeValue} characters.`
2038
+ );
2039
+ }
2040
+ }
2041
+ };
1721
2042
  const name = (v, path, what) => {
1722
2043
  if (typeof v !== "string" || !v) {
1723
2044
  issue(path, `${what} must be a non-empty string.`);
@@ -1890,7 +2211,31 @@ function parseForm(input) {
1890
2211
  } else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
1891
2212
  warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
1892
2213
  }
2214
+ const filter = isObject2(from) ? from.filter : void 0;
2215
+ if (filter !== void 0) {
2216
+ const at = `${path}.props.optionsFrom.filter`;
2217
+ if (!isObject2(filter) || typeof filter.attribute !== "string" || !filter.attribute || filter.attribute.length > OPTION_LIMITS.attributeName || typeof filter.field !== "string" || !filter.field) {
2218
+ issue(at, `Must be { attribute, field }: an attribute name (at most ${OPTION_LIMITS.attributeName} characters) and an earlier question's key.`);
2219
+ } else if (!keys.has(filter.field)) {
2220
+ issue(`${at}.field`, `"${filter.field}" must be the key of an earlier question.`);
2221
+ }
2222
+ }
1893
2223
  }
2224
+ const prefill = isObject2(block.props) ? block.props.prefill : void 0;
2225
+ if (prefill !== void 0) {
2226
+ if (!isObject2(prefill) || !isPrefillParam(prefill.param)) {
2227
+ issue(
2228
+ `${path}.props.prefill`,
2229
+ `Must be { param, hideWhenPrefilled? }: a link parameter of letters, digits, "_", "-" or "." (at most ${MAX_PREFILL_PARAM_LENGTH} characters).`
2230
+ );
2231
+ } else {
2232
+ optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
2233
+ if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2234
+ warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
2235
+ }
2236
+ }
2237
+ }
2238
+ if (isObject2(block.props)) optionalBoolean(block.props.searchable, `${path}.props.searchable`);
1894
2239
  if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
1895
2240
  else if (block.props.options !== void 0) {
1896
2241
  const options = block.props.options;
@@ -1899,7 +2244,10 @@ function parseForm(input) {
1899
2244
  options.forEach((o, j) => {
1900
2245
  const at = `${path}.props.options[${j}]`;
1901
2246
  if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
1902
- else optionalText(o.label, `${at}.label`);
2247
+ else {
2248
+ optionalText(o.label, `${at}.label`);
2249
+ optionDetails(o, at);
2250
+ }
1903
2251
  });
1904
2252
  if (from === void 0) {
1905
2253
  choices.set(
@@ -2192,6 +2540,377 @@ function parseForm(input) {
2192
2540
  return issues.length ? fail() : { ok: true, form: input, warnings };
2193
2541
  }
2194
2542
 
2543
+ // src/edit-link.ts
2544
+ var EDIT_LINK_TTL_SECONDS = 30 * 24 * 60 * 60;
2545
+ var MIN_EDIT_SECRET_BYTES = 32;
2546
+ var EDIT_LINK_PARAM = "edit";
2547
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2548
+ var NONCE = /^[A-Za-z0-9_-]{16,64}$/;
2549
+ var EXPIRES = /^[1-9][0-9]{0,11}$/;
2550
+ var SIG = /^[A-Za-z0-9_-]{43}$/;
2551
+ var DOMAIN = "edit-link:v2:";
2552
+ var MAX_TOKEN_LENGTH = 300;
2553
+ var encoder = new TextEncoder();
2554
+ function toBase64Url(bytes) {
2555
+ let binary = "";
2556
+ for (const b of bytes) binary += String.fromCharCode(b);
2557
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2558
+ }
2559
+ function fromBase64Url(text) {
2560
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
2561
+ const bytes = new Uint8Array(binary.length);
2562
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2563
+ return bytes;
2564
+ }
2565
+ function isUsableEditSecret(secret) {
2566
+ return typeof secret === "string" && secret.trim().length > 0 && encoder.encode(secret).length >= MIN_EDIT_SECRET_BYTES;
2567
+ }
2568
+ function hmacKey(secret, usage) {
2569
+ return crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [usage]);
2570
+ }
2571
+ var payload = (id, nonce, expiresAt) => encoder.encode(`${DOMAIN}${id}.${nonce}.${expiresAt}`);
2572
+ async function signEditToken(link, secret, options = {}) {
2573
+ if (!isUsableEditSecret(secret) || !UUID.test(link.id) || !NONCE.test(link.nonce)) return null;
2574
+ const ttl = options.ttlSeconds ?? EDIT_LINK_TTL_SECONDS;
2575
+ if (!Number.isFinite(ttl) || ttl <= 0) return null;
2576
+ const id = link.id.toLowerCase();
2577
+ const expiresAt = Math.floor((options.now ?? Date.now()) / 1e3) + Math.floor(ttl);
2578
+ const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret, "sign"), payload(id, link.nonce, expiresAt));
2579
+ return `${id}.${link.nonce}.${expiresAt}.${toBase64Url(new Uint8Array(sig))}`;
2580
+ }
2581
+ async function verifyEditToken(token, secret, options = {}) {
2582
+ if (!isUsableEditSecret(secret) || typeof token !== "string" || token.length > MAX_TOKEN_LENGTH) return null;
2583
+ const parts = token.split(".");
2584
+ if (parts.length !== 4) return null;
2585
+ const [id, nonce, expires, sig] = parts;
2586
+ if (!UUID.test(id) || id !== id.toLowerCase() || !NONCE.test(nonce) || !EXPIRES.test(expires)) return null;
2587
+ if (!SIG.test(sig)) return null;
2588
+ const expiresAt = Number(expires);
2589
+ const now = Math.floor((options.now ?? Date.now()) / 1e3);
2590
+ const ttl = options.ttlSeconds ?? EDIT_LINK_TTL_SECONDS;
2591
+ if (expiresAt <= now || expiresAt > now + ttl) return null;
2592
+ let bytes;
2593
+ try {
2594
+ bytes = fromBase64Url(sig);
2595
+ } catch {
2596
+ return null;
2597
+ }
2598
+ const ok = await crypto.subtle.verify("HMAC", await hmacKey(secret, "verify"), bytes, payload(id, nonce, expiresAt));
2599
+ return ok ? { id, nonce, expiresAt } : null;
2600
+ }
2601
+ function withEditToken(formUrl, token, param = EDIT_LINK_PARAM) {
2602
+ return `${formUrl}${formUrl.includes("?") ? "&" : "?"}${param}=${encodeURIComponent(token)}`;
2603
+ }
2604
+ async function editLinkUrl(formUrl, link, secret, options = {}) {
2605
+ const token = await signEditToken(link, secret, options);
2606
+ return token ? withEditToken(formUrl, token, options.param) : null;
2607
+ }
2608
+ function createEditSecretReader(options) {
2609
+ const { variable } = options;
2610
+ let warned = false;
2611
+ const read = (env, log) => {
2612
+ const secret = env[variable];
2613
+ if (isUsableEditSecret(secret)) return secret;
2614
+ if (!warned) {
2615
+ warned = true;
2616
+ (log ?? options.log ?? ((m) => console.warn(m)))(
2617
+ `${variable} is ${secret ? `shorter than ${MIN_EDIT_SECRET_BYTES} bytes` : "not set"}: edit links are off.`
2618
+ );
2619
+ }
2620
+ return null;
2621
+ };
2622
+ return Object.assign(read, {
2623
+ reset: () => {
2624
+ warned = false;
2625
+ }
2626
+ });
2627
+ }
2628
+ function isEditLinkRequest(url, options = {}) {
2629
+ const isFormPage = options.isFormPage ?? (() => true);
2630
+ return isFormPage(url) && url.searchParams.has(options.param ?? EDIT_LINK_PARAM);
2631
+ }
2632
+ function withEditLinkHeaders(request, response, options = {}) {
2633
+ let url;
2634
+ try {
2635
+ url = new URL(request.url);
2636
+ } catch {
2637
+ return response;
2638
+ }
2639
+ if (!isEditLinkRequest(url, options)) return response;
2640
+ const headers = new Headers(response.headers);
2641
+ headers.set("referrer-policy", "no-referrer");
2642
+ headers.set("cache-control", "private, no-store");
2643
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
2644
+ }
2645
+
2646
+ // src/rate-limit.ts
2647
+ var DEFAULT_TRUSTED_IP_HEADERS = ["x-vercel-forwarded-for", "x-forwarded-for", "x-real-ip"];
2648
+ var RATE_LIMIT_DEFAULTS = {
2649
+ submit: { limit: 60, windowSeconds: 600 },
2650
+ progress: { limit: 900, windowSeconds: 600 },
2651
+ upload: { limit: 120, windowSeconds: 600 }
2652
+ };
2653
+ function clientIp(headers, trusted = DEFAULT_TRUSTED_IP_HEADERS) {
2654
+ for (const name of trusted) {
2655
+ const first = headers.get(name)?.split(",")[0]?.trim();
2656
+ if (first) return first;
2657
+ }
2658
+ return "unknown";
2659
+ }
2660
+ function addressPrefix(ip) {
2661
+ const bare = ip.replace(/^\[|\](:\d+)?$/g, "").split("%")[0].toLowerCase();
2662
+ if (!bare.includes(":")) return bare;
2663
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(bare);
2664
+ if (mapped) return mapped[1];
2665
+ const [head = "", tail = ""] = bare.split("::");
2666
+ const left = head ? head.split(":") : [];
2667
+ const right = bare.includes("::") && tail ? tail.split(":") : [];
2668
+ const groups = bare.includes("::") ? [...left, ...Array(Math.max(0, 8 - left.length - right.length)).fill("0"), ...right] : left;
2669
+ return `${groups.slice(0, 4).map((g) => (g || "0").replace(/^0+(?=.)/, "")).join(":")}::/64`;
2670
+ }
2671
+ async function rateLimitKey(input) {
2672
+ const encoder2 = new TextEncoder();
2673
+ const key = await crypto.subtle.importKey("raw", encoder2.encode(input.secret), { name: "HMAC", hash: "SHA-256" }, false, [
2674
+ "sign"
2675
+ ]);
2676
+ const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder2.encode(addressPrefix(input.ip))));
2677
+ let hex = "";
2678
+ for (const b of mac) hex += b.toString(16).padStart(2, "0");
2679
+ return `${input.kind}:${input.scope}:${hex}`;
2680
+ }
2681
+ async function allow(store, check, options = {}) {
2682
+ let allowed;
2683
+ try {
2684
+ allowed = await store.hit(check.key, check.limit, check.windowSeconds);
2685
+ } catch {
2686
+ allowed = null;
2687
+ }
2688
+ if (allowed !== null) return allowed;
2689
+ (options.log ?? ((m) => console.warn(m)))(
2690
+ options.message ?? "The rate limit could not be checked; letting the request through."
2691
+ );
2692
+ return true;
2693
+ }
2694
+
2695
+ // src/uploads.ts
2696
+ function uploadFields(definition) {
2697
+ return definition.blocks.filter(
2698
+ (b) => b.kind === "field" && UPLOAD_FIELD_TYPES.includes(b.type)
2699
+ );
2700
+ }
2701
+ function uploadRules(field) {
2702
+ if (field.type !== PHOTO_TYPE) return null;
2703
+ const limits = photoLimits(field);
2704
+ return {
2705
+ maxFiles: limits.maxFiles,
2706
+ maxBytes: limits.maxBytes,
2707
+ mimeTypes: limits.mimeTypes,
2708
+ typeMessage: photoTypeMessage(field),
2709
+ sizeMessage: photoSizeMessage(field),
2710
+ countMessage: photoCountMessage(field)
2711
+ };
2712
+ }
2713
+ var NOT_UPLOADED_MESSAGE = "A photo didn't finish uploading. Remove it and add it again.";
2714
+ function owns(upload, owner) {
2715
+ if ("attemptHash" in owner) return upload.responseId === null && upload.attemptHash === owner.attemptHash;
2716
+ return upload.responseId === owner.responseId || upload.responseId === null && upload.editOf === owner.responseId;
2717
+ }
2718
+ async function checkUploads(definition, answers, options) {
2719
+ const asked = uploadFields(definition).filter((f) => photoRefs(answers[f.id]).length > 0);
2720
+ if (!asked.length) return { ok: true, answers };
2721
+ const { store, formId, owner } = options;
2722
+ if (!store) throw new Error("Answers that name uploaded files need an upload store.");
2723
+ const ids = asked.flatMap((f) => photoRefs(answers[f.id]).map((r) => r.id));
2724
+ const stored = new Map((await store.uploadRefs(formId, ids)).map((u) => [u.id, u]));
2725
+ const out = { ...answers };
2726
+ const errors = {};
2727
+ for (const field of asked) {
2728
+ const rules = uploadRules(field);
2729
+ const refs = photoRefs(answers[field.id]);
2730
+ if (refs.length > rules.maxFiles) {
2731
+ errors[field.id] = rules.countMessage;
2732
+ continue;
2733
+ }
2734
+ const checked = [];
2735
+ for (const ref of refs) {
2736
+ const upload = stored.get(ref.id);
2737
+ if (!upload || upload.fieldId !== field.id || !owns(upload, owner) || !upload.stored) {
2738
+ errors[field.id] = NOT_UPLOADED_MESSAGE;
2739
+ break;
2740
+ }
2741
+ const type = (upload.storedType ?? "").toLowerCase();
2742
+ const size = upload.storedSize ?? 0;
2743
+ if (!rules.mimeTypes.includes(type)) {
2744
+ errors[field.id] = rules.typeMessage;
2745
+ break;
2746
+ }
2747
+ if (size < 1 || size > rules.maxBytes) {
2748
+ errors[field.id] = rules.sizeMessage;
2749
+ break;
2750
+ }
2751
+ checked.push({ id: upload.id, name: upload.name, type, size });
2752
+ }
2753
+ out[field.id] = checked;
2754
+ }
2755
+ return Object.keys(errors).length ? { ok: false, errors } : { ok: true, answers: out };
2756
+ }
2757
+ async function attemptHash(attemptId) {
2758
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(attemptId)));
2759
+ let hex = "";
2760
+ for (const b of digest) hex += b.toString(16).padStart(2, "0");
2761
+ return hex;
2762
+ }
2763
+
2764
+ // src/upload-protocol.ts
2765
+ var MAX_UPLOAD_REQUEST_BYTES = 4e3;
2766
+
2767
+ // src/upload-handler.ts
2768
+ var UPLOAD_MESSAGES = {
2769
+ notJson: "Send the request as JSON.",
2770
+ tooLong: "That request is too long.",
2771
+ reload: "Reload the page and try again.",
2772
+ unavailable: "This form isn't available.",
2773
+ notOpen: "This form isn't open right now.",
2774
+ notTakingChanges: "This form isn't taking changes now.",
2775
+ badEditLink: "This edit link doesn't work any more. Use the newest one: from your latest confirmation email, or the one shown after your last change.",
2776
+ notUploadField: "This question doesn't take photos.",
2777
+ rateLimited: "Too many photos from here. Try again in a few minutes.",
2778
+ tooMany: "That's as many photos as one response can take.",
2779
+ failed: "That photo couldn't be added. Please try again."
2780
+ };
2781
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2782
+ var json = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store" } });
2783
+ async function readCapped(request, max) {
2784
+ if (!request.body) return "";
2785
+ const reader = request.body.getReader();
2786
+ const chunks = [];
2787
+ let size = 0;
2788
+ for (; ; ) {
2789
+ const { done, value } = await reader.read();
2790
+ if (done) break;
2791
+ size += value.byteLength;
2792
+ if (size > max) {
2793
+ await reader.cancel().catch(() => {
2794
+ });
2795
+ return null;
2796
+ }
2797
+ chunks.push(value);
2798
+ }
2799
+ const bytes = new Uint8Array(size);
2800
+ let offset = 0;
2801
+ for (const chunk of chunks) {
2802
+ bytes.set(chunk, offset);
2803
+ offset += chunk.byteLength;
2804
+ }
2805
+ return new TextDecoder().decode(bytes);
2806
+ }
2807
+ function createUploadHandler(options) {
2808
+ const say = { ...UPLOAD_MESSAGES, ...options.messages };
2809
+ const log = options.log ?? ((m) => console.error(`[upload] ${m}`));
2810
+ const hash = options.hashAttempt ?? attemptHash;
2811
+ const maxBytes = options.maxRequestBytes ?? MAX_UPLOAD_REQUEST_BYTES;
2812
+ return async (request, params) => {
2813
+ const contentType = (request.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
2814
+ if (contentType !== "application/json") return json({ error: say.notJson }, 415);
2815
+ const text = await readCapped(request, maxBytes);
2816
+ if (text === null) return json({ error: say.tooLong }, 413);
2817
+ let body;
2818
+ try {
2819
+ const parsed = JSON.parse(text);
2820
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object");
2821
+ body = parsed;
2822
+ } catch {
2823
+ return json({ error: say.notJson }, 400);
2824
+ }
2825
+ const fieldId = typeof body["fieldId"] === "string" ? body["fieldId"] : "";
2826
+ const name = typeof body["name"] === "string" ? body["name"].trim().slice(0, 255) : "";
2827
+ const type = typeof body["type"] === "string" ? body["type"].trim().toLowerCase() : "";
2828
+ const size = body["size"];
2829
+ const editing = body["edit"] !== void 0;
2830
+ const attemptId = body["attemptId"];
2831
+ if (!editing && (typeof attemptId !== "string" || !UUID2.test(attemptId))) {
2832
+ return json({ error: say.reload }, 400);
2833
+ }
2834
+ let form;
2835
+ try {
2836
+ form = await options.resolveForm(request, params);
2837
+ } catch (error) {
2838
+ log(error instanceof Error ? error.message : "Loading the form failed.");
2839
+ return json({ error: say.failed }, 503);
2840
+ }
2841
+ if (!form) return json({ error: say.unavailable }, 404);
2842
+ if (!form.open) {
2843
+ return editing ? json({ error: say.notTakingChanges, code: "closed" }, 409) : json({ error: say.notOpen }, 409);
2844
+ }
2845
+ const block = form.definition.blocks.find((b) => b.kind === "field" && b.id === fieldId);
2846
+ const field = block?.kind === "field" ? block : null;
2847
+ const rules = field ? uploadRules(field) : null;
2848
+ if (!field || !rules) return json({ error: say.notUploadField }, 400);
2849
+ if (!rules.mimeTypes.includes(type)) return json({ error: rules.typeMessage }, 415);
2850
+ if (typeof size !== "number" || !Number.isInteger(size) || size < 1 || size > rules.maxBytes) {
2851
+ return json({ error: rules.sizeMessage }, 413);
2852
+ }
2853
+ let editOf = null;
2854
+ if (editing) {
2855
+ const secret = typeof options.edit?.secret === "function" ? options.edit.secret() : options.edit?.secret;
2856
+ const claims = options.edit ? await verifyEditToken(
2857
+ typeof body["edit"] === "string" ? body["edit"] : null,
2858
+ secret,
2859
+ options.now ? { now: options.now().getTime() } : {}
2860
+ ) : null;
2861
+ if (!claims || form.allowsEdits !== true) return json({ error: say.badEditLink, code: "bad_link" }, 403);
2862
+ let stored;
2863
+ try {
2864
+ stored = await options.edit.response(claims.id, form);
2865
+ } catch {
2866
+ log(`Loading response ${claims.id} for an upload failed.`);
2867
+ return json({ error: say.failed }, 503);
2868
+ }
2869
+ if (!stored || stored.editNonce !== claims.nonce) return json({ error: say.badEditLink, code: "bad_link" }, 403);
2870
+ editOf = stored.id;
2871
+ }
2872
+ if (options.rateLimit) {
2873
+ const limit = options.rateLimit;
2874
+ const key = await rateLimitKey({
2875
+ kind: "upload",
2876
+ scope: form.scope,
2877
+ ip: clientIp(request.headers, limit.trustedIpHeaders ?? DEFAULT_TRUSTED_IP_HEADERS),
2878
+ secret: limit.secret
2879
+ });
2880
+ const allowed = await allow(
2881
+ limit.store,
2882
+ {
2883
+ key,
2884
+ limit: limit.limit ?? RATE_LIMIT_DEFAULTS.upload.limit,
2885
+ windowSeconds: limit.windowSeconds ?? RATE_LIMIT_DEFAULTS.upload.windowSeconds
2886
+ },
2887
+ { log, message: "The upload rate limit could not be checked; letting the upload through." }
2888
+ );
2889
+ if (!allowed) return json({ error: say.rateLimited }, 429);
2890
+ }
2891
+ const fileName = name || "photo";
2892
+ try {
2893
+ const begun = await options.store.beginUpload({
2894
+ formId: form.id,
2895
+ fieldId: field.id,
2896
+ name: fileName,
2897
+ contentType: type,
2898
+ size,
2899
+ attemptHash: editOf ? null : await hash(attemptId),
2900
+ editOf
2901
+ });
2902
+ if ("tooMany" in begun) return json({ error: say.tooMany, code: "too_many_photos" }, 429);
2903
+ const signed = await options.store.signUpload(begun.objectName);
2904
+ return json({ ref: { id: begun.id, name: fileName, type, size }, upload: signed });
2905
+ } catch (error) {
2906
+ log(
2907
+ `Starting an upload on ${form.label ?? form.id} for ${form.scope} failed: ${error instanceof Error ? error.message : "unexpected error"}`
2908
+ );
2909
+ return json({ error: say.failed }, 500);
2910
+ }
2911
+ };
2912
+ }
2913
+
2195
2914
  // src/server.ts
2196
2915
  var MAX_EVENTS = 1e4;
2197
2916
  var MAX_SUBMISSION_VALUES = 2e5;
@@ -2329,11 +3048,12 @@ function validateSubmission(published, submission, options = {}) {
2329
3048
  }
2330
3049
  } else {
2331
3050
  const errors = {};
2332
- for (const [id, value] of Object.entries(engine.answers())) {
3051
+ const kept = engine.answers();
3052
+ for (const [id, value] of Object.entries(kept)) {
2333
3053
  const message = validateField(fields.get(id), value, {
2334
3054
  requireAnswers: false,
2335
3055
  custom: options.validators?.[id]
2336
- });
3056
+ }) ?? filteredChoiceError(definition, fields.get(id), value, kept);
2337
3057
  if (message) errors[id] = message;
2338
3058
  }
2339
3059
  if (Object.keys(errors).length) return { ok: false, errors };
@@ -2375,11 +3095,39 @@ function publicDefinition(definition) {
2375
3095
  return JSON.parse(JSON.stringify(definition));
2376
3096
  }
2377
3097
 
3098
+ exports.DEFAULT_TRUSTED_IP_HEADERS = DEFAULT_TRUSTED_IP_HEADERS;
3099
+ exports.EDIT_LINK_PARAM = EDIT_LINK_PARAM;
3100
+ exports.EDIT_LINK_TTL_SECONDS = EDIT_LINK_TTL_SECONDS;
2378
3101
  exports.MAX_EVENTS = MAX_EVENTS;
2379
3102
  exports.MAX_REPLAY_WORK = MAX_REPLAY_WORK;
2380
3103
  exports.MAX_SUBMISSION_VALUES = MAX_SUBMISSION_VALUES;
3104
+ exports.MAX_UPLOAD_REQUEST_BYTES = MAX_UPLOAD_REQUEST_BYTES;
3105
+ exports.MIN_EDIT_SECRET_BYTES = MIN_EDIT_SECRET_BYTES;
3106
+ exports.NOT_UPLOADED_MESSAGE = NOT_UPLOADED_MESSAGE;
3107
+ exports.RATE_LIMIT_DEFAULTS = RATE_LIMIT_DEFAULTS;
2381
3108
  exports.RAW_EVENTS_PER_EVENT = RAW_EVENTS_PER_EVENT;
3109
+ exports.UPLOAD_MESSAGES = UPLOAD_MESSAGES;
3110
+ exports.addressPrefix = addressPrefix;
3111
+ exports.allow = allow;
3112
+ exports.answerSnapshots = answerSnapshots;
3113
+ exports.answersFromSnapshots = answersFromSnapshots;
3114
+ exports.attemptHash = attemptHash;
3115
+ exports.checkUploads = checkUploads;
3116
+ exports.clientIp = clientIp;
3117
+ exports.createEditSecretReader = createEditSecretReader;
3118
+ exports.createUploadHandler = createUploadHandler;
3119
+ exports.editLinkUrl = editLinkUrl;
3120
+ exports.isEditLinkRequest = isEditLinkRequest;
3121
+ exports.isUsableEditSecret = isUsableEditSecret;
2382
3122
  exports.publicDefinition = publicDefinition;
3123
+ exports.rateLimitKey = rateLimitKey;
3124
+ exports.signEditToken = signEditToken;
3125
+ exports.uploadFields = uploadFields;
3126
+ exports.uploadRules = uploadRules;
2383
3127
  exports.validateSubmission = validateSubmission;
3128
+ exports.verifyEditToken = verifyEditToken;
3129
+ exports.withEditLinkHeaders = withEditLinkHeaders;
3130
+ exports.withEditToken = withEditToken;
3131
+ exports.withHostOptions = withHostOptions;
2384
3132
  //# sourceMappingURL=server.cjs.map
2385
3133
  //# sourceMappingURL=server.cjs.map