@nifrajs/core 2.11.0 → 2.12.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 (180) hide show
  1. package/dist/assurance.d.ts +61 -3
  2. package/dist/assurance.d.ts.map +1 -1
  3. package/dist/assurance.js +152 -5
  4. package/dist/assurance.js.map +1 -1
  5. package/dist/capabilities.d.ts +9 -1
  6. package/dist/capabilities.d.ts.map +1 -1
  7. package/dist/capabilities.js +5 -1
  8. package/dist/capabilities.js.map +1 -1
  9. package/dist/diff.d.ts +2 -1
  10. package/dist/diff.d.ts.map +1 -1
  11. package/dist/diff.js +3 -1
  12. package/dist/diff.js.map +1 -1
  13. package/dist/errors.d.ts +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/evidence.d.ts +89 -0
  17. package/dist/evidence.d.ts.map +1 -0
  18. package/dist/evidence.js +170 -0
  19. package/dist/evidence.js.map +1 -0
  20. package/dist/execution-policy.d.ts +23 -0
  21. package/dist/execution-policy.d.ts.map +1 -0
  22. package/dist/execution-policy.js +28 -0
  23. package/dist/execution-policy.js.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/internal/route-assurance.d.ts +19 -0
  27. package/dist/internal/route-assurance.d.ts.map +1 -1
  28. package/dist/internal/route-assurance.js +38 -5
  29. package/dist/internal/route-assurance.js.map +1 -1
  30. package/dist/internal/route-execution.d.ts +18 -0
  31. package/dist/internal/route-execution.d.ts.map +1 -1
  32. package/dist/internal/route-execution.js +67 -1
  33. package/dist/internal/route-execution.js.map +1 -1
  34. package/dist/manifest.d.ts +5 -1
  35. package/dist/manifest.d.ts.map +1 -1
  36. package/dist/manifest.js +22 -4
  37. package/dist/manifest.js.map +1 -1
  38. package/dist/reflection.d.ts +5 -0
  39. package/dist/reflection.d.ts.map +1 -1
  40. package/dist/reflection.js +13 -4
  41. package/dist/reflection.js.map +1 -1
  42. package/dist/replay.d.ts +26 -0
  43. package/dist/replay.d.ts.map +1 -0
  44. package/dist/replay.js +63 -0
  45. package/dist/replay.js.map +1 -0
  46. package/dist/schema/standard.d.ts +2 -0
  47. package/dist/schema/standard.d.ts.map +1 -1
  48. package/dist/schema/standard.js +13 -0
  49. package/dist/schema/standard.js.map +1 -1
  50. package/dist/server/body.d.ts +49 -6
  51. package/dist/server/body.d.ts.map +1 -1
  52. package/dist/server/body.js +226 -8
  53. package/dist/server/body.js.map +1 -1
  54. package/dist/server/context.d.ts +20 -3
  55. package/dist/server/context.d.ts.map +1 -1
  56. package/dist/server/contract.d.ts +15 -3
  57. package/dist/server/contract.d.ts.map +1 -1
  58. package/dist/server/contract.js +22 -0
  59. package/dist/server/contract.js.map +1 -1
  60. package/dist/server/cookies.d.ts +26 -5
  61. package/dist/server/cookies.d.ts.map +1 -1
  62. package/dist/server/cookies.js +76 -9
  63. package/dist/server/cookies.js.map +1 -1
  64. package/dist/server/edge.d.ts.map +1 -1
  65. package/dist/server/edge.js +11 -0
  66. package/dist/server/edge.js.map +1 -1
  67. package/dist/server/header-case.d.ts +9 -0
  68. package/dist/server/header-case.d.ts.map +1 -0
  69. package/dist/server/header-case.js +54 -0
  70. package/dist/server/header-case.js.map +1 -0
  71. package/dist/server/headers.d.ts +8 -0
  72. package/dist/server/headers.d.ts.map +1 -0
  73. package/dist/server/headers.js +13 -0
  74. package/dist/server/headers.js.map +1 -0
  75. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  76. package/dist/server/node-outcome-hook.js +7 -11
  77. package/dist/server/node-outcome-hook.js.map +1 -1
  78. package/dist/server/node-outcome.d.ts +8 -1
  79. package/dist/server/node-outcome.d.ts.map +1 -1
  80. package/dist/server/node-outcome.js +10 -3
  81. package/dist/server/node-outcome.js.map +1 -1
  82. package/dist/server/plugin.d.ts +66 -7
  83. package/dist/server/plugin.d.ts.map +1 -1
  84. package/dist/server/plugin.js +14 -10
  85. package/dist/server/plugin.js.map +1 -1
  86. package/dist/server/proto-guard.d.ts +30 -0
  87. package/dist/server/proto-guard.d.ts.map +1 -0
  88. package/dist/server/proto-guard.js +116 -0
  89. package/dist/server/proto-guard.js.map +1 -0
  90. package/dist/server/registry.d.ts +6 -2
  91. package/dist/server/registry.d.ts.map +1 -1
  92. package/dist/server/request-context.d.ts +24 -6
  93. package/dist/server/request-context.d.ts.map +1 -1
  94. package/dist/server/request-context.js +157 -30
  95. package/dist/server/request-context.js.map +1 -1
  96. package/dist/server/respond.d.ts +2 -2
  97. package/dist/server/respond.d.ts.map +1 -1
  98. package/dist/server/respond.js +34 -8
  99. package/dist/server/respond.js.map +1 -1
  100. package/dist/server/response-contract-lane.d.ts +1 -1
  101. package/dist/server/response-contract-lane.d.ts.map +1 -1
  102. package/dist/server/response-contract-lane.js +45 -13
  103. package/dist/server/response-contract-lane.js.map +1 -1
  104. package/dist/server/response-hooks.d.ts +10 -0
  105. package/dist/server/response-hooks.d.ts.map +1 -0
  106. package/dist/server/response-hooks.js +52 -0
  107. package/dist/server/response-hooks.js.map +1 -0
  108. package/dist/server/runtime-core.d.ts +12 -0
  109. package/dist/server/runtime-core.d.ts.map +1 -1
  110. package/dist/server/runtime-core.js +8 -0
  111. package/dist/server/runtime-core.js.map +1 -1
  112. package/dist/server/server-types.d.ts +60 -6
  113. package/dist/server/server-types.d.ts.map +1 -1
  114. package/dist/server/server.d.ts +80 -23
  115. package/dist/server/server.d.ts.map +1 -1
  116. package/dist/server/server.js +424 -213
  117. package/dist/server/server.js.map +1 -1
  118. package/dist/server/static-headers.d.ts +14 -3
  119. package/dist/server/static-headers.d.ts.map +1 -1
  120. package/dist/server/static-headers.js +28 -6
  121. package/dist/server/static-headers.js.map +1 -1
  122. package/dist/server/transport-lane.d.ts +8 -0
  123. package/dist/server/transport-lane.d.ts.map +1 -1
  124. package/dist/server/transport-lane.js +12 -8
  125. package/dist/server/transport-lane.js.map +1 -1
  126. package/dist/server/websocket.d.ts +3 -0
  127. package/dist/server/websocket.d.ts.map +1 -1
  128. package/dist/server/websocket.js +14 -0
  129. package/dist/server/websocket.js.map +1 -1
  130. package/dist/server.d.ts +2 -2
  131. package/dist/server.d.ts.map +1 -1
  132. package/dist/server.js +2 -2
  133. package/dist/server.js.map +1 -1
  134. package/dist/tool-catalog.d.ts +21 -0
  135. package/dist/tool-catalog.d.ts.map +1 -0
  136. package/dist/tool-catalog.js +16 -0
  137. package/dist/tool-catalog.js.map +1 -0
  138. package/dist/tool-contract.d.ts +238 -0
  139. package/dist/tool-contract.d.ts.map +1 -0
  140. package/dist/tool-contract.js +569 -0
  141. package/dist/tool-contract.js.map +1 -0
  142. package/package.json +26 -1
  143. package/src/assurance.ts +236 -6
  144. package/src/capabilities.ts +14 -1
  145. package/src/diff.ts +5 -2
  146. package/src/errors.ts +1 -0
  147. package/src/evidence.ts +319 -0
  148. package/src/execution-policy.ts +57 -0
  149. package/src/index.ts +1 -1
  150. package/src/internal/route-assurance.ts +67 -4
  151. package/src/internal/route-execution.ts +276 -1
  152. package/src/manifest.ts +27 -5
  153. package/src/reflection.ts +24 -4
  154. package/src/replay.ts +96 -0
  155. package/src/schema/standard.ts +16 -0
  156. package/src/server/body.ts +275 -7
  157. package/src/server/context.ts +23 -3
  158. package/src/server/contract.ts +43 -3
  159. package/src/server/cookies.ts +80 -10
  160. package/src/server/edge.ts +10 -0
  161. package/src/server/header-case.ts +54 -0
  162. package/src/server/headers.ts +11 -0
  163. package/src/server/node-outcome-hook.ts +6 -11
  164. package/src/server/node-outcome.ts +10 -2
  165. package/src/server/plugin.ts +87 -8
  166. package/src/server/proto-guard.ts +118 -0
  167. package/src/server/registry.ts +6 -2
  168. package/src/server/request-context.ts +242 -26
  169. package/src/server/respond.ts +35 -4
  170. package/src/server/response-contract-lane.ts +6 -17
  171. package/src/server/response-hooks.ts +51 -0
  172. package/src/server/runtime-core.ts +14 -0
  173. package/src/server/server-types.ts +66 -6
  174. package/src/server/server.ts +667 -287
  175. package/src/server/static-headers.ts +27 -3
  176. package/src/server/transport-lane.ts +19 -8
  177. package/src/server/websocket.ts +18 -2
  178. package/src/server.ts +7 -0
  179. package/src/tool-catalog.ts +40 -0
  180. package/src/tool-contract.ts +896 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Canonical, token-only project evidence.
3
+ *
4
+ * This is the offline seam shared by agent views and deploy artifacts. It deliberately contains
5
+ * route contracts, assurance/capability decisions, and source locations only; validators, handlers,
6
+ * request bodies, secrets, and other runtime payloads never cross this interface.
7
+ */
8
+
9
+ import type { AssuranceEvidence, AssuranceFinding, AssuranceReport } from "./assurance.ts"
10
+ import type {
11
+ AssuredCapabilityRoute,
12
+ CapabilityAssuranceReport,
13
+ CapabilityEvidence,
14
+ CapabilityFinding,
15
+ } from "./capabilities.ts"
16
+ import type { ResponseClassification } from "./classification.ts"
17
+ import { evidenceProvenance } from "./internal/route-assurance.ts"
18
+ import {
19
+ type JsonSchema,
20
+ type ReflectedRoute,
21
+ type ReflectedSchemaField,
22
+ reflectRoutes,
23
+ type SchemaReflection,
24
+ } from "./reflection.ts"
25
+
26
+ export interface ProjectEvidenceSchemaPart {
27
+ readonly jsonSchema?: JsonSchema
28
+ readonly fields?: readonly ReflectedSchemaField[]
29
+ }
30
+
31
+ export interface ProjectEvidenceSchema {
32
+ readonly bodyLimit?: number | "unlimited"
33
+ readonly bodyLimitReason?: string
34
+ readonly headers?: ProjectEvidenceSchemaPart
35
+ readonly body?: ProjectEvidenceSchemaPart
36
+ readonly query?: ProjectEvidenceSchemaPart
37
+ readonly params?: ProjectEvidenceSchemaPart
38
+ readonly response?: ProjectEvidenceSchemaPart
39
+ readonly errors?: Readonly<Record<string, ProjectEvidenceSchemaPart>>
40
+ readonly sse?: ProjectEvidenceSchemaPart
41
+ }
42
+
43
+ export interface ProjectEvidenceSourceLocation {
44
+ readonly file: string
45
+ readonly line?: number
46
+ readonly column?: number
47
+ }
48
+
49
+ export interface ProjectEvidenceRoute {
50
+ readonly method: string
51
+ readonly path: string
52
+ readonly schema?: ProjectEvidenceSchema
53
+ readonly assurance?: readonly AssuranceEvidence[]
54
+ readonly capabilities?: readonly string[]
55
+ readonly family?: boolean
56
+ readonly classification?: ResponseClassification
57
+ readonly tool?: ReflectedRoute["tool"]
58
+ readonly source?: readonly ProjectEvidenceSourceLocation[]
59
+ }
60
+
61
+ export interface ProjectEvidenceAssuranceRoute {
62
+ readonly method: string
63
+ readonly path: string
64
+ readonly rule?: string
65
+ readonly evidence: readonly AssuranceEvidence[]
66
+ readonly missing: readonly string[]
67
+ readonly forbidden: readonly string[]
68
+ }
69
+
70
+ export interface ProjectEvidenceAssurance {
71
+ readonly ok: boolean
72
+ readonly routes: readonly ProjectEvidenceAssuranceRoute[]
73
+ readonly findings: readonly AssuranceFinding[]
74
+ }
75
+
76
+ export interface ProjectEvidenceCapabilityRoute {
77
+ readonly method: string
78
+ readonly path: string
79
+ readonly declared: readonly string[]
80
+ readonly evidence: readonly CapabilityEvidence[]
81
+ readonly unproven: readonly string[]
82
+ readonly covered: boolean
83
+ readonly classification?: AssuredCapabilityRoute["classification"]
84
+ }
85
+
86
+ export interface ProjectEvidenceCapabilities {
87
+ readonly ok: boolean
88
+ readonly routes: readonly ProjectEvidenceCapabilityRoute[]
89
+ readonly findings: readonly CapabilityFinding[]
90
+ }
91
+
92
+ /** One deterministic, persistence-safe view of a project's public and trust-relevant facts. */
93
+ export interface ProjectEvidenceSnapshot {
94
+ readonly version: 1
95
+ readonly routes: readonly ProjectEvidenceRoute[]
96
+ readonly assurance?: ProjectEvidenceAssurance
97
+ readonly capabilities?: ProjectEvidenceCapabilities
98
+ }
99
+
100
+ export interface ProjectEvidenceOptions {
101
+ readonly assurance?: AssuranceReport
102
+ readonly capabilities?: CapabilityAssuranceReport
103
+ /** Optional static source locations keyed by `${METHOD}\n${path}`. */
104
+ readonly sourceLocations?: ReadonlyMap<string, readonly ProjectEvidenceSourceLocation[]>
105
+ }
106
+
107
+ const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
108
+ value !== null && typeof value === "object" && !Array.isArray(value)
109
+ ? (value as Readonly<Record<string, unknown>>)
110
+ : undefined
111
+
112
+ function schemaPart(value: SchemaReflection | undefined): ProjectEvidenceSchemaPart | undefined {
113
+ if (value === undefined) return undefined
114
+ return Object.freeze({
115
+ ...(value.jsonSchema !== undefined ? { jsonSchema: value.jsonSchema } : {}),
116
+ ...(value.fields !== undefined ? { fields: Object.freeze([...value.fields]) } : {}),
117
+ })
118
+ }
119
+
120
+ function schemaOf(route: ReflectedRoute): ProjectEvidenceSchema | undefined {
121
+ const source = route.schema
122
+ if (source === undefined) return undefined
123
+ const headers = schemaPart(source.headers)
124
+ const body = schemaPart(source.body)
125
+ const query = schemaPart(source.query)
126
+ const params = schemaPart(source.params)
127
+ const response = schemaPart(source.response)
128
+ const sse = schemaPart(source.sse)
129
+ const errors: Record<string, ProjectEvidenceSchemaPart> = {}
130
+ for (const [status, value] of Object.entries(source.errors ?? {})) {
131
+ const part = schemaPart(value)
132
+ if (part !== undefined) errors[status] = part
133
+ }
134
+ const schema: ProjectEvidenceSchema = {
135
+ ...(source.bodyLimit !== undefined ? { bodyLimit: source.bodyLimit } : {}),
136
+ ...(source.bodyLimitReason !== undefined ? { bodyLimitReason: source.bodyLimitReason } : {}),
137
+ ...(headers !== undefined ? { headers } : {}),
138
+ ...(body !== undefined ? { body } : {}),
139
+ ...(query !== undefined ? { query } : {}),
140
+ ...(params !== undefined ? { params } : {}),
141
+ ...(response !== undefined ? { response } : {}),
142
+ ...(Object.keys(errors).length > 0 ? { errors: Object.freeze(errors) } : {}),
143
+ ...(sse !== undefined ? { sse } : {}),
144
+ }
145
+ return Object.freeze(schema)
146
+ }
147
+
148
+ const keyOf = (method: string, path: string): string => `${method.toUpperCase()}\n${path}`
149
+
150
+ function assuranceEvidenceOf(values: readonly AssuranceEvidence[]): readonly AssuranceEvidence[] {
151
+ return Object.freeze(
152
+ values
153
+ .map((item) =>
154
+ Object.freeze({
155
+ id: item.id,
156
+ source: item.source,
157
+ provenance: evidenceProvenance(item),
158
+ }),
159
+ )
160
+ .sort((a, b) => a.id.localeCompare(b.id) || a.source.localeCompare(b.source)),
161
+ )
162
+ }
163
+
164
+ function capabilityEvidenceOf(
165
+ values: readonly CapabilityEvidence[],
166
+ ): readonly CapabilityEvidence[] {
167
+ return Object.freeze(
168
+ [...values]
169
+ .map((item) => Object.freeze({ id: item.id, kind: item.kind, source: item.source }))
170
+ .sort(
171
+ (a, b) =>
172
+ a.id.localeCompare(b.id) ||
173
+ a.kind.localeCompare(b.kind) ||
174
+ a.source.localeCompare(b.source),
175
+ ),
176
+ )
177
+ }
178
+
179
+ const sortByRoute = <T extends { readonly method: string; readonly path: string }>(
180
+ values: readonly T[],
181
+ ): readonly T[] =>
182
+ Object.freeze(
183
+ [...values].sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method)),
184
+ )
185
+
186
+ function evidenceRouteOf(
187
+ route: ReflectedRoute,
188
+ locations: readonly ProjectEvidenceSourceLocation[] | undefined,
189
+ ): ProjectEvidenceRoute {
190
+ const schema = schemaOf(route)
191
+ return Object.freeze({
192
+ method: route.method.toUpperCase(),
193
+ path: route.path,
194
+ ...(schema !== undefined ? { schema } : {}),
195
+ ...(route.assurance !== undefined ? { assurance: assuranceEvidenceOf(route.assurance) } : {}),
196
+ ...(route.capabilities !== undefined
197
+ ? { capabilities: Object.freeze([...route.capabilities].sort()) }
198
+ : {}),
199
+ ...(route.family === true ? { family: true } : {}),
200
+ ...(route.classification !== undefined ? { classification: route.classification } : {}),
201
+ ...(route.tool !== undefined ? { tool: route.tool } : {}),
202
+ ...(locations !== undefined && locations.length > 0
203
+ ? { source: Object.freeze([...locations]) }
204
+ : {}),
205
+ })
206
+ }
207
+
208
+ function assuranceOf(report: AssuranceReport | undefined): ProjectEvidenceAssurance | undefined {
209
+ if (report === undefined) return undefined
210
+ return Object.freeze({
211
+ ok: report.ok,
212
+ routes: sortByRoute(
213
+ report.routes.map((route) =>
214
+ Object.freeze({
215
+ method: route.method,
216
+ path: route.path,
217
+ ...(route.rule !== undefined ? { rule: route.rule } : {}),
218
+ evidence: assuranceEvidenceOf(route.evidence),
219
+ missing: Object.freeze([...route.missing].sort()),
220
+ forbidden: Object.freeze([...route.forbidden].sort()),
221
+ }),
222
+ ),
223
+ ),
224
+ findings: Object.freeze(
225
+ [...report.findings].sort(
226
+ (a, b) =>
227
+ a.path.localeCompare(b.path) ||
228
+ a.method.localeCompare(b.method) ||
229
+ a.code.localeCompare(b.code),
230
+ ),
231
+ ),
232
+ })
233
+ }
234
+
235
+ function capabilitiesOf(
236
+ report: CapabilityAssuranceReport | undefined,
237
+ ): ProjectEvidenceCapabilities | undefined {
238
+ if (report === undefined) return undefined
239
+ return Object.freeze({
240
+ ok: report.ok,
241
+ routes: sortByRoute(
242
+ report.routes.map((route) =>
243
+ Object.freeze({
244
+ method: route.method,
245
+ path: route.path,
246
+ declared: Object.freeze([...route.declared].sort()),
247
+ evidence: capabilityEvidenceOf(route.evidence),
248
+ unproven: Object.freeze([...route.unproven].sort()),
249
+ covered: route.covered,
250
+ ...(route.classification !== undefined ? { classification: route.classification } : {}),
251
+ }),
252
+ ),
253
+ ),
254
+ findings: Object.freeze(
255
+ [...report.findings].sort(
256
+ (a, b) =>
257
+ a.path.localeCompare(b.path) ||
258
+ a.method.localeCompare(b.method) ||
259
+ a.code.localeCompare(b.code),
260
+ ),
261
+ ),
262
+ })
263
+ }
264
+
265
+ /** Build the canonical snapshot from reflection and already-evaluated offline reports. */
266
+ export function snapshotProjectEvidence(
267
+ source: unknown,
268
+ options: ProjectEvidenceOptions = {},
269
+ ): ProjectEvidenceSnapshot {
270
+ const locations = options.sourceLocations
271
+ const assurance = assuranceOf(options.assurance)
272
+ const capabilities = capabilitiesOf(options.capabilities)
273
+ const routes = sortByRoute(
274
+ // Importantly, this is the only reflection pass for this snapshot. Every projection can consume
275
+ // the result instead of independently asking the app for `.routes()` and re-shaping schemas.
276
+ reflectRoutes(source).map((route) =>
277
+ evidenceRouteOf(route, locations?.get(keyOf(route.method, route.path))),
278
+ ),
279
+ )
280
+ return Object.freeze({
281
+ version: 1,
282
+ routes,
283
+ ...(assurance !== undefined ? { assurance } : {}),
284
+ ...(capabilities !== undefined ? { capabilities } : {}),
285
+ })
286
+ }
287
+
288
+ function canonicalValue(value: unknown): string {
289
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
290
+ return JSON.stringify(value)
291
+ }
292
+ if (typeof value === "number") {
293
+ if (!Number.isFinite(value))
294
+ throw new TypeError("project evidence cannot encode non-finite numbers")
295
+ return JSON.stringify(value)
296
+ }
297
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalValue(item)).join(",")}]`
298
+ const record = recordOf(value)
299
+ if (record !== undefined) {
300
+ return `{${Object.keys(record)
301
+ .filter((key) => record[key] !== undefined)
302
+ .sort()
303
+ .map((key) => `${JSON.stringify(key)}:${canonicalValue(record[key])}`)
304
+ .join(",")}}`
305
+ }
306
+ throw new TypeError(`project evidence cannot encode ${typeof value}`)
307
+ }
308
+
309
+ /** Stable JSON for logs, MCP, generated artifacts, and snapshot tests. */
310
+ export function serializeProjectEvidence(snapshot: ProjectEvidenceSnapshot): string {
311
+ return canonicalValue(snapshot)
312
+ }
313
+
314
+ /** SHA-256 of the canonical snapshot, useful as a cheap freshness/reference token. */
315
+ export async function digestProjectEvidence(snapshot: ProjectEvidenceSnapshot): Promise<string> {
316
+ const bytes = new TextEncoder().encode(serializeProjectEvidence(snapshot))
317
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))
318
+ return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")
319
+ }
@@ -0,0 +1,57 @@
1
+ import { validCapabilityId } from "./internal/capability-runtime.ts"
2
+
3
+ /** The filesystem access a child process is expected to have. */
4
+ export type ExecutionFilesystemScope = "none" | "cwd" | "declared"
5
+
6
+ /** Network access requested by an execution policy. */
7
+ export type ExecutionNetworkAccess = "deny" | "allow"
8
+
9
+ /**
10
+ * A public, token-only execution policy. It describes a required capability; it is not an
11
+ * isolation mechanism by itself.
12
+ */
13
+ export interface ExecutionPolicy {
14
+ readonly filesystem: ExecutionFilesystemScope
15
+ readonly network: ExecutionNetworkAccess
16
+ readonly timeMs: number
17
+ readonly capabilityCeiling: readonly string[]
18
+ }
19
+
20
+ /** An adapter that can prove whether it satisfies a contract's execution policy. */
21
+ export interface ExecutionPolicyAdapter {
22
+ readonly name: string
23
+ canSatisfy(policy: ExecutionPolicy): boolean | PromiseLike<boolean>
24
+ /** Token-only limitations are suitable for evidence and diagnostics. */
25
+ limitations(policy: ExecutionPolicy): readonly string[]
26
+ }
27
+
28
+ export function defineExecutionPolicy(policy: ExecutionPolicy): ExecutionPolicy {
29
+ if (policy === null || typeof policy !== "object") {
30
+ throw new TypeError("execution policy: policy must be an object")
31
+ }
32
+ if (
33
+ policy.filesystem !== "none" &&
34
+ policy.filesystem !== "cwd" &&
35
+ policy.filesystem !== "declared"
36
+ ) {
37
+ throw new TypeError("execution policy: filesystem must be none, cwd, or declared")
38
+ }
39
+ if (policy.network !== "deny" && policy.network !== "allow") {
40
+ throw new TypeError("execution policy: network must be deny or allow")
41
+ }
42
+ if (!Number.isSafeInteger(policy.timeMs) || policy.timeMs < 1) {
43
+ throw new RangeError("execution policy: timeMs must be a positive safe integer")
44
+ }
45
+ if (
46
+ !Array.isArray(policy.capabilityCeiling) ||
47
+ policy.capabilityCeiling.some((value) => !validCapabilityId(value))
48
+ ) {
49
+ throw new TypeError("execution policy: capabilityCeiling must contain valid capabilities")
50
+ }
51
+ return Object.freeze({
52
+ filesystem: policy.filesystem,
53
+ network: policy.network,
54
+ timeMs: policy.timeMs,
55
+ capabilityCeiling: Object.freeze([...new Set(policy.capabilityCeiling)]),
56
+ })
57
+ }
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
11
11
  * `check:publish` asserts it equals `@nifrajs/core`'s package version.
12
12
  */
13
- export const VERSION = "2.11.0" as const
13
+ export const VERSION = "2.12.0" as const
14
14
 
15
15
  export type Version = typeof VERSION
16
16
 
@@ -7,6 +7,9 @@ export type AssuranceScope = "global" | "subsequent" | "plugin"
7
7
  export interface AssuranceEvidence {
8
8
  readonly id: string
9
9
  readonly source: string
10
+ /** Runtime-installed enforcement versus an author assertion on a route. Optional for legacy
11
+ * reflected descriptors; evaluators infer legacy values from source when absent. */
12
+ readonly provenance?: "runtime" | "declared"
10
13
  }
11
14
 
12
15
  /** Metadata installed on a middleware/plugin by {@link withRouteAssurance}. */
@@ -18,6 +21,16 @@ export interface AssuranceDeclaration extends AssuranceEvidence {
18
21
  readonly paths?: readonly string[]
19
22
  }
20
23
 
24
+ /**
25
+ * Evidence published from OUTSIDE the plugin chain - a deployment shell, a mount site, the call that
26
+ * hands the app to `serve`. `scope` defaults to `global` (retroactive, app-wide) because the shell
27
+ * runs after every route is registered, and `provenance` is always stamped `declared`: nifra did not
28
+ * install this enforcement and cannot see it, so a `requireProvenance: "runtime"` rule still rejects it.
29
+ */
30
+ export type AssuranceAttachment = Omit<AssuranceDeclaration, "scope" | "provenance"> & {
31
+ readonly scope?: AssuranceScope
32
+ }
33
+
21
34
  const EVIDENCE_ID = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/
22
35
  const METHOD_SET: ReadonlySet<string> = new Set(METHODS)
23
36
  const declarations = new WeakMap<object, readonly AssuranceDeclaration[]>()
@@ -31,8 +44,33 @@ export const NIFRA_ASSURANCE_IDS = Object.freeze({
31
44
  IP_RESTRICTED: "nifra.ip-restricted",
32
45
  RATE_LIMITED: "nifra.rate-limited",
33
46
  SECURITY_HEADERS: "nifra.security-headers",
47
+ RESPONSE_CONTRACT: "nifra.response-contract",
34
48
  } as const)
35
49
 
50
+ /** Create evidence with provenance stored non-enumerably so existing route descriptors remain
51
+ * byte-compatible while strict assurance policies can reject author-only assertions. */
52
+ export function evidenceWithProvenance(
53
+ id: string,
54
+ source: string,
55
+ provenance: "runtime" | "declared",
56
+ ): AssuranceEvidence {
57
+ const evidence: { id: string; source: string; provenance?: "runtime" | "declared" } = {
58
+ id,
59
+ source,
60
+ }
61
+ Object.defineProperty(evidence, "provenance", {
62
+ value: provenance,
63
+ enumerable: false,
64
+ configurable: false,
65
+ writable: false,
66
+ })
67
+ return Object.freeze(evidence)
68
+ }
69
+
70
+ export function evidenceProvenance(value: AssuranceEvidence): "runtime" | "declared" {
71
+ return value.provenance ?? (value.source === "declared" ? "declared" : "runtime")
72
+ }
73
+
36
74
  const escapeRegex = (value: string): string => value.replace(/[|\\{}()[\]^$+?.-]/g, "\\$&")
37
75
 
38
76
  /** Compile an absolute route glob. `*` is one segment; `**` is zero or more segments. */
@@ -89,26 +127,45 @@ function normalizeDeclaration(value: AssuranceDeclaration): AssuranceDeclaration
89
127
  routeGlob(path)
90
128
  return path
91
129
  })
130
+ if (
131
+ value.provenance !== undefined &&
132
+ value.provenance !== "runtime" &&
133
+ value.provenance !== "declared"
134
+ ) {
135
+ throw new Error(`route assurance: invalid provenance ${JSON.stringify(value.provenance)}`)
136
+ }
92
137
  return Object.freeze({
93
138
  id: value.id,
94
139
  source: value.source.trim(),
95
140
  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 } : {}),
96
144
  ...(methods !== undefined ? { methods: Object.freeze(methods as Method[]) } : {}),
97
145
  ...(paths !== undefined ? { paths: Object.freeze(paths) } : {}),
98
146
  })
99
147
  }
100
148
 
149
+ /** Validate and freeze declarations, one or many. */
150
+ export function normalizeAssuranceDeclarations(
151
+ declaration: AssuranceDeclaration | readonly AssuranceDeclaration[],
152
+ ): readonly AssuranceDeclaration[] {
153
+ const values: readonly AssuranceDeclaration[] = Array.isArray(declaration)
154
+ ? (declaration as readonly AssuranceDeclaration[])
155
+ : [declaration as AssuranceDeclaration]
156
+ if (values.length === 0) throw new Error("route assurance: at least one declaration is required")
157
+ return Object.freeze(values.map((value) => normalizeDeclaration(value)))
158
+ }
159
+
101
160
  /** Attach enforcement evidence to the middleware/plugin that installs it. */
102
161
  export function withRouteAssurance<T extends object>(
103
162
  target: T,
104
163
  declaration: AssuranceDeclaration | readonly AssuranceDeclaration[],
105
164
  ): T {
106
- const values = Array.isArray(declaration) ? declaration : [declaration]
107
- if (values.length === 0) throw new Error("route assurance: at least one declaration is required")
108
165
  const previous = declarations.get(target) ?? []
109
166
  declarations.set(
110
167
  target,
111
- Object.freeze([...previous, ...values.map((value) => normalizeDeclaration(value))]),
168
+ Object.freeze([...previous, ...normalizeAssuranceDeclarations(declaration)]),
112
169
  )
113
170
  return target
114
171
  }
@@ -142,7 +199,13 @@ export function assuranceEvidenceFor(
142
199
  const key = `${declaration.id}\n${declaration.source}`
143
200
  if (seen.has(key)) continue
144
201
  seen.add(key)
145
- evidence.push(Object.freeze({ id: declaration.id, source: declaration.source }))
202
+ evidence.push(
203
+ evidenceWithProvenance(
204
+ declaration.id,
205
+ declaration.source,
206
+ declaration.provenance === "declared" ? "declared" : "runtime",
207
+ ),
208
+ )
146
209
  }
147
210
  return Object.freeze(evidence)
148
211
  }