@nifrajs/core 2.11.0 → 2.12.1
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 +429 -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 +672 -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/src/evidence.ts
ADDED
|
@@ -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.
|
|
13
|
+
export const VERSION = "2.12.1" 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, ...
|
|
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(
|
|
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
|
}
|