@trokky/trokky 3.4.1 → 3.5.1

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 (59) hide show
  1. package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts.map +1 -1
  2. package/dist/adapters/filesystem-data/filesystem-data-adapter.js +3 -1
  3. package/dist/adapters/filesystem-data/filesystem-data-adapter.js.map +1 -1
  4. package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts.map +1 -1
  5. package/dist/adapters/filesystem-media/filesystem-media-adapter.js +3 -1
  6. package/dist/adapters/filesystem-media/filesystem-media-adapter.js.map +1 -1
  7. package/dist/adapters/s3-media/index-key.d.ts +52 -0
  8. package/dist/adapters/s3-media/index-key.d.ts.map +1 -0
  9. package/dist/adapters/s3-media/index-key.js +91 -0
  10. package/dist/adapters/s3-media/index-key.js.map +1 -0
  11. package/dist/adapters/s3-media/index.d.ts +3 -0
  12. package/dist/adapters/s3-media/index.d.ts.map +1 -0
  13. package/dist/adapters/s3-media/index.js +12 -0
  14. package/dist/adapters/s3-media/index.js.map +1 -0
  15. package/dist/adapters/s3-media/s3-client.d.ts +112 -0
  16. package/dist/adapters/s3-media/s3-client.d.ts.map +1 -0
  17. package/dist/adapters/s3-media/s3-client.js +261 -0
  18. package/dist/adapters/s3-media/s3-client.js.map +1 -0
  19. package/dist/adapters/s3-media/s3-media-adapter.d.ts +118 -0
  20. package/dist/adapters/s3-media/s3-media-adapter.d.ts.map +1 -0
  21. package/dist/adapters/s3-media/s3-media-adapter.js +631 -0
  22. package/dist/adapters/s3-media/s3-media-adapter.js.map +1 -0
  23. package/dist/adapters/s3-media/types.d.ts +63 -0
  24. package/dist/adapters/s3-media/types.d.ts.map +1 -0
  25. package/dist/adapters/s3-media/types.js +2 -0
  26. package/dist/adapters/s3-media/types.js.map +1 -0
  27. package/dist/core/services/auth-service.d.ts.map +1 -1
  28. package/dist/core/services/auth-service.js +4 -1
  29. package/dist/core/services/auth-service.js.map +1 -1
  30. package/dist/core/services/token-service.d.ts.map +1 -1
  31. package/dist/core/services/token-service.js +11 -1
  32. package/dist/core/services/token-service.js.map +1 -1
  33. package/dist/core/types/storage-adapters.d.ts +7 -0
  34. package/dist/core/types/storage-adapters.d.ts.map +1 -1
  35. package/dist/integrations/express/config.d.ts +6 -1
  36. package/dist/integrations/express/config.d.ts.map +1 -1
  37. package/dist/integrations/express/config.js.map +1 -1
  38. package/dist/integrations/express/integration.js +3 -3
  39. package/dist/integrations/express/integration.js.map +1 -1
  40. package/dist/integrations/workers/index.d.ts +5 -0
  41. package/dist/integrations/workers/index.d.ts.map +1 -1
  42. package/dist/integrations/workers/index.js +10 -1
  43. package/dist/integrations/workers/index.js.map +1 -1
  44. package/dist/routes/handlers/base.d.ts +21 -0
  45. package/dist/routes/handlers/base.d.ts.map +1 -1
  46. package/dist/routes/handlers/base.js +62 -12
  47. package/dist/routes/handlers/base.js.map +1 -1
  48. package/dist/routes/handlers/documents.d.ts.map +1 -1
  49. package/dist/routes/handlers/documents.js +16 -0
  50. package/dist/routes/handlers/documents.js.map +1 -1
  51. package/dist/routes/handlers/media.js +7 -7
  52. package/dist/routes/handlers/media.js.map +1 -1
  53. package/dist/routes/handlers/search.d.ts.map +1 -1
  54. package/dist/routes/handlers/search.js +8 -2
  55. package/dist/routes/handlers/search.js.map +1 -1
  56. package/dist/routes/handlers/tokens.d.ts.map +1 -1
  57. package/dist/routes/handlers/tokens.js +42 -8
  58. package/dist/routes/handlers/tokens.js.map +1 -1
  59. package/package.json +6 -1
@@ -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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"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * S3-compatible media storage adapter.
3
+ *
4
+ * Talks SigV4 over HTTP, so it reaches R2 from a Node process, and MinIO, Backblaze B2, Spaces,
5
+ * Ceph or AWS from anywhere. `CloudflareR2Adapter` is the better choice *inside* a Worker — a
6
+ * binding is faster and needs no credentials — and the two share a key layout on purpose, so
7
+ * one bucket can be handed from a Worker to a Node server and back.
8
+ *
9
+ * The layout, identical to the R2 adapter's:
10
+ *
11
+ * files/<id> the bytes
12
+ * meta/<id>.json the record — the only authority for existence
13
+ * variants/<id>/<name> variant bytes, format in user metadata
14
+ * index/... the listing cache, see index-key.ts
15
+ *
16
+ * What differs, and why:
17
+ *
18
+ * - **The listing is a join, not a sweep.** R2's `list()` can return `customMetadata`;
19
+ * `ListObjectsV2` cannot return user metadata at all. So the sortable fields are encoded into
20
+ * keys under `index/`, and `listMedia` enumerates ids from `meta/`, reads fields from the
21
+ * index, and falls back to reading the record for any id the index does not cover.
22
+ * - **The index must never be the enumeration.** Writes go bytes → record → index and deletes
23
+ * reverse that, so an interrupted operation leaves a record with no index entry. If the index
24
+ * were authoritative that file would exist and never be listed — a ghost. Enumerating from
25
+ * `meta/` makes a missing entry cost one GET instead of hiding a file, and `cleanup()`
26
+ * rebuilds it.
27
+ * - **`listVariants` HEADs each variant.** The format lives in user metadata and a listing does
28
+ * not carry it. A file has a handful of variants, so this is a handful of requests, and it
29
+ * keeps the R2 invariant that variant keys carry no format — re-encoding a variant to another
30
+ * format overwrites it instead of leaving both behind.
31
+ */
32
+ import type { MediaFile, MediaMetadata } from '../../core/types/index.js';
33
+ import type { MediaListOptions, MediaListResult, MediaStorageAdapter, MediaVariant } from '../../core/types/storage-adapters.js';
34
+ import type { S3MediaAdapterConfig } from './types.js';
35
+ export declare class S3MediaAdapter implements MediaStorageAdapter {
36
+ private readonly s3;
37
+ private readonly prefix;
38
+ private readonly publicBaseUrl?;
39
+ private readonly urlExpiry;
40
+ private readonly maxFileSize;
41
+ private readonly silent;
42
+ private readonly logger;
43
+ constructor(config: S3MediaAdapterConfig);
44
+ private fileKey;
45
+ private recordKey;
46
+ private variantKey;
47
+ private variantPrefix;
48
+ private recordPrefix;
49
+ uploadFile(file: File, metadata: MediaMetadata): Promise<MediaFile>;
50
+ getFile(id: string): Promise<MediaFile | null>;
51
+ updateFile(id: string, metadata: Record<string, any>): Promise<MediaFile>;
52
+ getFileContent(id: string): Promise<ArrayBuffer | null>;
53
+ getFileUrl(id: string, options?: {
54
+ expiresIn?: number;
55
+ }): Promise<string | null>;
56
+ fileExists(id: string): Promise<boolean>;
57
+ getFileSize(id: string): Promise<number | null>;
58
+ deleteFile(id: string): Promise<void>;
59
+ listMedia(options?: MediaListOptions): Promise<MediaListResult>;
60
+ saveVariantFile(parentId: string, variantName: string, buffer: Buffer, format: string): Promise<string>;
61
+ getVariantContent(parentId: string, variantName: string): Promise<ArrayBuffer | null>;
62
+ getVariantUrl(parentId: string, variantName: string, options?: {
63
+ expiresIn?: number;
64
+ }): Promise<string | null>;
65
+ listVariants(parentId: string): Promise<MediaVariant[]>;
66
+ deleteVariantFiles(parentId: string): Promise<void>;
67
+ deleteVariant(parentId: string, variantName: string): Promise<void>;
68
+ healthCheck(): Promise<boolean>;
69
+ getStorageInfo(): Promise<{
70
+ totalFiles: number;
71
+ totalSize: number;
72
+ availableSpace?: number;
73
+ usedSpace?: number;
74
+ [key: string]: unknown;
75
+ }>;
76
+ /**
77
+ * Reconcile the bucket against its records, which are the only authority.
78
+ *
79
+ * Three jobs, and the order they are listed in is the order of how much they matter:
80
+ *
81
+ * 1. **Rebuild missing index entries.** An upload interrupted after the record but before the
82
+ * index — or a delete interrupted the other way — leaves a file that `getFile` serves and
83
+ * no listing shows. `listMedia` already heals the symptom by reading the record; this
84
+ * removes the cost.
85
+ * 2. **Drop orphaned bytes.** Anything under `files/` or `variants/` whose id has no record.
86
+ * 3. **Drop stale index entries.** Unreachable by an interruption under these write orders,
87
+ * but reachable by a partial restore or by a re-upload whose old-entry delete failed.
88
+ *
89
+ * Returns the number of objects written or deleted.
90
+ */
91
+ cleanup(): Promise<number>;
92
+ private writeRecord;
93
+ private readRecord;
94
+ private writeIndexEntry;
95
+ /**
96
+ * Every index key for an id, whatever epoch it carries.
97
+ *
98
+ * One prefix listing rather than a sweep of the whole index — which is the entire reason the
99
+ * key puts the id before the timestamp. This runs on every upload.
100
+ */
101
+ private deleteIndexEntries;
102
+ private listRecordIds;
103
+ /**
104
+ * Every record in the bucket, with its listable fields.
105
+ *
106
+ * Two key-only listings — `meta/` for the ids, `index/` for the fields — joined by id. The
107
+ * records are the authority for *which* files exist; the index only saves reading them. An id
108
+ * the index does not cover (a bucket written by the R2 adapter, a hand-copied object, an
109
+ * interrupted upload) falls back to a real read, so it is never silently invisible.
110
+ */
111
+ private listIndex;
112
+ /** Every request here is a network round trip, so fan out — but not without a ceiling. */
113
+ private mapWithConcurrency;
114
+ private sortIndex;
115
+ private toMediaFile;
116
+ private validateMediaMetadata;
117
+ }
118
+ //# sourceMappingURL=s3-media-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-media-adapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/s3-media/s3-media-adapter.ts"],"names":[],"mappings":"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"}