@nifrajs/core 4.0.0 → 5.0.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 +6 -2
- package/dist/assurance.d.ts +71 -5
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +90 -25
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +52 -0
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-effect.js +1 -1
- package/dist/durable-effect.js.map +1 -1
- package/dist/durable-saga.d.ts +6 -0
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +83 -10
- package/dist/durable-saga.js.map +1 -1
- package/dist/edge-kit.d.ts +7 -2
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +7 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +16 -5
- package/dist/effect-scope.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 +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +9 -1
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-shared.d.ts +1 -2
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +25 -44
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/durable-types.d.ts +3 -0
- package/dist/internal/durable-types.d.ts.map +1 -1
- package/dist/internal/hook-origins.d.ts +67 -0
- package/dist/internal/hook-origins.d.ts.map +1 -0
- package/dist/internal/hook-origins.js +86 -0
- package/dist/internal/hook-origins.js.map +1 -0
- package/dist/internal/route-assurance.d.ts +6 -3
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +29 -14
- package/dist/internal/route-assurance.js.map +1 -1
- package/dist/internal/route-program.d.ts +1 -1
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +59 -16
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reconciliation-worker.d.ts.map +1 -1
- package/dist/reconciliation-worker.js +12 -2
- package/dist/reconciliation-worker.js.map +1 -1
- package/dist/reflection.d.ts +53 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +352 -5
- package/dist/reflection.js.map +1 -1
- package/dist/router/pattern.d.ts +1 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +6 -3
- package/dist/router/pattern.js.map +1 -1
- package/dist/security/baseline.d.ts +3 -0
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +9 -5
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/auth.d.ts +32 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +23 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +34 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +3 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -3
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +38 -17
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +3 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/methods.d.ts +1 -0
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js +1 -0
- package/dist/server/methods.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +29 -11
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/platform-adapters.d.ts.map +1 -1
- package/dist/server/platform-adapters.js +3 -2
- package/dist/server/platform-adapters.js.map +1 -1
- package/dist/server/request-context.d.ts +2 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +44 -11
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +192 -79
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -41
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +2 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +29 -5
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +44 -31
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +444 -321
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +11 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +7 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.d.ts +5 -2
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +17 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +22 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +21 -0
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-hook.d.ts +7 -1
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +6 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +20 -1
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +71 -40
- package/dist/tool-contract.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +3 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +180 -31
- package/src/capabilities.ts +59 -0
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +7 -2
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +6 -1
- package/src/internal/durable-shared.ts +25 -37
- package/src/internal/durable-types.ts +3 -0
- package/src/internal/hook-origins.ts +153 -0
- package/src/internal/route-assurance.ts +32 -18
- package/src/internal/route-program.ts +98 -44
- package/src/reconciliation-worker.ts +10 -2
- package/src/reflection.ts +455 -4
- package/src/router/pattern.ts +6 -3
- package/src/security/baseline.ts +10 -5
- package/src/server/auth.ts +45 -3
- package/src/server/body.ts +34 -1
- package/src/server/client-ip.ts +3 -0
- package/src/server/context.ts +2 -1
- package/src/server/idempotency-lane.ts +64 -22
- package/src/server/install.ts +3 -0
- package/src/server/logger.ts +3 -1
- package/src/server/methods.ts +1 -0
- package/src/server/node-outcome.ts +34 -12
- package/src/server/platform-adapters.ts +3 -2
- package/src/server/request-context.ts +5 -0
- package/src/server/respond.ts +222 -69
- package/src/server/response-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +29 -5
- package/src/server/server.ts +659 -530
- package/src/server/sse.ts +84 -55
- package/src/server/transport-lane.ts +7 -1
- package/src/server/webhook.ts +22 -3
- package/src/server/websocket.ts +44 -5
- package/src/server/ws-hook.ts +5 -0
- package/src/server.ts +5 -0
- package/src/single-copy.ts +20 -1
- package/src/tool-contract.ts +67 -39
- package/src/ws.ts +4 -0
package/src/errors.ts
CHANGED
package/src/evidence.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface ProjectEvidenceSchemaPart {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface ProjectEvidenceSchema {
|
|
37
|
+
/** Set only when the route chose it; absent means the default for its auth stages. */
|
|
38
|
+
readonly validationOrder?: "validate-before-auth" | "auth-before-validation"
|
|
37
39
|
readonly bodyLimit?: number | "unlimited"
|
|
38
40
|
readonly bodyLimitReason?: string
|
|
39
41
|
readonly headers?: ProjectEvidenceSchemaPart
|
|
@@ -160,6 +162,7 @@ function schemaOf(route: ReflectedRoute): ProjectEvidenceSchema | undefined {
|
|
|
160
162
|
if (part !== undefined) errors[status] = part
|
|
161
163
|
}
|
|
162
164
|
const schema: ProjectEvidenceSchema = {
|
|
165
|
+
...(source.validationOrder !== undefined ? { validationOrder: source.validationOrder } : {}),
|
|
163
166
|
...(source.bodyLimit !== undefined ? { bodyLimit: source.bodyLimit } : {}),
|
|
164
167
|
...(source.bodyLimitReason !== undefined ? { bodyLimitReason: source.bodyLimitReason } : {}),
|
|
165
168
|
...(headers !== undefined ? { headers } : {}),
|
|
@@ -328,9 +331,14 @@ function remapEvidenceSnapshot(
|
|
|
328
331
|
const key = route(item.method, item.path)
|
|
329
332
|
return Object.freeze({ ...item, ...key })
|
|
330
333
|
})
|
|
331
|
-
const mounts = evidence.mounts?.map((item) =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
+
const mounts = evidence.mounts?.map((item) => {
|
|
335
|
+
const path = composedPath(pathPrefix, item.path)
|
|
336
|
+
if (item.routes === undefined) return Object.freeze({ ...item, path })
|
|
337
|
+
const routes = item.routes.map((declared) =>
|
|
338
|
+
Object.freeze({ ...declared, path: composedPath(pathPrefix, declared.path) }),
|
|
339
|
+
)
|
|
340
|
+
return Object.freeze({ ...item, path, routes: Object.freeze(routes) })
|
|
341
|
+
})
|
|
334
342
|
const assurance =
|
|
335
343
|
evidence.assurance === undefined
|
|
336
344
|
? undefined
|
|
@@ -499,6 +507,7 @@ function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): Reflecte
|
|
|
499
507
|
const response = schemaPartToReflection(schema.response)
|
|
500
508
|
const sse = schemaPartToReflection(schema.sse)
|
|
501
509
|
return {
|
|
510
|
+
...(schema.validationOrder !== undefined ? { validationOrder: schema.validationOrder } : {}),
|
|
502
511
|
...(schema.bodyLimit !== undefined ? { bodyLimit: schema.bodyLimit } : {}),
|
|
503
512
|
...(schema.bodyLimitReason !== undefined ? { bodyLimitReason: schema.bodyLimitReason } : {}),
|
|
504
513
|
...(headers !== undefined ? { headers } : {}),
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
|
|
17
17
|
* `check:publish` asserts it equals `@nifrajs/core`'s package version.
|
|
18
18
|
*/
|
|
19
|
-
export const VERSION = "
|
|
19
|
+
export const VERSION = "5.0.0" as const
|
|
20
20
|
|
|
21
21
|
export type Version = typeof VERSION
|
|
22
22
|
|
|
@@ -54,6 +54,8 @@ export {
|
|
|
54
54
|
type AuthHeaders,
|
|
55
55
|
type AuthMaybePromise,
|
|
56
56
|
authenticated,
|
|
57
|
+
isAuthenticatedRequest,
|
|
58
|
+
markAuthenticatedRequest,
|
|
57
59
|
rejected,
|
|
58
60
|
} from "./server/auth.ts"
|
|
59
61
|
export type {
|
|
@@ -116,6 +118,7 @@ export type {
|
|
|
116
118
|
RouteInfoFor,
|
|
117
119
|
} from "./server/registry.ts"
|
|
118
120
|
export { replacedRequestOf } from "./server/request-lineage.ts"
|
|
121
|
+
export { writableResponse } from "./server/respond.ts"
|
|
119
122
|
export {
|
|
120
123
|
type PlainRender,
|
|
121
124
|
type ResponseResult,
|
|
@@ -127,6 +130,7 @@ export {
|
|
|
127
130
|
type AdmissionDecision,
|
|
128
131
|
type AnyServer,
|
|
129
132
|
type ContextPlugin,
|
|
133
|
+
type DeclaredMountRoute,
|
|
130
134
|
type DefinePluginResult,
|
|
131
135
|
defineContextPlugin,
|
|
132
136
|
defineIdentityPlugin,
|
|
@@ -150,6 +154,7 @@ export {
|
|
|
150
154
|
type NodeResponseHook,
|
|
151
155
|
type NodeServeOutcome,
|
|
152
156
|
type OnRequestResult,
|
|
157
|
+
type OpaqueMountDeclaration,
|
|
153
158
|
type PluginTypeCollapsed,
|
|
154
159
|
type PromptArgument,
|
|
155
160
|
type PromptMessage,
|
|
@@ -63,12 +63,22 @@ export function reconciliationLimit(value = DEFAULT_RECONCILIATION_LIMIT): numbe
|
|
|
63
63
|
return value
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
/**
|
|
67
|
+
* One page of `matches` in id order, the cursor being the last id returned. Keyed rather than an
|
|
68
|
+
* offset into the matching set: a caller reconciles each page before asking for the next, so the
|
|
69
|
+
* set shrinks under it, and an offset then lands past records that were never returned.
|
|
70
|
+
*/
|
|
71
|
+
function keysetPage<Record>(
|
|
72
|
+
matches: [id: string, record: Record][],
|
|
73
|
+
limit: number,
|
|
74
|
+
): ReconciliationScanPage<Record> {
|
|
75
|
+
// Code-unit order, the order the `id > cursor` comparisons use.
|
|
76
|
+
matches.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
77
|
+
const page = matches.slice(0, limit)
|
|
78
|
+
const records = Object.freeze(page.map(([, record]) => cloneValue(record)))
|
|
79
|
+
const last = page.at(-1)
|
|
80
|
+
if (matches.length <= limit || last === undefined) return Object.freeze({ records })
|
|
81
|
+
return Object.freeze({ records, cursor: last[0] })
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
export function memoryScan<
|
|
@@ -76,28 +86,18 @@ export function memoryScan<
|
|
|
76
86
|
State extends string,
|
|
77
87
|
>(
|
|
78
88
|
source: Iterable<Record>,
|
|
89
|
+
idOf: (record: Record) => string,
|
|
79
90
|
input: ReconciliationScanOptions<State>,
|
|
80
91
|
): ReconciliationScanPage<Record> {
|
|
81
92
|
const states = new Set(input.states)
|
|
82
|
-
const
|
|
83
|
-
const end = offset + input.limit
|
|
84
|
-
const records: Record[] = []
|
|
85
|
-
let seen = 0
|
|
86
|
-
let more = false
|
|
93
|
+
const matches: [string, Record][] = []
|
|
87
94
|
for (const record of source) {
|
|
88
95
|
if (!states.has(record.state)) continue
|
|
89
96
|
if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
if (seen >= offset) records.push(cloneValue(record))
|
|
95
|
-
seen++
|
|
97
|
+
const id = idOf(record)
|
|
98
|
+
if (input.cursor === undefined || id > input.cursor) matches.push([id, record])
|
|
96
99
|
}
|
|
97
|
-
return
|
|
98
|
-
records: Object.freeze(records),
|
|
99
|
-
...(more ? { cursor: String(offset + records.length) } : {}),
|
|
100
|
-
})
|
|
100
|
+
return keysetPage(matches, input.limit)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function bucketAdd<State extends string>(
|
|
@@ -129,29 +129,17 @@ export function indexedScan<Record extends { readonly updatedAt: number }, State
|
|
|
129
129
|
resolve: (id: string) => Record | undefined,
|
|
130
130
|
input: ReconciliationScanOptions<State>,
|
|
131
131
|
): ReconciliationScanPage<Record> {
|
|
132
|
-
const
|
|
133
|
-
const end = offset + input.limit
|
|
134
|
-
const records: Record[] = []
|
|
135
|
-
let seen = 0
|
|
136
|
-
let more = false
|
|
132
|
+
const matches: [string, Record][] = []
|
|
137
133
|
for (const state of new Set(input.states)) {
|
|
138
|
-
if (more) break
|
|
139
134
|
const bucket = byState.get(state)
|
|
140
135
|
if (bucket === undefined) continue
|
|
141
136
|
for (const id of bucket) {
|
|
137
|
+
if (input.cursor !== undefined && id <= input.cursor) continue
|
|
142
138
|
const record = resolve(id)
|
|
143
139
|
if (record === undefined) continue
|
|
144
140
|
if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
|
|
145
|
-
|
|
146
|
-
more = true
|
|
147
|
-
break
|
|
148
|
-
}
|
|
149
|
-
if (seen >= offset) records.push(cloneValue(record))
|
|
150
|
-
seen++
|
|
141
|
+
matches.push([id, record])
|
|
151
142
|
}
|
|
152
143
|
}
|
|
153
|
-
return
|
|
154
|
-
records: Object.freeze(records),
|
|
155
|
-
...(more ? { cursor: String(offset + records.length) } : {}),
|
|
156
|
-
})
|
|
144
|
+
return keysetPage(matches, input.limit)
|
|
157
145
|
}
|
|
@@ -138,6 +138,9 @@ export interface SagaRecord {
|
|
|
138
138
|
readonly createdAt: number
|
|
139
139
|
readonly updatedAt: number
|
|
140
140
|
readonly version: number
|
|
141
|
+
/** The `resume`/`compensate` call that owns the saga while `leaseUntil` is in the future. */
|
|
142
|
+
readonly owner?: string
|
|
143
|
+
readonly leaseUntil?: number
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
export interface SagaStore {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each registered hook came from, for `reflectHooks` (`nifra routes --hooks`). Development-only:
|
|
3
|
+
* every write sits behind the inline `process.env.NODE_ENV !== "production"` guard at its call site in
|
|
4
|
+
* `server.ts`, so a production define folds the writes away and this module drops out of the bundle.
|
|
5
|
+
* Reads happen only from reflection, never on the request path.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type HookKind =
|
|
9
|
+
| "onRequest"
|
|
10
|
+
| "around"
|
|
11
|
+
| "authenticate"
|
|
12
|
+
| "derive"
|
|
13
|
+
| "beforeHandle"
|
|
14
|
+
| "afterHandle"
|
|
15
|
+
| "onError"
|
|
16
|
+
| "onResponse"
|
|
17
|
+
| "responseHeaders"
|
|
18
|
+
| "onResponseFinalized"
|
|
19
|
+
|
|
20
|
+
export interface HookOrigin {
|
|
21
|
+
readonly kind: HookKind
|
|
22
|
+
/** The server the hook was registered on. */
|
|
23
|
+
readonly owner: object
|
|
24
|
+
/** The plugin being applied when it was registered, if any. */
|
|
25
|
+
readonly plugin: string | undefined
|
|
26
|
+
/** A group's static headers turned into this hook at adoption: report the owner's declarations. */
|
|
27
|
+
readonly statics?: true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Which requests an adopted hook still runs for: under a group prefix, or a merged server's routes. */
|
|
31
|
+
export type HookScopeTag =
|
|
32
|
+
| { readonly inner: unknown; readonly prefix: string }
|
|
33
|
+
| {
|
|
34
|
+
readonly inner: unknown
|
|
35
|
+
readonly routes: readonly { readonly method: string; readonly path: string }[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One `responseHeaders()` declaration that reached a server's static tier. */
|
|
39
|
+
export interface StaticHeadersTag {
|
|
40
|
+
readonly owner: object
|
|
41
|
+
readonly names: readonly string[]
|
|
42
|
+
readonly source: unknown
|
|
43
|
+
readonly plugin: string | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AdoptionTag {
|
|
47
|
+
readonly source: object
|
|
48
|
+
readonly prefix: string | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const origins = new WeakMap<object, HookOrigin>()
|
|
52
|
+
const scopes = new WeakMap<object, HookScopeTag>()
|
|
53
|
+
const sources = new WeakMap<object, unknown>()
|
|
54
|
+
const statics = new WeakMap<object, StaticHeadersTag[]>()
|
|
55
|
+
const adoptions = new WeakMap<object, AdoptionTag[]>()
|
|
56
|
+
let applying: string | undefined
|
|
57
|
+
// A merged server whose static headers are about to become one of the target's response hooks.
|
|
58
|
+
let staticsOwner: object | undefined
|
|
59
|
+
|
|
60
|
+
const isRef = (value: unknown): value is object =>
|
|
61
|
+
(typeof value === "function" || typeof value === "object") && value !== null
|
|
62
|
+
|
|
63
|
+
/** Record a hook's origin. The first registration wins, so a hook re-adopted elsewhere keeps its site. */
|
|
64
|
+
export function tagHook(hook: unknown, kind: HookKind, owner: object, groupStatics?: true): void {
|
|
65
|
+
if (!isRef(hook) || origins.has(hook)) return
|
|
66
|
+
if (kind === "responseHeaders" && staticsOwner !== undefined) {
|
|
67
|
+
origins.set(hook, { kind, owner: staticsOwner, plugin: applying, statics: true })
|
|
68
|
+
staticsOwner = undefined
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
origins.set(
|
|
72
|
+
hook,
|
|
73
|
+
groupStatics === true
|
|
74
|
+
? { kind, owner, plugin: applying, statics: true }
|
|
75
|
+
: { kind, owner, plugin: applying },
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A merge folds the source's static headers into the target: into its static tier, or - once the
|
|
81
|
+
* target has a response hook - into a new hook, which the next `responseHeaders` tag then is.
|
|
82
|
+
*/
|
|
83
|
+
export function adoptStaticTags(target: object, source: object, becomesHook: boolean): void {
|
|
84
|
+
if (becomesHook) {
|
|
85
|
+
staticsOwner = source
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
for (const tag of statics.get(source) ?? []) {
|
|
89
|
+
tagStatic(target, tag.names, tag.source, tag.owner, tag.plugin)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The author's function (or header record) an internal wrapper adapts, for naming it. */
|
|
94
|
+
export function tagSource(wrapper: unknown, source: unknown): void {
|
|
95
|
+
if (isRef(wrapper)) sources.set(wrapper, source)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Pair each scoped wrapper with the hook it gates and the scope it gates it by. */
|
|
99
|
+
export function tagScoped(
|
|
100
|
+
originals: readonly unknown[],
|
|
101
|
+
wrapped: readonly unknown[],
|
|
102
|
+
scope:
|
|
103
|
+
| { readonly prefix: string }
|
|
104
|
+
| { readonly routes: readonly { readonly method: string; readonly path: string }[] },
|
|
105
|
+
): void {
|
|
106
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
107
|
+
const wrapper = wrapped[i]
|
|
108
|
+
if (isRef(wrapper) && wrapper !== originals[i])
|
|
109
|
+
scopes.set(wrapper, { inner: originals[i], ...scope })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function tagStatic(
|
|
114
|
+
server: object,
|
|
115
|
+
names: readonly string[],
|
|
116
|
+
source: unknown,
|
|
117
|
+
owner: object = server,
|
|
118
|
+
plugin: string | undefined = applying,
|
|
119
|
+
): void {
|
|
120
|
+
const list = statics.get(server)
|
|
121
|
+
const tag = { owner, names: names.map((name) => name.toLowerCase()), source, plugin }
|
|
122
|
+
if (list === undefined) statics.set(server, [tag])
|
|
123
|
+
else list.push(tag)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function tagAdoption(target: object, source: object, prefix: string | undefined): void {
|
|
127
|
+
const list = adoptions.get(target)
|
|
128
|
+
const tag = { source, prefix }
|
|
129
|
+
if (list === undefined) adoptions.set(target, [tag])
|
|
130
|
+
else list.push(tag)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const outer: (string | undefined)[] = []
|
|
134
|
+
|
|
135
|
+
/** Mark the plugin a `use()` is applying, until the matching {@link leavePlugin}. */
|
|
136
|
+
export function enterPlugin(name: string | undefined): void {
|
|
137
|
+
outer.push(applying)
|
|
138
|
+
if (name !== undefined) applying = name
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function leavePlugin(): void {
|
|
142
|
+
applying = outer.pop()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const hookOriginOf = (hook: unknown): HookOrigin | undefined =>
|
|
146
|
+
isRef(hook) ? origins.get(hook) : undefined
|
|
147
|
+
export const hookScopeOf = (hook: unknown): HookScopeTag | undefined =>
|
|
148
|
+
isRef(hook) ? scopes.get(hook) : undefined
|
|
149
|
+
export const hookSourceOf = (hook: unknown): unknown =>
|
|
150
|
+
isRef(hook) ? sources.get(hook) : undefined
|
|
151
|
+
export const staticHeadersOf = (server: object): readonly StaticHeadersTag[] =>
|
|
152
|
+
statics.get(server) ?? []
|
|
153
|
+
export const adoptionsOf = (server: object): readonly AdoptionTag[] => adoptions.get(server) ?? []
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { METHODS, type Method } from "../router/router.ts"
|
|
1
|
+
import { isRegistrableMethod, METHODS, type Method, type RouteMethod } from "../router/router.ts"
|
|
2
2
|
|
|
3
3
|
/** Where enforcement evidence follows Nifra's route-registration semantics. */
|
|
4
4
|
export type AssuranceScope = "global" | "subsequent" | "plugin"
|
|
@@ -15,8 +15,11 @@ export interface AssuranceEvidence {
|
|
|
15
15
|
/** Metadata installed on a middleware/plugin by {@link withRouteAssurance}. */
|
|
16
16
|
export interface AssuranceDeclaration extends AssuranceEvidence {
|
|
17
17
|
readonly scope: AssuranceScope
|
|
18
|
-
/** Restrict evidence to these HTTP methods. Omit for every
|
|
19
|
-
|
|
18
|
+
/** Restrict evidence to these HTTP methods, a custom one such as `PURGE` included. Omit for every
|
|
19
|
+
* method. */
|
|
20
|
+
readonly methods?: readonly RouteMethod[]
|
|
21
|
+
/** Withhold evidence from these HTTP methods. Every other method keeps it, custom ones included. */
|
|
22
|
+
readonly exceptMethods?: readonly RouteMethod[]
|
|
20
23
|
/** Restrict evidence to these absolute route globs. Omit for every path. */
|
|
21
24
|
readonly paths?: readonly string[]
|
|
22
25
|
}
|
|
@@ -117,12 +120,8 @@ function normalizeDeclaration(value: AssuranceDeclaration): AssuranceDeclaration
|
|
|
117
120
|
if (value.scope !== "global" && value.scope !== "subsequent" && value.scope !== "plugin") {
|
|
118
121
|
throw new Error(`route assurance: invalid scope ${JSON.stringify(value.scope)}`)
|
|
119
122
|
}
|
|
120
|
-
const methods = value.methods
|
|
121
|
-
|
|
122
|
-
if (!METHOD_SET.has(method)) {
|
|
123
|
-
throw new Error(`route assurance: unsupported HTTP method ${JSON.stringify(method)}`)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
123
|
+
const methods = registrableMethods(value.methods)
|
|
124
|
+
const exceptMethods = registrableMethods(value.exceptMethods)
|
|
126
125
|
const paths = value.paths?.map((path) => {
|
|
127
126
|
routeGlob(path)
|
|
128
127
|
return path
|
|
@@ -134,16 +133,31 @@ function normalizeDeclaration(value: AssuranceDeclaration): AssuranceDeclaration
|
|
|
134
133
|
) {
|
|
135
134
|
throw new Error(`route assurance: invalid provenance ${JSON.stringify(value.provenance)}`)
|
|
136
135
|
}
|
|
137
|
-
|
|
136
|
+
const declaration: { -readonly [K in keyof AssuranceDeclaration]: AssuranceDeclaration[K] } = {
|
|
138
137
|
id: value.id,
|
|
139
138
|
source: value.source.trim(),
|
|
140
139
|
scope: value.scope,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
}
|
|
141
|
+
// Carried, not defaulted: evidence attached by an author (rather than installed by a running
|
|
142
|
+
// hook) must stay distinguishable so `requireProvenance: "runtime"` rules can still reject it.
|
|
143
|
+
if (value.provenance !== undefined) declaration.provenance = value.provenance
|
|
144
|
+
if (methods !== undefined) declaration.methods = methods
|
|
145
|
+
if (exceptMethods !== undefined) declaration.exceptMethods = exceptMethods
|
|
146
|
+
if (paths !== undefined) declaration.paths = Object.freeze(paths)
|
|
147
|
+
return Object.freeze(declaration)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function registrableMethods(
|
|
151
|
+
values: readonly string[] | undefined,
|
|
152
|
+
): readonly RouteMethod[] | undefined {
|
|
153
|
+
if (values === undefined) return undefined
|
|
154
|
+
const methods = values.map((method) => method.toUpperCase())
|
|
155
|
+
for (const method of methods) {
|
|
156
|
+
if (!isRegistrableMethod(method)) {
|
|
157
|
+
throw new Error(`route assurance: unsupported HTTP method ${JSON.stringify(method)}`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return Object.freeze(methods)
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
/** Validate and freeze declarations, one or many. */
|
|
@@ -179,8 +193,8 @@ export function declarationApplies(
|
|
|
179
193
|
method: string,
|
|
180
194
|
path: string,
|
|
181
195
|
): boolean {
|
|
182
|
-
if (declaration.methods !== undefined && !declaration.methods.includes(method
|
|
183
|
-
|
|
196
|
+
if (declaration.methods !== undefined && !declaration.methods.includes(method)) return false
|
|
197
|
+
if (declaration.exceptMethods?.includes(method) === true) return false
|
|
184
198
|
return (
|
|
185
199
|
declaration.paths === undefined ||
|
|
186
200
|
declaration.paths.some((pattern) => routeGlob(pattern).test(path))
|