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,253 @@
1
+ /**
2
+ * Content-Disposition header builder (RFC 6266 + RFC 8187).
3
+ *
4
+ * Builds Content-Disposition headers for HTTP responses that serve files.
5
+ * Handles both inline (in-app preview) and attachment (download) dispositions
6
+ * with proper RFC compliance for non-ASCII filenames.
7
+ *
8
+ * Security: Prevents CRLF header injection, path traversal, bidi override
9
+ * spoofing, and control character injection in filenames from untrusted
10
+ * sources (integration APIs, user uploads).
11
+ *
12
+ * Emits dual filename parameters for cross-browser compatibility:
13
+ * - `filename="ascii-safe.pdf"` for legacy browsers
14
+ * - `filename*=UTF-8''%C3%85rlig.pdf` for modern browsers (RFC 8187)
15
+ */
16
+ // ─── Constants ──────────────────────────────────────────────────────────────
17
+ /**
18
+ * RFC 2616 token characters. A filename that matches this pattern can be
19
+ * emitted unquoted in the `filename=` parameter (e.g. `filename=plans.pdf`).
20
+ * Anything outside this set requires quoted-string encoding.
21
+ *
22
+ * token = 1*<any CHAR except CTLs or separators>
23
+ * separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
24
+ * | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
25
+ */
26
+ const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/;
27
+ /**
28
+ * Characters that are valid US-ASCII text (printable range 0x20-0x7E).
29
+ * Filenames matching this can use the simple `filename=` parameter with
30
+ * quoted-string encoding. Non-ASCII filenames need `filename*=` (RFC 8187).
31
+ */
32
+ const ASCII_TEXT_REGEXP = /^[\x20-\x7e]*$/;
33
+ /**
34
+ * Characters that need escaping inside an RFC 2616 quoted-string.
35
+ * Per Section 2.2: quoted-pair = "\" CHAR, and the only characters
36
+ * that MUST be escaped are `\` and `"`.
37
+ */
38
+ const QUOTED_PAIR_REGEXP = /[\\"]/g;
39
+ /**
40
+ * Characters that are not valid in RFC 8187 attr-char, applied AFTER
41
+ * encodeURIComponent (so `%` is already handled). This catches characters
42
+ * that encodeURIComponent leaves unescaped but RFC 8187 requires encoded.
43
+ */
44
+ const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g; // eslint-disable-line no-control-regex
45
+ /**
46
+ * Non-US-ASCII characters. Replaced with `?` in the ASCII fallback
47
+ * for maximum compatibility with legacy HTTP clients.
48
+ */
49
+ const NON_ASCII_REGEXP = /[^\x20-\x7e]/g;
50
+ /**
51
+ * Filenames containing percent-encoded sequences (like `the%20plans.pdf`)
52
+ * should not use the simple `filename=` parameter because legacy clients
53
+ * may decode them. Force `filename*` for these.
54
+ */
55
+ const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
56
+ /**
57
+ * Matches lone surrogate code units (unpaired high or low surrogates).
58
+ * These occur when filenames are truncated mid-emoji with .slice().
59
+ * encodeURIComponent throws URIError on lone surrogates, so we must
60
+ * strip them before encoding.
61
+ */
62
+ const LONE_SURROGATE_REGEXP = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
63
+ // ─── Public API ─────────────────────────────────────────────────────────────
64
+ /**
65
+ * Build a Content-Disposition header value with RFC compliance and
66
+ * security sanitization.
67
+ *
68
+ * Accepts untrusted filenames from integration APIs, user uploads, and
69
+ * database records. Sanitizes for:
70
+ * - CRLF injection (strips \r, \n, and all control characters)
71
+ * - Path traversal (strips ../ and ..\\ sequences)
72
+ * - Basename extraction (strips directory paths)
73
+ * - Bidi override stripping (prevents RLO filename spoofing)
74
+ * - Double-quote and backslash escaping (RFC 2616 quoted-pair)
75
+ *
76
+ * Emits dual filename parameters for cross-browser compatibility:
77
+ * - `filename="ascii-safe.pdf"` for legacy browsers (IE, old Safari)
78
+ * - `filename*=UTF-8''%C3%85rlig.pdf` for modern browsers (RFC 8187)
79
+ *
80
+ * Token optimization: Simple ASCII filenames like `plans.pdf` are emitted
81
+ * unquoted (`filename=plans.pdf`).
82
+ *
83
+ * @param filename - Raw filename from untrusted source (upload, API, DB)
84
+ * @param options - Disposition type and fallback configuration
85
+ * @returns Complete Content-Disposition header value string
86
+ *
87
+ * @example
88
+ * // Simple ASCII attachment
89
+ * buildContentDisposition("report.pdf")
90
+ * // => 'attachment; filename=report.pdf'
91
+ *
92
+ * @example
93
+ * // Non-ASCII filename (Danish)
94
+ * buildContentDisposition("Årlig_Rapport.pdf")
95
+ * // => 'attachment; filename="?rlig_Rapport.pdf"; filename*=UTF-8\'\'%C3%85rlig_Rapport.pdf'
96
+ *
97
+ * @example
98
+ * // Inline disposition for preview
99
+ * buildContentDisposition("slides.pdf", { type: "inline" })
100
+ * // => 'inline; filename=slides.pdf'
101
+ *
102
+ * @example
103
+ * // Null input with custom fallback
104
+ * buildContentDisposition(null, { fallback: "export.csv" })
105
+ * // => 'attachment; filename=export.csv'
106
+ */
107
+ export function buildContentDisposition(filename, options) {
108
+ // Never trust the disposition type: a JS caller (or a mistyped
109
+ // `disposition` extractor returning a computed string) could otherwise
110
+ // smuggle header parameters through it. Anything but "inline" is
111
+ // "attachment" -- the safe default.
112
+ const type = options?.type === "inline" ? "inline" : "attachment";
113
+ // Sanitize BOTH the untrusted filename and the fallback through the same
114
+ // cleaner, then fall back to a constant. A filename that reduces to
115
+ // nothing (only controls/bidi/path components) must NOT emit a raw
116
+ // fallback into filename* -- that path leaked un-neutralized bidi
117
+ // overrides, defeating the anti-spoofing guarantee.
118
+ const sanitized = sanitizeFilename(filename ?? "")
119
+ || sanitizeFilename(options?.fallback ?? "")
120
+ || "document";
121
+ return `${type}${buildFilenameParams(sanitized)}`;
122
+ }
123
+ // ─── Internal Helpers ───────────────────────────────────────────────────────
124
+ /**
125
+ * Sanitize an untrusted filename for safe use in HTTP headers.
126
+ *
127
+ * Applied BEFORE any RFC encoding. This is the security layer that
128
+ * prevents header injection and path traversal attacks.
129
+ */
130
+ function sanitizeFilename(raw) {
131
+ // 1. Strip control characters and Unicode formatting controls.
132
+ // C0 controls (\x00-\x1F), DEL (\x7F): prevents CRLF header injection.
133
+ // C1 controls (\x80-\x9F): invisible formatting, no valid use in filenames.
134
+ // Bidi overrides (U+202A-202E, U+2066-2069): prevents RLO filename
135
+ // spoofing where "report\u202Efdp.exe" renders as "reportexe.pdf".
136
+ // Zero-width chars (U+200B-200F): invisible joiners/marks, strips
137
+ // directional marks that could confuse filename display.
138
+ // NBSP (U+00A0): often mistaken for regular space, normalize away.
139
+ const noControls = raw.replace(/[\r\n\x00-\x1F\x7F\x80-\x9F\u00A0\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, "");
140
+ // 2. Strip path traversal sequences (../ and ..\).
141
+ // Some integration APIs return full paths or relative paths.
142
+ // Even though HTTP clients shouldn't interpret these, defense-in-depth.
143
+ const noTraversal = noControls.replace(/\.\.[/\\]/g, "");
144
+ // 3. Extract basename (strip directory components).
145
+ // Integration APIs sometimes return full paths like "/uploads/2024/report.pdf".
146
+ // Uses lastIndexOf instead of split().pop() to avoid array allocation.
147
+ const lastSep = Math.max(noTraversal.lastIndexOf("/"), noTraversal.lastIndexOf("\\"));
148
+ const basename = lastSep >= 0 ? noTraversal.slice(lastSep + 1) : noTraversal;
149
+ // May be empty: the caller composes the fallback chain so both the
150
+ // filename and the fallback pass through this same sanitizer.
151
+ return basename;
152
+ }
153
+ /**
154
+ * Build the filename parameter string for Content-Disposition.
155
+ *
156
+ * Encoding strategy:
157
+ * 1. Pure ASCII token (no separators): emit unquoted `filename=report.pdf`
158
+ * 2. ASCII with special chars: emit quoted `filename="my report.pdf"`
159
+ * 3. Non-ASCII: emit both `filename="?rlig.pdf"` and `filename*=UTF-8''...`
160
+ *
161
+ * RFC 2616 Section 2.2 quoted-string escaping:
162
+ * - `\` -> `\\` (backslash must be escaped)
163
+ * - `"` -> `\"` (double-quote must be escaped)
164
+ */
165
+ function buildFilenameParams(filename) {
166
+ // Case 1: Pure ASCII that fits in a token -- emit unquoted.
167
+ // Also reject filenames with hex-encoded sequences (%20) which could
168
+ // be decoded by legacy clients.
169
+ if (ASCII_TEXT_REGEXP.test(filename) && !HEX_ESCAPE_REGEXP.test(filename)) {
170
+ if (TOKEN_REGEXP.test(filename)) {
171
+ return `; filename=${filename}`;
172
+ }
173
+ // ASCII but contains separators/spaces -- use quoted-string
174
+ return `; filename=${quoteString(filename)}`;
175
+ }
176
+ // Case 2: Contains non-ASCII characters or hex escapes.
177
+ // Emit both parameters for cross-browser compatibility:
178
+ // - filename="ascii-fallback" for legacy clients
179
+ // - filename*=UTF-8''percent-encoded for modern clients (RFC 8187)
180
+ const asciiFallback = toAsciiFallback(filename);
181
+ const encoded = encodeRfc8187(filename);
182
+ // When filename has hex escapes but is pure ASCII, we still need
183
+ // filename* because the hex escape could be decoded by legacy clients.
184
+ // Percent-encode % in the fallback to prevent unintended decoding.
185
+ const safeFallback = asciiFallback.replace(/%/g, "%25");
186
+ if (TOKEN_REGEXP.test(safeFallback)) {
187
+ return `; filename=${safeFallback}; filename*=${encoded}`;
188
+ }
189
+ return `; filename=${quoteString(safeFallback)}; filename*=${encoded}`;
190
+ }
191
+ /**
192
+ * Quote a string per RFC 2616 Section 2.2.
193
+ *
194
+ * quoted-string = ( <"> *(qdtext | quoted-pair) <"> )
195
+ * quoted-pair = "\" CHAR
196
+ *
197
+ * Both `\` and `"` MUST be escaped with a preceding backslash.
198
+ *
199
+ * In practice, `\` is stripped by sanitizeFilename (treated as path separator),
200
+ * so only `"` escaping is exercised through the normal code path. The `\`
201
+ * escaping is defense-in-depth for correctness if sanitization ever changes.
202
+ */
203
+ function quoteString(str) {
204
+ return `"${str.replace(QUOTED_PAIR_REGEXP, "\\$&")}"`;
205
+ }
206
+ /**
207
+ * Generate a US-ASCII fallback for non-ASCII filenames.
208
+ *
209
+ * Replaces non-ASCII characters with `?`. This is more honest than `_` --
210
+ * it signals to the user that characters were lost in translation, rather
211
+ * than suggesting underscores were in the original.
212
+ */
213
+ function toAsciiFallback(filename) {
214
+ return filename.replace(NON_ASCII_REGEXP, "?");
215
+ }
216
+ /**
217
+ * Encode a string per RFC 8187 (formerly RFC 5987).
218
+ *
219
+ * Format: charset'language'value-chars
220
+ * We always use UTF-8 and leave language empty.
221
+ *
222
+ * The encoding differs from encodeURIComponent in that RFC 8187
223
+ * defines its own set of "attr-char" that don't need encoding.
224
+ * We first apply encodeURIComponent (which handles all non-ASCII),
225
+ * then encode the characters that encodeURIComponent leaves unescaped
226
+ * but RFC 8187 requires encoded (including single quotes, which are
227
+ * the charset/language delimiter in the RFC 8187 format).
228
+ */
229
+ function encodeRfc8187(str) {
230
+ // encodeURIComponent itself throws URIError on lone surrogates
231
+ // (ECMA-262), so the exception IS the surrogate detector: clean strings
232
+ // (the overwhelming majority, including valid emoji pairs) pay no
233
+ // pre-scan at all. Lone surrogates only occur when upstream code
234
+ // truncates filenames mid-emoji with .slice() on UTF-16 code units;
235
+ // that pathological path eats the exception cost and retries stripped.
236
+ // Benchmarked against a surrogate-range pre-scan regex: this is ~6x
237
+ // faster for emoji filenames (a pre-scan cannot tell valid pairs from
238
+ // lone surrogates, so it triggered the strip for every emoji).
239
+ let encoded;
240
+ try {
241
+ encoded = encodeURIComponent(str);
242
+ }
243
+ catch {
244
+ encoded = encodeURIComponent(str.replace(LONE_SURROGATE_REGEXP, ""));
245
+ }
246
+ encoded = encoded.replace(ENCODE_URL_ATTR_CHAR_REGEXP, percentEncode);
247
+ return `UTF-8''${encoded}`;
248
+ }
249
+ /** Percent-encode a single character. */
250
+ function percentEncode(char) {
251
+ return "%" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
252
+ }
253
+ //# sourceMappingURL=content-disposition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-disposition.js","sourceRoot":"","sources":["../src/content-disposition.ts"],"names":[],"mappings":"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"}
package/dist/fs.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Local filesystem ObjectStore adapter for partial-content.
3
+ *
4
+ * Implements the read-only {@link ObjectStore} interface using Node.js
5
+ * `fs.stat()` and `fs.createReadStream()`. Suitable for:
6
+ * - Development servers
7
+ * - Small/medium deployments
8
+ * - Hybrid architectures (local cache + cloud primary)
9
+ * - Testing and CI pipelines
10
+ *
11
+ * Security: all keys are resolved relative to a fixed root directory.
12
+ * Path traversal attempts (`..`), absolute paths, and null bytes are
13
+ * rejected with ObjectNotFoundError. Symbolic links inside the root ARE
14
+ * followed (matching nginx/caddy defaults); do not place untrusted
15
+ * symlinks under the root if it must be a strict sandbox.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { fsStore } from "partial-content/fs";
20
+ * import { serveObject } from "partial-content/web";
21
+ *
22
+ * const store = fsStore({ root: "/var/data/uploads" });
23
+ *
24
+ * // Use with the web adapter:
25
+ * const handler = serveObject(store, { disposition: "inline" });
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ import { ObjectNotFoundError, type ObjectStore } from "./index.js";
31
+ export { ObjectNotFoundError };
32
+ export interface FsStoreOptions {
33
+ /**
34
+ * Root directory. All keys are resolved relative to this path.
35
+ * Must be an absolute path.
36
+ */
37
+ root: string;
38
+ /**
39
+ * Opt-in hot-object cache (metadata + small bodies), following the
40
+ * nginx `open_file_cache` model: entries revalidate on a TTL rather
41
+ * than a change watcher, so a served representation can lag a
42
+ * filesystem overwrite by up to `ttlMs`. Metadata and bytes are always
43
+ * captured together from one read, so responses are internally
44
+ * coherent (headers always describe the bytes actually sent).
45
+ *
46
+ * Off by default: correctness-first. Enable for hot small files
47
+ * (thumbnails, documents, content-addressed assets -- use a long TTL
48
+ * for immutable keys). Bodies at or below the single-read limit
49
+ * (128 KiB) are cached; larger objects always stream fresh from disk.
50
+ *
51
+ * Memory bound: `maxEntries` caps the entry count and `maxBytes` caps
52
+ * the total cached BODY bytes, so the worst case is
53
+ * `min(maxEntries * 128 KiB, maxBytes)` plus per-entry metadata.
54
+ */
55
+ cache?: {
56
+ /** How long an entry may serve before revalidating against disk. */
57
+ ttlMs: number;
58
+ /**
59
+ * Entry cap, evicted least-recently-used.
60
+ * @default 1024
61
+ */
62
+ maxEntries?: number;
63
+ /**
64
+ * Total byte budget for cached bodies, evicted least-recently-used.
65
+ * Metadata-only entries cost nothing against it. A single body larger
66
+ * than the budget is served normally but cached metadata-only, so an
67
+ * oversized object can never flush the whole cache to still not fit.
68
+ * `0` keeps the cache metadata-only (stat elision without body memory).
69
+ * @default 67108864 (64 MiB)
70
+ */
71
+ maxBytes?: number;
72
+ };
73
+ }
74
+ /**
75
+ * Create an {@link ObjectStore} backed by the local filesystem.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * import { fsStore } from "partial-content/fs";
80
+ *
81
+ * const store = fsStore({ root: "/var/data/uploads" });
82
+ * const meta = await store.headObject("reports/q4.pdf");
83
+ * ```
84
+ */
85
+ export declare function fsStore(opts: FsStoreOptions): ObjectStore;
86
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EACL,mBAAmB,EAEnB,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAI/B,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE;QACN,oEAAoE;QACpE,KAAK,EAAE,MAAM,CAAC;QACd;;;WAGG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB;;;;;;;WAOG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CA6TzD"}