@nifrajs/core 4.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +6 -2
  2. package/dist/assurance.d.ts +86 -6
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +186 -95
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +8 -0
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +53 -1
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/durable-effect.js +1 -1
  11. package/dist/durable-effect.js.map +1 -1
  12. package/dist/durable-saga.d.ts +6 -0
  13. package/dist/durable-saga.d.ts.map +1 -1
  14. package/dist/durable-saga.js +83 -10
  15. package/dist/durable-saga.js.map +1 -1
  16. package/dist/edge-kit.d.ts +8 -3
  17. package/dist/edge-kit.d.ts.map +1 -1
  18. package/dist/edge-kit.js +7 -2
  19. package/dist/edge-kit.js.map +1 -1
  20. package/dist/effect-scope.d.ts.map +1 -1
  21. package/dist/effect-scope.js +16 -5
  22. package/dist/effect-scope.js.map +1 -1
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js.map +1 -1
  26. package/dist/evidence.d.ts +2 -0
  27. package/dist/evidence.d.ts.map +1 -1
  28. package/dist/evidence.js +9 -1
  29. package/dist/evidence.js.map +1 -1
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal/durable-shared.d.ts +1 -2
  35. package/dist/internal/durable-shared.d.ts.map +1 -1
  36. package/dist/internal/durable-shared.js +25 -44
  37. package/dist/internal/durable-shared.js.map +1 -1
  38. package/dist/internal/durable-types.d.ts +3 -0
  39. package/dist/internal/durable-types.d.ts.map +1 -1
  40. package/dist/internal/hook-origins.d.ts +67 -0
  41. package/dist/internal/hook-origins.d.ts.map +1 -0
  42. package/dist/internal/hook-origins.js +86 -0
  43. package/dist/internal/hook-origins.js.map +1 -0
  44. package/dist/internal/route-assurance.d.ts +6 -3
  45. package/dist/internal/route-assurance.d.ts.map +1 -1
  46. package/dist/internal/route-assurance.js +29 -14
  47. package/dist/internal/route-assurance.js.map +1 -1
  48. package/dist/internal/route-compiler.d.ts +9 -0
  49. package/dist/internal/route-compiler.d.ts.map +1 -1
  50. package/dist/internal/route-compiler.js +95 -13
  51. package/dist/internal/route-compiler.js.map +1 -1
  52. package/dist/internal/route-program.d.ts +1 -1
  53. package/dist/internal/route-program.d.ts.map +1 -1
  54. package/dist/internal/route-program.js +59 -16
  55. package/dist/internal/route-program.js.map +1 -1
  56. package/dist/internal/same-origin.d.ts +11 -1
  57. package/dist/internal/same-origin.d.ts.map +1 -1
  58. package/dist/internal/same-origin.js +8 -0
  59. package/dist/internal/same-origin.js.map +1 -1
  60. package/dist/reconciliation-worker.d.ts.map +1 -1
  61. package/dist/reconciliation-worker.js +12 -2
  62. package/dist/reconciliation-worker.js.map +1 -1
  63. package/dist/reflection.d.ts +53 -2
  64. package/dist/reflection.d.ts.map +1 -1
  65. package/dist/reflection.js +352 -5
  66. package/dist/reflection.js.map +1 -1
  67. package/dist/router/pattern.d.ts +1 -1
  68. package/dist/router/pattern.d.ts.map +1 -1
  69. package/dist/router/pattern.js +6 -3
  70. package/dist/router/pattern.js.map +1 -1
  71. package/dist/security/baseline.d.ts +3 -0
  72. package/dist/security/baseline.d.ts.map +1 -1
  73. package/dist/security/baseline.js +9 -5
  74. package/dist/security/baseline.js.map +1 -1
  75. package/dist/server/auth.d.ts +32 -3
  76. package/dist/server/auth.d.ts.map +1 -1
  77. package/dist/server/auth.js +23 -0
  78. package/dist/server/auth.js.map +1 -1
  79. package/dist/server/body-lane.d.ts +8 -2
  80. package/dist/server/body-lane.d.ts.map +1 -1
  81. package/dist/server/body-lane.js +27 -2
  82. package/dist/server/body-lane.js.map +1 -1
  83. package/dist/server/body.d.ts +7 -0
  84. package/dist/server/body.d.ts.map +1 -1
  85. package/dist/server/body.js +34 -1
  86. package/dist/server/body.js.map +1 -1
  87. package/dist/server/client-ip.d.ts +3 -0
  88. package/dist/server/client-ip.d.ts.map +1 -1
  89. package/dist/server/client-ip.js +6 -5
  90. package/dist/server/client-ip.js.map +1 -1
  91. package/dist/server/context.d.ts +2 -1
  92. package/dist/server/context.d.ts.map +1 -1
  93. package/dist/server/cookies.d.ts.map +1 -1
  94. package/dist/server/cookies.js +13 -1
  95. package/dist/server/cookies.js.map +1 -1
  96. package/dist/server/edge.d.ts.map +1 -1
  97. package/dist/server/edge.js +2 -1
  98. package/dist/server/edge.js.map +1 -1
  99. package/dist/server/http.d.ts.map +1 -1
  100. package/dist/server/http.js +5 -1
  101. package/dist/server/http.js.map +1 -1
  102. package/dist/server/idempotency-lane.d.ts +6 -3
  103. package/dist/server/idempotency-lane.d.ts.map +1 -1
  104. package/dist/server/idempotency-lane.js +38 -17
  105. package/dist/server/idempotency-lane.js.map +1 -1
  106. package/dist/server/install.d.ts +2 -0
  107. package/dist/server/install.d.ts.map +1 -1
  108. package/dist/server/install.js +2 -0
  109. package/dist/server/install.js.map +1 -1
  110. package/dist/server/logger.d.ts.map +1 -1
  111. package/dist/server/logger.js +3 -1
  112. package/dist/server/logger.js.map +1 -1
  113. package/dist/server/methods.d.ts +1 -0
  114. package/dist/server/methods.d.ts.map +1 -1
  115. package/dist/server/methods.js +1 -0
  116. package/dist/server/methods.js.map +1 -1
  117. package/dist/server/node-outcome.d.ts.map +1 -1
  118. package/dist/server/node-outcome.js +29 -11
  119. package/dist/server/node-outcome.js.map +1 -1
  120. package/dist/server/platform-adapters.d.ts.map +1 -1
  121. package/dist/server/platform-adapters.js +3 -2
  122. package/dist/server/platform-adapters.js.map +1 -1
  123. package/dist/server/request-context.d.ts +2 -1
  124. package/dist/server/request-context.d.ts.map +1 -1
  125. package/dist/server/request-context.js +4 -1
  126. package/dist/server/request-context.js.map +1 -1
  127. package/dist/server/respond.d.ts +44 -11
  128. package/dist/server/respond.d.ts.map +1 -1
  129. package/dist/server/respond.js +192 -79
  130. package/dist/server/respond.js.map +1 -1
  131. package/dist/server/response-contract-lane.d.ts +21 -6
  132. package/dist/server/response-contract-lane.d.ts.map +1 -1
  133. package/dist/server/response-contract-lane.js +139 -14
  134. package/dist/server/response-contract-lane.js.map +1 -1
  135. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  136. package/dist/server/response-observer-runtime.js +10 -41
  137. package/dist/server/response-observer-runtime.js.map +1 -1
  138. package/dist/server/runtime-core.d.ts +2 -0
  139. package/dist/server/runtime-core.d.ts.map +1 -1
  140. package/dist/server/runtime-core.js +2 -0
  141. package/dist/server/runtime-core.js.map +1 -1
  142. package/dist/server/server-types.d.ts +45 -10
  143. package/dist/server/server-types.d.ts.map +1 -1
  144. package/dist/server/server.d.ts +67 -32
  145. package/dist/server/server.d.ts.map +1 -1
  146. package/dist/server/server.js +481 -334
  147. package/dist/server/server.js.map +1 -1
  148. package/dist/server/sse.d.ts +11 -1
  149. package/dist/server/sse.d.ts.map +1 -1
  150. package/dist/server/sse.js +18 -3
  151. package/dist/server/sse.js.map +1 -1
  152. package/dist/server/transport-lane.d.ts +7 -1
  153. package/dist/server/transport-lane.d.ts.map +1 -1
  154. package/dist/server/transport-lane.js.map +1 -1
  155. package/dist/server/webhook.d.ts +5 -2
  156. package/dist/server/webhook.d.ts.map +1 -1
  157. package/dist/server/webhook.js +17 -1
  158. package/dist/server/webhook.js.map +1 -1
  159. package/dist/server/websocket.d.ts +33 -14
  160. package/dist/server/websocket.d.ts.map +1 -1
  161. package/dist/server/websocket.js +47 -12
  162. package/dist/server/websocket.js.map +1 -1
  163. package/dist/server/ws-bun.d.ts +7 -1
  164. package/dist/server/ws-bun.d.ts.map +1 -1
  165. package/dist/server/ws-bun.js +42 -2
  166. package/dist/server/ws-bun.js.map +1 -1
  167. package/dist/server/ws-hook.d.ts +15 -3
  168. package/dist/server/ws-hook.d.ts.map +1 -1
  169. package/dist/server.d.ts +4 -3
  170. package/dist/server.d.ts.map +1 -1
  171. package/dist/server.js +3 -2
  172. package/dist/server.js.map +1 -1
  173. package/dist/single-copy.d.ts +6 -0
  174. package/dist/single-copy.d.ts.map +1 -1
  175. package/dist/single-copy.js +55 -6
  176. package/dist/single-copy.js.map +1 -1
  177. package/dist/tool-contract.d.ts.map +1 -1
  178. package/dist/tool-contract.js +71 -40
  179. package/dist/tool-contract.js.map +1 -1
  180. package/dist/wire.d.ts +5 -0
  181. package/dist/wire.d.ts.map +1 -1
  182. package/dist/wire.js +5 -0
  183. package/dist/wire.js.map +1 -1
  184. package/dist/ws.d.ts.map +1 -1
  185. package/dist/ws.js +4 -1
  186. package/dist/ws.js.map +1 -1
  187. package/package.json +1 -1
  188. package/src/assurance.ts +299 -111
  189. package/src/capabilities.ts +62 -1
  190. package/src/durable-effect.ts +1 -1
  191. package/src/durable-saga.ts +94 -10
  192. package/src/edge-kit.ts +8 -3
  193. package/src/effect-scope.ts +15 -5
  194. package/src/errors.ts +1 -0
  195. package/src/evidence.ts +12 -3
  196. package/src/index.ts +7 -2
  197. package/src/internal/durable-shared.ts +25 -37
  198. package/src/internal/durable-types.ts +3 -0
  199. package/src/internal/hook-origins.ts +153 -0
  200. package/src/internal/route-assurance.ts +32 -18
  201. package/src/internal/route-compiler.ts +107 -16
  202. package/src/internal/route-program.ts +98 -44
  203. package/src/internal/same-origin.ts +14 -1
  204. package/src/reconciliation-worker.ts +10 -2
  205. package/src/reflection.ts +455 -4
  206. package/src/router/pattern.ts +6 -3
  207. package/src/security/baseline.ts +10 -5
  208. package/src/server/auth.ts +45 -3
  209. package/src/server/body-lane.ts +36 -1
  210. package/src/server/body.ts +34 -1
  211. package/src/server/client-ip.ts +8 -5
  212. package/src/server/context.ts +2 -1
  213. package/src/server/cookies.ts +12 -1
  214. package/src/server/edge.ts +2 -1
  215. package/src/server/http.ts +7 -1
  216. package/src/server/idempotency-lane.ts +64 -22
  217. package/src/server/install.ts +3 -0
  218. package/src/server/logger.ts +3 -1
  219. package/src/server/methods.ts +1 -0
  220. package/src/server/node-outcome.ts +34 -12
  221. package/src/server/platform-adapters.ts +3 -2
  222. package/src/server/request-context.ts +5 -0
  223. package/src/server/respond.ts +222 -69
  224. package/src/server/response-contract-lane.ts +162 -10
  225. package/src/server/response-observer-runtime.ts +16 -38
  226. package/src/server/runtime-core.ts +2 -0
  227. package/src/server/server-types.ts +45 -10
  228. package/src/server/server.ts +707 -539
  229. package/src/server/sse.ts +84 -55
  230. package/src/server/transport-lane.ts +7 -1
  231. package/src/server/webhook.ts +22 -3
  232. package/src/server/websocket.ts +83 -18
  233. package/src/server/ws-bun.ts +48 -1
  234. package/src/server/ws-hook.ts +14 -2
  235. package/src/server.ts +6 -1
  236. package/src/single-copy.ts +55 -5
  237. package/src/tool-contract.ts +67 -39
  238. package/src/wire.ts +14 -0
  239. package/src/ws.ts +6 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Where each registered hook came from, for `reflectHooks` (`nifra routes --hooks`). Development-only:
3
+ * every write sits behind the inline `process.env.NODE_ENV !== "production"` guard at its call site in
4
+ * `server.ts`, so a production define folds the writes away and this module drops out of the bundle.
5
+ * Reads happen only from reflection, never on the request path.
6
+ */
7
+
8
+ export type HookKind =
9
+ | "onRequest"
10
+ | "around"
11
+ | "authenticate"
12
+ | "derive"
13
+ | "beforeHandle"
14
+ | "afterHandle"
15
+ | "onError"
16
+ | "onResponse"
17
+ | "responseHeaders"
18
+ | "onResponseFinalized"
19
+
20
+ export interface HookOrigin {
21
+ readonly kind: HookKind
22
+ /** The server the hook was registered on. */
23
+ readonly owner: object
24
+ /** The plugin being applied when it was registered, if any. */
25
+ readonly plugin: string | undefined
26
+ /** A group's static headers turned into this hook at adoption: report the owner's declarations. */
27
+ readonly statics?: true
28
+ }
29
+
30
+ /** Which requests an adopted hook still runs for: under a group prefix, or a merged server's routes. */
31
+ export type HookScopeTag =
32
+ | { readonly inner: unknown; readonly prefix: string }
33
+ | {
34
+ readonly inner: unknown
35
+ readonly routes: readonly { readonly method: string; readonly path: string }[]
36
+ }
37
+
38
+ /** One `responseHeaders()` declaration that reached a server's static tier. */
39
+ export interface StaticHeadersTag {
40
+ readonly owner: object
41
+ readonly names: readonly string[]
42
+ readonly source: unknown
43
+ readonly plugin: string | undefined
44
+ }
45
+
46
+ export interface AdoptionTag {
47
+ readonly source: object
48
+ readonly prefix: string | undefined
49
+ }
50
+
51
+ const origins = new WeakMap<object, HookOrigin>()
52
+ const scopes = new WeakMap<object, HookScopeTag>()
53
+ const sources = new WeakMap<object, unknown>()
54
+ const statics = new WeakMap<object, StaticHeadersTag[]>()
55
+ const adoptions = new WeakMap<object, AdoptionTag[]>()
56
+ let applying: string | undefined
57
+ // A merged server whose static headers are about to become one of the target's response hooks.
58
+ let staticsOwner: object | undefined
59
+
60
+ const isRef = (value: unknown): value is object =>
61
+ (typeof value === "function" || typeof value === "object") && value !== null
62
+
63
+ /** Record a hook's origin. The first registration wins, so a hook re-adopted elsewhere keeps its site. */
64
+ export function tagHook(hook: unknown, kind: HookKind, owner: object, groupStatics?: true): void {
65
+ if (!isRef(hook) || origins.has(hook)) return
66
+ if (kind === "responseHeaders" && staticsOwner !== undefined) {
67
+ origins.set(hook, { kind, owner: staticsOwner, plugin: applying, statics: true })
68
+ staticsOwner = undefined
69
+ return
70
+ }
71
+ origins.set(
72
+ hook,
73
+ groupStatics === true
74
+ ? { kind, owner, plugin: applying, statics: true }
75
+ : { kind, owner, plugin: applying },
76
+ )
77
+ }
78
+
79
+ /**
80
+ * A merge folds the source's static headers into the target: into its static tier, or - once the
81
+ * target has a response hook - into a new hook, which the next `responseHeaders` tag then is.
82
+ */
83
+ export function adoptStaticTags(target: object, source: object, becomesHook: boolean): void {
84
+ if (becomesHook) {
85
+ staticsOwner = source
86
+ return
87
+ }
88
+ for (const tag of statics.get(source) ?? []) {
89
+ tagStatic(target, tag.names, tag.source, tag.owner, tag.plugin)
90
+ }
91
+ }
92
+
93
+ /** The author's function (or header record) an internal wrapper adapts, for naming it. */
94
+ export function tagSource(wrapper: unknown, source: unknown): void {
95
+ if (isRef(wrapper)) sources.set(wrapper, source)
96
+ }
97
+
98
+ /** Pair each scoped wrapper with the hook it gates and the scope it gates it by. */
99
+ export function tagScoped(
100
+ originals: readonly unknown[],
101
+ wrapped: readonly unknown[],
102
+ scope:
103
+ | { readonly prefix: string }
104
+ | { readonly routes: readonly { readonly method: string; readonly path: string }[] },
105
+ ): void {
106
+ for (let i = 0; i < wrapped.length; i++) {
107
+ const wrapper = wrapped[i]
108
+ if (isRef(wrapper) && wrapper !== originals[i])
109
+ scopes.set(wrapper, { inner: originals[i], ...scope })
110
+ }
111
+ }
112
+
113
+ export function tagStatic(
114
+ server: object,
115
+ names: readonly string[],
116
+ source: unknown,
117
+ owner: object = server,
118
+ plugin: string | undefined = applying,
119
+ ): void {
120
+ const list = statics.get(server)
121
+ const tag = { owner, names: names.map((name) => name.toLowerCase()), source, plugin }
122
+ if (list === undefined) statics.set(server, [tag])
123
+ else list.push(tag)
124
+ }
125
+
126
+ export function tagAdoption(target: object, source: object, prefix: string | undefined): void {
127
+ const list = adoptions.get(target)
128
+ const tag = { source, prefix }
129
+ if (list === undefined) adoptions.set(target, [tag])
130
+ else list.push(tag)
131
+ }
132
+
133
+ const outer: (string | undefined)[] = []
134
+
135
+ /** Mark the plugin a `use()` is applying, until the matching {@link leavePlugin}. */
136
+ export function enterPlugin(name: string | undefined): void {
137
+ outer.push(applying)
138
+ if (name !== undefined) applying = name
139
+ }
140
+
141
+ export function leavePlugin(): void {
142
+ applying = outer.pop()
143
+ }
144
+
145
+ export const hookOriginOf = (hook: unknown): HookOrigin | undefined =>
146
+ isRef(hook) ? origins.get(hook) : undefined
147
+ export const hookScopeOf = (hook: unknown): HookScopeTag | undefined =>
148
+ isRef(hook) ? scopes.get(hook) : undefined
149
+ export const hookSourceOf = (hook: unknown): unknown =>
150
+ isRef(hook) ? sources.get(hook) : undefined
151
+ export const staticHeadersOf = (server: object): readonly StaticHeadersTag[] =>
152
+ statics.get(server) ?? []
153
+ export const adoptionsOf = (server: object): readonly AdoptionTag[] => adoptions.get(server) ?? []
@@ -1,4 +1,4 @@
1
- import { METHODS, type Method } from "../router/router.ts"
1
+ import { isRegistrableMethod, METHODS, type Method, type RouteMethod } from "../router/router.ts"
2
2
 
3
3
  /** Where enforcement evidence follows Nifra's route-registration semantics. */
4
4
  export type AssuranceScope = "global" | "subsequent" | "plugin"
@@ -15,8 +15,11 @@ export interface AssuranceEvidence {
15
15
  /** Metadata installed on a middleware/plugin by {@link withRouteAssurance}. */
16
16
  export interface AssuranceDeclaration extends AssuranceEvidence {
17
17
  readonly scope: AssuranceScope
18
- /** Restrict evidence to these HTTP methods. Omit for every method. */
19
- readonly methods?: readonly Method[]
18
+ /** Restrict evidence to these HTTP methods, a custom one such as `PURGE` included. Omit for every
19
+ * method. */
20
+ readonly methods?: readonly RouteMethod[]
21
+ /** Withhold evidence from these HTTP methods. Every other method keeps it, custom ones included. */
22
+ readonly exceptMethods?: readonly RouteMethod[]
20
23
  /** Restrict evidence to these absolute route globs. Omit for every path. */
21
24
  readonly paths?: readonly string[]
22
25
  }
@@ -117,12 +120,8 @@ function normalizeDeclaration(value: AssuranceDeclaration): AssuranceDeclaration
117
120
  if (value.scope !== "global" && value.scope !== "subsequent" && value.scope !== "plugin") {
118
121
  throw new Error(`route assurance: invalid scope ${JSON.stringify(value.scope)}`)
119
122
  }
120
- const methods = value.methods?.map((method) => method.toUpperCase())
121
- for (const method of methods ?? []) {
122
- if (!METHOD_SET.has(method)) {
123
- throw new Error(`route assurance: unsupported HTTP method ${JSON.stringify(method)}`)
124
- }
125
- }
123
+ const methods = registrableMethods(value.methods)
124
+ const exceptMethods = registrableMethods(value.exceptMethods)
126
125
  const paths = value.paths?.map((path) => {
127
126
  routeGlob(path)
128
127
  return path
@@ -134,16 +133,31 @@ function normalizeDeclaration(value: AssuranceDeclaration): AssuranceDeclaration
134
133
  ) {
135
134
  throw new Error(`route assurance: invalid provenance ${JSON.stringify(value.provenance)}`)
136
135
  }
137
- return Object.freeze({
136
+ const declaration: { -readonly [K in keyof AssuranceDeclaration]: AssuranceDeclaration[K] } = {
138
137
  id: value.id,
139
138
  source: value.source.trim(),
140
139
  scope: value.scope,
141
- // Carried, not defaulted: evidence attached by an author (rather than installed by a running
142
- // hook) must stay distinguishable so `requireProvenance: "runtime"` rules can still reject it.
143
- ...(value.provenance !== undefined ? { provenance: value.provenance } : {}),
144
- ...(methods !== undefined ? { methods: Object.freeze(methods as Method[]) } : {}),
145
- ...(paths !== undefined ? { paths: Object.freeze(paths) } : {}),
146
- })
140
+ }
141
+ // Carried, not defaulted: evidence attached by an author (rather than installed by a running
142
+ // hook) must stay distinguishable so `requireProvenance: "runtime"` rules can still reject it.
143
+ if (value.provenance !== undefined) declaration.provenance = value.provenance
144
+ if (methods !== undefined) declaration.methods = methods
145
+ if (exceptMethods !== undefined) declaration.exceptMethods = exceptMethods
146
+ if (paths !== undefined) declaration.paths = Object.freeze(paths)
147
+ return Object.freeze(declaration)
148
+ }
149
+
150
+ function registrableMethods(
151
+ values: readonly string[] | undefined,
152
+ ): readonly RouteMethod[] | undefined {
153
+ if (values === undefined) return undefined
154
+ const methods = values.map((method) => method.toUpperCase())
155
+ for (const method of methods) {
156
+ if (!isRegistrableMethod(method)) {
157
+ throw new Error(`route assurance: unsupported HTTP method ${JSON.stringify(method)}`)
158
+ }
159
+ }
160
+ return Object.freeze(methods)
147
161
  }
148
162
 
149
163
  /** Validate and freeze declarations, one or many. */
@@ -179,8 +193,8 @@ export function declarationApplies(
179
193
  method: string,
180
194
  path: string,
181
195
  ): boolean {
182
- if (declaration.methods !== undefined && !declaration.methods.includes(method as Method))
183
- return false
196
+ if (declaration.methods !== undefined && !declaration.methods.includes(method)) return false
197
+ if (declaration.exceptMethods?.includes(method) === true) return false
184
198
  return (
185
199
  declaration.paths === undefined ||
186
200
  declaration.paths.some((pattern) => routeGlob(pattern).test(path))
@@ -35,6 +35,7 @@ import {
35
35
  NIFRA_ASSURANCE_IDS,
36
36
  validEvidenceId,
37
37
  } from "./route-assurance.ts"
38
+ import type { CatalogRoute } from "./route-catalog.ts"
38
39
  import type {
39
40
  RawAfterHandle,
40
41
  RawAround,
@@ -43,7 +44,9 @@ import type {
43
44
  RawDerive,
44
45
  RawErrorHandler,
45
46
  } from "./route-execution.ts"
47
+ import { compileRouteExecutionPlan } from "./route-execution.ts"
46
48
  import { type RouteLaneSelection, selectRouteLanes } from "./route-lanes.ts"
49
+ import { compileRouteProgram } from "./route-program.ts"
47
50
 
48
51
  export interface RouteCompilerContext {
49
52
  readonly maxBodyBytes: number
@@ -83,6 +86,108 @@ export interface CompiledRouteOptions {
83
86
  readonly authenticated: boolean
84
87
  }
85
88
 
89
+ const RESPONSE_CONTRACT_EVIDENCE: AssuranceDeclaration = Object.freeze({
90
+ id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
91
+ source: "response-contract",
92
+ scope: "plugin",
93
+ })
94
+
95
+ function responseDefinitionOf(
96
+ schema: RouteSchema | undefined,
97
+ ): ResponseContractDefinition | undefined {
98
+ return schema?.response !== undefined || schema?.errors !== undefined
99
+ ? {
100
+ ...(schema.response === undefined ? {} : { response: schema.response }),
101
+ ...(schema.errors === undefined ? {} : { errors: schema.errors }),
102
+ }
103
+ : undefined
104
+ }
105
+
106
+ /**
107
+ * Recompile a route another server compiled, under the response contract and effect ledger the
108
+ * adopting server installed and the route's own server did not. Both only add a check or a record,
109
+ * so a merged route can never serve more than it did standalone - and without this, the same
110
+ * `.use()` covered a `group()` route but not a `merge()`d one. A route that already resolved its
111
+ * own contract or ledger keeps it.
112
+ */
113
+ export function adoptRouteRuntimes(
114
+ route: CatalogRoute,
115
+ responseContractRuntime: ResponseContractRuntime | undefined,
116
+ effectLedgerRuntime: EffectLedgerRuntime | undefined,
117
+ ): CatalogRoute {
118
+ const { entry, descriptor } = route
119
+ const definition =
120
+ entry.responseContract === undefined && responseContractRuntime !== undefined
121
+ ? responseDefinitionOf(entry.schema)
122
+ : undefined
123
+ const ledgered =
124
+ entry.ledgered ??
125
+ effectLedgerRuntime?.resolve(descriptor.capabilities ?? [], route.method, route.path)
126
+ if (definition === undefined && ledgered === entry.ledgered) return route
127
+ const responseContract =
128
+ definition === undefined || responseContractRuntime === undefined
129
+ ? entry.responseContract
130
+ : { runtime: responseContractRuntime, definition }
131
+ // Either runtime rules out every fused lane, so the route needs only the generic plan here - and
132
+ // nothing the source server's validation-error default decided about fusion can matter any more.
133
+ const lanes = selectRouteLanes({
134
+ schema: entry.schema,
135
+ hasAuth: entry.authStages.length > 0,
136
+ hasIdempotency: entry.idempotent !== undefined,
137
+ hasLedger: ledgered !== undefined,
138
+ hasResponseContract: responseContract !== undefined,
139
+ hasDecorations: entry.hasDecorations,
140
+ derives: entry.derives.length,
141
+ beforeHandle: entry.beforeHandle.length,
142
+ afterHandle: entry.afterHandle.length,
143
+ onError: entry.onError.length,
144
+ around: entry.around.length,
145
+ defaultOnValidationError: false,
146
+ })
147
+ const program = compileRouteProgram({
148
+ schema: entry.schema,
149
+ handler: entry.handler,
150
+ derives: entry.derives,
151
+ authStages: entry.authStages,
152
+ beforeHandle: entry.beforeHandle,
153
+ afterHandle: entry.afterHandle,
154
+ onError: entry.onError,
155
+ decorations: entry.decorations,
156
+ hasDecorations: entry.hasDecorations,
157
+ bodySchema: entry.schema?.body,
158
+ bodyLimit: entry.bodyLimit,
159
+ responseContract,
160
+ })
161
+ const execution = compileRouteExecutionPlan({
162
+ lane: lanes.lane,
163
+ contextless: false,
164
+ hasAround: entry.around.length > 0,
165
+ hasLedger: ledgered !== undefined,
166
+ fusedWeb: undefined,
167
+ fusedBody: undefined,
168
+ fusedLane: undefined,
169
+ })
170
+ const enforced = definition !== undefined && responseContractRuntime?.mode === "enforce"
171
+ return {
172
+ ...route,
173
+ entry: {
174
+ ...entry,
175
+ ledgered,
176
+ responseContract,
177
+ lifecycleHookLane: undefined,
178
+ execution,
179
+ program,
180
+ },
181
+ descriptor:
182
+ definition === undefined || responseContractRuntime === undefined
183
+ ? descriptor
184
+ : { ...descriptor, responseContract: responseContractRuntime.mode },
185
+ assurance: enforced
186
+ ? Object.freeze([...route.assurance, RESPONSE_CONTRACT_EVIDENCE])
187
+ : route.assurance,
188
+ }
189
+ }
190
+
86
191
  /** Resolve all registration-time policy and lane facts for one route. */
87
192
  export function compileRouteOptions(
88
193
  context: RouteCompilerContext,
@@ -181,13 +286,7 @@ export function compileRouteOptions(
181
286
  context.maxBodyBytes,
182
287
  )
183
288
  const ledgered = context.effectLedgerRuntime?.resolve(capabilities, method, path)
184
- const responseDefinition =
185
- schema?.response !== undefined || schema?.errors !== undefined
186
- ? {
187
- ...(schema.response === undefined ? {} : { response: schema.response }),
188
- ...(schema.errors === undefined ? {} : { errors: schema.errors }),
189
- }
190
- : undefined
289
+ const responseDefinition = responseDefinitionOf(schema)
191
290
  const contracted =
192
291
  context.responseContractRuntime !== undefined && responseDefinition !== undefined
193
292
  ? { runtime: context.responseContractRuntime, definition: responseDefinition }
@@ -208,15 +307,7 @@ export function compileRouteOptions(
208
307
  })
209
308
 
210
309
  const routeAssurance: AssuranceDeclaration[] = [...context.activeAssurance, ...handlerAssurance]
211
- if (contracted?.runtime.mode === "enforce") {
212
- routeAssurance.push(
213
- Object.freeze({
214
- id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
215
- source: "response-contract",
216
- scope: "plugin",
217
- }),
218
- )
219
- }
310
+ if (contracted?.runtime.mode === "enforce") routeAssurance.push(RESPONSE_CONTRACT_EVIDENCE)
220
311
  for (const id of schema?.assurance ?? []) {
221
312
  if (!validEvidenceId(id)) {
222
313
  throw new Error(