@nifrajs/core 5.7.0 → 5.9.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 +105 -0
- package/README.md +9 -4
- package/dist/assurance.d.ts +26 -1
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +43 -11
- package/dist/assurance.js.map +1 -1
- package/dist/canonical-json.d.ts +11 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +46 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +1 -8
- package/dist/capabilities.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +1 -8
- package/dist/data.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -12
- package/dist/diff.js.map +1 -1
- package/dist/durable-adapters.d.ts +2 -0
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +41 -49
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +14 -20
- package/dist/durable-saga.js.map +1 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +14 -68
- package/dist/evidence.js.map +1 -1
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +60 -23
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/control-character.d.ts +3 -0
- package/dist/internal/control-character.d.ts.map +1 -0
- package/dist/internal/control-character.js +10 -0
- package/dist/internal/control-character.js.map +1 -0
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +4 -1
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/hex.d.ts +5 -0
- package/dist/internal/hex.d.ts.map +1 -0
- package/dist/internal/hex.js +14 -0
- package/dist/internal/hex.js.map +1 -0
- package/dist/internal/record.d.ts +3 -0
- package/dist/internal/record.d.ts.map +1 -0
- package/dist/internal/record.js +5 -0
- package/dist/internal/record.js.map +1 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +2 -2
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-execution.d.ts +4 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -0
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +4 -3
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +16 -33
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/schema-parts.d.ts +13 -0
- package/dist/internal/schema-parts.d.ts.map +1 -0
- package/dist/internal/schema-parts.js +15 -0
- package/dist/internal/schema-parts.js.map +1 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +2 -14
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -37
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/range.js +1 -1
- package/dist/range.js.map +1 -1
- package/dist/serve-static.d.ts +22 -0
- package/dist/serve-static.d.ts.map +1 -0
- package/dist/serve-static.js +187 -0
- package/dist/serve-static.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +2 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +12 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/base64-answers.d.ts.map +1 -1
- package/dist/server/base64-answers.js +1 -4
- package/dist/server/base64-answers.js.map +1 -1
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +43 -38
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +4 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +1 -1
- package/dist/server/bridge-markers.js +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +4 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/http.d.ts +5 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +13 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +2 -0
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/idle-timeout.d.ts +39 -0
- package/dist/server/idle-timeout.d.ts.map +1 -0
- package/dist/server/idle-timeout.js +95 -0
- package/dist/server/idle-timeout.js.map +1 -0
- package/dist/server/json-shared.d.ts +23 -0
- package/dist/server/json-shared.d.ts.map +1 -0
- package/dist/server/json-shared.js +33 -0
- package/dist/server/json-shared.js.map +1 -0
- package/dist/server/json-text.d.ts +1 -1
- package/dist/server/json-text.js +1 -1
- package/dist/server/node-outcome.js +1 -1
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/request-context.d.ts +22 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +44 -4
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/request-local.d.ts +44 -0
- package/dist/server/request-local.d.ts.map +1 -0
- package/dist/server/request-local.js +91 -0
- package/dist/server/request-local.js.map +1 -0
- package/dist/server/respond.js +1 -1
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +10 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +93 -64
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/runtime-core.d.ts +7 -4
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +9 -7
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +27 -0
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +59 -59
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +869 -512
- package/dist/server/server.js.map +1 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +2 -0
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.js +1 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +3 -1
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +14 -1
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-adapter-view.d.ts +54 -0
- package/dist/server/ws-adapter-view.d.ts.map +1 -0
- package/dist/server/ws-adapter-view.js +7 -0
- package/dist/server/ws-adapter-view.js.map +1 -0
- package/dist/server.d.ts +2 -1
- 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.map +1 -1
- package/dist/single-copy.js +1 -1
- package/dist/single-copy.js.map +1 -1
- package/dist/static-file.d.ts +156 -4
- package/dist/static-file.d.ts.map +1 -1
- package/dist/static-file.js +198 -4
- package/dist/static-file.js.map +1 -1
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +1 -3
- package/dist/wire.js.map +1 -1
- package/package.json +16 -1
- package/src/assurance.ts +66 -16
- package/src/canonical-json.ts +43 -0
- package/src/capabilities.ts +1 -8
- package/src/data.ts +2 -8
- package/src/diff.ts +2 -12
- package/src/durable-adapters.ts +53 -50
- package/src/durable-saga.ts +30 -23
- package/src/evidence.ts +25 -90
- package/src/idempotency.ts +62 -20
- package/src/index.ts +13 -2
- package/src/internal/control-character.ts +8 -0
- package/src/internal/durable-shared.ts +4 -1
- package/src/internal/hex.ts +14 -0
- package/src/internal/record.ts +4 -0
- package/src/internal/route-compiler.ts +2 -2
- package/src/internal/route-execution.ts +7 -20
- package/src/internal/route-lanes.ts +20 -44
- package/src/internal/schema-parts.ts +19 -0
- package/src/ledger.ts +2 -16
- package/src/manifest.ts +10 -38
- package/src/open-resolved.ts +64 -0
- package/src/range.ts +1 -1
- package/src/serve-static.ts +227 -0
- package/src/server/bare-error-lane.ts +11 -2
- package/src/server/base64-answers.ts +2 -6
- package/src/server/body-lane.ts +58 -51
- package/src/server/body.ts +4 -1
- package/src/server/bridge-markers.ts +1 -1
- package/src/server/cookies.ts +4 -1
- package/src/server/core-copies.ts +1 -1
- package/src/server/http.ts +12 -0
- package/src/server/idempotency-lane.ts +2 -0
- package/src/server/idle-timeout.ts +107 -0
- package/src/server/json-shared.ts +38 -0
- package/src/server/json-text.ts +1 -1
- package/src/server/node-outcome.ts +1 -1
- package/src/server/request-context.ts +62 -3
- package/src/server/request-local.ts +109 -0
- package/src/server/respond.ts +1 -1
- package/src/server/response-contract-lane.ts +112 -65
- package/src/server/runtime-core.ts +10 -8
- package/src/server/server-types.ts +27 -0
- package/src/server/server.ts +1230 -800
- package/src/server/transport-lane.ts +2 -0
- package/src/server/webhook.ts +1 -1
- package/src/server/websocket.ts +18 -1
- package/src/server/ws-adapter-view.ts +63 -0
- package/src/server.ts +12 -1
- package/src/single-copy.ts +1 -3
- package/src/static-file.ts +350 -4
- package/src/wire.ts +2 -4
package/src/manifest.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Deterministic, signed route manifest: contract × assurance × capabilities × classification. */
|
|
2
2
|
|
|
3
3
|
import type { AssuranceReport } from "./assurance.ts"
|
|
4
|
+
import { canonicalJson } from "./canonical-json.ts"
|
|
4
5
|
import type { CapabilityAssuranceReport } from "./capabilities.ts"
|
|
5
6
|
import {
|
|
6
7
|
type DataClassification,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
} from "./diff.ts"
|
|
16
17
|
import { type ProjectEvidenceSnapshot, snapshotProjectEvidence } from "./evidence.ts"
|
|
17
18
|
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
19
|
+
import { hasControlCharacter } from "./internal/control-character.ts"
|
|
20
|
+
import { sha256Hex } from "./internal/hex.ts"
|
|
18
21
|
|
|
19
22
|
export interface NifraManifestAssurance {
|
|
20
23
|
readonly rule?: string
|
|
@@ -70,35 +73,6 @@ export interface BuildNifraManifestInput {
|
|
|
70
73
|
|
|
71
74
|
const encoder = new TextEncoder()
|
|
72
75
|
|
|
73
|
-
function hasControlCharacter(value: string): boolean {
|
|
74
|
-
for (let index = 0; index < value.length; index++) {
|
|
75
|
-
const code = value.charCodeAt(index)
|
|
76
|
-
if (code <= 31 || code === 127) return true
|
|
77
|
-
}
|
|
78
|
-
return false
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function canonicalValue(value: unknown): string {
|
|
82
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
83
|
-
return JSON.stringify(value)
|
|
84
|
-
}
|
|
85
|
-
if (typeof value === "number") {
|
|
86
|
-
if (!Number.isFinite(value))
|
|
87
|
-
throw new TypeError("nifra manifest cannot encode non-finite numbers")
|
|
88
|
-
return JSON.stringify(value)
|
|
89
|
-
}
|
|
90
|
-
if (Array.isArray(value)) return `[${value.map((item) => canonicalValue(item)).join(",")}]`
|
|
91
|
-
if (typeof value === "object") {
|
|
92
|
-
const record = value as Readonly<Record<string, unknown>>
|
|
93
|
-
return `{${Object.keys(record)
|
|
94
|
-
.filter((key) => record[key] !== undefined)
|
|
95
|
-
.sort()
|
|
96
|
-
.map((key) => `${JSON.stringify(key)}:${canonicalValue(record[key])}`)
|
|
97
|
-
.join(",")}}`
|
|
98
|
-
}
|
|
99
|
-
throw new TypeError(`nifra manifest cannot encode ${typeof value}`)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
76
|
function manifestBody(manifest: Pick<NifraManifest, "manifestVersion" | "routes">): {
|
|
103
77
|
readonly manifestVersion: 1
|
|
104
78
|
readonly routes: readonly NifraManifestRoute[]
|
|
@@ -110,17 +84,17 @@ function manifestBody(manifest: Pick<NifraManifest, "manifestVersion" | "routes"
|
|
|
110
84
|
export function canonicalManifest(
|
|
111
85
|
manifest: Pick<NifraManifest, "manifestVersion" | "routes">,
|
|
112
86
|
): string {
|
|
113
|
-
return
|
|
87
|
+
return canonicalJson(manifestBody(manifest))
|
|
114
88
|
}
|
|
115
89
|
|
|
116
90
|
/** Byte-stable artifact serialization (including `contentHash`). */
|
|
117
91
|
export function serializeNifraManifest(manifest: NifraManifest): string {
|
|
118
|
-
return
|
|
92
|
+
return canonicalJson(manifest)
|
|
119
93
|
}
|
|
120
94
|
|
|
121
95
|
/** Byte-stable serialization for the detached signature sidecar. */
|
|
122
96
|
export function serializeNifraManifestSignature(signature: NifraManifestSignature): string {
|
|
123
|
-
return
|
|
97
|
+
return canonicalJson(signature)
|
|
124
98
|
}
|
|
125
99
|
|
|
126
100
|
/** Parse the detached sidecar before selecting its operator-controlled public key. */
|
|
@@ -163,11 +137,6 @@ export function parseNifraManifestSignature(
|
|
|
163
137
|
return Object.freeze(candidate as NifraManifestSignature)
|
|
164
138
|
}
|
|
165
139
|
|
|
166
|
-
async function sha256Hex(value: string): Promise<string> {
|
|
167
|
-
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)))
|
|
168
|
-
return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
169
|
-
}
|
|
170
|
-
|
|
171
140
|
const routeKey = (method: string, path: string): string => `${method.toUpperCase()}\n${path}`
|
|
172
141
|
const sortedStrings = (values: Iterable<string>): readonly string[] =>
|
|
173
142
|
Object.freeze([...new Set(values)].sort())
|
|
@@ -255,7 +224,10 @@ export async function buildNifraManifest(input: BuildNifraManifestInput): Promis
|
|
|
255
224
|
function base64Url(bytes: Uint8Array): string {
|
|
256
225
|
let binary = ""
|
|
257
226
|
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
258
|
-
return btoa(binary)
|
|
227
|
+
return btoa(binary)
|
|
228
|
+
.replaceAll("+", "-")
|
|
229
|
+
.replaceAll("/", "_")
|
|
230
|
+
.replace(/={1,2}$/, "")
|
|
259
231
|
}
|
|
260
232
|
|
|
261
233
|
function fromBase64Url(value: string): Uint8Array<ArrayBuffer> {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { constants as FS } from "node:fs"
|
|
2
|
+
import { type FileHandle, open, readlink, realpath, stat } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
// Darwin's open(2) flag refusing a symlink in ANY component; `O_NOFOLLOW` covers only the last one.
|
|
5
|
+
const O_NOFOLLOW_ANY = 0x20000000
|
|
6
|
+
// Deno's node:fs rejects flags it does not know with EINVAL; the first refusal turns the flag off.
|
|
7
|
+
let noFollowAny: boolean | undefined
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open a name `realpath` already resolved, refusing it when any component became a symlink since.
|
|
11
|
+
* A local writer inside the served tree can swap an intermediate directory for a link after the
|
|
12
|
+
* containment check; `O_NOFOLLOW` alone would follow it. Darwin refuses such a path in the kernel;
|
|
13
|
+
* Linux reads back the name the descriptor holds. Elsewhere, Windows included, no file API can open
|
|
14
|
+
* without following a middle link or name an open descriptor's path, so the check in
|
|
15
|
+
* {@link opensAt} is a platform limit: it narrows the race to repeated swaps but cannot close it.
|
|
16
|
+
*/
|
|
17
|
+
export async function openResolved(realFile: string): Promise<FileHandle> {
|
|
18
|
+
// Read per open, not at load: this module can sit in a client bundle graph, where `process` is
|
|
19
|
+
// undefined.
|
|
20
|
+
if (noFollowAny ?? process.platform === "darwin") {
|
|
21
|
+
try {
|
|
22
|
+
return await open(realFile, FS.O_RDONLY | O_NOFOLLOW_ANY)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!(error instanceof Error && "code" in error && error.code === "EINVAL")) throw error
|
|
25
|
+
noFollowAny = false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const handle = await open(realFile, FS.O_RDONLY | FS.O_NOFOLLOW)
|
|
29
|
+
try {
|
|
30
|
+
if (await opensAt(handle, realFile)) return handle
|
|
31
|
+
} catch {}
|
|
32
|
+
await handle.close()
|
|
33
|
+
throw Object.assign(new Error(`${realFile} moved while it was opened`), { code: "ELOOP" })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether `handle` is the file at `realFile` with no symlink in its path. */
|
|
37
|
+
export async function opensAt(
|
|
38
|
+
handle: Pick<FileHandle, "fd" | "stat">,
|
|
39
|
+
realFile: string,
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
return (await readlink(`/proc/self/fd/${handle.fd}`)) === realFile
|
|
43
|
+
} catch {
|
|
44
|
+
return sameFileAt(handle, realFile)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* {@link opensAt} where no descriptor's path can be read. Exported for its test, which runs it on
|
|
50
|
+
* every host.
|
|
51
|
+
*/
|
|
52
|
+
export async function sameFileAt(
|
|
53
|
+
handle: Pick<FileHandle, "stat">,
|
|
54
|
+
realFile: string,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
// One step after another, so passing takes a swap back and a swap again between steps: the name
|
|
57
|
+
// resolves to itself, then to the opened file, then to itself again. Identities are compared as
|
|
58
|
+
// bigints because a Windows file index can exceed 2^53.
|
|
59
|
+
if ((await realpath(realFile)) !== realFile) return false
|
|
60
|
+
const opened = await handle.stat({ bigint: true })
|
|
61
|
+
const named = await stat(realFile, { bigint: true })
|
|
62
|
+
if (opened.dev !== named.dev || opened.ino !== named.ino) return false
|
|
63
|
+
return (await realpath(realFile)) === realFile
|
|
64
|
+
}
|
package/src/range.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function parseByteRange(value: string | null, size: number): ByteRangeRes
|
|
|
104
104
|
if (value.length > MAX_RANGE_HEADER_LENGTH) return { kind: "none" }
|
|
105
105
|
const single = parseSingleRange(value, size)
|
|
106
106
|
if (single !== undefined) return single
|
|
107
|
-
const match = /^bytes\s*=\s*(
|
|
107
|
+
const match = /^bytes\s*=\s*(\S.*)$/i.exec(value)
|
|
108
108
|
if (match === null) return { kind: "none" }
|
|
109
109
|
const source = match[1]!
|
|
110
110
|
let separators = 0
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `serveStatic()` for Bun's `listen({ static })`: files on disk under URL prefixes, with the same
|
|
3
|
+
* mounts `@nifrajs/node` and `@nifrajs/deno` take. Which request a mount takes and how it answers is
|
|
4
|
+
* `./static-file.ts`; this module only reads the file, through node:fs, which Bun runs natively.
|
|
5
|
+
*
|
|
6
|
+
* A subpath rather than part of `listen()`, so an app that serves no files bundles no file reader.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto"
|
|
9
|
+
import { type FileHandle, realpath } from "node:fs/promises"
|
|
10
|
+
import { isAbsolute, relative, resolve, sep } from "node:path"
|
|
11
|
+
import { Readable } from "node:stream"
|
|
12
|
+
import { fileURLToPath } from "node:url"
|
|
13
|
+
import { openResolved } from "./open-resolved.ts"
|
|
14
|
+
import { parseByteRange } from "./range.ts"
|
|
15
|
+
import { pathnameOf } from "./server/http.ts"
|
|
16
|
+
import {
|
|
17
|
+
CONTENT_SNAPSHOT_MAX,
|
|
18
|
+
contentTags,
|
|
19
|
+
isHiddenPath,
|
|
20
|
+
matchStatic,
|
|
21
|
+
planStatic,
|
|
22
|
+
type ServeStaticOptions,
|
|
23
|
+
type StaticMount,
|
|
24
|
+
staticMountsOf,
|
|
25
|
+
} from "./static-file.ts"
|
|
26
|
+
|
|
27
|
+
export type { HashedFiles, ServeStaticOptions } from "./static-file.ts"
|
|
28
|
+
export { IMMUTABLE, ONE_DAY } from "./static-file.ts"
|
|
29
|
+
|
|
30
|
+
// Bun.serve drops the declared length of any stream body and sends it chunked, and a Bun.file body
|
|
31
|
+
// would need this descriptor open for a send nothing reports the end of. So on Bun a body up to 1 MiB
|
|
32
|
+
// is read whole: a slow client then holds ~1 MB, against ~0.6 MB through Bun's stream sink.
|
|
33
|
+
const BUFFERED_BODY_MAX = typeof Bun === "undefined" ? CONTENT_SNAPSHOT_MAX : 1024 * 1024
|
|
34
|
+
|
|
35
|
+
// One per process: the tag of a file served from any mount is remembered by its real path.
|
|
36
|
+
const TAGS = contentTags(createHash)
|
|
37
|
+
|
|
38
|
+
function rootOf(dir: string | URL): string {
|
|
39
|
+
return resolve(typeof dir === "string" ? dir : fileURLToPath(dir))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The host of an absolute URL, as `new URL(url).host` would give it, without parsing the rest. */
|
|
43
|
+
function hostOf(url: string): string {
|
|
44
|
+
const start = url.indexOf("//") + 2
|
|
45
|
+
let end = start
|
|
46
|
+
while (end < url.length) {
|
|
47
|
+
const code = url.charCodeAt(end)
|
|
48
|
+
if (code === 47 /* '/' */ || code === 63 /* '?' */ || code === 35 /* '#' */) break
|
|
49
|
+
end++
|
|
50
|
+
}
|
|
51
|
+
return url.slice(start, end)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function refusalOf(status: 400 | 403 | 404): Response {
|
|
55
|
+
return new Response(status === 400 ? "Bad Request" : status === 403 ? "Forbidden" : "Not Found", {
|
|
56
|
+
status,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `length` bytes of `handle` from `start`; fewer when the file shrank since its stat. */
|
|
61
|
+
async function readPinned(
|
|
62
|
+
handle: FileHandle,
|
|
63
|
+
start: number,
|
|
64
|
+
length: number,
|
|
65
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
66
|
+
const bytes = new Uint8Array(length)
|
|
67
|
+
let offset = 0
|
|
68
|
+
while (offset < length) {
|
|
69
|
+
const { bytesRead } = await handle.read(bytes, offset, length - offset, start + offset)
|
|
70
|
+
if (bytesRead === 0) break
|
|
71
|
+
offset += bytesRead
|
|
72
|
+
}
|
|
73
|
+
return offset === length ? bytes : bytes.subarray(0, offset)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface BunMount extends StaticMount {
|
|
77
|
+
/** The directory's real path, resolved on first use and again after a miss, so one made later serves. */
|
|
78
|
+
realRoot: Promise<string | undefined> | undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The answer for `rel` under `mount`, or `undefined` when the mount falls through on a file it cannot
|
|
83
|
+
* serve.
|
|
84
|
+
*/
|
|
85
|
+
async function readStatic(
|
|
86
|
+
mount: BunMount,
|
|
87
|
+
rel: string,
|
|
88
|
+
immutable: boolean,
|
|
89
|
+
request: Request,
|
|
90
|
+
): Promise<Response | undefined> {
|
|
91
|
+
const missing = (status: 403 | 404 = 404): Response | undefined =>
|
|
92
|
+
mount.fallthrough ? undefined : refusalOf(status)
|
|
93
|
+
const file = resolve(mount.root, rel)
|
|
94
|
+
// A drive or root in the decoded name (`C:/x` on Windows) leaves the directory without a `..`.
|
|
95
|
+
const contained = relative(mount.root, file)
|
|
96
|
+
if (contained.startsWith("..") || isAbsolute(contained)) return missing(403)
|
|
97
|
+
// Containment is checked on the RESOLVED name and that name is what opens, so a link swapped inside
|
|
98
|
+
// the tree after the check cannot redirect the read; openResolved refuses a name that gained a link.
|
|
99
|
+
mount.realRoot ??= realpath(mount.root).catch(() => undefined)
|
|
100
|
+
const [realRoot, realFile] = await Promise.all([
|
|
101
|
+
mount.realRoot,
|
|
102
|
+
realpath(file).catch(() => undefined),
|
|
103
|
+
])
|
|
104
|
+
if (realRoot === undefined) mount.realRoot = undefined
|
|
105
|
+
if (
|
|
106
|
+
realRoot === undefined ||
|
|
107
|
+
realFile === undefined ||
|
|
108
|
+
(realFile !== realRoot && !realFile.startsWith(realRoot + sep))
|
|
109
|
+
) {
|
|
110
|
+
return missing()
|
|
111
|
+
}
|
|
112
|
+
// A visible name linking to a dotfile inside the directory answers as a missing file does.
|
|
113
|
+
if (
|
|
114
|
+
mount.denyDotfiles &&
|
|
115
|
+
realFile !== realRoot &&
|
|
116
|
+
isHiddenPath(realFile.slice(realRoot.length + 1))
|
|
117
|
+
) {
|
|
118
|
+
return missing()
|
|
119
|
+
}
|
|
120
|
+
let handle: FileHandle | undefined
|
|
121
|
+
try {
|
|
122
|
+
try {
|
|
123
|
+
handle = await openResolved(realFile)
|
|
124
|
+
} catch {
|
|
125
|
+
return missing()
|
|
126
|
+
}
|
|
127
|
+
const opened = handle
|
|
128
|
+
const stat = await opened.stat()
|
|
129
|
+
if (!stat.isFile()) return missing()
|
|
130
|
+
const method = request.method === "HEAD" ? "HEAD" : "GET"
|
|
131
|
+
let whole: Uint8Array<ArrayBuffer> | undefined
|
|
132
|
+
const plan = await planStatic(
|
|
133
|
+
mount,
|
|
134
|
+
immutable,
|
|
135
|
+
{ method, header: (name) => request.headers.get(name) },
|
|
136
|
+
{
|
|
137
|
+
path: realFile,
|
|
138
|
+
version: stat,
|
|
139
|
+
readWhole: async () => {
|
|
140
|
+
whole = await readPinned(opened, 0, stat.size)
|
|
141
|
+
return whole
|
|
142
|
+
},
|
|
143
|
+
restat: () => opened.stat(),
|
|
144
|
+
tags: TAGS,
|
|
145
|
+
parseRange: parseByteRange,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
if (!plan.body) return new Response(null, { status: plan.status, headers: plan.headers })
|
|
149
|
+
const { start, length, headers } = plan
|
|
150
|
+
if (length <= BUFFERED_BODY_MAX) {
|
|
151
|
+
const body =
|
|
152
|
+
whole === undefined
|
|
153
|
+
? await readPinned(opened, start, length)
|
|
154
|
+
: whole.subarray(start, start + length)
|
|
155
|
+
headers["content-length"] = String(body.byteLength)
|
|
156
|
+
return new Response(body, { status: plan.status, headers })
|
|
157
|
+
}
|
|
158
|
+
const stream = opened.createReadStream({ start, end: start + length - 1 })
|
|
159
|
+
// createReadStream owns closing the descriptor after EOF or abort.
|
|
160
|
+
handle = undefined
|
|
161
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: node:stream/web's ReadableStream is the global one on Bun; only the declarations differ.
|
|
162
|
+
const body = Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>
|
|
163
|
+
return new Response(body, { status: plan.status, headers })
|
|
164
|
+
} catch {
|
|
165
|
+
// Unreadable under the prefix: a 404 (not an SSR 404 page), unless the mount falls through.
|
|
166
|
+
return missing()
|
|
167
|
+
} finally {
|
|
168
|
+
try {
|
|
169
|
+
await handle?.close()
|
|
170
|
+
} catch {
|
|
171
|
+
// A close that fails changes nothing about the answer already chosen.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function serveFrom(
|
|
177
|
+
mounts: readonly BunMount[],
|
|
178
|
+
from: number,
|
|
179
|
+
request: Request,
|
|
180
|
+
path: string,
|
|
181
|
+
host: () => string,
|
|
182
|
+
): Promise<Response | undefined> | undefined {
|
|
183
|
+
for (let i = from; i < mounts.length; i++) {
|
|
184
|
+
const mount = mounts[i]
|
|
185
|
+
if (mount === undefined) break
|
|
186
|
+
const matched = matchStatic(mount, path, host)
|
|
187
|
+
if (matched === "pass") continue
|
|
188
|
+
// matchStatic passes what a falling-through mount refuses, so a status here is always answered.
|
|
189
|
+
if ("status" in matched) return Promise.resolve(refusalOf(matched.status))
|
|
190
|
+
return readStatic(mount, matched.rel, matched.immutable, request).then(
|
|
191
|
+
(response) => response ?? serveFrom(mounts, i + 1, request, path, host),
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
return undefined
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The `static` handler for files on disk, for Bun's `listen()`: each mount serves a directory under a
|
|
199
|
+
* URL prefix, and the first mount whose prefix (and `files`, when set) takes a GET or HEAD path
|
|
200
|
+
* answers it. A request no mount takes returns `undefined` synchronously and reaches the app. The
|
|
201
|
+
* same mounts work with `@nifrajs/node`'s and `@nifrajs/deno`'s `serveStatic`.
|
|
202
|
+
*
|
|
203
|
+
* Files up to 256 KiB carry a strong `ETag` from the exact bytes served. Larger files stream with no
|
|
204
|
+
* tag and a weak date, so an `If-Range` gets the whole file rather than a range spliced from two
|
|
205
|
+
* versions.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* import { serveStatic } from "@nifrajs/core/serve-static"
|
|
210
|
+
*
|
|
211
|
+
* app.listen(3000, { static: serveStatic({ dir: new URL("./assets/", import.meta.url) }) })
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function serveStatic(
|
|
215
|
+
mounts: ServeStaticOptions | readonly ServeStaticOptions[],
|
|
216
|
+
): (request: Request) => Promise<Response | undefined> | undefined {
|
|
217
|
+
const states: readonly BunMount[] = staticMountsOf(mounts, rootOf).map((mount) => ({
|
|
218
|
+
...mount,
|
|
219
|
+
realRoot: undefined,
|
|
220
|
+
}))
|
|
221
|
+
return (request) => {
|
|
222
|
+
const method = request.method
|
|
223
|
+
if (method !== "GET" && method !== "HEAD") return undefined
|
|
224
|
+
const url = request.url
|
|
225
|
+
return serveFrom(states, 0, request, pathnameOf(url), () => hostOf(url))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -31,7 +31,8 @@ export type ErrorLogDetail = "full" | "message" | "none"
|
|
|
31
31
|
* - a `Response` or a `status(...)` is control flow - the app answering directly, or plain data.
|
|
32
32
|
* Either renders through the same `finalize` a RETURNED one takes, with the request's `c.set`,
|
|
33
33
|
* so a thrown one means what the return means: a `Response` ships as built, plus the cookies the
|
|
34
|
-
* request queued (the set-session-then-`throw redirect()` pattern);
|
|
34
|
+
* request queued (the set-session-then-`throw redirect()` pattern); one whose body cannot render
|
|
35
|
+
* is a fault like any other, never a rejected request;
|
|
35
36
|
* - a request body the app's own `c.req.json()` or `c.req.formData()` could not parse is the
|
|
36
37
|
* client's 400 (`invalid_json`, `invalid_form`);
|
|
37
38
|
* - anything else is a real fault: logged once, then a bodyless 500 (never the caller's error text).
|
|
@@ -48,7 +49,15 @@ export function renderBareError<T>(
|
|
|
48
49
|
logError: (err: unknown, ctx: RawContext) => void,
|
|
49
50
|
wrapResponse: (response: ResponseResult) => T,
|
|
50
51
|
): T {
|
|
51
|
-
if (err instanceof Response || isResponseResult(err))
|
|
52
|
+
if (err instanceof Response || isResponseResult(err)) {
|
|
53
|
+
try {
|
|
54
|
+
return finalize(err, responseSet(ctx), ctx)
|
|
55
|
+
} catch (renderErr) {
|
|
56
|
+
// A thrown answer whose body cannot render (a `toJSON` that throws) is a fault, not control flow.
|
|
57
|
+
logError(renderErr, ctx)
|
|
58
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
const clientCode = clientBodyErrorCode(err)
|
|
53
62
|
if (clientCode !== undefined) return wrapResponse(plainError(400, clientCode))
|
|
54
63
|
logError(err, ctx)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { tagStandIn } from "../internal/hook-origins.ts"
|
|
12
12
|
import type { AnswerStages, JsonReplyContext } from "./answer-step.ts"
|
|
13
|
+
import { define } from "./json-shared.ts"
|
|
13
14
|
import { isResponseResult, status } from "./runtime-core.ts"
|
|
14
15
|
|
|
15
16
|
// Deeper than any real payload.
|
|
@@ -24,11 +25,6 @@ function base64(bytes: Uint8Array): string {
|
|
|
24
25
|
return btoa(binary)
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function define(out: object, key: string, value: unknown): void {
|
|
28
|
-
// `out[key] = value` for "__proto__" would replace the prototype instead of adding the field.
|
|
29
|
-
Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
/**
|
|
33
29
|
* `value` with each `Uint8Array` as base64, copying only the containers above one. A container already
|
|
34
30
|
* on the walk's path is left as it is, so a cycle costs one step, as it does `JSON.stringify`, which
|
|
@@ -52,7 +48,7 @@ function encoded(value: unknown, depth: number, path: Set<object>): unknown {
|
|
|
52
48
|
}
|
|
53
49
|
return out ?? value
|
|
54
50
|
}
|
|
55
|
-
let out:
|
|
51
|
+
let out: Record<string, unknown> | undefined
|
|
56
52
|
for (const key of Object.keys(value)) {
|
|
57
53
|
const item: unknown = Reflect.get(value, key)
|
|
58
54
|
const next = encoded(item, depth + 1, path)
|
package/src/server/body-lane.ts
CHANGED
|
@@ -22,16 +22,25 @@ type JsonErrorContinuation<T> = (error: unknown) => T | Promise<T>
|
|
|
22
22
|
|
|
23
23
|
type FramedJsonSource = RequestSource & {
|
|
24
24
|
readonly bytes?: () => Promise<Uint8Array>
|
|
25
|
+
readonly text?: () => Promise<string>
|
|
25
26
|
readonly jsonWithByteLength?: () => Promise<{
|
|
26
27
|
readonly value: unknown
|
|
27
28
|
readonly byteLength: number
|
|
28
29
|
}>
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Every lane calls `onResult` outside its parse guard: a throw from the continuation is the app's,
|
|
33
|
+
// and must never come back to the client as `invalid_json`.
|
|
31
34
|
const INVALID_JSON = (): ResponseResult => plainError(400, "invalid_json")
|
|
35
|
+
// A parameter, not a closure over the caller's `settle`: capturing it there costs every call a scope object.
|
|
36
|
+
const fromBuffer =
|
|
37
|
+
<T>(settle: (bytes: Uint8Array) => T) =>
|
|
38
|
+
(buffer: ArrayBuffer): T =>
|
|
39
|
+
settle(new Uint8Array(buffer))
|
|
32
40
|
|
|
33
41
|
// `Deno.serve`'s own `Request.json()` keeps a leading UTF-8 byte order mark that its `text()` and every
|
|
34
|
-
// other runtime's `json()` drop, so it refuses a body they parse. Deno
|
|
42
|
+
// other runtime's `json()` drop, so it refuses a body they parse. Deno uses `text()` for an
|
|
43
|
+
// untouched, runtime-framed request, or decodes the checked bytes below for other sources.
|
|
35
44
|
const nativeJsonKeepsBom = "Deno" in globalThis
|
|
36
45
|
|
|
37
46
|
/** Read a JSON source with the declared-length precheck, delivered-byte recheck, streaming cap, and
|
|
@@ -76,17 +85,39 @@ export function readBoundedJsonSource<T>(
|
|
|
76
85
|
? Promise.resolve(plainError(413, "payload_too_large"))
|
|
77
86
|
: Promise.resolve(plainError(413, "payload_too_large")).then(onResult)
|
|
78
87
|
|
|
88
|
+
// A trusted runtime frame makes Content-Length the byte bound. Wrapped or untrusted sources
|
|
89
|
+
// must keep the delivered-byte check below, and chunked bodies must keep the streaming cap.
|
|
90
|
+
if (
|
|
91
|
+
nativeJsonKeepsBom &&
|
|
92
|
+
raw === req &&
|
|
93
|
+
hasTrustedBodyFraming(req) &&
|
|
94
|
+
raw.text !== undefined &&
|
|
95
|
+
headerOf(req, "transfer-encoding") === null
|
|
96
|
+
) {
|
|
97
|
+
return raw.text().then(
|
|
98
|
+
(text) => {
|
|
99
|
+
let parsed: unknown
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseJsonGuarded(text, protoPoisoning)
|
|
102
|
+
} catch {
|
|
103
|
+
parsed = INVALID_JSON()
|
|
104
|
+
}
|
|
105
|
+
return onResult === undefined ? parsed : onResult(parsed)
|
|
106
|
+
},
|
|
107
|
+
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
if (hasTrustedBodyFraming(req) && !nativeJsonKeepsBom) {
|
|
80
112
|
return raw.json().then(
|
|
81
113
|
(parsed) => {
|
|
114
|
+
let guarded: unknown
|
|
82
115
|
try {
|
|
83
|
-
|
|
84
|
-
return onResult === undefined ? guarded : onResult(guarded)
|
|
116
|
+
guarded = guardParsedValue(parsed, protoPoisoning)
|
|
85
117
|
} catch {
|
|
86
|
-
|
|
87
|
-
? plainError(400, "invalid_json")
|
|
88
|
-
: onResult(plainError(400, "invalid_json"))
|
|
118
|
+
guarded = INVALID_JSON()
|
|
89
119
|
}
|
|
120
|
+
return onResult === undefined ? guarded : onResult(guarded)
|
|
90
121
|
},
|
|
91
122
|
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
92
123
|
)
|
|
@@ -102,60 +133,36 @@ export function readBoundedJsonSource<T>(
|
|
|
102
133
|
? plainError(413, "payload_too_large")
|
|
103
134
|
: onResult(plainError(413, "payload_too_large"))
|
|
104
135
|
}
|
|
136
|
+
let guarded: unknown
|
|
105
137
|
try {
|
|
106
|
-
|
|
107
|
-
return onResult === undefined ? guarded : onResult(guarded)
|
|
138
|
+
guarded = guardParsedValue(value, protoPoisoning)
|
|
108
139
|
} catch {
|
|
109
|
-
|
|
110
|
-
? plainError(400, "invalid_json")
|
|
111
|
-
: onResult(plainError(400, "invalid_json"))
|
|
140
|
+
guarded = INVALID_JSON()
|
|
112
141
|
}
|
|
142
|
+
return onResult === undefined ? guarded : onResult(guarded)
|
|
113
143
|
},
|
|
114
144
|
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
115
145
|
)
|
|
116
146
|
}
|
|
117
147
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return onResult === undefined
|
|
132
|
-
? plainError(400, "invalid_json")
|
|
133
|
-
: onResult(plainError(400, "invalid_json"))
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
137
|
-
)
|
|
148
|
+
const settle = (bytes: Uint8Array) => {
|
|
149
|
+
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
150
|
+
return onResult === undefined
|
|
151
|
+
? plainError(413, "payload_too_large")
|
|
152
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
153
|
+
}
|
|
154
|
+
let parsed: unknown
|
|
155
|
+
try {
|
|
156
|
+
parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
157
|
+
} catch {
|
|
158
|
+
parsed = INVALID_JSON()
|
|
159
|
+
}
|
|
160
|
+
return onResult === undefined ? parsed : onResult(parsed)
|
|
138
161
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
144
|
-
return onResult === undefined
|
|
145
|
-
? plainError(413, "payload_too_large")
|
|
146
|
-
: onResult(plainError(413, "payload_too_large"))
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
150
|
-
return onResult === undefined ? parsed : onResult(parsed)
|
|
151
|
-
} catch {
|
|
152
|
-
return onResult === undefined
|
|
153
|
-
? plainError(400, "invalid_json")
|
|
154
|
-
: onResult(plainError(400, "invalid_json"))
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
158
|
-
)
|
|
162
|
+
const refuse = () => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON()))
|
|
163
|
+
const bytesReader = raw.bytes
|
|
164
|
+
if (bytesReader !== undefined) return bytesReader.call(raw).then(settle, refuse)
|
|
165
|
+
return raw.arrayBuffer().then(fromBuffer(settle), refuse)
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
|
package/src/server/body.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { jsonError } from "./http.ts"
|
|
11
|
+
import { carryToCopy } from "./request-local.ts"
|
|
11
12
|
|
|
12
13
|
interface BodySource {
|
|
13
14
|
readonly headers: Pick<Headers, "get">
|
|
@@ -277,7 +278,9 @@ export function capBodyBytes(request: Request, maxBytes: number): Request {
|
|
|
277
278
|
),
|
|
278
279
|
duplex: "half",
|
|
279
280
|
}
|
|
280
|
-
|
|
281
|
+
const capped = new Request(request, init)
|
|
282
|
+
carryToCopy(request, capped)
|
|
283
|
+
return capped
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
/** First matching own-or-inherited descriptor, so instance shadows and prototype accessors both resolve. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Each entry names a symbol looked up with `Symbol.for(...)`, so the two packages agree ONLY by the
|
|
5
5
|
* string. A rename on one side and not the other is a silent protocol break - no type error, just a
|
|
6
6
|
* symbol that never matches and a body that stops taking the direct-write lane. This module is the
|
|
7
|
-
* single source: core defines its own symbols from these literals, and `scripts/gen-
|
|
7
|
+
* single source: core defines its own symbols from these literals, and `scripts/gen-vendored.ts`
|
|
8
8
|
* copies the map verbatim into the adapter's generated contract, so @nifrajs/node reads the identical
|
|
9
9
|
* strings without a runtime dependency on core (the same deployment shape the generated outcome type
|
|
10
10
|
* preserves).
|
package/src/server/cookies.ts
CHANGED
|
@@ -247,7 +247,10 @@ const toBase64Url = (buf: ArrayBuffer): string => {
|
|
|
247
247
|
let bin = ""
|
|
248
248
|
const bytes = new Uint8Array(buf)
|
|
249
249
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!)
|
|
250
|
-
return btoa(bin)
|
|
250
|
+
return btoa(bin)
|
|
251
|
+
.replace(/\+/g, "-")
|
|
252
|
+
.replace(/\//g, "_")
|
|
253
|
+
.replace(/={1,2}$/, "")
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
@@ -26,7 +26,7 @@ if (!copies.includes(here) && copies.push(here) > 1) {
|
|
|
26
26
|
|
|
27
27
|
/** This copy's version: a literal like the package root's `VERSION` (no fs on the edge), which
|
|
28
28
|
* `scripts/version.ts` rewrites on every release and `check:publish` asserts. */
|
|
29
|
-
const COPY_VERSION = "5.
|
|
29
|
+
const COPY_VERSION = "5.9.0"
|
|
30
30
|
|
|
31
31
|
/** Record this copy's version beside its URL. Called by each `server()` behind the development guard,
|
|
32
32
|
* so a production bundle drops it with the version; a copy that built a server has recorded one. */
|