@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.
- package/dist/assurance.d.ts +61 -3
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +152 -5
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +9 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +5 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/diff.d.ts +2 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +3 -1
- package/dist/diff.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +89 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +170 -0
- package/dist/evidence.js.map +1 -0
- package/dist/execution-policy.d.ts +23 -0
- package/dist/execution-policy.d.ts.map +1 -0
- package/dist/execution-policy.js +28 -0
- package/dist/execution-policy.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/route-assurance.d.ts +19 -0
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +38 -5
- package/dist/internal/route-assurance.js.map +1 -1
- package/dist/internal/route-execution.d.ts +18 -0
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +67 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/manifest.d.ts +5 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +22 -4
- package/dist/manifest.js.map +1 -1
- package/dist/reflection.d.ts +5 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +13 -4
- package/dist/reflection.js.map +1 -1
- package/dist/replay.d.ts +26 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +63 -0
- package/dist/replay.js.map +1 -0
- package/dist/schema/standard.d.ts +2 -0
- package/dist/schema/standard.d.ts.map +1 -1
- package/dist/schema/standard.js +13 -0
- package/dist/schema/standard.js.map +1 -1
- package/dist/server/body.d.ts +49 -6
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +226 -8
- package/dist/server/body.js.map +1 -1
- package/dist/server/context.d.ts +20 -3
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +15 -3
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +22 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts +26 -5
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +76 -9
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +11 -0
- package/dist/server/edge.js.map +1 -1
- package/dist/server/header-case.d.ts +9 -0
- package/dist/server/header-case.d.ts.map +1 -0
- package/dist/server/header-case.js +54 -0
- package/dist/server/header-case.js.map +1 -0
- package/dist/server/headers.d.ts +8 -0
- package/dist/server/headers.d.ts.map +1 -0
- package/dist/server/headers.js +13 -0
- package/dist/server/headers.js.map +1 -0
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js +7 -11
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +8 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +10 -3
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/plugin.d.ts +66 -7
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js +14 -10
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/proto-guard.d.ts +30 -0
- package/dist/server/proto-guard.d.ts.map +1 -0
- package/dist/server/proto-guard.js +116 -0
- package/dist/server/proto-guard.js.map +1 -0
- package/dist/server/registry.d.ts +6 -2
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +24 -6
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +157 -30
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +2 -2
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +34 -8
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +45 -13
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-hooks.d.ts +10 -0
- package/dist/server/response-hooks.d.ts.map +1 -0
- package/dist/server/response-hooks.js +52 -0
- package/dist/server/response-hooks.js.map +1 -0
- package/dist/server/runtime-core.d.ts +12 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +8 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +60 -6
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +80 -23
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +424 -213
- package/dist/server/server.js.map +1 -1
- package/dist/server/static-headers.d.ts +14 -3
- package/dist/server/static-headers.d.ts.map +1 -1
- package/dist/server/static-headers.js +28 -6
- package/dist/server/static-headers.js.map +1 -1
- package/dist/server/transport-lane.d.ts +8 -0
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +12 -8
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/websocket.d.ts +3 -0
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +14 -0
- package/dist/server/websocket.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tool-catalog.d.ts +21 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +16 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/tool-contract.d.ts +238 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +569 -0
- package/dist/tool-contract.js.map +1 -0
- package/package.json +26 -1
- package/src/assurance.ts +236 -6
- package/src/capabilities.ts +14 -1
- package/src/diff.ts +5 -2
- package/src/errors.ts +1 -0
- package/src/evidence.ts +319 -0
- package/src/execution-policy.ts +57 -0
- package/src/index.ts +1 -1
- package/src/internal/route-assurance.ts +67 -4
- package/src/internal/route-execution.ts +276 -1
- package/src/manifest.ts +27 -5
- package/src/reflection.ts +24 -4
- package/src/replay.ts +96 -0
- package/src/schema/standard.ts +16 -0
- package/src/server/body.ts +275 -7
- package/src/server/context.ts +23 -3
- package/src/server/contract.ts +43 -3
- package/src/server/cookies.ts +80 -10
- package/src/server/edge.ts +10 -0
- package/src/server/header-case.ts +54 -0
- package/src/server/headers.ts +11 -0
- package/src/server/node-outcome-hook.ts +6 -11
- package/src/server/node-outcome.ts +10 -2
- package/src/server/plugin.ts +87 -8
- package/src/server/proto-guard.ts +118 -0
- package/src/server/registry.ts +6 -2
- package/src/server/request-context.ts +242 -26
- package/src/server/respond.ts +35 -4
- package/src/server/response-contract-lane.ts +6 -17
- package/src/server/response-hooks.ts +51 -0
- package/src/server/runtime-core.ts +14 -0
- package/src/server/server-types.ts +66 -6
- package/src/server/server.ts +667 -287
- package/src/server/static-headers.ts +27 -3
- package/src/server/transport-lane.ts +19 -8
- package/src/server/websocket.ts +18 -2
- package/src/server.ts +7 -0
- package/src/tool-catalog.ts +40 -0
- package/src/tool-contract.ts +896 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Bun-native, contract-first HTTP framework - the router, server, and route descriptor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -67,11 +67,21 @@
|
|
|
67
67
|
"types": "./dist/diff.d.ts",
|
|
68
68
|
"default": "./dist/diff.js"
|
|
69
69
|
},
|
|
70
|
+
"./evidence": {
|
|
71
|
+
"bun": "./src/evidence.ts",
|
|
72
|
+
"types": "./dist/evidence.d.ts",
|
|
73
|
+
"default": "./dist/evidence.js"
|
|
74
|
+
},
|
|
70
75
|
"./assurance": {
|
|
71
76
|
"bun": "./src/assurance.ts",
|
|
72
77
|
"types": "./dist/assurance.d.ts",
|
|
73
78
|
"default": "./dist/assurance.js"
|
|
74
79
|
},
|
|
80
|
+
"./replay": {
|
|
81
|
+
"bun": "./src/replay.ts",
|
|
82
|
+
"types": "./dist/replay.d.ts",
|
|
83
|
+
"default": "./dist/replay.js"
|
|
84
|
+
},
|
|
75
85
|
"./binary": {
|
|
76
86
|
"bun": "./src/binary.ts",
|
|
77
87
|
"types": "./dist/binary.d.ts",
|
|
@@ -226,6 +236,21 @@
|
|
|
226
236
|
"bun": "./src/server/response-contract-lane.ts",
|
|
227
237
|
"types": "./dist/server/response-contract-lane.d.ts",
|
|
228
238
|
"default": "./dist/server/response-contract-lane.js"
|
|
239
|
+
},
|
|
240
|
+
"./tool-contract": {
|
|
241
|
+
"bun": "./src/tool-contract.ts",
|
|
242
|
+
"types": "./dist/tool-contract.d.ts",
|
|
243
|
+
"default": "./dist/tool-contract.js"
|
|
244
|
+
},
|
|
245
|
+
"./tool-catalog": {
|
|
246
|
+
"bun": "./src/tool-catalog.ts",
|
|
247
|
+
"types": "./dist/tool-catalog.d.ts",
|
|
248
|
+
"default": "./dist/tool-catalog.js"
|
|
249
|
+
},
|
|
250
|
+
"./execution-policy": {
|
|
251
|
+
"bun": "./src/execution-policy.ts",
|
|
252
|
+
"types": "./dist/execution-policy.d.ts",
|
|
253
|
+
"default": "./dist/execution-policy.js"
|
|
229
254
|
}
|
|
230
255
|
},
|
|
231
256
|
"files": [
|
package/src/assurance.ts
CHANGED
|
@@ -11,23 +11,32 @@ import {
|
|
|
11
11
|
type CapabilityZone,
|
|
12
12
|
defineCapabilityPolicy,
|
|
13
13
|
} from "./capabilities.ts"
|
|
14
|
+
import {
|
|
15
|
+
type DataClassification,
|
|
16
|
+
classificationAtLeast as isClassificationAtLeast,
|
|
17
|
+
isDataClassification,
|
|
18
|
+
} from "./classification.ts"
|
|
14
19
|
import type {
|
|
20
|
+
AssuranceAttachment,
|
|
15
21
|
AssuranceDeclaration,
|
|
16
22
|
AssuranceEvidence,
|
|
17
23
|
AssuranceScope,
|
|
18
24
|
} from "./internal/route-assurance.ts"
|
|
19
25
|
import {
|
|
26
|
+
evidenceProvenance,
|
|
20
27
|
NIFRA_ASSURANCE_IDS,
|
|
28
|
+
normalizeAssuranceDeclarations,
|
|
21
29
|
routeGlob,
|
|
22
30
|
validEvidenceId,
|
|
23
31
|
validMethod,
|
|
24
32
|
withRouteAssurance,
|
|
25
33
|
} from "./internal/route-assurance.ts"
|
|
34
|
+
import type { SealedEffectLedger } from "./ledger.ts"
|
|
26
35
|
import type { NifraManifestSigner } from "./manifest.ts"
|
|
27
36
|
import { type ReflectedRoute, reflectRoutes } from "./reflection.ts"
|
|
28
37
|
import type { Method } from "./router/router.ts"
|
|
29
38
|
|
|
30
|
-
export type { AssuranceDeclaration, AssuranceEvidence, AssuranceScope }
|
|
39
|
+
export type { AssuranceAttachment, AssuranceDeclaration, AssuranceEvidence, AssuranceScope }
|
|
31
40
|
export { withRouteAssurance }
|
|
32
41
|
|
|
33
42
|
/** Isolated request executor used by adversarial contract verification. */
|
|
@@ -36,6 +45,57 @@ export type InvariantExecutor = (request: Request) => Response | Promise<Respons
|
|
|
36
45
|
/** Canonical evidence ids emitted by Nifra's official middleware modules. */
|
|
37
46
|
export const NIFRA_ASSURANCE = NIFRA_ASSURANCE_IDS
|
|
38
47
|
|
|
48
|
+
/** The seam `assure()` writes through: an evidence-only middleware bundle, applied like any other. */
|
|
49
|
+
interface AssurableServer {
|
|
50
|
+
use(middleware: object): unknown
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Publish enforcement evidence from OUTSIDE the plugin chain - the deployment shell that wraps the
|
|
55
|
+
* app, the mount site, or the call that hands it to `serve`. When the thing doing the enforcing is
|
|
56
|
+
* not a nifra plugin (a gateway, a service mesh, an outer framework), the alternative is switching
|
|
57
|
+
* the affected rules off; this records what covers the routes instead, so the policy keeps running.
|
|
58
|
+
*
|
|
59
|
+
* const app = buildApp() // no assurance-bearing plugin in its .use() chain
|
|
60
|
+
* assure(app, { id: NIFRA_ASSURANCE.AUTHENTICATED, source: "edge-gateway" })
|
|
61
|
+
* serve(app)
|
|
62
|
+
*
|
|
63
|
+
* `scope` defaults to `global`: the shell runs after every route is registered, so the evidence is
|
|
64
|
+
* retroactive and app-wide. Narrow it with `methods`/`paths` (absolute globs), or pass
|
|
65
|
+
* `scope: "subsequent"` to cover only routes registered after the call.
|
|
66
|
+
*
|
|
67
|
+
* Provenance is always `declared` - nifra did not install this enforcement and cannot observe it, so
|
|
68
|
+
* a rule with `requireProvenance: "runtime"` still rejects the route, by design.
|
|
69
|
+
*/
|
|
70
|
+
export function assure(
|
|
71
|
+
app: unknown,
|
|
72
|
+
evidence: AssuranceAttachment | readonly AssuranceAttachment[],
|
|
73
|
+
): void {
|
|
74
|
+
const values: readonly AssuranceAttachment[] = Array.isArray(evidence)
|
|
75
|
+
? (evidence as readonly AssuranceAttachment[])
|
|
76
|
+
: [evidence as AssuranceAttachment]
|
|
77
|
+
const declarations = normalizeAssuranceDeclarations(
|
|
78
|
+
values.map((item) => ({
|
|
79
|
+
...item,
|
|
80
|
+
scope: item.scope ?? "global",
|
|
81
|
+
provenance: "declared" as const,
|
|
82
|
+
})),
|
|
83
|
+
)
|
|
84
|
+
for (const declaration of declarations) {
|
|
85
|
+
if (declaration.scope === "plugin") {
|
|
86
|
+
throw new Error('assure(): scope "plugin" may only annotate a plugin function')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const target = app as Partial<AssurableServer>
|
|
90
|
+
if (typeof target.use !== "function") {
|
|
91
|
+
throw new TypeError("assure(): expected a nifra server")
|
|
92
|
+
}
|
|
93
|
+
// An evidence-only bundle: `use()` already reads declarations off what it applies, and a bundle
|
|
94
|
+
// with no hooks registers nothing on any lifecycle array - so the kernel needs no seam of its own,
|
|
95
|
+
// the request path is untouched, and an app that never calls `assure()` carries none of this.
|
|
96
|
+
target.use(withRouteAssurance({}, declarations))
|
|
97
|
+
}
|
|
98
|
+
|
|
39
99
|
export interface AssuranceRouteSelector {
|
|
40
100
|
/** Omit for every method. */
|
|
41
101
|
readonly methods?: readonly Method[]
|
|
@@ -73,6 +133,8 @@ export interface AssuranceRouteSelector {
|
|
|
73
133
|
* match a route that only writes an audit log.
|
|
74
134
|
*/
|
|
75
135
|
readonly zone?: CapabilityZone
|
|
136
|
+
/** Match routes whose response classification is at least this sensitivity. */
|
|
137
|
+
readonly classificationAtLeast?: DataClassification
|
|
76
138
|
}
|
|
77
139
|
|
|
78
140
|
/** Extra inputs an assurance evaluation needs beyond the routes themselves. */
|
|
@@ -109,6 +171,19 @@ export interface AssuranceRule {
|
|
|
109
171
|
readonly require?: readonly string[]
|
|
110
172
|
/** Evidence ids the route must not carry (useful for public webhooks and health routes). */
|
|
111
173
|
readonly forbid?: readonly string[]
|
|
174
|
+
/**
|
|
175
|
+
* Provenance required for every id in require. any (default) preserves compatibility with
|
|
176
|
+
* existing policies; runtime rejects schema.assurance author assertions and accepts only
|
|
177
|
+
* evidence installed by middleware/plugins or framework runtime policy. declared is useful for
|
|
178
|
+
* explicitly reviewing in-handler assertions and should not be used as an enforcement gate.
|
|
179
|
+
*/
|
|
180
|
+
readonly requireProvenance?: "any" | "runtime" | "declared"
|
|
181
|
+
/**
|
|
182
|
+
* When true, an authenticated route selected by this rule must also carry runtime CSRF evidence.
|
|
183
|
+
* Enable this on rules covering cookie/session-authenticated browser routes; bearer-only APIs
|
|
184
|
+
* should use a separate rule because they do not have browser ambient-authority exposure.
|
|
185
|
+
*/
|
|
186
|
+
readonly requireCsrfWithAuthenticated?: boolean
|
|
112
187
|
}
|
|
113
188
|
|
|
114
189
|
export interface AssurancePolicy {
|
|
@@ -161,6 +236,23 @@ export interface AssuranceReport {
|
|
|
161
236
|
readonly capabilities?: CapabilityAssuranceReport
|
|
162
237
|
}
|
|
163
238
|
|
|
239
|
+
/** Application-supplied verification rules. The CLI validates the executable rule shape at runtime. */
|
|
240
|
+
export interface AssuranceRulePack {
|
|
241
|
+
readonly name: string
|
|
242
|
+
readonly rules: readonly unknown[]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface IdempotencyWorkload {
|
|
246
|
+
readonly name: string
|
|
247
|
+
readonly run: () => Promise<SealedEffectLedger> | SealedEffectLedger
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface AssuranceSizeBudget {
|
|
251
|
+
readonly outDir?: string
|
|
252
|
+
readonly maxBytes?: number
|
|
253
|
+
readonly maxGzipBytes?: number
|
|
254
|
+
}
|
|
255
|
+
|
|
164
256
|
export interface AssuranceConfig {
|
|
165
257
|
readonly source: unknown
|
|
166
258
|
readonly policy: AssurancePolicy
|
|
@@ -176,6 +268,14 @@ export interface AssuranceConfig {
|
|
|
176
268
|
readonly invariants?: {
|
|
177
269
|
readonly executor: InvariantExecutor
|
|
178
270
|
}
|
|
271
|
+
/** Optional application-supplied rule packs appended after built-in verification rules. */
|
|
272
|
+
readonly rulePacks?: readonly AssuranceRulePack[]
|
|
273
|
+
/** Optional sink for an assurance bundle. The CLI validates the callable shape at runtime. */
|
|
274
|
+
readonly assureSink?: unknown
|
|
275
|
+
/** Optional deterministic effect workloads for the tests gate. */
|
|
276
|
+
readonly idempotency?: readonly IdempotencyWorkload[]
|
|
277
|
+
/** Optional output-size budget for the assurance bundle's size gate. */
|
|
278
|
+
readonly size?: AssuranceSizeBudget
|
|
179
279
|
}
|
|
180
280
|
|
|
181
281
|
const nonEmpty = (value: string): boolean => value.trim() !== ""
|
|
@@ -229,7 +329,15 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
|
|
|
229
329
|
// silence - and a selector that loses its only constraint matches EVERY route, making the rule
|
|
230
330
|
// swallow everything after it. A misspelled key is a policy hole, so it is refused here.
|
|
231
331
|
const unknown = Object.keys(rule.match).filter(
|
|
232
|
-
(key) =>
|
|
332
|
+
(key) =>
|
|
333
|
+
![
|
|
334
|
+
"methods",
|
|
335
|
+
"paths",
|
|
336
|
+
"tools",
|
|
337
|
+
"capabilities",
|
|
338
|
+
...CLASS_SELECTOR_KEYS,
|
|
339
|
+
"classificationAtLeast",
|
|
340
|
+
].includes(key),
|
|
233
341
|
)
|
|
234
342
|
if (unknown.length > 0) {
|
|
235
343
|
throw new Error(
|
|
@@ -271,8 +379,40 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
|
|
|
271
379
|
`route assurance: rule ${JSON.stringify(name)} zone selector must be "domain" or "operational"`,
|
|
272
380
|
)
|
|
273
381
|
}
|
|
382
|
+
if (
|
|
383
|
+
rule.match.classificationAtLeast !== undefined &&
|
|
384
|
+
!isDataClassification(rule.match.classificationAtLeast)
|
|
385
|
+
) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
"route assurance: rule " +
|
|
388
|
+
JSON.stringify(name) +
|
|
389
|
+
' classificationAtLeast must be "public", "pii", or "secret"',
|
|
390
|
+
)
|
|
391
|
+
}
|
|
274
392
|
const required = normalizeEvidenceIds(rule.require, "required")
|
|
275
393
|
const forbidden = normalizeEvidenceIds(rule.forbid, "forbidden")
|
|
394
|
+
if (
|
|
395
|
+
rule.requireProvenance !== undefined &&
|
|
396
|
+
rule.requireProvenance !== "any" &&
|
|
397
|
+
rule.requireProvenance !== "runtime" &&
|
|
398
|
+
rule.requireProvenance !== "declared"
|
|
399
|
+
) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
"route assurance: rule " +
|
|
402
|
+
JSON.stringify(name) +
|
|
403
|
+
' requireProvenance must be "any", "runtime", or "declared"',
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
if (
|
|
407
|
+
rule.requireCsrfWithAuthenticated !== undefined &&
|
|
408
|
+
typeof rule.requireCsrfWithAuthenticated !== "boolean"
|
|
409
|
+
) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
"route assurance: rule " +
|
|
412
|
+
JSON.stringify(name) +
|
|
413
|
+
" requireCsrfWithAuthenticated must be boolean",
|
|
414
|
+
)
|
|
415
|
+
}
|
|
276
416
|
const overlap = required.find((id) => forbidden.includes(id))
|
|
277
417
|
if (overlap !== undefined) {
|
|
278
418
|
throw new Error(
|
|
@@ -288,9 +428,16 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
|
|
|
288
428
|
...(capabilities !== undefined ? { capabilities: Object.freeze([...capabilities]) } : {}),
|
|
289
429
|
...(access !== undefined ? { access } : {}),
|
|
290
430
|
...(zone !== undefined ? { zone } : {}),
|
|
431
|
+
...(rule.match.classificationAtLeast !== undefined
|
|
432
|
+
? { classificationAtLeast: rule.match.classificationAtLeast }
|
|
433
|
+
: {}),
|
|
291
434
|
}),
|
|
292
435
|
require: required,
|
|
293
436
|
forbid: forbidden,
|
|
437
|
+
...(rule.requireProvenance !== undefined
|
|
438
|
+
? { requireProvenance: rule.requireProvenance }
|
|
439
|
+
: {}),
|
|
440
|
+
...(rule.requireCsrfWithAuthenticated === true ? { requireCsrfWithAuthenticated: true } : {}),
|
|
294
441
|
})
|
|
295
442
|
})
|
|
296
443
|
return Object.freeze({
|
|
@@ -312,6 +459,57 @@ export function defineAssuranceConfig(config: AssuranceConfig): AssuranceConfig
|
|
|
312
459
|
if (config.invariants !== undefined && typeof config.invariants.executor !== "function") {
|
|
313
460
|
throw new Error("route assurance: invariant executor must be a function")
|
|
314
461
|
}
|
|
462
|
+
if (config.rulePacks !== undefined) {
|
|
463
|
+
if (!Array.isArray(config.rulePacks))
|
|
464
|
+
throw new TypeError("route assurance: rulePacks must be an array")
|
|
465
|
+
const names = new Set<string>()
|
|
466
|
+
for (const pack of config.rulePacks) {
|
|
467
|
+
if (typeof pack.name !== "string" || pack.name.trim() === "")
|
|
468
|
+
throw new TypeError("route assurance: rule pack name must be non-empty")
|
|
469
|
+
if (names.has(pack.name)) throw new Error(`route assurance: duplicate rule pack ${pack.name}`)
|
|
470
|
+
names.add(pack.name)
|
|
471
|
+
if (!Array.isArray(pack.rules))
|
|
472
|
+
throw new TypeError(`route assurance: invalid rules in ${pack.name}`)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (config.assureSink !== undefined) {
|
|
476
|
+
if (
|
|
477
|
+
typeof config.assureSink !== "object" ||
|
|
478
|
+
config.assureSink === null ||
|
|
479
|
+
Array.isArray(config.assureSink)
|
|
480
|
+
)
|
|
481
|
+
throw new TypeError("route assurance: assureSink must be an object")
|
|
482
|
+
const record = Object.fromEntries(Object.entries(config.assureSink))
|
|
483
|
+
if (typeof record.record !== "function")
|
|
484
|
+
throw new TypeError("route assurance: assureSink.record must be a function")
|
|
485
|
+
}
|
|
486
|
+
if (config.idempotency !== undefined) {
|
|
487
|
+
if (!Array.isArray(config.idempotency))
|
|
488
|
+
throw new TypeError("route assurance: idempotency must be an array")
|
|
489
|
+
for (const workload of config.idempotency) {
|
|
490
|
+
if (
|
|
491
|
+
typeof workload.name !== "string" ||
|
|
492
|
+
workload.name.trim() === "" ||
|
|
493
|
+
typeof workload.run !== "function"
|
|
494
|
+
)
|
|
495
|
+
throw new TypeError("route assurance: idempotency workloads require name and run")
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (config.size !== undefined) {
|
|
499
|
+
if (typeof config.size !== "object" || config.size === null || Array.isArray(config.size))
|
|
500
|
+
throw new TypeError("route assurance: size must be an object")
|
|
501
|
+
for (const [name, value] of Object.entries(config.size)) {
|
|
502
|
+
if (name !== "outDir" && name !== "maxBytes" && name !== "maxGzipBytes")
|
|
503
|
+
throw new TypeError(`route assurance: unknown size key ${name}`)
|
|
504
|
+
if (name === "outDir" && typeof value !== "string")
|
|
505
|
+
throw new TypeError("route assurance: size.outDir must be a string")
|
|
506
|
+
if (
|
|
507
|
+
(name === "maxBytes" || name === "maxGzipBytes") &&
|
|
508
|
+
(typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
509
|
+
)
|
|
510
|
+
throw new TypeError(`route assurance: size.${name} must be a non-negative number`)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
315
513
|
const policy = defineAssurancePolicy(config.policy)
|
|
316
514
|
const capabilities =
|
|
317
515
|
config.capabilities !== undefined ? defineCapabilityPolicy(config.capabilities) : undefined
|
|
@@ -344,6 +542,12 @@ export function defineAssuranceConfig(config: AssuranceConfig): AssuranceConfig
|
|
|
344
542
|
...(config.invariants !== undefined
|
|
345
543
|
? { invariants: Object.freeze({ executor: config.invariants.executor }) }
|
|
346
544
|
: {}),
|
|
545
|
+
...(config.rulePacks !== undefined ? { rulePacks: Object.freeze([...config.rulePacks]) } : {}),
|
|
546
|
+
...(config.assureSink !== undefined ? { assureSink: config.assureSink } : {}),
|
|
547
|
+
...(config.idempotency !== undefined
|
|
548
|
+
? { idempotency: Object.freeze([...config.idempotency]) }
|
|
549
|
+
: {}),
|
|
550
|
+
...(config.size !== undefined ? { size: Object.freeze({ ...config.size }) } : {}),
|
|
347
551
|
})
|
|
348
552
|
}
|
|
349
553
|
|
|
@@ -355,15 +559,21 @@ export function defineAssuranceConfig(config: AssuranceConfig): AssuranceConfig
|
|
|
355
559
|
* accept user policy must reject that combination up front - `defineAssuranceConfig` does.
|
|
356
560
|
*/
|
|
357
561
|
export function matchesAssuranceSelector(
|
|
358
|
-
route: Pick<ReflectedRoute, "method" | "path" | "tool" | "capabilities">,
|
|
562
|
+
route: Pick<ReflectedRoute, "method" | "path" | "tool" | "capabilities" | "classification">,
|
|
359
563
|
selector: AssuranceRouteSelector,
|
|
360
564
|
definitions?: ReadonlyMap<string, CapabilityDefinition>,
|
|
361
565
|
): boolean {
|
|
362
|
-
const { methods, paths, tools, capabilities, access, zone } = selector
|
|
566
|
+
const { methods, paths, tools, capabilities, access, zone, classificationAtLeast } = selector
|
|
363
567
|
if (methods !== undefined && !methods.includes(route.method as Method)) return false
|
|
364
568
|
if (paths !== undefined && !paths.some((pattern) => routeGlob(pattern).test(route.path)))
|
|
365
569
|
return false
|
|
366
570
|
if (tools !== undefined && (route.tool !== undefined) !== tools) return false
|
|
571
|
+
if (
|
|
572
|
+
classificationAtLeast !== undefined &&
|
|
573
|
+
(route.classification === undefined ||
|
|
574
|
+
!isClassificationAtLeast(route.classification.max, classificationAtLeast))
|
|
575
|
+
)
|
|
576
|
+
return false
|
|
367
577
|
const declared = route.capabilities ?? []
|
|
368
578
|
if (capabilities !== undefined && !capabilities.some((token) => declared.includes(token)))
|
|
369
579
|
return false
|
|
@@ -434,8 +644,25 @@ export function evaluateRouteAssurance(
|
|
|
434
644
|
continue
|
|
435
645
|
}
|
|
436
646
|
|
|
437
|
-
const
|
|
647
|
+
const requiredProvenance = rule.requireProvenance ?? "any"
|
|
648
|
+
let missing = (rule.require ?? []).filter((id) => {
|
|
649
|
+
if (!evidenceIds.has(id)) return true
|
|
650
|
+
if (requiredProvenance === "any") return false
|
|
651
|
+
return !evidence.some(
|
|
652
|
+
(item) => item.id === id && evidenceProvenance(item) === requiredProvenance,
|
|
653
|
+
)
|
|
654
|
+
})
|
|
438
655
|
const forbidden = (rule.forbid ?? []).filter((id) => evidenceIds.has(id))
|
|
656
|
+
if (
|
|
657
|
+
rule.requireCsrfWithAuthenticated === true &&
|
|
658
|
+
evidenceIds.has(NIFRA_ASSURANCE_IDS.AUTHENTICATED) &&
|
|
659
|
+
!evidence.some(
|
|
660
|
+
(item) => item.id === NIFRA_ASSURANCE_IDS.CSRF && evidenceProvenance(item) === "runtime",
|
|
661
|
+
)
|
|
662
|
+
) {
|
|
663
|
+
const id = NIFRA_ASSURANCE_IDS.CSRF
|
|
664
|
+
if (!missing.includes(id)) missing = [...missing, id]
|
|
665
|
+
}
|
|
439
666
|
for (const id of missing) {
|
|
440
667
|
findings.push({
|
|
441
668
|
code: "missing-evidence",
|
|
@@ -443,7 +670,10 @@ export function evaluateRouteAssurance(
|
|
|
443
670
|
path: route.path,
|
|
444
671
|
rule: rule.name,
|
|
445
672
|
evidence: id,
|
|
446
|
-
message:
|
|
673
|
+
message:
|
|
674
|
+
requiredProvenance === "any"
|
|
675
|
+
? `${route.method} ${route.path} (${rule.name}) is missing ${id}`
|
|
676
|
+
: `${route.method} ${route.path} (${rule.name}) is missing ${requiredProvenance} evidence for ${id}`,
|
|
447
677
|
})
|
|
448
678
|
}
|
|
449
679
|
for (const id of forbidden) {
|
package/src/capabilities.ts
CHANGED
|
@@ -91,6 +91,14 @@ export interface CapabilityImportRule {
|
|
|
91
91
|
/** Exact module specifier, or a trailing `/*` prefix rule. */
|
|
92
92
|
readonly specifier: string
|
|
93
93
|
readonly capabilities: readonly string[]
|
|
94
|
+
/**
|
|
95
|
+
* Allow this seam to be absent. A rule that matches no import anywhere in the project is reported
|
|
96
|
+
* as `unmatched-provenance-seam` by default, because a specifier written differently from the
|
|
97
|
+
* import it means to govern (`"./db/client.ts"` vs `import "./db/client"`) governs nothing and
|
|
98
|
+
* every route touching that effect still passes. Set this only for a policy shared across projects
|
|
99
|
+
* where some of them genuinely do not use the seam.
|
|
100
|
+
*/
|
|
101
|
+
readonly optional?: boolean
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
export interface ForbiddenCapabilityImport {
|
|
@@ -156,6 +164,7 @@ export type CapabilityFindingCode =
|
|
|
156
164
|
| "missing-durable-idempotency"
|
|
157
165
|
| "forbidden-effect-import"
|
|
158
166
|
| "provenance-truncated"
|
|
167
|
+
| "unmatched-provenance-seam"
|
|
159
168
|
|
|
160
169
|
export interface CapabilityFinding {
|
|
161
170
|
readonly code: CapabilityFindingCode
|
|
@@ -270,7 +279,11 @@ export function defineCapabilityPolicy(policy: CapabilityPolicy): CapabilityPoli
|
|
|
270
279
|
for (const id of capabilities) {
|
|
271
280
|
if (!ids.has(id)) throw new Error(`capability assurance: import references unknown ${id}`)
|
|
272
281
|
}
|
|
273
|
-
return Object.freeze({
|
|
282
|
+
return Object.freeze({
|
|
283
|
+
specifier: rule.specifier,
|
|
284
|
+
capabilities,
|
|
285
|
+
...(rule.optional === true ? { optional: true as const } : {}),
|
|
286
|
+
})
|
|
274
287
|
})
|
|
275
288
|
const forbiddenImports = policy.provenance.forbiddenImports.map(
|
|
276
289
|
(rule): ForbiddenCapabilityImport => {
|
package/src/diff.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface SchemaSnapshot {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface RouteSnapshotSchema {
|
|
35
|
+
readonly headers?: SchemaSnapshot
|
|
35
36
|
readonly body?: SchemaSnapshot
|
|
36
37
|
readonly query?: SchemaSnapshot
|
|
37
38
|
readonly response?: SchemaSnapshot
|
|
@@ -53,7 +54,7 @@ export interface RouteChange {
|
|
|
53
54
|
readonly method: string
|
|
54
55
|
readonly path: string
|
|
55
56
|
/** Which part of the contract changed; "route" for add/remove of the whole route. */
|
|
56
|
-
readonly section: "route" | "body" | "query" | "response" | "sse" | "errors"
|
|
57
|
+
readonly section: "route" | "headers" | "body" | "query" | "response" | "sse" | "errors"
|
|
57
58
|
/** The top-level field (or error status) the change is about, when field-granular. */
|
|
58
59
|
readonly field?: string
|
|
59
60
|
readonly message: string
|
|
@@ -98,10 +99,12 @@ const snapshotRoute = (route: ReflectedRoute): RouteSnapshot => {
|
|
|
98
99
|
if (snapped !== undefined) errors[status] = snapped
|
|
99
100
|
}
|
|
100
101
|
const body = schemaSnapshot(schema.body)
|
|
102
|
+
const headers = schemaSnapshot(schema.headers)
|
|
101
103
|
const query = schemaSnapshot(schema.query)
|
|
102
104
|
const response = schemaSnapshot(schema.response)
|
|
103
105
|
const sse = schemaSnapshot(schema.sse)
|
|
104
106
|
const snapped: RouteSnapshotSchema = {
|
|
107
|
+
...(headers !== undefined ? { headers } : {}),
|
|
105
108
|
...(body !== undefined ? { body } : {}),
|
|
106
109
|
...(query !== undefined ? { query } : {}),
|
|
107
110
|
...(response !== undefined ? { response } : {}),
|
|
@@ -353,7 +356,7 @@ export function diffRouteSnapshots(
|
|
|
353
356
|
const path = beforeRoute.path
|
|
354
357
|
const beforeSchema = beforeRoute.schema ?? {}
|
|
355
358
|
const afterSchema = afterRoute.schema ?? {}
|
|
356
|
-
for (const section of ["body", "query"] as const) {
|
|
359
|
+
for (const section of ["headers", "body", "query"] as const) {
|
|
357
360
|
diffSchemaSection(
|
|
358
361
|
{ method, path, section, direction: "request", changes },
|
|
359
362
|
beforeSchema[section],
|