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/src/http.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic HTTP ObjectStore adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Serves from ANY range-capable HTTP origin using plain `fetch`: Supabase
|
|
5
|
+
* Storage, S3/GCS/Azure presigned URLs, CDN origins, internal file services,
|
|
6
|
+
* or another partial-content server. Zero dependencies, every runtime.
|
|
7
|
+
*
|
|
8
|
+
* Capabilities mapped straight onto HTTP semantics:
|
|
9
|
+
* - `headObject` -> `HEAD` (metadata from Content-Length / ETag / Last-Modified)
|
|
10
|
+
* - `getObject` -> `GET` with `Range: bytes=...`
|
|
11
|
+
* - Pinned reads -> `If-Match` (origin 412 becomes {@link ObjectChangedError})
|
|
12
|
+
* - RFC 9530 -> `Repr-Digest: sha-256=:...:` response header is extracted
|
|
13
|
+
*
|
|
14
|
+
* Caveats:
|
|
15
|
+
* - The origin must send `Content-Length` (range-capable origins always do).
|
|
16
|
+
* - Presigned URLs are signed per-method: a GET-presigned URL will reject the
|
|
17
|
+
* HEAD probe. Presign both methods in `url()` (it receives the key only;
|
|
18
|
+
* return method-agnostic URLs, or front the origin with a service that
|
|
19
|
+
* accepts both).
|
|
20
|
+
* - Origins that ignore `If-Match` are still safe: the web adapter's
|
|
21
|
+
* response-side guard (actual Content-Range + GET validators) remains.
|
|
22
|
+
*
|
|
23
|
+
* @example Supabase Storage (authenticated)
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { httpStore } from "partial-content/http";
|
|
26
|
+
* import { serveObject } from "partial-content/web";
|
|
27
|
+
*
|
|
28
|
+
* const store = httpStore({
|
|
29
|
+
* url: (key) => `${SUPABASE_URL}/storage/v1/object/documents/${key}`,
|
|
30
|
+
* headers: { Authorization: `Bearer ${serviceRoleKey}` },
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* export const GET = serveObject(store, { disposition: "inline" });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
ObjectNotFoundError,
|
|
41
|
+
ObjectChangedError,
|
|
42
|
+
StoreUnavailableError,
|
|
43
|
+
resolveServedRange,
|
|
44
|
+
parseRetryAfterSeconds,
|
|
45
|
+
isOpenEndedRange,
|
|
46
|
+
type ObjectStore,
|
|
47
|
+
type ObjectMetadata,
|
|
48
|
+
type ObjectStream,
|
|
49
|
+
type ParsedRange,
|
|
50
|
+
} from "./index.js";
|
|
51
|
+
|
|
52
|
+
// Re-export for convenience
|
|
53
|
+
export { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError };
|
|
54
|
+
|
|
55
|
+
// ─── Options ────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface HttpStoreOptions {
|
|
58
|
+
/** Build the absolute URL for a storage key. */
|
|
59
|
+
url: (key: string) => string | URL;
|
|
60
|
+
/**
|
|
61
|
+
* Request headers (e.g. Authorization). A static record, or a function of
|
|
62
|
+
* the key for per-object credentials.
|
|
63
|
+
*/
|
|
64
|
+
headers?: Record<string, string> | ((key: string) => Record<string, string>);
|
|
65
|
+
/**
|
|
66
|
+
* Fetch implementation. Defaults to the global `fetch`.
|
|
67
|
+
* Inject for testing or to add retries/instrumentation. This is also the
|
|
68
|
+
* SSRF hook: when `url()` incorporates untrusted input, supply a fetch
|
|
69
|
+
* that validates resolved addresses (see SECURITY.md).
|
|
70
|
+
*/
|
|
71
|
+
fetch?: typeof globalThis.fetch;
|
|
72
|
+
/**
|
|
73
|
+
* Redirect policy for origin requests. Defaults to `"error"`: a serving layer
|
|
74
|
+
* whose `url()` may incorporate untrusted keys must not let a compromised or
|
|
75
|
+
* misconfigured origin bounce the store to internal endpoints (cloud metadata
|
|
76
|
+
* IPs) via a 3xx. Object-storage origins answer GET/HEAD with a direct 200, so
|
|
77
|
+
* the safe default costs nothing there. Set `"follow"` explicitly for origins
|
|
78
|
+
* that legitimately redirect (some presigned-URL or CDN front-door flows);
|
|
79
|
+
* pair it with a validating `fetch` (see SECURITY.md) when keys are untrusted.
|
|
80
|
+
*/
|
|
81
|
+
redirect?: "follow" | "error" | "manual";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create an {@link ObjectStore} backed by any range-capable HTTP origin.
|
|
88
|
+
*/
|
|
89
|
+
export function httpStore(opts: HttpStoreOptions): ObjectStore {
|
|
90
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
91
|
+
const redirect = opts.redirect ?? "error";
|
|
92
|
+
|
|
93
|
+
function requestHeaders(key: string): Record<string, string> {
|
|
94
|
+
const base = typeof opts.headers === "function"
|
|
95
|
+
? opts.headers(key)
|
|
96
|
+
: opts.headers ?? {};
|
|
97
|
+
return {
|
|
98
|
+
...base,
|
|
99
|
+
// Byte accounting depends on the origin NOT transparently compressing:
|
|
100
|
+
// a gzip'd body would break Content-Length/Content-Range math. Server
|
|
101
|
+
// runtimes (undici, Bun, Deno, Workers) honor this; identity is also
|
|
102
|
+
// what object-storage origins serve anyway.
|
|
103
|
+
"Accept-Encoding": "identity",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
supportsRange: true,
|
|
109
|
+
// 206 bounds/total are parsed from the origin's actual Content-Range
|
|
110
|
+
// (an origin ignoring Range degrades to 200): the orchestrator may skip
|
|
111
|
+
// the validating HEAD for plain range requests.
|
|
112
|
+
authoritativeRange: true,
|
|
113
|
+
|
|
114
|
+
async headObject(key: string, headOpts?: { signal?: AbortSignal }): Promise<ObjectMetadata> {
|
|
115
|
+
const response = await doFetch(opts.url(key), {
|
|
116
|
+
method: "HEAD",
|
|
117
|
+
headers: requestHeaders(key),
|
|
118
|
+
signal: headOpts?.signal,
|
|
119
|
+
redirect,
|
|
120
|
+
});
|
|
121
|
+
// HEAD bodies are empty; no need to drain before throwing.
|
|
122
|
+
if (response.status === 404) throw new ObjectNotFoundError(key);
|
|
123
|
+
if (isUnavailableStatus(response.status)) {
|
|
124
|
+
throw new StoreUnavailableError(key, { retryAfterSeconds: parseRetryAfter(response) });
|
|
125
|
+
}
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new Error(`httpStore HEAD ${key} failed: ${response.status}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const headEncoding = nonIdentityEncoding(response);
|
|
131
|
+
if (headEncoding) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`httpStore HEAD ${key}: origin returned Content-Encoding: ${headEncoding}; ` +
|
|
134
|
+
`Content-Length would report the compressed size, so range serving requires an identity-coded body`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const contentLength = parseSize(response.headers.get("content-length"));
|
|
139
|
+
if (contentLength === undefined) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`httpStore HEAD ${key}: origin sent no Content-Length; range serving requires sized responses`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
contentLength,
|
|
147
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
148
|
+
lastModified: response.headers.get("last-modified") ?? undefined,
|
|
149
|
+
digest: extractSha256Digest(response.headers.get("repr-digest")),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async getObject(
|
|
154
|
+
key: string,
|
|
155
|
+
getOpts?: { range?: ParsedRange; signal?: AbortSignal; ifMatch?: string },
|
|
156
|
+
): Promise<ObjectStream> {
|
|
157
|
+
const { range, signal, ifMatch } = getOpts ?? {};
|
|
158
|
+
const headers = requestHeaders(key);
|
|
159
|
+
// Open-ended fast-path ranges carry the OPEN_ENDED sentinel end; emit
|
|
160
|
+
// the bare `bytes=a-` form rather than a 16-digit last-byte-pos.
|
|
161
|
+
if (range) {
|
|
162
|
+
headers["Range"] = isOpenEndedRange(range)
|
|
163
|
+
? `bytes=${range.start}-`
|
|
164
|
+
: `bytes=${range.start}-${range.end}`;
|
|
165
|
+
}
|
|
166
|
+
// Pin the read to the validated representation. Origins that honor
|
|
167
|
+
// If-Match (S3, Azure, nginx, another partial-content server) answer
|
|
168
|
+
// 412 when the object changed; origins that ignore it fall through to
|
|
169
|
+
// the caller's response-side guard. Weak validators are never sent:
|
|
170
|
+
// RFC 9110 mandates strong comparison for If-Match, so a `W/` ETag
|
|
171
|
+
// would be rejected by every compliant origin on every attempt.
|
|
172
|
+
if (ifMatch && !ifMatch.startsWith("W/")) {
|
|
173
|
+
headers["If-Match"] = ifMatch;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const response = await doFetch(opts.url(key), {
|
|
177
|
+
method: "GET",
|
|
178
|
+
headers,
|
|
179
|
+
signal,
|
|
180
|
+
redirect,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (response.status === 404) {
|
|
184
|
+
await drain(response);
|
|
185
|
+
throw new ObjectNotFoundError(key);
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 412) {
|
|
188
|
+
await drain(response);
|
|
189
|
+
throw new ObjectChangedError(key);
|
|
190
|
+
}
|
|
191
|
+
if (isUnavailableStatus(response.status)) {
|
|
192
|
+
const retryAfterSeconds = parseRetryAfter(response);
|
|
193
|
+
await drain(response);
|
|
194
|
+
throw new StoreUnavailableError(key, { retryAfterSeconds });
|
|
195
|
+
}
|
|
196
|
+
if (response.status !== 200 && response.status !== 206) {
|
|
197
|
+
await drain(response);
|
|
198
|
+
throw new Error(`httpStore GET ${key} failed: ${response.status}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The fetch runtime transparently decodes a compressed body while
|
|
202
|
+
// Content-Length / Content-Range still describe the encoded bytes, so
|
|
203
|
+
// every byte count below would be wrong. Refuse rather than stream a
|
|
204
|
+
// body that disagrees with its own headers.
|
|
205
|
+
const getEncoding = nonIdentityEncoding(response);
|
|
206
|
+
if (getEncoding) {
|
|
207
|
+
await drain(response);
|
|
208
|
+
throw new Error(
|
|
209
|
+
`httpStore GET ${key}: origin returned Content-Encoding: ${getEncoding}; ` +
|
|
210
|
+
`range and length accounting require an identity-coded body`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const headerLength = parseSize(response.headers.get("content-length"));
|
|
215
|
+
let served: { start: number; end: number } | undefined;
|
|
216
|
+
let contentLength: number;
|
|
217
|
+
let totalSize: number | undefined;
|
|
218
|
+
|
|
219
|
+
if (response.status === 206) {
|
|
220
|
+
// A 206 MUST carry a parseable single-range Content-Range; without
|
|
221
|
+
// one the body's bounds are unknowable and serving it as anything
|
|
222
|
+
// would silently truncate or corrupt the client's view.
|
|
223
|
+
const contentRangeHeader = response.headers.get("content-range");
|
|
224
|
+
const resolved = contentRangeHeader ? resolveServedRange(contentRangeHeader) : null;
|
|
225
|
+
if (!resolved) {
|
|
226
|
+
await drain(response);
|
|
227
|
+
throw new Error(
|
|
228
|
+
`httpStore GET ${key}: origin sent 206 with ${contentRangeHeader ? `unparseable Content-Range "${contentRangeHeader}"` : "no Content-Range"}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
served = resolved.served;
|
|
232
|
+
// Content-Length, or derived from the actual Content-Range when the
|
|
233
|
+
// origin streams chunked 206s.
|
|
234
|
+
contentLength = headerLength ?? (resolved.served.end - resolved.served.start + 1);
|
|
235
|
+
// A proxied origin streaming a generated/transcoded body legitimately
|
|
236
|
+
// does not know its full length and sends `bytes a-b/*` (the resolver's
|
|
237
|
+
// `undefined` total). Propagate that so the served response repeats `*`
|
|
238
|
+
// honestly instead of fabricating a total that would draw a spurious
|
|
239
|
+
// 416 on a later range past the invented end.
|
|
240
|
+
totalSize = resolved.totalSize;
|
|
241
|
+
} else {
|
|
242
|
+
if (headerLength === undefined) {
|
|
243
|
+
await drain(response);
|
|
244
|
+
throw new Error(
|
|
245
|
+
`httpStore GET ${key}: origin sent no Content-Length; range serving requires sized responses`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
contentLength = headerLength;
|
|
249
|
+
totalSize = contentLength;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
body: response.body ?? emptyStream(),
|
|
254
|
+
contentLength,
|
|
255
|
+
totalSize,
|
|
256
|
+
range: served,
|
|
257
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
258
|
+
lastModified: response.headers.get("last-modified") ?? undefined,
|
|
259
|
+
digest: extractSha256Digest(response.headers.get("repr-digest")),
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Return the response's `Content-Encoding` when the origin transfer-compressed
|
|
269
|
+
* the body despite our `Accept-Encoding: identity` request, or `null` when the
|
|
270
|
+
* body is identity-coded. A compressed body is transparently decoded by the
|
|
271
|
+
* fetch runtime, but `Content-Length` keeps reporting the compressed size,
|
|
272
|
+
* which breaks all downstream byte accounting.
|
|
273
|
+
*/
|
|
274
|
+
function nonIdentityEncoding(response: Response): string | null {
|
|
275
|
+
const enc = response.headers.get("content-encoding");
|
|
276
|
+
return enc && enc.trim().toLowerCase() !== "identity" ? enc : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Parse a Content-Length header into a non-negative integer, or undefined. */
|
|
280
|
+
function parseSize(value: string | null): number | undefined {
|
|
281
|
+
if (!value || !/^\d+$/.test(value.trim())) return undefined;
|
|
282
|
+
const n = Number(value.trim());
|
|
283
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Whether an origin status means "transiently unavailable, retry" (mapped to
|
|
288
|
+
* a 503, not a 502): `503 Service Unavailable` and `429 Too Many Requests`.
|
|
289
|
+
*/
|
|
290
|
+
function isUnavailableStatus(status: number): boolean {
|
|
291
|
+
return status === 503 || status === 429;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Parse this origin response's `Retry-After` (RFC 9110 Section 10.2.3) into
|
|
296
|
+
* whole seconds via the shared parser: delay-seconds directly, or an HTTP-date
|
|
297
|
+
* as the non-negative delta from now. Returns undefined when absent/unparseable.
|
|
298
|
+
*/
|
|
299
|
+
function parseRetryAfter(response: Response): number | undefined {
|
|
300
|
+
return parseRetryAfterSeconds(response.headers.get("retry-after"), { allowHttpDate: true });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Extract the raw base64 SHA-256 from an RFC 9530 Repr-Digest header
|
|
305
|
+
* (`sha-256=:BASE64:`), or undefined when absent / different algorithm.
|
|
306
|
+
*/
|
|
307
|
+
function extractSha256Digest(header: string | null): string | undefined {
|
|
308
|
+
if (!header) return undefined;
|
|
309
|
+
const match = /sha-256=:([A-Za-z0-9+/]+=*):/i.exec(header);
|
|
310
|
+
return match ? match[1] : undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Consume up to `maxBytes` of an error/refusal body so the connection can be
|
|
315
|
+
* reused, then cancel. A bare `response.arrayBuffer()` would buffer the ENTIRE
|
|
316
|
+
* body -- and these paths include the malformed-206 and encoding-refusal
|
|
317
|
+
* branches whose body is the real (potentially whole-object) payload, plus the
|
|
318
|
+
* fetch runtime transparently INFLATES a compressed body during buffering. A
|
|
319
|
+
* hostile or misconfigured origin could then drive per-request OOM (a
|
|
320
|
+
* decompression bomb) just to make the adapter throw. Reading a small bounded
|
|
321
|
+
* prefix keeps connection reuse for genuine 404/412 error bodies while capping
|
|
322
|
+
* the cost; anything larger sacrifices one pooled connection, which is cheap.
|
|
323
|
+
*/
|
|
324
|
+
async function drain(response: Response, maxBytes = 64 * 1024): Promise<void> {
|
|
325
|
+
const body = response.body;
|
|
326
|
+
if (!body) return;
|
|
327
|
+
try {
|
|
328
|
+
const reader = body.getReader();
|
|
329
|
+
let seen = 0;
|
|
330
|
+
for (;;) {
|
|
331
|
+
const { done, value } = await reader.read();
|
|
332
|
+
if (done) return;
|
|
333
|
+
seen += value.byteLength;
|
|
334
|
+
if (seen > maxBytes) {
|
|
335
|
+
await reader.cancel();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// Body already consumed or connection gone; nothing to release.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** A closed empty stream for bodyless 200s (zero-byte objects). */
|
|
345
|
+
function emptyStream(): ReadableStream<Uint8Array> {
|
|
346
|
+
return new ReadableStream<Uint8Array>({
|
|
347
|
+
start(controller) {
|
|
348
|
+
controller.close();
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* partial-content
|
|
3
|
+
*
|
|
4
|
+
* RFC-compliant HTTP Range Requests, Conditional Requests, and
|
|
5
|
+
* Content-Disposition. Pure functions, zero dependencies, any runtime.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Range Requests (RFC 7233) ──────────────────────────────────────────────
|
|
11
|
+
export {
|
|
12
|
+
parseRangeHeader,
|
|
13
|
+
parseContentRange,
|
|
14
|
+
buildRangeResponseHeaders,
|
|
15
|
+
build416Headers,
|
|
16
|
+
OPEN_ENDED,
|
|
17
|
+
isOpenEndedRange,
|
|
18
|
+
} from "./kernel.js";
|
|
19
|
+
|
|
20
|
+
// ─── Multiple Ranges (multipart/byteranges) ─────────────────────────────────
|
|
21
|
+
export {
|
|
22
|
+
parseRanges,
|
|
23
|
+
MAX_RANGES_DEFAULT,
|
|
24
|
+
generateMultipartBoundary,
|
|
25
|
+
buildMultipartPartHeader,
|
|
26
|
+
multipartEpilogue,
|
|
27
|
+
buildMultipartHeaders,
|
|
28
|
+
} from "./kernel.js";
|
|
29
|
+
|
|
30
|
+
// ─── Header Safety ──────────────────────────────────────────────────────────
|
|
31
|
+
export { sanitizeHeaderValue } from "./kernel.js";
|
|
32
|
+
|
|
33
|
+
// ─── Conditional Requests (RFC 9110 / RFC 7232) ─────────────────────────────
|
|
34
|
+
export {
|
|
35
|
+
isConditionalFresh,
|
|
36
|
+
isPreconditionFailure,
|
|
37
|
+
isRangeFresh,
|
|
38
|
+
build304Headers,
|
|
39
|
+
build412Headers,
|
|
40
|
+
evaluateConditionalRequest,
|
|
41
|
+
evaluateConditionalWrite,
|
|
42
|
+
} from "./kernel.js";
|
|
43
|
+
|
|
44
|
+
// ─── Digest Negotiation (RFC 9530) ──────────────────────────────────────────
|
|
45
|
+
export { clientWantsDigest } from "./kernel.js";
|
|
46
|
+
|
|
47
|
+
// ─── ETag Generation ────────────────────────────────────────────────────────
|
|
48
|
+
export { generateETag } from "./kernel.js";
|
|
49
|
+
|
|
50
|
+
// ─── Adapters ───────────────────────────────────────────────────────────────
|
|
51
|
+
export { fromNodeHeaders } from "./kernel.js";
|
|
52
|
+
|
|
53
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
54
|
+
export type {
|
|
55
|
+
ParsedRange,
|
|
56
|
+
ParsedContentRange,
|
|
57
|
+
RangeResponseHeaderOpts,
|
|
58
|
+
RangeResponseHeaders,
|
|
59
|
+
EvaluatedRequest,
|
|
60
|
+
EvaluatedWrite,
|
|
61
|
+
ETagSource,
|
|
62
|
+
RangeSet,
|
|
63
|
+
MultipartResponse,
|
|
64
|
+
} from "./kernel.js";
|
|
65
|
+
|
|
66
|
+
// ─── Storage Contract ───────────────────────────────────────────────────────
|
|
67
|
+
export type {
|
|
68
|
+
CancelSignal,
|
|
69
|
+
ObjectStore,
|
|
70
|
+
ObjectMetadata,
|
|
71
|
+
ObjectStream,
|
|
72
|
+
ServedRange,
|
|
73
|
+
ResolvedContentRange,
|
|
74
|
+
HeadObjectOptions,
|
|
75
|
+
GetObjectOptions,
|
|
76
|
+
StoreErrorClassifiers,
|
|
77
|
+
} from "./object-store.js";
|
|
78
|
+
|
|
79
|
+
// ─── Storage Primitives ─────────────────────────────────────────────────────
|
|
80
|
+
export { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError, nodeStreamToWeb, guardStreamLength, resolveServedRange, classifyStoreRead, parseRetryAfterSeconds } from "./object-store.js";
|
|
81
|
+
|
|
82
|
+
// ─── Content-Disposition (RFC 6266 / RFC 8187) ──────────────────────────────
|
|
83
|
+
export { buildContentDisposition } from "./content-disposition.js";
|
|
84
|
+
export type { ContentDispositionOptions } from "./content-disposition.js";
|
|
85
|
+
|