@formancy/server-core 0.2.0 → 0.4.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/index.d.mts +276 -146
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +255 -190
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import { hmac } from "@noble/hashes/hmac.js";
|
|
|
3
3
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
4
4
|
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
5
5
|
import { diffSchemas, modelDataPaths, schemaHash } from "@formancy/spec";
|
|
6
|
-
import {
|
|
7
|
-
import { createFormEngine, expressionProblems } from "@formancy/core";
|
|
6
|
+
import { createFormEngine, expressionProblems, unknownReferences } from "@formancy/core";
|
|
8
7
|
import { check } from "recheck";
|
|
8
|
+
import { validateSchema } from "@formancy/spec/validate";
|
|
9
9
|
//#region src/options-membership.ts
|
|
10
10
|
/**
|
|
11
11
|
* Every sourced answer in a submission, as `path → { source, value }`.
|
|
@@ -29,12 +29,15 @@ function sourcedAnswers(schema, value) {
|
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
31
|
if (field.optionsSource === void 0) continue;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
const answers = Array.isArray(held) ? held : [held];
|
|
33
|
+
for (const answer of answers) {
|
|
34
|
+
if (typeof answer !== "string" || answer === "") continue;
|
|
35
|
+
found.push({
|
|
36
|
+
path: at,
|
|
37
|
+
source: field.optionsSource,
|
|
38
|
+
value: answer
|
|
39
|
+
});
|
|
40
|
+
}
|
|
38
41
|
}
|
|
39
42
|
};
|
|
40
43
|
walk(schema.model.fields, value, "");
|
|
@@ -492,6 +495,38 @@ async function offerUpload(deps, input) {
|
|
|
492
495
|
};
|
|
493
496
|
}
|
|
494
497
|
/**
|
|
498
|
+
* Whether an upload's bytes may be kept.
|
|
499
|
+
*
|
|
500
|
+
* **Fail closed.** A scanner that cannot be asked refuses the file, the same as one that
|
|
501
|
+
* found something: a deployment that configured a scanner did so to keep unscanned files
|
|
502
|
+
* out, and its being down is not a reason to let one in. The person is told to try again,
|
|
503
|
+
* and the field offers exactly that. Without a scanner every upload is kept, as it was
|
|
504
|
+
* before there was one — a supported state, said in the documentation rather than here
|
|
505
|
+
* ([0131](../../../docs/decisions/0131-an-upload-is-scanned-before-it-is-kept.md)).
|
|
506
|
+
*/
|
|
507
|
+
async function screenUpload(scanner, file, bytes) {
|
|
508
|
+
if (scanner === void 0) return { ok: true };
|
|
509
|
+
let verdict;
|
|
510
|
+
try {
|
|
511
|
+
verdict = await scanner.scan({
|
|
512
|
+
name: file.name,
|
|
513
|
+
contentType: file.contentType,
|
|
514
|
+
size: file.size
|
|
515
|
+
}, bytes);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
reason: "scanner_unavailable",
|
|
520
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return verdict.clean ? { ok: true } : {
|
|
524
|
+
ok: false,
|
|
525
|
+
reason: "refused",
|
|
526
|
+
finding: verdict.finding
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
495
530
|
* Which files a submission may claim, checked but not yet claimed.
|
|
496
531
|
*
|
|
497
532
|
* The claiming itself is `insertSubmission`'s job, in the same call that
|
|
@@ -623,187 +658,7 @@ function fileFieldPaths(schema) {
|
|
|
623
658
|
return modelDataPaths(schema.model).filter((path) => fileFieldAt(schema, path)?.type === "file");
|
|
624
659
|
}
|
|
625
660
|
//#endregion
|
|
626
|
-
//#region src/redos.ts
|
|
627
|
-
/** Every `pattern` in the model, with the data path of the field it sits on. */
|
|
628
|
-
function patternsIn(fields, prefix) {
|
|
629
|
-
const found = [];
|
|
630
|
-
for (const field of fields) {
|
|
631
|
-
const path = field.type === "page" ? prefix : field.type === "repeater" ? `${prefix}${field.key}[].` : field.type === "group" ? `${prefix}${field.key}.` : `${prefix}${field.key}`;
|
|
632
|
-
if (typeof field.pattern === "string" && field.pattern !== "") found.push({
|
|
633
|
-
path,
|
|
634
|
-
pattern: field.pattern
|
|
635
|
-
});
|
|
636
|
-
if (field.fields !== void 0) found.push(...patternsIn(field.fields, path));
|
|
637
|
-
}
|
|
638
|
-
return found;
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* The patterns in this schema that an attacker could exploit, or an empty array
|
|
642
|
-
* when there are none.
|
|
643
|
-
*
|
|
644
|
-
* A pattern `recheck` cannot decide about is ACCEPTED. Refusing on `unknown`
|
|
645
|
-
* would make publishing depend on an analysis timeout, which is a worse failure
|
|
646
|
-
* than the one being prevented — an author whose legitimate form stops
|
|
647
|
-
* publishing intermittently has no way to act on that.
|
|
648
|
-
*/
|
|
649
|
-
async function unsafePatterns(schema) {
|
|
650
|
-
const unsafe = [];
|
|
651
|
-
for (const { path, pattern } of patternsIn(schema.model.fields, "")) {
|
|
652
|
-
const diagnostics = await check(`^(?:${pattern})$`, "");
|
|
653
|
-
if (diagnostics.status !== "vulnerable") continue;
|
|
654
|
-
const complexity = diagnostics.complexity;
|
|
655
|
-
unsafe.push({
|
|
656
|
-
path,
|
|
657
|
-
pattern,
|
|
658
|
-
complexity: complexity === void 0 ? "unknown" : complexity.type === "polynomial" ? `polynomial degree ${String(complexity.degree)}` : complexity.type
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
return unsafe;
|
|
662
|
-
}
|
|
663
|
-
//#endregion
|
|
664
661
|
//#region src/use-cases.ts
|
|
665
|
-
/**
|
|
666
|
-
* Publish a schema as a form's next version.
|
|
667
|
-
*
|
|
668
|
-
* This is the save gate: structural validation, then the ENGINE's own compile
|
|
669
|
-
* — type-checked expressions, kind policies and the cycle graph — so a form
|
|
670
|
-
* that can loop or read a ghost field is refused here and never persisted.
|
|
671
|
-
* Republishing the identical document is idempotent, because "deploy again"
|
|
672
|
-
* must never manufacture a version.
|
|
673
|
-
*/
|
|
674
|
-
async function publishForm(deps, input) {
|
|
675
|
-
const validated = validateSchema(input.schema);
|
|
676
|
-
if (!validated.valid) return {
|
|
677
|
-
ok: false,
|
|
678
|
-
kind: "invalid_schema",
|
|
679
|
-
errors: validated.errors
|
|
680
|
-
};
|
|
681
|
-
const schema = validated.schema;
|
|
682
|
-
try {
|
|
683
|
-
createFormEngine({
|
|
684
|
-
schema,
|
|
685
|
-
capabilities: deps.capabilities
|
|
686
|
-
});
|
|
687
|
-
} catch (error) {
|
|
688
|
-
return {
|
|
689
|
-
ok: false,
|
|
690
|
-
kind: "invalid_logic",
|
|
691
|
-
message: error instanceof Error ? error.message : String(error)
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
const problems = expressionProblems(schema);
|
|
695
|
-
if (problems.length > 0) return {
|
|
696
|
-
ok: false,
|
|
697
|
-
kind: "invalid_logic",
|
|
698
|
-
message: problems.map((problem) => problem.message).join("\n")
|
|
699
|
-
};
|
|
700
|
-
const unsafe = await unsafePatterns(schema);
|
|
701
|
-
if (unsafe.length > 0) return {
|
|
702
|
-
ok: false,
|
|
703
|
-
kind: "unsafe_pattern",
|
|
704
|
-
patterns: unsafe
|
|
705
|
-
};
|
|
706
|
-
if (deps.optionsSources !== void 0) {
|
|
707
|
-
const unknown = sourceNamesIn(schema).filter((name) => deps.optionsSources?.[name] === void 0);
|
|
708
|
-
if (unknown.length > 0) return {
|
|
709
|
-
ok: false,
|
|
710
|
-
kind: "unknown_options_source",
|
|
711
|
-
sources: unknown
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
const hash = schemaHash(schema);
|
|
715
|
-
const existing = await deps.storage.getFormByPath(input.path);
|
|
716
|
-
if (existing !== void 0) {
|
|
717
|
-
const current = existing.currentVersionId === null ? void 0 : await deps.storage.getVersionById(existing.currentVersionId);
|
|
718
|
-
if (current !== void 0 && current.schemaHash === hash) return {
|
|
719
|
-
ok: true,
|
|
720
|
-
formId: existing.id,
|
|
721
|
-
versionId: current.id,
|
|
722
|
-
version: current.version,
|
|
723
|
-
schemaHash: hash
|
|
724
|
-
};
|
|
725
|
-
const version = await deps.storage.latestVersionNumber(existing.id) + 1;
|
|
726
|
-
const versionId = deps.newId();
|
|
727
|
-
await deps.storage.publishVersion({
|
|
728
|
-
version: {
|
|
729
|
-
id: versionId,
|
|
730
|
-
formId: existing.id,
|
|
731
|
-
version,
|
|
732
|
-
schema,
|
|
733
|
-
schemaHash: hash
|
|
734
|
-
},
|
|
735
|
-
audit: publishAudit(deps, input, {
|
|
736
|
-
path: input.path,
|
|
737
|
-
version,
|
|
738
|
-
hash,
|
|
739
|
-
sources: sourceNamesIn(schema)
|
|
740
|
-
})
|
|
741
|
-
});
|
|
742
|
-
return {
|
|
743
|
-
ok: true,
|
|
744
|
-
formId: existing.id,
|
|
745
|
-
versionId,
|
|
746
|
-
version,
|
|
747
|
-
schemaHash: hash
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
const formId = deps.newId();
|
|
751
|
-
const versionId = deps.newId();
|
|
752
|
-
await deps.storage.publishVersion({
|
|
753
|
-
form: {
|
|
754
|
-
id: formId,
|
|
755
|
-
path: input.path,
|
|
756
|
-
currentVersionId: null,
|
|
757
|
-
accessSubmit: "authenticated",
|
|
758
|
-
allowedOrigins: null
|
|
759
|
-
},
|
|
760
|
-
version: {
|
|
761
|
-
id: versionId,
|
|
762
|
-
formId,
|
|
763
|
-
version: 1,
|
|
764
|
-
schema,
|
|
765
|
-
schemaHash: hash
|
|
766
|
-
},
|
|
767
|
-
audit: publishAudit(deps, input, {
|
|
768
|
-
path: input.path,
|
|
769
|
-
version: 1,
|
|
770
|
-
hash,
|
|
771
|
-
sources: sourceNamesIn(schema)
|
|
772
|
-
})
|
|
773
|
-
});
|
|
774
|
-
return {
|
|
775
|
-
ok: true,
|
|
776
|
-
formId,
|
|
777
|
-
versionId,
|
|
778
|
-
version: 1,
|
|
779
|
-
schemaHash: hash
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* The audit row for a publish, built here so it can travel INSIDE the
|
|
784
|
-
* transaction rather than being appended after it.
|
|
785
|
-
*
|
|
786
|
-
* Before this it was written by the route once all three storage calls had
|
|
787
|
-
* returned, which meant a publish could half-apply and still be recorded as
|
|
788
|
-
* having happened. Now it rolls back with everything else.
|
|
789
|
-
*/
|
|
790
|
-
function publishAudit(deps, input, about) {
|
|
791
|
-
return {
|
|
792
|
-
id: deps.newId(),
|
|
793
|
-
at: deps.nowIso(),
|
|
794
|
-
action: "form.published",
|
|
795
|
-
subject: about.path,
|
|
796
|
-
...input.actor === void 0 ? {} : {
|
|
797
|
-
actorKind: input.actor.kind,
|
|
798
|
-
actorId: input.actor.id
|
|
799
|
-
},
|
|
800
|
-
detail: {
|
|
801
|
-
version: about.version,
|
|
802
|
-
schemaHash: about.hash,
|
|
803
|
-
...about.sources.length === 0 ? {} : { optionsSources: [...about.sources].sort().join(", ") }
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
662
|
async function resolveForm(deps, path) {
|
|
808
663
|
const form = await deps.storage.getFormByPath(path);
|
|
809
664
|
if (form === void 0 || form.currentVersionId === null) return void 0;
|
|
@@ -865,8 +720,11 @@ async function createSubmission(deps, input) {
|
|
|
865
720
|
const engine = createFormEngine({
|
|
866
721
|
schema: current.schema,
|
|
867
722
|
initialValue: input.data,
|
|
868
|
-
capabilities: deps.capabilities
|
|
723
|
+
capabilities: deps.capabilities,
|
|
724
|
+
mode: "server",
|
|
725
|
+
...deps.checks === void 0 ? {} : { checks: deps.checks }
|
|
869
726
|
});
|
|
727
|
+
await engine.settle();
|
|
870
728
|
const outcome = engine.submit();
|
|
871
729
|
if (!outcome.ok) return {
|
|
872
730
|
ok: false,
|
|
@@ -1224,6 +1082,44 @@ async function listVersions(deps, path) {
|
|
|
1224
1082
|
}));
|
|
1225
1083
|
}
|
|
1226
1084
|
//#endregion
|
|
1085
|
+
//#region src/redos.ts
|
|
1086
|
+
/** Every `pattern` in the model, with the data path of the field it sits on. */
|
|
1087
|
+
function patternsIn(fields, prefix) {
|
|
1088
|
+
const found = [];
|
|
1089
|
+
for (const field of fields) {
|
|
1090
|
+
const path = field.type === "page" ? prefix : field.type === "repeater" ? `${prefix}${field.key}[].` : field.type === "group" ? `${prefix}${field.key}.` : `${prefix}${field.key}`;
|
|
1091
|
+
if (typeof field.pattern === "string" && field.pattern !== "") found.push({
|
|
1092
|
+
path,
|
|
1093
|
+
pattern: field.pattern
|
|
1094
|
+
});
|
|
1095
|
+
if (field.fields !== void 0) found.push(...patternsIn(field.fields, path));
|
|
1096
|
+
}
|
|
1097
|
+
return found;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* The patterns in this schema that an attacker could exploit, or an empty array
|
|
1101
|
+
* when there are none.
|
|
1102
|
+
*
|
|
1103
|
+
* A pattern `recheck` cannot decide about is ACCEPTED. Refusing on `unknown`
|
|
1104
|
+
* would make publishing depend on an analysis timeout, which is a worse failure
|
|
1105
|
+
* than the one being prevented — an author whose legitimate form stops
|
|
1106
|
+
* publishing intermittently has no way to act on that.
|
|
1107
|
+
*/
|
|
1108
|
+
async function unsafePatterns(schema) {
|
|
1109
|
+
const unsafe = [];
|
|
1110
|
+
for (const { path, pattern } of patternsIn(schema.model.fields, "")) {
|
|
1111
|
+
const diagnostics = await check(`^(?:${pattern})$`, "");
|
|
1112
|
+
if (diagnostics.status !== "vulnerable") continue;
|
|
1113
|
+
const complexity = diagnostics.complexity;
|
|
1114
|
+
unsafe.push({
|
|
1115
|
+
path,
|
|
1116
|
+
pattern,
|
|
1117
|
+
complexity: complexity === void 0 ? "unknown" : complexity.type === "polynomial" ? `polynomial degree ${String(complexity.degree)}` : complexity.type
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return unsafe;
|
|
1121
|
+
}
|
|
1122
|
+
//#endregion
|
|
1227
1123
|
//#region src/testing/memory-storage.ts
|
|
1228
1124
|
/**
|
|
1229
1125
|
* The in-memory Storage — the second implementation that keeps the port
|
|
@@ -1543,6 +1439,175 @@ function isIpLiteral(host) {
|
|
|
1543
1439
|
return bare.includes(":");
|
|
1544
1440
|
}
|
|
1545
1441
|
//#endregion
|
|
1546
|
-
|
|
1442
|
+
//#region src/publishing.ts
|
|
1443
|
+
/**
|
|
1444
|
+
* Publish a schema as a form's next version.
|
|
1445
|
+
*
|
|
1446
|
+
* This is the save gate: structural validation, then the ENGINE's own compile
|
|
1447
|
+
* — type-checked expressions, kind policies and the cycle graph — so a form
|
|
1448
|
+
* that can loop or read a ghost field is refused here and never persisted.
|
|
1449
|
+
* Republishing the identical document is idempotent, because "deploy again"
|
|
1450
|
+
* must never manufacture a version.
|
|
1451
|
+
*/
|
|
1452
|
+
async function publishForm(deps, input) {
|
|
1453
|
+
const validated = validateSchema(input.schema);
|
|
1454
|
+
if (!validated.valid) return {
|
|
1455
|
+
ok: false,
|
|
1456
|
+
kind: "invalid_schema",
|
|
1457
|
+
errors: validated.errors
|
|
1458
|
+
};
|
|
1459
|
+
const schema = validated.schema;
|
|
1460
|
+
try {
|
|
1461
|
+
createFormEngine({
|
|
1462
|
+
schema,
|
|
1463
|
+
capabilities: deps.capabilities
|
|
1464
|
+
});
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
return {
|
|
1467
|
+
ok: false,
|
|
1468
|
+
kind: "invalid_logic",
|
|
1469
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
const problems = expressionProblems(schema);
|
|
1473
|
+
if (problems.length > 0) return {
|
|
1474
|
+
ok: false,
|
|
1475
|
+
kind: "invalid_logic",
|
|
1476
|
+
message: problems.map((problem) => problem.message).join("\n")
|
|
1477
|
+
};
|
|
1478
|
+
const warnings = unknownReferences(schema).map((found) => found.message);
|
|
1479
|
+
const unsafe = await unsafePatterns(schema);
|
|
1480
|
+
if (unsafe.length > 0) return {
|
|
1481
|
+
ok: false,
|
|
1482
|
+
kind: "unsafe_pattern",
|
|
1483
|
+
patterns: unsafe
|
|
1484
|
+
};
|
|
1485
|
+
if (deps.optionsSources !== void 0) {
|
|
1486
|
+
const unknown = sourceNamesIn(schema).filter((name) => deps.optionsSources?.[name] === void 0);
|
|
1487
|
+
if (unknown.length > 0) return {
|
|
1488
|
+
ok: false,
|
|
1489
|
+
kind: "unknown_options_source",
|
|
1490
|
+
sources: unknown
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const hash = schemaHash(schema);
|
|
1494
|
+
const existing = await deps.storage.getFormByPath(input.path);
|
|
1495
|
+
if (existing !== void 0) {
|
|
1496
|
+
const current = existing.currentVersionId === null ? void 0 : await deps.storage.getVersionById(existing.currentVersionId);
|
|
1497
|
+
if (current !== void 0 && current.schemaHash === hash) return {
|
|
1498
|
+
ok: true,
|
|
1499
|
+
formId: existing.id,
|
|
1500
|
+
versionId: current.id,
|
|
1501
|
+
version: current.version,
|
|
1502
|
+
schemaHash: hash,
|
|
1503
|
+
warnings
|
|
1504
|
+
};
|
|
1505
|
+
if (input.basedOnSchemaHash !== void 0 && input.basedOnSchemaHash !== current?.schemaHash) return {
|
|
1506
|
+
ok: false,
|
|
1507
|
+
kind: "version_changed",
|
|
1508
|
+
...current === void 0 ? {} : { current: {
|
|
1509
|
+
formId: existing.id,
|
|
1510
|
+
versionId: current.id,
|
|
1511
|
+
version: current.version,
|
|
1512
|
+
schema: current.schema,
|
|
1513
|
+
schemaHash: current.schemaHash
|
|
1514
|
+
} }
|
|
1515
|
+
};
|
|
1516
|
+
const already = (await deps.storage.listVersionsByForm(existing.id)).find((candidate) => candidate.schemaHash === hash);
|
|
1517
|
+
if (already !== void 0) return {
|
|
1518
|
+
ok: false,
|
|
1519
|
+
kind: "already_published",
|
|
1520
|
+
version: already.version
|
|
1521
|
+
};
|
|
1522
|
+
const version = await deps.storage.latestVersionNumber(existing.id) + 1;
|
|
1523
|
+
const versionId = deps.newId();
|
|
1524
|
+
await deps.storage.publishVersion({
|
|
1525
|
+
version: {
|
|
1526
|
+
id: versionId,
|
|
1527
|
+
formId: existing.id,
|
|
1528
|
+
version,
|
|
1529
|
+
schema,
|
|
1530
|
+
schemaHash: hash
|
|
1531
|
+
},
|
|
1532
|
+
audit: publishAudit(deps, input, {
|
|
1533
|
+
path: input.path,
|
|
1534
|
+
version,
|
|
1535
|
+
hash,
|
|
1536
|
+
sources: sourceNamesIn(schema)
|
|
1537
|
+
})
|
|
1538
|
+
});
|
|
1539
|
+
return {
|
|
1540
|
+
ok: true,
|
|
1541
|
+
formId: existing.id,
|
|
1542
|
+
versionId,
|
|
1543
|
+
version,
|
|
1544
|
+
schemaHash: hash,
|
|
1545
|
+
warnings
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
if (input.basedOnSchemaHash !== void 0) return {
|
|
1549
|
+
ok: false,
|
|
1550
|
+
kind: "version_changed"
|
|
1551
|
+
};
|
|
1552
|
+
const formId = deps.newId();
|
|
1553
|
+
const versionId = deps.newId();
|
|
1554
|
+
await deps.storage.publishVersion({
|
|
1555
|
+
form: {
|
|
1556
|
+
id: formId,
|
|
1557
|
+
path: input.path,
|
|
1558
|
+
currentVersionId: null,
|
|
1559
|
+
accessSubmit: "authenticated",
|
|
1560
|
+
allowedOrigins: null
|
|
1561
|
+
},
|
|
1562
|
+
version: {
|
|
1563
|
+
id: versionId,
|
|
1564
|
+
formId,
|
|
1565
|
+
version: 1,
|
|
1566
|
+
schema,
|
|
1567
|
+
schemaHash: hash
|
|
1568
|
+
},
|
|
1569
|
+
audit: publishAudit(deps, input, {
|
|
1570
|
+
path: input.path,
|
|
1571
|
+
version: 1,
|
|
1572
|
+
hash,
|
|
1573
|
+
sources: sourceNamesIn(schema)
|
|
1574
|
+
})
|
|
1575
|
+
});
|
|
1576
|
+
return {
|
|
1577
|
+
ok: true,
|
|
1578
|
+
formId,
|
|
1579
|
+
versionId,
|
|
1580
|
+
version: 1,
|
|
1581
|
+
schemaHash: hash,
|
|
1582
|
+
warnings
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* The audit row for a publish, built here so it can travel INSIDE the
|
|
1587
|
+
* transaction rather than being appended after it.
|
|
1588
|
+
*
|
|
1589
|
+
* Before this it was written by the route once all three storage calls had
|
|
1590
|
+
* returned, which meant a publish could half-apply and still be recorded as
|
|
1591
|
+
* having happened. Now it rolls back with everything else.
|
|
1592
|
+
*/
|
|
1593
|
+
function publishAudit(deps, input, about) {
|
|
1594
|
+
return {
|
|
1595
|
+
id: deps.newId(),
|
|
1596
|
+
at: deps.nowIso(),
|
|
1597
|
+
action: "form.published",
|
|
1598
|
+
subject: about.path,
|
|
1599
|
+
...input.actor === void 0 ? {} : {
|
|
1600
|
+
actorKind: input.actor.kind,
|
|
1601
|
+
actorId: input.actor.id
|
|
1602
|
+
},
|
|
1603
|
+
detail: {
|
|
1604
|
+
version: about.version,
|
|
1605
|
+
schemaHash: about.hash,
|
|
1606
|
+
...about.sources.length === 0 ? {} : { optionsSources: [...about.sources].sort().join(", ") }
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
export { AUDIT_ACTIONS, BREAKER_COOLDOWN_MS, BREAKER_THRESHOLD, CHALLENGE_TTL_SECONDS, DEFAULT_MAX_NUMBER, EVENT_ID_HEADER, MAX_ATTEMPTS, SIGNATURE_HEADER, afterAttempt, afterWebhookAttempt, auditedBy, authenticateApiKey, authenticateLocal, breakerState, can, checkMembership, collectAbandonedFiles, createApiKey, createLocalUser, createMemoryStorage, createSubmission, decodeSolution, deliveryHeaders, drainOutbox, encodeSolution, exportCsv, fileFieldPaths, filesToClaim, hashOf, healthOf, isIpLiteral, isPrivateAddress, listForms, listSubmissions, listVersions, mayAttempt, mintChallenge, offerUpload, publishForm, replayDelivery, resolveForm, resumeDraft, retryDelayMs, saveDraft, screenUpload, setFormAccess, signBody, signedPayload, solveChallenge, sourceNamesIn, sourcedAnswers, startDraft, unsafePatterns, verifySignature, verifySolution };
|
|
1547
1612
|
|
|
1548
1613
|
//# sourceMappingURL=index.mjs.map
|