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,472 @@
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 { type ParsedRange } from "./kernel.js";
11
+ /**
12
+ * Structural type matching the standard `AbortSignal` interface.
13
+ *
14
+ * Using a structural type instead of the global `AbortSignal` keeps the kernel
15
+ * free of DOM/lib dependencies. At call sites, `req.signal` (which is a real
16
+ * `AbortSignal`) satisfies this interface automatically.
17
+ */
18
+ export interface CancelSignal {
19
+ readonly aborted: boolean;
20
+ throwIfAborted(): void;
21
+ addEventListener(type: "abort", listener: () => void): void;
22
+ removeEventListener(type: "abort", listener: () => void): void;
23
+ }
24
+ /**
25
+ * Thrown when an object does not exist in a storage backend.
26
+ *
27
+ * All built-in adapters (S3, R2, GCS, Azure, fs) throw this error
28
+ * for missing objects. Framework adapters (e.g. `partial-content/web`)
29
+ * use the `status` property to distinguish "object not found" (404)
30
+ * from a transiently unavailable backend (503, {@link StoreUnavailableError})
31
+ * and other store failures (502).
32
+ */
33
+ export declare class ObjectNotFoundError extends Error {
34
+ /** HTTP status hint for downstream handlers. */
35
+ readonly status: 404;
36
+ /** The storage key that was not found. */
37
+ readonly key: string;
38
+ constructor(key: string, cause?: unknown);
39
+ }
40
+ /**
41
+ * Thrown when a pinned read ({@link GetObjectOptions.ifMatch}) finds the
42
+ * object changed since its validator was captured.
43
+ *
44
+ * Adapters map their backend's native rejection to this error (S3 412
45
+ * PreconditionFailed, R2 `onlyIf` body-less response, Azure 412
46
+ * ConditionNotMet, GCS etag mismatch). The web adapter treats it as a
47
+ * signal to re-validate the request against the object's new state.
48
+ */
49
+ export declare class ObjectChangedError extends Error {
50
+ /** HTTP status hint: the pinned read's precondition failed. */
51
+ readonly status: 412;
52
+ /** The storage key whose object changed. */
53
+ readonly key: string;
54
+ constructor(key: string, cause?: unknown);
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 declare class StoreUnavailableError extends Error {
71
+ /** HTTP status hint: the backend is transiently unavailable. */
72
+ readonly status: 503;
73
+ /** The storage key whose read failed. */
74
+ readonly key: string;
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
+ readonly retryAfterSeconds?: number;
86
+ constructor(key: string, opts?: {
87
+ retryAfterSeconds?: number;
88
+ cause?: unknown;
89
+ });
90
+ }
91
+ /**
92
+ * Normalize a `Retry-After` value to whole non-negative seconds, or `undefined`.
93
+ *
94
+ * The single sanctioned parser shared by every adapter and the web layer.
95
+ * Accepts the delay-seconds form (a number, or a numeric string) and -- when
96
+ * `allowHttpDate` is set -- the HTTP-date form (RFC 9110 Section 10.2.3) as a
97
+ * non-negative delta from now. Everything else yields `undefined`: NaN,
98
+ * Infinity, negatives, and any value whose floor exceeds `Number.MAX_SAFE_INTEGER`
99
+ * (so a huge finite hint can never serialize as `1e+21`, which violates the
100
+ * `delay-seconds = DIGIT+` grammar), plus non-numeric text. A hostile header or
101
+ * a buggy third-party classifier therefore can never emit a malformed header.
102
+ */
103
+ export declare function parseRetryAfterSeconds(raw: unknown, opts?: {
104
+ allowHttpDate?: boolean;
105
+ }): number | undefined;
106
+ /**
107
+ * Per-backend predicates that map a thrown SDK error to the contract's error
108
+ * types. An adapter supplies one set and reuses it for both `headObject` and
109
+ * `getObject`: a HEAD without a conditional never produces a `changed` (412),
110
+ * so sharing the set costs nothing and keeps the two paths provably symmetric.
111
+ */
112
+ export interface StoreErrorClassifiers {
113
+ /** The object does not exist (maps to {@link ObjectNotFoundError}, 404). */
114
+ notFound(err: unknown): boolean;
115
+ /**
116
+ * A pinned read's precondition failed (maps to {@link ObjectChangedError},
117
+ * 412). Omit for backends whose pin is an etag/metadata comparison rather
118
+ * than a native conditional error (e.g. GCS).
119
+ */
120
+ changed?(err: unknown): boolean;
121
+ /**
122
+ * Transient throttle/overload (maps to {@link StoreUnavailableError}, 503).
123
+ * Return `true` for a throttle with no back-off hint, or
124
+ * `{ retryAfterSeconds }` to surface a backend's `Retry-After` so the 503
125
+ * echoes it and shared caches back off for the advised interval. Return
126
+ * `false` when the error is not a throttle.
127
+ */
128
+ throttled(err: unknown): boolean | {
129
+ retryAfterSeconds: number;
130
+ };
131
+ }
132
+ /**
133
+ * Run a backend read and normalize its failures to the contract's error types.
134
+ *
135
+ * This is the single ordered classification pipeline every SDK-backed adapter
136
+ * shares: `notFound` -> `changed` -> `throttled` -> rethrow. Centralizing it
137
+ * means the "which errors are handled, in what order" contract is structural
138
+ * rather than copy-pasted into each adapter's head/get catch blocks, so a
139
+ * classifier can never be present on one path and silently missing on another.
140
+ * The predicates MUST be mutually exclusive on a given backend (a 404, 412, and
141
+ * 429/503 are distinct); the order only decides precedence if they are not.
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const meta = await classifyStoreRead(key, () => client.headObject(key), {
146
+ * notFound: isNotFound,
147
+ * changed: isPreconditionFailed,
148
+ * throttled: isThrottled,
149
+ * });
150
+ * ```
151
+ */
152
+ export declare function classifyStoreRead<T>(key: string, op: () => Promise<T>, classifiers: StoreErrorClassifiers): Promise<T>;
153
+ /** Options for `headObject`. */
154
+ export interface HeadObjectOptions {
155
+ /**
156
+ * Cancel signal. Pass `req.signal` to cancel the backend request when the
157
+ * client disconnects.
158
+ */
159
+ signal?: CancelSignal;
160
+ }
161
+ /**
162
+ * Options for `getObject`.
163
+ *
164
+ * `ifMatch` pins the read to the representation whose validator was captured
165
+ * by a prior `headObject` -- pass the RAW backend ETag from
166
+ * {@link ObjectMetadata.etag}, not a derived/formatted one, and only a
167
+ * STRONG validator (never `W/`-prefixed). Adapters map it to their backend's
168
+ * native conditional read (S3 `IfMatch`, R2 `onlyIf.etagMatches`, Azure
169
+ * `conditions.ifMatch`, GCS etag + generation pinning), making the HEAD->GET
170
+ * pair atomic: either the exact validated bytes are streamed, or the adapter
171
+ * throws {@link ObjectChangedError}.
172
+ */
173
+ export interface GetObjectOptions {
174
+ /** Validated byte range to stream. Omit for full content. */
175
+ range?: ParsedRange;
176
+ /**
177
+ * Cancel signal. Cancels the backend stream when the client disconnects,
178
+ * preventing orphaned TCP connections.
179
+ */
180
+ signal?: CancelSignal;
181
+ /** Raw backend ETag the object must still match (strong validators only). */
182
+ ifMatch?: string;
183
+ /**
184
+ * Opaque pin token from a prior `headObject` on the same key
185
+ * ({@link ObjectMetadata.pin}), passed back verbatim. Adapters that issue
186
+ * pins use it to stream the exact validated representation without
187
+ * re-fetching metadata; when the pinned version no longer exists they
188
+ * throw {@link ObjectChangedError}. Adapters that never issue pins
189
+ * ignore it.
190
+ */
191
+ pin?: string;
192
+ }
193
+ /**
194
+ * Convert a Node.js readable stream (async iterable) to a web ReadableStream
195
+ * with proper backpressure, Buffer coercion, signal propagation, and cleanup.
196
+ *
197
+ * This is the single implementation shared by all Node.js-based adapters
198
+ * (fs, GCS, Azure, S3 fallback). It handles:
199
+ *
200
+ * - **Pull-based backpressure**: the web ReadableStream's `pull()` method
201
+ * drives the async iterator, so chunks are only read when the consumer
202
+ * is ready.
203
+ * - **Buffer to Uint8Array coercion**: Node.js streams yield `Buffer`
204
+ * instances. We coerce to `Uint8Array` for cross-runtime compatibility
205
+ * (Buffer extends Uint8Array in Node, but the `instanceof` check
206
+ * ensures correctness in edge cases).
207
+ * - **AbortSignal propagation**: when the client disconnects (signal aborts),
208
+ * the underlying node stream is destroyed immediately to stop I/O.
209
+ * - **Cancel cleanup**: when the web ReadableStream is cancelled (e.g. by
210
+ * `Response.body.cancel()`), the node stream is destroyed.
211
+ *
212
+ * @param iterable - The async iterable (Node.js Readable stream). Typed
213
+ * `AsyncIterable<Uint8Array>` (Node `Buffer` extends `Uint8Array`, so Buffer
214
+ * streams satisfy it) to keep the `Buffer` name -- and its `@types/node`
215
+ * coupling -- out of the package's public type surface.
216
+ * @param opts.destroy - Destroys the underlying stream on cancel/abort. When
217
+ * omitted, a `destroy()` method on the iterable is auto-detected and used.
218
+ * @param opts.signal - Optional AbortSignal for client-disconnect detection
219
+ * @param opts.expectedBytes - Exact byte count the source promised. When set,
220
+ * a graceful end that delivered a different total (a file truncated or grown
221
+ * in place mid-read) errors the web stream instead of closing it short, so a
222
+ * torn body can never masquerade as a complete response under the committed
223
+ * `Content-Length`. A client abort tears down via a thrown iterator error,
224
+ * not a graceful end, so it is unaffected by this check.
225
+ */
226
+ export declare function nodeStreamToWeb(iterable: AsyncIterable<Uint8Array>, opts?: {
227
+ destroy?: () => void;
228
+ signal?: {
229
+ addEventListener(type: "abort", listener: () => void): void;
230
+ removeEventListener?(type: "abort", listener: () => void): void;
231
+ };
232
+ expectedBytes?: number;
233
+ }): ReadableStream<Uint8Array<ArrayBuffer>>;
234
+ /**
235
+ * Wrap a web `ReadableStream` so a graceful end that delivered a byte count
236
+ * other than `expectedBytes` errors the stream instead of closing it short.
237
+ *
238
+ * The Node-stream path gets this guard inside {@link nodeStreamToWeb}. This is
239
+ * the equivalent for adapters whose SDK hands back a web `ReadableStream`
240
+ * directly (S3 in Bun/Deno or via `transformToWebStream`, R2's native binding,
241
+ * a browser `Blob` stream) rather than a Node `Readable`. Without it, an
242
+ * S3-compatible backend that ends a body cleanly but short of the committed
243
+ * `Content-Length` (some do in-flight body retries) would under-run the
244
+ * response undetected -- the exact truncation `expectedBytes` exists to catch.
245
+ * Passing `undefined` disables the check and returns the stream unwrapped.
246
+ */
247
+ export declare function guardStreamLength(stream: ReadableStream<Uint8Array>, expectedBytes: number | undefined): ReadableStream<Uint8Array<ArrayBuffer>>;
248
+ /**
249
+ * Metadata from a HEAD request -- enough to evaluate conditionals before
250
+ * any body transfer.
251
+ */
252
+ export interface ObjectMetadata {
253
+ /** Full object size in bytes. */
254
+ contentLength: number;
255
+ /** Backend ETag, if any (already quoted or `W/`-prefixed -- pass through `generateETag`). */
256
+ etag?: string;
257
+ /** Last-Modified, in whatever format the backend emits (`Date.parse`-able). */
258
+ lastModified?: string;
259
+ /**
260
+ * RFC 9530 SHA-256 digest of the full representation (raw base64, no prefix).
261
+ *
262
+ * When provided, the adapter emits `Repr-Digest: sha-256=:<base64>:` on
263
+ * success responses for end-to-end integrity verification.
264
+ *
265
+ * S3: map from `x-amz-checksum-sha256`
266
+ * GCS: map from `x-goog-hash` (extract sha256 component)
267
+ */
268
+ digest?: string;
269
+ /**
270
+ * Opaque adapter token pinning a later `getObject` to this exact
271
+ * representation. The orchestrator round-trips it verbatim into
272
+ * {@link GetObjectOptions.pin} without interpreting it.
273
+ *
274
+ * Only needed by stores whose version identifier is not the ETag: GCS
275
+ * encodes its generation (plus the size/validators `getObject` would
276
+ * otherwise re-fetch), turning the HEAD->GET pair into a single backend
277
+ * read. ETag-pinning stores (S3, R2, Azure, http) omit it -- `ifMatch`
278
+ * already carries their pin.
279
+ */
280
+ pin?: string;
281
+ }
282
+ /**
283
+ * The byte range a backend actually served, inclusive on both ends
284
+ * (`bytes start-end/total` without the total: {@link ObjectStream.totalSize}
285
+ * carries it, or is `undefined` when the backend reported `bytes a-b/*`).
286
+ */
287
+ export interface ServedRange {
288
+ /** First byte position served (0-based, inclusive). */
289
+ start: number;
290
+ /** Last byte position served (inclusive). */
291
+ end: number;
292
+ }
293
+ /** Served bounds plus honest total parsed from a backend `Content-Range`. */
294
+ export interface ResolvedContentRange {
295
+ /** The byte range the backend actually served (inclusive bounds). */
296
+ served: ServedRange;
297
+ /**
298
+ * Full representation size, or `undefined` for the `bytes a-b/*`
299
+ * unknown-total sentinel (a streaming origin that does not know its length).
300
+ */
301
+ totalSize: number | undefined;
302
+ }
303
+ /**
304
+ * Parse a backend `Content-Range` response header into served bounds plus an
305
+ * honest total, applying the parser's `-1` unknown-total sentinel -> `undefined`
306
+ * mapping that every SDK adapter needs identically. Returns `null` ONLY when the
307
+ * header is present but unparseable -- the exact "byte accounting is
308
+ * untrustworthy, tear down and fail loudly" signal, which each adapter pairs
309
+ * with its own live-body cleanup (S3 `stream.cancel()`, Azure
310
+ * `destroyAzureDownload`, http `drain`) before throwing.
311
+ *
312
+ * Adapters that treat an ABSENT header as a full 200 (S3, Azure) check for the
313
+ * header before calling; callers that already know the response is partial
314
+ * (http's 206 branch) treat both an absent header and a `null` here as the same
315
+ * malformed-206 failure. Adapters that know their bounds natively (fs, memory,
316
+ * GCS, R2) construct {@link ServedRange} directly and never call this.
317
+ */
318
+ export declare function resolveServedRange(contentRange: string): ResolvedContentRange | null;
319
+ /**
320
+ * A body-transfer result from `getObject`.
321
+ *
322
+ * Every field is sourced from the **same GET response** -- including `etag`
323
+ * and `lastModified`. Consumers MUST prefer these over any prior HEAD values
324
+ * to avoid advertising stale validators against freshly-fetched bytes
325
+ * (the HEAD->GET TOCTOU window).
326
+ */
327
+ export interface ObjectStream {
328
+ /**
329
+ * The body: a `ReadableStream` for streamed transfers, or a plain
330
+ * `Uint8Array` when the adapter already has the exact bytes in memory
331
+ * (small files, in-memory stores). Byte bodies let consumers skip stream
332
+ * machinery entirely -- server adapters write them in a single syscall
333
+ * and `new Response(bytes)` takes the fetch runtime's static-body fast
334
+ * path. Return whichever form is natural; never buffer large transfers
335
+ * just to produce bytes.
336
+ *
337
+ * Typed `<ArrayBuffer>`-backed (not the wider `ArrayBufferLike`) so a
338
+ * consumer can pass it straight to `new Response(...)` under `lib: ["DOM"]`
339
+ * on TS >= 5.7, where `BodyInit` requires an `ArrayBuffer`-backed view.
340
+ */
341
+ body: ReadableStream<Uint8Array<ArrayBuffer>> | Uint8Array<ArrayBuffer>;
342
+ /** Bytes in THIS response (range length for 206, full size for 200). */
343
+ contentLength: number;
344
+ /**
345
+ * Full object size, independent of any range, or `undefined` when the
346
+ * backend served a partial response with an unknown total (`bytes a-b/*`,
347
+ * RFC 7233 Section 4.2) -- a streaming origin that does not know its full
348
+ * length. Object stores (S3, R2, GCS, Azure) always know their sizes and
349
+ * set a number; only proxied origins (the http adapter) legitimately leave
350
+ * it `undefined`. Valid ONLY alongside a `range`: a full response carries a
351
+ * concrete size in `contentLength`, so `undefined` with no `range` is an
352
+ * adapter bug (the orchestrator emits 200 and needs the length).
353
+ */
354
+ totalSize: number | undefined;
355
+ /**
356
+ * The byte range the backend ACTUALLY served (inclusive bounds), or
357
+ * `undefined` if it served full content.
358
+ *
359
+ * This is the source of truth for 206 vs 200: if a range was requested but
360
+ * `range` is `undefined`, the backend ignored it -- emit 200, never a
361
+ * lying 206. Adapters that receive a `Content-Range` string from their
362
+ * backend parse it with {@link parseContentRange} and fail loudly on
363
+ * garbage; adapters that know the bounds natively (fs, memory, GCS, R2)
364
+ * construct this directly -- malformed range strings cannot exist in the
365
+ * contract.
366
+ */
367
+ range?: ServedRange;
368
+ /** ETag as returned by the GET (authoritative for this body). */
369
+ etag?: string;
370
+ /** Last-Modified as returned by the GET (authoritative for this body). */
371
+ lastModified?: string;
372
+ /**
373
+ * RFC 9530 SHA-256 digest of the full representation (raw base64, no prefix).
374
+ *
375
+ * When provided by the GET response, the adapter can emit `Repr-Digest`
376
+ * even on Path C (no prior HEAD). This closes the digest gap for
377
+ * first-visit requests that skip the HEAD round-trip.
378
+ *
379
+ * S3: map from `x-amz-checksum-sha256` on GetObject response.
380
+ */
381
+ digest?: string;
382
+ }
383
+ /**
384
+ * Storage backend abstraction consumed by partial-content adapters.
385
+ *
386
+ * This contract is **read-only by design**. It covers the operations needed
387
+ * to evaluate conditional requests and stream object content. Write operations
388
+ * (PUT, PATCH, DELETE) remain the consumer's responsibility -- use
389
+ * `evaluateConditionalWrite()` to evaluate preconditions, then perform the
390
+ * write through your storage SDK directly.
391
+ *
392
+ * Implementations adapt a concrete store (S3/R2/Hetzner/GCS/fs) down to this
393
+ * surface. The kernel and framework adapters depend only on this interface,
394
+ * never on a storage SDK.
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * import type { ObjectStore } from "partial-content";
399
+ *
400
+ * const store: ObjectStore = {
401
+ * async headObject(key, opts) { ... },
402
+ * async getObject(key, opts) { ... },
403
+ * };
404
+ * ```
405
+ */
406
+ export interface ObjectStore {
407
+ /**
408
+ * Fetch metadata without a body. Used to evaluate conditional requests
409
+ * (304/412) and If-Range before deciding whether to transfer bytes.
410
+ *
411
+ * @param key - Object key within the bucket.
412
+ * @param opts - Cancellation ({@link HeadObjectOptions}).
413
+ */
414
+ headObject(key: string, opts?: HeadObjectOptions): Promise<ObjectMetadata>;
415
+ /**
416
+ * Stream an object, optionally a byte range.
417
+ *
418
+ * The adapter receives a `ParsedRange` in `opts.range` and formats it for
419
+ * the backend (e.g. `bytes=${start}-${end}`). `range.end` may exceed the
420
+ * object size (RFC 9110 lets servers clamp a last-byte-pos past EOF, and
421
+ * the fast range path uses an open end deliberately); adapters clamp it
422
+ * and report the bounds ACTUALLY served. `range.start` beyond EOF is the
423
+ * caller's error: local adapters throw a RangeError, remote backends
424
+ * reject natively. Returns the actual transfer result -- including the
425
+ * real `Content-Range` -- so the caller can build a truthful 200/206
426
+ * from a single round-trip.
427
+ *
428
+ * @param key - Object key within the bucket.
429
+ * @param opts - Range, cancellation, and pinning
430
+ * ({@link GetObjectOptions}). Stores that cannot pin reads may ignore
431
+ * `ifMatch`; the web adapter keeps a response-side guard (actual
432
+ * Content-Range + GET validators) for that case.
433
+ */
434
+ getObject(key: string, opts?: GetObjectOptions): Promise<ObjectStream>;
435
+ /**
436
+ * Capability flag. When `false`, the framework adapter degrades gracefully
437
+ * (e.g. signed-URL redirect) instead of attempting range streaming.
438
+ * Omit or set `true` for any S3-compatible backend.
439
+ *
440
+ * @default true
441
+ */
442
+ readonly supportsRange?: boolean;
443
+ /**
444
+ * Capability flag: this adapter's ranged responses carry bounds and total
445
+ * size taken from the BACKEND's actual response (`Content-Range`), not
446
+ * echoed from the request. When `true`, the framework adapter skips the
447
+ * validating HEAD for plain range requests (no conditionals, no
448
+ * If-Range) and serves the seek in a single round-trip -- validators,
449
+ * bounds, and digest all come from the GET response itself, which is
450
+ * also inherently TOCTOU-atomic. Leave unset for stores that need the
451
+ * orchestrator's pre-clamped ranges (fs, memory) or that fetch metadata
452
+ * themselves anyway (GCS).
453
+ *
454
+ * @default false
455
+ */
456
+ readonly authoritativeRange?: boolean;
457
+ /**
458
+ * Optional degradation path for backends that cannot stream ranges through
459
+ * the origin. Returns a short-lived URL the client is redirected to.
460
+ */
461
+ createSignedUrl?(key: string, opts: {
462
+ expiresInSeconds: number;
463
+ downloadFilename?: string;
464
+ }): Promise<{
465
+ ok: true;
466
+ url: string;
467
+ } | {
468
+ ok: false;
469
+ error: string;
470
+ }>;
471
+ }
472
+ //# sourceMappingURL=object-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-store.d.ts","sourceRoot":"","sources":["../src/object-store.ts"],"names":[],"mappings":"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"}