@trokky/trokky 3.4.0 → 3.5.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 (31) hide show
  1. package/dist/adapters/s3-media/index-key.d.ts +52 -0
  2. package/dist/adapters/s3-media/index-key.d.ts.map +1 -0
  3. package/dist/adapters/s3-media/index-key.js +91 -0
  4. package/dist/adapters/s3-media/index-key.js.map +1 -0
  5. package/dist/adapters/s3-media/index.d.ts +3 -0
  6. package/dist/adapters/s3-media/index.d.ts.map +1 -0
  7. package/dist/adapters/s3-media/index.js +12 -0
  8. package/dist/adapters/s3-media/index.js.map +1 -0
  9. package/dist/adapters/s3-media/s3-client.d.ts +112 -0
  10. package/dist/adapters/s3-media/s3-client.d.ts.map +1 -0
  11. package/dist/adapters/s3-media/s3-client.js +261 -0
  12. package/dist/adapters/s3-media/s3-client.js.map +1 -0
  13. package/dist/adapters/s3-media/s3-media-adapter.d.ts +118 -0
  14. package/dist/adapters/s3-media/s3-media-adapter.d.ts.map +1 -0
  15. package/dist/adapters/s3-media/s3-media-adapter.js +631 -0
  16. package/dist/adapters/s3-media/s3-media-adapter.js.map +1 -0
  17. package/dist/adapters/s3-media/types.d.ts +63 -0
  18. package/dist/adapters/s3-media/types.d.ts.map +1 -0
  19. package/dist/adapters/s3-media/types.js +2 -0
  20. package/dist/adapters/s3-media/types.js.map +1 -0
  21. package/dist/core/types/storage-adapters.d.ts +7 -0
  22. package/dist/core/types/storage-adapters.d.ts.map +1 -1
  23. package/dist/integrations/express/config.d.ts +6 -1
  24. package/dist/integrations/express/config.d.ts.map +1 -1
  25. package/dist/integrations/express/config.js.map +1 -1
  26. package/dist/integrations/express/integration.js +3 -3
  27. package/dist/integrations/express/integration.js.map +1 -1
  28. package/dist/routes/routes.d.ts.map +1 -1
  29. package/dist/routes/routes.js +2 -0
  30. package/dist/routes/routes.js.map +1 -1
  31. package/package.json +6 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The listing index, encoded into object keys.
3
+ *
4
+ * `ListObjectsV2` returns Key, Size, LastModified and nothing else — no user metadata — so the
5
+ * trick the R2 adapter uses (mirror the sortable fields into `customMetadata` and read them back
6
+ * from the listing) has no equivalent here. What a listing can always read is the key itself, so
7
+ * the fields go there:
8
+ *
9
+ * index/<id>/<createdAt as 13-digit epoch ms>/<base64url({ f, t, s })>
10
+ *
11
+ * **The id comes first, not the epoch.** Epoch-first would make a raw listing chronological, but
12
+ * `listMedia` has to sort in memory anyway — it supports name, size and type as well, and it
13
+ * needs every entry to compute a total — so that ordering buys nothing. Id-first buys something
14
+ * real: replacing or deleting one file's entries is a prefix listing of one key instead of a
15
+ * sweep of the whole index, which otherwise happens on every single upload.
16
+ *
17
+ * **This index is a cache, never the enumeration.** `meta/` is the only authority for whether a
18
+ * file exists; a missing index entry costs one record read and is rebuilt, it does not hide a
19
+ * file. See the adapter's `listMedia` and `cleanup`.
20
+ */
21
+ /**
22
+ * The longest key this encoding may produce. S3 caps a key at 1024 bytes, and a filename has no
23
+ * length limit worth trusting; the margin leaves room for the adapter's own prefix.
24
+ */
25
+ export declare const MAX_INDEX_KEY_BYTES = 900;
26
+ export interface IndexEntry {
27
+ id: string;
28
+ filename: string;
29
+ contentType: string;
30
+ size: number;
31
+ createdAt: Date;
32
+ }
33
+ /**
34
+ * The index key for an entry, under the adapter's prefix.
35
+ *
36
+ * A filename long enough to push the key past the cap is truncated **in the index only** — the
37
+ * record keeps the real one, so nothing is lost but the precision of `sort: 'name'` for that one
38
+ * entry. Refusing the upload instead would make a legal filename unstorable.
39
+ */
40
+ export declare function encodeIndexKey(prefix: string, entry: IndexEntry): string;
41
+ /**
42
+ * Read an entry back out of a key, or null when the key is not one of ours.
43
+ *
44
+ * Anything unparseable is treated as absent rather than thrown: a stray object under `index/`
45
+ * must not be able to break a listing, and `cleanup()` removes it.
46
+ */
47
+ export declare function decodeIndexKey(prefix: string, key: string): IndexEntry | null;
48
+ /** The prefix every index key lives under, for listing and for cleanup. */
49
+ export declare const indexPrefix: (prefix: string) => string;
50
+ /** The prefix one file's entries live under — the reason the id comes first. */
51
+ export declare const entryPrefix: (prefix: string, id: string) => string;
52
+ //# sourceMappingURL=index-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-key.d.ts","sourceRoot":"","sources":["../../../src/adapters/s3-media/index-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAEtC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,IAAI,CAAA;CAChB;AAgBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAcxE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAqB7E;AAED,2EAA2E;AAC3E,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAG,MAA8B,CAAA;AAE3E,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,IAAI,MAAM,KAAG,MAAoC,CAAA"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The listing index, encoded into object keys.
3
+ *
4
+ * `ListObjectsV2` returns Key, Size, LastModified and nothing else — no user metadata — so the
5
+ * trick the R2 adapter uses (mirror the sortable fields into `customMetadata` and read them back
6
+ * from the listing) has no equivalent here. What a listing can always read is the key itself, so
7
+ * the fields go there:
8
+ *
9
+ * index/<id>/<createdAt as 13-digit epoch ms>/<base64url({ f, t, s })>
10
+ *
11
+ * **The id comes first, not the epoch.** Epoch-first would make a raw listing chronological, but
12
+ * `listMedia` has to sort in memory anyway — it supports name, size and type as well, and it
13
+ * needs every entry to compute a total — so that ordering buys nothing. Id-first buys something
14
+ * real: replacing or deleting one file's entries is a prefix listing of one key instead of a
15
+ * sweep of the whole index, which otherwise happens on every single upload.
16
+ *
17
+ * **This index is a cache, never the enumeration.** `meta/` is the only authority for whether a
18
+ * file exists; a missing index entry costs one record read and is rebuilt, it does not hide a
19
+ * file. See the adapter's `listMedia` and `cleanup`.
20
+ */
21
+ const PREFIX = 'index/';
22
+ /** Enough digits for a millisecond epoch until the year 2286, and a stable sort until then. */
23
+ const EPOCH_DIGITS = 13;
24
+ /**
25
+ * The longest key this encoding may produce. S3 caps a key at 1024 bytes, and a filename has no
26
+ * length limit worth trusting; the margin leaves room for the adapter's own prefix.
27
+ */
28
+ export const MAX_INDEX_KEY_BYTES = 900;
29
+ const toBase64Url = (value) => {
30
+ const bytes = new TextEncoder().encode(value);
31
+ let binary = '';
32
+ for (const byte of bytes)
33
+ binary += String.fromCharCode(byte);
34
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
35
+ };
36
+ const fromBase64Url = (value) => {
37
+ const padded = value.replace(/-/g, '+').replace(/_/g, '/');
38
+ const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));
39
+ const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
40
+ return new TextDecoder().decode(bytes);
41
+ };
42
+ /**
43
+ * The index key for an entry, under the adapter's prefix.
44
+ *
45
+ * A filename long enough to push the key past the cap is truncated **in the index only** — the
46
+ * record keeps the real one, so nothing is lost but the precision of `sort: 'name'` for that one
47
+ * entry. Refusing the upload instead would make a legal filename unstorable.
48
+ */
49
+ export function encodeIndexKey(prefix, entry) {
50
+ const epoch = String(entry.createdAt.getTime()).padStart(EPOCH_DIGITS, '0');
51
+ const head = `${entryPrefix(prefix, entry.id)}${epoch}/`;
52
+ let filename = entry.filename;
53
+ let key = head + toBase64Url(JSON.stringify({ f: filename, t: entry.contentType, s: entry.size }));
54
+ while (key.length > MAX_INDEX_KEY_BYTES && filename.length > 1) {
55
+ // Halve rather than trim by one: a 4KB filename would otherwise take thousands of passes.
56
+ filename = filename.slice(0, Math.max(1, Math.floor(filename.length / 2)));
57
+ key = head + toBase64Url(JSON.stringify({ f: filename, t: entry.contentType, s: entry.size }));
58
+ }
59
+ return key;
60
+ }
61
+ /**
62
+ * Read an entry back out of a key, or null when the key is not one of ours.
63
+ *
64
+ * Anything unparseable is treated as absent rather than thrown: a stray object under `index/`
65
+ * must not be able to break a listing, and `cleanup()` removes it.
66
+ */
67
+ export function decodeIndexKey(prefix, key) {
68
+ const root = `${prefix}${PREFIX}`;
69
+ if (!key.startsWith(root))
70
+ return null;
71
+ const [id, epoch, payload] = key.slice(root.length).split('/');
72
+ if (!epoch || !id || !payload)
73
+ return null;
74
+ const createdAt = new Date(Number(epoch));
75
+ if (Number.isNaN(createdAt.getTime()))
76
+ return null;
77
+ try {
78
+ const { f, t, s } = JSON.parse(fromBase64Url(payload));
79
+ if (typeof f !== 'string' || typeof t !== 'string' || typeof s !== 'number')
80
+ return null;
81
+ return { id, filename: f, contentType: t, size: s, createdAt };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** The prefix every index key lives under, for listing and for cleanup. */
88
+ export const indexPrefix = (prefix) => `${prefix}${PREFIX}`;
89
+ /** The prefix one file's entries live under — the reason the id comes first. */
90
+ export const entryPrefix = (prefix, id) => `${prefix}${PREFIX}${id}/`;
91
+ //# sourceMappingURL=index-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-key.js","sourceRoot":"","sources":["../../../src/adapters/s3-media/index-key.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { S3MediaAdapter } from './s3-media-adapter.js';
2
+ export type { S3MediaAdapterConfig, S3MediaRecord } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/s3-media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,12 @@
1
+ export { S3MediaAdapter } from './s3-media-adapter.js';
2
+ // Auto-register in the adapter registry when imported, like the other adapters.
3
+ import { registerAdapter } from '../../core/index.js';
4
+ import { S3MediaAdapter } from './s3-media-adapter.js';
5
+ registerAdapter({
6
+ name: 's3-media',
7
+ type: 'media',
8
+ // Signing is WebCrypto and the transport is fetch, so this runs wherever Trokky does.
9
+ environments: ['node', 'edge', 'cloudflare'],
10
+ factory: (config) => new S3MediaAdapter(config)
11
+ });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/s3-media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAGtD,gFAAgF;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAGtD,eAAe,CAAC;IACd,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,OAAO;IACb,sFAAsF;IACtF,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC;IAC5C,OAAO,EAAE,CAAC,MAA4B,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC;CACtE,CAAC,CAAA"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The thinnest S3 client that serves a media adapter.
3
+ *
4
+ * Not an SDK. Six operations — put, get, head, delete, list, presign — over `aws4fetch`, which
5
+ * is ~6KB of WebCrypto and therefore works unchanged in Node, workerd and a browser. The AWS
6
+ * SDK would be two orders of magnitude larger for the same six calls.
7
+ *
8
+ * Two deliberate omissions:
9
+ *
10
+ * - **No `DeleteObjects`.** The multi-key delete requires a `Content-MD5` (or a checksum
11
+ * header whose support varies by backend), and WebCrypto has no MD5, so implementing it
12
+ * means shipping an MD5. Individual DELETEs with bounded concurrency cost more requests and
13
+ * no correctness, and this adapter only ever bulk-deletes a file's variants or a cleanup's
14
+ * orphans — both rare and small.
15
+ * - **No multipart upload.** The adapter caps a file at 100MB and S3's single-PUT limit is
16
+ * 5GB, so there is nothing to split. Raising the cap means implementing it.
17
+ *
18
+ * Listings ask for `encoding-type=url`, so keys come back percent-encoded and XML entities
19
+ * cannot smuggle a `&` or a `<` through a filename into the parser.
20
+ */
21
+ export interface S3ObjectSummary {
22
+ key: string;
23
+ size: number;
24
+ lastModified: Date;
25
+ }
26
+ export interface S3ListPage {
27
+ objects: S3ObjectSummary[];
28
+ nextToken?: string;
29
+ }
30
+ export interface S3ClientOptions {
31
+ endpoint: string;
32
+ bucket: string;
33
+ accessKeyId: string;
34
+ secretAccessKey: string;
35
+ region: string;
36
+ forcePathStyle: boolean;
37
+ }
38
+ export declare class S3RequestError extends Error {
39
+ readonly status: number;
40
+ readonly code?: string | undefined;
41
+ constructor(message: string, status: number, code?: string | undefined);
42
+ }
43
+ export declare class S3Client {
44
+ private readonly logger;
45
+ private readonly origin;
46
+ private readonly bucket;
47
+ private readonly forcePathStyle;
48
+ private readonly client;
49
+ constructor(options: S3ClientOptions);
50
+ /**
51
+ * The URL for a key. Every path segment is encoded individually: a key contains `/` as a real
52
+ * separator, and `encodeURIComponent` on the whole key would destroy it.
53
+ */
54
+ url(key: string): string;
55
+ private base;
56
+ /** One signed request, retried on throttling and 5xx, never on a 4xx that means what it says. */
57
+ private send;
58
+ put(key: string, body: ArrayBuffer | Uint8Array | string, options?: {
59
+ contentType?: string;
60
+ metadata?: Record<string, string>;
61
+ }): Promise<void>;
62
+ get(key: string): Promise<Response | null>;
63
+ /** Object metadata without the bytes, or null when there is no such key. */
64
+ head(key: string): Promise<{
65
+ size: number;
66
+ metadata: Record<string, string>;
67
+ } | null>;
68
+ /** Deleting a key that is not there is a success, exactly as it is on R2. */
69
+ delete(key: string): Promise<void>;
70
+ /**
71
+ * Delete many keys, a few at a time.
72
+ *
73
+ * Sixteen in flight keeps a cleanup of a large bucket from opening a thousand sockets while
74
+ * still finishing in a sensible time. See the header note on why this is not `DeleteObjects`.
75
+ */
76
+ deleteMany(keys: string[], concurrency?: number): Promise<void>;
77
+ /** One page of ListObjectsV2. */
78
+ list(prefix: string, options?: {
79
+ token?: string;
80
+ maxKeys?: number;
81
+ }): Promise<S3ListPage>;
82
+ /** Every page of a prefix. A partial listing would mis-count a total. */
83
+ listAll(prefix: string): Promise<S3ObjectSummary[]>;
84
+ /**
85
+ * A URL that carries its own signature, good for `expiresIn` seconds.
86
+ *
87
+ * The expiry goes through `URL.searchParams`, not string concatenation. Concatenating let a
88
+ * caller that forwards a request value smuggle extra query parameters — and because they land
89
+ * before signing, S3 honours them: `response-content-type=text/html` turns an uploaded image
90
+ * into stored XSS on the bucket origin.
91
+ */
92
+ presign(key: string, expiresIn: number): Promise<string>;
93
+ }
94
+ /**
95
+ * Pull the contents out of a ListObjectsV2 body.
96
+ *
97
+ * A regex rather than an XML parser because neither Node nor workerd ships one, the schema here
98
+ * is four fields deep, and `encoding-type=url` guarantees the only characters inside the tags we
99
+ * read are percent-encoded ASCII.
100
+ */
101
+ export declare function parseListResponse(xml: string): S3ListPage;
102
+ /**
103
+ * Percent-decode a value this adapter may not have written.
104
+ *
105
+ * `put()` encodes metadata values because a filename need not be header-safe, but
106
+ * `CloudflareR2Adapter` mirrors the filename into `customMetadata` raw — and sharing one bucket
107
+ * between the two is the point of this layout. A file named `100% off.png` written from a Worker
108
+ * would otherwise make `decodeURIComponent` throw and take `fileExists`, `listVariants` and
109
+ * `healthCheck` down with it.
110
+ */
111
+ export declare function decodeMetadata(value: string): string;
112
+ //# sourceMappingURL=s3-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-client.d.ts","sourceRoot":"","sources":["../../../src/adapters/s3-media/s3-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,IAAI,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;CACxB;AAoBD,qBAAa,cAAe,SAAQ,KAAK;aAGrB,MAAM,EAAE,MAAM;aACd,IAAI,CAAC,EAAE,MAAM;gBAF7B,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,YAAA;CAKhC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;gBAEtB,OAAO,EAAE,eAAe;IAYpC;;;OAGG;IACI,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAO/B,OAAO,CAAC,IAAI;IAMZ,iGAAiG;YACnF,IAAI;IA0CL,GAAG,CACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,EACvC,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAO,GACxE,OAAO,CAAC,IAAI,CAAC;IAaH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAQvD,4EAA4E;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IAelG,6EAA6E;IAChE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C;;;;;OAKG;IACU,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxE,iCAAiC;IACpB,IAAI,CACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GACjD,OAAO,CAAC,UAAU,CAAC;IAiBtB,yEAAyE;IAC5D,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAahE;;;;;;;OAOG;IACU,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAiBtE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAuBzD;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMpD"}
@@ -0,0 +1,261 @@
1
+ /**
2
+ * The thinnest S3 client that serves a media adapter.
3
+ *
4
+ * Not an SDK. Six operations — put, get, head, delete, list, presign — over `aws4fetch`, which
5
+ * is ~6KB of WebCrypto and therefore works unchanged in Node, workerd and a browser. The AWS
6
+ * SDK would be two orders of magnitude larger for the same six calls.
7
+ *
8
+ * Two deliberate omissions:
9
+ *
10
+ * - **No `DeleteObjects`.** The multi-key delete requires a `Content-MD5` (or a checksum
11
+ * header whose support varies by backend), and WebCrypto has no MD5, so implementing it
12
+ * means shipping an MD5. Individual DELETEs with bounded concurrency cost more requests and
13
+ * no correctness, and this adapter only ever bulk-deletes a file's variants or a cleanup's
14
+ * orphans — both rare and small.
15
+ * - **No multipart upload.** The adapter caps a file at 100MB and S3's single-PUT limit is
16
+ * 5GB, so there is nothing to split. Raising the cap means implementing it.
17
+ *
18
+ * Listings ask for `encoding-type=url`, so keys come back percent-encoded and XML entities
19
+ * cannot smuggle a `&` or a `<` through a filename into the parser.
20
+ */
21
+ import { AwsClient } from 'aws4fetch';
22
+ import { createLogger } from '../../core/index.js';
23
+ import { withRetry } from '../../core/utils/retry.js';
24
+ /** Status codes worth trying again: throttling and the backend having a bad moment. */
25
+ const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
26
+ /**
27
+ * Methods for which a 404 is an answer rather than a failure.
28
+ *
29
+ * On a GET, HEAD or DELETE it means "no such key", which every caller here handles. On a PUT or
30
+ * a LIST it means `NoSuchBucket` — and swallowing that made a misconfigured bucket look like an
31
+ * empty one: uploads returned a MediaFile having written nothing, and listings reported zero.
32
+ */
33
+ const ABSENCE_IS_NOT_FAILURE = new Set(['GET', 'HEAD', 'DELETE']);
34
+ /** A stalled connection is the classic object-store failure; without this the promise never settles. */
35
+ const REQUEST_TIMEOUT_MS = 30_000;
36
+ /** SigV4 caps a presigned URL at seven days. */
37
+ const MAX_PRESIGN_SECONDS = 604_800;
38
+ export class S3RequestError extends Error {
39
+ status;
40
+ code;
41
+ constructor(message, status, code) {
42
+ super(message);
43
+ this.status = status;
44
+ this.code = code;
45
+ this.name = 'S3RequestError';
46
+ }
47
+ }
48
+ export class S3Client {
49
+ logger = createLogger('adapter', 'S3Client');
50
+ origin;
51
+ bucket;
52
+ forcePathStyle;
53
+ client;
54
+ constructor(options) {
55
+ this.origin = options.endpoint.replace(/\/+$/, '');
56
+ this.bucket = options.bucket;
57
+ this.forcePathStyle = options.forcePathStyle;
58
+ this.client = new AwsClient({
59
+ accessKeyId: options.accessKeyId,
60
+ secretAccessKey: options.secretAccessKey,
61
+ region: options.region,
62
+ service: 's3'
63
+ });
64
+ }
65
+ /**
66
+ * The URL for a key. Every path segment is encoded individually: a key contains `/` as a real
67
+ * separator, and `encodeURIComponent` on the whole key would destroy it.
68
+ */
69
+ url(key) {
70
+ const path = key.split('/').map(encodeURIComponent).join('/');
71
+ return this.forcePathStyle
72
+ ? `${this.origin}/${this.bucket}/${path}`
73
+ : this.origin.replace('://', `://${this.bucket}.`) + `/${path}`;
74
+ }
75
+ base() {
76
+ return this.forcePathStyle
77
+ ? `${this.origin}/${this.bucket}`
78
+ : this.origin.replace('://', `://${this.bucket}.`);
79
+ }
80
+ /** One signed request, retried on throttling and 5xx, never on a 4xx that means what it says. */
81
+ async send(url, init, label) {
82
+ const method = (init.method ?? 'GET').toUpperCase();
83
+ const absenceIsAnswer = ABSENCE_IS_NOT_FAILURE.has(method);
84
+ return withRetry(async () => {
85
+ const response = await this.client.fetch(url, {
86
+ ...init,
87
+ // Without this a stalled connection never settles, so withRetry never sees an error
88
+ // and the caller's request hangs for as long as the process lives.
89
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
90
+ });
91
+ // A 404 means "no such key" on a read or a delete, which every caller here handles. On a
92
+ // PUT or a LIST it means NoSuchBucket, and treating that as success made a misconfigured
93
+ // bucket indistinguishable from an empty one.
94
+ if (response.ok || (response.status === 404 && absenceIsAnswer))
95
+ return response;
96
+ const body = await response.text().catch(() => '');
97
+ const code = /<Code>([^<]+)<\/Code>/.exec(body)?.[1];
98
+ // The body is NOT in the message. AWS puts AWSAccessKeyId, StringToSign and
99
+ // CanonicalRequest in a SignatureDoesNotMatch document, and this message travels
100
+ // to an HTTP 500 response body through the Express error handler.
101
+ this.logger.debug(`${label} failed`, { status: response.status, body: body.slice(0, 300) });
102
+ throw new S3RequestError(`${label} failed: ${response.status} ${response.statusText}${code ? ` (${code})` : ''}`, response.status, code);
103
+ }, {
104
+ attempts: 4,
105
+ isRetryable: error => !(error instanceof S3RequestError) || RETRYABLE_STATUS.has(error.status),
106
+ onRetry: ({ error, attempt }) => this.logger.warn(`${label}: retrying after failure`, { attempt, error: String(error) })
107
+ });
108
+ }
109
+ async put(key, body, options = {}) {
110
+ const headers = {};
111
+ if (options.contentType)
112
+ headers['content-type'] = options.contentType;
113
+ for (const [name, value] of Object.entries(options.metadata ?? {})) {
114
+ // User metadata must be a header-safe latin-1 string; a filename need not be either.
115
+ headers[`x-amz-meta-${name}`] = encodeURIComponent(value);
116
+ }
117
+ await drain(await this.send(this.url(key), { method: 'PUT', body: body, headers }, `PUT ${key}`));
118
+ }
119
+ async get(key) {
120
+ const response = await this.send(this.url(key), { method: 'GET' }, `GET ${key}`);
121
+ if (response.status !== 404)
122
+ return response;
123
+ // An undici body that is never read pins its connection until GC.
124
+ await drain(response);
125
+ return null;
126
+ }
127
+ /** Object metadata without the bytes, or null when there is no such key. */
128
+ async head(key) {
129
+ const response = await this.send(this.url(key), { method: 'HEAD' }, `HEAD ${key}`);
130
+ await drain(response);
131
+ if (response.status === 404)
132
+ return null;
133
+ const metadata = {};
134
+ response.headers.forEach((value, name) => {
135
+ if (name.toLowerCase().startsWith('x-amz-meta-')) {
136
+ metadata[name.slice('x-amz-meta-'.length).toLowerCase()] = decodeMetadata(value);
137
+ }
138
+ });
139
+ return { size: Number(response.headers.get('content-length') ?? 0), metadata };
140
+ }
141
+ /** Deleting a key that is not there is a success, exactly as it is on R2. */
142
+ async delete(key) {
143
+ await drain(await this.send(this.url(key), { method: 'DELETE' }, `DELETE ${key}`));
144
+ }
145
+ /**
146
+ * Delete many keys, a few at a time.
147
+ *
148
+ * Sixteen in flight keeps a cleanup of a large bucket from opening a thousand sockets while
149
+ * still finishing in a sensible time. See the header note on why this is not `DeleteObjects`.
150
+ */
151
+ async deleteMany(keys, concurrency = 16) {
152
+ for (let index = 0; index < keys.length; index += concurrency) {
153
+ await Promise.all(keys.slice(index, index + concurrency).map(key => this.delete(key)));
154
+ }
155
+ }
156
+ /** One page of ListObjectsV2. */
157
+ async list(prefix, options = {}) {
158
+ const query = new URLSearchParams({
159
+ 'list-type': '2',
160
+ prefix,
161
+ 'encoding-type': 'url',
162
+ 'max-keys': String(options.maxKeys ?? 1000)
163
+ });
164
+ if (options.token)
165
+ query.set('continuation-token', options.token);
166
+ const response = await this.send(`${this.base()}?${query.toString()}`, { method: 'GET' }, `LIST ${prefix}`);
167
+ return parseListResponse(await response.text());
168
+ }
169
+ /** Every page of a prefix. A partial listing would mis-count a total. */
170
+ async listAll(prefix) {
171
+ const objects = [];
172
+ let token;
173
+ do {
174
+ const page = await this.list(prefix, { token });
175
+ objects.push(...page.objects);
176
+ token = page.nextToken;
177
+ } while (token);
178
+ return objects;
179
+ }
180
+ /**
181
+ * A URL that carries its own signature, good for `expiresIn` seconds.
182
+ *
183
+ * The expiry goes through `URL.searchParams`, not string concatenation. Concatenating let a
184
+ * caller that forwards a request value smuggle extra query parameters — and because they land
185
+ * before signing, S3 honours them: `response-content-type=text/html` turns an uploaded image
186
+ * into stored XSS on the bucket origin.
187
+ */
188
+ async presign(key, expiresIn) {
189
+ const seconds = Math.floor(Number(expiresIn));
190
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > MAX_PRESIGN_SECONDS) {
191
+ throw new Error(`expiresIn must be a whole number of seconds between 1 and ${MAX_PRESIGN_SECONDS}`);
192
+ }
193
+ const url = new URL(this.url(key));
194
+ url.searchParams.set('X-Amz-Expires', String(seconds));
195
+ const signed = await this.client.sign(url.toString(), {
196
+ method: 'GET',
197
+ aws: { signQuery: true }
198
+ });
199
+ return signed.url;
200
+ }
201
+ }
202
+ /**
203
+ * Pull the contents out of a ListObjectsV2 body.
204
+ *
205
+ * A regex rather than an XML parser because neither Node nor workerd ships one, the schema here
206
+ * is four fields deep, and `encoding-type=url` guarantees the only characters inside the tags we
207
+ * read are percent-encoded ASCII.
208
+ */
209
+ export function parseListResponse(xml) {
210
+ const objects = [];
211
+ for (const match of xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)) {
212
+ const entry = match[1];
213
+ const key = /<Key>([\s\S]*?)<\/Key>/.exec(entry)?.[1];
214
+ if (key === undefined)
215
+ continue;
216
+ objects.push({
217
+ key: decodeURIComponent(key),
218
+ // \s* because a pretty-printing backend puts the digits on their own line.
219
+ size: Number(/<Size>\s*(\d+)\s*<\/Size>/.exec(entry)?.[1] ?? 0),
220
+ lastModified: new Date(/<LastModified>([\s\S]*?)<\/LastModified>/.exec(entry)?.[1] ?? 0)
221
+ });
222
+ }
223
+ const truncated = /<IsTruncated>\s*true\s*<\/IsTruncated>/i.test(xml);
224
+ const token = /<NextContinuationToken>([\s\S]*?)<\/NextContinuationToken>/.exec(xml)?.[1];
225
+ // `encoding-type=url` is specified to cover Delimiter, Prefix, Key and StartAfter — NOT the
226
+ // continuation token. AWS and R2 return base64, where decoding is a no-op, but a backend that
227
+ // returns a raw marker could carry a `%` that decodeURIComponent would throw on or corrupt.
228
+ return { objects, nextToken: truncated && token ? decodeMetadata(token) : undefined };
229
+ }
230
+ /**
231
+ * Read and discard a body we do not need.
232
+ *
233
+ * Under undici a response body that is neither read nor cancelled pins its connection until GC,
234
+ * so a cleanup issuing thousands of DELETEs can exhaust the pool.
235
+ */
236
+ async function drain(response) {
237
+ try {
238
+ await response.arrayBuffer();
239
+ }
240
+ catch {
241
+ // A body that cannot be read is already gone, which is the state we wanted.
242
+ }
243
+ }
244
+ /**
245
+ * Percent-decode a value this adapter may not have written.
246
+ *
247
+ * `put()` encodes metadata values because a filename need not be header-safe, but
248
+ * `CloudflareR2Adapter` mirrors the filename into `customMetadata` raw — and sharing one bucket
249
+ * between the two is the point of this layout. A file named `100% off.png` written from a Worker
250
+ * would otherwise make `decodeURIComponent` throw and take `fileExists`, `listVariants` and
251
+ * `healthCheck` down with it.
252
+ */
253
+ export function decodeMetadata(value) {
254
+ try {
255
+ return decodeURIComponent(value);
256
+ }
257
+ catch {
258
+ return value;
259
+ }
260
+ }
261
+ //# sourceMappingURL=s3-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-client.js","sourceRoot":"","sources":["../../../src/adapters/s3-media/s3-client.ts"],"names":[],"mappings":"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"}