@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.
Files changed (325) hide show
  1. package/README.md +26 -0
  2. package/dist/assurance.d.ts +7 -0
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +39 -3
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +20 -2
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +47 -15
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/diff.d.ts +2 -1
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +26 -2
  13. package/dist/diff.js.map +1 -1
  14. package/dist/durable-adapters.d.ts.map +1 -1
  15. package/dist/durable-adapters.js +16 -3
  16. package/dist/durable-adapters.js.map +1 -1
  17. package/dist/durable-effect.d.ts.map +1 -1
  18. package/dist/durable-effect.js +6 -3
  19. package/dist/durable-effect.js.map +1 -1
  20. package/dist/edge-kit.d.ts +2 -0
  21. package/dist/edge-kit.d.ts.map +1 -1
  22. package/dist/edge-kit.js +2 -0
  23. package/dist/edge-kit.js.map +1 -1
  24. package/dist/effect-lifecycle.d.ts.map +1 -1
  25. package/dist/effect-lifecycle.js +2 -1
  26. package/dist/effect-lifecycle.js.map +1 -1
  27. package/dist/effect-scope.d.ts.map +1 -1
  28. package/dist/effect-scope.js +1 -1
  29. package/dist/effect-scope.js.map +1 -1
  30. package/dist/errors.d.ts +1 -1
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js.map +1 -1
  33. package/dist/evidence.d.ts +24 -1
  34. package/dist/evidence.d.ts.map +1 -1
  35. package/dist/evidence.js +168 -13
  36. package/dist/evidence.js.map +1 -1
  37. package/dist/idempotency.d.ts +31 -0
  38. package/dist/idempotency.d.ts.map +1 -1
  39. package/dist/idempotency.js +40 -8
  40. package/dist/idempotency.js.map +1 -1
  41. package/dist/index.d.ts +8 -5
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/internal/code-unit-order.d.ts +7 -0
  46. package/dist/internal/code-unit-order.d.ts.map +1 -0
  47. package/dist/internal/code-unit-order.js +9 -0
  48. package/dist/internal/code-unit-order.js.map +1 -0
  49. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  50. package/dist/internal/durable-saga-protocol.js +2 -0
  51. package/dist/internal/durable-saga-protocol.js.map +1 -1
  52. package/dist/internal/route-catalog.d.ts +6 -6
  53. package/dist/internal/route-catalog.d.ts.map +1 -1
  54. package/dist/internal/route-catalog.js +20 -18
  55. package/dist/internal/route-catalog.js.map +1 -1
  56. package/dist/internal/route-compiler.d.ts +7 -6
  57. package/dist/internal/route-compiler.d.ts.map +1 -1
  58. package/dist/internal/route-compiler.js +36 -4
  59. package/dist/internal/route-compiler.js.map +1 -1
  60. package/dist/internal/route-execution.d.ts +7 -3
  61. package/dist/internal/route-execution.d.ts.map +1 -1
  62. package/dist/internal/route-execution.js.map +1 -1
  63. package/dist/internal/route-lanes.d.ts +1 -0
  64. package/dist/internal/route-lanes.d.ts.map +1 -1
  65. package/dist/internal/route-lanes.js +34 -29
  66. package/dist/internal/route-lanes.js.map +1 -1
  67. package/dist/internal/route-program.d.ts +8 -2
  68. package/dist/internal/route-program.d.ts.map +1 -1
  69. package/dist/internal/route-program.js +222 -20
  70. package/dist/internal/route-program.js.map +1 -1
  71. package/dist/ledger.d.ts.map +1 -1
  72. package/dist/ledger.js +8 -5
  73. package/dist/ledger.js.map +1 -1
  74. package/dist/manifest.d.ts.map +1 -1
  75. package/dist/manifest.js +41 -37
  76. package/dist/manifest.js.map +1 -1
  77. package/dist/mount.d.ts +36 -0
  78. package/dist/mount.d.ts.map +1 -1
  79. package/dist/mount.js +32 -0
  80. package/dist/mount.js.map +1 -1
  81. package/dist/reflection.d.ts +26 -0
  82. package/dist/reflection.d.ts.map +1 -1
  83. package/dist/reflection.js +65 -1
  84. package/dist/reflection.js.map +1 -1
  85. package/dist/response-observer.d.ts +1 -1
  86. package/dist/response-observer.d.ts.map +1 -1
  87. package/dist/response-observer.js.map +1 -1
  88. package/dist/router/overlap.d.ts +23 -0
  89. package/dist/router/overlap.d.ts.map +1 -0
  90. package/dist/router/overlap.js +224 -0
  91. package/dist/router/overlap.js.map +1 -0
  92. package/dist/router/pattern.d.ts +117 -2
  93. package/dist/router/pattern.d.ts.map +1 -1
  94. package/dist/router/pattern.js +283 -23
  95. package/dist/router/pattern.js.map +1 -1
  96. package/dist/router/router.d.ts +18 -4
  97. package/dist/router/router.d.ts.map +1 -1
  98. package/dist/router/router.js +51 -44
  99. package/dist/router/router.js.map +1 -1
  100. package/dist/server/auth.d.ts +53 -0
  101. package/dist/server/auth.d.ts.map +1 -0
  102. package/dist/server/auth.js +9 -0
  103. package/dist/server/auth.js.map +1 -0
  104. package/dist/server/bare-error-lane.d.ts +12 -7
  105. package/dist/server/bare-error-lane.d.ts.map +1 -1
  106. package/dist/server/bare-error-lane.js +13 -11
  107. package/dist/server/bare-error-lane.js.map +1 -1
  108. package/dist/server/body-lane.d.ts +20 -2
  109. package/dist/server/body-lane.d.ts.map +1 -1
  110. package/dist/server/body-lane.js +26 -7
  111. package/dist/server/body-lane.js.map +1 -1
  112. package/dist/server/body-parser.d.ts +68 -0
  113. package/dist/server/body-parser.d.ts.map +1 -0
  114. package/dist/server/body-parser.js +172 -0
  115. package/dist/server/body-parser.js.map +1 -0
  116. package/dist/server/body.d.ts.map +1 -1
  117. package/dist/server/body.js +62 -7
  118. package/dist/server/body.js.map +1 -1
  119. package/dist/server/client-ip.d.ts +8 -0
  120. package/dist/server/client-ip.d.ts.map +1 -1
  121. package/dist/server/client-ip.js +8 -0
  122. package/dist/server/client-ip.js.map +1 -1
  123. package/dist/server/context.d.ts +71 -6
  124. package/dist/server/context.d.ts.map +1 -1
  125. package/dist/server/contract.d.ts +15 -5
  126. package/dist/server/contract.d.ts.map +1 -1
  127. package/dist/server/contract.js +25 -4
  128. package/dist/server/contract.js.map +1 -1
  129. package/dist/server/core-copies.d.ts +2 -0
  130. package/dist/server/core-copies.d.ts.map +1 -0
  131. package/dist/server/core-copies.js +20 -0
  132. package/dist/server/core-copies.js.map +1 -0
  133. package/dist/server/edge.d.ts.map +1 -1
  134. package/dist/server/edge.js +2 -0
  135. package/dist/server/edge.js.map +1 -1
  136. package/dist/server/http.d.ts +11 -0
  137. package/dist/server/http.d.ts.map +1 -1
  138. package/dist/server/http.js +36 -0
  139. package/dist/server/http.js.map +1 -1
  140. package/dist/server/idempotency-lane.d.ts +6 -0
  141. package/dist/server/idempotency-lane.d.ts.map +1 -1
  142. package/dist/server/idempotency-lane.js +130 -73
  143. package/dist/server/idempotency-lane.js.map +1 -1
  144. package/dist/server/install.d.ts +4 -0
  145. package/dist/server/install.d.ts.map +1 -1
  146. package/dist/server/install.js +4 -0
  147. package/dist/server/install.js.map +1 -1
  148. package/dist/server/methods.d.ts +45 -0
  149. package/dist/server/methods.d.ts.map +1 -0
  150. package/dist/server/methods.js +37 -0
  151. package/dist/server/methods.js.map +1 -0
  152. package/dist/server/multipart.d.ts +63 -0
  153. package/dist/server/multipart.d.ts.map +1 -0
  154. package/dist/server/multipart.js +374 -0
  155. package/dist/server/multipart.js.map +1 -0
  156. package/dist/server/node-outcome-hook.d.ts +20 -0
  157. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  158. package/dist/server/node-outcome-hook.js +15 -6
  159. package/dist/server/node-outcome-hook.js.map +1 -1
  160. package/dist/server/not-found-answer.d.ts +40 -0
  161. package/dist/server/not-found-answer.d.ts.map +1 -0
  162. package/dist/server/not-found-answer.js +56 -0
  163. package/dist/server/not-found-answer.js.map +1 -0
  164. package/dist/server/not-found.d.ts +23 -0
  165. package/dist/server/not-found.d.ts.map +1 -0
  166. package/dist/server/not-found.js +81 -0
  167. package/dist/server/not-found.js.map +1 -0
  168. package/dist/server/plugin.d.ts +26 -1
  169. package/dist/server/plugin.d.ts.map +1 -1
  170. package/dist/server/plugin.js.map +1 -1
  171. package/dist/server/proto-guard.d.ts +18 -5
  172. package/dist/server/proto-guard.d.ts.map +1 -1
  173. package/dist/server/proto-guard.js +114 -9
  174. package/dist/server/proto-guard.js.map +1 -1
  175. package/dist/server/query.d.ts +7 -0
  176. package/dist/server/query.d.ts.map +1 -1
  177. package/dist/server/query.js +47 -0
  178. package/dist/server/query.js.map +1 -1
  179. package/dist/server/registry.d.ts +13 -2
  180. package/dist/server/registry.d.ts.map +1 -1
  181. package/dist/server/request-context.d.ts +11 -0
  182. package/dist/server/request-context.d.ts.map +1 -1
  183. package/dist/server/request-context.js +33 -19
  184. package/dist/server/request-context.js.map +1 -1
  185. package/dist/server/request-lineage.d.ts +8 -0
  186. package/dist/server/request-lineage.d.ts.map +1 -0
  187. package/dist/server/request-lineage.js +14 -0
  188. package/dist/server/request-lineage.js.map +1 -0
  189. package/dist/server/request-target.d.ts +31 -0
  190. package/dist/server/request-target.d.ts.map +1 -0
  191. package/dist/server/request-target.js +44 -0
  192. package/dist/server/request-target.js.map +1 -0
  193. package/dist/server/respond.d.ts +1 -1
  194. package/dist/server/respond.d.ts.map +1 -1
  195. package/dist/server/respond.js +16 -4
  196. package/dist/server/respond.js.map +1 -1
  197. package/dist/server/response-contract-lane.d.ts +19 -1
  198. package/dist/server/response-contract-lane.d.ts.map +1 -1
  199. package/dist/server/response-contract-lane.js +59 -14
  200. package/dist/server/response-contract-lane.js.map +1 -1
  201. package/dist/server/response-observer-runtime.d.ts +9 -5
  202. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  203. package/dist/server/response-observer-runtime.js +10 -6
  204. package/dist/server/response-observer-runtime.js.map +1 -1
  205. package/dist/server/runtime-core.d.ts +2 -0
  206. package/dist/server/runtime-core.d.ts.map +1 -1
  207. package/dist/server/runtime-core.js.map +1 -1
  208. package/dist/server/server-types.d.ts +53 -3
  209. package/dist/server/server-types.d.ts.map +1 -1
  210. package/dist/server/server.d.ts +191 -19
  211. package/dist/server/server.d.ts.map +1 -1
  212. package/dist/server/server.js +1206 -218
  213. package/dist/server/server.js.map +1 -1
  214. package/dist/server/sse.d.ts.map +1 -1
  215. package/dist/server/sse.js +10 -7
  216. package/dist/server/sse.js.map +1 -1
  217. package/dist/server/static-headers.d.ts +3 -0
  218. package/dist/server/static-headers.d.ts.map +1 -1
  219. package/dist/server/static-headers.js +19 -1
  220. package/dist/server/static-headers.js.map +1 -1
  221. package/dist/server/transport-lane.d.ts +8 -0
  222. package/dist/server/transport-lane.d.ts.map +1 -1
  223. package/dist/server/transport-lane.js +9 -8
  224. package/dist/server/transport-lane.js.map +1 -1
  225. package/dist/server/validation.d.ts.map +1 -1
  226. package/dist/server/validation.js +5 -1
  227. package/dist/server/validation.js.map +1 -1
  228. package/dist/server/websocket.d.ts +10 -3
  229. package/dist/server/websocket.d.ts.map +1 -1
  230. package/dist/server/websocket.js +7 -4
  231. package/dist/server/websocket.js.map +1 -1
  232. package/dist/server/ws-bun.d.ts +2 -0
  233. package/dist/server/ws-bun.d.ts.map +1 -1
  234. package/dist/server/ws-bun.js +6 -3
  235. package/dist/server/ws-bun.js.map +1 -1
  236. package/dist/server/ws-hook.d.ts +2 -1
  237. package/dist/server/ws-hook.d.ts.map +1 -1
  238. package/dist/server.d.ts +7 -4
  239. package/dist/server.d.ts.map +1 -1
  240. package/dist/server.js +4 -1
  241. package/dist/server.js.map +1 -1
  242. package/dist/single-copy-register.js +5 -0
  243. package/dist/single-copy-register.js.map +1 -1
  244. package/dist/single-copy.d.ts +46 -3
  245. package/dist/single-copy.d.ts.map +1 -1
  246. package/dist/single-copy.js +125 -12
  247. package/dist/single-copy.js.map +1 -1
  248. package/dist/tool-contract.d.ts +23 -0
  249. package/dist/tool-contract.d.ts.map +1 -1
  250. package/dist/tool-contract.js +37 -3
  251. package/dist/tool-contract.js.map +1 -1
  252. package/dist/transport-codec.d.ts.map +1 -1
  253. package/dist/transport-codec.js +2 -1
  254. package/dist/transport-codec.js.map +1 -1
  255. package/package.json +27 -2
  256. package/src/assurance.ts +52 -2
  257. package/src/capabilities.ts +81 -22
  258. package/src/diff.ts +31 -6
  259. package/src/durable-adapters.ts +20 -3
  260. package/src/durable-effect.ts +10 -3
  261. package/src/edge-kit.ts +7 -0
  262. package/src/effect-lifecycle.ts +5 -1
  263. package/src/effect-scope.ts +2 -1
  264. package/src/errors.ts +3 -0
  265. package/src/evidence.ts +237 -12
  266. package/src/idempotency.ts +72 -8
  267. package/src/index.ts +40 -3
  268. package/src/internal/code-unit-order.ts +8 -0
  269. package/src/internal/durable-saga-protocol.ts +2 -0
  270. package/src/internal/route-catalog.ts +19 -18
  271. package/src/internal/route-compiler.ts +62 -13
  272. package/src/internal/route-execution.ts +13 -3
  273. package/src/internal/route-lanes.ts +37 -28
  274. package/src/internal/route-program.ts +434 -20
  275. package/src/ledger.ts +11 -5
  276. package/src/manifest.ts +41 -36
  277. package/src/mount.ts +62 -0
  278. package/src/reflection.ts +84 -1
  279. package/src/response-observer.ts +1 -0
  280. package/src/router/overlap.ts +258 -0
  281. package/src/router/pattern.ts +349 -25
  282. package/src/router/router.ts +76 -51
  283. package/src/server/auth.ts +75 -0
  284. package/src/server/bare-error-lane.ts +12 -11
  285. package/src/server/body-lane.ts +46 -6
  286. package/src/server/body-parser.ts +241 -0
  287. package/src/server/body.ts +82 -9
  288. package/src/server/client-ip.ts +9 -0
  289. package/src/server/context.ts +219 -12
  290. package/src/server/contract.ts +54 -9
  291. package/src/server/core-copies.ts +21 -0
  292. package/src/server/edge.ts +2 -0
  293. package/src/server/http.ts +34 -0
  294. package/src/server/idempotency-lane.ts +147 -72
  295. package/src/server/install.ts +6 -0
  296. package/src/server/methods.ts +143 -0
  297. package/src/server/multipart.ts +424 -0
  298. package/src/server/node-outcome-hook.ts +40 -7
  299. package/src/server/not-found-answer.ts +109 -0
  300. package/src/server/not-found.ts +121 -0
  301. package/src/server/plugin.ts +45 -1
  302. package/src/server/proto-guard.ts +128 -9
  303. package/src/server/query.ts +49 -0
  304. package/src/server/registry.ts +17 -2
  305. package/src/server/request-context.ts +44 -16
  306. package/src/server/request-lineage.ts +15 -0
  307. package/src/server/request-target.ts +46 -0
  308. package/src/server/respond.ts +14 -3
  309. package/src/server/response-contract-lane.ts +97 -16
  310. package/src/server/response-observer-runtime.ts +37 -8
  311. package/src/server/runtime-core.ts +2 -0
  312. package/src/server/server-types.ts +59 -2
  313. package/src/server/server.ts +1613 -269
  314. package/src/server/sse.ts +10 -8
  315. package/src/server/static-headers.ts +22 -1
  316. package/src/server/transport-lane.ts +17 -9
  317. package/src/server/validation.ts +7 -1
  318. package/src/server/websocket.ts +23 -9
  319. package/src/server/ws-bun.ts +8 -3
  320. package/src/server/ws-hook.ts +2 -1
  321. package/src/server.ts +38 -2
  322. package/src/single-copy-register.ts +5 -0
  323. package/src/single-copy.ts +160 -12
  324. package/src/tool-contract.ts +66 -3
  325. package/src/transport-codec.ts +2 -1
@@ -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
- .sort(([a], [b]) => a.localeCompare(b))
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
- ...(hasMore && lastVisited !== undefined ? { cursor: lastVisited } : {}),
938
+ ...(cursor === undefined ? {} : { cursor }),
922
939
  })
923
940
  }
924
941
  async acquireLease(
@@ -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 (record.digest !== undefined && !/^[0-9a-f]{64}$/u.test(record.digest))
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 (record.errorCode !== undefined && !ERROR_CODE.test(record.errorCode))
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"
@@ -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 (input.errorCode !== undefined && !ERROR_CODE.test(input.errorCode)) return
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,
@@ -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)) throw new TypeError("effect errorCode is invalid")
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.localeCompare(b.id) || a.source.localeCompare(b.source)),
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.localeCompare(b.id) ||
179
- a.kind.localeCompare(b.kind) ||
180
- a.source.localeCompare(b.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.localeCompare(b.path) || a.method.localeCompare(b.method)),
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.localeCompare(b.path) ||
234
- a.method.localeCompare(b.method) ||
235
- a.code.localeCompare(b.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.localeCompare(b.path) ||
264
- a.method.localeCompare(b.method) ||
265
- a.code.localeCompare(b.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
- : { standard: undefined, jsonSchema: value.jsonSchema, fields: value.fields }
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)