@nifrajs/core 3.5.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/assurance.d.ts +7 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +39 -3
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +20 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +47 -15
- package/dist/capabilities.js.map +1 -1
- package/dist/diff.d.ts +2 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +26 -2
- package/dist/diff.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +16 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-effect.d.ts.map +1 -1
- package/dist/durable-effect.js +6 -3
- package/dist/durable-effect.js.map +1 -1
- package/dist/edge-kit.d.ts +2 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +2 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-lifecycle.d.ts.map +1 -1
- package/dist/effect-lifecycle.js +2 -1
- package/dist/effect-lifecycle.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +1 -1
- package/dist/effect-scope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +24 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +168 -13
- package/dist/evidence.js.map +1 -1
- package/dist/idempotency.d.ts +31 -0
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +40 -8
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +7 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +9 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.js +2 -0
- package/dist/internal/durable-saga-protocol.js.map +1 -1
- package/dist/internal/route-catalog.d.ts +6 -6
- package/dist/internal/route-catalog.d.ts.map +1 -1
- package/dist/internal/route-catalog.js +20 -18
- package/dist/internal/route-catalog.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +7 -6
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +36 -4
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-execution.d.ts +7 -3
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +1 -0
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +34 -29
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/route-program.d.ts +8 -2
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +222 -20
- package/dist/internal/route-program.js.map +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +8 -5
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +41 -37
- package/dist/manifest.js.map +1 -1
- package/dist/mount.d.ts +36 -0
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +32 -0
- package/dist/mount.js.map +1 -1
- package/dist/reflection.d.ts +26 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +65 -1
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -1
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js.map +1 -1
- package/dist/router/overlap.d.ts +23 -0
- package/dist/router/overlap.d.ts.map +1 -0
- package/dist/router/overlap.js +224 -0
- package/dist/router/overlap.js.map +1 -0
- package/dist/router/pattern.d.ts +117 -2
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +283 -23
- package/dist/router/pattern.js.map +1 -1
- package/dist/router/router.d.ts +18 -4
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +51 -44
- package/dist/router/router.js.map +1 -1
- package/dist/server/auth.d.ts +53 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +9 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +12 -7
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +13 -11
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/body-lane.d.ts +20 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +26 -7
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts +68 -0
- package/dist/server/body-parser.d.ts.map +1 -0
- package/dist/server/body-parser.js +172 -0
- package/dist/server/body-parser.js.map +1 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +62 -7
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +8 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +8 -0
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +71 -6
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +15 -5
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +25 -4
- package/dist/server/contract.js.map +1 -1
- package/dist/server/core-copies.d.ts +2 -0
- package/dist/server/core-copies.d.ts.map +1 -0
- package/dist/server/core-copies.js +20 -0
- package/dist/server/core-copies.js.map +1 -0
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -0
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +36 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -0
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +130 -73
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +4 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +4 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/methods.d.ts +45 -0
- package/dist/server/methods.d.ts.map +1 -0
- package/dist/server/methods.js +37 -0
- package/dist/server/methods.js.map +1 -0
- package/dist/server/multipart.d.ts +63 -0
- package/dist/server/multipart.d.ts.map +1 -0
- package/dist/server/multipart.js +374 -0
- package/dist/server/multipart.js.map +1 -0
- package/dist/server/node-outcome-hook.d.ts +20 -0
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js +15 -6
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/not-found-answer.d.ts +40 -0
- package/dist/server/not-found-answer.d.ts.map +1 -0
- package/dist/server/not-found-answer.js +56 -0
- package/dist/server/not-found-answer.js.map +1 -0
- package/dist/server/not-found.d.ts +23 -0
- package/dist/server/not-found.d.ts.map +1 -0
- package/dist/server/not-found.js +81 -0
- package/dist/server/not-found.js.map +1 -0
- package/dist/server/plugin.d.ts +26 -1
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/proto-guard.d.ts +18 -5
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +114 -9
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts +7 -0
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +47 -0
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +13 -2
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +11 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +33 -19
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/request-lineage.d.ts +8 -0
- package/dist/server/request-lineage.d.ts.map +1 -0
- package/dist/server/request-lineage.js +14 -0
- package/dist/server/request-lineage.js.map +1 -0
- package/dist/server/request-target.d.ts +31 -0
- package/dist/server/request-target.d.ts.map +1 -0
- package/dist/server/request-target.js +44 -0
- package/dist/server/request-target.js.map +1 -0
- package/dist/server/respond.d.ts +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +16 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +19 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +59 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +9 -5
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -6
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +53 -3
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +191 -19
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +1206 -218
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +10 -7
- package/dist/server/sse.js.map +1 -1
- package/dist/server/static-headers.d.ts +3 -0
- package/dist/server/static-headers.d.ts.map +1 -1
- package/dist/server/static-headers.js +19 -1
- package/dist/server/static-headers.js.map +1 -1
- package/dist/server/transport-lane.d.ts +8 -0
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +9 -8
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/validation.d.ts.map +1 -1
- package/dist/server/validation.js +5 -1
- package/dist/server/validation.js.map +1 -1
- package/dist/server/websocket.d.ts +10 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +7 -4
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +2 -0
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +6 -3
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +2 -1
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +7 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.js +5 -0
- package/dist/single-copy-register.js.map +1 -1
- package/dist/single-copy.d.ts +46 -3
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +125 -12
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts +23 -0
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +37 -3
- package/dist/tool-contract.js.map +1 -1
- package/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +27 -2
- package/src/assurance.ts +52 -2
- package/src/capabilities.ts +81 -22
- package/src/diff.ts +31 -6
- package/src/durable-adapters.ts +20 -3
- package/src/durable-effect.ts +10 -3
- package/src/edge-kit.ts +7 -0
- package/src/effect-lifecycle.ts +5 -1
- package/src/effect-scope.ts +2 -1
- package/src/errors.ts +3 -0
- package/src/evidence.ts +237 -12
- package/src/idempotency.ts +72 -8
- package/src/index.ts +40 -3
- package/src/internal/code-unit-order.ts +8 -0
- package/src/internal/durable-saga-protocol.ts +2 -0
- package/src/internal/route-catalog.ts +19 -18
- package/src/internal/route-compiler.ts +62 -13
- package/src/internal/route-execution.ts +13 -3
- package/src/internal/route-lanes.ts +37 -28
- package/src/internal/route-program.ts +434 -20
- package/src/ledger.ts +11 -5
- package/src/manifest.ts +41 -36
- package/src/mount.ts +62 -0
- package/src/reflection.ts +84 -1
- package/src/response-observer.ts +1 -0
- package/src/router/overlap.ts +258 -0
- package/src/router/pattern.ts +349 -25
- package/src/router/router.ts +76 -51
- package/src/server/auth.ts +75 -0
- package/src/server/bare-error-lane.ts +12 -11
- package/src/server/body-lane.ts +46 -6
- package/src/server/body-parser.ts +241 -0
- package/src/server/body.ts +82 -9
- package/src/server/client-ip.ts +9 -0
- package/src/server/context.ts +219 -12
- package/src/server/contract.ts +54 -9
- package/src/server/core-copies.ts +21 -0
- package/src/server/edge.ts +2 -0
- package/src/server/http.ts +34 -0
- package/src/server/idempotency-lane.ts +147 -72
- package/src/server/install.ts +6 -0
- package/src/server/methods.ts +143 -0
- package/src/server/multipart.ts +424 -0
- package/src/server/node-outcome-hook.ts +40 -7
- package/src/server/not-found-answer.ts +109 -0
- package/src/server/not-found.ts +121 -0
- package/src/server/plugin.ts +45 -1
- package/src/server/proto-guard.ts +128 -9
- package/src/server/query.ts +49 -0
- package/src/server/registry.ts +17 -2
- package/src/server/request-context.ts +44 -16
- package/src/server/request-lineage.ts +15 -0
- package/src/server/request-target.ts +46 -0
- package/src/server/respond.ts +14 -3
- package/src/server/response-contract-lane.ts +97 -16
- package/src/server/response-observer-runtime.ts +37 -8
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +59 -2
- package/src/server/server.ts +1613 -269
- package/src/server/sse.ts +10 -8
- package/src/server/static-headers.ts +22 -1
- package/src/server/transport-lane.ts +17 -9
- package/src/server/validation.ts +7 -1
- package/src/server/websocket.ts +23 -9
- package/src/server/ws-bun.ts +8 -3
- package/src/server/ws-hook.ts +2 -1
- package/src/server.ts +38 -2
- package/src/single-copy-register.ts +5 -0
- package/src/single-copy.ts +160 -12
- package/src/tool-contract.ts +66 -3
- package/src/transport-codec.ts +2 -1
package/src/durable-adapters.ts
CHANGED
|
@@ -222,7 +222,8 @@ export class MemoryDurableRecordBackend implements DurableRecordBackend {
|
|
|
222
222
|
if (!key.startsWith(prefix) || !input.states.includes(value.state ?? "")) return false
|
|
223
223
|
return input.updatedBefore === undefined || (value.updatedAt ?? 0) <= input.updatedBefore
|
|
224
224
|
})
|
|
225
|
-
|
|
225
|
+
// Code-unit order, the order the cursor comparison below uses (and SQLite's BINARY collation).
|
|
226
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
226
227
|
.filter(([key]) => input.cursor === undefined || key > prefix + input.cursor)
|
|
227
228
|
const page = records.slice(0, input.limit)
|
|
228
229
|
return Object.freeze({
|
|
@@ -382,6 +383,14 @@ export async function runDurableExecutionAdapterConformance(
|
|
|
382
383
|
version: 1,
|
|
383
384
|
}
|
|
384
385
|
requireConformance(await adapter.sagas.create(saga), "saga create rejected")
|
|
386
|
+
requireConformance(
|
|
387
|
+
!(await adapter.sagas.compareAndSet({
|
|
388
|
+
sagaId,
|
|
389
|
+
version: 1,
|
|
390
|
+
record: { ...saga, sagaId: `${sagaId}-other`, state: "completed", updatedAt: 2, version: 2 },
|
|
391
|
+
})),
|
|
392
|
+
"saga CAS accepted a record for another saga",
|
|
393
|
+
)
|
|
385
394
|
requireConformance(
|
|
386
395
|
await adapter.sagas.compareAndSet({
|
|
387
396
|
sagaId,
|
|
@@ -914,11 +923,19 @@ export class DurableObjectRecordBackend implements DurableRecordBackend {
|
|
|
914
923
|
}
|
|
915
924
|
if (selected.length > input.limit) break
|
|
916
925
|
}
|
|
917
|
-
const hasMore = selected.length > input.limit || listed.size > input.limit
|
|
918
926
|
const page = selected.slice(0, input.limit)
|
|
927
|
+
// A full page resumes after its own last record: the matching record that overflowed it was
|
|
928
|
+
// visited but not returned, so the cursor must not pass it. Otherwise every listed key was
|
|
929
|
+
// returned or filtered out, and the next page starts after the last one visited.
|
|
930
|
+
const cursor =
|
|
931
|
+
selected.length > input.limit
|
|
932
|
+
? page.at(-1)?.id
|
|
933
|
+
: listed.size > input.limit
|
|
934
|
+
? lastVisited
|
|
935
|
+
: undefined
|
|
919
936
|
return Object.freeze({
|
|
920
937
|
records: Object.freeze(page.map(({ record }) => clone(record))),
|
|
921
|
-
...(
|
|
938
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
922
939
|
})
|
|
923
940
|
}
|
|
924
941
|
async acquireLease(
|
package/src/durable-effect.ts
CHANGED
|
@@ -57,6 +57,7 @@ export function createDurableEffectJournal(
|
|
|
57
57
|
if (record === undefined) throw new DurableEffectTransitionError(effectId, "missing")
|
|
58
58
|
if (
|
|
59
59
|
record.effectId !== effectId ||
|
|
60
|
+
typeof record.capability !== "string" ||
|
|
60
61
|
!validCapabilityId(record.capability) ||
|
|
61
62
|
!["admission", "executing", "committed", "failed", "unknown"].includes(record.state) ||
|
|
62
63
|
!Number.isSafeInteger(record.version) ||
|
|
@@ -69,7 +70,10 @@ export function createDurableEffectJournal(
|
|
|
69
70
|
if (record.createdAt > record.updatedAt)
|
|
70
71
|
throw new TypeError(`durable effect ${effectId}: updatedAt precedes createdAt`)
|
|
71
72
|
if (record.target !== undefined) assertToken(record.target, "stored durable effect target", 128)
|
|
72
|
-
if (
|
|
73
|
+
if (
|
|
74
|
+
record.digest !== undefined &&
|
|
75
|
+
(typeof record.digest !== "string" || !/^[0-9a-f]{64}$/u.test(record.digest))
|
|
76
|
+
)
|
|
73
77
|
throw new TypeError(`durable effect ${effectId}: store returned an invalid digest`)
|
|
74
78
|
if (record.tenantId !== undefined)
|
|
75
79
|
assertToken(record.tenantId, "stored durable effect tenantId")
|
|
@@ -77,7 +81,10 @@ export function createDurableEffectJournal(
|
|
|
77
81
|
assertToken(record.principalId, "stored durable effect principalId")
|
|
78
82
|
if ((record.tenantId === undefined) !== (record.principalId === undefined))
|
|
79
83
|
throw new TypeError(`durable effect ${effectId}: store returned a partial identity`)
|
|
80
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
record.errorCode !== undefined &&
|
|
86
|
+
(typeof record.errorCode !== "string" || !ERROR_CODE.test(record.errorCode))
|
|
87
|
+
)
|
|
81
88
|
throw new TypeError(`durable effect ${effectId}: store returned an invalid error code`)
|
|
82
89
|
return record
|
|
83
90
|
}
|
|
@@ -145,7 +152,7 @@ export function createDurableEffectJournal(
|
|
|
145
152
|
await transition(effectId, "executing", "committed")
|
|
146
153
|
},
|
|
147
154
|
async failed(effectId, input) {
|
|
148
|
-
if (!ERROR_CODE.test(input.errorCode))
|
|
155
|
+
if (typeof input.errorCode !== "string" || !ERROR_CODE.test(input.errorCode))
|
|
149
156
|
throw new TypeError("durable effect errorCode is invalid")
|
|
150
157
|
const record = await current(effectId)
|
|
151
158
|
const to = input.began ? "unknown" : "failed"
|
package/src/edge-kit.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - {@link toResponse} render a {@link ResponseResult} (e.g. a 413 / 415 rejection) to a `Response`
|
|
12
12
|
* - {@link plainError} the structured rejection envelope the lane returns
|
|
13
13
|
* - {@link queryObjectOf} / {@link searchOf} query parse without a full WHATWG URL construction
|
|
14
|
+
* - {@link answerNotFound} run a not-found handler under the same rules (404 only, no 2xx, fail closed)
|
|
14
15
|
*
|
|
15
16
|
* Everything here carries only structure - bytes, guards, and envelopes - never routing, lifecycle,
|
|
16
17
|
* or app state. It is the interface, not the framework.
|
|
@@ -18,6 +19,12 @@
|
|
|
18
19
|
|
|
19
20
|
export { readBodyFramed } from "./server/body-lane.ts"
|
|
20
21
|
export { plainError } from "./server/http.ts"
|
|
22
|
+
export {
|
|
23
|
+
answerNotFound,
|
|
24
|
+
type NotFoundHandler,
|
|
25
|
+
type NotFoundInput,
|
|
26
|
+
notFoundInput,
|
|
27
|
+
} from "./server/not-found-answer.ts"
|
|
21
28
|
export type { ProtoPoisoning } from "./server/proto-guard.ts"
|
|
22
29
|
export { type QueryValue, queryObjectOf, searchOf } from "./server/query.ts"
|
|
23
30
|
export { toResponse } from "./server/respond.ts"
|
package/src/effect-lifecycle.ts
CHANGED
|
@@ -93,7 +93,11 @@ export function emitEffectLifecycle(
|
|
|
93
93
|
if (input.attempt !== undefined && (!Number.isSafeInteger(input.attempt) || input.attempt < 1)) {
|
|
94
94
|
return
|
|
95
95
|
}
|
|
96
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
input.errorCode !== undefined &&
|
|
98
|
+
(typeof input.errorCode !== "string" || !ERROR_CODE.test(input.errorCode))
|
|
99
|
+
)
|
|
100
|
+
return
|
|
97
101
|
const event: EffectLifecycleEvent = Object.freeze({
|
|
98
102
|
effectId: input.effectId,
|
|
99
103
|
capability: input.capability,
|
package/src/effect-scope.ts
CHANGED
|
@@ -108,7 +108,8 @@ function codeFor(
|
|
|
108
108
|
typeof configured === "function"
|
|
109
109
|
? configured(error, began)
|
|
110
110
|
: (configured ?? (began ? "execution_failed" : "admission_failed"))
|
|
111
|
-
if (!ERROR_CODE.test(code))
|
|
111
|
+
if (typeof code !== "string" || !ERROR_CODE.test(code))
|
|
112
|
+
throw new TypeError("effect errorCode is invalid")
|
|
112
113
|
return code
|
|
113
114
|
}
|
|
114
115
|
|
package/src/errors.ts
CHANGED
|
@@ -27,7 +27,10 @@ export type RouteConfigErrorCode =
|
|
|
27
27
|
| "INVALID_ASSURANCE"
|
|
28
28
|
| "INVALID_IDEMPOTENCY"
|
|
29
29
|
| "INVALID_WS_SEND_VALIDATION"
|
|
30
|
+
| "INVALID_WIRE"
|
|
31
|
+
| "INVALID_VALIDATION_ORDER"
|
|
30
32
|
| "INVALID_BODY_LIMIT"
|
|
33
|
+
| "PLUGIN_RECONFIGURED"
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* Thrown at route registration when a route is misconfigured. This is the
|
package/src/evidence.ts
CHANGED
|
@@ -14,11 +14,14 @@ import type {
|
|
|
14
14
|
CapabilityFinding,
|
|
15
15
|
} from "./capabilities.ts"
|
|
16
16
|
import type { ResponseClassification } from "./classification.ts"
|
|
17
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
17
18
|
import { evidenceProvenance } from "./internal/route-assurance.ts"
|
|
18
19
|
import {
|
|
19
20
|
type JsonSchema,
|
|
21
|
+
type ReflectedMount,
|
|
20
22
|
type ReflectedRoute,
|
|
21
23
|
type ReflectedSchemaField,
|
|
24
|
+
reflectMounts,
|
|
22
25
|
reflectRoutes,
|
|
23
26
|
type SchemaReflection,
|
|
24
27
|
} from "./reflection.ts"
|
|
@@ -26,12 +29,15 @@ import {
|
|
|
26
29
|
export interface ProjectEvidenceSchemaPart {
|
|
27
30
|
readonly jsonSchema?: JsonSchema
|
|
28
31
|
readonly fields?: readonly ReflectedSchemaField[]
|
|
32
|
+
/** The media types a body schema reads through a parser of its own. */
|
|
33
|
+
readonly mediaTypes?: readonly string[]
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export interface ProjectEvidenceSchema {
|
|
32
37
|
readonly bodyLimit?: number | "unlimited"
|
|
33
38
|
readonly bodyLimitReason?: string
|
|
34
39
|
readonly headers?: ProjectEvidenceSchemaPart
|
|
40
|
+
readonly cookies?: ProjectEvidenceSchemaPart
|
|
35
41
|
readonly body?: ProjectEvidenceSchemaPart
|
|
36
42
|
readonly query?: ProjectEvidenceSchemaPart
|
|
37
43
|
readonly params?: ProjectEvidenceSchemaPart
|
|
@@ -50,6 +56,7 @@ export interface ProjectEvidenceRoute {
|
|
|
50
56
|
readonly method: string
|
|
51
57
|
readonly path: string
|
|
52
58
|
readonly schema?: ProjectEvidenceSchema
|
|
59
|
+
readonly responseContract?: "warn" | "enforce"
|
|
53
60
|
readonly assurance?: readonly AssuranceEvidence[]
|
|
54
61
|
readonly capabilities?: readonly string[]
|
|
55
62
|
readonly family?: boolean
|
|
@@ -93,6 +100,8 @@ export interface ProjectEvidenceCapabilities {
|
|
|
93
100
|
export interface ProjectEvidenceSnapshot {
|
|
94
101
|
readonly version: 1
|
|
95
102
|
readonly routes: readonly ProjectEvidenceRoute[]
|
|
103
|
+
/** Mounted children whose routes are not in `routes`, present when there is at least one. */
|
|
104
|
+
readonly mounts?: readonly ReflectedMount[]
|
|
96
105
|
readonly assurance?: ProjectEvidenceAssurance
|
|
97
106
|
readonly capabilities?: ProjectEvidenceCapabilities
|
|
98
107
|
}
|
|
@@ -108,6 +117,15 @@ export interface ProjectEvidenceOptions {
|
|
|
108
117
|
readonly routes?: readonly ReflectedRoute[]
|
|
109
118
|
/** Optional static source locations keyed by `${METHOD}\n${path}`. */
|
|
110
119
|
readonly sourceLocations?: ReadonlyMap<string, readonly ProjectEvidenceSourceLocation[]>
|
|
120
|
+
/** An existing mount reflection, like `routes`. Default: `reflectMounts(source)`. */
|
|
121
|
+
readonly mounts?: readonly ReflectedMount[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One token-only evidence snapshot in a composed application surface. */
|
|
125
|
+
export interface ProjectEvidenceCompositionPart {
|
|
126
|
+
readonly evidence: ProjectEvidenceSnapshot
|
|
127
|
+
/** Public pathname prefix to add when the child receives a stripped mount request. */
|
|
128
|
+
readonly pathPrefix?: string
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
@@ -120,6 +138,9 @@ function schemaPart(value: SchemaReflection | undefined): ProjectEvidenceSchemaP
|
|
|
120
138
|
return Object.freeze({
|
|
121
139
|
...(value.jsonSchema !== undefined ? { jsonSchema: value.jsonSchema } : {}),
|
|
122
140
|
...(value.fields !== undefined ? { fields: Object.freeze([...value.fields]) } : {}),
|
|
141
|
+
...(value.mediaTypes !== undefined
|
|
142
|
+
? { mediaTypes: Object.freeze([...value.mediaTypes].sort()) }
|
|
143
|
+
: {}),
|
|
123
144
|
})
|
|
124
145
|
}
|
|
125
146
|
|
|
@@ -127,6 +148,7 @@ function schemaOf(route: ReflectedRoute): ProjectEvidenceSchema | undefined {
|
|
|
127
148
|
const source = route.schema
|
|
128
149
|
if (source === undefined) return undefined
|
|
129
150
|
const headers = schemaPart(source.headers)
|
|
151
|
+
const cookies = schemaPart(source.cookies)
|
|
130
152
|
const body = schemaPart(source.body)
|
|
131
153
|
const query = schemaPart(source.query)
|
|
132
154
|
const params = schemaPart(source.params)
|
|
@@ -141,6 +163,7 @@ function schemaOf(route: ReflectedRoute): ProjectEvidenceSchema | undefined {
|
|
|
141
163
|
...(source.bodyLimit !== undefined ? { bodyLimit: source.bodyLimit } : {}),
|
|
142
164
|
...(source.bodyLimitReason !== undefined ? { bodyLimitReason: source.bodyLimitReason } : {}),
|
|
143
165
|
...(headers !== undefined ? { headers } : {}),
|
|
166
|
+
...(cookies !== undefined ? { cookies } : {}),
|
|
144
167
|
...(body !== undefined ? { body } : {}),
|
|
145
168
|
...(query !== undefined ? { query } : {}),
|
|
146
169
|
...(params !== undefined ? { params } : {}),
|
|
@@ -163,7 +186,7 @@ function assuranceEvidenceOf(values: readonly AssuranceEvidence[]): readonly Ass
|
|
|
163
186
|
provenance: evidenceProvenance(item),
|
|
164
187
|
}),
|
|
165
188
|
)
|
|
166
|
-
.sort((a, b) => a.id
|
|
189
|
+
.sort((a, b) => codeUnitOrder(a.id, b.id) || codeUnitOrder(a.source, b.source)),
|
|
167
190
|
)
|
|
168
191
|
}
|
|
169
192
|
|
|
@@ -175,9 +198,9 @@ function capabilityEvidenceOf(
|
|
|
175
198
|
.map((item) => Object.freeze({ id: item.id, kind: item.kind, source: item.source }))
|
|
176
199
|
.sort(
|
|
177
200
|
(a, b) =>
|
|
178
|
-
a.id
|
|
179
|
-
a.kind
|
|
180
|
-
a.source
|
|
201
|
+
codeUnitOrder(a.id, b.id) ||
|
|
202
|
+
codeUnitOrder(a.kind, b.kind) ||
|
|
203
|
+
codeUnitOrder(a.source, b.source),
|
|
181
204
|
),
|
|
182
205
|
)
|
|
183
206
|
}
|
|
@@ -186,7 +209,7 @@ const sortByRoute = <T extends { readonly method: string; readonly path: string
|
|
|
186
209
|
values: readonly T[],
|
|
187
210
|
): readonly T[] =>
|
|
188
211
|
Object.freeze(
|
|
189
|
-
[...values].sort((a, b) => a.path
|
|
212
|
+
[...values].sort((a, b) => codeUnitOrder(a.path, b.path) || codeUnitOrder(a.method, b.method)),
|
|
190
213
|
)
|
|
191
214
|
|
|
192
215
|
function evidenceRouteOf(
|
|
@@ -198,6 +221,7 @@ function evidenceRouteOf(
|
|
|
198
221
|
method: route.method.toUpperCase(),
|
|
199
222
|
path: route.path,
|
|
200
223
|
...(schema !== undefined ? { schema } : {}),
|
|
224
|
+
...(route.responseContract === undefined ? {} : { responseContract: route.responseContract }),
|
|
201
225
|
...(route.assurance !== undefined ? { assurance: assuranceEvidenceOf(route.assurance) } : {}),
|
|
202
226
|
...(route.capabilities !== undefined
|
|
203
227
|
? { capabilities: Object.freeze([...route.capabilities].sort()) }
|
|
@@ -230,9 +254,9 @@ function assuranceOf(report: AssuranceReport | undefined): ProjectEvidenceAssura
|
|
|
230
254
|
findings: Object.freeze(
|
|
231
255
|
[...report.findings].sort(
|
|
232
256
|
(a, b) =>
|
|
233
|
-
a.path
|
|
234
|
-
a.method
|
|
235
|
-
a.code
|
|
257
|
+
codeUnitOrder(a.path, b.path) ||
|
|
258
|
+
codeUnitOrder(a.method, b.method) ||
|
|
259
|
+
codeUnitOrder(a.code, b.code),
|
|
236
260
|
),
|
|
237
261
|
),
|
|
238
262
|
})
|
|
@@ -260,20 +284,203 @@ function capabilitiesOf(
|
|
|
260
284
|
findings: Object.freeze(
|
|
261
285
|
[...report.findings].sort(
|
|
262
286
|
(a, b) =>
|
|
263
|
-
a.path
|
|
264
|
-
a.method
|
|
265
|
-
a.code
|
|
287
|
+
codeUnitOrder(a.path, b.path) ||
|
|
288
|
+
codeUnitOrder(a.method, b.method) ||
|
|
289
|
+
codeUnitOrder(a.code, b.code),
|
|
266
290
|
),
|
|
267
291
|
),
|
|
268
292
|
})
|
|
269
293
|
}
|
|
270
294
|
|
|
295
|
+
function trimTrailingSlashes(path: string): string {
|
|
296
|
+
// Index-scan instead of `/\/+$/`: path prefixes can be library input, and the
|
|
297
|
+
// unanchored-start trailing-run replacement can backtrack quadratically.
|
|
298
|
+
let end = path.length
|
|
299
|
+
while (end > 0 && path.charCodeAt(end - 1) === 47 /* '/' */) end--
|
|
300
|
+
return path.slice(0, end)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const composedPath = (prefix: string, path: string): string => {
|
|
304
|
+
if (path === "*") return path
|
|
305
|
+
if (!path.startsWith("/"))
|
|
306
|
+
throw new TypeError(`project evidence: route path must start with "/": ${path}`)
|
|
307
|
+
if (prefix === "") return path
|
|
308
|
+
if (!prefix.startsWith("/")) {
|
|
309
|
+
throw new TypeError(`project evidence: path prefix must start with "/": ${prefix}`)
|
|
310
|
+
}
|
|
311
|
+
const normalized = prefix === "/" ? "" : trimTrailingSlashes(prefix)
|
|
312
|
+
return normalized === "" ? path : path === "/" ? normalized : `${normalized}${path}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function remapEvidenceSnapshot(
|
|
316
|
+
evidence: ProjectEvidenceSnapshot,
|
|
317
|
+
pathPrefix: string,
|
|
318
|
+
): ProjectEvidenceSnapshot {
|
|
319
|
+
if (evidence.version !== 1) throw new Error("project evidence: unsupported snapshot version")
|
|
320
|
+
const route = (
|
|
321
|
+
method: string,
|
|
322
|
+
path: string,
|
|
323
|
+
): { readonly method: string; readonly path: string } => ({
|
|
324
|
+
method: method.toUpperCase(),
|
|
325
|
+
path: composedPath(pathPrefix, path),
|
|
326
|
+
})
|
|
327
|
+
const routes = evidence.routes.map((item) => {
|
|
328
|
+
const key = route(item.method, item.path)
|
|
329
|
+
return Object.freeze({ ...item, ...key })
|
|
330
|
+
})
|
|
331
|
+
const mounts = evidence.mounts?.map((item) =>
|
|
332
|
+
Object.freeze({ ...item, path: composedPath(pathPrefix, item.path) }),
|
|
333
|
+
)
|
|
334
|
+
const assurance =
|
|
335
|
+
evidence.assurance === undefined
|
|
336
|
+
? undefined
|
|
337
|
+
: Object.freeze({
|
|
338
|
+
ok: evidence.assurance.ok,
|
|
339
|
+
routes: Object.freeze(
|
|
340
|
+
evidence.assurance.routes.map((item) =>
|
|
341
|
+
Object.freeze({ ...item, ...route(item.method, item.path) }),
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
findings: Object.freeze(
|
|
345
|
+
evidence.assurance.findings.map((item) =>
|
|
346
|
+
Object.freeze({ ...item, ...route(item.method, item.path) }),
|
|
347
|
+
),
|
|
348
|
+
),
|
|
349
|
+
})
|
|
350
|
+
const capabilities =
|
|
351
|
+
evidence.capabilities === undefined
|
|
352
|
+
? undefined
|
|
353
|
+
: Object.freeze({
|
|
354
|
+
ok: evidence.capabilities.ok,
|
|
355
|
+
routes: Object.freeze(
|
|
356
|
+
evidence.capabilities.routes.map((item) =>
|
|
357
|
+
Object.freeze({ ...item, ...route(item.method, item.path) }),
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
findings: Object.freeze(
|
|
361
|
+
evidence.capabilities.findings.map((item) =>
|
|
362
|
+
Object.freeze({ ...item, ...route(item.method, item.path) }),
|
|
363
|
+
),
|
|
364
|
+
),
|
|
365
|
+
})
|
|
366
|
+
return Object.freeze({
|
|
367
|
+
version: 1,
|
|
368
|
+
routes: Object.freeze(routes),
|
|
369
|
+
...(mounts === undefined ? {} : { mounts: Object.freeze(mounts) }),
|
|
370
|
+
...(assurance === undefined ? {} : { assurance }),
|
|
371
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function routeKeyForEvidence(method: string, path: string): string {
|
|
376
|
+
return `${method.toUpperCase()}\n${path}`
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function validateComposedReportRoutes(
|
|
380
|
+
report: ProjectEvidenceAssurance | ProjectEvidenceCapabilities,
|
|
381
|
+
routeKeys: ReadonlySet<string>,
|
|
382
|
+
kind: "assurance" | "capabilities",
|
|
383
|
+
): void {
|
|
384
|
+
for (const route of report.routes) {
|
|
385
|
+
if (!routeKeys.has(routeKeyForEvidence(route.method, route.path))) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`project evidence: ${kind} route ${route.method} ${route.path} is not present in the composed route set`,
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Compose page and mounted-app evidence into one deterministic snapshot.
|
|
395
|
+
*
|
|
396
|
+
* This is an offline operation. It carries route contracts, assurance/capability tokens, and source
|
|
397
|
+
* locations only; it never invokes handlers or copies request/business data. A duplicate public
|
|
398
|
+
* method+path or a report that no longer describes a composed route is rejected rather than silently
|
|
399
|
+
* dropping one side of the trust boundary.
|
|
400
|
+
*/
|
|
401
|
+
export function composeProjectEvidence(
|
|
402
|
+
parts: readonly ProjectEvidenceCompositionPart[],
|
|
403
|
+
): ProjectEvidenceSnapshot {
|
|
404
|
+
const routes: ProjectEvidenceRoute[] = []
|
|
405
|
+
const routeKeys = new Set<string>()
|
|
406
|
+
const mounts: ReflectedMount[] = []
|
|
407
|
+
const assuranceReports: ProjectEvidenceAssurance[] = []
|
|
408
|
+
const capabilityReports: ProjectEvidenceCapabilities[] = []
|
|
409
|
+
|
|
410
|
+
for (const part of parts) {
|
|
411
|
+
const prefix = part.pathPrefix ?? ""
|
|
412
|
+
const mapped = remapEvidenceSnapshot(part.evidence, prefix)
|
|
413
|
+
for (const route of mapped.routes) {
|
|
414
|
+
const key = routeKeyForEvidence(route.method, route.path)
|
|
415
|
+
if (routeKeys.has(key)) {
|
|
416
|
+
throw new Error(`project evidence: duplicate composed route ${route.method} ${route.path}`)
|
|
417
|
+
}
|
|
418
|
+
routeKeys.add(key)
|
|
419
|
+
routes.push(route)
|
|
420
|
+
}
|
|
421
|
+
mounts.push(...(mapped.mounts ?? []))
|
|
422
|
+
if (mapped.assurance !== undefined) assuranceReports.push(mapped.assurance)
|
|
423
|
+
if (mapped.capabilities !== undefined) capabilityReports.push(mapped.capabilities)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const assurance =
|
|
427
|
+
assuranceReports.length === 0
|
|
428
|
+
? undefined
|
|
429
|
+
: Object.freeze({
|
|
430
|
+
ok: assuranceReports.every((report) => report.ok),
|
|
431
|
+
routes: sortByRoute(assuranceReports.flatMap((report) => report.routes)),
|
|
432
|
+
findings: Object.freeze(
|
|
433
|
+
assuranceReports
|
|
434
|
+
.flatMap((report) => report.findings)
|
|
435
|
+
.sort(
|
|
436
|
+
(a, b) =>
|
|
437
|
+
codeUnitOrder(a.path, b.path) ||
|
|
438
|
+
codeUnitOrder(a.method, b.method) ||
|
|
439
|
+
codeUnitOrder(a.code, b.code),
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
})
|
|
443
|
+
const capabilities =
|
|
444
|
+
capabilityReports.length === 0
|
|
445
|
+
? undefined
|
|
446
|
+
: Object.freeze({
|
|
447
|
+
ok: capabilityReports.every((report) => report.ok),
|
|
448
|
+
routes: sortByRoute(capabilityReports.flatMap((report) => report.routes)),
|
|
449
|
+
findings: Object.freeze(
|
|
450
|
+
capabilityReports
|
|
451
|
+
.flatMap((report) => report.findings)
|
|
452
|
+
.sort(
|
|
453
|
+
(a, b) =>
|
|
454
|
+
codeUnitOrder(a.path, b.path) ||
|
|
455
|
+
codeUnitOrder(a.method, b.method) ||
|
|
456
|
+
codeUnitOrder(a.code, b.code),
|
|
457
|
+
),
|
|
458
|
+
),
|
|
459
|
+
})
|
|
460
|
+
if (assurance !== undefined) validateComposedReportRoutes(assurance, routeKeys, "assurance")
|
|
461
|
+
if (capabilities !== undefined)
|
|
462
|
+
validateComposedReportRoutes(capabilities, routeKeys, "capabilities")
|
|
463
|
+
|
|
464
|
+
return Object.freeze({
|
|
465
|
+
version: 1,
|
|
466
|
+
routes: sortByRoute(routes),
|
|
467
|
+
...mountsPart(mounts),
|
|
468
|
+
...(assurance === undefined ? {} : { assurance }),
|
|
469
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
271
473
|
function schemaPartToReflection(
|
|
272
474
|
value: ProjectEvidenceSchemaPart | undefined,
|
|
273
475
|
): SchemaReflection | undefined {
|
|
274
476
|
return value === undefined
|
|
275
477
|
? undefined
|
|
276
|
-
: {
|
|
478
|
+
: {
|
|
479
|
+
standard: undefined,
|
|
480
|
+
jsonSchema: value.jsonSchema,
|
|
481
|
+
fields: value.fields,
|
|
482
|
+
...(value.mediaTypes !== undefined ? { mediaTypes: value.mediaTypes } : {}),
|
|
483
|
+
}
|
|
277
484
|
}
|
|
278
485
|
|
|
279
486
|
function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): ReflectedRoute["schema"] {
|
|
@@ -285,6 +492,7 @@ function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): Reflecte
|
|
|
285
492
|
]),
|
|
286
493
|
)
|
|
287
494
|
const headers = schemaPartToReflection(schema.headers)
|
|
495
|
+
const cookies = schemaPartToReflection(schema.cookies)
|
|
288
496
|
const body = schemaPartToReflection(schema.body)
|
|
289
497
|
const query = schemaPartToReflection(schema.query)
|
|
290
498
|
const params = schemaPartToReflection(schema.params)
|
|
@@ -294,6 +502,7 @@ function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): Reflecte
|
|
|
294
502
|
...(schema.bodyLimit !== undefined ? { bodyLimit: schema.bodyLimit } : {}),
|
|
295
503
|
...(schema.bodyLimitReason !== undefined ? { bodyLimitReason: schema.bodyLimitReason } : {}),
|
|
296
504
|
...(headers !== undefined ? { headers } : {}),
|
|
505
|
+
...(cookies !== undefined ? { cookies } : {}),
|
|
297
506
|
...(body !== undefined ? { body } : {}),
|
|
298
507
|
...(query !== undefined ? { query } : {}),
|
|
299
508
|
...(params !== undefined ? { params } : {}),
|
|
@@ -311,12 +520,14 @@ function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): Reflecte
|
|
|
311
520
|
export function reflectedRoutesFromEvidence(
|
|
312
521
|
evidence: ProjectEvidenceSnapshot,
|
|
313
522
|
): readonly ReflectedRoute[] {
|
|
523
|
+
if (evidence.version !== 1) throw new Error("project evidence: unsupported snapshot version")
|
|
314
524
|
return evidence.routes.map((route) => {
|
|
315
525
|
const schema = schemaFromEvidence(route.schema)
|
|
316
526
|
return {
|
|
317
527
|
method: route.method,
|
|
318
528
|
path: route.path,
|
|
319
529
|
...(schema !== undefined ? { schema } : {}),
|
|
530
|
+
...(route.responseContract === undefined ? {} : { responseContract: route.responseContract }),
|
|
320
531
|
...(route.assurance !== undefined ? { assurance: route.assurance } : {}),
|
|
321
532
|
...(route.capabilities !== undefined ? { capabilities: route.capabilities } : {}),
|
|
322
533
|
...(route.family === true ? { family: true } : {}),
|
|
@@ -345,11 +556,25 @@ export function snapshotProjectEvidence(
|
|
|
345
556
|
return Object.freeze({
|
|
346
557
|
version: 1,
|
|
347
558
|
routes,
|
|
559
|
+
...mountsPart(options.mounts ?? reflectMounts(source)),
|
|
348
560
|
...(assurance !== undefined ? { assurance } : {}),
|
|
349
561
|
...(capabilities !== undefined ? { capabilities } : {}),
|
|
350
562
|
})
|
|
351
563
|
}
|
|
352
564
|
|
|
565
|
+
/** The snapshot's `mounts` field, sorted, and absent when empty so a mount-free app's snapshot (and its
|
|
566
|
+
* manifest hash) is unchanged. */
|
|
567
|
+
const mountsPart = (
|
|
568
|
+
mounts: readonly ReflectedMount[],
|
|
569
|
+
): { readonly mounts?: readonly ReflectedMount[] } =>
|
|
570
|
+
mounts.length === 0
|
|
571
|
+
? {}
|
|
572
|
+
: {
|
|
573
|
+
mounts: Object.freeze(
|
|
574
|
+
[...mounts].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
|
575
|
+
),
|
|
576
|
+
}
|
|
577
|
+
|
|
353
578
|
function canonicalValue(value: unknown): string {
|
|
354
579
|
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
355
580
|
return JSON.stringify(value)
|