@synapsor/runner 1.6.0 → 1.6.1
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/CHANGELOG.md +32 -4
- package/README.md +16 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +4 -1
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +1624 -42
- package/dist/runtime.mjs +1011 -25
- package/dist/shadow.mjs +408 -15
- package/docs/app-owned-executors.md +8 -0
- package/docs/capability-authoring.md +45 -0
- package/docs/current-scope.md +5 -0
- package/docs/cursor-plugin.md +2 -2
- package/docs/limitations.md +15 -2
- package/docs/local-mode.md +22 -3
- package/docs/production.md +19 -0
- package/docs/proposal-evidence-freshness.md +334 -0
- package/docs/release-notes.md +30 -4
- package/docs/runner-config-reference.md +46 -0
- package/docs/security-boundary.md +17 -0
- package/docs/store-lifecycle.md +8 -0
- package/docs/troubleshooting-first-run.md +76 -0
- package/docs/writeback-executors.md +8 -0
- package/examples/app-owned-writeback/command-handler.mjs +0 -0
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -0
- package/examples/reference-support-billing-app/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-evaluation.sh +0 -0
- package/fixtures/compatibility/published-1.6.0/manifest.json +76 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
- package/fixtures/protocol/MANIFEST.json +37 -7
- package/fixtures/protocol/change-set.freshness-update.v2.json +58 -0
- package/fixtures/protocol/freshness-authority.invoice.v1.json +35 -0
- package/fixtures/protocol/freshness-proof.fresh.v1.json +38 -0
- package/fixtures/protocol/writeback-job.freshness-update.v2.json +50 -0
- package/llms.txt +1 -0
- package/package.json +9 -8
- package/schemas/change-set.v1.schema.json +1 -0
- package/schemas/change-set.v2.schema.json +1 -0
- package/schemas/change-set.v3.schema.json +1 -0
- package/schemas/freshness-authority.v1.schema.json +89 -0
- package/schemas/freshness-proof.v1.schema.json +73 -0
- package/schemas/synapsor.runner.schema.json +32 -0
- package/schemas/writeback-job.v1.schema.json +1 -0
- package/schemas/writeback-job.v2.schema.json +1 -0
- package/schemas/writeback-job.v3.schema.json +1 -0
package/dist/shadow.mjs
CHANGED
|
@@ -64,6 +64,8 @@ var protocolVersions = {
|
|
|
64
64
|
runnerProposal: "synapsor.runner-proposal.v1",
|
|
65
65
|
runnerActivity: "synapsor.runner-activity.v1",
|
|
66
66
|
principalScope: "synapsor.principal-scope.v1",
|
|
67
|
+
freshnessAuthority: "synapsor.freshness-authority.v1",
|
|
68
|
+
freshnessProof: "synapsor.freshness-proof.v1",
|
|
67
69
|
legacyWritebackJob: "1.0",
|
|
68
70
|
normalizedWritebackJobV2: "2.0",
|
|
69
71
|
normalizedWritebackJobV3: "3.0",
|
|
@@ -111,6 +113,101 @@ var principalScopeGuardSchema = z.object({
|
|
|
111
113
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope fingerprint does not match its trusted value", path: ["value_fingerprint"] });
|
|
112
114
|
}
|
|
113
115
|
});
|
|
116
|
+
var freshnessDependencyUnsignedSchema = z.object({
|
|
117
|
+
id: safeIdentifier,
|
|
118
|
+
capability: z.string().regex(/^[A-Za-z_][A-Za-z0-9_.-]*\.[A-Za-z_][A-Za-z0-9_.-]*$/, "expected reviewed capability name"),
|
|
119
|
+
source_id: z.string().min(1).max(160),
|
|
120
|
+
engine: writebackEngineSchema,
|
|
121
|
+
target: z.object({
|
|
122
|
+
schema: safeIdentifier,
|
|
123
|
+
table: safeIdentifier,
|
|
124
|
+
primary_key: columnValueSchema,
|
|
125
|
+
tenant_column: safeIdentifier,
|
|
126
|
+
principal_column: safeIdentifier.optional()
|
|
127
|
+
}),
|
|
128
|
+
expected_version: columnValueSchema,
|
|
129
|
+
evidence: z.object({
|
|
130
|
+
bundle_id: z.string().min(1).max(200),
|
|
131
|
+
query_fingerprint: sha256
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
var freshnessDependencyV1Schema = freshnessDependencyUnsignedSchema.extend({
|
|
135
|
+
descriptor_digest: sha256
|
|
136
|
+
}).superRefine((dependency, ctx) => {
|
|
137
|
+
const { descriptor_digest: _digest, ...unsigned } = dependency;
|
|
138
|
+
if (canonicalJsonDigest(unsigned) !== dependency.descriptor_digest) {
|
|
139
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency digest mismatch", path: ["descriptor_digest"] });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
var freshnessAuthorityUnsignedSchema = z.object({
|
|
143
|
+
schema_version: z.literal(protocolVersions.freshnessAuthority),
|
|
144
|
+
required: z.literal(true),
|
|
145
|
+
target: z.object({
|
|
146
|
+
mode: z.enum(["exact_guard", "frozen_set", "not_applicable"]),
|
|
147
|
+
member_count: z.number().int().min(0).max(100)
|
|
148
|
+
}),
|
|
149
|
+
dependencies: z.array(freshnessDependencyV1Schema).max(16)
|
|
150
|
+
});
|
|
151
|
+
var freshnessAuthorityV1Schema = freshnessAuthorityUnsignedSchema.extend({
|
|
152
|
+
dependency_set_digest: sha256
|
|
153
|
+
}).superRefine((authority, ctx) => {
|
|
154
|
+
const identities = authority.dependencies.map((dependency) => dependency.id);
|
|
155
|
+
if (new Set(identities).size !== identities.length) {
|
|
156
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency ids must be unique", path: ["dependencies"] });
|
|
157
|
+
}
|
|
158
|
+
const sorted = [...authority.dependencies].sort((left, right) => left.source_id.localeCompare(right.source_id) || left.target.schema.localeCompare(right.target.schema) || left.target.table.localeCompare(right.target.table) || JSON.stringify(left.target.primary_key.value).localeCompare(JSON.stringify(right.target.primary_key.value)) || left.id.localeCompare(right.id));
|
|
159
|
+
if (JSON.stringify(sorted) !== JSON.stringify(authority.dependencies)) {
|
|
160
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependencies must use deterministic lock order", path: ["dependencies"] });
|
|
161
|
+
}
|
|
162
|
+
const { dependency_set_digest: _digest, ...unsigned } = authority;
|
|
163
|
+
if (canonicalJsonDigest(unsigned) !== authority.dependency_set_digest) {
|
|
164
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency-set digest mismatch", path: ["dependency_set_digest"] });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
var freshnessCheckStatusSchema = z.enum(["fresh", "stale", "unavailable", "invalid", "unsupported"]);
|
|
168
|
+
var freshnessItemResultSchema = z.object({
|
|
169
|
+
id: z.string().min(1).max(160),
|
|
170
|
+
kind: z.enum(["target", "supporting"]),
|
|
171
|
+
status: z.enum(["fresh", "stale", "not_applicable", "unavailable", "invalid", "unsupported"]),
|
|
172
|
+
safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
173
|
+
expected_version_digest: sha256.optional(),
|
|
174
|
+
observed_version_digest: sha256.optional()
|
|
175
|
+
});
|
|
176
|
+
var freshnessProofUnsignedSchema = z.object({
|
|
177
|
+
schema_version: z.literal(protocolVersions.freshnessProof),
|
|
178
|
+
proposal_id: z.string().min(1),
|
|
179
|
+
proposal_hash: sha256,
|
|
180
|
+
proposal_version: z.number().int().positive(),
|
|
181
|
+
dependency_set_digest: sha256,
|
|
182
|
+
checked_at: z.string().datetime(),
|
|
183
|
+
valid_until: z.string().datetime(),
|
|
184
|
+
source_adapters: z.array(z.object({
|
|
185
|
+
source_id: z.string().min(1).max(160),
|
|
186
|
+
engine: writebackEngineSchema
|
|
187
|
+
})).min(1).max(8),
|
|
188
|
+
result: freshnessCheckStatusSchema,
|
|
189
|
+
safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
190
|
+
target_count: z.number().int().min(0).max(100),
|
|
191
|
+
supporting_count: z.number().int().min(0).max(16),
|
|
192
|
+
checks: z.array(freshnessItemResultSchema).max(116)
|
|
193
|
+
});
|
|
194
|
+
var freshnessProofV1Schema = freshnessProofUnsignedSchema.extend({
|
|
195
|
+
proof_digest: sha256
|
|
196
|
+
}).superRefine((proof, ctx) => {
|
|
197
|
+
if (Date.parse(proof.valid_until) < Date.parse(proof.checked_at)) {
|
|
198
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof validity ends before it was checked", path: ["valid_until"] });
|
|
199
|
+
}
|
|
200
|
+
if (proof.checks.filter((check) => check.kind === "target").length !== proof.target_count) {
|
|
201
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness target count mismatch", path: ["target_count"] });
|
|
202
|
+
}
|
|
203
|
+
if (proof.checks.filter((check) => check.kind === "supporting").length !== proof.supporting_count) {
|
|
204
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness supporting count mismatch", path: ["supporting_count"] });
|
|
205
|
+
}
|
|
206
|
+
const { proof_digest: _digest, ...unsigned } = proof;
|
|
207
|
+
if (canonicalJsonDigest(unsigned) !== proof.proof_digest) {
|
|
208
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof digest mismatch", path: ["proof_digest"] });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
114
211
|
var resolvedDeduplicationComponentSchema = z.object({
|
|
115
212
|
column: safeIdentifier,
|
|
116
213
|
value: scalar,
|
|
@@ -240,6 +337,7 @@ var changeSetV1Schema = z.object({
|
|
|
240
337
|
allowed_columns: z.array(z.string().min(1)).min(1),
|
|
241
338
|
expected_version: columnValueSchema
|
|
242
339
|
}),
|
|
340
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
243
341
|
evidence: z.object({
|
|
244
342
|
bundle_id: z.string().min(1),
|
|
245
343
|
query_fingerprint: sha256,
|
|
@@ -260,6 +358,7 @@ var changeSetV1Schema = z.object({
|
|
|
260
358
|
}),
|
|
261
359
|
created_at: z.string().min(1)
|
|
262
360
|
}).superRefine((changeSet, ctx) => {
|
|
361
|
+
validateFreshnessBinding(changeSet.freshness, changeSet.source.source_id, sourceEngine(changeSet.source.kind), "exact_guard", 1, ctx);
|
|
263
362
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
264
363
|
for (const column of Object.keys(changeSet.patch)) {
|
|
265
364
|
if (!allowed.has(column)) {
|
|
@@ -327,6 +426,7 @@ var changeSetV2Schema = z.object({
|
|
|
327
426
|
version_advance: versionAdvanceSchema.optional(),
|
|
328
427
|
deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional()
|
|
329
428
|
}),
|
|
429
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
330
430
|
reversibility: reversibilityRequestSchema.optional(),
|
|
331
431
|
evidence: z.object({
|
|
332
432
|
bundle_id: z.string().min(1),
|
|
@@ -346,6 +446,14 @@ var changeSetV2Schema = z.object({
|
|
|
346
446
|
integrity: z.object({ proposal_hash: sha256 }),
|
|
347
447
|
created_at: z.string().min(1)
|
|
348
448
|
}).superRefine((changeSet, ctx) => {
|
|
449
|
+
validateFreshnessBinding(
|
|
450
|
+
changeSet.freshness,
|
|
451
|
+
changeSet.source.source_id,
|
|
452
|
+
sourceEngine(changeSet.source.kind),
|
|
453
|
+
changeSet.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
454
|
+
changeSet.operation === "single_row_insert" ? 0 : 1,
|
|
455
|
+
ctx
|
|
456
|
+
);
|
|
349
457
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
350
458
|
for (const column of Object.keys(changeSet.patch)) {
|
|
351
459
|
if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
|
|
@@ -412,6 +520,7 @@ var changeSetV3Schema = z.object({
|
|
|
412
520
|
expected_version: columnValueSchema.optional(),
|
|
413
521
|
version_advance: versionAdvanceSchema.optional()
|
|
414
522
|
}),
|
|
523
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
415
524
|
frozen_set: frozenSetSchema,
|
|
416
525
|
reversibility: reversibilityRequestSchema.optional(),
|
|
417
526
|
evidence: z.object({ bundle_id: z.string().min(1), query_fingerprint: sha256, items: z.array(z.unknown()).max(100) }).passthrough(),
|
|
@@ -426,6 +535,14 @@ var changeSetV3Schema = z.object({
|
|
|
426
535
|
integrity: z.object({ proposal_hash: sha256 }),
|
|
427
536
|
created_at: z.string().min(1)
|
|
428
537
|
}).superRefine((changeSet, ctx) => {
|
|
538
|
+
validateFreshnessBinding(
|
|
539
|
+
changeSet.freshness,
|
|
540
|
+
changeSet.source.source_id,
|
|
541
|
+
sourceEngine(changeSet.source.kind),
|
|
542
|
+
changeSet.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
543
|
+
changeSet.operation === "batch_insert" ? 0 : changeSet.frozen_set.row_count,
|
|
544
|
+
ctx
|
|
545
|
+
);
|
|
429
546
|
if (changeSet.source.primary_key.value !== void 0 || changeSet.guards.expected_version) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set guards live on frozen members, not the top-level envelope", path: ["frozen_set"] });
|
|
430
547
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
431
548
|
for (const column of Object.keys(changeSet.patch)) if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
|
|
@@ -521,6 +638,7 @@ var writebackJobV1Schema = z.object({
|
|
|
521
638
|
allowed_columns: z.array(safeIdentifier).min(1),
|
|
522
639
|
patch: scalarMap,
|
|
523
640
|
conflict_guard: publicConflictGuardSchema,
|
|
641
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
524
642
|
idempotency_key: z.string().min(1),
|
|
525
643
|
lease: z.object({
|
|
526
644
|
lease_id: z.string().min(1),
|
|
@@ -528,6 +646,7 @@ var writebackJobV1Schema = z.object({
|
|
|
528
646
|
expires_at: z.string().min(1)
|
|
529
647
|
})
|
|
530
648
|
}).superRefine((job, ctx) => {
|
|
649
|
+
validateFreshnessBinding(job.freshness, job.runner_scope.source_id, job.engine, "exact_guard", 1, ctx);
|
|
531
650
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.tenant_guard.column, ctx);
|
|
532
651
|
if (job.principal_scope && job.allowed_columns.includes(job.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
|
|
533
652
|
});
|
|
@@ -549,6 +668,7 @@ var normalizedWritebackJobV1Schema = writebackJobV1Schema.transform((job) => ({
|
|
|
549
668
|
allowed_columns: job.allowed_columns,
|
|
550
669
|
patch: job.patch,
|
|
551
670
|
conflict_guard: normalizeConflictGuard(job.conflict_guard),
|
|
671
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
552
672
|
idempotency_key: job.idempotency_key,
|
|
553
673
|
lease_expires_at: job.lease.expires_at,
|
|
554
674
|
attempt_count: job.lease.attempt
|
|
@@ -582,10 +702,12 @@ var legacyWritebackJobSchema = z.object({
|
|
|
582
702
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
583
703
|
z.object({ kind: z.literal("none") })
|
|
584
704
|
]),
|
|
705
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
585
706
|
idempotency_key: z.string().min(1),
|
|
586
707
|
lease_expires_at: z.union([z.string(), z.number()]),
|
|
587
708
|
attempt_count: z.number().int().nonnegative().optional()
|
|
588
709
|
}).superRefine((job, ctx) => {
|
|
710
|
+
validateFreshnessBinding(job.freshness, job.source_id, job.engine, "exact_guard", 1, ctx);
|
|
589
711
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.target.tenant_guard.column, ctx);
|
|
590
712
|
if (job.target.principal_scope && job.allowed_columns.includes(job.target.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
|
|
591
713
|
});
|
|
@@ -612,6 +734,7 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
612
734
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
613
735
|
z.object({ kind: z.literal("none") })
|
|
614
736
|
]),
|
|
737
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
615
738
|
version_advance: versionAdvanceSchema.optional(),
|
|
616
739
|
deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional(),
|
|
617
740
|
idempotency_key: z.string().min(1),
|
|
@@ -619,6 +742,14 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
619
742
|
attempt_count: z.number().int().positive(),
|
|
620
743
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
621
744
|
}).superRefine((job, ctx) => {
|
|
745
|
+
validateFreshnessBinding(
|
|
746
|
+
job.freshness,
|
|
747
|
+
job.source_id,
|
|
748
|
+
job.engine,
|
|
749
|
+
job.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
750
|
+
job.operation === "single_row_insert" ? 0 : 1,
|
|
751
|
+
ctx
|
|
752
|
+
);
|
|
622
753
|
if (job.operation !== "single_row_insert" && job.target.primary_key.value === void 0) {
|
|
623
754
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
|
|
624
755
|
}
|
|
@@ -684,6 +815,7 @@ var writebackJobV2Schema = z.object({
|
|
|
684
815
|
principal_scope: principalScopeGuardSchema.optional(),
|
|
685
816
|
allowed_columns: z.array(safeIdentifier),
|
|
686
817
|
mutation: writebackMutationV2Schema,
|
|
818
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
687
819
|
idempotency_key: z.string().min(1),
|
|
688
820
|
inverse_capture: inverseDescriptorV1Schema.optional(),
|
|
689
821
|
lease: z.object({
|
|
@@ -692,6 +824,14 @@ var writebackJobV2Schema = z.object({
|
|
|
692
824
|
expires_at: z.string().min(1)
|
|
693
825
|
})
|
|
694
826
|
}).superRefine((job, ctx) => {
|
|
827
|
+
validateFreshnessBinding(
|
|
828
|
+
job.freshness,
|
|
829
|
+
job.runner_scope.source_id,
|
|
830
|
+
job.engine,
|
|
831
|
+
job.mutation.kind === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
832
|
+
job.mutation.kind === "single_row_insert" ? 0 : 1,
|
|
833
|
+
ctx
|
|
834
|
+
);
|
|
695
835
|
const mutation = job.mutation;
|
|
696
836
|
if (mutation.kind !== "single_row_insert" && job.target.primary_key.value === void 0) {
|
|
697
837
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
|
|
@@ -730,6 +870,7 @@ var writebackJobV2Schema = z.object({
|
|
|
730
870
|
conflict_guard: job.mutation.kind === "single_row_insert" ? { kind: "none" } : normalizeConflictGuard(job.mutation.conflict_guard),
|
|
731
871
|
...job.mutation.kind === "single_row_update" && job.mutation.version_advance ? { version_advance: job.mutation.version_advance } : {},
|
|
732
872
|
...job.mutation.kind === "single_row_insert" ? { deduplication: job.mutation.deduplication } : {},
|
|
873
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
733
874
|
idempotency_key: job.idempotency_key,
|
|
734
875
|
...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
|
|
735
876
|
lease_expires_at: job.lease.expires_at,
|
|
@@ -748,6 +889,7 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
748
889
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
749
890
|
patch: boundedScalarRecord,
|
|
750
891
|
conflict_guard: z.object({ kind: z.literal("none") }).default({ kind: "none" }),
|
|
892
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
751
893
|
version_advance: versionAdvanceSchema.optional(),
|
|
752
894
|
frozen_set: frozenSetSchema,
|
|
753
895
|
idempotency_key: z.string().min(1),
|
|
@@ -755,6 +897,14 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
755
897
|
attempt_count: z.number().int().positive(),
|
|
756
898
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
757
899
|
}).superRefine((job, ctx) => {
|
|
900
|
+
validateFreshnessBinding(
|
|
901
|
+
job.freshness,
|
|
902
|
+
job.source_id,
|
|
903
|
+
job.engine,
|
|
904
|
+
job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
905
|
+
job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
|
|
906
|
+
ctx
|
|
907
|
+
);
|
|
758
908
|
if (job.target.principal_scope && job.allowed_columns.includes(job.target.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
|
|
759
909
|
if (job.operation === "set_delete" && (job.allowed_columns.length || Object.keys(job.patch).length || job.version_advance)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set DELETE cannot carry patch authority", path: ["patch"] });
|
|
760
910
|
if (job.operation === "set_update" && (!Object.keys(job.patch).length || !job.version_advance)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set UPDATE requires patch and version advance", path: ["patch"] });
|
|
@@ -776,9 +926,19 @@ var writebackJobV3Schema = z.object({
|
|
|
776
926
|
patch: boundedScalarRecord,
|
|
777
927
|
version_advance: versionAdvanceSchema.optional(),
|
|
778
928
|
frozen_set: frozenSetSchema,
|
|
929
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
779
930
|
idempotency_key: z.string().min(1),
|
|
780
931
|
inverse_capture: inverseDescriptorV1Schema.optional(),
|
|
781
932
|
lease: z.object({ lease_id: z.string().min(1), attempt: z.number().int().positive(), expires_at: z.string().min(1) })
|
|
933
|
+
}).superRefine((job, ctx) => {
|
|
934
|
+
validateFreshnessBinding(
|
|
935
|
+
job.freshness,
|
|
936
|
+
job.runner_scope.source_id,
|
|
937
|
+
job.engine,
|
|
938
|
+
job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
939
|
+
job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
|
|
940
|
+
ctx
|
|
941
|
+
);
|
|
782
942
|
}).transform((job) => ({
|
|
783
943
|
protocol_version: protocolVersions.normalizedWritebackJobV3,
|
|
784
944
|
job_id: job.writeback_job_id,
|
|
@@ -793,6 +953,7 @@ var writebackJobV3Schema = z.object({
|
|
|
793
953
|
conflict_guard: { kind: "none" },
|
|
794
954
|
...job.version_advance ? { version_advance: job.version_advance } : {},
|
|
795
955
|
frozen_set: job.frozen_set,
|
|
956
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
796
957
|
idempotency_key: job.idempotency_key,
|
|
797
958
|
...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
|
|
798
959
|
lease_expires_at: job.lease.expires_at,
|
|
@@ -1146,6 +1307,38 @@ var runnerActivityV1Schema = z.object({
|
|
|
1146
1307
|
detail: z.record(z.unknown()).optional(),
|
|
1147
1308
|
occurred_at: z.string().min(1).max(80).optional()
|
|
1148
1309
|
}).strict();
|
|
1310
|
+
function sourceEngine(kind) {
|
|
1311
|
+
if (kind === "external_postgres") return "postgres";
|
|
1312
|
+
if (kind === "external_mysql") return "mysql";
|
|
1313
|
+
return void 0;
|
|
1314
|
+
}
|
|
1315
|
+
function validateFreshnessBinding(freshness, sourceId, engine, expectedTargetMode, expectedMemberCount, ctx) {
|
|
1316
|
+
if (!freshness) return;
|
|
1317
|
+
if (!engine) {
|
|
1318
|
+
ctx.addIssue({
|
|
1319
|
+
code: z.ZodIssueCode.custom,
|
|
1320
|
+
message: "freshness authority requires an external PostgreSQL or MySQL source",
|
|
1321
|
+
path: ["freshness"]
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (freshness.target.mode !== expectedTargetMode || freshness.target.member_count !== expectedMemberCount) {
|
|
1326
|
+
ctx.addIssue({
|
|
1327
|
+
code: z.ZodIssueCode.custom,
|
|
1328
|
+
message: `freshness target authority must be ${expectedTargetMode} with ${expectedMemberCount} reviewed member(s)`,
|
|
1329
|
+
path: ["freshness", "target"]
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
for (const [index, dependency] of freshness.dependencies.entries()) {
|
|
1333
|
+
if (dependency.source_id !== sourceId || dependency.engine !== engine) {
|
|
1334
|
+
ctx.addIssue({
|
|
1335
|
+
code: z.ZodIssueCode.custom,
|
|
1336
|
+
message: "freshness dependencies must use the proposal/writeback source and engine",
|
|
1337
|
+
path: ["freshness", "dependencies", index, "source_id"]
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1149
1342
|
function validateAllowedPatchColumns(allowedColumns, patchColumns, primaryKeyColumn, tenantGuardColumn, ctx) {
|
|
1150
1343
|
const allow = new Set(allowedColumns);
|
|
1151
1344
|
for (const column of patchColumns) {
|
|
@@ -1206,6 +1399,12 @@ function normalizeConflictGuard(guard) {
|
|
|
1206
1399
|
function parseChangeSet(input) {
|
|
1207
1400
|
return z.union([changeSetV1Schema, changeSetV2Schema, changeSetV3Schema, compensationChangeSetV1Schema]).parse(input);
|
|
1208
1401
|
}
|
|
1402
|
+
function parseFreshnessAuthority(input) {
|
|
1403
|
+
return freshnessAuthorityV1Schema.parse(input);
|
|
1404
|
+
}
|
|
1405
|
+
function parseFreshnessProof(input) {
|
|
1406
|
+
return freshnessProofV1Schema.parse(input);
|
|
1407
|
+
}
|
|
1209
1408
|
function parseWritebackJob(input) {
|
|
1210
1409
|
return writebackJobSchema.parse(input);
|
|
1211
1410
|
}
|
|
@@ -1395,6 +1594,7 @@ var ProposalStore = class {
|
|
|
1395
1594
|
decision_hash TEXT,
|
|
1396
1595
|
signature TEXT,
|
|
1397
1596
|
integrity_hash TEXT,
|
|
1597
|
+
freshness_proof_digest TEXT,
|
|
1398
1598
|
created_at TEXT NOT NULL,
|
|
1399
1599
|
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
1400
1600
|
);
|
|
@@ -1668,6 +1868,7 @@ var ProposalStore = class {
|
|
|
1668
1868
|
this.ensureColumn("approvals", "decision_hash", "TEXT");
|
|
1669
1869
|
this.ensureColumn("approvals", "signature", "TEXT");
|
|
1670
1870
|
this.ensureColumn("approvals", "integrity_hash", "TEXT");
|
|
1871
|
+
this.ensureColumn("approvals", "freshness_proof_digest", "TEXT");
|
|
1671
1872
|
}
|
|
1672
1873
|
ensureSearchIndexes() {
|
|
1673
1874
|
this.db.exec(`
|
|
@@ -1958,6 +2159,75 @@ var ProposalStore = class {
|
|
|
1958
2159
|
const row = this.db.prepare("SELECT proposal_id FROM query_audit WHERE evidence_bundle_id = ? AND proposal_id IS NOT NULL ORDER BY created_at DESC LIMIT 1").get(evidenceBundleId);
|
|
1959
2160
|
return isRecord(row) && row.proposal_id != null ? String(row.proposal_id) : void 0;
|
|
1960
2161
|
}
|
|
2162
|
+
recordFreshnessProof(input) {
|
|
2163
|
+
const proof = parseFreshnessProof(input);
|
|
2164
|
+
const proposal = this.requireProposal(proof.proposal_id);
|
|
2165
|
+
assertProposalIdentity(proposal, proof.proposal_hash, proof.proposal_version);
|
|
2166
|
+
const authority = proposalFreshnessAuthority(proposal);
|
|
2167
|
+
if (!authority) {
|
|
2168
|
+
throw new ProposalStoreError(
|
|
2169
|
+
"FRESHNESS_NOT_REQUIRED",
|
|
2170
|
+
`proposal ${proof.proposal_id} has no reviewed freshness authority`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
if (proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
2174
|
+
throw new ProposalStoreError(
|
|
2175
|
+
"FRESHNESS_PROOF_AUTHORITY_MISMATCH",
|
|
2176
|
+
`freshness proof does not match proposal ${proof.proposal_id}`
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2180
|
+
this.transaction(() => {
|
|
2181
|
+
this.appendEvent(proof.proposal_id, "proposal_freshness_checked", "runner", { proof });
|
|
2182
|
+
if (proof.result === "stale") {
|
|
2183
|
+
const current = this.requireProposal(proof.proposal_id);
|
|
2184
|
+
if (current.state === "pending_review" || current.state === "approved" || current.state === "pending_worker") {
|
|
2185
|
+
this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("conflict", now, proof.proposal_id);
|
|
2186
|
+
this.appendEvent(proof.proposal_id, "proposal_conflict", "runner", {
|
|
2187
|
+
reason: "freshness_stale",
|
|
2188
|
+
safe_code: proof.safe_code,
|
|
2189
|
+
proof_digest: proof.proof_digest
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
return proof;
|
|
2195
|
+
}
|
|
2196
|
+
latestFreshnessProof(proposalId) {
|
|
2197
|
+
this.requireProposal(proposalId);
|
|
2198
|
+
const row = this.db.prepare(`
|
|
2199
|
+
SELECT payload_json
|
|
2200
|
+
FROM proposal_events
|
|
2201
|
+
WHERE proposal_id = ? AND kind = 'proposal_freshness_checked'
|
|
2202
|
+
ORDER BY event_id DESC
|
|
2203
|
+
LIMIT 1
|
|
2204
|
+
`).get(proposalId);
|
|
2205
|
+
if (!isRecord(row)) return void 0;
|
|
2206
|
+
try {
|
|
2207
|
+
const payload = JSON.parse(String(row.payload_json));
|
|
2208
|
+
return parseFreshnessProof(payload.proof);
|
|
2209
|
+
} catch {
|
|
2210
|
+
throw new ProposalStoreError(
|
|
2211
|
+
"FRESHNESS_PROOF_TAMPERED",
|
|
2212
|
+
`stored freshness proof for proposal ${proposalId} failed integrity validation`
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
recordFreshnessApprovalBlocked(proposalId, input) {
|
|
2217
|
+
this.requireProposal(proposalId);
|
|
2218
|
+
const proof = this.latestFreshnessProof(proposalId);
|
|
2219
|
+
if (!proof || proof.proof_digest !== input.proof_digest || proof.result === "fresh") {
|
|
2220
|
+
throw new ProposalStoreError(
|
|
2221
|
+
"FRESHNESS_BLOCK_RECORD_INVALID",
|
|
2222
|
+
`freshness block for proposal ${proposalId} does not match its latest non-fresh proof`
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
this.appendEvent(proposalId, "proposal_approval_blocked_freshness", input.actor, {
|
|
2226
|
+
proof_digest: proof.proof_digest,
|
|
2227
|
+
safe_code: input.safe_code,
|
|
2228
|
+
result: proof.result
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
1961
2231
|
approveProposal(proposalId, options) {
|
|
1962
2232
|
const proposal = this.requireProposal(proposalId);
|
|
1963
2233
|
assertWritebackAllowed(proposal, "approved");
|
|
@@ -1978,11 +2248,12 @@ var ProposalStore = class {
|
|
|
1978
2248
|
if (isRecord(existing)) {
|
|
1979
2249
|
throw new ProposalStoreError("APPROVER_ALREADY_COUNTED", `operator ${options.approver} already recorded a decision for proposal ${proposalId}`);
|
|
1980
2250
|
}
|
|
2251
|
+
this.assertApprovalFreshness(current, options.freshness_proof_digest, now);
|
|
1981
2252
|
this.db.prepare(`
|
|
1982
2253
|
INSERT INTO approvals (
|
|
1983
2254
|
proposal_id, proposal_version, proposal_hash, approver, status, reason,
|
|
1984
|
-
identity_json, decision_hash, signature, integrity_hash, created_at
|
|
1985
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2255
|
+
identity_json, decision_hash, signature, integrity_hash, freshness_proof_digest, created_at
|
|
2256
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1986
2257
|
`).run(
|
|
1987
2258
|
proposalId,
|
|
1988
2259
|
options.proposal_version,
|
|
@@ -1994,6 +2265,7 @@ var ProposalStore = class {
|
|
|
1994
2265
|
options.identity?.decision_hash ?? null,
|
|
1995
2266
|
options.identity?.signature ?? null,
|
|
1996
2267
|
options.identity?.integrity_hash ?? null,
|
|
2268
|
+
options.freshness_proof_digest ?? null,
|
|
1997
2269
|
now
|
|
1998
2270
|
);
|
|
1999
2271
|
const progress = this.approvalProgress(proposalId);
|
|
@@ -2006,6 +2278,7 @@ var ProposalStore = class {
|
|
|
2006
2278
|
proposal_version: options.proposal_version,
|
|
2007
2279
|
reason: options.reason ?? null,
|
|
2008
2280
|
identity: publicIdentitySummary(options.identity),
|
|
2281
|
+
freshness_proof_digest: options.freshness_proof_digest ?? null,
|
|
2009
2282
|
approvals: progress.approved,
|
|
2010
2283
|
required_approvals: progress.required,
|
|
2011
2284
|
remaining_approvals: progress.remaining
|
|
@@ -2114,17 +2387,31 @@ var ProposalStore = class {
|
|
|
2114
2387
|
});
|
|
2115
2388
|
return;
|
|
2116
2389
|
}
|
|
2390
|
+
this.assertApprovalFreshness(proposal, options.freshness_proof_digest, now);
|
|
2117
2391
|
this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("approved", now, proposalId);
|
|
2118
2392
|
this.db.prepare(`
|
|
2119
|
-
INSERT INTO approvals (
|
|
2120
|
-
|
|
2121
|
-
|
|
2393
|
+
INSERT INTO approvals (
|
|
2394
|
+
proposal_id, proposal_version, proposal_hash, approver, status, reason,
|
|
2395
|
+
freshness_proof_digest, created_at
|
|
2396
|
+
)
|
|
2397
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2398
|
+
`).run(
|
|
2399
|
+
proposalId,
|
|
2400
|
+
options.proposal_version,
|
|
2401
|
+
options.proposal_hash,
|
|
2402
|
+
actor,
|
|
2403
|
+
"approved",
|
|
2404
|
+
options.reason,
|
|
2405
|
+
options.freshness_proof_digest ?? null,
|
|
2406
|
+
now
|
|
2407
|
+
);
|
|
2122
2408
|
this.appendEvent(proposalId, "proposal_approved", actor, {
|
|
2123
2409
|
proposal_hash: options.proposal_hash,
|
|
2124
2410
|
proposal_version: options.proposal_version,
|
|
2125
2411
|
reason: options.reason,
|
|
2126
2412
|
policy: options.policy,
|
|
2127
|
-
aggregate_limits: options.limits ?? []
|
|
2413
|
+
aggregate_limits: options.limits ?? [],
|
|
2414
|
+
freshness_proof_digest: options.freshness_proof_digest ?? null
|
|
2128
2415
|
});
|
|
2129
2416
|
});
|
|
2130
2417
|
return {
|
|
@@ -2472,7 +2759,8 @@ var ProposalStore = class {
|
|
|
2472
2759
|
writeback_job_id: receipt.writeback_job_id,
|
|
2473
2760
|
rows_affected: receipt.rows_affected,
|
|
2474
2761
|
source_database_mutated: receipt.source_database_mutated,
|
|
2475
|
-
receipt_hash: receipt.receipt_hash
|
|
2762
|
+
receipt_hash: receipt.receipt_hash,
|
|
2763
|
+
safe_error_code: "safe_error_code" in receipt ? receipt.safe_error_code ?? null : null
|
|
2476
2764
|
});
|
|
2477
2765
|
}
|
|
2478
2766
|
recordHandlerWritebackJob(input) {
|
|
@@ -2556,6 +2844,7 @@ var ProposalStore = class {
|
|
|
2556
2844
|
tenant_guard: changeSet.guards.tenant,
|
|
2557
2845
|
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
2558
2846
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
2847
|
+
..."freshness" in changeSet && changeSet.freshness ? { freshness: parseFreshnessAuthority(changeSet.freshness) } : {},
|
|
2559
2848
|
idempotency_key: `${proposal.proposal_id}:${proposal.object_id}`,
|
|
2560
2849
|
lease
|
|
2561
2850
|
};
|
|
@@ -3009,7 +3298,21 @@ var ProposalStore = class {
|
|
|
3009
3298
|
const key = `${tenantId}\0${capability}`;
|
|
3010
3299
|
let row = rows.get(key);
|
|
3011
3300
|
if (!row) {
|
|
3012
|
-
row = {
|
|
3301
|
+
row = {
|
|
3302
|
+
tenant_id: tenantId,
|
|
3303
|
+
capability,
|
|
3304
|
+
worker_retries: 0,
|
|
3305
|
+
dead_letters: 0,
|
|
3306
|
+
auto_approval_limit_trips: 0,
|
|
3307
|
+
freshness_checks: 0,
|
|
3308
|
+
freshness_fresh: 0,
|
|
3309
|
+
freshness_stale_target: 0,
|
|
3310
|
+
freshness_stale_supporting: 0,
|
|
3311
|
+
freshness_unavailable: 0,
|
|
3312
|
+
freshness_unsupported: 0,
|
|
3313
|
+
freshness_approval_blocked: 0,
|
|
3314
|
+
freshness_apply_blocked: 0
|
|
3315
|
+
};
|
|
3013
3316
|
rows.set(key, row);
|
|
3014
3317
|
}
|
|
3015
3318
|
return row;
|
|
@@ -3017,7 +3320,14 @@ var ProposalStore = class {
|
|
|
3017
3320
|
const events = this.db.prepare(`
|
|
3018
3321
|
SELECT p.tenant_id, p.action, e.kind, e.payload_json
|
|
3019
3322
|
FROM proposal_events e JOIN proposals p ON p.proposal_id = e.proposal_id
|
|
3020
|
-
WHERE e.kind IN (
|
|
3323
|
+
WHERE e.kind IN (
|
|
3324
|
+
'writeback_retry_scheduled',
|
|
3325
|
+
'writeback_dead_lettered',
|
|
3326
|
+
'policy_auto_approval_deferred',
|
|
3327
|
+
'proposal_freshness_checked',
|
|
3328
|
+
'proposal_approval_blocked_freshness',
|
|
3329
|
+
'writeback_conflict'
|
|
3330
|
+
)
|
|
3021
3331
|
AND (? IS NULL OR p.tenant_id = ?)
|
|
3022
3332
|
AND (? IS NULL OR p.action = ?)
|
|
3023
3333
|
`).all(filters.tenant ?? null, filters.tenant ?? null, filters.capability ?? null, filters.capability ?? null);
|
|
@@ -3026,13 +3336,29 @@ var ProposalStore = class {
|
|
|
3026
3336
|
const row = ensure(String(raw.tenant_id), String(raw.action));
|
|
3027
3337
|
if (raw.kind === "writeback_retry_scheduled") row.worker_retries += 1;
|
|
3028
3338
|
if (raw.kind === "writeback_dead_lettered") row.dead_letters += 1;
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3339
|
+
let payload = {};
|
|
3340
|
+
try {
|
|
3341
|
+
const parsed = JSON.parse(String(raw.payload_json));
|
|
3342
|
+
if (isRecord(parsed)) payload = parsed;
|
|
3343
|
+
} catch {
|
|
3344
|
+
}
|
|
3345
|
+
if (raw.kind === "proposal_freshness_checked") {
|
|
3346
|
+
row.freshness_checks += 1;
|
|
3347
|
+
const proof = isRecord(payload.proof) ? payload.proof : {};
|
|
3348
|
+
if (proof.result === "fresh") row.freshness_fresh += 1;
|
|
3349
|
+
if (proof.result === "unavailable") row.freshness_unavailable += 1;
|
|
3350
|
+
if (proof.result === "unsupported") row.freshness_unsupported += 1;
|
|
3351
|
+
if (proof.result === "stale") {
|
|
3352
|
+
const checks = Array.isArray(proof.checks) ? proof.checks.filter(isRecord) : [];
|
|
3353
|
+
if (checks.some((check) => check.kind === "target" && check.status === "stale")) row.freshness_stale_target += 1;
|
|
3354
|
+
if (checks.some((check) => check.kind === "supporting" && check.status === "stale")) row.freshness_stale_supporting += 1;
|
|
3034
3355
|
}
|
|
3035
3356
|
}
|
|
3357
|
+
if (raw.kind === "proposal_approval_blocked_freshness") row.freshness_approval_blocked += 1;
|
|
3358
|
+
if (raw.kind === "writeback_conflict" && /^FRESHNESS_/.test(String(payload.safe_error_code ?? ""))) row.freshness_apply_blocked += 1;
|
|
3359
|
+
if (raw.kind === "policy_auto_approval_deferred") {
|
|
3360
|
+
if (Array.isArray(payload.tripped_limits) && payload.tripped_limits.length > 0) row.auto_approval_limit_trips += 1;
|
|
3361
|
+
}
|
|
3036
3362
|
}
|
|
3037
3363
|
return [...rows.values()].sort((left, right) => left.tenant_id.localeCompare(right.tenant_id) || left.capability.localeCompare(right.capability));
|
|
3038
3364
|
}
|
|
@@ -3880,6 +4206,61 @@ var ProposalStore = class {
|
|
|
3880
4206
|
}
|
|
3881
4207
|
return proposal;
|
|
3882
4208
|
}
|
|
4209
|
+
assertApprovalFreshness(proposal, proofDigest, now) {
|
|
4210
|
+
const authority = proposalFreshnessAuthority(proposal);
|
|
4211
|
+
if (!authority) {
|
|
4212
|
+
if (proofDigest !== void 0) {
|
|
4213
|
+
throw new ProposalStoreError(
|
|
4214
|
+
"FRESHNESS_PROOF_UNEXPECTED",
|
|
4215
|
+
`proposal ${proposal.proposal_id} does not require a freshness proof`
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (!proofDigest) {
|
|
4221
|
+
throw new ProposalStoreError(
|
|
4222
|
+
"FRESHNESS_PROOF_REQUIRED",
|
|
4223
|
+
`proposal ${proposal.proposal_id} requires a fresh live proof before approval`
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
const proof = this.latestFreshnessProof(proposal.proposal_id);
|
|
4227
|
+
if (!proof || proof.proof_digest !== proofDigest) {
|
|
4228
|
+
throw new ProposalStoreError(
|
|
4229
|
+
"FRESHNESS_PROOF_MISSING",
|
|
4230
|
+
`the latest freshness proof for proposal ${proposal.proposal_id} was not supplied`
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
if (proof.proposal_hash !== proposal.proposal_hash || proof.proposal_version !== proposal.proposal_version || proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
4234
|
+
throw new ProposalStoreError(
|
|
4235
|
+
"FRESHNESS_PROOF_AUTHORITY_MISMATCH",
|
|
4236
|
+
`freshness proof does not match proposal ${proposal.proposal_id}`
|
|
4237
|
+
);
|
|
4238
|
+
}
|
|
4239
|
+
if (proof.result !== "fresh") {
|
|
4240
|
+
throw new ProposalStoreError(
|
|
4241
|
+
proof.result === "stale" ? "FRESHNESS_STALE" : "FRESHNESS_NOT_VERIFIED",
|
|
4242
|
+
`proposal ${proposal.proposal_id} freshness result is ${proof.result}`
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
if (Date.parse(proof.valid_until) < Date.parse(now)) {
|
|
4246
|
+
throw new ProposalStoreError(
|
|
4247
|
+
"FRESHNESS_PROOF_EXPIRED",
|
|
4248
|
+
`freshness proof for proposal ${proposal.proposal_id} has expired`
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
const used = this.db.prepare(`
|
|
4252
|
+
SELECT approval_id
|
|
4253
|
+
FROM approvals
|
|
4254
|
+
WHERE proposal_id = ? AND freshness_proof_digest = ?
|
|
4255
|
+
LIMIT 1
|
|
4256
|
+
`).get(proposal.proposal_id, proofDigest);
|
|
4257
|
+
if (isRecord(used)) {
|
|
4258
|
+
throw new ProposalStoreError(
|
|
4259
|
+
"FRESHNESS_PROOF_ALREADY_USED",
|
|
4260
|
+
`freshness proof for proposal ${proposal.proposal_id} already authorized a reviewer decision`
|
|
4261
|
+
);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
3883
4264
|
setState(proposalId, state, actor, payload) {
|
|
3884
4265
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3885
4266
|
this.transaction(() => {
|
|
@@ -4040,6 +4421,17 @@ function stateFromChangeSet(changeSet) {
|
|
|
4040
4421
|
if (changeSet.approval.status === "canceled") return "canceled";
|
|
4041
4422
|
return "pending_review";
|
|
4042
4423
|
}
|
|
4424
|
+
function proposalFreshnessAuthority(proposal) {
|
|
4425
|
+
if (!("freshness" in proposal.change_set) || proposal.change_set.freshness === void 0) return void 0;
|
|
4426
|
+
try {
|
|
4427
|
+
return parseFreshnessAuthority(proposal.change_set.freshness);
|
|
4428
|
+
} catch {
|
|
4429
|
+
throw new ProposalStoreError(
|
|
4430
|
+
"FRESHNESS_AUTHORITY_TAMPERED",
|
|
4431
|
+
`stored freshness authority for proposal ${proposal.proposal_id} failed integrity validation`
|
|
4432
|
+
);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4043
4435
|
function requiredApprovalCount(proposal) {
|
|
4044
4436
|
const configured = proposal.change_set.approval.required_approvals;
|
|
4045
4437
|
return typeof configured === "number" && Number.isSafeInteger(configured) && configured >= 1 ? configured : 1;
|
|
@@ -4566,6 +4958,7 @@ function rowToApproval(row) {
|
|
|
4566
4958
|
decision_hash: row.decision_hash == null ? void 0 : String(row.decision_hash),
|
|
4567
4959
|
signature: row.signature == null ? void 0 : String(row.signature),
|
|
4568
4960
|
integrity_hash: row.integrity_hash == null ? void 0 : String(row.integrity_hash),
|
|
4961
|
+
freshness_proof_digest: row.freshness_proof_digest == null ? void 0 : String(row.freshness_proof_digest),
|
|
4569
4962
|
created_at: String(row.created_at)
|
|
4570
4963
|
};
|
|
4571
4964
|
}
|
|
@@ -4942,7 +5335,7 @@ var sharedLedgerRestoreSpecs = {
|
|
|
4942
5335
|
"updated_at"
|
|
4943
5336
|
], ["proposal_id", "proposal_version", "proposal_hash", "action", "state", "tenant_id", "business_object", "object_id", "source_kind", "source_id", "source_schema", "source_table", "source_database_mutated", "change_set_json", "created_at", "updated_at"]),
|
|
4944
5337
|
proposal_events: restoreSpec("event_id", ["event_id", "proposal_id", "kind", "actor", "payload_json", "created_at"], ["event_id", "proposal_id", "kind", "actor", "payload_json", "created_at"]),
|
|
4945
|
-
approvals: restoreSpec("approval_id", ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "reason", "identity_json", "decision_hash", "signature", "integrity_hash", "created_at"], ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "created_at"]),
|
|
5338
|
+
approvals: restoreSpec("approval_id", ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "reason", "identity_json", "decision_hash", "signature", "integrity_hash", "freshness_proof_digest", "created_at"], ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "created_at"]),
|
|
4946
5339
|
writeback_jobs: restoreSpec("writeback_job_id", ["writeback_job_id", "proposal_id", "proposal_hash", "status", "job_json", "created_at", "updated_at"], ["writeback_job_id", "proposal_id", "proposal_hash", "status", "job_json", "created_at", "updated_at"]),
|
|
4947
5340
|
writeback_intents: restoreSpec("intent_id", ["intent_id", "idempotency_key", "writeback_job_id", "proposal_id", "proposal_hash", "runner_id", "operation", "status", "intent_json", "result_json", "reconciliation_reason", "created_at", "updated_at"], ["intent_id", "idempotency_key", "writeback_job_id", "proposal_id", "proposal_hash", "runner_id", "operation", "status", "intent_json", "created_at", "updated_at"]),
|
|
4948
5341
|
idempotency_receipts: restoreSpec("idempotency_key", ["idempotency_key", "writeback_job_id", "proposal_id", "receipt_status", "receipt_json", "created_at"], ["idempotency_key", "writeback_job_id", "proposal_id", "receipt_status", "receipt_json", "created_at"]),
|