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.
Files changed (81) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +601 -0
  4. package/SECURITY.md +92 -0
  5. package/dist/azure.d.ts +79 -0
  6. package/dist/azure.d.ts.map +1 -0
  7. package/dist/azure.js +251 -0
  8. package/dist/azure.js.map +1 -0
  9. package/dist/content-disposition.d.ts +74 -0
  10. package/dist/content-disposition.d.ts.map +1 -0
  11. package/dist/content-disposition.js +253 -0
  12. package/dist/content-disposition.js.map +1 -0
  13. package/dist/fs.d.ts +86 -0
  14. package/dist/fs.d.ts.map +1 -0
  15. package/dist/fs.js +375 -0
  16. package/dist/fs.js.map +1 -0
  17. package/dist/gcs.d.ts +72 -0
  18. package/dist/gcs.d.ts.map +1 -0
  19. package/dist/gcs.js +202 -0
  20. package/dist/gcs.js.map +1 -0
  21. package/dist/hono.d.ts +92 -0
  22. package/dist/hono.d.ts.map +1 -0
  23. package/dist/hono.js +61 -0
  24. package/dist/hono.js.map +1 -0
  25. package/dist/http.d.ts +70 -0
  26. package/dist/http.d.ts.map +1 -0
  27. package/dist/http.js +281 -0
  28. package/dist/http.js.map +1 -0
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +27 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/kernel.d.ts +541 -0
  34. package/dist/kernel.d.ts.map +1 -0
  35. package/dist/kernel.js +1218 -0
  36. package/dist/kernel.js.map +1 -0
  37. package/dist/memory.d.ts +55 -0
  38. package/dist/memory.d.ts.map +1 -0
  39. package/dist/memory.js +107 -0
  40. package/dist/memory.js.map +1 -0
  41. package/dist/mime.d.ts +49 -0
  42. package/dist/mime.d.ts.map +1 -0
  43. package/dist/mime.js +150 -0
  44. package/dist/mime.js.map +1 -0
  45. package/dist/node.d.ts +84 -0
  46. package/dist/node.d.ts.map +1 -0
  47. package/dist/node.js +215 -0
  48. package/dist/node.js.map +1 -0
  49. package/dist/object-store.d.ts +472 -0
  50. package/dist/object-store.d.ts.map +1 -0
  51. package/dist/object-store.js +335 -0
  52. package/dist/object-store.js.map +1 -0
  53. package/dist/r2.d.ts +94 -0
  54. package/dist/r2.d.ts.map +1 -0
  55. package/dist/r2.js +150 -0
  56. package/dist/r2.js.map +1 -0
  57. package/dist/s3.d.ts +49 -0
  58. package/dist/s3.d.ts.map +1 -0
  59. package/dist/s3.js +263 -0
  60. package/dist/s3.js.map +1 -0
  61. package/dist/web.d.ts +336 -0
  62. package/dist/web.d.ts.map +1 -0
  63. package/dist/web.js +1094 -0
  64. package/dist/web.js.map +1 -0
  65. package/docs/DESIGN.md +426 -0
  66. package/package.json +182 -0
  67. package/src/azure.ts +329 -0
  68. package/src/content-disposition.ts +300 -0
  69. package/src/fs.ts +469 -0
  70. package/src/gcs.ts +290 -0
  71. package/src/hono.ts +123 -0
  72. package/src/http.ts +351 -0
  73. package/src/index.ts +85 -0
  74. package/src/kernel.ts +1498 -0
  75. package/src/memory.ts +148 -0
  76. package/src/mime.ts +160 -0
  77. package/src/node.ts +261 -0
  78. package/src/object-store.ts +665 -0
  79. package/src/r2.ts +232 -0
  80. package/src/s3.ts +324 -0
  81. package/src/web.ts +1603 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Storage backend contract for partial-content adapters.
3
+ *
4
+ * Implementations adapt a concrete store (S3, R2, Hetzner, GCS, fs) down to
5
+ * this surface. The kernel and framework adapters depend only on these types,
6
+ * never on a storage SDK.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { parseContentRange } from "./kernel.js";
11
+ // ─── Error Types ────────────────────────────────────────────────────────────
12
+ /**
13
+ * Thrown when an object does not exist in a storage backend.
14
+ *
15
+ * All built-in adapters (S3, R2, GCS, Azure, fs) throw this error
16
+ * for missing objects. Framework adapters (e.g. `partial-content/web`)
17
+ * use the `status` property to distinguish "object not found" (404)
18
+ * from a transiently unavailable backend (503, {@link StoreUnavailableError})
19
+ * and other store failures (502).
20
+ */
21
+ export class ObjectNotFoundError extends Error {
22
+ /** HTTP status hint for downstream handlers. */
23
+ status = 404;
24
+ /** The storage key that was not found. */
25
+ key;
26
+ constructor(key, cause) {
27
+ super(`Object not found: ${key}`);
28
+ this.name = "ObjectNotFoundError";
29
+ this.key = key;
30
+ if (cause !== undefined)
31
+ this.cause = cause;
32
+ }
33
+ }
34
+ /**
35
+ * Thrown when a pinned read ({@link GetObjectOptions.ifMatch}) finds the
36
+ * object changed since its validator was captured.
37
+ *
38
+ * Adapters map their backend's native rejection to this error (S3 412
39
+ * PreconditionFailed, R2 `onlyIf` body-less response, Azure 412
40
+ * ConditionNotMet, GCS etag mismatch). The web adapter treats it as a
41
+ * signal to re-validate the request against the object's new state.
42
+ */
43
+ export class ObjectChangedError extends Error {
44
+ /** HTTP status hint: the pinned read's precondition failed. */
45
+ status = 412;
46
+ /** The storage key whose object changed. */
47
+ key;
48
+ constructor(key, cause) {
49
+ super(`Object changed since validation: ${key}`);
50
+ this.name = "ObjectChangedError";
51
+ this.key = key;
52
+ if (cause !== undefined)
53
+ this.cause = cause;
54
+ }
55
+ }
56
+ /**
57
+ * Thrown when a storage backend is transiently unavailable: overloaded,
58
+ * throttling, or timing out after the adapter's own retries are exhausted.
59
+ *
60
+ * A generic store failure (a malformed response, an unparseable
61
+ * `Content-Range`, an empty body) is reported by the framework adapter as
62
+ * `502 Bad Gateway` -- "the upstream returned something invalid." This error
63
+ * is the distinct RETRYABLE case: the upstream is healthy but momentarily
64
+ * cannot serve. The web adapter maps it to `503 Service Unavailable` and, when
65
+ * {@link retryAfterSeconds} is set, emits a `Retry-After` header so clients and
66
+ * shared caches back off instead of hammering an origin that is already shedding
67
+ * load. Adapters map their backend's throttle signals here (S3/R2/Hetzner
68
+ * `503 SlowDown`, `429 TooManyRequests`, request timeouts).
69
+ */
70
+ export class StoreUnavailableError extends Error {
71
+ /** HTTP status hint: the backend is transiently unavailable. */
72
+ status = 503;
73
+ /** The storage key whose read failed. */
74
+ key;
75
+ /**
76
+ * Suggested back-off in whole seconds, echoed as `Retry-After`. Normalized at
77
+ * construction: a non-negative finite hint is floored to an integer (a
78
+ * fractional `2.9` -> `2`), and a NaN/negative/infinite/out-of-safe-range hint
79
+ * (from a hostile backend header or a buggy third-party classifier) is dropped
80
+ * entirely. So every consumer -- the web adapter AND anyone reading
81
+ * `.retryAfterSeconds` directly off the frozen contract -- sees a clean
82
+ * non-negative integer or `undefined`. Absent means the adapter emits `503`
83
+ * without a `Retry-After` (RFC 9110 Section 15.6.4 permits its absence).
84
+ */
85
+ retryAfterSeconds;
86
+ constructor(key, opts) {
87
+ super(`Storage backend unavailable: ${key}`);
88
+ this.name = "StoreUnavailableError";
89
+ this.key = key;
90
+ const hint = parseRetryAfterSeconds(opts?.retryAfterSeconds);
91
+ if (hint !== undefined)
92
+ this.retryAfterSeconds = hint;
93
+ if (opts?.cause !== undefined)
94
+ this.cause = opts.cause;
95
+ }
96
+ }
97
+ /**
98
+ * Normalize a `Retry-After` value to whole non-negative seconds, or `undefined`.
99
+ *
100
+ * The single sanctioned parser shared by every adapter and the web layer.
101
+ * Accepts the delay-seconds form (a number, or a numeric string) and -- when
102
+ * `allowHttpDate` is set -- the HTTP-date form (RFC 9110 Section 10.2.3) as a
103
+ * non-negative delta from now. Everything else yields `undefined`: NaN,
104
+ * Infinity, negatives, and any value whose floor exceeds `Number.MAX_SAFE_INTEGER`
105
+ * (so a huge finite hint can never serialize as `1e+21`, which violates the
106
+ * `delay-seconds = DIGIT+` grammar), plus non-numeric text. A hostile header or
107
+ * a buggy third-party classifier therefore can never emit a malformed header.
108
+ */
109
+ export function parseRetryAfterSeconds(raw, opts) {
110
+ if (typeof raw === "number") {
111
+ if (!Number.isFinite(raw) || raw < 0)
112
+ return undefined;
113
+ const floored = Math.floor(raw);
114
+ return Number.isSafeInteger(floored) ? floored : undefined;
115
+ }
116
+ if (typeof raw !== "string")
117
+ return undefined;
118
+ const trimmed = raw.trim();
119
+ if (/^\d+$/.test(trimmed)) {
120
+ const n = Number(trimmed);
121
+ return Number.isSafeInteger(n) ? n : undefined;
122
+ }
123
+ if (opts?.allowHttpDate) {
124
+ const when = Date.parse(trimmed);
125
+ if (Number.isNaN(when))
126
+ return undefined;
127
+ return Math.max(0, Math.round((when - Date.now()) / 1000));
128
+ }
129
+ return undefined;
130
+ }
131
+ /**
132
+ * Run a backend read and normalize its failures to the contract's error types.
133
+ *
134
+ * This is the single ordered classification pipeline every SDK-backed adapter
135
+ * shares: `notFound` -> `changed` -> `throttled` -> rethrow. Centralizing it
136
+ * means the "which errors are handled, in what order" contract is structural
137
+ * rather than copy-pasted into each adapter's head/get catch blocks, so a
138
+ * classifier can never be present on one path and silently missing on another.
139
+ * The predicates MUST be mutually exclusive on a given backend (a 404, 412, and
140
+ * 429/503 are distinct); the order only decides precedence if they are not.
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const meta = await classifyStoreRead(key, () => client.headObject(key), {
145
+ * notFound: isNotFound,
146
+ * changed: isPreconditionFailed,
147
+ * throttled: isThrottled,
148
+ * });
149
+ * ```
150
+ */
151
+ export async function classifyStoreRead(key, op, classifiers) {
152
+ try {
153
+ return await op();
154
+ }
155
+ catch (err) {
156
+ if (classifiers.notFound(err))
157
+ throw new ObjectNotFoundError(key, err);
158
+ if (classifiers.changed?.(err))
159
+ throw new ObjectChangedError(key, err);
160
+ const throttled = classifiers.throttled(err);
161
+ if (throttled) {
162
+ throw new StoreUnavailableError(key, {
163
+ cause: err,
164
+ // `throttled` may return the backend's advised back-off; a bare `true`
165
+ // means "throttled, no hint" and emits a 503 without `Retry-After`.
166
+ retryAfterSeconds: typeof throttled === "object" ? throttled.retryAfterSeconds : undefined,
167
+ });
168
+ }
169
+ throw err;
170
+ }
171
+ }
172
+ // ─── Node Stream Conversion ─────────────────────────────────────────────────
173
+ /**
174
+ * Convert a Node.js readable stream (async iterable) to a web ReadableStream
175
+ * with proper backpressure, Buffer coercion, signal propagation, and cleanup.
176
+ *
177
+ * This is the single implementation shared by all Node.js-based adapters
178
+ * (fs, GCS, Azure, S3 fallback). It handles:
179
+ *
180
+ * - **Pull-based backpressure**: the web ReadableStream's `pull()` method
181
+ * drives the async iterator, so chunks are only read when the consumer
182
+ * is ready.
183
+ * - **Buffer to Uint8Array coercion**: Node.js streams yield `Buffer`
184
+ * instances. We coerce to `Uint8Array` for cross-runtime compatibility
185
+ * (Buffer extends Uint8Array in Node, but the `instanceof` check
186
+ * ensures correctness in edge cases).
187
+ * - **AbortSignal propagation**: when the client disconnects (signal aborts),
188
+ * the underlying node stream is destroyed immediately to stop I/O.
189
+ * - **Cancel cleanup**: when the web ReadableStream is cancelled (e.g. by
190
+ * `Response.body.cancel()`), the node stream is destroyed.
191
+ *
192
+ * @param iterable - The async iterable (Node.js Readable stream). Typed
193
+ * `AsyncIterable<Uint8Array>` (Node `Buffer` extends `Uint8Array`, so Buffer
194
+ * streams satisfy it) to keep the `Buffer` name -- and its `@types/node`
195
+ * coupling -- out of the package's public type surface.
196
+ * @param opts.destroy - Destroys the underlying stream on cancel/abort. When
197
+ * omitted, a `destroy()` method on the iterable is auto-detected and used.
198
+ * @param opts.signal - Optional AbortSignal for client-disconnect detection
199
+ * @param opts.expectedBytes - Exact byte count the source promised. When set,
200
+ * a graceful end that delivered a different total (a file truncated or grown
201
+ * in place mid-read) errors the web stream instead of closing it short, so a
202
+ * torn body can never masquerade as a complete response under the committed
203
+ * `Content-Length`. A client abort tears down via a thrown iterator error,
204
+ * not a graceful end, so it is unaffected by this check.
205
+ */
206
+ export function nodeStreamToWeb(iterable, opts) {
207
+ const { signal, expectedBytes } = opts ?? {};
208
+ let seen = 0;
209
+ // Auto-detect the Node-stream destroy capability when the caller doesn't
210
+ // supply one: every adapter was hand-rolling the same
211
+ // `typeof stream.destroy === "function"` guard.
212
+ const nativeDestroy = iterable.destroy;
213
+ const destroy = opts?.destroy ?? (typeof nativeDestroy === "function" ? () => nativeDestroy.call(iterable) : undefined);
214
+ // Propagate abort signal to the underlying stream. The listener MUST be
215
+ // removed once the stream settles: a consumer that reuses one long-lived
216
+ // signal across many getObject calls would otherwise accumulate one
217
+ // listener (and one retained stream reference) per completed transfer
218
+ // until the signal is aborted or GC'd.
219
+ const abortListener = signal && destroy ? destroy : undefined;
220
+ if (signal && abortListener)
221
+ signal.addEventListener("abort", abortListener);
222
+ const detach = () => {
223
+ if (signal && abortListener)
224
+ signal.removeEventListener?.("abort", abortListener);
225
+ };
226
+ const iterator = iterable[Symbol.asyncIterator]();
227
+ return new ReadableStream({
228
+ async pull(controller) {
229
+ let result;
230
+ try {
231
+ result = await iterator.next();
232
+ }
233
+ catch (err) {
234
+ // Stop backend I/O when iteration fails, then error the web stream.
235
+ // Without this, a mid-transfer backend failure leaks the socket.
236
+ detach();
237
+ destroy?.();
238
+ throw err;
239
+ }
240
+ if (result.done) {
241
+ detach();
242
+ // A graceful end that under- or over-ran the promised length means the
243
+ // source changed under an already-committed Content-Length: fail the
244
+ // body loudly (a reset the client sees as a failed transfer) rather
245
+ // than deliver a truncated stream that looks complete.
246
+ if (expectedBytes !== undefined && seen !== expectedBytes) {
247
+ destroy?.();
248
+ controller.error(new Error(`stream delivered ${seen} bytes, expected ${expectedBytes} (source changed mid-read)`));
249
+ return;
250
+ }
251
+ controller.close();
252
+ }
253
+ else {
254
+ // Node.js streams yield Buffer; ensure Uint8Array for cross-runtime compat.
255
+ // Node Buffers and adapter chunks are ArrayBuffer-backed, so the narrow
256
+ // is runtime-safe and lets the web stream advertise a BodyInit-assignable
257
+ // Uint8Array<ArrayBuffer> (see F5 rationale in guardStreamLength).
258
+ const raw = result.value instanceof Uint8Array ? result.value : Buffer.from(result.value);
259
+ const chunk = raw;
260
+ seen += chunk.byteLength;
261
+ controller.enqueue(chunk);
262
+ }
263
+ },
264
+ async cancel(reason) {
265
+ detach();
266
+ try {
267
+ await iterator.return?.(reason);
268
+ }
269
+ catch {
270
+ // Iterator cleanup failed; socket will be GC'd
271
+ }
272
+ destroy?.();
273
+ },
274
+ });
275
+ }
276
+ /**
277
+ * Wrap a web `ReadableStream` so a graceful end that delivered a byte count
278
+ * other than `expectedBytes` errors the stream instead of closing it short.
279
+ *
280
+ * The Node-stream path gets this guard inside {@link nodeStreamToWeb}. This is
281
+ * the equivalent for adapters whose SDK hands back a web `ReadableStream`
282
+ * directly (S3 in Bun/Deno or via `transformToWebStream`, R2's native binding,
283
+ * a browser `Blob` stream) rather than a Node `Readable`. Without it, an
284
+ * S3-compatible backend that ends a body cleanly but short of the committed
285
+ * `Content-Length` (some do in-flight body retries) would under-run the
286
+ * response undetected -- the exact truncation `expectedBytes` exists to catch.
287
+ * Passing `undefined` disables the check and returns the stream unwrapped.
288
+ */
289
+ export function guardStreamLength(stream, expectedBytes) {
290
+ // Return position narrows to <ArrayBuffer> (backend byte chunks are always
291
+ // ArrayBuffer-backed) so the guarded body stays `new Response(...)`-assignable
292
+ // under DOM lib; the input stays wide for callers.
293
+ if (expectedBytes === undefined)
294
+ return stream;
295
+ let seen = 0;
296
+ return stream.pipeThrough(new TransformStream({
297
+ transform(chunk, controller) {
298
+ seen += chunk.byteLength;
299
+ controller.enqueue(chunk);
300
+ },
301
+ flush(controller) {
302
+ // A short (or long) graceful end means the source diverged from the
303
+ // committed length: fail the body loudly rather than deliver a
304
+ // truncated stream that looks complete. Mirrors nodeStreamToWeb.
305
+ if (seen !== expectedBytes) {
306
+ controller.error(new Error(`stream delivered ${seen} bytes, expected ${expectedBytes} (source changed mid-read)`));
307
+ }
308
+ },
309
+ }));
310
+ }
311
+ /**
312
+ * Parse a backend `Content-Range` response header into served bounds plus an
313
+ * honest total, applying the parser's `-1` unknown-total sentinel -> `undefined`
314
+ * mapping that every SDK adapter needs identically. Returns `null` ONLY when the
315
+ * header is present but unparseable -- the exact "byte accounting is
316
+ * untrustworthy, tear down and fail loudly" signal, which each adapter pairs
317
+ * with its own live-body cleanup (S3 `stream.cancel()`, Azure
318
+ * `destroyAzureDownload`, http `drain`) before throwing.
319
+ *
320
+ * Adapters that treat an ABSENT header as a full 200 (S3, Azure) check for the
321
+ * header before calling; callers that already know the response is partial
322
+ * (http's 206 branch) treat both an absent header and a `null` here as the same
323
+ * malformed-206 failure. Adapters that know their bounds natively (fs, memory,
324
+ * GCS, R2) construct {@link ServedRange} directly and never call this.
325
+ */
326
+ export function resolveServedRange(contentRange) {
327
+ const parsed = parseContentRange(contentRange);
328
+ if (!parsed)
329
+ return null;
330
+ return {
331
+ served: { start: parsed.start, end: parsed.end },
332
+ totalSize: parsed.totalSize < 0 ? undefined : parsed.totalSize,
333
+ };
334
+ }
335
+ //# sourceMappingURL=object-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-store.js","sourceRoot":"","sources":["../src/object-store.ts"],"names":[],"mappings":"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"}
package/dist/r2.d.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Cloudflare R2 native ObjectStore adapter for partial-content.
3
+ *
4
+ * Uses Cloudflare's native R2 bindings (`R2Bucket`) directly, without the
5
+ * AWS SDK. This eliminates the ~50-package `@aws-sdk/client-s3` dependency
6
+ * and its cold-start overhead in Workers.
7
+ *
8
+ * For S3-compatible access to R2 (e.g. from Node.js outside Workers),
9
+ * use `partial-content/s3` instead.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { r2Store } from "partial-content/r2";
14
+ * import { serveObject } from "partial-content/hono";
15
+ *
16
+ * // In a Cloudflare Worker with R2 binding:
17
+ * app.get("/files/:key", serveObject(
18
+ * r2Store({ bucket: env.MY_BUCKET }),
19
+ * { key: (c) => c.req.param("key") },
20
+ * ));
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ import { ObjectNotFoundError, ObjectChangedError, type ObjectStore } from "./index.js";
26
+ export { ObjectNotFoundError, ObjectChangedError };
27
+ /**
28
+ * Minimal R2Bucket interface from Cloudflare Workers types.
29
+ *
30
+ * Declared locally to avoid a dependency on `@cloudflare/workers-types`.
31
+ * The shapes match the runtime behavior; type-checking is the caller's
32
+ * responsibility (their Worker project imports the full types).
33
+ */
34
+ interface R2Bucket {
35
+ head(key: string): Promise<R2Object | null>;
36
+ /**
37
+ * With `onlyIf`, a failed precondition resolves to a body-less `R2Object`
38
+ * (not null, not an error) -- callers must check for `body`.
39
+ */
40
+ get(key: string, options?: R2GetOptions): Promise<R2Object | R2ObjectBody | null>;
41
+ }
42
+ interface R2Object {
43
+ key: string;
44
+ size: number;
45
+ etag: string;
46
+ uploaded: Date;
47
+ checksums: R2Checksums;
48
+ httpMetadata?: R2HttpMetadata;
49
+ }
50
+ interface R2ObjectBody extends R2Object {
51
+ body: ReadableStream<Uint8Array>;
52
+ range: R2Range;
53
+ }
54
+ interface R2Range {
55
+ offset: number;
56
+ length: number;
57
+ }
58
+ interface R2GetOptions {
59
+ range?: {
60
+ offset: number;
61
+ length?: number;
62
+ };
63
+ onlyIf?: {
64
+ etagMatches?: string;
65
+ };
66
+ }
67
+ interface R2Checksums {
68
+ sha256?: ArrayBuffer;
69
+ }
70
+ interface R2HttpMetadata {
71
+ contentType?: string;
72
+ }
73
+ export interface R2StoreOptions {
74
+ /** The R2 bucket binding from the Worker environment. */
75
+ bucket: R2Bucket;
76
+ }
77
+ /**
78
+ * Create an {@link ObjectStore} backed by a Cloudflare R2 bucket.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { r2Store } from "partial-content/r2";
83
+ *
84
+ * export default {
85
+ * async fetch(request, env) {
86
+ * const store = r2Store({ bucket: env.MY_BUCKET });
87
+ * const meta = await store.headObject("reports/q4.pdf");
88
+ * // ...
89
+ * },
90
+ * };
91
+ * ```
92
+ */
93
+ export declare function r2Store(opts: R2StoreOptions): ObjectStore;
94
+ //# sourceMappingURL=r2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAGL,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;AAInD;;;;;;GAMG;AACH,UAAU,QAAQ;IAChB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAC5C;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,CAAC;CACnF;AAED,UAAU,QAAQ;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,WAAW,CAAC;IACvB,YAAY,CAAC,EAAE,cAAc,CAAC;CAC/B;AAED,UAAU,YAAa,SAAQ,QAAQ;IACrC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACjC,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,OAAO;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,YAAY;IAGpB,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC;AAED,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,MAAM,EAAE,QAAQ,CAAC;CAClB;AAwBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CA+FzD"}
package/dist/r2.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Cloudflare R2 native ObjectStore adapter for partial-content.
3
+ *
4
+ * Uses Cloudflare's native R2 bindings (`R2Bucket`) directly, without the
5
+ * AWS SDK. This eliminates the ~50-package `@aws-sdk/client-s3` dependency
6
+ * and its cold-start overhead in Workers.
7
+ *
8
+ * For S3-compatible access to R2 (e.g. from Node.js outside Workers),
9
+ * use `partial-content/s3` instead.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { r2Store } from "partial-content/r2";
14
+ * import { serveObject } from "partial-content/hono";
15
+ *
16
+ * // In a Cloudflare Worker with R2 binding:
17
+ * app.get("/files/:key", serveObject(
18
+ * r2Store({ bucket: env.MY_BUCKET }),
19
+ * { key: (c) => c.req.param("key") },
20
+ * ));
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ import { isOpenEndedRange, guardStreamLength, ObjectNotFoundError, ObjectChangedError, } from "./index.js";
26
+ // Re-export for convenience
27
+ export { ObjectNotFoundError, ObjectChangedError };
28
+ // ─── Helpers ────────────────────────────────────────────────────────────────
29
+ /**
30
+ * Convert an ArrayBuffer SHA-256 checksum to base64 for RFC 9530 Repr-Digest.
31
+ *
32
+ * `checksums.sha256` is R2's whole-object SHA-256 as raw bytes, so unlike the
33
+ * S3 adapter there is no `<base64>-<partCount>` composite string form to
34
+ * reject: a 32-byte buffer always describes the full representation. If R2
35
+ * ever exposes a per-part checksum in this field, this digest would need the
36
+ * same whole-object guard S3 applies.
37
+ */
38
+ function arrayBufferToBase64(buffer) {
39
+ const bytes = new Uint8Array(buffer);
40
+ let binary = "";
41
+ for (let i = 0; i < bytes.length; i++) {
42
+ binary += String.fromCharCode(bytes[i]);
43
+ }
44
+ return btoa(binary);
45
+ }
46
+ // ─── Factory ────────────────────────────────────────────────────────────────
47
+ /**
48
+ * Create an {@link ObjectStore} backed by a Cloudflare R2 bucket.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { r2Store } from "partial-content/r2";
53
+ *
54
+ * export default {
55
+ * async fetch(request, env) {
56
+ * const store = r2Store({ bucket: env.MY_BUCKET });
57
+ * const meta = await store.headObject("reports/q4.pdf");
58
+ * // ...
59
+ * },
60
+ * };
61
+ * ```
62
+ */
63
+ export function r2Store(opts) {
64
+ const { bucket } = opts;
65
+ // No `classifyStoreRead` here (unlike s3/gcs/azure): the R2 Workers binding
66
+ // signals its recoverable outcomes structurally, not as classifiable errors.
67
+ // Not-found is a `null` return, a failed `onlyIf` pin is a body-less object,
68
+ // and both are handled inline below. R2 exposes NO documented throttle/503
69
+ // error shape on the native binding (rate limiting surfaces at the Workers
70
+ // platform layer, before the handler runs), so there is nothing to map to
71
+ // `StoreUnavailableError`. Inventing a classifier for an undocumented error
72
+ // shape would be guessing; the README correctly omits R2 from the throttle-
73
+ // classification list. Use `partial-content/s3` (R2's S3 endpoint) if you
74
+ // need SlowDown/429 -> 503 mapping.
75
+ return {
76
+ supportsRange: true,
77
+ // 206 bounds/total come from the R2ObjectBody's actual size/offset: the
78
+ // orchestrator may skip the validating HEAD for plain range requests.
79
+ authoritativeRange: true,
80
+ async headObject(key, opts) {
81
+ opts?.signal?.throwIfAborted();
82
+ const obj = await bucket.head(key);
83
+ if (!obj)
84
+ throw new ObjectNotFoundError(key);
85
+ return {
86
+ contentLength: obj.size,
87
+ etag: obj.etag,
88
+ lastModified: obj.uploaded.toUTCString(),
89
+ digest: obj.checksums.sha256
90
+ ? arrayBufferToBase64(obj.checksums.sha256)
91
+ : undefined,
92
+ };
93
+ },
94
+ async getObject(key, opts) {
95
+ const { range, ifMatch } = opts ?? {};
96
+ opts?.signal?.throwIfAborted();
97
+ // An open-ended fast-path range (OPEN_ENDED sentinel end) reads to the
98
+ // end of the object: pass offset alone so R2 streams the tail rather
99
+ // than requesting a ~9e15 length.
100
+ const r2Range = range
101
+ ? (isOpenEndedRange(range)
102
+ ? { offset: range.start }
103
+ : { offset: range.start, length: range.end - range.start + 1 })
104
+ : undefined;
105
+ const getOptions = r2Range || ifMatch
106
+ ? {
107
+ ...(r2Range ? { range: r2Range } : {}),
108
+ // Pin the read to the validated representation. On mismatch R2
109
+ // returns the object WITHOUT a body (checked below).
110
+ ...(ifMatch ? { onlyIf: { etagMatches: ifMatch } } : {}),
111
+ }
112
+ : undefined;
113
+ const obj = await bucket.get(key, getOptions);
114
+ if (!obj)
115
+ throw new ObjectNotFoundError(key);
116
+ // onlyIf precondition failed: R2 resolves with metadata but no body.
117
+ if (!("body" in obj) || !obj.body) {
118
+ throw new ObjectChangedError(key);
119
+ }
120
+ const body = obj;
121
+ const totalSize = obj.size;
122
+ // Content-Range must reflect what R2 ACTUALLY returned, not what was
123
+ // requested. `body.range` is R2's authoritative offset/length for this
124
+ // response; if R2 clamped the range (object changed between HEAD and
125
+ // GET), fabricating from the request would corrupt the client's bytes.
126
+ const returned = range ? body.range : undefined;
127
+ const start = returned?.offset ?? range?.start;
128
+ const length = returned?.length
129
+ ?? (range
130
+ ? (isOpenEndedRange(range) ? totalSize - range.start : range.end - range.start + 1)
131
+ : undefined);
132
+ const isRanged = start !== undefined && length !== undefined;
133
+ const contentLength = isRanged ? length : totalSize;
134
+ return {
135
+ // Guard the committed length: if R2 ever ends the body short of the
136
+ // computed contentLength, error the stream rather than under-run it.
137
+ body: guardStreamLength(body.body, contentLength),
138
+ contentLength,
139
+ totalSize,
140
+ range: isRanged ? { start, end: start + length - 1 } : undefined,
141
+ etag: obj.etag,
142
+ lastModified: obj.uploaded.toUTCString(),
143
+ digest: obj.checksums.sha256
144
+ ? arrayBufferToBase64(obj.checksums.sha256)
145
+ : undefined,
146
+ };
147
+ },
148
+ };
149
+ }
150
+ //# sourceMappingURL=r2.js.map
package/dist/r2.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2.js","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"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"}
package/dist/s3.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * S3-compatible ObjectStore adapter for partial-content.
3
+ *
4
+ * Implements the read-only {@link ObjectStore} interface from the kernel,
5
+ * wrapping `@aws-sdk/client-s3` HeadObject/GetObject commands. Covers:
6
+ * - AWS S3
7
+ * - Cloudflare R2 (S3-compatible mode)
8
+ * - Hetzner Object Storage
9
+ * - MinIO / Backblaze B2 / Wasabi
10
+ *
11
+ * For native R2 bindings (without the AWS SDK), use `partial-content/r2`.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { S3Client } from "@aws-sdk/client-s3";
16
+ * import { s3Store } from "partial-content/s3";
17
+ *
18
+ * const client = new S3Client({ region: "eu-central-1" });
19
+ * const store = s3Store({ client, bucket: "documents" });
20
+ * ```
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ import { S3Client } from "@aws-sdk/client-s3";
25
+ import { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError, type ObjectStore } from "./index.js";
26
+ export { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError };
27
+ export interface S3StoreOptions {
28
+ /** Pre-configured S3Client instance (BYOC, no config coupling). */
29
+ client: S3Client;
30
+ /** The S3 bucket name. */
31
+ bucket: string;
32
+ }
33
+ /**
34
+ * Create an {@link ObjectStore} backed by an S3-compatible bucket.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import { S3Client } from "@aws-sdk/client-s3";
39
+ * import { s3Store } from "partial-content/s3";
40
+ *
41
+ * const client = new S3Client({ region: "eu-central-1" });
42
+ * const store = s3Store({ client, bucket: "documents" });
43
+ *
44
+ * // Use with partial-content's evaluateConditionalRequest:
45
+ * const meta = await store.headObject("reports/q4.pdf");
46
+ * ```
47
+ */
48
+ export declare function s3Store(opts: S3StoreOptions): ObjectStore;
49
+ //# sourceMappingURL=s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,QAAQ,EAKT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EAOrB,KAAK,WAAW,EAKjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;AAiC1E,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,MAAM,EAAE,QAAQ,CAAC;IACjB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CA0IzD"}