@nestm/storage 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (87) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +398 -19
  3. package/SECURITY.md +69 -0
  4. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +74 -0
  5. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
  6. package/dist/ai-sdk/ai-sdk-workspace-tools.js +414 -0
  7. package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
  8. package/dist/ai-sdk/index.d.ts +2 -0
  9. package/dist/ai-sdk/index.d.ts.map +1 -0
  10. package/dist/ai-sdk/index.js +2 -0
  11. package/dist/ai-sdk/index.js.map +1 -0
  12. package/dist/files-sdk/files-sdk.driver.d.ts +64 -6
  13. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  14. package/dist/files-sdk/files-sdk.driver.js +918 -38
  15. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  16. package/dist/files-sdk/fs/index.d.ts +21 -2
  17. package/dist/files-sdk/fs/index.d.ts.map +1 -1
  18. package/dist/files-sdk/fs/index.js +833 -1
  19. package/dist/files-sdk/fs/index.js.map +1 -1
  20. package/dist/files-sdk/index.d.ts +1 -1
  21. package/dist/files-sdk/index.d.ts.map +1 -1
  22. package/dist/files-sdk/index.js.map +1 -1
  23. package/dist/files-sdk/provider/index.d.ts +9 -5
  24. package/dist/files-sdk/provider/index.d.ts.map +1 -1
  25. package/dist/files-sdk/provider/index.js +60 -15
  26. package/dist/files-sdk/provider/index.js.map +1 -1
  27. package/dist/files-sdk/s3/construction-metadata.d.ts +7 -0
  28. package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
  29. package/dist/files-sdk/s3/construction-metadata.js +13 -0
  30. package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
  31. package/dist/files-sdk/s3/index.d.ts +43 -14
  32. package/dist/files-sdk/s3/index.d.ts.map +1 -1
  33. package/dist/files-sdk/s3/index.js +1046 -57
  34. package/dist/files-sdk/s3/index.js.map +1 -1
  35. package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
  36. package/dist/gateway/storage-gateway.controller.js +25 -5
  37. package/dist/gateway/storage-gateway.controller.js.map +1 -1
  38. package/dist/storage-etag.d.ts +13 -0
  39. package/dist/storage-etag.d.ts.map +1 -0
  40. package/dist/storage-etag.js +31 -0
  41. package/dist/storage-etag.js.map +1 -0
  42. package/dist/storage.client.d.ts +7 -1
  43. package/dist/storage.client.d.ts.map +1 -1
  44. package/dist/storage.client.js +124 -10
  45. package/dist/storage.client.js.map +1 -1
  46. package/dist/storage.driver.d.ts +23 -1
  47. package/dist/storage.driver.d.ts.map +1 -1
  48. package/dist/storage.driver.js.map +1 -1
  49. package/dist/storage.service.d.ts.map +1 -1
  50. package/dist/storage.service.js +28 -3
  51. package/dist/storage.service.js.map +1 -1
  52. package/dist/storage.types.d.ts +113 -6
  53. package/dist/storage.types.d.ts.map +1 -1
  54. package/dist/storage.types.js.map +1 -1
  55. package/dist/testing/index.d.ts +1 -0
  56. package/dist/testing/index.d.ts.map +1 -1
  57. package/dist/testing/index.js +1 -0
  58. package/dist/testing/index.js.map +1 -1
  59. package/dist/testing/provider-conformance.d.ts +65 -0
  60. package/dist/testing/provider-conformance.d.ts.map +1 -0
  61. package/dist/testing/provider-conformance.js +853 -0
  62. package/dist/testing/provider-conformance.js.map +1 -0
  63. package/dist/workspace/index.d.ts +5 -0
  64. package/dist/workspace/index.d.ts.map +1 -0
  65. package/dist/workspace/index.js +5 -0
  66. package/dist/workspace/index.js.map +1 -0
  67. package/dist/workspace/storage-workspace.cursor.d.ts +71 -0
  68. package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
  69. package/dist/workspace/storage-workspace.cursor.js +408 -0
  70. package/dist/workspace/storage-workspace.cursor.js.map +1 -0
  71. package/dist/workspace/storage-workspace.d.ts +6 -0
  72. package/dist/workspace/storage-workspace.d.ts.map +1 -0
  73. package/dist/workspace/storage-workspace.error.d.ts +28 -0
  74. package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
  75. package/dist/workspace/storage-workspace.error.js +67 -0
  76. package/dist/workspace/storage-workspace.error.js.map +1 -0
  77. package/dist/workspace/storage-workspace.js +755 -0
  78. package/dist/workspace/storage-workspace.js.map +1 -0
  79. package/dist/workspace/storage-workspace.path.d.ts +9 -0
  80. package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
  81. package/dist/workspace/storage-workspace.path.js +68 -0
  82. package/dist/workspace/storage-workspace.path.js.map +1 -0
  83. package/dist/workspace/storage-workspace.types.d.ts +105 -0
  84. package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
  85. package/dist/workspace/storage-workspace.types.js +21 -0
  86. package/dist/workspace/storage-workspace.types.js.map +1 -0
  87. package/package.json +24 -5
@@ -1,25 +1,312 @@
1
1
  import { Readable } from 'node:stream';
2
- import { Files, FilesError, } from 'files-sdk';
2
+ import { Files, FilesError, handlers, } from 'files-sdk';
3
3
  import { StorageError, StorageErrorCode, isStorageError, } from '../storage.error.js';
4
+ import { isCanonicalStorageEtag, normalizeProviderStorageEtag, } from '../storage-etag.js';
4
5
  import { getFilesSdkUploadControl } from '../storage-upload-control.js';
6
+ const FILES_SDK_S3_RESERVED_EXTENSION_KEYS = [
7
+ 'conditionalCopyDestination',
8
+ 'conditionalCopySource',
9
+ 'conditionalCreate',
10
+ 'conditionalDelete',
11
+ 'conditionalMultipartCompletion',
12
+ 'conditionalRead',
13
+ 'conditionalReplace',
14
+ 'deleteConditional',
15
+ 'downloadConditional',
16
+ 'physicalKey',
17
+ 'promote',
18
+ 'signedDownloadPolicy',
19
+ 'signedUploadPolicy',
20
+ 'uploadConditional',
21
+ ];
22
+ const FILES_SDK_S3_AUTHORITY_KEYS = [
23
+ ...FILES_SDK_S3_RESERVED_EXTENSION_KEYS,
24
+ 'bucket',
25
+ 'copy',
26
+ 'delete',
27
+ 'deleteMany',
28
+ 'download',
29
+ 'exists',
30
+ 'head',
31
+ 'list',
32
+ 'move',
33
+ 'reportsUploadProgress',
34
+ 'resumableUpload',
35
+ 'signedUploadUrl',
36
+ 'signedUrl',
37
+ 'supportsCacheControl',
38
+ 'supportsDelimiter',
39
+ 'supportsMetadata',
40
+ 'supportsRange',
41
+ 'supportsServerSideCopy',
42
+ 'upload',
43
+ 'url',
44
+ ];
45
+ const s3AdapterProvenance = new WeakMap();
46
+ const undecoratedS3Adapters = new WeakSet();
47
+ const s3AdapterAuthority = new WeakMap();
48
+ const s3MethodAuthority = new WeakMap();
49
+ const s3AuthorityState = new WeakMap();
50
+ const conflictingS3Authority = Symbol('conflictingS3Authority');
51
+ function adapterRawObject(adapter) {
52
+ try {
53
+ const raw = adapter.raw;
54
+ return (typeof raw === 'object' && raw !== null) ||
55
+ typeof raw === 'function'
56
+ ? raw
57
+ : undefined;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ function s3AdapterAuthorityTokenOf(adapter) {
64
+ let resolved = s3AdapterAuthority.get(adapter);
65
+ const consider = (token) => {
66
+ if (token === undefined)
67
+ return true;
68
+ if (resolved !== undefined && resolved !== token)
69
+ return false;
70
+ resolved = token;
71
+ return true;
72
+ };
73
+ try {
74
+ const candidate = adapter;
75
+ for (const key of FILES_SDK_S3_AUTHORITY_KEYS) {
76
+ if (!(key in candidate))
77
+ continue;
78
+ const value = candidate[key];
79
+ if (typeof value === 'function' &&
80
+ !consider(s3MethodAuthority.get(value))) {
81
+ return conflictingS3Authority;
82
+ }
83
+ }
84
+ return resolved;
85
+ }
86
+ catch {
87
+ return conflictingS3Authority;
88
+ }
89
+ }
90
+ function bindS3AdapterAuthorityMethods(adapter, token) {
91
+ const candidate = adapter;
92
+ for (const key of FILES_SDK_S3_AUTHORITY_KEYS) {
93
+ if (!(key in candidate))
94
+ continue;
95
+ const value = candidate[key];
96
+ if (typeof value !== 'function')
97
+ continue;
98
+ const existing = s3MethodAuthority.get(value);
99
+ if (existing !== undefined && existing !== token) {
100
+ throw new TypeError('S3 adapter methods have conflicting authority.');
101
+ }
102
+ s3MethodAuthority.set(value, token);
103
+ }
104
+ }
105
+ function attachS3AdapterAuthority(adapter, raw) {
106
+ const existing = s3AdapterAuthorityTokenOf(adapter);
107
+ if (existing === conflictingS3Authority) {
108
+ throw new TypeError('S3 adapter authority is inconsistent.');
109
+ }
110
+ const token = existing ?? Object.freeze({});
111
+ const state = s3AuthorityState.get(token);
112
+ if (state !== undefined && state.raw !== raw) {
113
+ throw new TypeError('S3 adapter authority raw client cannot be changed.');
114
+ }
115
+ if (state === undefined)
116
+ s3AuthorityState.set(token, { raw });
117
+ s3AdapterAuthority.set(adapter, token);
118
+ bindS3AdapterAuthorityMethods(adapter, token);
119
+ return token;
120
+ }
121
+ function setS3AdapterProvenance(target, adapter, provenance) {
122
+ const existing = s3AdapterProvenanceOfRaw(target);
123
+ if (existing !== undefined) {
124
+ if (existing.provenance !== provenance ||
125
+ mismatchedS3Authority(adapter, existing.surface) !== undefined) {
126
+ throw new TypeError('S3 adapter provenance cannot be changed.');
127
+ }
128
+ s3AdapterProvenance.set(target, existing);
129
+ return existing;
130
+ }
131
+ const record = Object.freeze({
132
+ provenance,
133
+ surface: s3AuthoritySnapshot(adapter),
134
+ });
135
+ s3AdapterProvenance.set(target, record);
136
+ return record;
137
+ }
138
+ function s3AdapterProvenanceOfRaw(raw) {
139
+ return s3AdapterProvenance.get(raw);
140
+ }
141
+ function s3AuthoritySnapshot(adapter) {
142
+ const candidate = adapter;
143
+ return Object.freeze(Object.fromEntries(FILES_SDK_S3_AUTHORITY_KEYS.map((key) => {
144
+ const present = key in candidate;
145
+ return [
146
+ key,
147
+ Object.freeze({
148
+ present,
149
+ value: present ? candidate[key] : undefined,
150
+ }),
151
+ ];
152
+ })));
153
+ }
154
+ function mismatchedS3Authority(adapter, snapshot) {
155
+ const candidate = adapter;
156
+ try {
157
+ return FILES_SDK_S3_AUTHORITY_KEYS.find((key) => {
158
+ const present = key in candidate;
159
+ const expected = snapshot[key];
160
+ return (present !== expected.present ||
161
+ !Object.is(present ? candidate[key] : undefined, expected.value));
162
+ });
163
+ }
164
+ catch {
165
+ return FILES_SDK_S3_AUTHORITY_KEYS[0];
166
+ }
167
+ }
168
+ /** Rejects adapters already carrying reserved storage S3 extensions. */
169
+ export function assertFilesSdkS3AdapterHasNoReservedExtensions(adapter) {
170
+ const reserved = filesSdkS3ReservedExtensionOf(adapter);
171
+ if (reserved !== undefined) {
172
+ throw new TypeError(`S3 adapter already defines reserved extension "${reserved}".`);
173
+ }
174
+ }
175
+ function filesSdkS3ReservedExtensionOf(adapter) {
176
+ const candidate = adapter;
177
+ try {
178
+ return FILES_SDK_S3_RESERVED_EXTENSION_KEYS.find((key) => key in candidate);
179
+ }
180
+ catch {
181
+ return FILES_SDK_S3_RESERVED_EXTENSION_KEYS[0];
182
+ }
183
+ }
184
+ /** Records that an S3 adapter was constructed but not provider-decorated. */
185
+ export function markFilesSdkS3AdapterUndecorated(adapter) {
186
+ const raw = adapterRawObject(adapter);
187
+ if (raw === undefined) {
188
+ throw new TypeError('S3 adapter must expose an object raw client.');
189
+ }
190
+ attachS3AdapterAuthority(adapter, raw);
191
+ undecoratedS3Adapters.add(raw);
192
+ }
193
+ /** Records the provider verification state of one decorated S3 adapter. */
194
+ export function markFilesSdkS3AdapterProvenance(adapter, provenance) {
195
+ const raw = adapterRawObject(adapter);
196
+ if (raw === undefined) {
197
+ throw new TypeError('S3 adapter must expose an object raw client.');
198
+ }
199
+ markFilesSdkS3RawClientProvenance(raw, adapter, provenance);
200
+ }
201
+ /** Records final provider verification state on one stable raw S3 client. */
202
+ export function markFilesSdkS3RawClientProvenance(raw, adapter, provenance) {
203
+ const authority = attachS3AdapterAuthority(adapter, raw);
204
+ const record = setS3AdapterProvenance(raw, adapter, provenance);
205
+ s3AuthorityState.set(authority, { raw, record });
206
+ bindS3AdapterAuthorityMethods(adapter, authority);
207
+ }
208
+ function isUndecoratedS3Raw(raw) {
209
+ return undecoratedS3Adapters.has(raw);
210
+ }
211
+ function isStructuralS3Raw(raw) {
212
+ try {
213
+ const candidate = raw;
214
+ return (typeof candidate.send === 'function' &&
215
+ typeof candidate.config === 'object' &&
216
+ candidate.config !== null &&
217
+ candidate.config.serviceId === 'S3');
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ }
5
223
  function conditionalCopyAdapterOf(adapter) {
6
224
  if (typeof adapter !== 'object' ||
7
225
  adapter === null ||
8
- !('conditionalCopy' in adapter) ||
9
226
  !('promote' in adapter)) {
10
227
  return undefined;
11
228
  }
12
229
  const candidate = adapter;
13
- const capability = candidate.conditionalCopy;
14
- if (capability === undefined ||
15
- typeof capability.supported !== 'boolean' ||
16
- typeof capability.etag !== 'boolean' ||
17
- typeof capability.version !== 'boolean' ||
230
+ const source = candidate.conditionalCopySource;
231
+ const destination = candidate.conditionalCopyDestination;
232
+ if ((source === undefined && destination === undefined) ||
233
+ (source !== undefined &&
234
+ (typeof source.etag !== 'boolean' ||
235
+ typeof source.version !== 'boolean')) ||
236
+ (destination !== undefined &&
237
+ (typeof destination.create !== 'boolean' ||
238
+ typeof destination.replace !== 'boolean' ||
239
+ typeof destination.atomicWithSource !== 'boolean')) ||
18
240
  typeof candidate.promote !== 'function') {
19
241
  return undefined;
20
242
  }
21
243
  return candidate;
22
244
  }
245
+ function conditionalUploadAdapterOf(adapter) {
246
+ if (typeof adapter !== 'object' ||
247
+ adapter === null ||
248
+ !('uploadConditional' in adapter)) {
249
+ return undefined;
250
+ }
251
+ const candidate = adapter;
252
+ const create = candidate.conditionalCreate;
253
+ const replace = candidate.conditionalReplace;
254
+ const multipart = candidate.conditionalMultipartCompletion;
255
+ if ((create === undefined && replace === undefined) ||
256
+ (create !== undefined && typeof create.resultEtag !== 'boolean') ||
257
+ (replace !== undefined && typeof replace.resultEtag !== 'boolean') ||
258
+ (multipart !== undefined &&
259
+ (typeof multipart.create !== 'boolean' ||
260
+ typeof multipart.replace !== 'boolean')) ||
261
+ typeof candidate.uploadConditional !== 'function') {
262
+ return undefined;
263
+ }
264
+ return candidate;
265
+ }
266
+ function conditionalDeleteAdapterOf(adapter) {
267
+ if (typeof adapter !== 'object' ||
268
+ adapter === null ||
269
+ !('conditionalDelete' in adapter) ||
270
+ !('deleteConditional' in adapter)) {
271
+ return undefined;
272
+ }
273
+ const candidate = adapter;
274
+ if (candidate.conditionalDelete === undefined ||
275
+ typeof candidate.conditionalDelete.etag !== 'boolean' ||
276
+ typeof candidate.deleteConditional !== 'function') {
277
+ return undefined;
278
+ }
279
+ return candidate;
280
+ }
281
+ function conditionalReadAdapterOf(adapter) {
282
+ if (typeof adapter !== 'object' ||
283
+ adapter === null ||
284
+ !('conditionalRead' in adapter) ||
285
+ !('downloadConditional' in adapter)) {
286
+ return undefined;
287
+ }
288
+ const candidate = adapter;
289
+ if (candidate.conditionalRead === undefined ||
290
+ typeof candidate.conditionalRead.etag !== 'boolean' ||
291
+ typeof candidate.conditionalRead.version !== 'boolean' ||
292
+ typeof candidate.downloadConditional !== 'function') {
293
+ return undefined;
294
+ }
295
+ return candidate;
296
+ }
297
+ function physicalKeyAdapterOf(adapter) {
298
+ if (typeof adapter !== 'object' ||
299
+ adapter === null ||
300
+ !('physicalKey' in adapter)) {
301
+ return undefined;
302
+ }
303
+ const candidate = adapter;
304
+ const maxBytes = candidate.physicalKey?.maxBytes;
305
+ if (!Number.isSafeInteger(maxBytes) || (maxBytes ?? 0) <= 0) {
306
+ return undefined;
307
+ }
308
+ return candidate;
309
+ }
23
310
  function signedUploadPolicyAdapterOf(adapter) {
24
311
  if (typeof adapter !== 'object' ||
25
312
  adapter === null ||
@@ -93,19 +380,42 @@ function unwrapFilesError(error) {
93
380
  }
94
381
  return current;
95
382
  }
383
+ const PUBLIC_PROVIDER_ERROR_MESSAGES = Object.freeze({
384
+ [StorageErrorCode.NOT_FOUND]: 'Storage provider object was not found.',
385
+ [StorageErrorCode.UNAUTHORIZED]: 'Storage provider operation was unauthorized.',
386
+ [StorageErrorCode.CONFLICT]: 'Storage provider operation conflicted with current state.',
387
+ [StorageErrorCode.READ_ONLY]: 'Storage provider is read-only.',
388
+ [StorageErrorCode.INVALID_ARGUMENT]: 'Storage provider rejected an invalid argument.',
389
+ [StorageErrorCode.NOT_SUPPORTED]: 'Storage provider operation is not supported.',
390
+ [StorageErrorCode.ABORTED]: 'Storage provider operation was aborted.',
391
+ [StorageErrorCode.TIMEOUT]: 'Storage provider operation timed out.',
392
+ [StorageErrorCode.LIMIT_EXCEEDED]: 'Storage provider operation exceeded a limit.',
393
+ [StorageErrorCode.PROVIDER]: 'Storage provider operation failed.',
394
+ });
395
+ function sanitizedStorageError(error) {
396
+ return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[error.code], {
397
+ aborted: error.aborted,
398
+ code: error.code,
399
+ permanent: error.permanent,
400
+ timedOut: error.timedOut,
401
+ });
402
+ }
96
403
  export function mapFilesSdkError(error) {
97
- if (isStorageError(error)) {
98
- return error;
99
- }
404
+ if (isStorageError(error))
405
+ return sanitizedStorageError(error);
100
406
  if (!isFilesErrorLike(error)) {
101
- return new StorageError(error instanceof Error ? error.message : String(error), {
102
- cause: error,
407
+ return new StorageError('Storage provider operation failed.', {
103
408
  code: StorageErrorCode.PROVIDER,
104
409
  });
105
410
  }
106
411
  const filesError = unwrapFilesError(error);
107
- if (isStorageError(filesError.cause)) {
108
- return filesError.cause;
412
+ if (filesError.code === 'Provider' &&
413
+ !filesError.aborted &&
414
+ !filesError.timedOut &&
415
+ !filesError.permanent &&
416
+ isStorageError(filesError.cause) &&
417
+ filesError.message === filesError.cause.message) {
418
+ return sanitizedStorageError(filesError.cause);
109
419
  }
110
420
  let code;
111
421
  if (filesError.timedOut) {
@@ -135,9 +445,8 @@ export function mapFilesSdkError(error) {
135
445
  break;
136
446
  }
137
447
  }
138
- return new StorageError(filesError.message, {
448
+ return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[code], {
139
449
  aborted: filesError.aborted,
140
- cause: filesError.cause ?? error,
141
450
  code,
142
451
  permanent: filesError.permanent,
143
452
  timedOut: filesError.timedOut,
@@ -157,6 +466,32 @@ function mapRetryOptions(retries) {
157
466
  }),
158
467
  };
159
468
  }
469
+ function storageRetryOptions(retries) {
470
+ if (retries === undefined || typeof retries === 'number') {
471
+ return retries;
472
+ }
473
+ return {
474
+ max: retries.max,
475
+ ...(retries.backoff !== undefined && {
476
+ backoff: ({ attempt, error }) => retries.backoff?.({
477
+ attempt,
478
+ error: new FilesError(error.code === StorageErrorCode.NOT_FOUND
479
+ ? 'NotFound'
480
+ : error.code === StorageErrorCode.UNAUTHORIZED
481
+ ? 'Unauthorized'
482
+ : error.code === StorageErrorCode.CONFLICT
483
+ ? 'Conflict'
484
+ : error.code === StorageErrorCode.READ_ONLY
485
+ ? 'ReadOnly'
486
+ : 'Provider', error.message, error, {
487
+ aborted: error.aborted,
488
+ permanent: error.permanent,
489
+ timedOut: error.timedOut,
490
+ }),
491
+ }) ?? 0,
492
+ }),
493
+ };
494
+ }
160
495
  function operationOptions(options) {
161
496
  if (options === undefined) {
162
497
  return undefined;
@@ -319,10 +654,34 @@ function signedUploadOptions(options) {
319
654
  ...(options.minSize !== undefined && { minSize: options.minSize }),
320
655
  };
321
656
  }
322
- function metadataOf(file) {
657
+ function providerEtag(value, key, operation) {
658
+ if (value === undefined) {
659
+ return undefined;
660
+ }
661
+ const etag = normalizeProviderStorageEtag(value);
662
+ if (etag === undefined) {
663
+ throw new StorageError('Storage adapter returned an invalid ETag.', {
664
+ code: StorageErrorCode.PROVIDER,
665
+ key,
666
+ operation,
667
+ permanent: true,
668
+ });
669
+ }
670
+ return etag;
671
+ }
672
+ function invalidConditionalEtag(label, key, operation) {
673
+ return new StorageError(`${label} must be a canonical storage ETag.`, {
674
+ code: StorageErrorCode.INVALID_ARGUMENT,
675
+ key,
676
+ operation,
677
+ permanent: true,
678
+ });
679
+ }
680
+ function metadataOf(file, operation) {
681
+ const etag = providerEtag(file.etag, file.key, operation);
323
682
  return {
324
683
  contentType: file.type,
325
- ...(file.etag !== undefined && { etag: file.etag }),
684
+ ...(etag !== undefined && { etag }),
326
685
  key: file.key,
327
686
  ...(file.lastModified !== undefined && {
328
687
  lastModified: new Date(file.lastModified),
@@ -335,9 +694,10 @@ function metadataOf(file) {
335
694
  };
336
695
  }
337
696
  function uploadResultOf(result) {
697
+ const etag = providerEtag(result.etag, result.key, 'upload');
338
698
  return {
339
699
  contentType: result.contentType,
340
- ...(result.etag !== undefined && { etag: result.etag }),
700
+ ...(etag !== undefined && { etag }),
341
701
  key: result.key,
342
702
  ...(result.lastModified !== undefined && {
343
703
  lastModified: new Date(result.lastModified),
@@ -345,18 +705,253 @@ function uploadResultOf(result) {
345
705
  size: result.size,
346
706
  };
347
707
  }
708
+ function normalizeFilesSdkPrefix(prefix) {
709
+ if (prefix === undefined)
710
+ return '';
711
+ if (typeof prefix !== 'string') {
712
+ throw new StorageError('prefix must be a string.', {
713
+ code: StorageErrorCode.INVALID_ARGUMENT,
714
+ permanent: true,
715
+ });
716
+ }
717
+ const normalized = prefix.replaceAll(/^\/+|(?<!\/)\/+$/gu, '');
718
+ if (normalized.length === 0 || normalized.includes('\0')) {
719
+ throw new StorageError('prefix must be a non-empty string without null bytes.', {
720
+ code: StorageErrorCode.INVALID_ARGUMENT,
721
+ permanent: true,
722
+ });
723
+ }
724
+ if (normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
725
+ throw new StorageError('prefix must not contain . or .. path segments.', {
726
+ code: StorageErrorCode.INVALID_ARGUMENT,
727
+ permanent: true,
728
+ });
729
+ }
730
+ return normalized;
731
+ }
732
+ function physicalPluginKey(prefix, key) {
733
+ if (typeof key !== 'string' || key.length === 0) {
734
+ throw new StorageError('The final plugin key must be a non-empty string.', {
735
+ code: StorageErrorCode.INVALID_ARGUMENT,
736
+ permanent: true,
737
+ });
738
+ }
739
+ if (key.includes('\0')) {
740
+ throw new StorageError('The final plugin key must not contain null bytes.', {
741
+ code: StorageErrorCode.INVALID_ARGUMENT,
742
+ key,
743
+ permanent: true,
744
+ });
745
+ }
746
+ const normalized = key.replace(/^\/+/u, '');
747
+ if (normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
748
+ throw new StorageError('The final plugin key must not contain . or .. path segments.', {
749
+ code: StorageErrorCode.INVALID_ARGUMENT,
750
+ key,
751
+ permanent: true,
752
+ });
753
+ }
754
+ return {
755
+ logicalKey: key,
756
+ physicalKey: prefix.length === 0 ? key : `${prefix}/${normalized}`,
757
+ };
758
+ }
759
+ function assertPhysicalPluginBudget(physicalKey, maxBytes, logicalKey) {
760
+ if (maxBytes === undefined ||
761
+ new TextEncoder().encode(physicalKey).byteLength <= maxBytes) {
762
+ return;
763
+ }
764
+ throw new StorageError(`The combined storage prefix and key exceed the provider's ${maxBytes}-byte physical-key limit.`, {
765
+ code: StorageErrorCode.LIMIT_EXCEEDED,
766
+ ...(logicalKey !== undefined && { key: logicalKey }),
767
+ permanent: true,
768
+ });
769
+ }
770
+ function physicalKeyGuardPlugin(prefix, maxBytes) {
771
+ const assertKey = (key) => {
772
+ const resolved = physicalPluginKey(prefix, key);
773
+ assertPhysicalPluginBudget(resolved.physicalKey, maxBytes, resolved.logicalKey);
774
+ return resolved.logicalKey;
775
+ };
776
+ const assertListOptions = (options) => {
777
+ if (options !== undefined &&
778
+ (typeof options !== 'object' || options === null)) {
779
+ throw new StorageError('The final plugin list options must be an object.', {
780
+ code: StorageErrorCode.INVALID_ARGUMENT,
781
+ permanent: true,
782
+ });
783
+ }
784
+ const listPrefix = options
785
+ ?.prefix;
786
+ if (listPrefix === undefined || listPrefix === '') {
787
+ if (prefix.length > 0) {
788
+ assertPhysicalPluginBudget(`${prefix}/`, maxBytes);
789
+ }
790
+ return options === undefined
791
+ ? undefined
792
+ : (() => {
793
+ const { prefix: _prefix, ...rest } = options;
794
+ return Object.freeze({
795
+ ...rest,
796
+ ...(listPrefix === '' && { prefix: listPrefix }),
797
+ });
798
+ })();
799
+ }
800
+ if (typeof listPrefix !== 'string') {
801
+ throw new StorageError('The final plugin list prefix must be a string.', {
802
+ code: StorageErrorCode.INVALID_ARGUMENT,
803
+ permanent: true,
804
+ });
805
+ }
806
+ const resolved = physicalPluginKey(prefix, listPrefix);
807
+ assertPhysicalPluginBudget(resolved.physicalKey, maxBytes, resolved.logicalKey);
808
+ return Object.freeze({
809
+ ...options,
810
+ prefix: resolved.logicalKey,
811
+ });
812
+ };
813
+ return {
814
+ name: '@nestm/storage/physical-key-guard',
815
+ wrap: handlers({
816
+ copy: (operation, next) => {
817
+ const from = assertKey(operation.from);
818
+ const to = assertKey(operation.to);
819
+ return next(Object.freeze({ ...operation, from, to }));
820
+ },
821
+ delete: (operation, next) => {
822
+ const key = assertKey(operation.key);
823
+ return next(Object.freeze({ ...operation, key }));
824
+ },
825
+ download: (operation, next) => {
826
+ const key = assertKey(operation.key);
827
+ return next(Object.freeze({ ...operation, key }));
828
+ },
829
+ exists: (operation, next) => {
830
+ const key = assertKey(operation.key);
831
+ return next(Object.freeze({ ...operation, key }));
832
+ },
833
+ head: (operation, next) => {
834
+ const key = assertKey(operation.key);
835
+ return next(Object.freeze({ ...operation, key }));
836
+ },
837
+ list: (operation, next) => {
838
+ const options = assertListOptions(operation.options);
839
+ return next(Object.freeze({
840
+ ...operation,
841
+ ...(options !== undefined && { options }),
842
+ }));
843
+ },
844
+ move: (operation, next) => {
845
+ const from = assertKey(operation.from);
846
+ const to = assertKey(operation.to);
847
+ return next(Object.freeze({ ...operation, from, to }));
848
+ },
849
+ signedUploadUrl: (operation, next) => {
850
+ const key = assertKey(operation.key);
851
+ return next(Object.freeze({ ...operation, key }));
852
+ },
853
+ upload: (operation, next) => {
854
+ const key = assertKey(operation.key);
855
+ return next(Object.freeze({ ...operation, key }));
856
+ },
857
+ url: (operation, next) => {
858
+ const key = assertKey(operation.key);
859
+ return next(Object.freeze({ ...operation, key }));
860
+ },
861
+ }),
862
+ };
863
+ }
348
864
  export class FilesSdkStorageDriver {
349
865
  #files;
350
866
  #name;
351
867
  #conditionalCopy;
868
+ #conditionalDelete;
869
+ #conditionalRead;
870
+ #conditionalUpload;
871
+ #physicalKey;
872
+ #prefix;
873
+ #readOnly;
874
+ #retries;
875
+ #signal;
876
+ #timeout;
352
877
  #signedUploadPolicy;
353
878
  #signedDownloadPolicy;
354
879
  constructor(options) {
355
- this.#files = new Files(options);
356
- this.#name = options.adapter.name;
357
- this.#conditionalCopy = conditionalCopyAdapterOf(options.adapter);
358
- this.#signedUploadPolicy = signedUploadPolicyAdapterOf(options.adapter);
359
- this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(options.adapter);
880
+ const raw = adapterRawObject(options.adapter);
881
+ const rawProvenance = raw === undefined ? undefined : s3AdapterProvenanceOfRaw(raw);
882
+ const authorityToken = s3AdapterAuthorityTokenOf(options.adapter);
883
+ const authorityState = typeof authorityToken === 'object'
884
+ ? s3AuthorityState.get(authorityToken)
885
+ : undefined;
886
+ const authorityInvalid = authorityToken === conflictingS3Authority ||
887
+ (authorityState !== undefined &&
888
+ (authorityState.record === undefined ||
889
+ raw === undefined ||
890
+ (raw !== authorityState.raw &&
891
+ rawProvenance !== authorityState.record)));
892
+ const s3Provenance = authorityState?.record ?? rawProvenance;
893
+ if (authorityInvalid ||
894
+ (s3Provenance === undefined &&
895
+ raw !== undefined &&
896
+ (isUndecoratedS3Raw(raw) || isStructuralS3Raw(raw)))) {
897
+ throw new StorageError('Raw S3 adapters must be configured with withS3Capabilities() or createS3StorageDriver().', {
898
+ code: StorageErrorCode.INVALID_ARGUMENT,
899
+ permanent: true,
900
+ });
901
+ }
902
+ const mismatchedAuthority = s3Provenance === undefined
903
+ ? undefined
904
+ : mismatchedS3Authority(options.adapter, s3Provenance.surface);
905
+ if (mismatchedAuthority !== undefined) {
906
+ throw new StorageError(`S3 adapter authority "${mismatchedAuthority}" does not match its verified provider profile.`, {
907
+ code: StorageErrorCode.INVALID_ARGUMENT,
908
+ permanent: true,
909
+ });
910
+ }
911
+ const adapterForFiles = s3Provenance === undefined
912
+ ? options.adapter
913
+ : Object.freeze({ ...options.adapter });
914
+ const copiedAuthorityMismatch = s3Provenance === undefined
915
+ ? undefined
916
+ : mismatchedS3Authority(adapterForFiles, s3Provenance.surface);
917
+ if (copiedAuthorityMismatch !== undefined) {
918
+ throw new StorageError(`S3 adapter authority "${copiedAuthorityMismatch}" changed while it was being bound.`, {
919
+ code: StorageErrorCode.INVALID_ARGUMENT,
920
+ permanent: true,
921
+ });
922
+ }
923
+ const readOnly = options.readonly === true || s3Provenance?.provenance === 'unverified';
924
+ this.#physicalKey = physicalKeyAdapterOf(adapterForFiles);
925
+ const guardedPrefix = normalizeFilesSdkPrefix(options.prefix);
926
+ const plugins = [
927
+ ...(options.plugins ?? []),
928
+ physicalKeyGuardPlugin(guardedPrefix, this.#physicalKey?.physicalKey.maxBytes),
929
+ ];
930
+ this.#files = new Files({
931
+ ...options,
932
+ adapter: adapterForFiles,
933
+ plugins,
934
+ readonly: readOnly,
935
+ });
936
+ this.#name = adapterForFiles.name;
937
+ this.#conditionalCopy = conditionalCopyAdapterOf(adapterForFiles);
938
+ this.#conditionalDelete = conditionalDeleteAdapterOf(adapterForFiles);
939
+ this.#conditionalRead = conditionalReadAdapterOf(adapterForFiles);
940
+ this.#conditionalUpload = conditionalUploadAdapterOf(adapterForFiles);
941
+ this.#prefix = this.#files.prefix;
942
+ if (guardedPrefix !== this.#prefix) {
943
+ throw new StorageError('Storage prefix normalization was inconsistent.', {
944
+ code: StorageErrorCode.INVALID_ARGUMENT,
945
+ permanent: true,
946
+ });
947
+ }
948
+ this.#assertPhysicalKeyBudget(this.#prefix);
949
+ this.#readOnly = readOnly;
950
+ this.#retries = storageRetryOptions(options.retries);
951
+ this.#signal = options.signal;
952
+ this.#timeout = options.timeout;
953
+ this.#signedUploadPolicy = signedUploadPolicyAdapterOf(adapterForFiles);
954
+ this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(adapterForFiles);
360
955
  }
361
956
  get name() {
362
957
  return this.#name;
@@ -368,10 +963,52 @@ export class FilesSdkStorageDriver {
368
963
  delimiter: capabilities.delimiter,
369
964
  metadata: capabilities.metadata,
370
965
  rangeRead: capabilities.rangeRead,
371
- resumableUpload: capabilities.multipart,
372
- serverSideCopy: capabilities.serverSideCopy,
373
- ...(this.#conditionalCopy !== undefined && {
374
- conditionalCopy: { ...this.#conditionalCopy.conditionalCopy },
966
+ resumableUpload: !this.#readOnly && capabilities.multipart,
967
+ serverSideCopy: !this.#readOnly && capabilities.serverSideCopy,
968
+ ...(this.#conditionalCopy !== undefined &&
969
+ !this.#readOnly && {
970
+ ...(this.#conditionalCopy.conditionalCopySource !== undefined && {
971
+ conditionalCopySource: {
972
+ ...this.#conditionalCopy.conditionalCopySource,
973
+ },
974
+ }),
975
+ ...(this.#conditionalCopy.conditionalCopyDestination !==
976
+ undefined && {
977
+ conditionalCopyDestination: {
978
+ ...this.#conditionalCopy.conditionalCopyDestination,
979
+ },
980
+ }),
981
+ }),
982
+ ...(this.#conditionalDelete !== undefined &&
983
+ !this.#readOnly && {
984
+ conditionalDelete: {
985
+ ...this.#conditionalDelete.conditionalDelete,
986
+ },
987
+ }),
988
+ ...(this.#conditionalRead !== undefined && {
989
+ conditionalRead: { ...this.#conditionalRead.conditionalRead },
990
+ }),
991
+ ...(this.#conditionalUpload !== undefined &&
992
+ !this.#readOnly && {
993
+ ...(this.#conditionalUpload.conditionalCreate !== undefined && {
994
+ conditionalCreate: {
995
+ ...this.#conditionalUpload.conditionalCreate,
996
+ },
997
+ }),
998
+ ...(this.#conditionalUpload.conditionalReplace !== undefined && {
999
+ conditionalReplace: {
1000
+ ...this.#conditionalUpload.conditionalReplace,
1001
+ },
1002
+ }),
1003
+ ...(this.#conditionalUpload.conditionalMultipartCompletion !==
1004
+ undefined && {
1005
+ conditionalMultipartCompletion: {
1006
+ ...this.#conditionalUpload.conditionalMultipartCompletion,
1007
+ },
1008
+ }),
1009
+ }),
1010
+ ...(this.#physicalKey !== undefined && {
1011
+ physicalKey: { ...this.#physicalKey.physicalKey },
375
1012
  }),
376
1013
  signedDownload: { ...capabilities.signedUrl },
377
1014
  ...(this.#signedDownloadPolicy !== undefined && {
@@ -379,45 +1016,219 @@ export class FilesSdkStorageDriver {
379
1016
  ...this.#signedDownloadPolicy.signedDownloadPolicy,
380
1017
  },
381
1018
  }),
382
- signedUpload: 'runtime',
383
- ...(this.#signedUploadPolicy !== undefined && {
384
- signedUploadPolicy: { ...this.#signedUploadPolicy.signedUploadPolicy },
1019
+ signedUpload: this.#readOnly ? false : 'runtime',
1020
+ ...(this.#signedUploadPolicy !== undefined &&
1021
+ !this.#readOnly && {
1022
+ signedUploadPolicy: {
1023
+ ...this.#signedUploadPolicy.signedUploadPolicy,
1024
+ },
385
1025
  }),
386
- nativeUploadProgress: capabilities.uploadProgress,
1026
+ nativeUploadProgress: !this.#readOnly && capabilities.uploadProgress,
387
1027
  };
388
1028
  }
389
1029
  async upload(key, body, options) {
1030
+ this.#assertLogicalKey(key);
390
1031
  return this.#call(async () => uploadResultOf(await this.#files.upload(key, mapBody(body), uploadOptions(options))));
391
1032
  }
1033
+ uploadConditional(key, body, options) {
1034
+ if (options.condition.type === 'replace' &&
1035
+ !isCanonicalStorageEtag(options.condition.etag)) {
1036
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'upload'));
1037
+ }
1038
+ if (this.#readOnly) {
1039
+ return Promise.reject(new StorageError(`Cannot call uploadConditional() on a read-only storage adapter.`, {
1040
+ code: StorageErrorCode.READ_ONLY,
1041
+ key,
1042
+ operation: 'upload',
1043
+ permanent: true,
1044
+ }));
1045
+ }
1046
+ const adapter = this.#conditionalUpload;
1047
+ const capability = options.condition.type === 'create'
1048
+ ? adapter?.conditionalCreate
1049
+ : adapter?.conditionalReplace;
1050
+ const multipartRequested = options.multipart !== undefined && options.multipart !== false;
1051
+ const multipartSupported = !multipartRequested ||
1052
+ (options.condition.type === 'create'
1053
+ ? adapter?.conditionalMultipartCompletion?.create === true
1054
+ : adapter?.conditionalMultipartCompletion?.replace === true);
1055
+ if (adapter === undefined ||
1056
+ capability === undefined ||
1057
+ !multipartSupported) {
1058
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional upload.`, {
1059
+ code: StorageErrorCode.NOT_SUPPORTED,
1060
+ key,
1061
+ operation: 'upload',
1062
+ permanent: true,
1063
+ }));
1064
+ }
1065
+ const mergedOptions = this.#conditionalOptions(options);
1066
+ return this.#call(async () => {
1067
+ const physicalKey = this.#path(key);
1068
+ const result = await adapter.uploadConditional(physicalKey, mapBody(body), mergedOptions);
1069
+ if (result.key !== physicalKey) {
1070
+ throw new StorageError('Storage adapter returned an unexpected conditional upload key.', {
1071
+ code: StorageErrorCode.PROVIDER,
1072
+ key,
1073
+ operation: 'upload',
1074
+ permanent: true,
1075
+ });
1076
+ }
1077
+ const etag = providerEtag(result.etag, key, 'upload');
1078
+ return {
1079
+ ...result,
1080
+ ...(etag === undefined ? {} : { etag }),
1081
+ key,
1082
+ };
1083
+ });
1084
+ }
392
1085
  async download(key, options) {
1086
+ this.#assertLogicalKey(key);
393
1087
  return this.#call(async () => {
394
1088
  const file = await this.#files.download(key, downloadOptions(options));
395
1089
  return {
396
- ...metadataOf(file),
1090
+ ...metadataOf(file, 'download'),
397
1091
  body: normalizeDownloadStream(file.stream()),
398
1092
  };
399
1093
  });
400
1094
  }
1095
+ downloadConditional(key, options) {
1096
+ if (options.condition.etag !== undefined &&
1097
+ !isCanonicalStorageEtag(options.condition.etag)) {
1098
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'download'));
1099
+ }
1100
+ const adapter = this.#conditionalRead;
1101
+ if (adapter === undefined ||
1102
+ (options.condition.etag !== undefined && !adapter.conditionalRead.etag) ||
1103
+ (options.condition.version !== undefined &&
1104
+ !adapter.conditionalRead.version)) {
1105
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional read.`, {
1106
+ code: StorageErrorCode.NOT_SUPPORTED,
1107
+ key,
1108
+ operation: 'download',
1109
+ permanent: true,
1110
+ }));
1111
+ }
1112
+ return this.#call(async () => {
1113
+ const physicalKey = this.#path(key);
1114
+ const object = await adapter.downloadConditional(physicalKey, this.#conditionalOptions(options));
1115
+ if (object.key !== physicalKey) {
1116
+ await object.body.cancel().catch(() => undefined);
1117
+ throw new StorageError('Storage adapter returned an unexpected conditional download key.', {
1118
+ code: StorageErrorCode.PROVIDER,
1119
+ key,
1120
+ operation: 'download',
1121
+ permanent: true,
1122
+ });
1123
+ }
1124
+ let etag;
1125
+ try {
1126
+ etag = providerEtag(object.etag, key, 'download');
1127
+ }
1128
+ catch (error) {
1129
+ await object.body.cancel(error).catch(() => undefined);
1130
+ throw error;
1131
+ }
1132
+ return {
1133
+ ...object,
1134
+ body: normalizeDownloadStream(object.body),
1135
+ ...(etag === undefined ? {} : { etag }),
1136
+ key,
1137
+ name: key.split('/').at(-1) ?? key,
1138
+ };
1139
+ });
1140
+ }
401
1141
  async head(key, options) {
402
- return this.#call(async () => metadataOf(await this.#files.head(key, operationOptions(options))));
1142
+ this.#assertLogicalKey(key);
1143
+ return this.#call(async () => metadataOf(await this.#files.head(key, operationOptions(options)), 'head'));
403
1144
  }
404
1145
  exists(key, options) {
1146
+ this.#assertLogicalKey(key);
405
1147
  return this.#call(() => this.#files.exists(key, operationOptions(options)));
406
1148
  }
407
1149
  delete(key, options) {
1150
+ this.#assertLogicalKey(key);
408
1151
  return this.#call(async () => {
409
1152
  await this.#files.delete(key, operationOptions(options));
410
1153
  });
411
1154
  }
1155
+ deleteConditional(key, options) {
1156
+ if (!isCanonicalStorageEtag(options.condition.etag)) {
1157
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'delete'));
1158
+ }
1159
+ if (this.#readOnly) {
1160
+ return Promise.reject(new StorageError(`Cannot call deleteConditional() on a read-only storage adapter.`, {
1161
+ code: StorageErrorCode.READ_ONLY,
1162
+ key,
1163
+ operation: 'delete',
1164
+ permanent: true,
1165
+ }));
1166
+ }
1167
+ const adapter = this.#conditionalDelete;
1168
+ if (adapter === undefined || !adapter.conditionalDelete.etag) {
1169
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional delete.`, {
1170
+ code: StorageErrorCode.NOT_SUPPORTED,
1171
+ key,
1172
+ operation: 'delete',
1173
+ permanent: true,
1174
+ }));
1175
+ }
1176
+ const mergedOptions = this.#conditionalOptions(options);
1177
+ return this.#call(() => adapter.deleteConditional(this.#path(key), mergedOptions));
1178
+ }
412
1179
  copy(sourceKey, destinationKey, options) {
1180
+ this.#assertLogicalKey(sourceKey);
1181
+ this.#assertLogicalKey(destinationKey);
413
1182
  return this.#call(() => this.#files.copy(sourceKey, destinationKey, operationOptions(options)));
414
1183
  }
415
1184
  move(sourceKey, destinationKey, options) {
1185
+ this.#assertLogicalKey(sourceKey);
1186
+ this.#assertLogicalKey(destinationKey);
416
1187
  return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
417
1188
  }
418
1189
  promote(sourceKey, destinationKey, options) {
1190
+ const destination = options.destination;
1191
+ if (destination !== undefined &&
1192
+ (typeof destination !== 'object' ||
1193
+ destination === null ||
1194
+ (destination.type !== 'create' && destination.type !== 'replace'))) {
1195
+ return Promise.reject(new StorageError('destination.type must be "create" or "replace".', {
1196
+ code: StorageErrorCode.INVALID_ARGUMENT,
1197
+ key: destinationKey,
1198
+ operation: 'promote',
1199
+ permanent: true,
1200
+ }));
1201
+ }
1202
+ if (options.sourceEtag !== undefined &&
1203
+ !isCanonicalStorageEtag(options.sourceEtag)) {
1204
+ return Promise.reject(invalidConditionalEtag('sourceEtag', sourceKey, 'promote'));
1205
+ }
1206
+ if (destination?.type === 'replace' &&
1207
+ !isCanonicalStorageEtag(destination.etag)) {
1208
+ return Promise.reject(invalidConditionalEtag('destination.etag', destinationKey, 'promote'));
1209
+ }
1210
+ if (this.#readOnly) {
1211
+ return Promise.reject(new StorageError(`Cannot call promote() on a read-only storage adapter.`, {
1212
+ code: StorageErrorCode.READ_ONLY,
1213
+ key: sourceKey,
1214
+ operation: 'promote',
1215
+ permanent: true,
1216
+ }));
1217
+ }
419
1218
  const adapter = this.#conditionalCopy;
420
- if (adapter === undefined || !adapter.conditionalCopy.supported) {
1219
+ if (adapter === undefined ||
1220
+ (options.sourceEtag !== undefined &&
1221
+ adapter.conditionalCopySource?.etag !== true) ||
1222
+ (options.sourceVersion !== undefined &&
1223
+ adapter.conditionalCopySource?.version !== true) ||
1224
+ (options.destination?.type === 'create' &&
1225
+ adapter.conditionalCopyDestination?.create !== true) ||
1226
+ (options.destination?.type === 'replace' &&
1227
+ adapter.conditionalCopyDestination?.replace !== true) ||
1228
+ (options.destination !== undefined &&
1229
+ (options.sourceEtag !== undefined ||
1230
+ options.sourceVersion !== undefined) &&
1231
+ adapter.conditionalCopyDestination?.atomicWithSource !== true)) {
421
1232
  return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
422
1233
  code: StorageErrorCode.NOT_SUPPORTED,
423
1234
  key: sourceKey,
@@ -425,13 +1236,14 @@ export class FilesSdkStorageDriver {
425
1236
  permanent: true,
426
1237
  }));
427
1238
  }
428
- return this.#call(() => adapter.promote(sourceKey, destinationKey, options));
1239
+ return this.#call(() => adapter.promote(this.#path(sourceKey), this.#path(destinationKey), this.#conditionalOptions(options)));
429
1240
  }
430
1241
  async list(options) {
1242
+ this.#assertListPhysicalPrefix(options?.prefix);
431
1243
  return this.#call(async () => {
432
1244
  const result = await this.#files.list(listOptions(options));
433
1245
  return {
434
- items: result.items.map(metadataOf),
1246
+ items: result.items.map((file) => metadataOf(file, 'list')),
435
1247
  ...(result.cursor !== undefined && { cursor: result.cursor }),
436
1248
  ...(result.prefixes !== undefined && {
437
1249
  prefixes: [...result.prefixes],
@@ -445,7 +1257,7 @@ export class FilesSdkStorageDriver {
445
1257
  async *#search(pattern, options) {
446
1258
  try {
447
1259
  for await (const file of this.#files.search(pattern, searchOptions(options))) {
448
- yield metadataOf(file);
1260
+ yield metadataOf(file, 'search');
449
1261
  }
450
1262
  }
451
1263
  catch (error) {
@@ -453,9 +1265,11 @@ export class FilesSdkStorageDriver {
453
1265
  }
454
1266
  }
455
1267
  signDownload(key, options) {
1268
+ this.#assertLogicalKey(key);
456
1269
  return this.#call(() => this.#files.url(key, signedDownloadOptions(options)));
457
1270
  }
458
1271
  signUpload(key, options) {
1272
+ this.#assertLogicalKey(key);
459
1273
  return this.#call(() => this.#files.signedUploadUrl(key, signedUploadOptions(options)));
460
1274
  }
461
1275
  async #call(operation) {
@@ -466,6 +1280,72 @@ export class FilesSdkStorageDriver {
466
1280
  throw mapFilesSdkError(error);
467
1281
  }
468
1282
  }
1283
+ #path(key) {
1284
+ if (typeof key !== 'string' || key.length === 0) {
1285
+ throw new StorageError('key must be a non-empty string.', {
1286
+ code: StorageErrorCode.INVALID_ARGUMENT,
1287
+ permanent: true,
1288
+ });
1289
+ }
1290
+ if (key.includes('\0')) {
1291
+ throw new StorageError('key must not contain null bytes.', {
1292
+ code: StorageErrorCode.INVALID_ARGUMENT,
1293
+ key,
1294
+ permanent: true,
1295
+ });
1296
+ }
1297
+ const normalized = key.replace(/^\/+/u, '');
1298
+ if (normalized
1299
+ .split('/')
1300
+ .some((segment) => segment === '.' || segment === '..')) {
1301
+ throw new StorageError('key must not contain . or .. path segments.', {
1302
+ code: StorageErrorCode.INVALID_ARGUMENT,
1303
+ key,
1304
+ permanent: true,
1305
+ });
1306
+ }
1307
+ const physicalKey = this.#prefix.length === 0 ? key : `${this.#prefix}/${normalized}`;
1308
+ this.#assertPhysicalKeyBudget(physicalKey, key);
1309
+ return physicalKey;
1310
+ }
1311
+ #assertLogicalKey(key) {
1312
+ this.#path(key);
1313
+ }
1314
+ #assertListPhysicalPrefix(prefix) {
1315
+ if (prefix !== undefined && prefix.length > 0) {
1316
+ this.#assertLogicalKey(prefix);
1317
+ return;
1318
+ }
1319
+ if (this.#prefix.length > 0) {
1320
+ this.#assertPhysicalKeyBudget(`${this.#prefix}/`);
1321
+ }
1322
+ }
1323
+ #physicalKeyExceedsBudget(physicalKey) {
1324
+ const maxBytes = this.#physicalKey?.physicalKey.maxBytes;
1325
+ return (maxBytes !== undefined &&
1326
+ new TextEncoder().encode(physicalKey).byteLength > maxBytes);
1327
+ }
1328
+ #assertPhysicalKeyBudget(physicalKey, logicalKey) {
1329
+ const maxBytes = this.#physicalKey?.physicalKey.maxBytes;
1330
+ if (maxBytes === undefined || !this.#physicalKeyExceedsBudget(physicalKey))
1331
+ return;
1332
+ throw new StorageError(`The combined storage prefix and key exceed the provider's ${maxBytes}-byte physical-key limit.`, {
1333
+ code: StorageErrorCode.LIMIT_EXCEEDED,
1334
+ ...(logicalKey !== undefined && { key: logicalKey }),
1335
+ permanent: true,
1336
+ });
1337
+ }
1338
+ #conditionalOptions(options) {
1339
+ return {
1340
+ ...options,
1341
+ ...(options.retries === undefined &&
1342
+ this.#retries !== undefined && { retries: this.#retries }),
1343
+ ...(options.signal === undefined &&
1344
+ this.#signal !== undefined && { signal: this.#signal }),
1345
+ ...(options.timeout === undefined &&
1346
+ this.#timeout !== undefined && { timeout: this.#timeout }),
1347
+ };
1348
+ }
469
1349
  }
470
1350
  export function createFilesSdkDriver(options) {
471
1351
  return new FilesSdkStorageDriver(options);