@nifrajs/core 1.13.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/dist/assurance.d.ts +11 -2
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +20 -0
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +54 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +240 -5
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-adapters.d.ts +241 -0
- package/dist/durable-adapters.d.ts.map +1 -0
- package/dist/durable-adapters.js +697 -0
- package/dist/durable-adapters.js.map +1 -0
- package/dist/durable-execution.d.ts +349 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +1173 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/effect-ledger.d.ts +9 -0
- package/dist/effect-ledger.d.ts.map +1 -0
- package/dist/effect-ledger.js +8 -0
- package/dist/effect-ledger.js.map +1 -0
- package/dist/effect-lifecycle.d.ts +42 -0
- package/dist/effect-lifecycle.d.ts.map +1 -0
- package/dist/effect-lifecycle.js +66 -0
- package/dist/effect-lifecycle.js.map +1 -0
- package/dist/effect-scope.d.ts +75 -0
- package/dist/effect-scope.d.ts.map +1 -0
- package/dist/effect-scope.js +177 -0
- package/dist/effect-scope.js.map +1 -0
- package/dist/idempotency-plugin.d.ts +13 -0
- package/dist/idempotency-plugin.d.ts.map +1 -0
- package/dist/idempotency-plugin.js +16 -0
- package/dist/idempotency-plugin.js.map +1 -0
- package/dist/index.d.ts +6 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -31
- package/dist/index.js.map +1 -1
- package/dist/internal/capability-runtime.d.ts +62 -1
- package/dist/internal/capability-runtime.d.ts.map +1 -1
- package/dist/internal/capability-runtime.js +11 -2
- package/dist/internal/capability-runtime.js.map +1 -1
- package/dist/internal/effect-execution.d.ts +16 -0
- package/dist/internal/effect-execution.d.ts.map +1 -0
- package/dist/internal/effect-execution.js +50 -0
- package/dist/internal/effect-execution.js.map +1 -0
- package/dist/internal/safe-equal.d.ts +20 -0
- package/dist/internal/safe-equal.d.ts.map +1 -0
- package/dist/internal/safe-equal.js +31 -0
- package/dist/internal/safe-equal.js.map +1 -0
- package/dist/ledger.d.ts +14 -6
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +31 -5
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +56 -0
- package/dist/mcp.js.map +1 -0
- package/dist/node-direct.d.ts +5 -0
- package/dist/node-direct.d.ts.map +1 -0
- package/dist/node-direct.js +27 -0
- package/dist/node-direct.js.map +1 -0
- package/dist/reconciliation-worker.d.ts +108 -0
- package/dist/reconciliation-worker.d.ts.map +1 -0
- package/dist/reconciliation-worker.js +239 -0
- package/dist/reconciliation-worker.js.map +1 -0
- package/dist/reflection.d.ts +5 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +2 -0
- package/dist/reflection.js.map +1 -1
- package/dist/server/client-ip.d.ts +35 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/client-ip.js +42 -0
- package/dist/server/client-ip.js.map +1 -0
- package/dist/server/context.d.ts +47 -2
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +14 -4
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +8 -2
- package/dist/server/contract.js.map +1 -1
- package/dist/server/http.d.ts +14 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +45 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/idempotency-lane.d.ts +45 -0
- package/dist/server/idempotency-lane.d.ts.map +1 -0
- package/dist/server/idempotency-lane.js +205 -0
- package/dist/server/idempotency-lane.js.map +1 -0
- package/dist/server/install.d.ts +21 -0
- package/dist/server/install.d.ts.map +1 -0
- package/dist/server/install.js +21 -0
- package/dist/server/install.js.map +1 -0
- package/dist/server/ledger-lane.d.ts +53 -0
- package/dist/server/ledger-lane.d.ts.map +1 -0
- package/dist/server/ledger-lane.js +82 -0
- package/dist/server/ledger-lane.js.map +1 -0
- package/dist/server/mcp-hook.d.ts +28 -0
- package/dist/server/mcp-hook.d.ts.map +1 -0
- package/dist/server/mcp-hook.js +2 -0
- package/dist/server/mcp-hook.js.map +1 -0
- package/dist/server/node-outcome-hook.d.ts +9 -0
- package/dist/server/node-outcome-hook.d.ts.map +1 -0
- package/dist/server/node-outcome-hook.js +2 -0
- package/dist/server/node-outcome-hook.js.map +1 -0
- package/dist/server/node-outcome.d.ts +37 -0
- package/dist/server/node-outcome.d.ts.map +1 -0
- package/dist/server/node-outcome.js +75 -0
- package/dist/server/node-outcome.js.map +1 -0
- package/dist/server/plugin.d.ts +78 -0
- package/dist/server/plugin.d.ts.map +1 -0
- package/dist/server/plugin.js +57 -0
- package/dist/server/plugin.js.map +1 -0
- package/dist/server/query.d.ts +18 -0
- package/dist/server/query.d.ts.map +1 -0
- package/dist/server/query.js +102 -0
- package/dist/server/query.js.map +1 -0
- package/dist/server/registry.d.ts +31 -6
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +51 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +184 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/respond.d.ts +17 -0
- package/dist/server/respond.d.ts.map +1 -0
- package/dist/server/respond.js +81 -0
- package/dist/server/respond.js.map +1 -0
- package/dist/server/runtime-core.d.ts +36 -0
- package/dist/server/runtime-core.d.ts.map +1 -0
- package/dist/server/runtime-core.js +51 -0
- package/dist/server/runtime-core.js.map +1 -0
- package/dist/server/server-types.d.ts +203 -0
- package/dist/server/server-types.d.ts.map +1 -0
- package/dist/server/server-types.js +2 -0
- package/dist/server/server-types.js.map +1 -0
- package/dist/server/server.d.ts +119 -345
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +247 -833
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse-hook.d.ts +10 -0
- package/dist/server/sse-hook.d.ts.map +1 -0
- package/dist/server/sse-hook.js +2 -0
- package/dist/server/sse-hook.js.map +1 -0
- package/dist/server/sse.d.ts +7 -23
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -0
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +28 -0
- package/dist/server/transport-lane.d.ts.map +1 -0
- package/dist/server/transport-lane.js +106 -0
- package/dist/server/transport-lane.js.map +1 -0
- package/dist/server/websocket.d.ts +23 -1
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +10 -1
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-hook.d.ts +8 -11
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server/ws-hook.js +7 -14
- package/dist/server/ws-hook.js.map +1 -1
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +16 -0
- package/dist/server.js.map +1 -0
- package/dist/transport-codec-rich.d.ts +8 -0
- package/dist/transport-codec-rich.d.ts.map +1 -0
- package/dist/transport-codec-rich.js +14 -0
- package/dist/transport-codec-rich.js.map +1 -0
- package/dist/transport-codec.d.ts +31 -0
- package/dist/transport-codec.d.ts.map +1 -0
- package/dist/transport-codec.js +190 -0
- package/dist/transport-codec.js.map +1 -0
- package/dist/transport-plugin.d.ts +3 -0
- package/dist/transport-plugin.d.ts.map +1 -0
- package/dist/transport-plugin.js +3 -0
- package/dist/transport-plugin.js.map +1 -0
- package/dist/wire.d.ts +58 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +445 -0
- package/dist/wire.js.map +1 -0
- package/dist/ws.d.ts +16 -7
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +25 -10
- package/dist/ws.js.map +1 -1
- package/package.json +122 -6
- package/src/assurance.ts +36 -1
- package/src/capabilities.ts +306 -5
- package/src/durable-adapters.ts +1039 -0
- package/src/durable-execution.ts +1676 -0
- package/src/effect-ledger.ts +9 -0
- package/src/effect-lifecycle.ts +120 -0
- package/src/effect-scope.ts +267 -0
- package/src/idempotency-plugin.ts +17 -0
- package/src/index.ts +6 -133
- package/src/internal/capability-runtime.ts +87 -1
- package/src/internal/effect-execution.ts +70 -0
- package/src/internal/safe-equal.ts +27 -0
- package/src/ledger.ts +45 -11
- package/src/mcp.ts +65 -0
- package/src/node-direct.ts +37 -0
- package/src/reconciliation-worker.ts +372 -0
- package/src/reflection.ts +7 -0
- package/src/server/client-ip.ts +60 -0
- package/src/server/context.ts +50 -2
- package/src/server/contract.ts +26 -7
- package/src/server/http.ts +58 -0
- package/src/server/idempotency-lane.ts +321 -0
- package/src/server/install.ts +28 -0
- package/src/server/ledger-lane.ts +125 -0
- package/src/server/mcp-hook.ts +41 -0
- package/src/server/node-outcome-hook.ts +9 -0
- package/src/server/node-outcome.ts +112 -0
- package/src/server/plugin.ts +92 -0
- package/src/server/query.ts +119 -0
- package/src/server/registry.ts +36 -6
- package/src/server/request-context.ts +271 -0
- package/src/server/respond.ts +94 -0
- package/src/server/runtime-core.ts +74 -0
- package/src/server/server-types.ts +204 -0
- package/src/server/server.ts +448 -1314
- package/src/server/sse-hook.ts +14 -0
- package/src/server/sse.ts +26 -0
- package/src/server/transport-lane.ts +141 -0
- package/src/server/websocket.ts +35 -1
- package/src/server/ws-hook.ts +8 -19
- package/src/server.ts +92 -0
- package/src/transport-codec-rich.ts +20 -0
- package/src/transport-codec.ts +250 -0
- package/src/transport-plugin.ts +2 -0
- package/src/wire.ts +474 -0
- package/src/ws.ts +33 -10
- package/dist/invariants.d.ts +0 -70
- package/dist/invariants.d.ts.map +0 -1
- package/dist/invariants.js +0 -487
- package/dist/invariants.js.map +0 -1
- package/src/invariants.ts +0 -604
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Bun-native, contract-first HTTP framework — the router, server, and route descriptor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -27,11 +27,36 @@
|
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"default": "./dist/index.js"
|
|
29
29
|
},
|
|
30
|
+
"./server": {
|
|
31
|
+
"bun": "./src/server.ts",
|
|
32
|
+
"types": "./dist/server.d.ts",
|
|
33
|
+
"default": "./dist/server.js"
|
|
34
|
+
},
|
|
30
35
|
"./ws": {
|
|
31
36
|
"bun": "./src/ws.ts",
|
|
32
37
|
"types": "./dist/ws.d.ts",
|
|
33
38
|
"default": "./dist/ws.js"
|
|
34
39
|
},
|
|
40
|
+
"./wire": {
|
|
41
|
+
"bun": "./src/wire.ts",
|
|
42
|
+
"types": "./dist/wire.d.ts",
|
|
43
|
+
"default": "./dist/wire.js"
|
|
44
|
+
},
|
|
45
|
+
"./transport-codec": {
|
|
46
|
+
"bun": "./src/transport-codec.ts",
|
|
47
|
+
"types": "./dist/transport-codec.d.ts",
|
|
48
|
+
"default": "./dist/transport-codec.js"
|
|
49
|
+
},
|
|
50
|
+
"./transport-codec-rich": {
|
|
51
|
+
"bun": "./src/transport-codec-rich.ts",
|
|
52
|
+
"types": "./dist/transport-codec-rich.d.ts",
|
|
53
|
+
"default": "./dist/transport-codec-rich.js"
|
|
54
|
+
},
|
|
55
|
+
"./transport-plugin": {
|
|
56
|
+
"bun": "./src/transport-plugin.ts",
|
|
57
|
+
"types": "./dist/transport-plugin.d.ts",
|
|
58
|
+
"default": "./dist/transport-plugin.js"
|
|
59
|
+
},
|
|
35
60
|
"./reflection": {
|
|
36
61
|
"bun": "./src/reflection.ts",
|
|
37
62
|
"types": "./dist/reflection.d.ts",
|
|
@@ -62,16 +87,46 @@
|
|
|
62
87
|
"types": "./dist/idempotency.d.ts",
|
|
63
88
|
"default": "./dist/idempotency.js"
|
|
64
89
|
},
|
|
65
|
-
"./
|
|
66
|
-
"bun": "./src/
|
|
67
|
-
"types": "./dist/
|
|
68
|
-
"default": "./dist/
|
|
90
|
+
"./idempotency-plugin": {
|
|
91
|
+
"bun": "./src/idempotency-plugin.ts",
|
|
92
|
+
"types": "./dist/idempotency-plugin.d.ts",
|
|
93
|
+
"default": "./dist/idempotency-plugin.js"
|
|
69
94
|
},
|
|
70
95
|
"./ledger": {
|
|
71
96
|
"bun": "./src/ledger.ts",
|
|
72
97
|
"types": "./dist/ledger.d.ts",
|
|
73
98
|
"default": "./dist/ledger.js"
|
|
74
99
|
},
|
|
100
|
+
"./effect-ledger": {
|
|
101
|
+
"bun": "./src/effect-ledger.ts",
|
|
102
|
+
"types": "./dist/effect-ledger.d.ts",
|
|
103
|
+
"default": "./dist/effect-ledger.js"
|
|
104
|
+
},
|
|
105
|
+
"./effect-lifecycle": {
|
|
106
|
+
"bun": "./src/effect-lifecycle.ts",
|
|
107
|
+
"types": "./dist/effect-lifecycle.d.ts",
|
|
108
|
+
"default": "./dist/effect-lifecycle.js"
|
|
109
|
+
},
|
|
110
|
+
"./effect-scope": {
|
|
111
|
+
"bun": "./src/effect-scope.ts",
|
|
112
|
+
"types": "./dist/effect-scope.d.ts",
|
|
113
|
+
"default": "./dist/effect-scope.js"
|
|
114
|
+
},
|
|
115
|
+
"./durable-execution": {
|
|
116
|
+
"bun": "./src/durable-execution.ts",
|
|
117
|
+
"types": "./dist/durable-execution.d.ts",
|
|
118
|
+
"default": "./dist/durable-execution.js"
|
|
119
|
+
},
|
|
120
|
+
"./durable-adapters": {
|
|
121
|
+
"bun": "./src/durable-adapters.ts",
|
|
122
|
+
"types": "./dist/durable-adapters.d.ts",
|
|
123
|
+
"default": "./dist/durable-adapters.js"
|
|
124
|
+
},
|
|
125
|
+
"./reconciliation-worker": {
|
|
126
|
+
"bun": "./src/reconciliation-worker.ts",
|
|
127
|
+
"types": "./dist/reconciliation-worker.d.ts",
|
|
128
|
+
"default": "./dist/reconciliation-worker.js"
|
|
129
|
+
},
|
|
75
130
|
"./manifest": {
|
|
76
131
|
"bun": "./src/manifest.ts",
|
|
77
132
|
"types": "./dist/manifest.d.ts",
|
|
@@ -82,6 +137,16 @@
|
|
|
82
137
|
"types": "./dist/mount.d.ts",
|
|
83
138
|
"default": "./dist/mount.js"
|
|
84
139
|
},
|
|
140
|
+
"./mcp": {
|
|
141
|
+
"bun": "./src/mcp.ts",
|
|
142
|
+
"types": "./dist/mcp.d.ts",
|
|
143
|
+
"default": "./dist/mcp.js"
|
|
144
|
+
},
|
|
145
|
+
"./node-direct": {
|
|
146
|
+
"bun": "./src/node-direct.ts",
|
|
147
|
+
"types": "./dist/node-direct.d.ts",
|
|
148
|
+
"default": "./dist/node-direct.js"
|
|
149
|
+
},
|
|
85
150
|
"./pattern": {
|
|
86
151
|
"bun": "./src/router/pattern.ts",
|
|
87
152
|
"types": "./dist/router/pattern.d.ts",
|
|
@@ -91,6 +156,51 @@
|
|
|
91
156
|
"bun": "./src/causality.ts",
|
|
92
157
|
"types": "./dist/causality.d.ts",
|
|
93
158
|
"default": "./dist/causality.js"
|
|
159
|
+
},
|
|
160
|
+
"./classification": {
|
|
161
|
+
"bun": "./src/classification.ts",
|
|
162
|
+
"types": "./dist/classification.d.ts",
|
|
163
|
+
"default": "./dist/classification.js"
|
|
164
|
+
},
|
|
165
|
+
"./contract": {
|
|
166
|
+
"bun": "./src/server/contract.ts",
|
|
167
|
+
"types": "./dist/server/contract.d.ts",
|
|
168
|
+
"default": "./dist/server/contract.js"
|
|
169
|
+
},
|
|
170
|
+
"./cookies": {
|
|
171
|
+
"bun": "./src/server/cookies.ts",
|
|
172
|
+
"types": "./dist/server/cookies.d.ts",
|
|
173
|
+
"default": "./dist/server/cookies.js"
|
|
174
|
+
},
|
|
175
|
+
"./logger": {
|
|
176
|
+
"bun": "./src/server/logger.ts",
|
|
177
|
+
"types": "./dist/server/logger.d.ts",
|
|
178
|
+
"default": "./dist/server/logger.js"
|
|
179
|
+
},
|
|
180
|
+
"./router": {
|
|
181
|
+
"bun": "./src/router/router.ts",
|
|
182
|
+
"types": "./dist/router/router.d.ts",
|
|
183
|
+
"default": "./dist/router/router.js"
|
|
184
|
+
},
|
|
185
|
+
"./schema": {
|
|
186
|
+
"bun": "./src/schema/standard.ts",
|
|
187
|
+
"types": "./dist/schema/standard.d.ts",
|
|
188
|
+
"default": "./dist/schema/standard.js"
|
|
189
|
+
},
|
|
190
|
+
"./seo": {
|
|
191
|
+
"bun": "./src/seo.ts",
|
|
192
|
+
"types": "./dist/seo.d.ts",
|
|
193
|
+
"default": "./dist/seo.js"
|
|
194
|
+
},
|
|
195
|
+
"./sse": {
|
|
196
|
+
"bun": "./src/server/sse.ts",
|
|
197
|
+
"types": "./dist/server/sse.d.ts",
|
|
198
|
+
"default": "./dist/server/sse.js"
|
|
199
|
+
},
|
|
200
|
+
"./webhook": {
|
|
201
|
+
"bun": "./src/server/webhook.ts",
|
|
202
|
+
"types": "./dist/server/webhook.d.ts",
|
|
203
|
+
"default": "./dist/server/webhook.js"
|
|
94
204
|
}
|
|
95
205
|
},
|
|
96
206
|
"files": [
|
|
@@ -99,7 +209,13 @@
|
|
|
99
209
|
],
|
|
100
210
|
"sideEffects": [
|
|
101
211
|
"./src/ws.ts",
|
|
102
|
-
"./dist/ws.js"
|
|
212
|
+
"./dist/ws.js",
|
|
213
|
+
"./src/server/sse.ts",
|
|
214
|
+
"./dist/server/sse.js",
|
|
215
|
+
"./src/node-direct.ts",
|
|
216
|
+
"./dist/node-direct.js",
|
|
217
|
+
"./src/mcp-runtime.ts",
|
|
218
|
+
"./dist/mcp-runtime.js"
|
|
103
219
|
],
|
|
104
220
|
"scripts": {
|
|
105
221
|
"build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
|
package/src/assurance.ts
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
validMethod,
|
|
21
21
|
withRouteAssurance,
|
|
22
22
|
} from "./internal/route-assurance.ts"
|
|
23
|
-
import type { InvariantExecutor } from "./invariants.ts"
|
|
24
23
|
import type { NifraManifestSigner } from "./manifest.ts"
|
|
25
24
|
import { type ReflectedRoute, reflectRoutes } from "./reflection.ts"
|
|
26
25
|
import type { Method } from "./router/router.ts"
|
|
@@ -28,6 +27,9 @@ import type { Method } from "./router/router.ts"
|
|
|
28
27
|
export type { AssuranceDeclaration, AssuranceEvidence, AssuranceScope }
|
|
29
28
|
export { withRouteAssurance }
|
|
30
29
|
|
|
30
|
+
/** Isolated request executor used by adversarial contract verification. */
|
|
31
|
+
export type InvariantExecutor = (request: Request) => Response | Promise<Response>
|
|
32
|
+
|
|
31
33
|
/** Canonical evidence ids emitted by Nifra's official middleware modules. */
|
|
32
34
|
export const NIFRA_ASSURANCE = NIFRA_ASSURANCE_IDS
|
|
33
35
|
|
|
@@ -57,6 +59,14 @@ export interface AssurancePolicy {
|
|
|
57
59
|
readonly unmatched?: "error" | "ignore"
|
|
58
60
|
/** Default false: reject an empty reflected source so a wrong import cannot pass CI silently. */
|
|
59
61
|
readonly allowEmpty?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Default false. When true, a route matched by a **pure-classification** rule (no `require`, no `forbid`)
|
|
64
|
+
* that carries NO enforcement evidence is reported (`classified-no-evidence`). This surfaces the gap the
|
|
65
|
+
* feedback flagged: a classification-only policy silently degrades "proof" to a "label". Opt-in, because
|
|
66
|
+
* a genuinely public route legitimately carries no evidence; enable it once your guards emit evidence
|
|
67
|
+
* (inline `schema.assurance` or a `withRouteAssurance` middleware) to keep classification honest.
|
|
68
|
+
*/
|
|
69
|
+
readonly flagClassifiedWithoutEvidence?: boolean
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
export type AssuranceFindingCode =
|
|
@@ -64,6 +74,7 @@ export type AssuranceFindingCode =
|
|
|
64
74
|
| "unclassified-route"
|
|
65
75
|
| "missing-evidence"
|
|
66
76
|
| "forbidden-evidence"
|
|
77
|
+
| "classified-no-evidence"
|
|
67
78
|
|
|
68
79
|
export interface AssuranceFinding {
|
|
69
80
|
readonly code: AssuranceFindingCode
|
|
@@ -140,6 +151,12 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
|
|
|
140
151
|
if (policy.allowEmpty !== undefined && typeof policy.allowEmpty !== "boolean") {
|
|
141
152
|
throw new Error(`route assurance: allowEmpty must be boolean`)
|
|
142
153
|
}
|
|
154
|
+
if (
|
|
155
|
+
policy.flagClassifiedWithoutEvidence !== undefined &&
|
|
156
|
+
typeof policy.flagClassifiedWithoutEvidence !== "boolean"
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(`route assurance: flagClassifiedWithoutEvidence must be boolean`)
|
|
159
|
+
}
|
|
143
160
|
const names = new Set<string>()
|
|
144
161
|
const rules = policy.rules.map((rule): AssuranceRule => {
|
|
145
162
|
if (typeof rule.name !== "string" || !nonEmpty(rule.name)) {
|
|
@@ -187,6 +204,7 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
|
|
|
187
204
|
rules: Object.freeze(rules),
|
|
188
205
|
unmatched: policy.unmatched ?? "error",
|
|
189
206
|
allowEmpty: policy.allowEmpty ?? false,
|
|
207
|
+
flagClassifiedWithoutEvidence: policy.flagClassifiedWithoutEvidence ?? false,
|
|
190
208
|
})
|
|
191
209
|
}
|
|
192
210
|
|
|
@@ -292,6 +310,23 @@ export function evaluateRouteAssurance(
|
|
|
292
310
|
message: `${route.method} ${route.path} (${rule.name}) carries forbidden ${id}`,
|
|
293
311
|
})
|
|
294
312
|
}
|
|
313
|
+
// Opt-in visibility: a pure-classification rule (no require + no forbid) matching a route with zero
|
|
314
|
+
// evidence is a "label without proof". Surface it so the gap isn't silently green. A rule that expects
|
|
315
|
+
// or forbids evidence already speaks for itself via the loops above, so it's excluded here.
|
|
316
|
+
if (
|
|
317
|
+
policy.flagClassifiedWithoutEvidence === true &&
|
|
318
|
+
evidence.length === 0 &&
|
|
319
|
+
(rule.require ?? []).length === 0 &&
|
|
320
|
+
(rule.forbid ?? []).length === 0
|
|
321
|
+
) {
|
|
322
|
+
findings.push({
|
|
323
|
+
code: "classified-no-evidence",
|
|
324
|
+
method: route.method,
|
|
325
|
+
path: route.path,
|
|
326
|
+
rule: rule.name,
|
|
327
|
+
message: `${route.method} ${route.path} (${rule.name}) is classified but carries no enforcement evidence`,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
295
330
|
routes.push({
|
|
296
331
|
method: route.method,
|
|
297
332
|
path: route.path,
|
package/src/capabilities.ts
CHANGED
|
@@ -4,17 +4,45 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { DataClassification } from "./classification.ts"
|
|
7
|
+
import { type EffectLifecycleObserver, effectTraceParentOf } from "./effect-lifecycle.ts"
|
|
8
|
+
import { createEffectScope } from "./effect-scope.ts"
|
|
7
9
|
import {
|
|
10
|
+
type AroundCapabilityOptions,
|
|
8
11
|
CAPABILITY_GUARD,
|
|
12
|
+
type CapabilityApprovalGate,
|
|
13
|
+
type CapabilityApprovalInput,
|
|
14
|
+
type CapabilityExecutionIdentity,
|
|
15
|
+
type CapabilityExecutionJournal,
|
|
9
16
|
type CapabilityGuard,
|
|
17
|
+
type CapabilityInterceptor,
|
|
18
|
+
type CapabilityInterceptorEvent,
|
|
19
|
+
type CapabilityInterceptorNext,
|
|
10
20
|
type CapabilityUseEvent,
|
|
21
|
+
type RegisteredCapabilityInterceptor,
|
|
11
22
|
validCapabilityId,
|
|
12
23
|
} from "./internal/capability-runtime.ts"
|
|
24
|
+
import { effectScopeForContext } from "./internal/effect-execution.ts"
|
|
13
25
|
import { NIFRA_ASSURANCE_IDS } from "./internal/route-assurance.ts"
|
|
14
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
type EffectCost,
|
|
28
|
+
type EffectPhase,
|
|
29
|
+
effectLedgerOf,
|
|
30
|
+
normalizeEffectMetadata,
|
|
31
|
+
} from "./ledger.ts"
|
|
15
32
|
import { reflectRoutes } from "./reflection.ts"
|
|
16
33
|
|
|
17
|
-
export type {
|
|
34
|
+
export type {
|
|
35
|
+
AroundCapabilityOptions,
|
|
36
|
+
CapabilityApprovalGate,
|
|
37
|
+
CapabilityApprovalInput,
|
|
38
|
+
CapabilityExecutionIdentity,
|
|
39
|
+
CapabilityExecutionJournal,
|
|
40
|
+
CapabilityInterceptor,
|
|
41
|
+
CapabilityInterceptorEvent,
|
|
42
|
+
CapabilityInterceptorNext,
|
|
43
|
+
CapabilityUseEvent,
|
|
44
|
+
EffectLifecycleObserver,
|
|
45
|
+
}
|
|
18
46
|
|
|
19
47
|
export type CapabilityZone = "domain" | "operational"
|
|
20
48
|
export type CapabilityAccess = "read" | "write"
|
|
@@ -409,6 +437,162 @@ export interface CapabilityOutcomeOptions extends UseCapabilityOptions {
|
|
|
409
437
|
readonly error?: { readonly code: string }
|
|
410
438
|
}
|
|
411
439
|
|
|
440
|
+
/** Durable controls consumed by `executeCapability`; none of these fields enter the effect ledger. */
|
|
441
|
+
export interface CapabilityExecutionOptions extends UseCapabilityOptions {
|
|
442
|
+
readonly approval?: CapabilityApprovalInput & { readonly gate: CapabilityApprovalGate }
|
|
443
|
+
readonly journal?: CapabilityExecutionJournal
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Context passed to the owned effect callback. Use the signal for cancellation-aware I/O. */
|
|
447
|
+
export interface CapabilityExecutionContext {
|
|
448
|
+
readonly effectId: string
|
|
449
|
+
readonly signal: AbortSignal
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export type CapabilityExecutor<T> = (execution: CapabilityExecutionContext) => T | PromiseLike<T>
|
|
453
|
+
|
|
454
|
+
/** A capability admission policy returned without calling `next()`. */
|
|
455
|
+
export class CapabilityDeniedError extends Error {
|
|
456
|
+
constructor(
|
|
457
|
+
public readonly capability: string,
|
|
458
|
+
public readonly effectId: string,
|
|
459
|
+
) {
|
|
460
|
+
super(`capability assurance: ${capability} was denied by an async interceptor`)
|
|
461
|
+
this.name = "CapabilityDeniedError"
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** A capability admission policy exceeded its configured bound. */
|
|
466
|
+
export class CapabilityInterceptorTimeoutError extends Error {
|
|
467
|
+
constructor(
|
|
468
|
+
public readonly capability: string,
|
|
469
|
+
public readonly effectId: string,
|
|
470
|
+
public readonly timeoutMs: number,
|
|
471
|
+
) {
|
|
472
|
+
super(`capability assurance: ${capability} admission timed out after ${timeoutMs}ms`)
|
|
473
|
+
this.name = "CapabilityInterceptorTimeoutError"
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The request was cancelled while capability admission was pending. */
|
|
478
|
+
export class CapabilityAdmissionAbortedError extends Error {
|
|
479
|
+
constructor(
|
|
480
|
+
public readonly capability: string,
|
|
481
|
+
public readonly effectId: string,
|
|
482
|
+
) {
|
|
483
|
+
super(`capability assurance: ${capability} admission was aborted`)
|
|
484
|
+
this.name = "CapabilityAdmissionAbortedError"
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** An interceptor called its one-shot `next()` continuation more than once. */
|
|
489
|
+
export class CapabilityInterceptorProtocolError extends Error {
|
|
490
|
+
constructor(
|
|
491
|
+
public readonly capability: string,
|
|
492
|
+
public readonly effectId: string,
|
|
493
|
+
) {
|
|
494
|
+
super(`capability assurance: ${capability} interceptor called next() more than once`)
|
|
495
|
+
this.name = "CapabilityInterceptorProtocolError"
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** The effect may have committed, but its durable terminal transition could not be recorded. */
|
|
500
|
+
export class CapabilityJournalTransitionError extends Error {
|
|
501
|
+
constructor(
|
|
502
|
+
public readonly capability: string,
|
|
503
|
+
public readonly effectId: string,
|
|
504
|
+
public readonly transition: "committed",
|
|
505
|
+
) {
|
|
506
|
+
super(`capability assurance: ${capability} durable ${transition} transition failed`)
|
|
507
|
+
this.name = "CapabilityJournalTransitionError"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let unboundedCapabilitySignal: AbortSignal | undefined
|
|
512
|
+
|
|
513
|
+
function signalFor(context: object): AbortSignal {
|
|
514
|
+
const signal = (context as { readonly signal?: AbortSignal }).signal
|
|
515
|
+
if (signal !== undefined) return signal
|
|
516
|
+
unboundedCapabilitySignal ??= new AbortController().signal
|
|
517
|
+
return unboundedCapabilitySignal
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function admissionErrorCode(error: unknown): string {
|
|
521
|
+
if (error instanceof CapabilityDeniedError) return "admission_denied"
|
|
522
|
+
if (error instanceof CapabilityInterceptorTimeoutError) return "admission_timeout"
|
|
523
|
+
if (error instanceof CapabilityAdmissionAbortedError) return "admission_aborted"
|
|
524
|
+
if (error instanceof CapabilityInterceptorProtocolError) return "admission_protocol"
|
|
525
|
+
return "admission_failed"
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function withInterceptorBound(
|
|
529
|
+
registration: RegisteredCapabilityInterceptor,
|
|
530
|
+
parentSignal: AbortSignal,
|
|
531
|
+
capability: string,
|
|
532
|
+
effectId: string,
|
|
533
|
+
run: (signal: AbortSignal) => Promise<void>,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
if (parentSignal.aborted) throw new CapabilityAdmissionAbortedError(capability, effectId)
|
|
536
|
+
const controller = new AbortController()
|
|
537
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
538
|
+
let rejectGate: ((error: Error) => void) | undefined
|
|
539
|
+
const gate = new Promise<never>((_resolve, reject) => {
|
|
540
|
+
rejectGate = reject
|
|
541
|
+
})
|
|
542
|
+
const abort = (): void => {
|
|
543
|
+
const error = new CapabilityAdmissionAbortedError(capability, effectId)
|
|
544
|
+
controller.abort(error)
|
|
545
|
+
rejectGate?.(error)
|
|
546
|
+
}
|
|
547
|
+
parentSignal.addEventListener("abort", abort, { once: true })
|
|
548
|
+
timeout = setTimeout(() => {
|
|
549
|
+
const error = new CapabilityInterceptorTimeoutError(
|
|
550
|
+
capability,
|
|
551
|
+
effectId,
|
|
552
|
+
registration.timeoutMs,
|
|
553
|
+
)
|
|
554
|
+
controller.abort(error)
|
|
555
|
+
rejectGate?.(error)
|
|
556
|
+
}, registration.timeoutMs)
|
|
557
|
+
try {
|
|
558
|
+
await Promise.race([run(controller.signal), gate])
|
|
559
|
+
} finally {
|
|
560
|
+
clearTimeout(timeout)
|
|
561
|
+
parentSignal.removeEventListener("abort", abort)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function runCapabilityInterceptors(
|
|
566
|
+
registrations: readonly RegisteredCapabilityInterceptor[],
|
|
567
|
+
baseEvent: Omit<CapabilityInterceptorEvent, "signal">,
|
|
568
|
+
parentSignal: AbortSignal,
|
|
569
|
+
index = 0,
|
|
570
|
+
): Promise<void> {
|
|
571
|
+
const registration = registrations[index]
|
|
572
|
+
if (registration === undefined) return
|
|
573
|
+
let nextPromise: Promise<void> | undefined
|
|
574
|
+
await withInterceptorBound(
|
|
575
|
+
registration,
|
|
576
|
+
parentSignal,
|
|
577
|
+
baseEvent.capability,
|
|
578
|
+
baseEvent.effectId,
|
|
579
|
+
async (signal) => {
|
|
580
|
+
const event = Object.freeze({ ...baseEvent, signal })
|
|
581
|
+
await registration.interceptor(event, () => {
|
|
582
|
+
if (nextPromise !== undefined) {
|
|
583
|
+
throw new CapabilityInterceptorProtocolError(baseEvent.capability, baseEvent.effectId)
|
|
584
|
+
}
|
|
585
|
+
nextPromise = runCapabilityInterceptors(registrations, baseEvent, signal, index + 1)
|
|
586
|
+
return nextPromise
|
|
587
|
+
})
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
if (nextPromise === undefined) {
|
|
591
|
+
throw new CapabilityDeniedError(baseEvent.capability, baseEvent.effectId)
|
|
592
|
+
}
|
|
593
|
+
await nextPromise
|
|
594
|
+
}
|
|
595
|
+
|
|
412
596
|
function guardFor(context: object, capability: string): CapabilityGuard {
|
|
413
597
|
if (!validCapabilityId(capability)) {
|
|
414
598
|
throw new Error(
|
|
@@ -444,6 +628,7 @@ export function useCapability(
|
|
|
444
628
|
// Validate/append the intent before private admission hooks debit budgets. Overflow or malformed
|
|
445
629
|
// evidence therefore cannot consume quota and then fail the request.
|
|
446
630
|
ledger?.append({ capability, ...options, phase: "intent" })
|
|
631
|
+
guard.trackEffect?.(context, false)
|
|
447
632
|
guard.onUse?.({ capability, method: guard.method, path: guard.path })
|
|
448
633
|
}
|
|
449
634
|
|
|
@@ -453,10 +638,126 @@ export function recordCapabilityOutcome(
|
|
|
453
638
|
capability: string,
|
|
454
639
|
options: CapabilityOutcomeOptions,
|
|
455
640
|
): void {
|
|
456
|
-
guardFor(context, capability)
|
|
641
|
+
const guard = guardFor(context, capability)
|
|
457
642
|
const ledger = effectLedgerOf(context)
|
|
458
|
-
|
|
459
|
-
|
|
643
|
+
ledger?.append({ capability, ...options })
|
|
644
|
+
if (options.phase === "committed" || options.phase === "compensated") {
|
|
645
|
+
guard.trackEffect?.(context, true)
|
|
646
|
+
} else {
|
|
647
|
+
guard.trackEffect?.(context, false)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Execute one owned effect behind a fail-closed capability boundary. The boundary assigns a stable
|
|
653
|
+
* effect id, records intent before execution, and records exactly one terminal outcome automatically.
|
|
654
|
+
* The callback result and errors never enter the token-only ledger.
|
|
655
|
+
*/
|
|
656
|
+
export async function executeCapability<T>(
|
|
657
|
+
context: object,
|
|
658
|
+
capability: string,
|
|
659
|
+
options: CapabilityExecutionOptions,
|
|
660
|
+
executor: CapabilityExecutor<T>,
|
|
661
|
+
): Promise<T> {
|
|
662
|
+
if (typeof executor !== "function") {
|
|
663
|
+
throw new TypeError("capability assurance: executeCapability executor must be a function")
|
|
664
|
+
}
|
|
665
|
+
const guard = guardFor(context, capability)
|
|
666
|
+
const ledger = effectLedgerOf(context)
|
|
667
|
+
const effectId = crypto.randomUUID()
|
|
668
|
+
const metadata = normalizeEffectMetadata(options)
|
|
669
|
+
const trace = effectTraceParentOf(context)
|
|
670
|
+
const identity = options.approval
|
|
671
|
+
const requestEvidence = effectScopeForContext(context)
|
|
672
|
+
const scope = createEffectScope({}, requestEvidence)
|
|
673
|
+
const result = await scope.run(
|
|
674
|
+
{
|
|
675
|
+
effectId,
|
|
676
|
+
capability,
|
|
677
|
+
...metadata,
|
|
678
|
+
signal: signalFor(context),
|
|
679
|
+
observers: guard.observers,
|
|
680
|
+
...(trace === undefined ? {} : { trace }),
|
|
681
|
+
errorCode: (error, began) => (began ? "execution_failed" : admissionErrorCode(error)),
|
|
682
|
+
async admit(execution) {
|
|
683
|
+
if (identity !== undefined) {
|
|
684
|
+
await identity.gate.authorize({
|
|
685
|
+
effectId: execution.effectId,
|
|
686
|
+
capability,
|
|
687
|
+
...(metadata.target === undefined ? {} : { target: metadata.target }),
|
|
688
|
+
...(metadata.digest === undefined ? {} : { digest: metadata.digest }),
|
|
689
|
+
identity: { tenantId: identity.tenantId, principalId: identity.principalId },
|
|
690
|
+
...(identity.resumeToken === undefined ? {} : { resumeToken: identity.resumeToken }),
|
|
691
|
+
signal: execution.signal,
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
guard.onUse?.({ capability, method: guard.method, path: guard.path })
|
|
695
|
+
if (guard.interceptors.length > 0) {
|
|
696
|
+
await runCapabilityInterceptors(
|
|
697
|
+
guard.interceptors,
|
|
698
|
+
Object.freeze({
|
|
699
|
+
capability,
|
|
700
|
+
method: guard.method,
|
|
701
|
+
path: guard.path,
|
|
702
|
+
effectId: execution.effectId,
|
|
703
|
+
...metadata,
|
|
704
|
+
}),
|
|
705
|
+
execution.signal,
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
transitions: {
|
|
710
|
+
async intent(execution) {
|
|
711
|
+
ledger?.append({ capability, effectId: execution.effectId, ...metadata, phase: "intent" })
|
|
712
|
+
await options.journal?.intent({
|
|
713
|
+
effectId: execution.effectId,
|
|
714
|
+
capability,
|
|
715
|
+
...(metadata.target === undefined ? {} : { target: metadata.target }),
|
|
716
|
+
...(metadata.digest === undefined ? {} : { digest: metadata.digest }),
|
|
717
|
+
...(identity === undefined
|
|
718
|
+
? {}
|
|
719
|
+
: { identity: { tenantId: identity.tenantId, principalId: identity.principalId } }),
|
|
720
|
+
})
|
|
721
|
+
},
|
|
722
|
+
async executing(execution) {
|
|
723
|
+
await options.journal?.executing(execution.effectId)
|
|
724
|
+
},
|
|
725
|
+
async committed(execution) {
|
|
726
|
+
try {
|
|
727
|
+
await options.journal?.committed(execution.effectId)
|
|
728
|
+
} catch {
|
|
729
|
+
throw new CapabilityJournalTransitionError(capability, execution.effectId, "committed")
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
async failed(execution, failure) {
|
|
733
|
+
try {
|
|
734
|
+
await options.journal?.failed(execution.effectId, {
|
|
735
|
+
began: failure.began,
|
|
736
|
+
errorCode: failure.errorCode,
|
|
737
|
+
})
|
|
738
|
+
} finally {
|
|
739
|
+
try {
|
|
740
|
+
ledger?.append({
|
|
741
|
+
capability,
|
|
742
|
+
effectId: execution.effectId,
|
|
743
|
+
...metadata,
|
|
744
|
+
phase: "failed",
|
|
745
|
+
error: { code: failure.errorCode },
|
|
746
|
+
})
|
|
747
|
+
} catch {
|
|
748
|
+
// Preserve the original effect failure when terminal evidence overflows.
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
(execution) =>
|
|
755
|
+
executor(Object.freeze({ effectId: execution.effectId, signal: execution.signal })),
|
|
756
|
+
)
|
|
757
|
+
// Durable commit and success telemetry are already final. A ledger overflow here must not rewrite
|
|
758
|
+
// the effect to failed/unknown or erase the request scope's proven committed evidence.
|
|
759
|
+
ledger?.append({ capability, effectId, ...metadata, phase: "committed" })
|
|
760
|
+
return result
|
|
460
761
|
}
|
|
461
762
|
|
|
462
763
|
/**
|