partial-content 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/SECURITY.md +92 -0
- package/dist/azure.d.ts +79 -0
- package/dist/azure.d.ts.map +1 -0
- package/dist/azure.js +251 -0
- package/dist/azure.js.map +1 -0
- package/dist/content-disposition.d.ts +74 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +253 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/fs.d.ts +86 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +375 -0
- package/dist/fs.js.map +1 -0
- package/dist/gcs.d.ts +72 -0
- package/dist/gcs.d.ts.map +1 -0
- package/dist/gcs.js +202 -0
- package/dist/gcs.js.map +1 -0
- package/dist/hono.d.ts +92 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +61 -0
- package/dist/hono.js.map +1 -0
- package/dist/http.d.ts +70 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +281 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +541 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1218 -0
- package/dist/kernel.js.map +1 -0
- package/dist/memory.d.ts +55 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +107 -0
- package/dist/memory.js.map +1 -0
- package/dist/mime.d.ts +49 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +150 -0
- package/dist/mime.js.map +1 -0
- package/dist/node.d.ts +84 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +215 -0
- package/dist/node.js.map +1 -0
- package/dist/object-store.d.ts +472 -0
- package/dist/object-store.d.ts.map +1 -0
- package/dist/object-store.js +335 -0
- package/dist/object-store.js.map +1 -0
- package/dist/r2.d.ts +94 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +150 -0
- package/dist/r2.js.map +1 -0
- package/dist/s3.d.ts +49 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +263 -0
- package/dist/s3.js.map +1 -0
- package/dist/web.d.ts +336 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1094 -0
- package/dist/web.js.map +1 -0
- package/docs/DESIGN.md +426 -0
- package/package.json +182 -0
- package/src/azure.ts +329 -0
- package/src/content-disposition.ts +300 -0
- package/src/fs.ts +469 -0
- package/src/gcs.ts +290 -0
- package/src/hono.ts +123 -0
- package/src/http.ts +351 -0
- package/src/index.ts +85 -0
- package/src/kernel.ts +1498 -0
- package/src/memory.ts +148 -0
- package/src/mime.ts +160 -0
- package/src/node.ts +261 -0
- package/src/object-store.ts +665 -0
- package/src/r2.ts +232 -0
- package/src/s3.ts +324 -0
- package/src/web.ts +1603 -0
package/dist/kernel.d.ts
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Range header parser and response builder (RFC 7233 + RFC 9110).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for building RFC-compliant HTTP responses that serve
|
|
5
|
+
* files from any storage backend (S3, R2, GCS, local disk). Handles
|
|
6
|
+
* range parsing, conditional request evaluation, and response header
|
|
7
|
+
* construction.
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - `If-None-Match` -> 304 Not Modified (bandwidth savings on revisits)
|
|
11
|
+
* - `If-Match` / `If-Unmodified-Since` -> 412 Precondition Failed
|
|
12
|
+
* - `If-Range` -> ETag/date validation (prevents data corruption)
|
|
13
|
+
* - `Range: bytes=...` -> 206 Partial Content (seeking in video/PDF)
|
|
14
|
+
* - 416 Range Not Satisfiable (proper error for unsatisfiable ranges)
|
|
15
|
+
*
|
|
16
|
+
* Supports both single byte ranges and multiple ranges (`parseRanges` +
|
|
17
|
+
* the `multipart/byteranges` builders below). Multi-range coalesces
|
|
18
|
+
* overlapping/adjacent parts and caps the count (amplification defense); the
|
|
19
|
+
* single-range fast path is kept separate so the common seek pays no
|
|
20
|
+
* multi-range overhead.
|
|
21
|
+
*/
|
|
22
|
+
/** A validated, clamped byte range with inclusive bounds. */
|
|
23
|
+
export interface ParsedRange {
|
|
24
|
+
/** First byte position (0-indexed). */
|
|
25
|
+
start: number;
|
|
26
|
+
/**
|
|
27
|
+
* Last byte position (inclusive). May equal {@link OPEN_ENDED} on the
|
|
28
|
+
* single-round-trip fast path, meaning "to end of object" -- the range's
|
|
29
|
+
* true end is unknown until the backend responds. Adapters MUST treat
|
|
30
|
+
* `end === OPEN_ENDED` as an open-ended read (`bytes=start-`, or omit the
|
|
31
|
+
* count) rather than emitting the sentinel as a literal last-byte-pos.
|
|
32
|
+
*/
|
|
33
|
+
end: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sentinel `ParsedRange.end` meaning "to the end of the object". Used only
|
|
37
|
+
* by the fast path for a `bytes=a-` request, where the total size is not yet
|
|
38
|
+
* known. It is deliberately NOT `Number.MAX_SAFE_INTEGER`-as-a-magic-number
|
|
39
|
+
* at call sites: adapters compare against this named constant and emit the
|
|
40
|
+
* idiomatic open-ended wire form, so no 16-digit last-byte-pos ever reaches
|
|
41
|
+
* a backend or proxy that might reject it.
|
|
42
|
+
*/
|
|
43
|
+
export declare const OPEN_ENDED: number;
|
|
44
|
+
/** True when a range is the fast-path open-ended form (`bytes=start-`). */
|
|
45
|
+
export declare function isOpenEndedRange(range: {
|
|
46
|
+
end: number;
|
|
47
|
+
} | undefined | null): boolean;
|
|
48
|
+
/** Options for building range response headers. */
|
|
49
|
+
export interface RangeResponseHeaderOpts {
|
|
50
|
+
/**
|
|
51
|
+
* Total object size in bytes, or `undefined` when the total is unknown --
|
|
52
|
+
* a `bytes a-b/*` partial response from a streaming origin that does not
|
|
53
|
+
* know its full length (RFC 7233 Section 4.2). Unknown is valid ONLY
|
|
54
|
+
* alongside a `range`: a 206's `Content-Length` is the range span, so the
|
|
55
|
+
* total is not needed to size the body and is emitted as `*`. A full (200)
|
|
56
|
+
* response has no length without it and rejects `undefined`.
|
|
57
|
+
*/
|
|
58
|
+
totalSize: number | undefined;
|
|
59
|
+
/** Parsed range, or null for a full-content response. */
|
|
60
|
+
range: ParsedRange | null;
|
|
61
|
+
/** MIME type from storage metadata. */
|
|
62
|
+
contentType: string | undefined;
|
|
63
|
+
/** ETag from storage metadata, for conditional caching. */
|
|
64
|
+
etag: string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Last-Modified date from storage metadata.
|
|
67
|
+
*
|
|
68
|
+
* Accepts any string parseable by `Date.parse()` (ISO 8601, IMF-fixdate,
|
|
69
|
+
* RFC 850, asctime). The library normalizes it to IMF-fixdate
|
|
70
|
+
* (e.g. "Sun, 29 Jun 2025 12:00:00 GMT") before emitting the
|
|
71
|
+
* `Last-Modified` response header, because ISO strings are not valid
|
|
72
|
+
* HTTP-dates and would break `If-Modified-Since` revalidation.
|
|
73
|
+
*/
|
|
74
|
+
lastModified: string | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* RFC 9530 representation digest for end-to-end integrity.
|
|
77
|
+
*
|
|
78
|
+
* When provided, emitted as `Repr-Digest: sha-256=:<base64>:` on 200/206
|
|
79
|
+
* responses. The digest covers the *full representation* (not the partial
|
|
80
|
+
* range), so it remains stable across range requests.
|
|
81
|
+
*
|
|
82
|
+
* Storage backends typically provide this:
|
|
83
|
+
* - S3: `x-amz-checksum-sha256`
|
|
84
|
+
* - GCS: `x-goog-hash: crc32c=...,md5=...`
|
|
85
|
+
*
|
|
86
|
+
* Must be a raw base64-encoded SHA-256 hash (no prefix, no colons).
|
|
87
|
+
*/
|
|
88
|
+
digest?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Cache-Control directive to include in the response.
|
|
91
|
+
*
|
|
92
|
+
* The library never generates cache directives on its own. It only echoes
|
|
93
|
+
* a value you provide. Common patterns:
|
|
94
|
+
* - `"private, no-cache"` (revalidate every request)
|
|
95
|
+
* - `"private, max-age=3600"` (cache 1 hour)
|
|
96
|
+
* - `"public, max-age=31536000, immutable"` (content-addressed)
|
|
97
|
+
*/
|
|
98
|
+
cacheControl?: string;
|
|
99
|
+
}
|
|
100
|
+
/** Result of building range response headers. */
|
|
101
|
+
export interface RangeResponseHeaders {
|
|
102
|
+
/** HTTP status code: 200 for full content, 206 for partial, 304 for not modified, 412 for precondition failure, 416 for unsatisfiable. */
|
|
103
|
+
status: 200 | 206 | 304 | 412 | 416;
|
|
104
|
+
/** Headers to set on the response. */
|
|
105
|
+
headers: Record<string, string>;
|
|
106
|
+
}
|
|
107
|
+
/** Result of the full conditional request evaluation chain. */
|
|
108
|
+
export interface EvaluatedRequest extends RangeResponseHeaders {
|
|
109
|
+
/** Parsed range, or null if full content or an error status. */
|
|
110
|
+
range: ParsedRange | null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse an HTTP Range header value into a validated byte range.
|
|
114
|
+
*
|
|
115
|
+
* Supports three RFC 7233 forms:
|
|
116
|
+
* - `bytes=0-499` (first 500 bytes)
|
|
117
|
+
* - `bytes=500-` (everything from byte 500 onward)
|
|
118
|
+
* - `bytes=-500` (last 500 bytes, suffix range)
|
|
119
|
+
*
|
|
120
|
+
* Returns `null` for:
|
|
121
|
+
* - Missing or empty header
|
|
122
|
+
* - Malformed syntax
|
|
123
|
+
* - Multi-range requests (e.g. `bytes=0-100,200-300`)
|
|
124
|
+
* - Non-byte range units
|
|
125
|
+
*
|
|
126
|
+
* Returns `"unsatisfiable"` for:
|
|
127
|
+
* - Start >= totalSize (valid syntax but out of bounds)
|
|
128
|
+
* - Suffix of 0 bytes
|
|
129
|
+
* - Start > end after clamping
|
|
130
|
+
*
|
|
131
|
+
* This distinction matters: `null` means "not a range request" (serve 200),
|
|
132
|
+
* while `"unsatisfiable"` means "valid range syntax but cannot be honored" (serve 416).
|
|
133
|
+
*
|
|
134
|
+
* @param rangeHeader - The raw `Range` header value (e.g. "bytes=0-499")
|
|
135
|
+
* @param totalSize - Total file size in bytes
|
|
136
|
+
*/
|
|
137
|
+
export declare function parseRangeHeader(rangeHeader: string | null | undefined, totalSize: number): ParsedRange | "unsatisfiable" | null;
|
|
138
|
+
/** Default cap on distinct coalesced ranges before a request degrades to 200. */
|
|
139
|
+
export declare const MAX_RANGES_DEFAULT = 50;
|
|
140
|
+
/** A validated, coalesced set of satisfiable ranges. */
|
|
141
|
+
export interface RangeSet {
|
|
142
|
+
/**
|
|
143
|
+
* Coalesced satisfiable ranges in ascending order (length >= 1). A length
|
|
144
|
+
* of 1 is served as a normal single 206; length > 1 as multipart/byteranges.
|
|
145
|
+
*/
|
|
146
|
+
ranges: ParsedRange[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Parse a possibly-multi-range `Range` header into a coalesced, satisfiable
|
|
150
|
+
* range set, applying range-amplification defenses.
|
|
151
|
+
*
|
|
152
|
+
* Return contract mirrors {@link parseRangeHeader} but for a set:
|
|
153
|
+
* - `null` -- not a byte range, ignorable syntax, or a defense
|
|
154
|
+
* tripped: serve the full 200.
|
|
155
|
+
* - `"unsatisfiable"` -- every element was valid syntax but out of bounds:
|
|
156
|
+
* serve 416.
|
|
157
|
+
* - {@link RangeSet} -- 1+ satisfiable ranges (overlapping/adjacent already
|
|
158
|
+
* coalesced): serve 206 (single) or multipart (>1).
|
|
159
|
+
*
|
|
160
|
+
* Amplification defenses (informed by Go `net/http.ServeContent`'s
|
|
161
|
+
* sum-of-ranges check and nginx `max_ranges` + adjacent coalescing):
|
|
162
|
+
* 1. Overlapping/adjacent ranges are coalesced, so a client cannot force
|
|
163
|
+
* redundant bytes by requesting the same region many times.
|
|
164
|
+
* 2. If the coalesced set still exceeds `maxRanges` distinct parts, or the
|
|
165
|
+
* bytes it covers reach or exceed the whole representation, the ranges
|
|
166
|
+
* are ignored and the full 200 is served -- multipart framing over the
|
|
167
|
+
* entire file is pure overhead and a classic amplification vector.
|
|
168
|
+
*
|
|
169
|
+
* Empty list elements (`bytes=0-1,,2-3`) are skipped per the RFC 9110
|
|
170
|
+
* Section 5.6.1 list rule; a genuinely malformed element voids the whole
|
|
171
|
+
* header (serve 200), matching single-range leniency.
|
|
172
|
+
*/
|
|
173
|
+
export declare function parseRanges(rangeHeader: string | null | undefined, totalSize: number, maxRanges?: number): RangeSet | "unsatisfiable" | null;
|
|
174
|
+
/**
|
|
175
|
+
* Generate a multipart/byteranges boundary token. Hyphen-free so the token
|
|
176
|
+
* itself can never contain the `--` delimiter run; random so it cannot appear
|
|
177
|
+
* in body content by construction.
|
|
178
|
+
*/
|
|
179
|
+
export declare function generateMultipartBoundary(): string;
|
|
180
|
+
/**
|
|
181
|
+
* Build the per-part header block for one multipart/byteranges part:
|
|
182
|
+
* `--BOUNDARY CRLF [Content-Type CRLF] Content-Range CRLF CRLF`. The part's
|
|
183
|
+
* body bytes and a trailing CRLF follow (the caller emits those).
|
|
184
|
+
*/
|
|
185
|
+
export declare function buildMultipartPartHeader(boundary: string, range: ParsedRange, totalSize: number, contentType: string | undefined): string;
|
|
186
|
+
/** The closing delimiter for a multipart/byteranges body: `--BOUNDARY-- CRLF`. */
|
|
187
|
+
export declare function multipartEpilogue(boundary: string): string;
|
|
188
|
+
/** Result of {@link buildMultipartHeaders}. */
|
|
189
|
+
export interface MultipartResponse {
|
|
190
|
+
status: 206;
|
|
191
|
+
headers: Record<string, string>;
|
|
192
|
+
/** Exact byte length of the full multipart body (framing + all part bytes). */
|
|
193
|
+
contentLength: number;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build the top-level headers and exact Content-Length for a
|
|
197
|
+
* multipart/byteranges (206) response. The per-representation Content-Type
|
|
198
|
+
* lives in each part; the top-level type is `multipart/byteranges`.
|
|
199
|
+
*
|
|
200
|
+
* Content-Length is computed deterministically from the framing and the
|
|
201
|
+
* range spans, so the response is never chunked and the client gets an exact
|
|
202
|
+
* length up front. `contentType` is the representation's own MIME (placed in
|
|
203
|
+
* every part header).
|
|
204
|
+
*/
|
|
205
|
+
export declare function buildMultipartHeaders(opts: {
|
|
206
|
+
boundary: string;
|
|
207
|
+
ranges: ParsedRange[];
|
|
208
|
+
totalSize: number;
|
|
209
|
+
contentType: string | undefined;
|
|
210
|
+
etag?: string;
|
|
211
|
+
lastModified?: string;
|
|
212
|
+
cacheControl?: string;
|
|
213
|
+
digest?: string;
|
|
214
|
+
}): MultipartResponse;
|
|
215
|
+
/**
|
|
216
|
+
* Check if a conditional GET request is "fresh" (resource not modified).
|
|
217
|
+
*
|
|
218
|
+
* Implements RFC 7232 Section 3.2 (If-None-Match) and Section 3.3
|
|
219
|
+
* (If-Modified-Since). `If-None-Match` takes precedence per the spec.
|
|
220
|
+
*
|
|
221
|
+
* When this returns true, the server should respond with 304 Not Modified
|
|
222
|
+
* and skip streaming the body entirely.
|
|
223
|
+
*
|
|
224
|
+
* @param reqHeaders - Request headers (must support `.get(name)`)
|
|
225
|
+
* @param etag - Current resource ETag
|
|
226
|
+
* @param lastModified - Current resource Last-Modified date string
|
|
227
|
+
*/
|
|
228
|
+
export declare function isConditionalFresh(reqHeaders: {
|
|
229
|
+
get(name: string): string | null;
|
|
230
|
+
}, etag: string | undefined, lastModified: string | undefined): boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Check if a request's preconditions have failed.
|
|
233
|
+
*
|
|
234
|
+
* Implements RFC 7232 Section 3.1 (If-Match) and Section 3.4
|
|
235
|
+
* (If-Unmodified-Since). `If-Match` takes precedence per the spec.
|
|
236
|
+
*
|
|
237
|
+
* When this returns true, the server MUST respond with 412 Precondition
|
|
238
|
+
* Failed and skip processing the request body.
|
|
239
|
+
*
|
|
240
|
+
* Evaluation order per RFC 7232 Section 6:
|
|
241
|
+
* 1. isPreconditionFailure -> 412 (this function)
|
|
242
|
+
* 2. isConditionalFresh -> 304
|
|
243
|
+
* 3. Process range / serve content
|
|
244
|
+
*
|
|
245
|
+
* @param reqHeaders - Request headers (must support `.get(name)`)
|
|
246
|
+
* @param etag - Current resource ETag
|
|
247
|
+
* @param lastModified - Current resource Last-Modified date string
|
|
248
|
+
* @param exists - Whether the resource currently exists. When omitted,
|
|
249
|
+
* inferred from `etag !== undefined`. Callers that have already fetched
|
|
250
|
+
* metadata (e.g. the read orchestrator) should pass `true` explicitly.
|
|
251
|
+
*/
|
|
252
|
+
export declare function isPreconditionFailure(reqHeaders: {
|
|
253
|
+
get(name: string): string | null;
|
|
254
|
+
}, etag: string | undefined, lastModified: string | undefined, exists?: boolean): boolean;
|
|
255
|
+
/**
|
|
256
|
+
* Check if a Range request's `If-Range` precondition is "fresh".
|
|
257
|
+
*
|
|
258
|
+
* RFC 7233 Section 3.2: If the client sends `If-Range`, the server MUST
|
|
259
|
+
* only honor the Range when the validator matches. If it doesn't match,
|
|
260
|
+
* the resource has changed since the client cached it, so serving a partial
|
|
261
|
+
* response would result in data corruption (new bytes appended to old content).
|
|
262
|
+
*
|
|
263
|
+
* Returns `true` if the Range should be honored (validator matches or no If-Range).
|
|
264
|
+
* Returns `false` if the Range should be ignored (serve full 200 instead).
|
|
265
|
+
*
|
|
266
|
+
* @param reqHeaders - Request headers
|
|
267
|
+
* @param etag - Current resource ETag
|
|
268
|
+
* @param lastModified - Current resource Last-Modified date string
|
|
269
|
+
*/
|
|
270
|
+
export declare function isRangeFresh(reqHeaders: {
|
|
271
|
+
get(name: string): string | null;
|
|
272
|
+
}, etag: string | undefined, lastModified: string | undefined): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Build HTTP response headers for a full or partial content response.
|
|
275
|
+
*
|
|
276
|
+
* Always includes `Accept-Ranges: bytes` to advertise range support
|
|
277
|
+
* (even on 200 responses, per RFC 7233 Section 2.3).
|
|
278
|
+
*
|
|
279
|
+
* Handles:
|
|
280
|
+
* - 200 OK (full content, when range is null)
|
|
281
|
+
* - 206 Partial Content (valid range)
|
|
282
|
+
*
|
|
283
|
+
* For error statuses (304, 412, 416), use the dedicated builders
|
|
284
|
+
* (`build304Headers`, `build412Headers`, `build416Headers`).
|
|
285
|
+
*
|
|
286
|
+
* @returns Status code and headers dict ready to pass to `new Response()`.
|
|
287
|
+
*/
|
|
288
|
+
export declare function buildRangeResponseHeaders(opts: RangeResponseHeaderOpts): RangeResponseHeaders;
|
|
289
|
+
/**
|
|
290
|
+
* Build 416 Range Not Satisfiable response headers.
|
|
291
|
+
*
|
|
292
|
+
* Per RFC 7233 Section 4.4, the response MUST include a Content-Range
|
|
293
|
+
* header with the unsatisfied-range syntax: `bytes * /totalSize`.
|
|
294
|
+
*
|
|
295
|
+
* Deliberately omits ETag, Last-Modified, and Content-Type. These are
|
|
296
|
+
* representation metadata for the successful response; including them
|
|
297
|
+
* on error responses can poison shared caches.
|
|
298
|
+
*/
|
|
299
|
+
export declare function build416Headers(totalSize: number): RangeResponseHeaders;
|
|
300
|
+
/** Result of parsing a Content-Range response header. */
|
|
301
|
+
export interface ParsedContentRange {
|
|
302
|
+
/** First byte of the range (inclusive, 0-indexed). */
|
|
303
|
+
start: number;
|
|
304
|
+
/** Last byte of the range (inclusive). */
|
|
305
|
+
end: number;
|
|
306
|
+
/** Total size of the complete representation. */
|
|
307
|
+
totalSize: number;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Parse a `Content-Range` response header into structured fields.
|
|
311
|
+
*
|
|
312
|
+
* Understands the two RFC 7233 Section 4.2 forms:
|
|
313
|
+
* - `bytes 0-499/1000` (byte range with known total)
|
|
314
|
+
* - `bytes 0-499/*` (byte range with unknown total, returns `totalSize: -1`)
|
|
315
|
+
*
|
|
316
|
+
* The unsatisfied-range form (`bytes * /1000`) is NOT parsed because it carries
|
|
317
|
+
* no range information -- callers should check the HTTP status (416) instead.
|
|
318
|
+
*
|
|
319
|
+
* Returns `null` for:
|
|
320
|
+
* - Missing or empty header
|
|
321
|
+
* - Non-byte range units
|
|
322
|
+
* - Malformed syntax
|
|
323
|
+
* - Unsatisfied range form (`bytes * /...`)
|
|
324
|
+
*
|
|
325
|
+
* Storage adapters use this to extract `totalSize` from a 206 response when
|
|
326
|
+
* the backend serves a range slice. Without it, each adapter reimplements the
|
|
327
|
+
* same regex inline, with varying correctness on edge cases.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```typescript
|
|
331
|
+
* import { parseContentRange } from "partial-content";
|
|
332
|
+
*
|
|
333
|
+
* const cr = parseContentRange("bytes 0-499/1000");
|
|
334
|
+
* // => { start: 0, end: 499, totalSize: 1000 }
|
|
335
|
+
*
|
|
336
|
+
* const unknown = parseContentRange("bytes 0-499/*");
|
|
337
|
+
* // => { start: 0, end: 499, totalSize: -1 }
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
export declare function parseContentRange(header: string | null | undefined): ParsedContentRange | null;
|
|
341
|
+
/**
|
|
342
|
+
* Build 412 Precondition Failed response headers.
|
|
343
|
+
*
|
|
344
|
+
* Per RFC 7232 Section 4.2, a 412 response indicates that one or more
|
|
345
|
+
* conditions in the request headers (If-Match, If-Unmodified-Since)
|
|
346
|
+
* evaluated to false.
|
|
347
|
+
*
|
|
348
|
+
* Deliberately omits representation metadata (ETag, Last-Modified,
|
|
349
|
+
* Content-Type) to prevent cache poisoning.
|
|
350
|
+
*/
|
|
351
|
+
export declare function build412Headers(): RangeResponseHeaders;
|
|
352
|
+
/**
|
|
353
|
+
* Build 304 Not Modified response headers.
|
|
354
|
+
*
|
|
355
|
+
* Per RFC 7232 Section 4.1, a 304 response MUST NOT contain a message
|
|
356
|
+
* body and SHOULD NOT include representation headers (Content-Type,
|
|
357
|
+
* Content-Length, Content-Encoding, Content-Language, Content-Range).
|
|
358
|
+
*
|
|
359
|
+
* Only includes: ETag, Last-Modified, and Cache-Control.
|
|
360
|
+
*/
|
|
361
|
+
export declare function build304Headers(etag: string | undefined, lastModified: string | undefined, cacheControl?: string): RangeResponseHeaders;
|
|
362
|
+
/**
|
|
363
|
+
* Evaluate a conditional request and return the correct HTTP response.
|
|
364
|
+
*
|
|
365
|
+
* Implements the full RFC 7232 Section 6 evaluation chain in the correct
|
|
366
|
+
* order. This is the recommended entry point for most consumers -- it
|
|
367
|
+
* eliminates the risk of misordering the evaluation steps.
|
|
368
|
+
*
|
|
369
|
+
* Evaluation order:
|
|
370
|
+
* 1. `isPreconditionFailure` -> 412 Precondition Failed
|
|
371
|
+
* 2. `isConditionalFresh` -> 304 Not Modified
|
|
372
|
+
* 3. `isRangeFresh` -> Should we honor the Range?
|
|
373
|
+
* 4. `parseRangeHeader` -> Parse and validate the Range
|
|
374
|
+
* 5. `buildRangeResponseHeaders` or `build416Headers`
|
|
375
|
+
*
|
|
376
|
+
* @returns Status, headers, and the parsed range (null if full content or error status).
|
|
377
|
+
*/
|
|
378
|
+
export declare function evaluateConditionalRequest(reqHeaders: {
|
|
379
|
+
get(name: string): string | null;
|
|
380
|
+
}, meta: {
|
|
381
|
+
totalSize: number;
|
|
382
|
+
contentType?: string;
|
|
383
|
+
etag?: string;
|
|
384
|
+
lastModified?: string;
|
|
385
|
+
cacheControl?: string;
|
|
386
|
+
/** RFC 9530 SHA-256 digest (raw base64, no prefix). Emitted as `Repr-Digest` on 200/206. */
|
|
387
|
+
digest?: string;
|
|
388
|
+
}): EvaluatedRequest;
|
|
389
|
+
/**
|
|
390
|
+
* Result of a conditional-write evaluation.
|
|
391
|
+
*
|
|
392
|
+
* Discriminated union on `proceed`:
|
|
393
|
+
* - `{ proceed: true }` -- safe to execute the write.
|
|
394
|
+
* - `{ proceed: false, status: 412, headers }` -- return 412 to the client.
|
|
395
|
+
*/
|
|
396
|
+
export type EvaluatedWrite = {
|
|
397
|
+
proceed: true;
|
|
398
|
+
} | {
|
|
399
|
+
proceed: false;
|
|
400
|
+
status: 412;
|
|
401
|
+
headers: Record<string, string>;
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Evaluate conditional headers for a **write** request (PUT, PATCH, DELETE).
|
|
405
|
+
*
|
|
406
|
+
* On read requests (GET/HEAD), `If-None-Match` match triggers 304 Not Modified.
|
|
407
|
+
* On write requests, the same match triggers **412 Precondition Failed**
|
|
408
|
+
* (RFC 9110 Section 13.1.2). This function implements the write-side
|
|
409
|
+
* evaluation so consumers don't accidentally use the read-side orchestrator
|
|
410
|
+
* for OCC flows.
|
|
411
|
+
*
|
|
412
|
+
* Evaluation order (RFC 9110 Section 13.2.2):
|
|
413
|
+
* 1. `If-Match` / `If-Unmodified-Since` -> 412 (same as reads)
|
|
414
|
+
* 2. `If-None-Match` -> 412 (differs from reads: 412, not 304)
|
|
415
|
+
*
|
|
416
|
+
* ### OCC pattern (If-Match)
|
|
417
|
+
*
|
|
418
|
+
* ```typescript
|
|
419
|
+
* const result = evaluateConditionalWrite(req.headers, {
|
|
420
|
+
* etag: '"v2"',
|
|
421
|
+
* lastModified: "2025-06-28T12:00:00.000Z",
|
|
422
|
+
* });
|
|
423
|
+
* if (!result.proceed) {
|
|
424
|
+
* return new Response(null, { status: result.status, headers: result.headers });
|
|
425
|
+
* }
|
|
426
|
+
* // Safe to apply the mutation
|
|
427
|
+
* ```
|
|
428
|
+
*
|
|
429
|
+
* ### PUT-if-absent pattern (If-None-Match: *)
|
|
430
|
+
*
|
|
431
|
+
* ```typescript
|
|
432
|
+
* const result = evaluateConditionalWrite(req.headers, {
|
|
433
|
+
* etag: existingEtag, // undefined if resource doesn't exist yet
|
|
434
|
+
* exists: resourceExists,
|
|
435
|
+
* });
|
|
436
|
+
* ```
|
|
437
|
+
*
|
|
438
|
+
* @param reqHeaders - Request headers (must support `.get(name)`)
|
|
439
|
+
* @param meta - Current resource state
|
|
440
|
+
*/
|
|
441
|
+
export declare function evaluateConditionalWrite(reqHeaders: {
|
|
442
|
+
get(name: string): string | null;
|
|
443
|
+
}, meta: {
|
|
444
|
+
etag?: string;
|
|
445
|
+
lastModified?: string;
|
|
446
|
+
/**
|
|
447
|
+
* Whether the target resource currently exists. Used to evaluate
|
|
448
|
+
* `If-None-Match: *` (PUT-if-absent / create-only pattern).
|
|
449
|
+
*
|
|
450
|
+
* When omitted, existence is inferred from `etag !== undefined`.
|
|
451
|
+
*/
|
|
452
|
+
exists?: boolean;
|
|
453
|
+
}): EvaluatedWrite;
|
|
454
|
+
/**
|
|
455
|
+
* Adapter for Node.js HTTP servers (Express, Fastify, raw `http`).
|
|
456
|
+
*
|
|
457
|
+
* Converts a plain Node headers object (`IncomingHttpHeaders`) into the
|
|
458
|
+
* `{ get(name) }` interface expected by `partial-content` functions.
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* import { fromNodeHeaders, isConditionalFresh } from "partial-content";
|
|
463
|
+
*
|
|
464
|
+
* // Express
|
|
465
|
+
* app.get("/file", (req, res) => {
|
|
466
|
+
* const headers = fromNodeHeaders(req.headers);
|
|
467
|
+
* if (isConditionalFresh(headers, etag, lastModified)) {
|
|
468
|
+
* return res.status(304).end();
|
|
469
|
+
* }
|
|
470
|
+
* });
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
export declare function fromNodeHeaders(headers: Record<string, string | string[] | undefined>): {
|
|
474
|
+
get(name: string): string | null;
|
|
475
|
+
};
|
|
476
|
+
/** Metadata from a storage backend used to derive an entity-tag. */
|
|
477
|
+
export interface ETagSource {
|
|
478
|
+
/**
|
|
479
|
+
* Content digest from the backend (S3/Azure/GCS object hash).
|
|
480
|
+
* Yields a STRONG validator. May include surrounding quotes or `W/` prefix,
|
|
481
|
+
* both of which are normalized.
|
|
482
|
+
*/
|
|
483
|
+
hash?: string;
|
|
484
|
+
/** Object size in bytes. With `mtime`, yields a WEAK validator when no hash exists. */
|
|
485
|
+
size?: number;
|
|
486
|
+
/**
|
|
487
|
+
* Last-modified: `Date`, epoch-ms number, or any `Date.parse()`-able string.
|
|
488
|
+
* Floor to whole seconds for consistency with emitted `Last-Modified` headers.
|
|
489
|
+
*/
|
|
490
|
+
mtime?: Date | number | string;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Derive an entity-tag from storage metadata.
|
|
494
|
+
*
|
|
495
|
+
* This is a **formatter**, not a hasher. It classifies what the storage backend
|
|
496
|
+
* already provides into the correct validator strength per RFC 9110 Section 8.8.1:
|
|
497
|
+
*
|
|
498
|
+
* - `hash` present -> strong `"<hash>"` (a content digest asserts byte equality)
|
|
499
|
+
* - `size` + `mtime` only -> weak `W/"<size>-<mtime>"` (cannot assert byte equality;
|
|
500
|
+
* mtime resolution is coarse, collisions exist)
|
|
501
|
+
* - insufficient metadata -> `undefined` (caller omits ETag; never fabricate a
|
|
502
|
+
* validator we cannot stand behind)
|
|
503
|
+
*
|
|
504
|
+
* The dangerous failure mode is emitting a strong or size-only validator that ignores
|
|
505
|
+
* modification, which would serve stale 304s. Every `undefined` return guards that.
|
|
506
|
+
*
|
|
507
|
+
* @param source - Storage metadata to derive the ETag from
|
|
508
|
+
* @returns A correctly-typed ETag string, or `undefined` if insufficient metadata
|
|
509
|
+
*/
|
|
510
|
+
export declare function generateETag(source: ETagSource): string | undefined;
|
|
511
|
+
/**
|
|
512
|
+
* Strip bytes outside RFC 9110 field-value grammar to prevent HTTP header
|
|
513
|
+
* injection (CWE-113) and header-write crashes.
|
|
514
|
+
*
|
|
515
|
+
* Storage backends may pass through untrusted ETag, Last-Modified,
|
|
516
|
+
* Content-Type, digest, Cache-Control, or Location values. Exported so
|
|
517
|
+
* framework adapters can sanitize backend-derived headers the kernel does
|
|
518
|
+
* not build itself (e.g. the web adapter's signed-URL `Location`).
|
|
519
|
+
*/
|
|
520
|
+
export declare function sanitizeHeaderValue(s: string): string;
|
|
521
|
+
/**
|
|
522
|
+
* RFC 9530 Section 4: Check if the client wants a `sha-256` digest.
|
|
523
|
+
*
|
|
524
|
+
* `Want-Repr-Digest` uses Structured Fields Dictionary syntax:
|
|
525
|
+
* Want-Repr-Digest: sha-256=5, sha-512=3
|
|
526
|
+
*
|
|
527
|
+
* Each key is a hash algorithm, each value is a preference weight (0-10).
|
|
528
|
+
* Weight 0 means "explicitly unwanted." If the header is absent or contains
|
|
529
|
+
* `sha-256` with weight > 0, we emit the digest. If it only lists algorithms
|
|
530
|
+
* we don't support (e.g. `sha-512=5`), we skip emission entirely.
|
|
531
|
+
*
|
|
532
|
+
* We also check `Want-Content-Digest` since our Content-Digest uses the
|
|
533
|
+
* same algorithm.
|
|
534
|
+
*
|
|
535
|
+
* Exported so framework adapters apply the same negotiation the orchestrator
|
|
536
|
+
* uses internally -- digest emission behaves identically at every layer.
|
|
537
|
+
*/
|
|
538
|
+
export declare function clientWantsDigest(reqHeaders: {
|
|
539
|
+
get(name: string): string | null;
|
|
540
|
+
}): boolean;
|
|
541
|
+
//# sourceMappingURL=kernel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":"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"}
|