@nestm/storage 0.1.0-alpha.7 → 0.1.0-alpha.9

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