@nestm/storage 0.1.0-alpha.7 → 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 (74) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +217 -18
  3. package/SECURITY.md +34 -5
  4. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +14 -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 +54 -23
  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 +784 -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 +1 -0
  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 +54 -46
  69. package/dist/workspace/storage-workspace.js.map +1 -1
  70. package/dist/workspace/storage-workspace.types.d.ts +10 -2
  71. package/dist/workspace/storage-workspace.types.d.ts.map +1 -1
  72. package/dist/workspace/storage-workspace.types.js +1 -0
  73. package/dist/workspace/storage-workspace.types.js.map +1 -1
  74. package/package.json +3 -3
@@ -1,46 +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
  }
23
- function conditionalMutationAdapterOf(adapter) {
245
+ function conditionalUploadAdapterOf(adapter) {
24
246
  if (typeof adapter !== 'object' ||
25
247
  adapter === null ||
26
- !('conditionalMutation' in adapter) ||
27
- !('uploadConditional' in adapter) ||
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) ||
28
270
  !('deleteConditional' in adapter)) {
29
271
  return undefined;
30
272
  }
31
273
  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' ||
274
+ if (candidate.conditionalDelete === undefined ||
275
+ typeof candidate.conditionalDelete.etag !== 'boolean' ||
39
276
  typeof candidate.deleteConditional !== 'function') {
40
277
  return undefined;
41
278
  }
42
279
  return candidate;
43
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
+ }
44
310
  function signedUploadPolicyAdapterOf(adapter) {
45
311
  if (typeof adapter !== 'object' ||
46
312
  adapter === null ||
@@ -114,19 +380,42 @@ function unwrapFilesError(error) {
114
380
  }
115
381
  return current;
116
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
+ }
117
403
  export function mapFilesSdkError(error) {
118
- if (isStorageError(error)) {
119
- return error;
120
- }
404
+ if (isStorageError(error))
405
+ return sanitizedStorageError(error);
121
406
  if (!isFilesErrorLike(error)) {
122
- return new StorageError(error instanceof Error ? error.message : String(error), {
123
- cause: error,
407
+ return new StorageError('Storage provider operation failed.', {
124
408
  code: StorageErrorCode.PROVIDER,
125
409
  });
126
410
  }
127
411
  const filesError = unwrapFilesError(error);
128
- if (isStorageError(filesError.cause)) {
129
- 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);
130
419
  }
131
420
  let code;
132
421
  if (filesError.timedOut) {
@@ -156,9 +445,8 @@ export function mapFilesSdkError(error) {
156
445
  break;
157
446
  }
158
447
  }
159
- return new StorageError(filesError.message, {
448
+ return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[code], {
160
449
  aborted: filesError.aborted,
161
- cause: filesError.cause ?? error,
162
450
  code,
163
451
  permanent: filesError.permanent,
164
452
  timedOut: filesError.timedOut,
@@ -366,10 +654,34 @@ function signedUploadOptions(options) {
366
654
  ...(options.minSize !== undefined && { minSize: options.minSize }),
367
655
  };
368
656
  }
369
- 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);
370
682
  return {
371
683
  contentType: file.type,
372
- ...(file.etag !== undefined && { etag: file.etag }),
684
+ ...(etag !== undefined && { etag }),
373
685
  key: file.key,
374
686
  ...(file.lastModified !== undefined && {
375
687
  lastModified: new Date(file.lastModified),
@@ -382,9 +694,10 @@ function metadataOf(file) {
382
694
  };
383
695
  }
384
696
  function uploadResultOf(result) {
697
+ const etag = providerEtag(result.etag, result.key, 'upload');
385
698
  return {
386
699
  contentType: result.contentType,
387
- ...(result.etag !== undefined && { etag: result.etag }),
700
+ ...(etag !== undefined && { etag }),
388
701
  key: result.key,
389
702
  ...(result.lastModified !== undefined && {
390
703
  lastModified: new Date(result.lastModified),
@@ -392,11 +705,170 @@ function uploadResultOf(result) {
392
705
  size: result.size,
393
706
  };
394
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
+ }
395
864
  export class FilesSdkStorageDriver {
396
865
  #files;
397
866
  #name;
398
867
  #conditionalCopy;
399
- #conditionalMutation;
868
+ #conditionalDelete;
869
+ #conditionalRead;
870
+ #conditionalUpload;
871
+ #physicalKey;
400
872
  #prefix;
401
873
  #readOnly;
402
874
  #retries;
@@ -405,17 +877,81 @@ export class FilesSdkStorageDriver {
405
877
  #signedUploadPolicy;
406
878
  #signedDownloadPolicy;
407
879
  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);
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);
412
941
  this.#prefix = this.#files.prefix;
413
- this.#readOnly = options.readonly === true;
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;
414
950
  this.#retries = storageRetryOptions(options.retries);
415
951
  this.#signal = options.signal;
416
952
  this.#timeout = options.timeout;
417
- this.#signedUploadPolicy = signedUploadPolicyAdapterOf(options.adapter);
418
- this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(options.adapter);
953
+ this.#signedUploadPolicy = signedUploadPolicyAdapterOf(adapterForFiles);
954
+ this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(adapterForFiles);
419
955
  }
420
956
  get name() {
421
957
  return this.#name;
@@ -427,34 +963,78 @@ export class FilesSdkStorageDriver {
427
963
  delimiter: capabilities.delimiter,
428
964
  metadata: capabilities.metadata,
429
965
  rangeRead: capabilities.rangeRead,
430
- resumableUpload: capabilities.multipart,
431
- serverSideCopy: capabilities.serverSideCopy,
432
- ...(this.#conditionalCopy !== undefined && {
433
- 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
+ }),
434
981
  }),
435
- ...(this.#conditionalMutation !== undefined &&
982
+ ...(this.#conditionalDelete !== undefined &&
436
983
  !this.#readOnly && {
437
- conditionalMutation: {
438
- ...this.#conditionalMutation.conditionalMutation,
984
+ conditionalDelete: {
985
+ ...this.#conditionalDelete.conditionalDelete,
439
986
  },
440
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 },
1012
+ }),
441
1013
  signedDownload: { ...capabilities.signedUrl },
442
1014
  ...(this.#signedDownloadPolicy !== undefined && {
443
1015
  signedDownloadPolicy: {
444
1016
  ...this.#signedDownloadPolicy.signedDownloadPolicy,
445
1017
  },
446
1018
  }),
447
- signedUpload: 'runtime',
448
- ...(this.#signedUploadPolicy !== undefined && {
449
- signedUploadPolicy: { ...this.#signedUploadPolicy.signedUploadPolicy },
1019
+ signedUpload: this.#readOnly ? false : 'runtime',
1020
+ ...(this.#signedUploadPolicy !== undefined &&
1021
+ !this.#readOnly && {
1022
+ signedUploadPolicy: {
1023
+ ...this.#signedUploadPolicy.signedUploadPolicy,
1024
+ },
450
1025
  }),
451
- nativeUploadProgress: capabilities.uploadProgress,
1026
+ nativeUploadProgress: !this.#readOnly && capabilities.uploadProgress,
452
1027
  };
453
1028
  }
454
1029
  async upload(key, body, options) {
1030
+ this.#assertLogicalKey(key);
455
1031
  return this.#call(async () => uploadResultOf(await this.#files.upload(key, mapBody(body), uploadOptions(options))));
456
1032
  }
457
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
+ }
458
1038
  if (this.#readOnly) {
459
1039
  return Promise.reject(new StorageError(`Cannot call uploadConditional() on a read-only storage adapter.`, {
460
1040
  code: StorageErrorCode.READ_ONLY,
@@ -463,8 +1043,18 @@ export class FilesSdkStorageDriver {
463
1043
  permanent: true,
464
1044
  }));
465
1045
  }
466
- const adapter = this.#conditionalMutation;
467
- if (adapter === undefined) {
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) {
468
1058
  return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional upload.`, {
469
1059
  code: StorageErrorCode.NOT_SUPPORTED,
470
1060
  key,
@@ -484,30 +1074,88 @@ export class FilesSdkStorageDriver {
484
1074
  permanent: true,
485
1075
  });
486
1076
  }
487
- return { ...result, key };
1077
+ const etag = providerEtag(result.etag, key, 'upload');
1078
+ return {
1079
+ ...result,
1080
+ ...(etag === undefined ? {} : { etag }),
1081
+ key,
1082
+ };
488
1083
  });
489
1084
  }
490
1085
  async download(key, options) {
1086
+ this.#assertLogicalKey(key);
491
1087
  return this.#call(async () => {
492
1088
  const file = await this.#files.download(key, downloadOptions(options));
493
1089
  return {
494
- ...metadataOf(file),
1090
+ ...metadataOf(file, 'download'),
495
1091
  body: normalizeDownloadStream(file.stream()),
496
1092
  };
497
1093
  });
498
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
+ }
499
1141
  async head(key, options) {
500
- 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'));
501
1144
  }
502
1145
  exists(key, options) {
1146
+ this.#assertLogicalKey(key);
503
1147
  return this.#call(() => this.#files.exists(key, operationOptions(options)));
504
1148
  }
505
1149
  delete(key, options) {
1150
+ this.#assertLogicalKey(key);
506
1151
  return this.#call(async () => {
507
1152
  await this.#files.delete(key, operationOptions(options));
508
1153
  });
509
1154
  }
510
1155
  deleteConditional(key, options) {
1156
+ if (!isCanonicalStorageEtag(options.condition.etag)) {
1157
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'delete'));
1158
+ }
511
1159
  if (this.#readOnly) {
512
1160
  return Promise.reject(new StorageError(`Cannot call deleteConditional() on a read-only storage adapter.`, {
513
1161
  code: StorageErrorCode.READ_ONLY,
@@ -516,8 +1164,8 @@ export class FilesSdkStorageDriver {
516
1164
  permanent: true,
517
1165
  }));
518
1166
  }
519
- const adapter = this.#conditionalMutation;
520
- if (adapter === undefined) {
1167
+ const adapter = this.#conditionalDelete;
1168
+ if (adapter === undefined || !adapter.conditionalDelete.etag) {
521
1169
  return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional delete.`, {
522
1170
  code: StorageErrorCode.NOT_SUPPORTED,
523
1171
  key,
@@ -529,14 +1177,58 @@ export class FilesSdkStorageDriver {
529
1177
  return this.#call(() => adapter.deleteConditional(this.#path(key), mergedOptions));
530
1178
  }
531
1179
  copy(sourceKey, destinationKey, options) {
1180
+ this.#assertLogicalKey(sourceKey);
1181
+ this.#assertLogicalKey(destinationKey);
532
1182
  return this.#call(() => this.#files.copy(sourceKey, destinationKey, operationOptions(options)));
533
1183
  }
534
1184
  move(sourceKey, destinationKey, options) {
1185
+ this.#assertLogicalKey(sourceKey);
1186
+ this.#assertLogicalKey(destinationKey);
535
1187
  return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
536
1188
  }
537
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
+ }
538
1218
  const adapter = this.#conditionalCopy;
539
- 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)) {
540
1232
  return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
541
1233
  code: StorageErrorCode.NOT_SUPPORTED,
542
1234
  key: sourceKey,
@@ -544,13 +1236,14 @@ export class FilesSdkStorageDriver {
544
1236
  permanent: true,
545
1237
  }));
546
1238
  }
547
- return this.#call(() => adapter.promote(sourceKey, destinationKey, options));
1239
+ return this.#call(() => adapter.promote(this.#path(sourceKey), this.#path(destinationKey), this.#conditionalOptions(options)));
548
1240
  }
549
1241
  async list(options) {
1242
+ this.#assertListPhysicalPrefix(options?.prefix);
550
1243
  return this.#call(async () => {
551
1244
  const result = await this.#files.list(listOptions(options));
552
1245
  return {
553
- items: result.items.map(metadataOf),
1246
+ items: result.items.map((file) => metadataOf(file, 'list')),
554
1247
  ...(result.cursor !== undefined && { cursor: result.cursor }),
555
1248
  ...(result.prefixes !== undefined && {
556
1249
  prefixes: [...result.prefixes],
@@ -564,7 +1257,7 @@ export class FilesSdkStorageDriver {
564
1257
  async *#search(pattern, options) {
565
1258
  try {
566
1259
  for await (const file of this.#files.search(pattern, searchOptions(options))) {
567
- yield metadataOf(file);
1260
+ yield metadataOf(file, 'search');
568
1261
  }
569
1262
  }
570
1263
  catch (error) {
@@ -572,9 +1265,11 @@ export class FilesSdkStorageDriver {
572
1265
  }
573
1266
  }
574
1267
  signDownload(key, options) {
1268
+ this.#assertLogicalKey(key);
575
1269
  return this.#call(() => this.#files.url(key, signedDownloadOptions(options)));
576
1270
  }
577
1271
  signUpload(key, options) {
1272
+ this.#assertLogicalKey(key);
578
1273
  return this.#call(() => this.#files.signedUploadUrl(key, signedUploadOptions(options)));
579
1274
  }
580
1275
  async #call(operation) {
@@ -599,9 +1294,6 @@ export class FilesSdkStorageDriver {
599
1294
  permanent: true,
600
1295
  });
601
1296
  }
602
- if (this.#prefix.length === 0) {
603
- return key;
604
- }
605
1297
  const normalized = key.replace(/^\/+/u, '');
606
1298
  if (normalized
607
1299
  .split('/')
@@ -612,7 +1304,36 @@ export class FilesSdkStorageDriver {
612
1304
  permanent: true,
613
1305
  });
614
1306
  }
615
- return `${this.#prefix}/${normalized}`;
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
+ });
616
1337
  }
617
1338
  #conditionalOptions(options) {
618
1339
  return {