@nifrajs/core 5.3.0 → 5.5.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 +65 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +2 -1
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/route-program.d.ts +5 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +36 -15
- package/dist/internal/route-program.js.map +1 -1
- package/dist/range.d.ts.map +1 -1
- package/dist/range.js +62 -2
- package/dist/range.js.map +1 -1
- package/dist/router/router.d.ts +3 -2
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +22 -6
- package/dist/router/router.js.map +1 -1
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +4 -3
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +4 -3
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts +2 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +5 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +9 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/idempotency-lane.d.ts +11 -12
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +301 -151
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/methods.d.ts +3 -2
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js.map +1 -1
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +42 -13
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +2 -2
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/query.d.ts +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +13 -5
- package/dist/server/query.js.map +1 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +3 -3
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +17 -0
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +81 -3
- package/dist/server/respond.js.map +1 -1
- package/dist/server/runtime-core.d.ts +15 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +41 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server.d.ts +14 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +187 -62
- package/dist/server/server.js.map +1 -1
- package/dist/server/transport-lane.d.ts +2 -2
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +20 -20
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/static-file.d.ts +127 -0
- package/dist/static-file.d.ts.map +1 -0
- package/dist/static-file.js +269 -0
- package/dist/static-file.js.map +1 -0
- package/package.json +6 -1
- package/src/index.ts +1 -1
- package/src/internal/route-lanes.ts +18 -16
- package/src/internal/route-program.ts +76 -12
- package/src/range.ts +61 -2
- package/src/router/router.ts +26 -5
- package/src/security/baseline.ts +4 -3
- package/src/server/body-lane.ts +4 -4
- package/src/server/body.ts +6 -0
- package/src/server/cookies.ts +7 -2
- package/src/server/core-copies.ts +1 -1
- package/src/server/idempotency-lane.ts +343 -209
- package/src/server/methods.ts +3 -2
- package/src/server/multipart.ts +62 -14
- package/src/server/node-outcome.ts +2 -1
- package/src/server/query.ts +12 -5
- package/src/server/request-context.ts +6 -3
- package/src/server/respond.ts +108 -6
- package/src/server/runtime-core.ts +54 -1
- package/src/server/server.ts +257 -102
- package/src/server/transport-lane.ts +28 -22
- package/src/static-file.ts +362 -0
|
@@ -158,6 +158,36 @@ interface RouteProgramRuntime {
|
|
|
158
158
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
159
159
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
160
160
|
): MaybePromise<T>
|
|
161
|
+
/** Installed by `idempotency()`, and present whenever a route resolved idempotency. */
|
|
162
|
+
readonly idempotencyRuntime: {
|
|
163
|
+
gate<T>(
|
|
164
|
+
entry: RouteEntry,
|
|
165
|
+
ctx: RawContext,
|
|
166
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
167
|
+
resume: IdempotencyResume<T>,
|
|
168
|
+
fail: (error: unknown) => MaybePromise<T>,
|
|
169
|
+
): Promise<T>
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The rest of a program after the idempotency gate. The gate continues it with the request it
|
|
175
|
+
* buffered, and with a replay entry and program when the key already holds a response.
|
|
176
|
+
*/
|
|
177
|
+
export type IdempotencyResume<T> = (entry: RouteEntry, program: RouteProgram) => ProgramResult<T>
|
|
178
|
+
|
|
179
|
+
/** Idempotency infrastructure failures use the same boundary as the resumed route program. */
|
|
180
|
+
function runIdempotencyGate<T>(
|
|
181
|
+
host: RouteProgramRuntime,
|
|
182
|
+
entry: RouteEntry,
|
|
183
|
+
ctx: RawContext,
|
|
184
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
185
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
186
|
+
resume: IdempotencyResume<T>,
|
|
187
|
+
): Promise<T> {
|
|
188
|
+
return host.idempotencyRuntime.gate(entry, ctx, wrapResponse, resume, (error) =>
|
|
189
|
+
host.handleProgramError(entry, error, ctx, finalize, wrapResponse),
|
|
190
|
+
)
|
|
161
191
|
}
|
|
162
192
|
|
|
163
193
|
function responseSet(ctx: RawContext): CtxSet {
|
|
@@ -278,6 +308,22 @@ function runAuthenticationStages<T>(
|
|
|
278
308
|
validationComplete = false,
|
|
279
309
|
): ProgramResult<T> {
|
|
280
310
|
if (start >= program.authStages.length) {
|
|
311
|
+
// Auth-first: the idempotency gate buffers the body, so it waits for every stage to pass. It
|
|
312
|
+
// resumes on an entry without the gate, so the program does not meet it twice.
|
|
313
|
+
if (entry.idempotent !== undefined && !validationComplete) {
|
|
314
|
+
return runIdempotencyGate(host, entry, ctx, finalize, wrapResponse, (e, p) =>
|
|
315
|
+
runAuthenticationStages(
|
|
316
|
+
host,
|
|
317
|
+
e,
|
|
318
|
+
p,
|
|
319
|
+
source,
|
|
320
|
+
ctx,
|
|
321
|
+
finalize,
|
|
322
|
+
wrapResponse,
|
|
323
|
+
p.authStages.length,
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
}
|
|
281
327
|
if (program.authFastLane) {
|
|
282
328
|
const validationIndex = validationComplete ? program.validationStages.length : 0
|
|
283
329
|
return runFastAuthenticatedProgram(
|
|
@@ -578,22 +624,33 @@ async function runAuthBeforeValidation<T>(
|
|
|
578
624
|
ctx: RawContext,
|
|
579
625
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
580
626
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
627
|
+
gated = false,
|
|
581
628
|
): Promise<T> {
|
|
582
629
|
try {
|
|
583
|
-
if (
|
|
630
|
+
if (!gated) {
|
|
631
|
+
if (program.hasDecorations) Object.assign(ctx, program.decorations)
|
|
584
632
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
633
|
+
for (const derive of program.derives) {
|
|
634
|
+
const result = derive(ctx)
|
|
635
|
+
const settled = result instanceof Promise ? await result : result
|
|
636
|
+
if (isEarly(settled)) return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
|
|
637
|
+
Object.assign(ctx, settled)
|
|
638
|
+
}
|
|
591
639
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
640
|
+
for (const before of program.beforeHandle) {
|
|
641
|
+
const result = before(ctx)
|
|
642
|
+
const settled = result instanceof Promise ? await result : result
|
|
643
|
+
if (settled !== undefined)
|
|
644
|
+
return finishEarly(host, entry, ctx, settled, finalize, wrapResponse)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// The policy hooks have passed, so the idempotency gate may buffer the body now. Not awaited:
|
|
648
|
+
// the gate reports its own failures, and one the resumed program let escape must skip this catch.
|
|
649
|
+
if (entry.idempotent !== undefined) {
|
|
650
|
+
return runIdempotencyGate(host, entry, ctx, finalize, wrapResponse, (e, p) =>
|
|
651
|
+
runAuthBeforeValidation(host, e, p, source, ctx, finalize, wrapResponse, true),
|
|
652
|
+
)
|
|
653
|
+
}
|
|
597
654
|
}
|
|
598
655
|
|
|
599
656
|
for (const stage of program.validationStages) {
|
|
@@ -634,6 +691,13 @@ export function executeRouteProgram<T, R extends Registry, Ctx>(
|
|
|
634
691
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
635
692
|
): ProgramResult<T> {
|
|
636
693
|
const host = runtime as unknown as RouteProgramRuntime
|
|
694
|
+
// An auth-first program meets the idempotency gate once authentication has passed, so the gate
|
|
695
|
+
// never buffers a body for a caller it would refuse. Every other program meets it first.
|
|
696
|
+
if (entry.idempotent !== undefined && !program.authBeforeValidation) {
|
|
697
|
+
return runIdempotencyGate(host, entry, ctx, finalize, wrapResponse, (e, p) =>
|
|
698
|
+
executeRouteProgram(runtime, e, p, source, ctx, finalize, wrapResponse),
|
|
699
|
+
)
|
|
700
|
+
}
|
|
637
701
|
try {
|
|
638
702
|
if (program.authStages.length > 0) {
|
|
639
703
|
if (program.hasDecorations) Object.assign(ctx, program.decorations)
|
package/src/range.ts
CHANGED
|
@@ -32,6 +32,63 @@ function validSize(size: number): void {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `bytes=<digits>-<digits>`, the one form clients send, answered with a single scan; `undefined`
|
|
37
|
+
* for anything else, which the general path below answers. The answers must stay the general path's:
|
|
38
|
+
* test/range.test.ts compares both against a frozen copy of it.
|
|
39
|
+
*/
|
|
40
|
+
function parseSingleRange(value: string, size: number): ByteRangeResult | undefined {
|
|
41
|
+
const length = value.length
|
|
42
|
+
// `| 0x20` folds only ASCII case, which is all the general path's /bytes/i matches.
|
|
43
|
+
if (
|
|
44
|
+
length < 7 ||
|
|
45
|
+
value.charCodeAt(5) !== 0x3d ||
|
|
46
|
+
(value.charCodeAt(0) | 0x20) !== 0x62 ||
|
|
47
|
+
(value.charCodeAt(1) | 0x20) !== 0x79 ||
|
|
48
|
+
(value.charCodeAt(2) | 0x20) !== 0x74 ||
|
|
49
|
+
(value.charCodeAt(3) | 0x20) !== 0x65 ||
|
|
50
|
+
(value.charCodeAt(4) | 0x20) !== 0x73
|
|
51
|
+
) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
let dash = -1
|
|
55
|
+
for (let index = 6; index < length; index++) {
|
|
56
|
+
const code = value.charCodeAt(index)
|
|
57
|
+
if (code === 0x2d) {
|
|
58
|
+
if (dash !== -1) return undefined
|
|
59
|
+
dash = index
|
|
60
|
+
} else if (code < 0x30 || code > 0x39) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (dash === -1) return undefined
|
|
65
|
+
const leftDigits = dash - 6
|
|
66
|
+
const rightDigits = length - dash - 1
|
|
67
|
+
// INTEGER: no leading zero.
|
|
68
|
+
if (
|
|
69
|
+
(leftDigits > 1 && value.charCodeAt(6) === 0x30) ||
|
|
70
|
+
(rightDigits > 1 && value.charCodeAt(dash + 1) === 0x30)
|
|
71
|
+
) {
|
|
72
|
+
return { kind: "none" }
|
|
73
|
+
}
|
|
74
|
+
let start: number
|
|
75
|
+
let end: number
|
|
76
|
+
if (leftDigits === 0) {
|
|
77
|
+
// A suffix with no length, or a zero one, is ignored.
|
|
78
|
+
if (rightDigits === 0 || (rightDigits === 1 && value.charCodeAt(dash + 1) === 0x30)) {
|
|
79
|
+
return { kind: "none" }
|
|
80
|
+
}
|
|
81
|
+
const suffix = Number(value.slice(dash + 1))
|
|
82
|
+
start = suffix >= size ? 0 : size - suffix
|
|
83
|
+
end = size - 1
|
|
84
|
+
} else {
|
|
85
|
+
start = Number(value.slice(6, dash))
|
|
86
|
+
end = rightDigits === 0 ? size - 1 : Math.min(Number(value.slice(dash + 1)), size - 1)
|
|
87
|
+
}
|
|
88
|
+
if (start > end || start >= size || size === 0) return { kind: "unsatisfiable" }
|
|
89
|
+
return { kind: "satisfiable", ranges: [{ start, end }] }
|
|
90
|
+
}
|
|
91
|
+
|
|
35
92
|
/**
|
|
36
93
|
* Parse an HTTP `Range: bytes=...` header against a known representation size.
|
|
37
94
|
*
|
|
@@ -45,12 +102,14 @@ export function parseByteRange(value: string | null, size: number): ByteRangeRes
|
|
|
45
102
|
// Bound the input before the regex runs, not after: an oversized header must never reach the
|
|
46
103
|
// matcher in the first place.
|
|
47
104
|
if (value.length > MAX_RANGE_HEADER_LENGTH) return { kind: "none" }
|
|
105
|
+
const single = parseSingleRange(value, size)
|
|
106
|
+
if (single !== undefined) return single
|
|
48
107
|
const match = /^bytes\s*=\s*(.+)$/i.exec(value)
|
|
49
108
|
if (match === null) return { kind: "none" }
|
|
50
109
|
const source = match[1]!
|
|
51
110
|
let separators = 0
|
|
52
|
-
for (
|
|
53
|
-
if (
|
|
111
|
+
for (let comma = source.indexOf(","); comma !== -1; comma = source.indexOf(",", comma + 1)) {
|
|
112
|
+
if (++separators >= MAX_RANGE_SPECIFIERS) return { kind: "none" }
|
|
54
113
|
}
|
|
55
114
|
|
|
56
115
|
const ranges: ByteRange[] = []
|
package/src/router/router.ts
CHANGED
|
@@ -10,8 +10,18 @@ import {
|
|
|
10
10
|
mixedSegmentShape,
|
|
11
11
|
} from "./pattern.ts"
|
|
12
12
|
|
|
13
|
-
/** The standard HTTP methods: the ones
|
|
14
|
-
|
|
13
|
+
/** The standard HTTP methods: the ones the typed registry and the typed client know. Each has a server
|
|
14
|
+
* builder except `QUERY` (RFC 9110's safe, idempotent read with a body), which `method()` registers. */
|
|
15
|
+
export const METHODS = [
|
|
16
|
+
"GET",
|
|
17
|
+
"POST",
|
|
18
|
+
"PUT",
|
|
19
|
+
"PATCH",
|
|
20
|
+
"DELETE",
|
|
21
|
+
"HEAD",
|
|
22
|
+
"OPTIONS",
|
|
23
|
+
"QUERY",
|
|
24
|
+
] as const
|
|
15
25
|
|
|
16
26
|
export type Method = (typeof METHODS)[number]
|
|
17
27
|
|
|
@@ -37,6 +47,8 @@ export function isRegistrableMethod(method: string): boolean {
|
|
|
37
47
|
const SLASH = 47
|
|
38
48
|
export const EMPTY_PARAMS: Record<string, string> = Object.freeze({})
|
|
39
49
|
const DYNAMIC_MATCH_CACHE_MAX = 2048
|
|
50
|
+
// The client picks every key, so their length is bounded too: 2048 paths of 8 KiB held 16 MiB.
|
|
51
|
+
const DYNAMIC_MATCH_CACHE_CHARS = 256 * 1024
|
|
40
52
|
|
|
41
53
|
/**
|
|
42
54
|
* Result of {@link Router.find}. The `found: false` cases deliberately separate
|
|
@@ -67,6 +79,8 @@ interface DynamicMatchCacheEntry<T> {
|
|
|
67
79
|
|
|
68
80
|
interface DynamicMatchCacheState<T> {
|
|
69
81
|
cache: Map<string, DynamicMatchCacheEntry<T>> | undefined
|
|
82
|
+
/** Total length of the cached paths. */
|
|
83
|
+
chars: number
|
|
70
84
|
pendingPath: string | undefined
|
|
71
85
|
}
|
|
72
86
|
|
|
@@ -79,6 +93,7 @@ function dynamicMatchCacheStateFor<T>(owner: object): DynamicMatchCacheState<T>
|
|
|
79
93
|
}
|
|
80
94
|
const state: DynamicMatchCacheState<T> = {
|
|
81
95
|
cache: undefined,
|
|
96
|
+
chars: 0,
|
|
82
97
|
pendingPath: undefined,
|
|
83
98
|
}
|
|
84
99
|
DYNAMIC_MATCH_CACHES.set(owner, state as DynamicMatchCacheState<unknown>)
|
|
@@ -476,13 +491,19 @@ export class Router<T> {
|
|
|
476
491
|
cache = new Map<string, DynamicMatchCacheEntry<T>>()
|
|
477
492
|
state.cache = cache
|
|
478
493
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
494
|
+
for (const oldest of cache.keys()) {
|
|
495
|
+
if (
|
|
496
|
+
cache.size < DYNAMIC_MATCH_CACHE_MAX &&
|
|
497
|
+
state.chars + path.length <= DYNAMIC_MATCH_CACHE_CHARS
|
|
498
|
+
)
|
|
499
|
+
break
|
|
500
|
+
cache.delete(oldest)
|
|
501
|
+
state.chars -= oldest.length
|
|
482
502
|
}
|
|
483
503
|
cache.set(path, {
|
|
484
504
|
terminal,
|
|
485
505
|
values: values.slice(0, terminal.paramNames.length),
|
|
486
506
|
})
|
|
507
|
+
state.chars += path.length
|
|
487
508
|
}
|
|
488
509
|
}
|
package/src/security/baseline.ts
CHANGED
|
@@ -41,9 +41,10 @@ export interface SecurityBaselineOptions {
|
|
|
41
41
|
readonly requireRuntimeProvenance?: boolean
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** A mutation is every method these leave out: POST, PUT, PATCH, DELETE and any custom method.
|
|
45
|
-
|
|
46
|
-
const
|
|
44
|
+
/** A mutation is every method these leave out: POST, PUT, PATCH, DELETE and any custom method. QUERY
|
|
45
|
+
* reads, with a body that the generic body rule still bounds. */
|
|
46
|
+
const NOT_MUTATING: readonly (Method | "WS")[] = ["GET", "HEAD", "OPTIONS", "QUERY", "WS"]
|
|
47
|
+
const READING = ["GET", "HEAD", "QUERY"] as const
|
|
47
48
|
/** The classification threshold above which the baseline demands authentication. */
|
|
48
49
|
const SENSITIVE = "pii" as const
|
|
49
50
|
|
package/src/server/body-lane.ts
CHANGED
|
@@ -11,8 +11,7 @@ import { isJsonMediaType, isUrlEncodedForm, readBoundedForm } from "./query.ts"
|
|
|
11
11
|
import {
|
|
12
12
|
headerOf,
|
|
13
13
|
isResponseResult,
|
|
14
|
-
|
|
15
|
-
type PreDecodedBody,
|
|
14
|
+
preDecodedBody,
|
|
16
15
|
type ResponseResult,
|
|
17
16
|
TEXT_DECODER,
|
|
18
17
|
} from "./runtime-core.ts"
|
|
@@ -52,7 +51,7 @@ export function readBoundedJsonSource<T>(
|
|
|
52
51
|
onResult?: JsonResultContinuation<T>,
|
|
53
52
|
onError?: JsonErrorContinuation<T>,
|
|
54
53
|
): Promise<unknown | ResponseResult | T> {
|
|
55
|
-
const preDecoded = (req
|
|
54
|
+
const preDecoded = preDecodedBody(req)
|
|
56
55
|
if (preDecoded !== undefined) {
|
|
57
56
|
const value =
|
|
58
57
|
preDecoded.byteLength > maxBytes ? plainError(413, "payload_too_large") : preDecoded.value
|
|
@@ -237,7 +236,8 @@ export function readBodyFramed<T>(
|
|
|
237
236
|
formOrigins?: FormOrigins,
|
|
238
237
|
): Promise<T> {
|
|
239
238
|
const contentType = headerOf(source, "content-type") ?? ""
|
|
240
|
-
|
|
239
|
+
// A body a transport codec decoded keeps its media type; the JSON lane below takes its value.
|
|
240
|
+
if (!isJsonMediaType(contentType) && preDecodedBody(source) === undefined) {
|
|
241
241
|
// JSON needs a preflight, so CORS judges it; a form body reaches here from any site with cookies.
|
|
242
242
|
if (isFormMediaType(contentType) && !formOriginAllowed(source, formOrigins)) {
|
|
243
243
|
return Promise.resolve(wrapResponse(plainError(403, "forbidden_origin")))
|
package/src/server/body.ts
CHANGED
|
@@ -462,6 +462,12 @@ export function markTransportCap(source: object, maxBytes: number): void {
|
|
|
462
462
|
;(source as { [TRANSPORT_CAP]?: number })[TRANSPORT_CAP] = maxBytes
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
/** The cap {@link markTransportCap} set on `source`, for a copy that must keep it. */
|
|
466
|
+
export function transportCapOf(source: object): number | undefined {
|
|
467
|
+
const cap: unknown = Reflect.get(source, TRANSPORT_CAP)
|
|
468
|
+
return typeof cap === "number" ? cap : undefined
|
|
469
|
+
}
|
|
470
|
+
|
|
465
471
|
/**
|
|
466
472
|
* Enforce a marked source's transport byte cap on **direct user reads** of its request
|
|
467
473
|
* (`c.req.json()`, `c.req.arrayBuffer()`, the raw `body` stream, `clone()`, ...), without swapping
|
package/src/server/cookies.ts
CHANGED
|
@@ -78,6 +78,9 @@ export function parseCookies(header: string | null | undefined): Record<string,
|
|
|
78
78
|
if (!header) return out
|
|
79
79
|
const len = header.length
|
|
80
80
|
let pos = 0
|
|
81
|
+
// The first "=" at or after the segment, kept while it lies in a later one, so a run of bare
|
|
82
|
+
// segments does not rescan the rest of the header each time. -1: none left.
|
|
83
|
+
let eq = header.indexOf("=")
|
|
81
84
|
while (pos < len) {
|
|
82
85
|
let end = header.indexOf(";", pos)
|
|
83
86
|
if (end === -1) end = len
|
|
@@ -86,7 +89,7 @@ export function parseCookies(header: string | null | undefined): Record<string,
|
|
|
86
89
|
while (start < end && header.charCodeAt(start) === 32) start++
|
|
87
90
|
let stop = end
|
|
88
91
|
while (stop > start && header.charCodeAt(stop - 1) === 32) stop--
|
|
89
|
-
|
|
92
|
+
if (eq !== -1 && eq < start) eq = header.indexOf("=", start)
|
|
90
93
|
if (eq > start && eq < stop) {
|
|
91
94
|
// name: trim trailing spaces before '='; value: trim leading spaces after it
|
|
92
95
|
let nameEnd = eq
|
|
@@ -125,6 +128,8 @@ export function hasDuplicateCookie(header: string | null | undefined, targetName
|
|
|
125
128
|
let found = false
|
|
126
129
|
const len = header.length
|
|
127
130
|
let pos = 0
|
|
131
|
+
// Kept across segments as in parseCookies.
|
|
132
|
+
let eq = header.indexOf("=")
|
|
128
133
|
while (pos < len) {
|
|
129
134
|
let end = header.indexOf(";", pos)
|
|
130
135
|
if (end === -1) end = len
|
|
@@ -132,7 +137,7 @@ export function hasDuplicateCookie(header: string | null | undefined, targetName
|
|
|
132
137
|
while (start < end && header.charCodeAt(start) === 32) start++
|
|
133
138
|
let stop = end
|
|
134
139
|
while (stop > start && header.charCodeAt(stop - 1) === 32) stop--
|
|
135
|
-
|
|
140
|
+
if (eq !== -1 && eq < start) eq = header.indexOf("=", start)
|
|
136
141
|
if (eq > start && eq < stop) {
|
|
137
142
|
let nameEnd = eq
|
|
138
143
|
while (nameEnd > start && header.charCodeAt(nameEnd - 1) === 32) nameEnd--
|
|
@@ -23,7 +23,7 @@ if (!copies.includes(here) && copies.push(here) > 1) {
|
|
|
23
23
|
|
|
24
24
|
/** This copy's version: a literal like the package root's `VERSION` (no fs on the edge), which
|
|
25
25
|
* `scripts/version.ts` rewrites on every release and `check:publish` asserts. */
|
|
26
|
-
const COPY_VERSION = "5.
|
|
26
|
+
const COPY_VERSION = "5.5.0"
|
|
27
27
|
|
|
28
28
|
/** Record this copy's version beside its URL. Called by each `server()` behind the development guard,
|
|
29
29
|
* so a production bundle drops it with the version; a copy that built a server has recorded one. */
|