@nifrajs/core 5.5.1 → 5.6.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/CHANGELOG.md +153 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +2 -0
- package/dist/evidence.js.map +1 -1
- package/dist/file-type.d.ts +30 -0
- package/dist/file-type.d.ts.map +1 -0
- package/dist/file-type.js +118 -0
- package/dist/file-type.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/route-program.d.ts +11 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +133 -85
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reflection.d.ts +14 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +34 -7
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -0
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js +1 -0
- package/dist/response-observer.js.map +1 -1
- package/dist/server/bare-error-lane.d.ts +4 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +9 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/before-handler.d.ts +10 -0
- package/dist/server/before-handler.d.ts.map +1 -0
- package/dist/server/before-handler.js +12 -0
- package/dist/server/before-handler.js.map +1 -0
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +8 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts.map +1 -1
- package/dist/server/body-parser.js +2 -5
- package/dist/server/body-parser.js.map +1 -1
- package/dist/server/body.d.ts +24 -8
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +86 -33
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +6 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/multipart.d.ts +3 -0
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +226 -97
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +39 -4
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -14
- package/dist/server/query.js.map +1 -1
- package/dist/server/repeated-prefix.d.ts +17 -0
- package/dist/server/repeated-prefix.d.ts.map +1 -0
- package/dist/server/repeated-prefix.js +17 -0
- package/dist/server/repeated-prefix.js.map +1 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +1 -3
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +1 -3
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +21 -16
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +12 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +135 -8
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/runtime-core.d.ts +7 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +8 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -2
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +6 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +239 -78
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +5 -4
- package/dist/server/sse.js.map +1 -1
- package/dist/server/webhook.d.ts +1 -1
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +24 -19
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +18 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +45 -5
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +7 -4
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/tool-cli.d.ts +64 -0
- package/dist/tool-cli.d.ts.map +1 -0
- package/dist/tool-cli.js +495 -0
- package/dist/tool-cli.js.map +1 -0
- package/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/evidence.ts +4 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +2 -1
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +53 -7
- package/src/response-observer.ts +6 -0
- package/src/server/bare-error-lane.ts +9 -4
- package/src/server/before-handler.ts +14 -0
- package/src/server/body-lane.ts +19 -9
- package/src/server/body-parser.ts +1 -5
- package/src/server/body.ts +96 -36
- package/src/server/core-copies.ts +1 -1
- package/src/server/http.ts +6 -15
- package/src/server/multipart.ts +232 -100
- package/src/server/proto-guard.ts +39 -4
- package/src/server/query.ts +7 -13
- package/src/server/repeated-prefix.ts +25 -0
- package/src/server/request-context.ts +1 -3
- package/src/server/respond.ts +23 -15
- package/src/server/response-contract-lane.ts +130 -8
- package/src/server/runtime-core.ts +10 -1
- package/src/server/server-types.ts +3 -2
- package/src/server/server.ts +265 -76
- package/src/server/sse.ts +6 -5
- package/src/server/webhook.ts +29 -24
- package/src/server/websocket.ts +57 -8
- package/src/server/ws-bun.ts +7 -3
- package/src/server.ts +1 -0
- package/src/tool-cli.ts +584 -0
package/src/file-type.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magic-byte file-type detection - trust the bytes, not the `Content-Type` header (which a client
|
|
3
|
+
* sets freely). Reads only the leading bytes; imports nothing, so it is edge-safe. Covers the common
|
|
4
|
+
* upload types; returns `null` for anything unrecognized (incl. text formats like SVG/CSV that have no
|
|
5
|
+
* magic number - handle those explicitly if you accept them).
|
|
6
|
+
*
|
|
7
|
+
* The shared owner for every type-by-bytes check: `@nifrajs/schema/form`'s `t.file({ accept })` and
|
|
8
|
+
* `@nifrajs/uploads`'s `validateUpload` must give one file one verdict, so both read this module.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface FileType {
|
|
12
|
+
/** Detected MIME type, e.g. `"image/png"`. */
|
|
13
|
+
readonly mime: string
|
|
14
|
+
/** Canonical extension (no dot), e.g. `"png"`. */
|
|
15
|
+
readonly ext: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How many leading bytes {@link detectFileType} looks at. Reading this many from the start of a file
|
|
20
|
+
* (`file.slice(0, FILE_TYPE_PREFIX_BYTES)`) is enough for every type it knows: it reaches the first four
|
|
21
|
+
* compatible brands of an ISO-BMFF `ftyp` box, where an AVIF with the generic `mif1` brand says `avif`.
|
|
22
|
+
*/
|
|
23
|
+
export const FILE_TYPE_PREFIX_BYTES = 32
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every MIME type {@link detectFileType} can return. An allow-list entry outside this set (and
|
|
27
|
+
* outside a `type/*` wildcard that covers one of them) can never match, so check a configured
|
|
28
|
+
* allow-list against it up front instead of rejecting every upload at request time.
|
|
29
|
+
*/
|
|
30
|
+
export const DETECTABLE_MIME_TYPES: readonly string[] = [
|
|
31
|
+
"image/jpeg",
|
|
32
|
+
"image/png",
|
|
33
|
+
"image/gif",
|
|
34
|
+
"image/webp",
|
|
35
|
+
"audio/wav",
|
|
36
|
+
"video/x-msvideo",
|
|
37
|
+
"image/avif",
|
|
38
|
+
"image/heic",
|
|
39
|
+
"audio/mp4",
|
|
40
|
+
"video/mp4",
|
|
41
|
+
"video/webm",
|
|
42
|
+
"audio/ogg",
|
|
43
|
+
"audio/mpeg",
|
|
44
|
+
"application/pdf",
|
|
45
|
+
"application/zip",
|
|
46
|
+
"application/gzip",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An MPEG audio frame header: an 11-bit frame sync, then a version, layer, bitrate and sample rate that
|
|
51
|
+
* are not reserved values. An MP3 needs no ID3 tag, so this is how an untagged one starts. AAC's ADTS
|
|
52
|
+
* sync shares the first 12 bits, but its layer is always the reserved `00`.
|
|
53
|
+
*/
|
|
54
|
+
function mpegAudioFrame(bytes: Uint8Array): boolean {
|
|
55
|
+
const [b0 = 0, b1 = 0, b2 = 0] = bytes
|
|
56
|
+
if (bytes.length < 4 || b0 !== 0xff || (b1 & 0xe0) !== 0xe0) return false
|
|
57
|
+
const version = (b1 >> 3) & 0b11
|
|
58
|
+
const layer = (b1 >> 1) & 0b11
|
|
59
|
+
const bitrate = b2 >> 4
|
|
60
|
+
const sampleRate = (b2 >> 2) & 0b11
|
|
61
|
+
return version !== 0b01 && layer !== 0b00 && bitrate !== 0b1111 && sampleRate !== 0b11
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Detect a file's type from its magic bytes, or `null` if unrecognized. */
|
|
65
|
+
export function detectFileType(bytes: Uint8Array): FileType | null {
|
|
66
|
+
const at = (offset: number, ...sig: number[]): boolean =>
|
|
67
|
+
sig.every((byte, i) => bytes[offset + i] === byte)
|
|
68
|
+
|
|
69
|
+
// Images
|
|
70
|
+
if (at(0, 0xff, 0xd8, 0xff)) return { mime: "image/jpeg", ext: "jpg" }
|
|
71
|
+
if (at(0, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
|
|
72
|
+
return { mime: "image/png", ext: "png" }
|
|
73
|
+
if (at(0, 0x47, 0x49, 0x46, 0x38)) return { mime: "image/gif", ext: "gif" }
|
|
74
|
+
|
|
75
|
+
// RIFF container (bytes 0-3 "RIFF", 8-11 the form type)
|
|
76
|
+
if (at(0, 0x52, 0x49, 0x46, 0x46)) {
|
|
77
|
+
if (at(8, 0x57, 0x45, 0x42, 0x50)) return { mime: "image/webp", ext: "webp" }
|
|
78
|
+
if (at(8, 0x57, 0x41, 0x56, 0x45)) return { mime: "audio/wav", ext: "wav" }
|
|
79
|
+
if (at(8, 0x41, 0x56, 0x49, 0x20)) return { mime: "video/x-msvideo", ext: "avi" }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ISO-BMFF (`ftyp` box at offset 4) - mp4 / avif / heic / m4a, disambiguated by the major brand.
|
|
83
|
+
if (at(4, 0x66, 0x74, 0x79, 0x70)) {
|
|
84
|
+
const brandAt = (offset: number): string =>
|
|
85
|
+
String.fromCharCode(
|
|
86
|
+
bytes[offset] ?? 0,
|
|
87
|
+
bytes[offset + 1] ?? 0,
|
|
88
|
+
bytes[offset + 2] ?? 0,
|
|
89
|
+
bytes[offset + 3] ?? 0,
|
|
90
|
+
)
|
|
91
|
+
const brand = brandAt(8)
|
|
92
|
+
// The compatible brands follow the minor version, to the end of the ftyp box. Only those inside the
|
|
93
|
+
// prefix count: the box size is the sender's claim, and a verdict must not depend on bytes a caller
|
|
94
|
+
// that sniffs `FILE_TYPE_PREFIX_BYTES` never reads.
|
|
95
|
+
const boxEnd = Math.min(
|
|
96
|
+
bytes.length,
|
|
97
|
+
FILE_TYPE_PREFIX_BYTES,
|
|
98
|
+
((bytes[0] ?? 0) << 24) | ((bytes[1] ?? 0) << 16) | ((bytes[2] ?? 0) << 8) | (bytes[3] ?? 0),
|
|
99
|
+
)
|
|
100
|
+
const compatible: string[] = []
|
|
101
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4) compatible.push(brandAt(offset))
|
|
102
|
+
// A HEIF-family file whose major brand is the generic `mif1`/`msf1`/`miaf` names its codec among
|
|
103
|
+
// the compatible brands: AVIF says `avif`/`avis`.
|
|
104
|
+
const avif = (name: string): boolean => name === "avif" || name === "avis"
|
|
105
|
+
const generic = brand === "mif1" || brand === "msf1" || brand === "miaf"
|
|
106
|
+
if (avif(brand) || (generic && compatible.some(avif)))
|
|
107
|
+
return { mime: "image/avif", ext: "avif" }
|
|
108
|
+
if (brand.startsWith("hei") || brand.startsWith("hev") || generic) {
|
|
109
|
+
return { mime: "image/heic", ext: "heic" }
|
|
110
|
+
}
|
|
111
|
+
// Audio brands share the ISO-BMFF container but are NOT video - labeling them `video/mp4` would
|
|
112
|
+
// both reject real audio under an `audio/*` allow-list and admit it under `video/*`. `M4A `/`M4B `
|
|
113
|
+
// are AAC audio / audiobooks.
|
|
114
|
+
if (brand === "M4A " || brand === "M4B ") return { mime: "audio/mp4", ext: "m4a" }
|
|
115
|
+
return { mime: "video/mp4", ext: "mp4" }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A/V + archives
|
|
119
|
+
if (at(0, 0x1a, 0x45, 0xdf, 0xa3)) return { mime: "video/webm", ext: "webm" } // Matroska/WebM
|
|
120
|
+
if (at(0, 0x4f, 0x67, 0x67, 0x53)) return { mime: "audio/ogg", ext: "ogg" }
|
|
121
|
+
if (at(0, 0x49, 0x44, 0x33)) return { mime: "audio/mpeg", ext: "mp3" } // ID3 tag
|
|
122
|
+
if (mpegAudioFrame(bytes)) return { mime: "audio/mpeg", ext: "mp3" } // an untagged MP3
|
|
123
|
+
if (at(0, 0x25, 0x50, 0x44, 0x46)) return { mime: "application/pdf", ext: "pdf" } // "%PDF"
|
|
124
|
+
if (at(0, 0x50, 0x4b, 0x03, 0x04) || at(0, 0x50, 0x4b, 0x05, 0x06)) {
|
|
125
|
+
return { mime: "application/zip", ext: "zip" } // also docx/xlsx/odt containers
|
|
126
|
+
}
|
|
127
|
+
if (at(0, 0x1f, 0x8b)) return { mime: "application/gzip", ext: "gz" }
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
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 = "5.
|
|
19
|
+
export const VERSION = "5.6.0" as const
|
|
20
20
|
|
|
21
21
|
export type Version = typeof VERSION
|
|
22
22
|
|
|
@@ -58,6 +58,7 @@ export {
|
|
|
58
58
|
markAuthenticatedRequest,
|
|
59
59
|
rejected,
|
|
60
60
|
} from "./server/auth.ts"
|
|
61
|
+
export { beforeHandler } from "./server/before-handler.ts"
|
|
61
62
|
export type {
|
|
62
63
|
Context,
|
|
63
64
|
Params,
|
|
@@ -11,8 +11,10 @@ import type { StandardSchemaV1 } from "../schema/standard.ts"
|
|
|
11
11
|
import { markAuthenticatedRequest } from "../server/auth.ts"
|
|
12
12
|
import { declaredHeaderNames } from "../server/headers.ts"
|
|
13
13
|
import { pathnameOf } from "../server/http.ts"
|
|
14
|
+
import type { Logger } from "../server/logger.ts"
|
|
14
15
|
import { queryObjectOf } from "../server/query.ts"
|
|
15
16
|
import type { Registry } from "../server/registry.ts"
|
|
17
|
+
import { toResponse } from "../server/respond.ts"
|
|
16
18
|
import {
|
|
17
19
|
CONTEXT_SEARCH,
|
|
18
20
|
CONTEXT_SET,
|
|
@@ -262,6 +264,77 @@ function authenticationInputOf(
|
|
|
262
264
|
})
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Run the `authenticate()` stages a mount was registered after, in order, against the request the
|
|
269
|
+
* mount would receive: `undefined` once each one authenticated, else the response that refuses it. A
|
|
270
|
+
* stage that throws or decides nothing valid answers 503, and its cause goes to `logger`, as a route
|
|
271
|
+
* error no hook answers does.
|
|
272
|
+
*/
|
|
273
|
+
export function authenticateMount(
|
|
274
|
+
stages: readonly RawAuthStage[],
|
|
275
|
+
request: Request,
|
|
276
|
+
platform: { readonly env?: unknown; readonly clientIp?: string | undefined } | undefined,
|
|
277
|
+
logger: Logger,
|
|
278
|
+
): MaybePromise<Response | undefined> {
|
|
279
|
+
let query: Readonly<Record<string, string | readonly string[]>> | undefined
|
|
280
|
+
const search = (() => {
|
|
281
|
+
const at = request.url.indexOf("?")
|
|
282
|
+
return at === -1 ? "" : request.url.slice(at)
|
|
283
|
+
})()
|
|
284
|
+
const input = Object.freeze({
|
|
285
|
+
method: request.method,
|
|
286
|
+
pathname: pathnameOf(request.url),
|
|
287
|
+
headers: authHeadersOf(request),
|
|
288
|
+
params: Object.freeze({}),
|
|
289
|
+
search,
|
|
290
|
+
get query() {
|
|
291
|
+
query ??= frozenQueryOf(search)
|
|
292
|
+
return query
|
|
293
|
+
},
|
|
294
|
+
request,
|
|
295
|
+
env: platform?.env,
|
|
296
|
+
clientIp: platform?.clientIp,
|
|
297
|
+
signal: request.signal,
|
|
298
|
+
})
|
|
299
|
+
const unavailable = (error: unknown): Response => {
|
|
300
|
+
logger.error("authenticate() stage failed before a mount", {
|
|
301
|
+
method: request.method,
|
|
302
|
+
path: input.pathname,
|
|
303
|
+
error: error instanceof Error ? error.message : String(error),
|
|
304
|
+
})
|
|
305
|
+
return toResponse(defaultAuthenticationResponse("unavailable"), EMPTY_RESPONSE_CONTROLS)
|
|
306
|
+
}
|
|
307
|
+
const decide = (
|
|
308
|
+
index: number,
|
|
309
|
+
result: import("../server/auth.ts").AuthenticationResult<unknown>,
|
|
310
|
+
): MaybePromise<Response | undefined> => {
|
|
311
|
+
if (!isAuthenticationDecision(result)) {
|
|
312
|
+
const id = stages[index]?.id
|
|
313
|
+
return unavailable(new TypeError(`authenticate() stage "${id}" returned an invalid decision`))
|
|
314
|
+
}
|
|
315
|
+
if (result.kind === "rejected") {
|
|
316
|
+
const refusal = result.response ?? defaultAuthenticationResponse(result.reason)
|
|
317
|
+
return refusal instanceof Response ? refusal : toResponse(refusal, EMPTY_RESPONSE_CONTROLS)
|
|
318
|
+
}
|
|
319
|
+
markAuthenticatedRequest(request)
|
|
320
|
+
return run(index + 1)
|
|
321
|
+
}
|
|
322
|
+
const run = (index: number): MaybePromise<Response | undefined> => {
|
|
323
|
+
const stage = stages[index]
|
|
324
|
+
if (stage === undefined) return undefined
|
|
325
|
+
let outcome: MaybePromise<import("../server/auth.ts").AuthenticationResult<unknown>>
|
|
326
|
+
try {
|
|
327
|
+
outcome = stage.run(input)
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return unavailable(error)
|
|
330
|
+
}
|
|
331
|
+
return outcome instanceof Promise
|
|
332
|
+
? outcome.then((settled) => decide(index, settled), unavailable)
|
|
333
|
+
: decide(index, outcome)
|
|
334
|
+
}
|
|
335
|
+
return run(0)
|
|
336
|
+
}
|
|
337
|
+
|
|
265
338
|
function defaultAuthenticationResponse(
|
|
266
339
|
reason: import("../server/auth.ts").AuthenticationFailureReason | undefined,
|
|
267
340
|
): ResponseResult {
|
|
@@ -270,6 +343,25 @@ function defaultAuthenticationResponse(
|
|
|
270
343
|
return status(401, { ok: false, error: "unauthorized" })
|
|
271
344
|
}
|
|
272
345
|
|
|
346
|
+
/** Whether a stage returned a decision it may: a rejection with a known reason and a response, if
|
|
347
|
+
* any, that answers; or a principal. */
|
|
348
|
+
function isAuthenticationDecision(
|
|
349
|
+
result: import("../server/auth.ts").AuthenticationResult<unknown>,
|
|
350
|
+
): boolean {
|
|
351
|
+
return (
|
|
352
|
+
result !== null &&
|
|
353
|
+
typeof result === "object" &&
|
|
354
|
+
(result.kind === "rejected"
|
|
355
|
+
? validAuthenticationFailureReason(result.reason) &&
|
|
356
|
+
(result.response === undefined ||
|
|
357
|
+
result.response instanceof Response ||
|
|
358
|
+
isResponseResult(result.response))
|
|
359
|
+
: result.kind === "authenticated" &&
|
|
360
|
+
result.principal !== null &&
|
|
361
|
+
result.principal !== undefined)
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
273
365
|
function validAuthenticationFailureReason(
|
|
274
366
|
value: unknown,
|
|
275
367
|
): value is import("../server/auth.ts").AuthenticationFailureReason | undefined {
|
|
@@ -410,18 +502,7 @@ function finishAuthentication<T>(
|
|
|
410
502
|
next: number,
|
|
411
503
|
validationComplete = false,
|
|
412
504
|
): ProgramResult<T> {
|
|
413
|
-
if (
|
|
414
|
-
result === null ||
|
|
415
|
-
typeof result !== "object" ||
|
|
416
|
-
(result.kind === "rejected"
|
|
417
|
-
? !validAuthenticationFailureReason(result.reason) ||
|
|
418
|
-
(result.response !== undefined &&
|
|
419
|
-
!(result.response instanceof Response) &&
|
|
420
|
-
!isResponseResult(result.response))
|
|
421
|
-
: result.kind !== "authenticated" ||
|
|
422
|
-
result.principal === null ||
|
|
423
|
-
result.principal === undefined)
|
|
424
|
-
) {
|
|
505
|
+
if (!isAuthenticationDecision(result)) {
|
|
425
506
|
const id = program.authStages[next - 1]!.id
|
|
426
507
|
const error = new TypeError(`authenticate() stage "${id}" returned an invalid decision`)
|
|
427
508
|
return failAuthentication(host, entry, ctx, finalize, wrapResponse, error)
|
|
@@ -499,7 +580,7 @@ async function continueFastAuthenticatedProgram<T>(
|
|
|
499
580
|
try {
|
|
500
581
|
const settled = await pending
|
|
501
582
|
if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
|
|
502
|
-
|
|
583
|
+
const outcome = runFastAuthenticatedProgram(
|
|
503
584
|
host,
|
|
504
585
|
entry,
|
|
505
586
|
program,
|
|
@@ -509,6 +590,7 @@ async function continueFastAuthenticatedProgram<T>(
|
|
|
509
590
|
wrapResponse,
|
|
510
591
|
validationIndex,
|
|
511
592
|
)
|
|
593
|
+
return outcome instanceof Promise ? await outcome : outcome
|
|
512
594
|
} catch (error) {
|
|
513
595
|
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
514
596
|
}
|
|
@@ -585,7 +667,7 @@ async function continueValidationBeforeAuthentication<T>(
|
|
|
585
667
|
try {
|
|
586
668
|
const settled = await pending
|
|
587
669
|
if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
|
|
588
|
-
|
|
670
|
+
const outcome = runValidationBeforeAuthentication(
|
|
589
671
|
host,
|
|
590
672
|
entry,
|
|
591
673
|
program,
|
|
@@ -595,6 +677,7 @@ async function continueValidationBeforeAuthentication<T>(
|
|
|
595
677
|
wrapResponse,
|
|
596
678
|
validationIndex,
|
|
597
679
|
)
|
|
680
|
+
return outcome instanceof Promise ? await outcome : outcome
|
|
598
681
|
} catch (error) {
|
|
599
682
|
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
600
683
|
}
|
|
@@ -616,7 +699,7 @@ function runValidatedProgram<T>(
|
|
|
616
699
|
type ProgramResult<T> = MaybePromise<T>
|
|
617
700
|
|
|
618
701
|
/** Opt-in sensitive-route lane: run authentication/policy hooks before parsing untrusted input. */
|
|
619
|
-
|
|
702
|
+
function runAuthBeforeValidation<T>(
|
|
620
703
|
host: RouteProgramRuntime,
|
|
621
704
|
entry: RouteEntry,
|
|
622
705
|
program: RouteProgram,
|
|
@@ -625,23 +708,53 @@ async function runAuthBeforeValidation<T>(
|
|
|
625
708
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
626
709
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
627
710
|
gated = false,
|
|
628
|
-
|
|
711
|
+
deriveStart = 0,
|
|
712
|
+
beforeStart = 0,
|
|
713
|
+
validationStart = 0,
|
|
714
|
+
): ProgramResult<T> {
|
|
629
715
|
try {
|
|
630
716
|
if (!gated) {
|
|
631
|
-
if (
|
|
717
|
+
if (deriveStart === 0 && beforeStart === 0 && program.hasDecorations)
|
|
718
|
+
Object.assign(ctx, program.decorations)
|
|
632
719
|
|
|
633
|
-
for (
|
|
634
|
-
const result =
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
720
|
+
for (let i = deriveStart; i < program.derives.length; i++) {
|
|
721
|
+
const result = program.derives[i]!(ctx)
|
|
722
|
+
if (result instanceof Promise) {
|
|
723
|
+
return continueProgram(
|
|
724
|
+
host,
|
|
725
|
+
entry,
|
|
726
|
+
program,
|
|
727
|
+
source,
|
|
728
|
+
ctx,
|
|
729
|
+
finalize,
|
|
730
|
+
wrapResponse,
|
|
731
|
+
{ kind: "derive", next: i + 1, policy: true },
|
|
732
|
+
result,
|
|
733
|
+
undefined,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
if (isEarly(result)) return finishEarly(host, entry, ctx, result, finalize, wrapResponse)
|
|
737
|
+
Object.assign(ctx, result)
|
|
638
738
|
}
|
|
639
739
|
|
|
640
|
-
for (
|
|
641
|
-
const result =
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
740
|
+
for (let i = beforeStart; i < program.beforeHandle.length; i++) {
|
|
741
|
+
const result = program.beforeHandle[i]!(ctx)
|
|
742
|
+
if (result instanceof Promise) {
|
|
743
|
+
return continueProgram(
|
|
744
|
+
host,
|
|
745
|
+
entry,
|
|
746
|
+
program,
|
|
747
|
+
source,
|
|
748
|
+
ctx,
|
|
749
|
+
finalize,
|
|
750
|
+
wrapResponse,
|
|
751
|
+
{ kind: "before", next: i + 1, policy: true },
|
|
752
|
+
result,
|
|
753
|
+
undefined,
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
if (result !== undefined)
|
|
757
|
+
return finishEarly(host, entry, ctx, result, finalize, wrapResponse)
|
|
645
758
|
}
|
|
646
759
|
|
|
647
760
|
// The policy hooks have passed, so the idempotency gate may buffer the body now. Not awaited:
|
|
@@ -653,30 +766,52 @@ async function runAuthBeforeValidation<T>(
|
|
|
653
766
|
}
|
|
654
767
|
}
|
|
655
768
|
|
|
656
|
-
for (
|
|
769
|
+
for (let i = validationStart; i < program.validationStages.length; i++) {
|
|
770
|
+
const stage = program.validationStages[i]!
|
|
657
771
|
const result =
|
|
658
772
|
stage.kind === "body"
|
|
659
|
-
?
|
|
773
|
+
? host.readProgramBody(entry, source, ctx)
|
|
660
774
|
: host.validateProgramStage(entry, stage, source, ctx)
|
|
661
|
-
|
|
662
|
-
|
|
775
|
+
if (result instanceof Promise) {
|
|
776
|
+
return continueProgram(
|
|
777
|
+
host,
|
|
778
|
+
entry,
|
|
779
|
+
program,
|
|
780
|
+
source,
|
|
781
|
+
ctx,
|
|
782
|
+
finalize,
|
|
783
|
+
wrapResponse,
|
|
784
|
+
{ kind: "validation", next: i + 1, policy: true },
|
|
785
|
+
result,
|
|
786
|
+
undefined,
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
if (result !== undefined) return wrapResponse(result)
|
|
663
790
|
}
|
|
664
791
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
792
|
+
// A pending outcome here already met the error boundary (continueProgram or the response
|
|
793
|
+
// contract), so wrapping it in another catch would only add a promise.
|
|
794
|
+
return runHooksSync(
|
|
795
|
+
host,
|
|
796
|
+
entry,
|
|
797
|
+
program,
|
|
798
|
+
source,
|
|
799
|
+
ctx,
|
|
800
|
+
finalize,
|
|
801
|
+
wrapResponse,
|
|
802
|
+
program.derives.length,
|
|
803
|
+
program.beforeHandle.length,
|
|
804
|
+
undefined,
|
|
805
|
+
)
|
|
671
806
|
} catch (error) {
|
|
672
|
-
return
|
|
807
|
+
return host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
673
808
|
}
|
|
674
809
|
}
|
|
675
810
|
|
|
676
811
|
type PendingPhase =
|
|
677
|
-
| { readonly kind: "validation"; readonly next: number }
|
|
678
|
-
| { readonly kind: "derive"; readonly next: number }
|
|
679
|
-
| { readonly kind: "before"; readonly next: number }
|
|
812
|
+
| { readonly kind: "validation"; readonly next: number; readonly policy?: true }
|
|
813
|
+
| { readonly kind: "derive"; readonly next: number; readonly policy?: true }
|
|
814
|
+
| { readonly kind: "before"; readonly next: number; readonly policy?: true }
|
|
680
815
|
| { readonly kind: "handler" }
|
|
681
816
|
| { readonly kind: "after"; readonly next: number }
|
|
682
817
|
|
|
@@ -757,7 +892,7 @@ function runSync<T>(
|
|
|
757
892
|
}
|
|
758
893
|
if (outcome !== undefined) return wrapResponse(outcome)
|
|
759
894
|
}
|
|
760
|
-
return runHooksSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, 0,
|
|
895
|
+
return runHooksSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, 0, initial)
|
|
761
896
|
}
|
|
762
897
|
|
|
763
898
|
function runHooksSync<T>(
|
|
@@ -770,7 +905,6 @@ function runHooksSync<T>(
|
|
|
770
905
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
771
906
|
deriveIndex: number,
|
|
772
907
|
beforeIndex: number,
|
|
773
|
-
afterIndex: number,
|
|
774
908
|
initial: unknown,
|
|
775
909
|
): ProgramResult<T> {
|
|
776
910
|
for (let i = deriveIndex; i < program.derives.length; i++) {
|
|
@@ -826,7 +960,20 @@ function runHooksSync<T>(
|
|
|
826
960
|
initial,
|
|
827
961
|
)
|
|
828
962
|
}
|
|
829
|
-
|
|
963
|
+
return runAfterSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, output)
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function runAfterSync<T>(
|
|
967
|
+
host: RouteProgramRuntime,
|
|
968
|
+
entry: RouteEntry,
|
|
969
|
+
program: RouteProgram,
|
|
970
|
+
source: RequestSource,
|
|
971
|
+
ctx: RawContext,
|
|
972
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
973
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
974
|
+
afterIndex: number,
|
|
975
|
+
result: unknown,
|
|
976
|
+
): ProgramResult<T> {
|
|
830
977
|
for (let i = afterIndex; i < program.afterHandle.length; i++) {
|
|
831
978
|
const outcome = program.afterHandle[i]!(result, ctx)
|
|
832
979
|
if (outcome instanceof Promise) {
|
|
@@ -876,7 +1023,7 @@ async function continueProgram<T>(
|
|
|
876
1023
|
} else {
|
|
877
1024
|
result = settled
|
|
878
1025
|
}
|
|
879
|
-
|
|
1026
|
+
const outcome = runAsyncStages(
|
|
880
1027
|
host,
|
|
881
1028
|
entry,
|
|
882
1029
|
program,
|
|
@@ -887,12 +1034,13 @@ async function continueProgram<T>(
|
|
|
887
1034
|
pendingPhase,
|
|
888
1035
|
result,
|
|
889
1036
|
)
|
|
1037
|
+
return outcome instanceof Promise ? await outcome : outcome
|
|
890
1038
|
} catch (error) {
|
|
891
1039
|
return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
|
|
892
1040
|
}
|
|
893
1041
|
}
|
|
894
1042
|
|
|
895
|
-
|
|
1043
|
+
function runAsyncStages<T>(
|
|
896
1044
|
host: RouteProgramRuntime,
|
|
897
1045
|
entry: RouteEntry,
|
|
898
1046
|
program: RouteProgram,
|
|
@@ -902,54 +1050,74 @@ async function runAsyncStages<T>(
|
|
|
902
1050
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
903
1051
|
pendingPhase: PendingPhase,
|
|
904
1052
|
result: unknown,
|
|
905
|
-
):
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const outcome = program.derives[i]!(ctx)
|
|
924
|
-
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
925
|
-
if (isEarly(settled)) return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
|
|
926
|
-
Object.assign(ctx, settled)
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const beforeStart = pendingPhase.kind === "before" ? pendingPhase.next : 0
|
|
931
|
-
if (
|
|
932
|
-
pendingPhase.kind === "validation" ||
|
|
933
|
-
pendingPhase.kind === "derive" ||
|
|
934
|
-
pendingPhase.kind === "before"
|
|
935
|
-
) {
|
|
936
|
-
for (let i = beforeStart; i < program.beforeHandle.length; i++) {
|
|
937
|
-
const outcome = program.beforeHandle[i]!(ctx)
|
|
938
|
-
const settled = outcome instanceof Promise ? await outcome : outcome
|
|
939
|
-
if (settled !== undefined)
|
|
940
|
-
return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
|
|
941
|
-
}
|
|
942
|
-
const output = program.handler(ctx)
|
|
943
|
-
result = output instanceof Promise ? await output : output
|
|
1053
|
+
): ProgramResult<T> {
|
|
1054
|
+
// After an actual async boundary, resume the same sync runner until the next pending stage.
|
|
1055
|
+
// Sync hooks and finalization need no async function wrapper or additional microtask turn.
|
|
1056
|
+
if (pendingPhase.kind === "validation") {
|
|
1057
|
+
if (pendingPhase.policy) {
|
|
1058
|
+
return runAuthBeforeValidation(
|
|
1059
|
+
host,
|
|
1060
|
+
entry,
|
|
1061
|
+
program,
|
|
1062
|
+
source,
|
|
1063
|
+
ctx,
|
|
1064
|
+
finalize,
|
|
1065
|
+
wrapResponse,
|
|
1066
|
+
true,
|
|
1067
|
+
0,
|
|
1068
|
+
0,
|
|
1069
|
+
pendingPhase.next,
|
|
1070
|
+
)
|
|
944
1071
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1072
|
+
return runSync(
|
|
1073
|
+
host,
|
|
1074
|
+
entry,
|
|
1075
|
+
program,
|
|
1076
|
+
source,
|
|
1077
|
+
ctx,
|
|
1078
|
+
finalize,
|
|
1079
|
+
wrapResponse,
|
|
1080
|
+
pendingPhase.next,
|
|
1081
|
+
result,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
if (pendingPhase.kind === "derive" || pendingPhase.kind === "before") {
|
|
1085
|
+
if (pendingPhase.policy) {
|
|
1086
|
+
return runAuthBeforeValidation(
|
|
1087
|
+
host,
|
|
1088
|
+
entry,
|
|
1089
|
+
program,
|
|
1090
|
+
source,
|
|
1091
|
+
ctx,
|
|
1092
|
+
finalize,
|
|
1093
|
+
wrapResponse,
|
|
1094
|
+
false,
|
|
1095
|
+
pendingPhase.kind === "derive" ? pendingPhase.next : program.derives.length,
|
|
1096
|
+
pendingPhase.kind === "before" ? pendingPhase.next : 0,
|
|
1097
|
+
)
|
|
950
1098
|
}
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
|
|
1099
|
+
return runHooksSync(
|
|
1100
|
+
host,
|
|
1101
|
+
entry,
|
|
1102
|
+
program,
|
|
1103
|
+
source,
|
|
1104
|
+
ctx,
|
|
1105
|
+
finalize,
|
|
1106
|
+
wrapResponse,
|
|
1107
|
+
pendingPhase.kind === "derive" ? pendingPhase.next : program.derives.length,
|
|
1108
|
+
pendingPhase.kind === "before" ? pendingPhase.next : 0,
|
|
1109
|
+
result,
|
|
1110
|
+
)
|
|
954
1111
|
}
|
|
1112
|
+
return runAfterSync(
|
|
1113
|
+
host,
|
|
1114
|
+
entry,
|
|
1115
|
+
program,
|
|
1116
|
+
source,
|
|
1117
|
+
ctx,
|
|
1118
|
+
finalize,
|
|
1119
|
+
wrapResponse,
|
|
1120
|
+
pendingPhase.kind === "after" ? pendingPhase.next : 0,
|
|
1121
|
+
result,
|
|
1122
|
+
)
|
|
955
1123
|
}
|