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/s3.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
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, GetObjectCommand, HeadObjectCommand, NoSuchKey, NotFound, } from "@aws-sdk/client-s3";
|
|
25
|
+
import { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError, classifyStoreRead, nodeStreamToWeb, guardStreamLength, resolveServedRange, buildContentDisposition, isOpenEndedRange, } from "./index.js";
|
|
26
|
+
// Re-export for convenience
|
|
27
|
+
export { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError };
|
|
28
|
+
// ─── S3 Body -> Web ReadableStream ──────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Convert an S3 SDK response Body to a web ReadableStream.
|
|
31
|
+
*
|
|
32
|
+
* The SDK Body is a web ReadableStream in Bun/Deno, a Node Readable in
|
|
33
|
+
* Node.js, and may expose `transformToWebStream()` as a convenience.
|
|
34
|
+
* We try each path in order of preference.
|
|
35
|
+
*/
|
|
36
|
+
function toWebStream(body, signal, expectedBytes) {
|
|
37
|
+
// Every branch enforces the committed length: an S3-compatible backend that
|
|
38
|
+
// ends a body cleanly but short of ContentLength (some do in-flight body
|
|
39
|
+
// retries) must error the stream, not silently under-run the response. The
|
|
40
|
+
// web-stream branches are the ones Node aws-sdk v3 and Bun/Deno actually take,
|
|
41
|
+
// so guarding only the Node-Readable fallback would leave the guard dead.
|
|
42
|
+
if (body instanceof ReadableStream) {
|
|
43
|
+
return guardStreamLength(body, expectedBytes);
|
|
44
|
+
}
|
|
45
|
+
if (typeof body.transformToWebStream === "function") {
|
|
46
|
+
return guardStreamLength(body.transformToWebStream(), expectedBytes);
|
|
47
|
+
}
|
|
48
|
+
// Node Readable fallback: the shared utility auto-detects destroy() and
|
|
49
|
+
// applies the same length guard internally.
|
|
50
|
+
return nodeStreamToWeb(body, { signal, expectedBytes });
|
|
51
|
+
}
|
|
52
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Create an {@link ObjectStore} backed by an S3-compatible bucket.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { S3Client } from "@aws-sdk/client-s3";
|
|
59
|
+
* import { s3Store } from "partial-content/s3";
|
|
60
|
+
*
|
|
61
|
+
* const client = new S3Client({ region: "eu-central-1" });
|
|
62
|
+
* const store = s3Store({ client, bucket: "documents" });
|
|
63
|
+
*
|
|
64
|
+
* // Use with partial-content's evaluateConditionalRequest:
|
|
65
|
+
* const meta = await store.headObject("reports/q4.pdf");
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function s3Store(opts) {
|
|
69
|
+
const { client, bucket } = opts;
|
|
70
|
+
return {
|
|
71
|
+
supportsRange: true,
|
|
72
|
+
// 206 bounds/total are parsed from S3's actual Content-Range: the
|
|
73
|
+
// orchestrator may skip the validating HEAD for plain range requests.
|
|
74
|
+
authoritativeRange: true,
|
|
75
|
+
async headObject(key, opts) {
|
|
76
|
+
const response = await classifyStoreRead(key, () => client.send(
|
|
77
|
+
// ChecksumMode is required for S3 to return ChecksumSHA256 at all;
|
|
78
|
+
// without it the digest would silently never be emitted.
|
|
79
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key, ChecksumMode: "ENABLED" }), { abortSignal: opts?.signal }), s3Classifiers);
|
|
80
|
+
if (response.ContentLength == null) {
|
|
81
|
+
throw new Error(`HeadObject returned no ContentLength for ${key}`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
contentLength: response.ContentLength,
|
|
85
|
+
etag: response.ETag,
|
|
86
|
+
lastModified: response.LastModified?.toUTCString(),
|
|
87
|
+
digest: toReprDigest(response.ChecksumSHA256),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
async getObject(key, opts) {
|
|
91
|
+
const { range, signal, ifMatch } = opts ?? {};
|
|
92
|
+
// Open-ended fast-path ranges (`bytes=a-`) carry the OPEN_ENDED
|
|
93
|
+
// sentinel end; emit the bare open form so no 16-digit last-byte-pos
|
|
94
|
+
// reaches the wire (S3 clamps it, but strict proxies may reject it).
|
|
95
|
+
const rangeHeader = range
|
|
96
|
+
? (isOpenEndedRange(range) ? `bytes=${range.start}-` : `bytes=${range.start}-${range.end}`)
|
|
97
|
+
: undefined;
|
|
98
|
+
const response = await classifyStoreRead(key, () => client.send(new GetObjectCommand({
|
|
99
|
+
Bucket: bucket,
|
|
100
|
+
Key: key,
|
|
101
|
+
ChecksumMode: "ENABLED",
|
|
102
|
+
// Pin the read to the validated representation: S3 rejects with
|
|
103
|
+
// 412 PreconditionFailed if the object changed since HEAD.
|
|
104
|
+
...(ifMatch ? { IfMatch: ifMatch } : {}),
|
|
105
|
+
...(rangeHeader ? { Range: rangeHeader } : {}),
|
|
106
|
+
}), { abortSignal: signal }), s3Classifiers);
|
|
107
|
+
if (!response.Body) {
|
|
108
|
+
throw new Error(`S3 GetObject returned empty body for ${key}`);
|
|
109
|
+
}
|
|
110
|
+
const stream = toWebStream(response.Body, signal, response.ContentLength);
|
|
111
|
+
// A null ContentLength on a GET with a live body is a degenerate response:
|
|
112
|
+
// fabricating `0` would commit `Content-Length: 0` over real bytes (a wire
|
|
113
|
+
// framing error) and silently disable the short-read guard. headObject
|
|
114
|
+
// already fails loudly on this; mirror it -- cancel the body and throw.
|
|
115
|
+
if (response.ContentLength == null) {
|
|
116
|
+
stream.cancel().catch(() => { });
|
|
117
|
+
throw new Error(`S3 GetObject returned no ContentLength for ${key}`);
|
|
118
|
+
}
|
|
119
|
+
const contentLength = response.ContentLength;
|
|
120
|
+
// Extract served bounds + total size from Content-Range
|
|
121
|
+
// (e.g. "bytes 0-999/5000") via the shared resolver, or fall back to
|
|
122
|
+
// ContentLength for full responses. A Content-Range S3 emits but the
|
|
123
|
+
// resolver cannot parse means the byte accounting is untrustworthy:
|
|
124
|
+
// cancel the live body and fail loudly rather than guess.
|
|
125
|
+
let totalSize;
|
|
126
|
+
let served;
|
|
127
|
+
if (response.ContentRange) {
|
|
128
|
+
const resolved = resolveServedRange(response.ContentRange);
|
|
129
|
+
if (!resolved) {
|
|
130
|
+
stream.cancel().catch(() => { });
|
|
131
|
+
throw new Error(`S3 returned unparseable Content-Range for ${key}: ${response.ContentRange}`);
|
|
132
|
+
}
|
|
133
|
+
served = resolved.served;
|
|
134
|
+
totalSize = resolved.totalSize;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
totalSize = contentLength;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
body: stream,
|
|
141
|
+
contentLength,
|
|
142
|
+
totalSize,
|
|
143
|
+
range: served,
|
|
144
|
+
etag: response.ETag,
|
|
145
|
+
lastModified: response.LastModified?.toUTCString(),
|
|
146
|
+
// Repr-Digest MUST hash the full representation. AWS omits the checksum
|
|
147
|
+
// on ranged GETs, but a non-conforming S3-compatible backend could
|
|
148
|
+
// return a range-scoped one; never advertise it as a whole-object
|
|
149
|
+
// digest. HEAD (always whole-object) and full 200s still carry it.
|
|
150
|
+
digest: served ? undefined : toReprDigest(response.ChecksumSHA256),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
async createSignedUrl(key, signOpts) {
|
|
154
|
+
try {
|
|
155
|
+
// A signed URL is a 302 redirect target: the client fetches bytes
|
|
156
|
+
// DIRECTLY from S3, bypassing the serve route's security headers
|
|
157
|
+
// (nosniff, CSP, CORP). Force a download disposition AND an inert
|
|
158
|
+
// content type so a stored SVG/HTML polyglot cannot render inline off
|
|
159
|
+
// the header-less origin response. Both are S3 query-parameter
|
|
160
|
+
// overrides honored on the presigned GET; the stored object is
|
|
161
|
+
// untouched. `downloadFilename` only customizes the name.
|
|
162
|
+
const command = new GetObjectCommand({
|
|
163
|
+
Bucket: bucket,
|
|
164
|
+
Key: key,
|
|
165
|
+
ResponseContentType: "application/octet-stream",
|
|
166
|
+
ResponseContentDisposition: buildContentDisposition(signOpts.downloadFilename ?? "download", { type: "attachment" }),
|
|
167
|
+
});
|
|
168
|
+
// Lazy import: the presigner is an optional peer needed ONLY for
|
|
169
|
+
// signed URLs. A static import would crash `partial-content/s3` at
|
|
170
|
+
// module load for every consumer who installed just
|
|
171
|
+
// @aws-sdk/client-s3 (the documented baseline) and never presigns.
|
|
172
|
+
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
173
|
+
const url = await getSignedUrl(client, command, {
|
|
174
|
+
expiresIn: signOpts.expiresInSeconds,
|
|
175
|
+
});
|
|
176
|
+
return { ok: true, url };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: err instanceof Error ? err.message : String(err),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
188
|
+
/** A raw base64-encoded SHA-256 hash: exactly 43 base64 chars plus padding. */
|
|
189
|
+
const SHA256_BASE64_RE = /^[A-Za-z0-9+/]{43}=$/;
|
|
190
|
+
/**
|
|
191
|
+
* Validate an S3 checksum for use as an RFC 9530 representation digest.
|
|
192
|
+
*
|
|
193
|
+
* Multipart uploads produce composite checksums ("checksum-of-checksums")
|
|
194
|
+
* in the form `<base64>-<partCount>`, which do NOT hash the object bytes
|
|
195
|
+
* and are invalid inside `Repr-Digest: sha-256=:...:`. Only a plain
|
|
196
|
+
* base64 SHA-256 of the full object passes through.
|
|
197
|
+
*/
|
|
198
|
+
function toReprDigest(checksum) {
|
|
199
|
+
return checksum && SHA256_BASE64_RE.test(checksum) ? checksum : undefined;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if an S3 error indicates the object does not exist.
|
|
203
|
+
*
|
|
204
|
+
* Covers:
|
|
205
|
+
* - AWS SDK v3 `NoSuchKey` and `NotFound` error classes
|
|
206
|
+
* - Generic S3-compatible providers that set `$metadata.httpStatusCode: 404`
|
|
207
|
+
* - Providers that set `name: "NotFound"` without using the SDK error classes
|
|
208
|
+
*/
|
|
209
|
+
function isNotFoundError(err) {
|
|
210
|
+
if (err instanceof NoSuchKey || err instanceof NotFound)
|
|
211
|
+
return true;
|
|
212
|
+
if (err instanceof Error && err.name === "NotFound")
|
|
213
|
+
return true;
|
|
214
|
+
const meta = err.$metadata;
|
|
215
|
+
if (meta?.httpStatusCode === 404)
|
|
216
|
+
return true;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Check if an S3 error is a failed IfMatch precondition (object changed).
|
|
221
|
+
* The SDK surfaces this as name "PreconditionFailed" with HTTP 412.
|
|
222
|
+
*/
|
|
223
|
+
function isPreconditionFailedError(err) {
|
|
224
|
+
if (err instanceof Error && err.name === "PreconditionFailed")
|
|
225
|
+
return true;
|
|
226
|
+
const meta = err.$metadata;
|
|
227
|
+
return meta?.httpStatusCode === 412;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Check if an S3 error is a transient throttle/overload the client should
|
|
231
|
+
* retry (mapped to a 503, not a 502). The AWS SDK exhausts its own adaptive
|
|
232
|
+
* retries first, so reaching here means sustained pressure.
|
|
233
|
+
*
|
|
234
|
+
* Covers:
|
|
235
|
+
* - `$retryable.throttling` (the SDK's own throttle classification)
|
|
236
|
+
* - HTTP 503 (`SlowDown`) and 429 (`TooManyRequests`) on `$metadata`
|
|
237
|
+
* - the named throttle errors S3-compatible backends raise without setting
|
|
238
|
+
* an HTTP status code
|
|
239
|
+
*/
|
|
240
|
+
function isThrottledError(err) {
|
|
241
|
+
if (err.$retryable?.throttling === true)
|
|
242
|
+
return true;
|
|
243
|
+
const meta = err.$metadata;
|
|
244
|
+
if (meta?.httpStatusCode === 503 || meta?.httpStatusCode === 429)
|
|
245
|
+
return true;
|
|
246
|
+
if (err instanceof Error) {
|
|
247
|
+
const name = err.name;
|
|
248
|
+
return (name === "SlowDown" ||
|
|
249
|
+
name === "ThrottlingException" ||
|
|
250
|
+
name === "TooManyRequestsException" ||
|
|
251
|
+
name === "RequestThrottled" ||
|
|
252
|
+
name === "RequestThrottledException" ||
|
|
253
|
+
name === "ProvisionedThroughputExceededException");
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
/** The ordered error-classification set shared by headObject and getObject. */
|
|
258
|
+
const s3Classifiers = {
|
|
259
|
+
notFound: isNotFoundError,
|
|
260
|
+
changed: isPreconditionFailedError,
|
|
261
|
+
throttled: isThrottledError,
|
|
262
|
+
};
|
|
263
|
+
//# sourceMappingURL=s3.js.map
|
package/dist/s3.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,gBAAgB,GAMjB,MAAM,YAAY,CAAC;AAEpB,4BAA4B;AAC5B,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;AAE1E,+EAA+E;AAE/E;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,IAAa,EAAE,MAAoB,EAAE,aAAsB;IAC9E,4EAA4E;IAC5E,yEAAyE;IACzE,2EAA2E;IAC3E,+EAA+E;IAC/E,0EAA0E;IAC1E,IAAI,IAAI,YAAY,cAAc,EAAE,CAAC;QACnC,OAAO,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,OAAQ,IAAwD,CAAC,oBAAoB,KAAK,UAAU,EAAE,CAAC;QACzG,OAAO,iBAAiB,CACrB,IAAmE,CAAC,oBAAoB,EAAE,EAC3F,aAAa,CACd,CAAC;IACJ,CAAC;IACD,wEAAwE;IACxE,4CAA4C;IAC5C,OAAO,eAAe,CAAC,IAA0C,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;AAChG,CAAC;AAWD,+EAA+E;AAE/E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,OAAO,CAAC,IAAoB;IAC1C,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEhC,OAAO;QACL,aAAa,EAAE,IAAI;QACnB,kEAAkE;QAClE,sEAAsE;QACtE,kBAAkB,EAAE,IAAI;QAExB,KAAK,CAAC,UAAU,CAAC,GAAW,EAAE,IAA+B;YAC3D,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI;YAC7D,mEAAmE;YACnE,yDAAyD;YACzD,IAAI,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,EAC5E,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,CAC9B,EAAE,aAAa,CAAC,CAAC;YAElB,IAAI,QAAQ,CAAC,aAAa,IAAI,IAAI,EAAE,CAAC;gBACnC,MAAM,IAAI,KAAK,CAAC,4CAA4C,GAAG,EAAE,CAAC,CAAC;YACrE,CAAC;YAED,OAAO;gBACL,aAAa,EAAE,QAAQ,CAAC,aAAa;gBACrC,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,WAAW,EAAE;gBAClD,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,cAAc,CAAC;aAC9C,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,IAAsE;YACjG,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC;YAC9C,gEAAgE;YAChE,qEAAqE;YACrE,qEAAqE;YACrE,MAAM,WAAW,GAAG,KAAK;gBACvB,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;gBAC3F,CAAC,CAAC,SAAS,CAAC;YAEd,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAC7D,IAAI,gBAAgB,CAAC;gBACnB,MAAM,EAAE,MAAM;gBACd,GAAG,EAAE,GAAG;gBACR,YAAY,EAAE,SAAS;gBACvB,gEAAgE;gBAChE,2DAA2D;gBAC3D,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/C,CAAC,EACF,EAAE,WAAW,EAAE,MAAM,EAAE,CACxB,EAAE,aAAa,CAAC,CAAC;YAElB,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,EAAE,CAAC,CAAC;YACjE,CAAC;YAED,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;YAE1E,2EAA2E;YAC3E,2EAA2E;YAC3E,uEAAuE;YACvE,wEAAwE;YACxE,IAAI,QAAQ,CAAC,aAAa,IAAI,IAAI,EAAE,CAAC;gBACnC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAA+C,CAAC,CAAC,CAAC;gBAC7E,MAAM,IAAI,KAAK,CAAC,8CAA8C,GAAG,EAAE,CAAC,CAAC;YACvE,CAAC;YACD,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YAE7C,wDAAwD;YACxD,qEAAqE;YACrE,qEAAqE;YACrE,oEAAoE;YACpE,0DAA0D;YAC1D,IAAI,SAA6B,CAAC;YAClC,IAAI,MAAkD,CAAC;YACvD,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC1B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;gBAC3D,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAA+C,CAAC,CAAC,CAAC;oBAC7E,MAAM,IAAI,KAAK,CAAC,6CAA6C,GAAG,KAAK,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC;gBAChG,CAAC;gBACD,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;gBACzB,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,SAAS,GAAG,aAAa,CAAC;YAC5B,CAAC;YAED,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,aAAa;gBACb,SAAS;gBACT,KAAK,EAAE,MAAM;gBACb,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,WAAW,EAAE;gBAClD,wEAAwE;gBACxE,mEAAmE;gBACnE,kEAAkE;gBAClE,mEAAmE;gBACnE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,cAAc,CAAC;aACnE,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,eAAe,CAAC,GAAG,EAAE,QAAQ;YACjC,IAAI,CAAC;gBACH,kEAAkE;gBAClE,iEAAiE;gBACjE,kEAAkE;gBAClE,sEAAsE;gBACtE,+DAA+D;gBAC/D,+DAA+D;gBAC/D,0DAA0D;gBAC1D,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC;oBACnC,MAAM,EAAE,MAAM;oBACd,GAAG,EAAE,GAAG;oBACR,mBAAmB,EAAE,0BAA0B;oBAC/C,0BAA0B,EAAE,uBAAuB,CACjD,QAAQ,CAAC,gBAAgB,IAAI,UAAU,EACvC,EAAE,IAAI,EAAE,YAAY,EAAE,CACvB;iBACF,CAAC,CAAC;gBAEH,iEAAiE;gBACjE,mEAAmE;gBACnE,oDAAoD;gBACpD,mEAAmE;gBACnE,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;gBACvE,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE;oBAC9C,SAAS,EAAE,QAAQ,CAAC,gBAAgB;iBACrC,CAAC,CAAC;gBAEH,OAAO,EAAE,EAAE,EAAE,IAAa,EAAE,GAAG,EAAE,CAAC;YACpC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO;oBACL,EAAE,EAAE,KAAc;oBAClB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBACxD,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEhD;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,QAA4B;IAChD,OAAO,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5E,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,GAAY;IACnC,IAAI,GAAG,YAAY,SAAS,IAAI,GAAG,YAAY,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrE,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,IAAI,GAAI,GAAmD,CAAC,SAAS,CAAC;IAC5E,IAAI,IAAI,EAAE,cAAc,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,SAAS,yBAAyB,CAAC,GAAY;IAC7C,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,oBAAoB;QAAE,OAAO,IAAI,CAAC;IAC3E,MAAM,IAAI,GAAI,GAAmD,CAAC,SAAS,CAAC;IAC5E,OAAO,IAAI,EAAE,cAAc,KAAK,GAAG,CAAC;AACtC,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,gBAAgB,CAAC,GAAY;IACpC,IAAK,GAAiD,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACpG,MAAM,IAAI,GAAI,GAAmD,CAAC,SAAS,CAAC;IAC5E,IAAI,IAAI,EAAE,cAAc,KAAK,GAAG,IAAI,IAAI,EAAE,cAAc,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9E,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACtB,OAAO,CACL,IAAI,KAAK,UAAU;YACnB,IAAI,KAAK,qBAAqB;YAC9B,IAAI,KAAK,0BAA0B;YACnC,IAAI,KAAK,kBAAkB;YAC3B,IAAI,KAAK,2BAA2B;YACpC,IAAI,KAAK,wCAAwC,CAClD,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+EAA+E;AAC/E,MAAM,aAAa,GAA0B;IAC3C,QAAQ,EAAE,eAAe;IACzB,OAAO,EAAE,yBAAyB;IAClC,SAAS,EAAE,gBAAgB;CAC5B,CAAC"}
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web / Fetch API adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Composes a partial-content {@link ObjectStore} with the kernel's conditional
|
|
5
|
+
* request evaluation to produce a standards-compliant file-serving handler.
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic: works with Next.js App Router, Hono, SvelteKit, Remix,
|
|
8
|
+
* Cloudflare Workers, Bun.serve, Deno.serve, or any runtime that uses the
|
|
9
|
+
* standard Request/Response API.
|
|
10
|
+
*
|
|
11
|
+
* Handles: 200 (full), 206 (partial), 304 (not modified), 412 (precondition
|
|
12
|
+
* failed), 416 (range not satisfiable), and HEAD (headers only).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { serveObject } from "partial-content/web";
|
|
17
|
+
* import { s3Store } from "partial-content/s3";
|
|
18
|
+
*
|
|
19
|
+
* const store = s3Store({ client, bucket: "documents" });
|
|
20
|
+
*
|
|
21
|
+
* export const GET = serveObject(store, {
|
|
22
|
+
* key: (req) => req.url.split("/").pop()!,
|
|
23
|
+
* disposition: "inline",
|
|
24
|
+
* });
|
|
25
|
+
* export const HEAD = GET;
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
import { type ObjectStore } from "./index.js";
|
|
31
|
+
export type { CancelSignal, ObjectStore, ObjectMetadata, ObjectStream, ParsedRange, ETagSource, } from "./index.js";
|
|
32
|
+
/**
|
|
33
|
+
* The request surface the orchestrator actually consumes. A standard fetch
|
|
34
|
+
* `Request` satisfies it structurally; server adapters may pass a
|
|
35
|
+
* lightweight view instead, avoiding per-request construction of fetch
|
|
36
|
+
* primitives (an undici `Request` + `Headers` pair costs a measurable
|
|
37
|
+
* fraction of a small-file serve).
|
|
38
|
+
*/
|
|
39
|
+
export interface ServableRequest {
|
|
40
|
+
/** HTTP method. GET and HEAD are served; anything else gets a 405. */
|
|
41
|
+
method: string;
|
|
42
|
+
/** Case-insensitive header lookup: a `Headers` object or any equivalent view. */
|
|
43
|
+
headers: {
|
|
44
|
+
get(name: string): string | null;
|
|
45
|
+
};
|
|
46
|
+
/** Client-disconnect signal, forwarded to the storage backend. */
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}
|
|
49
|
+
/** Metadata resolved by the consumer before the handler runs. */
|
|
50
|
+
export interface ServeContext {
|
|
51
|
+
/** Storage key (path within the bucket). */
|
|
52
|
+
key: string;
|
|
53
|
+
/** Override MIME type. When omitted, defaults to "application/octet-stream". */
|
|
54
|
+
mime?: string;
|
|
55
|
+
/** Filename for Content-Disposition. When omitted, no disposition header is set. */
|
|
56
|
+
filename?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Per-request Cache-Control, overriding {@link ServeObjectOptions.cacheControl}.
|
|
59
|
+
* Lets one handler serve mixed cacheability (immutable content-addressed
|
|
60
|
+
* blobs next to private user uploads) without instantiating N handlers.
|
|
61
|
+
* Used verbatim: the `immutable` option is NOT appended to it.
|
|
62
|
+
*/
|
|
63
|
+
cacheControl?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ServeObjectOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Content-Disposition strategy.
|
|
68
|
+
* - `"inline"` -- render in the browser (PDF, images, video)
|
|
69
|
+
* - `"attachment"` -- force download
|
|
70
|
+
* - A function mapping MIME to disposition type (for per-type policy)
|
|
71
|
+
*
|
|
72
|
+
* @default "attachment"
|
|
73
|
+
*/
|
|
74
|
+
disposition?: "inline" | "attachment" | ((mime: string) => "inline" | "attachment");
|
|
75
|
+
/**
|
|
76
|
+
* Cache-Control value for 200/206 responses.
|
|
77
|
+
* @default "private, no-cache"
|
|
78
|
+
*/
|
|
79
|
+
cacheControl?: string;
|
|
80
|
+
/**
|
|
81
|
+
* When true, appends `immutable` to the Cache-Control directive.
|
|
82
|
+
* Use for content-addressed storage where the key contains a hash
|
|
83
|
+
* (e.g. `<sha256>.pdf`) and the resource will never change.
|
|
84
|
+
*
|
|
85
|
+
* @default false
|
|
86
|
+
*/
|
|
87
|
+
immutable?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Security headers applied to every response that carries a body (200, 206).
|
|
90
|
+
* Receives the MIME type so the policy can vary per format (e.g. relaxed
|
|
91
|
+
* CSP for PDF.js, strict sandbox for images).
|
|
92
|
+
*
|
|
93
|
+
* @default No extra security headers.
|
|
94
|
+
*/
|
|
95
|
+
securityHeaders?: (mime: string) => Record<string, string>;
|
|
96
|
+
/**
|
|
97
|
+
* Cross-Origin-Resource-Policy value for success responses.
|
|
98
|
+
*
|
|
99
|
+
* Controls which origins can embed this resource:
|
|
100
|
+
* - `"same-origin"` -- only your origin (most secure)
|
|
101
|
+
* - `"same-site"` -- same registrable domain
|
|
102
|
+
* - `"cross-origin"` -- any origin (for public CDN assets)
|
|
103
|
+
*
|
|
104
|
+
* Required for pages with `Cross-Origin-Embedder-Policy: require-corp`.
|
|
105
|
+
* When omitted, no CORP header is set (caller decides).
|
|
106
|
+
*/
|
|
107
|
+
crossOriginResourcePolicy?: "same-origin" | "same-site" | "cross-origin";
|
|
108
|
+
/**
|
|
109
|
+
* Timing-Allow-Origin value for success responses.
|
|
110
|
+
*
|
|
111
|
+
* Allows cross-origin pages to read Server-Timing and Resource Timing data
|
|
112
|
+
* via the PerformanceObserver API. Without this, browser security policy
|
|
113
|
+
* zeros out timing metrics for cross-origin resources.
|
|
114
|
+
*
|
|
115
|
+
* Set to `"*"` for public assets, or a specific origin for private APIs.
|
|
116
|
+
*/
|
|
117
|
+
timingAllowOrigin?: string;
|
|
118
|
+
/**
|
|
119
|
+
* When true, emits a `Server-Timing` header with storage and evaluation
|
|
120
|
+
* latency metrics. Also calls `onTiming` if provided.
|
|
121
|
+
*
|
|
122
|
+
* Caution: timing data may leak internal architecture details.
|
|
123
|
+
* Enable only when you control the deployment environment.
|
|
124
|
+
*
|
|
125
|
+
* @default false
|
|
126
|
+
*/
|
|
127
|
+
timing?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Timing callback for observability.
|
|
130
|
+
*
|
|
131
|
+
* Called with structured timing data for every request. Use to ship
|
|
132
|
+
* metrics to your RUM/APM backend.
|
|
133
|
+
*/
|
|
134
|
+
onTiming?: (metrics: {
|
|
135
|
+
storeMs: number;
|
|
136
|
+
evaluateMs: number;
|
|
137
|
+
totalMs: number;
|
|
138
|
+
}) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Fallback filename used by buildContentDisposition when no filename is
|
|
141
|
+
* provided and the disposition needs an ASCII fallback.
|
|
142
|
+
*
|
|
143
|
+
* @default "download"
|
|
144
|
+
*/
|
|
145
|
+
fallbackFilename?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Error callback for observability.
|
|
148
|
+
*
|
|
149
|
+
* Called when the storage backend throws during HEAD or GET operations.
|
|
150
|
+
* Use this to log errors with request IDs, correlation tokens, and
|
|
151
|
+
* structured metadata for production monitoring.
|
|
152
|
+
*
|
|
153
|
+
* The error is NOT exposed to the client (the response body is generic).
|
|
154
|
+
*
|
|
155
|
+
* @param error - The original error from the storage backend
|
|
156
|
+
* @param context - Additional context about the failed operation
|
|
157
|
+
* (`audit` = the consumer's own onServe hook threw; the response was
|
|
158
|
+
* still served, the hook failure is surfaced here instead of crashing).
|
|
159
|
+
*/
|
|
160
|
+
onError?: (error: unknown, context: {
|
|
161
|
+
key: string;
|
|
162
|
+
operation: "head" | "get" | "audit";
|
|
163
|
+
}) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Audit callback for compliance logging (SOC 2 CC7.2, ISO 27001 A.8.15).
|
|
166
|
+
*
|
|
167
|
+
* Called on every response that grants access (200, 206, 304, and 302
|
|
168
|
+
* signed-URL redirects) with structured metadata suitable for audit trail
|
|
169
|
+
* ingestion. Not called on errors (use `onError` for those).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* onServe: (event) => logger.info({ ...event }, "file.served")
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
onServe?: (event: ServeAuditEvent) => void;
|
|
177
|
+
/**
|
|
178
|
+
* Transfer-completion callback for true egress accounting and truncation
|
|
179
|
+
* detection.
|
|
180
|
+
*
|
|
181
|
+
* `onServe` fires when headers are committed and reports bytes *granted*
|
|
182
|
+
* (the response Content-Length). This fires once when the response body
|
|
183
|
+
* reaches its terminal state and reports bytes *actually transferred*
|
|
184
|
+
* through it, plus whether it drained fully or the client disconnected
|
|
185
|
+
* early. Use it for egress billing (`bytesTransferred`) or abandonment
|
|
186
|
+
* analytics (`completed === false`).
|
|
187
|
+
*
|
|
188
|
+
* Zero-cost when unset: the body is returned untouched, so byte bodies keep
|
|
189
|
+
* the runtime's static-body fast path. When set, the body is routed through
|
|
190
|
+
* a counting stream (byte bodies are wrapped too, so measurement is uniform
|
|
191
|
+
* across stores) -- a deliberate cost you opt into for the metering.
|
|
192
|
+
*
|
|
193
|
+
* Fires only for 200/206 GET responses (a body was served). Never fires for
|
|
194
|
+
* HEAD, 304, 302, 412, 416, or error responses. A throwing callback cannot
|
|
195
|
+
* corrupt the transfer: its error is routed to `onError` (operation
|
|
196
|
+
* `"audit"`).
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* onTransfer: (e) => {
|
|
201
|
+
* meter.recordEgress(e.key, e.bytesTransferred);
|
|
202
|
+
* if (!e.completed) log.info({ ...e }, "download.abandoned");
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
onTransfer?: (event: TransferEvent) => void;
|
|
207
|
+
/**
|
|
208
|
+
* Maximum number of distinct (coalesced) byte ranges to serve as
|
|
209
|
+
* multipart/byteranges before a multi-range request degrades to a full 200.
|
|
210
|
+
*
|
|
211
|
+
* A range-amplification defense: a client sending thousands of tiny or
|
|
212
|
+
* overlapping ranges would otherwise force a large multipart response.
|
|
213
|
+
* Overlapping/adjacent ranges are coalesced first, and if the result still
|
|
214
|
+
* exceeds this cap (or already covers the whole object) the full 200 is
|
|
215
|
+
* served instead. Matches the intent of nginx `max_ranges` and Go's
|
|
216
|
+
* sum-of-ranges check.
|
|
217
|
+
*
|
|
218
|
+
* @default 50
|
|
219
|
+
*/
|
|
220
|
+
maxRanges?: number;
|
|
221
|
+
/**
|
|
222
|
+
* When true, appends `; charset=utf-8` to textual Content-Type values
|
|
223
|
+
* (text/*, application/json, application/xml, etc.) if not already present.
|
|
224
|
+
*
|
|
225
|
+
* Prevents UTF-7 encoding-sniffing XSS in legacy browsers that probe for
|
|
226
|
+
* a charset declaration and fall back to auto-detection when none is found.
|
|
227
|
+
*
|
|
228
|
+
* @default true
|
|
229
|
+
*/
|
|
230
|
+
enforceCharset?: boolean;
|
|
231
|
+
}
|
|
232
|
+
/** Structured audit event for compliance logging. */
|
|
233
|
+
export interface ServeAuditEvent {
|
|
234
|
+
/** Storage key that was served. */
|
|
235
|
+
key: string;
|
|
236
|
+
/**
|
|
237
|
+
* Request method. Distinguishes a HEAD metadata probe (no bytes transferred)
|
|
238
|
+
* from a GET, which otherwise both surface as `status: 200, bytesServed: 0`
|
|
239
|
+
* for an empty object.
|
|
240
|
+
*/
|
|
241
|
+
method: "GET" | "HEAD";
|
|
242
|
+
/**
|
|
243
|
+
* HTTP status code. 302 = access granted via signed-URL redirect.
|
|
244
|
+
* 412/416 = access DENIED (failed precondition / unsatisfiable range):
|
|
245
|
+
* emitted so compliance trails capture denials, not only grants -- a 412
|
|
246
|
+
* is an optimistic-concurrency conflict signal auditors ask for.
|
|
247
|
+
*/
|
|
248
|
+
status: 200 | 206 | 302 | 304 | 412 | 416;
|
|
249
|
+
/** MIME type of the served content. */
|
|
250
|
+
mime: string;
|
|
251
|
+
/**
|
|
252
|
+
* Body bytes GRANTED, not confirmed transferred: the body's
|
|
253
|
+
* Content-Length on a 200/206 GET (the event fires when headers are
|
|
254
|
+
* committed, before the stream drains, so a client disconnect can
|
|
255
|
+
* receive fewer bytes). 0 for HEAD, 304, 302, 412, and 416. Treat as
|
|
256
|
+
* access volume, never as exfiltration volume.
|
|
257
|
+
*/
|
|
258
|
+
bytesServed: number;
|
|
259
|
+
/** Range start (inclusive), present only on 206. */
|
|
260
|
+
rangeStart?: number;
|
|
261
|
+
/** Range end (inclusive), present only on 206. */
|
|
262
|
+
rangeEnd?: number;
|
|
263
|
+
/** ETag of the served representation, if available. */
|
|
264
|
+
etag?: string;
|
|
265
|
+
}
|
|
266
|
+
/** Structured transfer-completion event ({@link ServeObjectOptions.onTransfer}). */
|
|
267
|
+
export interface TransferEvent {
|
|
268
|
+
/** Storage key that was served. */
|
|
269
|
+
key: string;
|
|
270
|
+
/** Always GET: only GET responses carry a body to transfer. */
|
|
271
|
+
method: "GET";
|
|
272
|
+
/** HTTP status of the served body. */
|
|
273
|
+
status: 200 | 206;
|
|
274
|
+
/**
|
|
275
|
+
* Bytes GRANTED: the response body's Content-Length (206 range span or 200
|
|
276
|
+
* full size). Compare against {@link bytesTransferred} to detect truncation.
|
|
277
|
+
*/
|
|
278
|
+
bytesExpected: number;
|
|
279
|
+
/**
|
|
280
|
+
* Bytes ACTUALLY read through the response body before it reached its
|
|
281
|
+
* terminal state. Equals {@link bytesExpected} on a fully-drained transfer;
|
|
282
|
+
* less when the client disconnected or cancelled early.
|
|
283
|
+
*/
|
|
284
|
+
bytesTransferred: number;
|
|
285
|
+
/**
|
|
286
|
+
* `true` if the body drained completely, `false` if it was cancelled /
|
|
287
|
+
* the client disconnected before the last byte. The honest signal for
|
|
288
|
+
* egress billing and abandonment analytics.
|
|
289
|
+
*/
|
|
290
|
+
completed: boolean;
|
|
291
|
+
/** Range start (inclusive), present only on 206. */
|
|
292
|
+
rangeStart?: number;
|
|
293
|
+
/** Range end (inclusive), present only on 206. */
|
|
294
|
+
rangeEnd?: number;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Serve an object from an {@link ObjectStore} with full RFC 7232/7233 support.
|
|
298
|
+
*
|
|
299
|
+
* Returns a function that takes a standard `Request` plus {@link ServeContext}
|
|
300
|
+
* and returns a `Response`. This is framework-agnostic: it works with Next.js
|
|
301
|
+
* App Router, Hono, SvelteKit, Remix, or any runtime that uses the Fetch API.
|
|
302
|
+
*
|
|
303
|
+
* The evaluation chain:
|
|
304
|
+
* 1. HEAD -> get metadata (ETag, Last-Modified, size)
|
|
305
|
+
* 2. Preconditions (If-Match / If-Unmodified-Since) -> 412
|
|
306
|
+
* 3. Freshness (If-None-Match / If-Modified-Since) -> 304
|
|
307
|
+
* 4. If-Range validation -> honor or ignore Range
|
|
308
|
+
* 5. Range parsing -> 206 or 416
|
|
309
|
+
* 6. Stream bytes -> 200 or 206
|
|
310
|
+
*
|
|
311
|
+
* TOCTOU guard: ETag/Last-Modified from the GET response are preferred over
|
|
312
|
+
* HEAD values. If the GET response omits Content-Range for a range request,
|
|
313
|
+
* the handler degrades to 200 (never emits a lying 206).
|
|
314
|
+
*/
|
|
315
|
+
export interface RawResponseParts {
|
|
316
|
+
status: number;
|
|
317
|
+
/** Reason phrase (e.g. "Partial Content", "Client Closed Request"). */
|
|
318
|
+
statusText: string;
|
|
319
|
+
headers: Record<string, string>;
|
|
320
|
+
/**
|
|
321
|
+
* `null` for bodyless statuses (HEAD, 304, 412, 416, 302, 405).
|
|
322
|
+
* `<ArrayBuffer>`-backed so `new Response(parts.body)` compiles under DOM lib.
|
|
323
|
+
*/
|
|
324
|
+
body: ReadableStream<Uint8Array<ArrayBuffer>> | Uint8Array<ArrayBuffer> | null;
|
|
325
|
+
}
|
|
326
|
+
export declare function serveObject(store: ObjectStore, opts?: ServeObjectOptions): (req: ServableRequest, ctx: ServeContext) => Promise<Response>;
|
|
327
|
+
/**
|
|
328
|
+
* The engine behind serveObject, exposed for server adapters: identical
|
|
329
|
+
* protocol behavior, but the result is RawResponseParts instead of a
|
|
330
|
+
* Response. Use this when your server writes status/headers/body natively
|
|
331
|
+
* (the bundled node adapter does) -- constructing a Response + Headers pair
|
|
332
|
+
* per request just to immediately deconstruct them is measurable overhead
|
|
333
|
+
* on hot paths.
|
|
334
|
+
*/
|
|
335
|
+
export declare function serveObjectRaw(store: ObjectStore, opts?: ServeObjectOptions): (req: ServableRequest, ctx: ServeContext) => Promise<RawResponseParts>;
|
|
336
|
+
//# sourceMappingURL=web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
|