@nestm/storage 0.1.0-alpha.0 → 0.1.0-alpha.10

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 (122) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/README.md +724 -25
  3. package/SECURITY.md +101 -0
  4. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +91 -0
  5. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
  6. package/dist/ai-sdk/ai-sdk-workspace-tools.js +491 -0
  7. package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
  8. package/dist/ai-sdk/index.d.ts +2 -0
  9. package/dist/ai-sdk/index.d.ts.map +1 -0
  10. package/dist/ai-sdk/index.js +2 -0
  11. package/dist/ai-sdk/index.js.map +1 -0
  12. package/dist/core/index.d.ts +6 -0
  13. package/dist/core/index.d.ts.map +1 -0
  14. package/dist/core/index.js +4 -0
  15. package/dist/core/index.js.map +1 -0
  16. package/dist/files-sdk/files-sdk.driver.d.ts +85 -3
  17. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  18. package/dist/files-sdk/files-sdk.driver.js +1437 -37
  19. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  20. package/dist/files-sdk/fs/index.d.ts +42 -0
  21. package/dist/files-sdk/fs/index.d.ts.map +1 -0
  22. package/dist/files-sdk/fs/index.js +1117 -0
  23. package/dist/files-sdk/fs/index.js.map +1 -0
  24. package/dist/files-sdk/index.d.ts +1 -1
  25. package/dist/files-sdk/index.d.ts.map +1 -1
  26. package/dist/files-sdk/index.js.map +1 -1
  27. package/dist/files-sdk/provider/index.d.ts +58 -0
  28. package/dist/files-sdk/provider/index.d.ts.map +1 -0
  29. package/dist/files-sdk/provider/index.js +147 -0
  30. package/dist/files-sdk/provider/index.js.map +1 -0
  31. package/dist/files-sdk/s3/construction-metadata.d.ts +9 -0
  32. package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
  33. package/dist/files-sdk/s3/construction-metadata.js +24 -0
  34. package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
  35. package/dist/files-sdk/s3/index.d.ts +54 -0
  36. package/dist/files-sdk/s3/index.d.ts.map +1 -0
  37. package/dist/files-sdk/s3/index.js +1505 -0
  38. package/dist/files-sdk/s3/index.js.map +1 -0
  39. package/dist/gateway/index.d.ts +1 -1
  40. package/dist/gateway/index.d.ts.map +1 -1
  41. package/dist/gateway/index.js.map +1 -1
  42. package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -1
  43. package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -1
  44. package/dist/gateway/storage-gateway.controller.d.ts +12 -11
  45. package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
  46. package/dist/gateway/storage-gateway.controller.js +240 -70
  47. package/dist/gateway/storage-gateway.controller.js.map +1 -1
  48. package/dist/gateway/storage-gateway.guard.d.ts.map +1 -1
  49. package/dist/gateway/storage-gateway.guard.js.map +1 -1
  50. package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
  51. package/dist/gateway/storage-gateway.module.js +60 -1
  52. package/dist/gateway/storage-gateway.module.js.map +1 -1
  53. package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
  54. package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
  55. package/dist/gateway/storage-gateway.tokens.js +1 -0
  56. package/dist/gateway/storage-gateway.tokens.js.map +1 -1
  57. package/dist/gateway/storage-gateway.types.d.ts +53 -10
  58. package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
  59. package/dist/gateway/storage-gateway.types.js.map +1 -1
  60. package/dist/index.d.ts +1 -5
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1 -3
  63. package/dist/index.js.map +1 -1
  64. package/dist/inject-storage.decorator.js.map +1 -1
  65. package/dist/storage-etag.d.ts +13 -0
  66. package/dist/storage-etag.d.ts.map +1 -0
  67. package/dist/storage-etag.js +31 -0
  68. package/dist/storage-etag.js.map +1 -0
  69. package/dist/storage-upload-control.d.ts.map +1 -1
  70. package/dist/storage.client.d.ts +15 -3
  71. package/dist/storage.client.d.ts.map +1 -1
  72. package/dist/storage.client.js +204 -2
  73. package/dist/storage.client.js.map +1 -1
  74. package/dist/storage.driver.d.ts +28 -1
  75. package/dist/storage.driver.d.ts.map +1 -1
  76. package/dist/storage.driver.js.map +1 -1
  77. package/dist/storage.error.d.ts +17 -10
  78. package/dist/storage.error.d.ts.map +1 -1
  79. package/dist/storage.error.js +47 -1
  80. package/dist/storage.error.js.map +1 -1
  81. package/dist/storage.module.d.ts.map +1 -1
  82. package/dist/storage.module.js.map +1 -1
  83. package/dist/storage.service.d.ts.map +1 -1
  84. package/dist/storage.service.js +28 -3
  85. package/dist/storage.service.js.map +1 -1
  86. package/dist/storage.tokens.js.map +1 -1
  87. package/dist/storage.types.d.ts +150 -1
  88. package/dist/storage.types.d.ts.map +1 -1
  89. package/dist/storage.types.js.map +1 -1
  90. package/dist/testing/index.d.ts +1 -0
  91. package/dist/testing/index.d.ts.map +1 -1
  92. package/dist/testing/index.js +1 -0
  93. package/dist/testing/index.js.map +1 -1
  94. package/dist/testing/provider-conformance.d.ts +65 -0
  95. package/dist/testing/provider-conformance.d.ts.map +1 -0
  96. package/dist/testing/provider-conformance.js +869 -0
  97. package/dist/testing/provider-conformance.js.map +1 -0
  98. package/dist/workspace/index.d.ts +5 -0
  99. package/dist/workspace/index.d.ts.map +1 -0
  100. package/dist/workspace/index.js +5 -0
  101. package/dist/workspace/index.js.map +1 -0
  102. package/dist/workspace/storage-workspace.cursor.d.ts +71 -0
  103. package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
  104. package/dist/workspace/storage-workspace.cursor.js +408 -0
  105. package/dist/workspace/storage-workspace.cursor.js.map +1 -0
  106. package/dist/workspace/storage-workspace.d.ts +6 -0
  107. package/dist/workspace/storage-workspace.d.ts.map +1 -0
  108. package/dist/workspace/storage-workspace.error.d.ts +32 -0
  109. package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
  110. package/dist/workspace/storage-workspace.error.js +75 -0
  111. package/dist/workspace/storage-workspace.error.js.map +1 -0
  112. package/dist/workspace/storage-workspace.js +849 -0
  113. package/dist/workspace/storage-workspace.js.map +1 -0
  114. package/dist/workspace/storage-workspace.path.d.ts +9 -0
  115. package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
  116. package/dist/workspace/storage-workspace.path.js +68 -0
  117. package/dist/workspace/storage-workspace.path.js.map +1 -0
  118. package/dist/workspace/storage-workspace.types.d.ts +123 -0
  119. package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
  120. package/dist/workspace/storage-workspace.types.js +22 -0
  121. package/dist/workspace/storage-workspace.types.js.map +1 -0
  122. package/package.json +103 -35
@@ -1,29 +1,609 @@
1
1
  import { Readable } from 'node:stream';
2
- import { Files, FilesError, } from 'files-sdk';
3
- import { StorageError, StorageErrorCode } from '../storage.error.js';
2
+ import { Files, FilesError, handlers, } from 'files-sdk';
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';
5
- function mapFilesError(error) {
6
- if (error instanceof StorageError) {
7
- return error;
6
+ /**
7
+ * Detects caller policy that must not be bypassed by NestM-only conditional
8
+ * fallbacks that Files SDK cannot represent (versions, multipart completion,
9
+ * or a copy with only one side conditioned).
10
+ */
11
+ function hasCallerFilesOperationPolicy(options) {
12
+ const hooks = options.hooks;
13
+ return ((options.plugins?.length ?? 0) > 0 ||
14
+ typeof hooks?.onAction === 'function' ||
15
+ typeof hooks?.onError === 'function' ||
16
+ typeof hooks?.onRetry === 'function' ||
17
+ (options.receipts !== undefined && options.receipts !== false));
18
+ }
19
+ const FILES_SDK_S3_RESERVED_EXTENSION_KEYS = [
20
+ 'conditionalCopyDestination',
21
+ 'conditionalCopySource',
22
+ 'conditionalCreate',
23
+ 'conditionalDelete',
24
+ 'conditionalMultipartCompletion',
25
+ 'conditionalRead',
26
+ 'conditionalReplace',
27
+ 'deleteConditional',
28
+ 'downloadConditional',
29
+ 'physicalKey',
30
+ 'promote',
31
+ 'signedDownloadPolicy',
32
+ 'signedUploadPolicy',
33
+ 'uploadConditional',
34
+ ];
35
+ const FILES_SDK_S3_AUTHORITY_KEYS = [
36
+ ...FILES_SDK_S3_RESERVED_EXTENSION_KEYS,
37
+ 'bucket',
38
+ 'conditional',
39
+ 'copy',
40
+ 'delete',
41
+ 'deleteMany',
42
+ 'download',
43
+ 'exists',
44
+ 'head',
45
+ 'list',
46
+ 'move',
47
+ 'reportsUploadProgress',
48
+ 'resumableUpload',
49
+ 'signedUploadUrl',
50
+ 'signedUrl',
51
+ 'supportsCacheControl',
52
+ 'supportsDelimiter',
53
+ 'supportsMetadata',
54
+ 'supportsRange',
55
+ 'supportsServerSideCopy',
56
+ 'upload',
57
+ 'url',
58
+ ];
59
+ const s3AdapterProvenance = new WeakMap();
60
+ const undecoratedS3Adapters = new WeakSet();
61
+ const s3AdapterAuthority = new WeakMap();
62
+ const s3MethodAuthority = new WeakMap();
63
+ const s3AuthorityState = new WeakMap();
64
+ const conflictingS3Authority = Symbol('conflictingS3Authority');
65
+ function adapterRawObject(adapter) {
66
+ try {
67
+ const raw = adapter.raw;
68
+ return (typeof raw === 'object' && raw !== null) ||
69
+ typeof raw === 'function'
70
+ ? raw
71
+ : undefined;
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ function s3AdapterAuthorityTokenOf(adapter) {
78
+ let resolved = s3AdapterAuthority.get(adapter);
79
+ const consider = (token) => {
80
+ if (token === undefined)
81
+ return true;
82
+ if (resolved !== undefined && resolved !== token)
83
+ return false;
84
+ resolved = token;
85
+ return true;
86
+ };
87
+ try {
88
+ const candidate = adapter;
89
+ for (const key of FILES_SDK_S3_AUTHORITY_KEYS) {
90
+ if (!(key in candidate))
91
+ continue;
92
+ const value = candidate[key];
93
+ if (typeof value === 'function' &&
94
+ !consider(s3MethodAuthority.get(value))) {
95
+ return conflictingS3Authority;
96
+ }
97
+ }
98
+ return resolved;
99
+ }
100
+ catch {
101
+ return conflictingS3Authority;
102
+ }
103
+ }
104
+ function bindS3AdapterAuthorityMethods(adapter, token) {
105
+ const candidate = adapter;
106
+ for (const key of FILES_SDK_S3_AUTHORITY_KEYS) {
107
+ if (!(key in candidate))
108
+ continue;
109
+ const value = candidate[key];
110
+ if (typeof value !== 'function')
111
+ continue;
112
+ const existing = s3MethodAuthority.get(value);
113
+ if (existing !== undefined && existing !== token) {
114
+ throw new TypeError('S3 adapter methods have conflicting authority.');
115
+ }
116
+ s3MethodAuthority.set(value, token);
117
+ }
118
+ }
119
+ function attachS3AdapterAuthority(adapter, raw) {
120
+ const existing = s3AdapterAuthorityTokenOf(adapter);
121
+ if (existing === conflictingS3Authority) {
122
+ throw new TypeError('S3 adapter authority is inconsistent.');
123
+ }
124
+ const token = existing ?? Object.freeze({});
125
+ const state = s3AuthorityState.get(token);
126
+ if (state !== undefined && state.raw !== raw) {
127
+ throw new TypeError('S3 adapter authority raw client cannot be changed.');
128
+ }
129
+ if (state === undefined)
130
+ s3AuthorityState.set(token, { raw });
131
+ s3AdapterAuthority.set(adapter, token);
132
+ bindS3AdapterAuthorityMethods(adapter, token);
133
+ return token;
134
+ }
135
+ function setS3AdapterProvenance(target, adapter, provenance) {
136
+ const existing = s3AdapterProvenanceOfRaw(target);
137
+ if (existing !== undefined) {
138
+ if (existing.provenance !== provenance ||
139
+ mismatchedS3Authority(adapter, existing.surface) !== undefined) {
140
+ throw new TypeError('S3 adapter provenance cannot be changed.');
141
+ }
142
+ s3AdapterProvenance.set(target, existing);
143
+ return existing;
144
+ }
145
+ const record = Object.freeze({
146
+ provenance,
147
+ surface: s3AuthoritySnapshot(adapter),
148
+ });
149
+ s3AdapterProvenance.set(target, record);
150
+ return record;
151
+ }
152
+ function s3AdapterProvenanceOfRaw(raw) {
153
+ return s3AdapterProvenance.get(raw);
154
+ }
155
+ function s3AuthoritySnapshot(adapter) {
156
+ const candidate = adapter;
157
+ return Object.freeze(Object.fromEntries(FILES_SDK_S3_AUTHORITY_KEYS.map((key) => {
158
+ const present = key in candidate;
159
+ return [
160
+ key,
161
+ Object.freeze({
162
+ present,
163
+ value: present ? candidate[key] : undefined,
164
+ }),
165
+ ];
166
+ })));
167
+ }
168
+ function mismatchedS3Authority(adapter, snapshot) {
169
+ const candidate = adapter;
170
+ try {
171
+ return FILES_SDK_S3_AUTHORITY_KEYS.find((key) => {
172
+ const present = key in candidate;
173
+ const expected = snapshot[key];
174
+ return (present !== expected.present ||
175
+ !Object.is(present ? candidate[key] : undefined, expected.value));
176
+ });
177
+ }
178
+ catch {
179
+ return FILES_SDK_S3_AUTHORITY_KEYS[0];
180
+ }
181
+ }
182
+ /** Rejects adapters already carrying reserved storage S3 extensions. */
183
+ export function assertFilesSdkS3AdapterHasNoReservedExtensions(adapter) {
184
+ const reserved = filesSdkS3ReservedExtensionOf(adapter);
185
+ if (reserved !== undefined) {
186
+ throw new TypeError(`S3 adapter already defines reserved extension "${reserved}".`);
187
+ }
188
+ }
189
+ function filesSdkS3ReservedExtensionOf(adapter) {
190
+ const candidate = adapter;
191
+ try {
192
+ return FILES_SDK_S3_RESERVED_EXTENSION_KEYS.find((key) => key in candidate);
193
+ }
194
+ catch {
195
+ return FILES_SDK_S3_RESERVED_EXTENSION_KEYS[0];
196
+ }
197
+ }
198
+ /** Records that an S3 adapter was constructed but not provider-decorated. */
199
+ export function markFilesSdkS3AdapterUndecorated(adapter) {
200
+ const raw = adapterRawObject(adapter);
201
+ if (raw === undefined) {
202
+ throw new TypeError('S3 adapter must expose an object raw client.');
203
+ }
204
+ attachS3AdapterAuthority(adapter, raw);
205
+ undecoratedS3Adapters.add(raw);
206
+ }
207
+ /** Records the provider verification state of one decorated S3 adapter. */
208
+ export function markFilesSdkS3AdapterProvenance(adapter, provenance) {
209
+ const raw = adapterRawObject(adapter);
210
+ if (raw === undefined) {
211
+ throw new TypeError('S3 adapter must expose an object raw client.');
212
+ }
213
+ markFilesSdkS3RawClientProvenance(raw, adapter, provenance);
214
+ }
215
+ /** Records final provider verification state on one stable raw S3 client. */
216
+ export function markFilesSdkS3RawClientProvenance(raw, adapter, provenance) {
217
+ const authority = attachS3AdapterAuthority(adapter, raw);
218
+ const record = setS3AdapterProvenance(raw, adapter, provenance);
219
+ s3AuthorityState.set(authority, { raw, record });
220
+ bindS3AdapterAuthorityMethods(adapter, authority);
221
+ }
222
+ function isUndecoratedS3Raw(raw) {
223
+ return undecoratedS3Adapters.has(raw);
224
+ }
225
+ function isStructuralS3Raw(raw) {
226
+ try {
227
+ const candidate = raw;
228
+ return (typeof candidate.send === 'function' &&
229
+ typeof candidate.config === 'object' &&
230
+ candidate.config !== null &&
231
+ candidate.config.serviceId === 'S3');
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ function conditionalCopyAdapterOf(adapter) {
238
+ if (typeof adapter !== 'object' ||
239
+ adapter === null ||
240
+ !('promote' in adapter)) {
241
+ return undefined;
242
+ }
243
+ const candidate = adapter;
244
+ const source = candidate.conditionalCopySource;
245
+ const destination = candidate.conditionalCopyDestination;
246
+ if ((source === undefined && destination === undefined) ||
247
+ (source !== undefined &&
248
+ (typeof source.etag !== 'boolean' ||
249
+ typeof source.version !== 'boolean' ||
250
+ (source.requiresDestinationPredicate !== undefined &&
251
+ typeof source.requiresDestinationPredicate !== 'boolean'))) ||
252
+ (destination !== undefined &&
253
+ (typeof destination.create !== 'boolean' ||
254
+ typeof destination.replace !== 'boolean' ||
255
+ (destination.requiresSourcePredicate !== undefined &&
256
+ typeof destination.requiresSourcePredicate !== 'boolean') ||
257
+ typeof destination.atomicWithSource !== 'boolean')) ||
258
+ typeof candidate.promote !== 'function') {
259
+ return undefined;
260
+ }
261
+ return candidate;
262
+ }
263
+ function conditionalCopyCapabilities(direct, pipeline) {
264
+ const directSource = direct?.conditionalCopySource !== undefined &&
265
+ (direct.conditionalCopySource.etag || direct.conditionalCopySource.version)
266
+ ? direct.conditionalCopySource
267
+ : undefined;
268
+ const directDestination = direct?.conditionalCopyDestination !== undefined &&
269
+ (direct.conditionalCopyDestination.create ||
270
+ direct.conditionalCopyDestination.replace)
271
+ ? direct.conditionalCopyDestination
272
+ : undefined;
273
+ const pipelineSupported = pipeline.sourceEtag &&
274
+ pipeline.atomicSourceDestination &&
275
+ (pipeline.destinationCreate || pipeline.destinationReplace);
276
+ const pipelineCapabilities = pipelineSupported
277
+ ? {
278
+ source: {
279
+ etag: true,
280
+ requiresDestinationPredicate: true,
281
+ version: false,
282
+ },
283
+ destination: {
284
+ atomicWithSource: true,
285
+ create: pipeline.destinationCreate,
286
+ replace: pipeline.destinationReplace,
287
+ requiresSourcePredicate: true,
288
+ },
289
+ }
290
+ : {};
291
+ if (directSource === undefined && directDestination === undefined) {
292
+ return pipelineCapabilities;
293
+ }
294
+ if (!pipelineSupported) {
295
+ return {
296
+ ...(directSource !== undefined && { source: { ...directSource } }),
297
+ ...(directDestination !== undefined && {
298
+ destination: { ...directDestination },
299
+ }),
300
+ };
301
+ }
302
+ // Never splice unrelated one-sided direct declarations onto the paired
303
+ // Files surface. Without both direct halves there is no coherent route for
304
+ // the cross-product the public capability model would otherwise imply.
305
+ if (directSource === undefined || directDestination === undefined) {
306
+ return {
307
+ ...(directSource !== undefined && { source: { ...directSource } }),
308
+ ...(directDestination !== undefined && {
309
+ destination: { ...directDestination },
310
+ }),
311
+ };
312
+ }
313
+ const sourcePredicates = [
314
+ ...(directSource.etag || pipeline.sourceEtag ? ['etag'] : []),
315
+ ...(directSource.version ? ['version'] : []),
316
+ ];
317
+ const destinationPredicates = [
318
+ ...(directDestination.create || pipeline.destinationCreate
319
+ ? ['create']
320
+ : []),
321
+ ...(directDestination.replace || pipeline.destinationReplace
322
+ ? ['replace']
323
+ : []),
324
+ ];
325
+ const supportsDirectSourceAlone = (predicate) => directSource[predicate] &&
326
+ directSource.requiresDestinationPredicate !== true;
327
+ const supportsDirectDestinationAlone = (predicate) => directDestination[predicate] &&
328
+ directDestination.requiresSourcePredicate !== true;
329
+ const sourceAlone = sourcePredicates.map(supportsDirectSourceAlone);
330
+ const destinationAlone = destinationPredicates.map(supportsDirectDestinationAlone);
331
+ // One shared dependency flag cannot express a union where only some source
332
+ // or destination predicate kinds require their counterpart.
333
+ if ((sourceAlone.some(Boolean) && !sourceAlone.every(Boolean)) ||
334
+ (destinationAlone.some(Boolean) && !destinationAlone.every(Boolean))) {
335
+ return {
336
+ source: { ...directSource },
337
+ destination: { ...directDestination },
338
+ };
339
+ }
340
+ const everyPairIsAtomic = sourcePredicates.every((source) => destinationPredicates.every((destination) => {
341
+ const throughPipeline = source === 'etag' &&
342
+ (destination === 'create'
343
+ ? pipeline.destinationCreate
344
+ : pipeline.destinationReplace);
345
+ const throughDirect = directSource[source] &&
346
+ directDestination[destination] &&
347
+ directDestination.atomicWithSource;
348
+ return throughPipeline || throughDirect;
349
+ }));
350
+ if (!everyPairIsAtomic) {
351
+ return {
352
+ source: { ...directSource },
353
+ destination: { ...directDestination },
354
+ };
355
+ }
356
+ return {
357
+ source: {
358
+ etag: sourcePredicates.includes('etag'),
359
+ ...(sourceAlone.every((supported) => !supported) && {
360
+ requiresDestinationPredicate: true,
361
+ }),
362
+ version: sourcePredicates.includes('version'),
363
+ },
364
+ destination: {
365
+ atomicWithSource: true,
366
+ create: destinationPredicates.includes('create'),
367
+ replace: destinationPredicates.includes('replace'),
368
+ ...(destinationAlone.every((supported) => !supported) && {
369
+ requiresSourcePredicate: true,
370
+ }),
371
+ },
372
+ };
373
+ }
374
+ function conditionalUploadAdapterOf(adapter) {
375
+ if (typeof adapter !== 'object' ||
376
+ adapter === null ||
377
+ !('uploadConditional' in adapter)) {
378
+ return undefined;
379
+ }
380
+ const candidate = adapter;
381
+ const create = candidate.conditionalCreate;
382
+ const replace = candidate.conditionalReplace;
383
+ const multipart = candidate.conditionalMultipartCompletion;
384
+ if ((create === undefined && replace === undefined) ||
385
+ (create !== undefined && typeof create.resultEtag !== 'boolean') ||
386
+ (replace !== undefined && typeof replace.resultEtag !== 'boolean') ||
387
+ (multipart !== undefined &&
388
+ (typeof multipart.create !== 'boolean' ||
389
+ typeof multipart.replace !== 'boolean')) ||
390
+ typeof candidate.uploadConditional !== 'function') {
391
+ return undefined;
392
+ }
393
+ return candidate;
394
+ }
395
+ function conditionalDeleteAdapterOf(adapter) {
396
+ if (typeof adapter !== 'object' ||
397
+ adapter === null ||
398
+ !('conditionalDelete' in adapter) ||
399
+ !('deleteConditional' in adapter)) {
400
+ return undefined;
401
+ }
402
+ const candidate = adapter;
403
+ if (candidate.conditionalDelete === undefined ||
404
+ typeof candidate.conditionalDelete.etag !== 'boolean' ||
405
+ typeof candidate.deleteConditional !== 'function') {
406
+ return undefined;
407
+ }
408
+ return candidate;
409
+ }
410
+ function conditionalReadAdapterOf(adapter) {
411
+ if (typeof adapter !== 'object' ||
412
+ adapter === null ||
413
+ !('conditionalRead' in adapter) ||
414
+ !('downloadConditional' in adapter)) {
415
+ return undefined;
416
+ }
417
+ const candidate = adapter;
418
+ if (candidate.conditionalRead === undefined ||
419
+ typeof candidate.conditionalRead.etag !== 'boolean' ||
420
+ typeof candidate.conditionalRead.version !== 'boolean' ||
421
+ typeof candidate.downloadConditional !== 'function') {
422
+ return undefined;
423
+ }
424
+ return candidate;
425
+ }
426
+ function physicalKeyAdapterOf(adapter) {
427
+ if (typeof adapter !== 'object' ||
428
+ adapter === null ||
429
+ !('physicalKey' in adapter)) {
430
+ return undefined;
431
+ }
432
+ const candidate = adapter;
433
+ const maxBytes = candidate.physicalKey?.maxBytes;
434
+ if (!Number.isSafeInteger(maxBytes) || (maxBytes ?? 0) <= 0) {
435
+ return undefined;
436
+ }
437
+ return candidate;
438
+ }
439
+ function signedUploadPolicyAdapterOf(adapter) {
440
+ if (typeof adapter !== 'object' ||
441
+ adapter === null ||
442
+ !('signedUploadPolicy' in adapter)) {
443
+ return undefined;
444
+ }
445
+ const candidate = adapter;
446
+ const capability = candidate.signedUploadPolicy;
447
+ if (capability === undefined ||
448
+ typeof capability.contentType !== 'boolean' ||
449
+ typeof capability.sizeRange !== 'boolean') {
450
+ return undefined;
451
+ }
452
+ return candidate;
453
+ }
454
+ function signedDownloadPolicyAdapterOf(adapter) {
455
+ if (typeof adapter !== 'object' ||
456
+ adapter === null ||
457
+ !('signedDownloadPolicy' in adapter)) {
458
+ return undefined;
459
+ }
460
+ const candidate = adapter;
461
+ const capability = candidate.signedDownloadPolicy;
462
+ if (capability === undefined || typeof capability.expiresIn !== 'boolean') {
463
+ return undefined;
464
+ }
465
+ return candidate;
466
+ }
467
+ function isFilesErrorCode(value) {
468
+ return (value === 'NotFound' ||
469
+ value === 'Unauthorized' ||
470
+ value === 'Conflict' ||
471
+ value === 'ReadOnly' ||
472
+ value === 'Provider');
473
+ }
474
+ function isFilesErrorLike(error) {
475
+ if (error instanceof FilesError) {
476
+ return true;
477
+ }
478
+ if (!(error instanceof Error)) {
479
+ return false;
480
+ }
481
+ try {
482
+ return (error.name === 'FilesError' &&
483
+ typeof error.message === 'string' &&
484
+ 'code' in error &&
485
+ isFilesErrorCode(error.code) &&
486
+ 'aborted' in error &&
487
+ typeof error.aborted === 'boolean' &&
488
+ 'timedOut' in error &&
489
+ typeof error.timedOut === 'boolean' &&
490
+ 'permanent' in error &&
491
+ typeof error.permanent === 'boolean' &&
492
+ (!('applied' in error) || typeof error.applied === 'boolean') &&
493
+ (!('appliedEtag' in error) || typeof error.appliedEtag === 'string'));
494
+ }
495
+ catch {
496
+ return false;
497
+ }
498
+ }
499
+ function unwrapFilesError(error) {
500
+ const seen = new Set();
501
+ let current = error;
502
+ while (current.code === 'Provider' &&
503
+ !current.aborted &&
504
+ !current.timedOut &&
505
+ !current.permanent &&
506
+ current.applied !== true &&
507
+ isFilesErrorLike(current.cause) &&
508
+ current.message === current.cause.message &&
509
+ !seen.has(current.cause)) {
510
+ seen.add(current);
511
+ current = current.cause;
512
+ }
513
+ return current;
514
+ }
515
+ const PUBLIC_PROVIDER_ERROR_MESSAGES = Object.freeze({
516
+ [StorageErrorCode.NOT_FOUND]: 'Storage provider object was not found.',
517
+ [StorageErrorCode.UNAUTHORIZED]: 'Storage provider operation was unauthorized.',
518
+ [StorageErrorCode.CONFLICT]: 'Storage provider operation conflicted with current state.',
519
+ [StorageErrorCode.READ_ONLY]: 'Storage provider is read-only.',
520
+ [StorageErrorCode.INVALID_ARGUMENT]: 'Storage provider rejected an invalid argument.',
521
+ [StorageErrorCode.NOT_SUPPORTED]: 'Storage provider operation is not supported.',
522
+ [StorageErrorCode.ABORTED]: 'Storage provider operation was aborted.',
523
+ [StorageErrorCode.TIMEOUT]: 'Storage provider operation timed out.',
524
+ [StorageErrorCode.LIMIT_EXCEEDED]: 'Storage provider operation exceeded a limit.',
525
+ [StorageErrorCode.PROVIDER]: 'Storage provider operation failed.',
526
+ });
527
+ function sanitizedStorageError(error) {
528
+ return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[error.code], {
529
+ aborted: error.aborted,
530
+ applied: error.applied,
531
+ ...(isCanonicalStorageEtag(error.appliedEtag) && {
532
+ appliedEtag: error.appliedEtag,
533
+ }),
534
+ code: error.code,
535
+ permanent: error.permanent,
536
+ timedOut: error.timedOut,
537
+ });
538
+ }
539
+ function filesErrorCodeOfStorage(error) {
540
+ switch (error.code) {
541
+ case StorageErrorCode.NOT_FOUND:
542
+ return 'NotFound';
543
+ case StorageErrorCode.UNAUTHORIZED:
544
+ return 'Unauthorized';
545
+ case StorageErrorCode.CONFLICT:
546
+ return error.permanent ? 'Conflict' : 'Provider';
547
+ case StorageErrorCode.READ_ONLY:
548
+ return 'ReadOnly';
549
+ default:
550
+ return 'Provider';
8
551
  }
9
- if (!(error instanceof FilesError)) {
10
- return new StorageError(error instanceof Error ? error.message : String(error), {
11
- cause: error,
552
+ }
553
+ /** Converts NestM adapter failures before they enter the Files retry pipeline. */
554
+ export function mapStorageErrorToFilesSdkError(error) {
555
+ if (error instanceof FilesError)
556
+ return error;
557
+ if (!isStorageError(error))
558
+ return FilesError.wrap(error);
559
+ const sanitized = sanitizedStorageError(error);
560
+ return new FilesError(filesErrorCodeOfStorage(sanitized), sanitized.message, sanitized, {
561
+ aborted: sanitized.aborted,
562
+ applied: sanitized.applied,
563
+ ...(isCanonicalStorageEtag(sanitized.appliedEtag) && {
564
+ appliedEtag: sanitized.appliedEtag,
565
+ }),
566
+ permanent: sanitized.permanent,
567
+ timedOut: sanitized.timedOut,
568
+ });
569
+ }
570
+ export function mapFilesSdkError(error) {
571
+ if (isStorageError(error))
572
+ return sanitizedStorageError(error);
573
+ if (!isFilesErrorLike(error)) {
574
+ return new StorageError('Storage provider operation failed.', {
12
575
  code: StorageErrorCode.PROVIDER,
13
576
  });
14
577
  }
15
- if (error.cause instanceof StorageError) {
16
- return error.cause;
578
+ const filesError = unwrapFilesError(error);
579
+ if (filesError.code === 'Provider' &&
580
+ !filesError.aborted &&
581
+ !filesError.timedOut &&
582
+ isStorageError(filesError.cause) &&
583
+ filesError.message === filesError.cause.message) {
584
+ const storageCause = filesError.cause;
585
+ return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[storageCause.code], {
586
+ aborted: filesError.aborted || storageCause.aborted,
587
+ applied: filesError.applied === true || storageCause.applied,
588
+ ...(isCanonicalStorageEtag(filesError.appliedEtag)
589
+ ? { appliedEtag: filesError.appliedEtag }
590
+ : isCanonicalStorageEtag(storageCause.appliedEtag)
591
+ ? { appliedEtag: storageCause.appliedEtag }
592
+ : {}),
593
+ code: storageCause.code,
594
+ permanent: filesError.permanent || storageCause.permanent,
595
+ timedOut: filesError.timedOut || storageCause.timedOut,
596
+ });
17
597
  }
18
598
  let code;
19
- if (error.timedOut) {
599
+ if (filesError.timedOut) {
20
600
  code = StorageErrorCode.TIMEOUT;
21
601
  }
22
- else if (error.aborted) {
602
+ else if (filesError.aborted) {
23
603
  code = StorageErrorCode.ABORTED;
24
604
  }
25
605
  else {
26
- switch (error.code) {
606
+ switch (filesError.code) {
27
607
  case 'NotFound':
28
608
  code = StorageErrorCode.NOT_FOUND;
29
609
  break;
@@ -37,18 +617,21 @@ function mapFilesError(error) {
37
617
  code = StorageErrorCode.READ_ONLY;
38
618
  break;
39
619
  case 'Provider':
40
- code = /(?:not supported|does not support|unsupported)/iu.test(error.message)
620
+ code = /(?:not supported|does not support|unsupported)/iu.test(filesError.message)
41
621
  ? StorageErrorCode.NOT_SUPPORTED
42
622
  : StorageErrorCode.PROVIDER;
43
623
  break;
44
624
  }
45
625
  }
46
- return new StorageError(error.message, {
47
- aborted: error.aborted,
48
- cause: error.cause ?? error,
626
+ return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[code], {
627
+ aborted: filesError.aborted,
628
+ applied: filesError.applied === true,
629
+ ...(isCanonicalStorageEtag(filesError.appliedEtag) && {
630
+ appliedEtag: filesError.appliedEtag,
631
+ }),
49
632
  code,
50
- permanent: error.permanent,
51
- timedOut: error.timedOut,
633
+ permanent: filesError.permanent,
634
+ timedOut: filesError.timedOut,
52
635
  });
53
636
  }
54
637
  function mapRetryOptions(retries) {
@@ -60,7 +643,37 @@ function mapRetryOptions(retries) {
60
643
  ...(retries.backoff !== undefined && {
61
644
  backoff: ({ attempt, error }) => retries.backoff?.({
62
645
  attempt,
63
- error: mapFilesError(error),
646
+ error: mapFilesSdkError(error),
647
+ }) ?? 0,
648
+ }),
649
+ };
650
+ }
651
+ function storageRetryOptions(retries) {
652
+ if (retries === undefined || typeof retries === 'number') {
653
+ return retries;
654
+ }
655
+ return {
656
+ max: retries.max,
657
+ ...(retries.backoff !== undefined && {
658
+ backoff: ({ attempt, error }) => retries.backoff?.({
659
+ attempt,
660
+ error: new FilesError(error.code === StorageErrorCode.NOT_FOUND
661
+ ? 'NotFound'
662
+ : error.code === StorageErrorCode.UNAUTHORIZED
663
+ ? 'Unauthorized'
664
+ : error.code === StorageErrorCode.CONFLICT
665
+ ? 'Conflict'
666
+ : error.code === StorageErrorCode.READ_ONLY
667
+ ? 'ReadOnly'
668
+ : 'Provider', error.message, error, {
669
+ aborted: error.aborted,
670
+ applied: error.applied,
671
+ ...(isCanonicalStorageEtag(error.appliedEtag) && {
672
+ appliedEtag: error.appliedEtag,
673
+ }),
674
+ permanent: error.permanent,
675
+ timedOut: error.timedOut,
676
+ }),
64
677
  }) ?? 0,
65
678
  }),
66
679
  };
@@ -114,7 +727,7 @@ function normalizeDownloadStream(source) {
114
727
  await activeReader.cancel(reason);
115
728
  }
116
729
  catch (error) {
117
- throw mapFilesError(error);
730
+ throw mapFilesSdkError(error);
118
731
  }
119
732
  finally {
120
733
  release();
@@ -137,7 +750,7 @@ function normalizeDownloadStream(source) {
137
750
  }
138
751
  catch (error) {
139
752
  release();
140
- controller.error(mapFilesError(error));
753
+ controller.error(mapFilesSdkError(error));
141
754
  }
142
755
  },
143
756
  });
@@ -166,6 +779,22 @@ function uploadOptions(options) {
166
779
  }),
167
780
  };
168
781
  }
782
+ function conditionalUploadOptions(options) {
783
+ return {
784
+ ...operationOptions(options),
785
+ ...(options.cacheControl !== undefined && {
786
+ cacheControl: options.cacheControl,
787
+ }),
788
+ condition: options.condition,
789
+ ...(options.contentType !== undefined && {
790
+ contentType: options.contentType,
791
+ }),
792
+ ...(options.metadata !== undefined && { metadata: options.metadata }),
793
+ ...(options.onProgress !== undefined && {
794
+ onProgress: options.onProgress,
795
+ }),
796
+ };
797
+ }
169
798
  function downloadOptions(options) {
170
799
  return {
171
800
  ...operationOptions(options),
@@ -227,10 +856,42 @@ function signedUploadOptions(options) {
227
856
  ...(options.minSize !== undefined && { minSize: options.minSize }),
228
857
  };
229
858
  }
230
- function metadataOf(file) {
859
+ function providerEtag(value, key, operation) {
860
+ if (value === undefined) {
861
+ return undefined;
862
+ }
863
+ const etag = normalizeProviderStorageEtag(value);
864
+ if (etag === undefined) {
865
+ throw new StorageError('Storage adapter returned an invalid ETag.', {
866
+ code: StorageErrorCode.PROVIDER,
867
+ key,
868
+ operation,
869
+ permanent: true,
870
+ });
871
+ }
872
+ return etag;
873
+ }
874
+ function invalidConditionalEtag(label, key, operation) {
875
+ return new StorageError(`${label} must be a canonical storage ETag.`, {
876
+ code: StorageErrorCode.INVALID_ARGUMENT,
877
+ key,
878
+ operation,
879
+ permanent: true,
880
+ });
881
+ }
882
+ function invalidConditionalArgument(message, key, operation) {
883
+ return new StorageError(message, {
884
+ code: StorageErrorCode.INVALID_ARGUMENT,
885
+ key,
886
+ operation,
887
+ permanent: true,
888
+ });
889
+ }
890
+ function metadataOf(file, operation) {
891
+ const etag = providerEtag(file.etag, file.key, operation);
231
892
  return {
232
893
  contentType: file.type,
233
- ...(file.etag !== undefined && { etag: file.etag }),
894
+ ...(etag !== undefined && { etag }),
234
895
  key: file.key,
235
896
  ...(file.lastModified !== undefined && {
236
897
  lastModified: new Date(file.lastModified),
@@ -243,9 +904,10 @@ function metadataOf(file) {
243
904
  };
244
905
  }
245
906
  function uploadResultOf(result) {
907
+ const etag = providerEtag(result.etag, result.key, 'upload');
246
908
  return {
247
909
  contentType: result.contentType,
248
- ...(result.etag !== undefined && { etag: result.etag }),
910
+ ...(etag !== undefined && { etag }),
249
911
  key: result.key,
250
912
  ...(result.lastModified !== undefined && {
251
913
  lastModified: new Date(result.lastModified),
@@ -253,64 +915,726 @@ function uploadResultOf(result) {
253
915
  size: result.size,
254
916
  };
255
917
  }
918
+ function conditionalUploadResultOf(result) {
919
+ try {
920
+ return uploadResultOf(result);
921
+ }
922
+ catch (error) {
923
+ if (!isStorageError(error))
924
+ throw error;
925
+ throw new StorageError(error.message, {
926
+ applied: true,
927
+ code: error.code,
928
+ permanent: error.permanent,
929
+ });
930
+ }
931
+ }
932
+ function directConditionalUploadResultOf(result, logicalKey, physicalKey) {
933
+ let etag;
934
+ try {
935
+ etag = providerEtag(result.etag, logicalKey, 'upload');
936
+ }
937
+ catch (error) {
938
+ if (!isStorageError(error))
939
+ throw error;
940
+ throw new StorageError(error.message, {
941
+ applied: true,
942
+ code: error.code,
943
+ key: logicalKey,
944
+ operation: 'upload',
945
+ permanent: true,
946
+ });
947
+ }
948
+ if (result.key !== physicalKey) {
949
+ throw new StorageError('Storage adapter returned an unexpected conditional upload key.', {
950
+ applied: true,
951
+ ...(etag !== undefined && { appliedEtag: etag }),
952
+ code: StorageErrorCode.PROVIDER,
953
+ key: logicalKey,
954
+ operation: 'upload',
955
+ permanent: true,
956
+ });
957
+ }
958
+ return {
959
+ ...result,
960
+ ...(etag === undefined ? {} : { etag }),
961
+ key: logicalKey,
962
+ };
963
+ }
964
+ function storageObjectOf(file) {
965
+ return {
966
+ ...metadataOf(file, 'download'),
967
+ body: normalizeDownloadStream(file.stream()),
968
+ };
969
+ }
970
+ function normalizeFilesSdkPrefix(prefix) {
971
+ if (prefix === undefined)
972
+ return '';
973
+ if (typeof prefix !== 'string') {
974
+ throw new StorageError('prefix must be a string.', {
975
+ code: StorageErrorCode.INVALID_ARGUMENT,
976
+ permanent: true,
977
+ });
978
+ }
979
+ const normalized = prefix.replaceAll(/^\/+|(?<!\/)\/+$/gu, '');
980
+ if (normalized.length === 0 || normalized.includes('\0')) {
981
+ throw new StorageError('prefix must be a non-empty string without null bytes.', {
982
+ code: StorageErrorCode.INVALID_ARGUMENT,
983
+ permanent: true,
984
+ });
985
+ }
986
+ if (normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
987
+ throw new StorageError('prefix must not contain . or .. path segments.', {
988
+ code: StorageErrorCode.INVALID_ARGUMENT,
989
+ permanent: true,
990
+ });
991
+ }
992
+ return normalized;
993
+ }
994
+ function physicalPluginKey(prefix, key) {
995
+ if (typeof key !== 'string' || key.length === 0) {
996
+ throw new StorageError('The final plugin key must be a non-empty string.', {
997
+ code: StorageErrorCode.INVALID_ARGUMENT,
998
+ permanent: true,
999
+ });
1000
+ }
1001
+ if (key.includes('\0')) {
1002
+ throw new StorageError('The final plugin key must not contain null bytes.', {
1003
+ code: StorageErrorCode.INVALID_ARGUMENT,
1004
+ key,
1005
+ permanent: true,
1006
+ });
1007
+ }
1008
+ const normalized = key.replace(/^\/+/u, '');
1009
+ if (normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
1010
+ throw new StorageError('The final plugin key must not contain . or .. path segments.', {
1011
+ code: StorageErrorCode.INVALID_ARGUMENT,
1012
+ key,
1013
+ permanent: true,
1014
+ });
1015
+ }
1016
+ return {
1017
+ logicalKey: key,
1018
+ physicalKey: prefix.length === 0 ? key : `${prefix}/${normalized}`,
1019
+ };
1020
+ }
1021
+ function assertPhysicalPluginBudget(physicalKey, maxBytes, logicalKey) {
1022
+ if (maxBytes === undefined ||
1023
+ new TextEncoder().encode(physicalKey).byteLength <= maxBytes) {
1024
+ return;
1025
+ }
1026
+ throw new StorageError(`The combined storage prefix and key exceed the provider's ${maxBytes}-byte physical-key limit.`, {
1027
+ code: StorageErrorCode.LIMIT_EXCEEDED,
1028
+ ...(logicalKey !== undefined && { key: logicalKey }),
1029
+ permanent: true,
1030
+ });
1031
+ }
1032
+ function physicalKeyGuardPlugin(prefix, maxBytes) {
1033
+ const assertKey = (key) => {
1034
+ const resolved = physicalPluginKey(prefix, key);
1035
+ assertPhysicalPluginBudget(resolved.physicalKey, maxBytes, resolved.logicalKey);
1036
+ return resolved.logicalKey;
1037
+ };
1038
+ const assertListOptions = (options) => {
1039
+ if (options !== undefined &&
1040
+ (typeof options !== 'object' || options === null)) {
1041
+ throw new StorageError('The final plugin list options must be an object.', {
1042
+ code: StorageErrorCode.INVALID_ARGUMENT,
1043
+ permanent: true,
1044
+ });
1045
+ }
1046
+ const listPrefix = options
1047
+ ?.prefix;
1048
+ if (listPrefix === undefined || listPrefix === '') {
1049
+ if (prefix.length > 0) {
1050
+ assertPhysicalPluginBudget(`${prefix}/`, maxBytes);
1051
+ }
1052
+ return options === undefined
1053
+ ? undefined
1054
+ : (() => {
1055
+ const { prefix: _prefix, ...rest } = options;
1056
+ return Object.freeze({
1057
+ ...rest,
1058
+ ...(listPrefix === '' && { prefix: listPrefix }),
1059
+ });
1060
+ })();
1061
+ }
1062
+ if (typeof listPrefix !== 'string') {
1063
+ throw new StorageError('The final plugin list prefix must be a string.', {
1064
+ code: StorageErrorCode.INVALID_ARGUMENT,
1065
+ permanent: true,
1066
+ });
1067
+ }
1068
+ const resolved = physicalPluginKey(prefix, listPrefix);
1069
+ assertPhysicalPluginBudget(resolved.physicalKey, maxBytes, resolved.logicalKey);
1070
+ return Object.freeze({
1071
+ ...options,
1072
+ prefix: resolved.logicalKey,
1073
+ });
1074
+ };
1075
+ return {
1076
+ name: '@nestm/storage/physical-key-guard',
1077
+ wrap: handlers({
1078
+ copy: (operation, next) => {
1079
+ const from = assertKey(operation.from);
1080
+ const to = assertKey(operation.to);
1081
+ return next(Object.freeze({ ...operation, from, to }));
1082
+ },
1083
+ delete: (operation, next) => {
1084
+ const key = assertKey(operation.key);
1085
+ return next(Object.freeze({ ...operation, key }));
1086
+ },
1087
+ download: (operation, next) => {
1088
+ const key = assertKey(operation.key);
1089
+ return next(Object.freeze({ ...operation, key }));
1090
+ },
1091
+ exists: (operation, next) => {
1092
+ const key = assertKey(operation.key);
1093
+ return next(Object.freeze({ ...operation, key }));
1094
+ },
1095
+ head: (operation, next) => {
1096
+ const key = assertKey(operation.key);
1097
+ return next(Object.freeze({ ...operation, key }));
1098
+ },
1099
+ list: (operation, next) => {
1100
+ const options = assertListOptions(operation.options);
1101
+ return next(Object.freeze({
1102
+ ...operation,
1103
+ ...(options !== undefined && { options }),
1104
+ }));
1105
+ },
1106
+ move: (operation, next) => {
1107
+ const from = assertKey(operation.from);
1108
+ const to = assertKey(operation.to);
1109
+ return next(Object.freeze({ ...operation, from, to }));
1110
+ },
1111
+ signedUploadUrl: (operation, next) => {
1112
+ const key = assertKey(operation.key);
1113
+ return next(Object.freeze({ ...operation, key }));
1114
+ },
1115
+ upload: (operation, next) => {
1116
+ const key = assertKey(operation.key);
1117
+ return next(Object.freeze({ ...operation, key }));
1118
+ },
1119
+ url: (operation, next) => {
1120
+ const key = assertKey(operation.key);
1121
+ return next(Object.freeze({ ...operation, key }));
1122
+ },
1123
+ }),
1124
+ };
1125
+ }
256
1126
  export class FilesSdkStorageDriver {
257
1127
  #files;
258
1128
  #name;
1129
+ #conditionalCopy;
1130
+ #conditionalDelete;
1131
+ #conditionalRead;
1132
+ #conditionalUpload;
1133
+ #directConditionalFallbackBlocked;
1134
+ #physicalKey;
1135
+ #prefix;
1136
+ #readOnly;
1137
+ #retries;
1138
+ #signal;
1139
+ #timeout;
1140
+ #signedUploadPolicy;
1141
+ #signedDownloadPolicy;
259
1142
  constructor(options) {
260
- this.#files = new Files(options);
261
- this.#name = options.adapter.name;
1143
+ const raw = adapterRawObject(options.adapter);
1144
+ const rawProvenance = raw === undefined ? undefined : s3AdapterProvenanceOfRaw(raw);
1145
+ const authorityToken = s3AdapterAuthorityTokenOf(options.adapter);
1146
+ const authorityState = typeof authorityToken === 'object'
1147
+ ? s3AuthorityState.get(authorityToken)
1148
+ : undefined;
1149
+ const authorityInvalid = authorityToken === conflictingS3Authority ||
1150
+ (authorityState !== undefined &&
1151
+ (authorityState.record === undefined ||
1152
+ raw === undefined ||
1153
+ (raw !== authorityState.raw &&
1154
+ rawProvenance !== authorityState.record)));
1155
+ const s3Provenance = authorityState?.record ?? rawProvenance;
1156
+ if (authorityInvalid ||
1157
+ (s3Provenance === undefined &&
1158
+ raw !== undefined &&
1159
+ (isUndecoratedS3Raw(raw) || isStructuralS3Raw(raw)))) {
1160
+ throw new StorageError('Raw S3 adapters must be configured with withS3Capabilities() or createS3StorageDriver().', {
1161
+ code: StorageErrorCode.INVALID_ARGUMENT,
1162
+ permanent: true,
1163
+ });
1164
+ }
1165
+ const mismatchedAuthority = s3Provenance === undefined
1166
+ ? undefined
1167
+ : mismatchedS3Authority(options.adapter, s3Provenance.surface);
1168
+ if (mismatchedAuthority !== undefined) {
1169
+ throw new StorageError(`S3 adapter authority "${mismatchedAuthority}" does not match its verified provider profile.`, {
1170
+ code: StorageErrorCode.INVALID_ARGUMENT,
1171
+ permanent: true,
1172
+ });
1173
+ }
1174
+ const adapterForFiles = s3Provenance === undefined
1175
+ ? options.adapter
1176
+ : Object.freeze({ ...options.adapter });
1177
+ const copiedAuthorityMismatch = s3Provenance === undefined
1178
+ ? undefined
1179
+ : mismatchedS3Authority(adapterForFiles, s3Provenance.surface);
1180
+ if (copiedAuthorityMismatch !== undefined) {
1181
+ throw new StorageError(`S3 adapter authority "${copiedAuthorityMismatch}" changed while it was being bound.`, {
1182
+ code: StorageErrorCode.INVALID_ARGUMENT,
1183
+ permanent: true,
1184
+ });
1185
+ }
1186
+ const readOnly = options.readonly === true || s3Provenance?.provenance === 'unverified';
1187
+ // Evaluate caller policy before appending NestM's internal key guard below.
1188
+ this.#directConditionalFallbackBlocked =
1189
+ hasCallerFilesOperationPolicy(options);
1190
+ // Files retains the hooks object by reference. Snapshot it so an initially
1191
+ // inactive object cannot be mutated after this compatibility decision and
1192
+ // start observing only ordinary operations.
1193
+ const hooks = options.hooks === undefined
1194
+ ? undefined
1195
+ : Object.freeze({ ...options.hooks });
1196
+ this.#physicalKey = physicalKeyAdapterOf(adapterForFiles);
1197
+ const guardedPrefix = normalizeFilesSdkPrefix(options.prefix);
1198
+ const plugins = [
1199
+ ...(options.plugins ?? []),
1200
+ physicalKeyGuardPlugin(guardedPrefix, this.#physicalKey?.physicalKey.maxBytes),
1201
+ ];
1202
+ this.#files = new Files({
1203
+ ...options,
1204
+ adapter: adapterForFiles,
1205
+ ...(hooks !== undefined && { hooks }),
1206
+ plugins,
1207
+ readonly: readOnly,
1208
+ });
1209
+ this.#name = adapterForFiles.name;
1210
+ this.#conditionalCopy = conditionalCopyAdapterOf(adapterForFiles);
1211
+ this.#conditionalDelete = conditionalDeleteAdapterOf(adapterForFiles);
1212
+ this.#conditionalRead = conditionalReadAdapterOf(adapterForFiles);
1213
+ this.#conditionalUpload = conditionalUploadAdapterOf(adapterForFiles);
1214
+ this.#prefix = this.#files.prefix;
1215
+ if (guardedPrefix !== this.#prefix) {
1216
+ throw new StorageError('Storage prefix normalization was inconsistent.', {
1217
+ code: StorageErrorCode.INVALID_ARGUMENT,
1218
+ permanent: true,
1219
+ });
1220
+ }
1221
+ this.#assertPhysicalKeyBudget(this.#prefix);
1222
+ this.#readOnly = readOnly;
1223
+ this.#retries = storageRetryOptions(options.retries);
1224
+ this.#signal = options.signal;
1225
+ this.#timeout = options.timeout;
1226
+ this.#signedUploadPolicy = signedUploadPolicyAdapterOf(adapterForFiles);
1227
+ this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(adapterForFiles);
262
1228
  }
263
1229
  get name() {
264
1230
  return this.#name;
265
1231
  }
266
1232
  get capabilities() {
267
1233
  const capabilities = this.#files.capabilities;
1234
+ const conditional = capabilities.conditional;
1235
+ const directFallbackAllowed = !this.#directConditionalFallbackBlocked;
1236
+ const directCopy = directFallbackAllowed
1237
+ ? this.#conditionalCopy
1238
+ : undefined;
1239
+ const pipelineCopy = conditional.copy;
1240
+ const copyCapabilities = conditionalCopyCapabilities(directCopy, pipelineCopy);
1241
+ const conditionalCopySource = copyCapabilities.source;
1242
+ const conditionalCopyDestination = copyCapabilities.destination;
1243
+ const directDelete = directFallbackAllowed
1244
+ ? this.#conditionalDelete
1245
+ : undefined;
1246
+ const directRead = directFallbackAllowed
1247
+ ? this.#conditionalRead
1248
+ : undefined;
1249
+ const directUpload = directFallbackAllowed
1250
+ ? this.#conditionalUpload
1251
+ : undefined;
1252
+ const conditionalReadEtag = conditional.exactRead || directRead?.conditionalRead.etag === true;
1253
+ const conditionalReadVersion = directRead?.conditionalRead.version === true;
268
1254
  return {
269
1255
  cacheControl: capabilities.cacheControl,
270
1256
  delimiter: capabilities.delimiter,
271
1257
  metadata: capabilities.metadata,
272
1258
  rangeRead: capabilities.rangeRead,
273
- resumableUpload: capabilities.multipart,
274
- serverSideCopy: capabilities.serverSideCopy,
1259
+ resumableUpload: !this.#readOnly && capabilities.multipart,
1260
+ serverSideCopy: !this.#readOnly && capabilities.serverSideCopy,
1261
+ ...(!this.#readOnly &&
1262
+ conditionalCopySource !== undefined && {
1263
+ conditionalCopySource,
1264
+ }),
1265
+ ...(!this.#readOnly &&
1266
+ conditionalCopyDestination !== undefined && {
1267
+ conditionalCopyDestination,
1268
+ }),
1269
+ ...(!this.#readOnly &&
1270
+ (conditional.delete ||
1271
+ directDelete?.conditionalDelete.etag === true) && {
1272
+ conditionalDelete: { etag: true },
1273
+ }),
1274
+ ...((conditionalReadEtag || conditionalReadVersion) && {
1275
+ conditionalRead: {
1276
+ etag: conditionalReadEtag,
1277
+ version: conditionalReadVersion,
1278
+ },
1279
+ }),
1280
+ ...(!this.#readOnly &&
1281
+ (conditional.create ||
1282
+ directUpload?.conditionalCreate !== undefined) && {
1283
+ conditionalCreate: {
1284
+ resultEtag: conditional.create ||
1285
+ directUpload?.conditionalCreate?.resultEtag === true,
1286
+ },
1287
+ }),
1288
+ ...(!this.#readOnly &&
1289
+ (conditional.replace ||
1290
+ directUpload?.conditionalReplace !== undefined) && {
1291
+ conditionalReplace: {
1292
+ resultEtag: conditional.replace ||
1293
+ directUpload?.conditionalReplace?.resultEtag === true,
1294
+ },
1295
+ }),
1296
+ ...(!this.#readOnly &&
1297
+ directUpload?.conditionalMultipartCompletion !== undefined && {
1298
+ conditionalMultipartCompletion: {
1299
+ ...directUpload.conditionalMultipartCompletion,
1300
+ },
1301
+ }),
1302
+ ...(this.#physicalKey !== undefined && {
1303
+ physicalKey: { ...this.#physicalKey.physicalKey },
1304
+ }),
275
1305
  signedDownload: { ...capabilities.signedUrl },
276
- signedUpload: 'runtime',
277
- nativeUploadProgress: capabilities.uploadProgress,
1306
+ ...(this.#signedDownloadPolicy !== undefined && {
1307
+ signedDownloadPolicy: {
1308
+ ...this.#signedDownloadPolicy.signedDownloadPolicy,
1309
+ },
1310
+ }),
1311
+ signedUpload: this.#readOnly ? false : 'runtime',
1312
+ ...(this.#signedUploadPolicy !== undefined &&
1313
+ !this.#readOnly && {
1314
+ signedUploadPolicy: {
1315
+ ...this.#signedUploadPolicy.signedUploadPolicy,
1316
+ },
1317
+ }),
1318
+ nativeUploadProgress: !this.#readOnly && capabilities.uploadProgress,
278
1319
  };
279
1320
  }
280
1321
  async upload(key, body, options) {
1322
+ this.#assertLogicalKey(key);
281
1323
  return this.#call(async () => uploadResultOf(await this.#files.upload(key, mapBody(body), uploadOptions(options))));
282
1324
  }
1325
+ uploadConditional(key, body, options) {
1326
+ const condition = options
1327
+ ?.condition;
1328
+ if (typeof condition !== 'object' ||
1329
+ condition === null ||
1330
+ !('type' in condition) ||
1331
+ (condition.type !== 'create' && condition.type !== 'replace')) {
1332
+ return Promise.reject(invalidConditionalArgument('condition.type must be "create" or "replace".', key, 'upload'));
1333
+ }
1334
+ if (condition.type === 'replace' &&
1335
+ !isCanonicalStorageEtag('etag' in condition ? condition.etag : undefined)) {
1336
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'upload'));
1337
+ }
1338
+ this.#assertLogicalKey(key);
1339
+ if (this.#readOnly) {
1340
+ return Promise.reject(new StorageError(`Cannot call uploadConditional() on a read-only storage adapter.`, {
1341
+ code: StorageErrorCode.READ_ONLY,
1342
+ key,
1343
+ operation: 'upload',
1344
+ permanent: true,
1345
+ }));
1346
+ }
1347
+ const adapter = this.#conditionalUpload;
1348
+ const capability = condition.type === 'create'
1349
+ ? adapter?.conditionalCreate
1350
+ : adapter?.conditionalReplace;
1351
+ const multipartRequested = options.multipart !== undefined && options.multipart !== false;
1352
+ const controlRequested = options.control !== undefined;
1353
+ const pipelineSupported = !multipartRequested &&
1354
+ !controlRequested &&
1355
+ (condition.type === 'create'
1356
+ ? this.#files.capabilities.conditional.create
1357
+ : this.#files.capabilities.conditional.replace);
1358
+ if (pipelineSupported) {
1359
+ return this.#call(async () => conditionalUploadResultOf(await this.#files.upload(key, mapBody(body), conditionalUploadOptions(options))));
1360
+ }
1361
+ const multipartSupported = !multipartRequested ||
1362
+ (condition.type === 'create'
1363
+ ? adapter?.conditionalMultipartCompletion?.create === true
1364
+ : adapter?.conditionalMultipartCompletion?.replace === true);
1365
+ const directFallbackSupported = adapter !== undefined && capability !== undefined && multipartSupported;
1366
+ if (directFallbackSupported && this.#directConditionalFallbackBlocked) {
1367
+ return Promise.reject(this.#conditionalFallbackPolicyError(key, 'upload'));
1368
+ }
1369
+ if (!directFallbackSupported) {
1370
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional upload.`, {
1371
+ code: StorageErrorCode.NOT_SUPPORTED,
1372
+ key,
1373
+ operation: 'upload',
1374
+ permanent: true,
1375
+ }));
1376
+ }
1377
+ const mergedOptions = this.#conditionalOptions(options);
1378
+ return this.#call(async () => {
1379
+ const physicalKey = this.#path(key);
1380
+ const result = await adapter.uploadConditional(physicalKey, mapBody(body), mergedOptions);
1381
+ return directConditionalUploadResultOf(result, key, physicalKey);
1382
+ });
1383
+ }
283
1384
  async download(key, options) {
1385
+ this.#assertLogicalKey(key);
284
1386
  return this.#call(async () => {
285
1387
  const file = await this.#files.download(key, downloadOptions(options));
286
1388
  return {
287
- ...metadataOf(file),
1389
+ ...metadataOf(file, 'download'),
288
1390
  body: normalizeDownloadStream(file.stream()),
289
1391
  };
290
1392
  });
291
1393
  }
1394
+ downloadConditional(key, options) {
1395
+ const condition = options
1396
+ ?.condition;
1397
+ if (typeof condition !== 'object' || condition === null) {
1398
+ return Promise.reject(invalidConditionalArgument('conditional read requires a condition object.', key, 'download'));
1399
+ }
1400
+ const etag = 'etag' in condition ? condition.etag : undefined;
1401
+ const version = 'version' in condition ? condition.version : undefined;
1402
+ if (etag !== undefined && !isCanonicalStorageEtag(etag)) {
1403
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'download'));
1404
+ }
1405
+ if (version !== undefined &&
1406
+ (typeof version !== 'string' || version.length === 0)) {
1407
+ return Promise.reject(invalidConditionalArgument('condition.version must be a non-empty string.', key, 'download'));
1408
+ }
1409
+ if (etag === undefined && version === undefined) {
1410
+ return Promise.reject(invalidConditionalArgument('conditional read requires an etag, version, or both.', key, 'download'));
1411
+ }
1412
+ this.#assertLogicalKey(key);
1413
+ const pipelineSupported = etag !== undefined &&
1414
+ version === undefined &&
1415
+ this.#files.capabilities.conditional.exactRead;
1416
+ if (pipelineSupported) {
1417
+ return this.#call(async () => storageObjectOf(await this.#files.download(key, {
1418
+ ...downloadOptions(options),
1419
+ condition: { etag },
1420
+ })));
1421
+ }
1422
+ const adapter = this.#conditionalRead;
1423
+ const directFallbackSupported = adapter !== undefined &&
1424
+ (etag === undefined || adapter.conditionalRead.etag) &&
1425
+ (version === undefined || adapter.conditionalRead.version);
1426
+ if (directFallbackSupported && this.#directConditionalFallbackBlocked) {
1427
+ return Promise.reject(this.#conditionalFallbackPolicyError(key, 'download'));
1428
+ }
1429
+ if (!directFallbackSupported) {
1430
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional read.`, {
1431
+ code: StorageErrorCode.NOT_SUPPORTED,
1432
+ key,
1433
+ operation: 'download',
1434
+ permanent: true,
1435
+ }));
1436
+ }
1437
+ return this.#call(async () => {
1438
+ const physicalKey = this.#path(key);
1439
+ const object = await adapter.downloadConditional(physicalKey, this.#conditionalOptions(options));
1440
+ if (object.key !== physicalKey) {
1441
+ await object.body.cancel().catch(() => undefined);
1442
+ throw new StorageError('Storage adapter returned an unexpected conditional download key.', {
1443
+ code: StorageErrorCode.PROVIDER,
1444
+ key,
1445
+ operation: 'download',
1446
+ permanent: true,
1447
+ });
1448
+ }
1449
+ let etag;
1450
+ try {
1451
+ etag = providerEtag(object.etag, key, 'download');
1452
+ }
1453
+ catch (error) {
1454
+ await object.body.cancel(error).catch(() => undefined);
1455
+ throw error;
1456
+ }
1457
+ return {
1458
+ ...object,
1459
+ body: normalizeDownloadStream(object.body),
1460
+ ...(etag === undefined ? {} : { etag }),
1461
+ key,
1462
+ name: key.split('/').at(-1) ?? key,
1463
+ };
1464
+ });
1465
+ }
292
1466
  async head(key, options) {
293
- return this.#call(async () => metadataOf(await this.#files.head(key, operationOptions(options))));
1467
+ this.#assertLogicalKey(key);
1468
+ return this.#call(async () => metadataOf(await this.#files.head(key, operationOptions(options)), 'head'));
294
1469
  }
295
1470
  exists(key, options) {
1471
+ this.#assertLogicalKey(key);
296
1472
  return this.#call(() => this.#files.exists(key, operationOptions(options)));
297
1473
  }
298
1474
  delete(key, options) {
1475
+ this.#assertLogicalKey(key);
299
1476
  return this.#call(async () => {
300
1477
  await this.#files.delete(key, operationOptions(options));
301
1478
  });
302
1479
  }
1480
+ deleteConditional(key, options) {
1481
+ const condition = options
1482
+ ?.condition;
1483
+ if (typeof condition !== 'object' || condition === null) {
1484
+ return Promise.reject(invalidConditionalArgument('conditional delete requires a condition object.', key, 'delete'));
1485
+ }
1486
+ const etag = 'etag' in condition ? condition.etag : undefined;
1487
+ if (!isCanonicalStorageEtag(etag)) {
1488
+ return Promise.reject(invalidConditionalEtag('condition.etag', key, 'delete'));
1489
+ }
1490
+ this.#assertLogicalKey(key);
1491
+ if (this.#readOnly) {
1492
+ return Promise.reject(new StorageError(`Cannot call deleteConditional() on a read-only storage adapter.`, {
1493
+ code: StorageErrorCode.READ_ONLY,
1494
+ key,
1495
+ operation: 'delete',
1496
+ permanent: true,
1497
+ }));
1498
+ }
1499
+ if (this.#files.capabilities.conditional.delete) {
1500
+ return this.#call(() => this.#files.delete(key, {
1501
+ ...operationOptions(options),
1502
+ condition: options.condition,
1503
+ }));
1504
+ }
1505
+ const adapter = this.#conditionalDelete;
1506
+ const directFallbackSupported = adapter !== undefined && adapter.conditionalDelete.etag;
1507
+ if (directFallbackSupported && this.#directConditionalFallbackBlocked) {
1508
+ return Promise.reject(this.#conditionalFallbackPolicyError(key, 'delete'));
1509
+ }
1510
+ if (!directFallbackSupported) {
1511
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional delete.`, {
1512
+ code: StorageErrorCode.NOT_SUPPORTED,
1513
+ key,
1514
+ operation: 'delete',
1515
+ permanent: true,
1516
+ }));
1517
+ }
1518
+ const mergedOptions = this.#conditionalOptions(options);
1519
+ return this.#call(() => adapter.deleteConditional(this.#path(key), mergedOptions));
1520
+ }
303
1521
  copy(sourceKey, destinationKey, options) {
1522
+ this.#assertLogicalKey(sourceKey);
1523
+ this.#assertLogicalKey(destinationKey);
304
1524
  return this.#call(() => this.#files.copy(sourceKey, destinationKey, operationOptions(options)));
305
1525
  }
306
1526
  move(sourceKey, destinationKey, options) {
1527
+ this.#assertLogicalKey(sourceKey);
1528
+ this.#assertLogicalKey(destinationKey);
307
1529
  return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
308
1530
  }
1531
+ promote(sourceKey, destinationKey, options) {
1532
+ if (typeof options !== 'object' || options === null) {
1533
+ return Promise.reject(invalidConditionalArgument('promote requires an options object with a precondition.', sourceKey, 'promote'));
1534
+ }
1535
+ const destination = options.destination;
1536
+ if (destination !== undefined &&
1537
+ (typeof destination !== 'object' ||
1538
+ destination === null ||
1539
+ (destination.type !== 'create' && destination.type !== 'replace'))) {
1540
+ return Promise.reject(new StorageError('destination.type must be "create" or "replace".', {
1541
+ code: StorageErrorCode.INVALID_ARGUMENT,
1542
+ key: destinationKey,
1543
+ operation: 'promote',
1544
+ permanent: true,
1545
+ }));
1546
+ }
1547
+ if (options.sourceVersion !== undefined &&
1548
+ (typeof options.sourceVersion !== 'string' ||
1549
+ options.sourceVersion.length === 0)) {
1550
+ return Promise.reject(invalidConditionalArgument('sourceVersion must be a non-empty string.', sourceKey, 'promote'));
1551
+ }
1552
+ if (options.sourceEtag === undefined &&
1553
+ options.sourceVersion === undefined &&
1554
+ destination === undefined) {
1555
+ return Promise.reject(invalidConditionalArgument('promote requires a source or destination precondition.', sourceKey, 'promote'));
1556
+ }
1557
+ if (options.sourceEtag !== undefined &&
1558
+ !isCanonicalStorageEtag(options.sourceEtag)) {
1559
+ return Promise.reject(invalidConditionalEtag('sourceEtag', sourceKey, 'promote'));
1560
+ }
1561
+ if (destination?.type === 'replace' &&
1562
+ !isCanonicalStorageEtag(destination.etag)) {
1563
+ return Promise.reject(invalidConditionalEtag('destination.etag', destinationKey, 'promote'));
1564
+ }
1565
+ this.#assertLogicalKey(sourceKey);
1566
+ this.#assertLogicalKey(destinationKey);
1567
+ if (this.#readOnly) {
1568
+ return Promise.reject(new StorageError(`Cannot call promote() on a read-only storage adapter.`, {
1569
+ code: StorageErrorCode.READ_ONLY,
1570
+ key: sourceKey,
1571
+ operation: 'promote',
1572
+ permanent: true,
1573
+ }));
1574
+ }
1575
+ const pipelineCopy = this.#files.capabilities.conditional.copy;
1576
+ const pipelineDestinationSupported = destination?.type === 'create'
1577
+ ? pipelineCopy.destinationCreate
1578
+ : destination?.type === 'replace'
1579
+ ? pipelineCopy.destinationReplace
1580
+ : false;
1581
+ const pipelineSupported = options.sourceEtag !== undefined &&
1582
+ options.sourceVersion === undefined &&
1583
+ destination !== undefined &&
1584
+ pipelineCopy.sourceEtag &&
1585
+ pipelineCopy.atomicSourceDestination &&
1586
+ pipelineDestinationSupported;
1587
+ if (pipelineSupported) {
1588
+ return this.#call(() => this.#files.copy(sourceKey, destinationKey, {
1589
+ ...operationOptions(options),
1590
+ condition: {
1591
+ destination,
1592
+ source: { etag: options.sourceEtag },
1593
+ },
1594
+ }));
1595
+ }
1596
+ const adapter = this.#conditionalCopy;
1597
+ const directFallbackSupported = adapter !== undefined &&
1598
+ (options.sourceEtag === undefined ||
1599
+ adapter.conditionalCopySource?.etag === true) &&
1600
+ (options.sourceVersion === undefined ||
1601
+ adapter.conditionalCopySource?.version === true) &&
1602
+ (options.sourceEtag === undefined && options.sourceVersion === undefined
1603
+ ? true
1604
+ : options.destination !== undefined ||
1605
+ adapter.conditionalCopySource?.requiresDestinationPredicate !==
1606
+ true) &&
1607
+ (options.destination?.type !== 'create' ||
1608
+ adapter.conditionalCopyDestination?.create === true) &&
1609
+ (options.destination?.type !== 'replace' ||
1610
+ adapter.conditionalCopyDestination?.replace === true) &&
1611
+ (options.destination === undefined ||
1612
+ options.sourceEtag !== undefined ||
1613
+ options.sourceVersion !== undefined ||
1614
+ adapter.conditionalCopyDestination?.requiresSourcePredicate !== true) &&
1615
+ (options.destination === undefined ||
1616
+ (options.sourceEtag === undefined &&
1617
+ options.sourceVersion === undefined) ||
1618
+ adapter.conditionalCopyDestination?.atomicWithSource === true);
1619
+ if (directFallbackSupported && this.#directConditionalFallbackBlocked) {
1620
+ return Promise.reject(this.#conditionalFallbackPolicyError(sourceKey, 'promote'));
1621
+ }
1622
+ if (!directFallbackSupported) {
1623
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
1624
+ code: StorageErrorCode.NOT_SUPPORTED,
1625
+ key: sourceKey,
1626
+ operation: 'promote',
1627
+ permanent: true,
1628
+ }));
1629
+ }
1630
+ return this.#call(() => adapter.promote(this.#path(sourceKey), this.#path(destinationKey), this.#conditionalOptions(options)));
1631
+ }
309
1632
  async list(options) {
1633
+ this.#assertListPhysicalPrefix(options?.prefix);
310
1634
  return this.#call(async () => {
311
1635
  const result = await this.#files.list(listOptions(options));
312
1636
  return {
313
- items: result.items.map(metadataOf),
1637
+ items: result.items.map((file) => metadataOf(file, 'list')),
314
1638
  ...(result.cursor !== undefined && { cursor: result.cursor }),
315
1639
  ...(result.prefixes !== undefined && {
316
1640
  prefixes: [...result.prefixes],
@@ -324,17 +1648,19 @@ export class FilesSdkStorageDriver {
324
1648
  async *#search(pattern, options) {
325
1649
  try {
326
1650
  for await (const file of this.#files.search(pattern, searchOptions(options))) {
327
- yield metadataOf(file);
1651
+ yield metadataOf(file, 'search');
328
1652
  }
329
1653
  }
330
1654
  catch (error) {
331
- throw mapFilesError(error);
1655
+ throw mapFilesSdkError(error);
332
1656
  }
333
1657
  }
334
1658
  signDownload(key, options) {
1659
+ this.#assertLogicalKey(key);
335
1660
  return this.#call(() => this.#files.url(key, signedDownloadOptions(options)));
336
1661
  }
337
1662
  signUpload(key, options) {
1663
+ this.#assertLogicalKey(key);
338
1664
  return this.#call(() => this.#files.signedUploadUrl(key, signedUploadOptions(options)));
339
1665
  }
340
1666
  async #call(operation) {
@@ -342,9 +1668,83 @@ export class FilesSdkStorageDriver {
342
1668
  return await operation();
343
1669
  }
344
1670
  catch (error) {
345
- throw mapFilesError(error);
1671
+ throw mapFilesSdkError(error);
346
1672
  }
347
1673
  }
1674
+ #path(key) {
1675
+ if (typeof key !== 'string' || key.length === 0) {
1676
+ throw new StorageError('key must be a non-empty string.', {
1677
+ code: StorageErrorCode.INVALID_ARGUMENT,
1678
+ permanent: true,
1679
+ });
1680
+ }
1681
+ if (key.includes('\0')) {
1682
+ throw new StorageError('key must not contain null bytes.', {
1683
+ code: StorageErrorCode.INVALID_ARGUMENT,
1684
+ key,
1685
+ permanent: true,
1686
+ });
1687
+ }
1688
+ const normalized = key.replace(/^\/+/u, '');
1689
+ if (normalized
1690
+ .split('/')
1691
+ .some((segment) => segment === '.' || segment === '..')) {
1692
+ throw new StorageError('key must not contain . or .. path segments.', {
1693
+ code: StorageErrorCode.INVALID_ARGUMENT,
1694
+ key,
1695
+ permanent: true,
1696
+ });
1697
+ }
1698
+ const physicalKey = this.#prefix.length === 0 ? key : `${this.#prefix}/${normalized}`;
1699
+ this.#assertPhysicalKeyBudget(physicalKey, key);
1700
+ return physicalKey;
1701
+ }
1702
+ #assertLogicalKey(key) {
1703
+ this.#path(key);
1704
+ }
1705
+ #assertListPhysicalPrefix(prefix) {
1706
+ if (prefix !== undefined && prefix.length > 0) {
1707
+ this.#assertLogicalKey(prefix);
1708
+ return;
1709
+ }
1710
+ if (this.#prefix.length > 0) {
1711
+ this.#assertPhysicalKeyBudget(`${this.#prefix}/`);
1712
+ }
1713
+ }
1714
+ #physicalKeyExceedsBudget(physicalKey) {
1715
+ const maxBytes = this.#physicalKey?.physicalKey.maxBytes;
1716
+ return (maxBytes !== undefined &&
1717
+ new TextEncoder().encode(physicalKey).byteLength > maxBytes);
1718
+ }
1719
+ #assertPhysicalKeyBudget(physicalKey, logicalKey) {
1720
+ const maxBytes = this.#physicalKey?.physicalKey.maxBytes;
1721
+ if (maxBytes === undefined || !this.#physicalKeyExceedsBudget(physicalKey))
1722
+ return;
1723
+ throw new StorageError(`The combined storage prefix and key exceed the provider's ${maxBytes}-byte physical-key limit.`, {
1724
+ code: StorageErrorCode.LIMIT_EXCEEDED,
1725
+ ...(logicalKey !== undefined && { key: logicalKey }),
1726
+ permanent: true,
1727
+ });
1728
+ }
1729
+ #conditionalFallbackPolicyError(key, operation) {
1730
+ return new StorageError('This conditional form requires a NestM adapter fallback and is unavailable while Files SDK plugins, hooks, or receipts are configured.', {
1731
+ code: StorageErrorCode.NOT_SUPPORTED,
1732
+ key,
1733
+ operation,
1734
+ permanent: true,
1735
+ });
1736
+ }
1737
+ #conditionalOptions(options) {
1738
+ return {
1739
+ ...options,
1740
+ ...(options.retries === undefined &&
1741
+ this.#retries !== undefined && { retries: this.#retries }),
1742
+ ...(options.signal === undefined &&
1743
+ this.#signal !== undefined && { signal: this.#signal }),
1744
+ ...(options.timeout === undefined &&
1745
+ this.#timeout !== undefined && { timeout: this.#timeout }),
1746
+ };
1747
+ }
348
1748
  }
349
1749
  export function createFilesSdkDriver(options) {
350
1750
  return new FilesSdkStorageDriver(options);