@nestm/storage 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +111 -13
  3. package/SECURITY.md +23 -0
  4. package/dist/files-sdk/files-sdk.driver.d.ts +36 -7
  5. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  6. package/dist/files-sdk/files-sdk.driver.js +141 -20
  7. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  8. package/dist/files-sdk/index.d.ts +1 -1
  9. package/dist/files-sdk/index.d.ts.map +1 -1
  10. package/dist/files-sdk/index.js.map +1 -1
  11. package/dist/files-sdk/s3/index.d.ts +14 -0
  12. package/dist/files-sdk/s3/index.d.ts.map +1 -0
  13. package/dist/files-sdk/s3/index.js +108 -0
  14. package/dist/files-sdk/s3/index.js.map +1 -0
  15. package/dist/gateway/index.d.ts +1 -1
  16. package/dist/gateway/index.d.ts.map +1 -1
  17. package/dist/gateway/index.js.map +1 -1
  18. package/dist/gateway/storage-gateway.controller.d.ts +12 -11
  19. package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
  20. package/dist/gateway/storage-gateway.controller.js +209 -65
  21. package/dist/gateway/storage-gateway.controller.js.map +1 -1
  22. package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
  23. package/dist/gateway/storage-gateway.module.js +60 -1
  24. package/dist/gateway/storage-gateway.module.js.map +1 -1
  25. package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
  26. package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
  27. package/dist/gateway/storage-gateway.tokens.js +1 -0
  28. package/dist/gateway/storage-gateway.tokens.js.map +1 -1
  29. package/dist/gateway/storage-gateway.types.d.ts +43 -0
  30. package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
  31. package/dist/gateway/storage-gateway.types.js.map +1 -1
  32. package/dist/storage.client.d.ts +8 -1
  33. package/dist/storage.client.d.ts.map +1 -1
  34. package/dist/storage.client.js +40 -0
  35. package/dist/storage.client.js.map +1 -1
  36. package/dist/storage.driver.d.ts +6 -1
  37. package/dist/storage.driver.d.ts.map +1 -1
  38. package/dist/storage.driver.js.map +1 -1
  39. package/dist/storage.error.d.ts +3 -0
  40. package/dist/storage.error.d.ts.map +1 -1
  41. package/dist/storage.error.js +33 -1
  42. package/dist/storage.error.js.map +1 -1
  43. package/dist/storage.types.d.ts +39 -1
  44. package/dist/storage.types.d.ts.map +1 -1
  45. package/dist/storage.types.js.map +1 -1
  46. package/package.json +25 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @nestm/storage
2
2
 
3
+ ## 0.1.0-alpha.3
4
+
5
+ ### Minor Changes
6
+
7
+ - b947634: Harden storage integration boundaries with cross-copy-safe `StorageError`
8
+ detection, a package-owned S3 driver factory, conditional staged-object
9
+ promotion, and a mandatory parsed key policy plus signed-transfer limits for
10
+ the optional HTTP gateway.
11
+
12
+ ### Patch Changes
13
+
14
+ - b947634: Map structurally branded `files-sdk` errors, including errors wrapped across
15
+ duplicate package copies, so missing objects retain the `NOT_FOUND` storage
16
+ error code.
17
+
3
18
  ## 0.1.0-alpha.2
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -25,7 +25,15 @@ consumers do not download NestJS.
25
25
  ## Install
26
26
 
27
27
  ```sh
28
- pnpm add @nestm/storage@alpha files-sdk@2.2.2
28
+ pnpm add @nestm/storage@alpha
29
+ ```
30
+
31
+ The package-owned S3 factory needs no direct `files-sdk` import. Install the
32
+ pinned engine explicitly only when the application imports another adapter
33
+ such as `files-sdk/gcs`:
34
+
35
+ ```sh
36
+ pnpm add files-sdk@2.2.2
29
37
  ```
30
38
 
31
39
  Install only the native SDKs required by the chosen provider. For example:
@@ -82,14 +90,15 @@ runtime or declaration imports. Provider adapters remain available through
82
90
 
83
91
  ## Configure named stores
84
92
 
85
- Create provider adapters with `files-sdk`, wrap them through the explicit
86
- bridge, and register the resulting drivers:
93
+ Use the package-owned S3 factory when applicable. For other providers, create a
94
+ `files-sdk` adapter, wrap it through the explicit bridge, and register the
95
+ resulting driver:
87
96
 
88
97
  ```ts
89
98
  import { Module } from '@nestjs/common';
90
- import { s3 } from 'files-sdk/s3';
91
99
  import { gcs } from 'files-sdk/gcs';
92
100
  import { createFilesSdkDriver } from '@nestm/storage/files-sdk';
101
+ import { createS3StorageDriver } from '@nestm/storage/files-sdk/s3';
93
102
  import { StorageModule } from '@nestm/storage';
94
103
 
95
104
  export const StorageKey = {
@@ -104,11 +113,11 @@ export const StorageKey = {
104
113
  stores: [
105
114
  {
106
115
  name: StorageKey.MEDIA,
107
- driver: createFilesSdkDriver({
108
- adapter: s3({
116
+ driver: createS3StorageDriver({
117
+ adapter: {
109
118
  bucket: 'media',
110
119
  region: 'us-east-1',
111
- }),
120
+ },
112
121
  }),
113
122
  },
114
123
  {
@@ -194,11 +203,11 @@ StorageModule.forRootAsync({
194
203
  name: 'media',
195
204
  inject: [ConfigService],
196
205
  useFactory: (config: ConfigService) =>
197
- createFilesSdkDriver({
198
- adapter: s3({
206
+ createS3StorageDriver({
207
+ adapter: {
199
208
  bucket: config.getOrThrow('MEDIA_BUCKET'),
200
209
  region: config.getOrThrow('AWS_REGION'),
201
- }),
210
+ },
202
211
  }),
203
212
  },
204
213
  ],
@@ -221,6 +230,7 @@ whitespace.
221
230
  `StorageClient` exposes:
222
231
 
223
232
  - `upload`, `downloadStream`, `head`, `exists`, `delete`, `copy`, and `move`;
233
+ - conditional staged-object `promote` when the driver advertises it;
224
234
  - `list`, cursor-aware `listAll`, and lazy `search`;
225
235
  - `signDownload` and discriminated PUT/POST `signUpload`;
226
236
  - `uploadMany`, `downloadMany`, `headMany`, `existsMany`, and `deleteMany`;
@@ -250,6 +260,37 @@ Node `Readable` uploads are accepted and converted to Web streams without
250
260
  buffering. Provider capability gaps fail closed with `StorageError` rather than
251
261
  silently discarding a range, metadata, or cache-control request.
252
262
 
263
+ ### Race-free staged-object promotion
264
+
265
+ The S3 bridge advertises ETag- and version-conditional server-side copy. This
266
+ lets an application validate a staged object and copy that exact source to its
267
+ final key instead of re-reading whichever bytes occupy the staging key later:
268
+
269
+ ```ts
270
+ import { StorageError, StorageErrorCode } from '@nestm/storage';
271
+
272
+ const staged = await media.head(stagingKey);
273
+ if (staged.etag === undefined) {
274
+ throw new StorageError('Provider did not return a source ETag.', {
275
+ code: StorageErrorCode.NOT_SUPPORTED,
276
+ });
277
+ }
278
+
279
+ // Validate size, declared MIME, and magic bytes before this call.
280
+ await media.file(stagingKey).promoteTo(finalKey, {
281
+ sourceEtag: staged.etag,
282
+ });
283
+
284
+ // Commit ready metadata first. Promotion deliberately retains the staged
285
+ // object so a failed database commit remains recoverable.
286
+ await media.delete(stagingKey);
287
+ ```
288
+
289
+ `sourceVersion` can select an immutable S3 version and may be combined with
290
+ `sourceEtag`. A promotion without either identity is rejected. Drivers that do
291
+ not publish `capabilities.conditionalCopy` fail with `NOT_SUPPORTED` rather
292
+ than falling back to an unsafe ordinary copy.
293
+
253
294
  ### Resumable uploads
254
295
 
255
296
  ```ts
@@ -301,20 +342,42 @@ The gateway lives at `@nestm/storage/gateway` and is never mounted by
301
342
  `StorageModule`.
302
343
 
303
344
  ```ts
304
- import { Module } from '@nestjs/common';
345
+ import { Injectable, Module } from '@nestjs/common';
305
346
  import {
306
347
  StorageGatewayModule,
307
348
  StorageGatewayOperation,
349
+ type StorageGatewayKeyPolicy,
308
350
  } from '@nestm/storage/gateway';
309
351
 
352
+ @Injectable()
353
+ class TenantStorageKeyPolicy implements StorageGatewayKeyPolicy {
354
+ resolve({ input, request, target }) {
355
+ const tenantId = tenantIdFromAuthenticatedRequest(request);
356
+ const root = `tenants/${base64url(tenantId)}`;
357
+ if (target === 'pattern') {
358
+ // Search is already constrained by the separately resolved prefix.
359
+ return input?.value ?? '*';
360
+ }
361
+ return `${root}/${input?.value ?? ''}`;
362
+ }
363
+ }
364
+
365
+ @Module({
366
+ providers: [TenantStorageKeyPolicy],
367
+ exports: [TenantStorageKeyPolicy],
368
+ })
369
+ class StoragePolicyModule {}
370
+
310
371
  @Module({
311
372
  imports: [
312
373
  AppStorageModule,
313
374
  AuthModule,
375
+ StoragePolicyModule,
314
376
  StorageGatewayModule.register({
315
- imports: [AppStorageModule, AuthModule],
377
+ imports: [AppStorageModule, AuthModule, StoragePolicyModule],
316
378
  store: 'media',
317
379
  guards: [JwtAuthGuard],
380
+ keyPolicy: TenantStorageKeyPolicy,
318
381
  mode: 'hybrid',
319
382
  operations: [
320
383
  StorageGatewayOperation.DOWNLOAD,
@@ -325,6 +388,9 @@ import {
325
388
  StorageGatewayOperation.SIGN_UPLOAD,
326
389
  ],
327
390
  maxUploadBytes: 100 * 1024 * 1024,
391
+ maxSignedUploadBytes: 10 * 1024 * 1024,
392
+ signedUploadContentTypes: ['image/jpeg', 'image/png'],
393
+ maxSignedUrlExpiresIn: 900,
328
394
  maxListResults: 1000,
329
395
  maxSearchResults: 1000,
330
396
  proxyInlineContentTypes: ['image/jpeg', 'image/png'],
@@ -338,12 +404,39 @@ Registration fails without at least one existing Nest guard. The only bypass is
338
404
  the explicit `allowUnauthenticated: true` development escape hatch. Operations
339
405
  are deny-by-default and must be allowlisted individually.
340
406
 
407
+ Registration also fails without a `keyPolicy`. Guards answer whether a request
408
+ may reach the gateway; the key policy independently resolves every parsed
409
+ `key`, `prefix`, search `pattern`, `from`, and `to` value to the exact provider
410
+ path. It runs even when a list/search prefix was omitted, so the policy can
411
+ always impose a tenant root. Key-policy providers may be request scoped.
412
+ Returned paths are parsed again and reject absolute paths, backslashes, control
413
+ characters, empty segments, and dot/parent segments.
414
+
415
+ Existing single-tenant applications can temporarily set
416
+ `unsafeAllowUnscopedKeys: true` instead of `keyPolicy`. The name is intentional:
417
+ it preserves caller-controlled provider keys and must not be used on an exposed
418
+ or multi-tenant gateway. It cannot be combined with `keyPolicy`.
419
+
341
420
  Proxy downloads default to `Content-Disposition: attachment` and always send
342
421
  `X-Content-Type-Options: nosniff`. Add only trusted, non-active MIME types to
343
422
  `proxyInlineContentTypes` when browser rendering is required. Search responses
344
423
  are capped by `maxSearchResults`, and list pages by `maxListResults` (both
345
424
  1,000 by default).
346
425
 
426
+ Every signed URL is capped by `maxSignedUrlExpiresIn` (3,600 seconds by
427
+ default). Signed uploads always carry a provider-enforced maximum size, capped
428
+ by `maxSignedUploadBytes`, and require an exact lowercase MIME type from
429
+ `signedUploadContentTypes`. The default direct-upload allowlist contains only
430
+ `application/octet-stream`. Gateway callers may request only the literal
431
+ `attachment` or `inline` response disposition; arbitrary response-header text
432
+ and filenames are rejected. A driver must also advertise
433
+ `signedUploadPolicy.contentType` and `signedUploadPolicy.sizeRange`; otherwise
434
+ the gateway refuses to mint the URL. `createS3StorageDriver()` advertises both
435
+ and uses S3 POST policy conditions. Signed downloads similarly require
436
+ `signedDownloadPolicy.expiresIn`. The S3 factory advertises it only when no
437
+ permanent `publicBaseUrl` was configured, preventing a configured TTL from
438
+ silently returning a non-expiring public link.
439
+
347
440
  The fixed gateway prefix is `/storage`:
348
441
 
349
442
  | Method | Path | Operation |
@@ -394,9 +487,14 @@ try {
394
487
  }
395
488
  ```
396
489
 
490
+ `files-sdk` `NotFound` failures retain `StorageErrorCode.NOT_FOUND`, including
491
+ when a provider adapter and this driver resolve separate copies of `files-sdk`.
492
+ `isStorageError()` likewise recognizes branded and exact legacy structural
493
+ errors produced by a duplicated `@nestm/storage` package copy.
494
+
397
495
  Capability flags cover range reads, native byte-level upload progress,
398
496
  delimiter listing, metadata, cache control, resumable uploads, server-side
399
- copy, and signed transfers.
497
+ copy, conditional promotion, and signed transfers.
400
498
  Provider-specific native clients are intentionally not exposed from the root
401
499
  package.
402
500
 
package/SECURITY.md CHANGED
@@ -23,3 +23,26 @@ cross-store pruning, or dependency compromise are especially useful.
23
23
  For a vulnerability originating in NestJS, `files-sdk`, or a provider SDK,
24
24
  follow that project's security policy as well. You may still report it here
25
25
  privately when this integration needs a mitigation.
26
+
27
+ ## Gateway security boundary
28
+
29
+ Mounting the optional HTTP gateway requires both authentication guards and a
30
+ `StorageGatewayKeyPolicy`. The policy receives parsed paths and must derive the
31
+ provider key space from trusted request context. Do not treat a client prefix
32
+ or object key as a tenant identifier. `unsafeAllowUnscopedKeys` exists only to
33
+ migrate trusted single-tenant deployments and is unsafe on an exposed gateway.
34
+
35
+ Signed URL expiry, signed-upload byte size, and signed-upload MIME are bounded
36
+ by module configuration and cannot be increased by a request. Only `inline`
37
+ and `attachment` content dispositions are accepted. File type still must be
38
+ verified from magic bytes after upload; a signed MIME condition proves only
39
+ what header the uploader supplied. The gateway rejects signed-upload drivers
40
+ that do not advertise provider-enforced content-type and size-range policies.
41
+ It also rejects download adapters that cannot guarantee the requested expiry;
42
+ in particular, the S3 bridge does not treat `publicBaseUrl` links as expiring.
43
+
44
+ For staged uploads on S3, use `promote`/`promoteTo` with the ETag returned by
45
+ the validated `head` or with an immutable provider version. Ordinary `copy`
46
+ does not protect against a staging-key replay between validation and copy.
47
+ Conditional promotion keeps the staging source; delete it only after the
48
+ application's metadata transaction commits.
@@ -1,24 +1,52 @@
1
1
  import { type Adapter, type FilesOptions } from 'files-sdk';
2
+ import { StorageError } from '../storage.error.js';
2
3
  import type { StorageDriver } from '../storage.driver.js';
3
- import type { StorageBody, StorageDownloadOptions, StorageListOptions, StorageListResult, StorageObject, StorageObjectMetadata, StorageOperationOptions, StorageSearchOptions, StorageSignedDownloadOptions, StorageSignedUpload, StorageSignedUploadOptions, StorageUploadOptions, StorageUploadResult } from '../storage.types.js';
4
+ import type { StorageBody, StorageDownloadOptions, StorageListOptions, StorageListResult, StorageObject, StorageObjectMetadata, StorageOperationOptions, StorageConditionalCopyCapability, StoragePromotionOptions, StorageSearchOptions, StorageSignedUploadPolicyCapability, StorageSignedDownloadPolicyCapability, StorageSignedDownloadOptions, StorageSignedUpload, StorageSignedUploadOptions, StorageUploadOptions, StorageUploadResult } from '../storage.types.js';
4
5
  export type FilesSdkDriverOptions<AdapterType extends Adapter> = FilesOptions<AdapterType>;
6
+ /**
7
+ * Optional adapter extension for providers that can conditionally copy an
8
+ * immutable source identity. Plain files-sdk adapters remain fully supported.
9
+ */
10
+ export interface FilesSdkConditionalCopyAdapter {
11
+ readonly conditionalCopy: StorageConditionalCopyCapability;
12
+ promote(sourceKey: string, destinationKey: string, options: StoragePromotionOptions): Promise<void>;
13
+ }
14
+ export interface FilesSdkSignedUploadPolicyAdapter {
15
+ readonly signedUploadPolicy: StorageSignedUploadPolicyCapability;
16
+ }
17
+ export interface FilesSdkSignedDownloadPolicyAdapter {
18
+ readonly signedDownloadPolicy: StorageSignedDownloadPolicyCapability;
19
+ }
20
+ export declare function mapFilesSdkError(error: unknown): StorageError;
5
21
  export declare class FilesSdkStorageDriver<AdapterType extends Adapter = Adapter> implements StorageDriver {
6
22
  #private;
7
23
  constructor(options: FilesSdkDriverOptions<AdapterType>);
8
24
  get name(): string;
9
25
  get capabilities(): {
26
+ nativeUploadProgress: boolean;
27
+ signedUploadPolicy?: {
28
+ contentType: boolean;
29
+ sizeRange: boolean;
30
+ };
31
+ signedUpload: "runtime";
32
+ signedDownloadPolicy?: {
33
+ expiresIn: boolean;
34
+ };
35
+ signedDownload: {
36
+ supported: boolean;
37
+ maxExpiresIn?: number;
38
+ };
39
+ conditionalCopy?: {
40
+ supported: boolean;
41
+ etag: boolean;
42
+ version: boolean;
43
+ };
10
44
  cacheControl: boolean;
11
45
  delimiter: boolean;
12
46
  metadata: boolean;
13
47
  rangeRead: boolean;
14
48
  resumableUpload: boolean;
15
49
  serverSideCopy: boolean;
16
- signedDownload: {
17
- supported: boolean;
18
- maxExpiresIn?: number;
19
- };
20
- signedUpload: "runtime";
21
- nativeUploadProgress: boolean;
22
50
  };
23
51
  upload(key: string, body: StorageBody, options?: StorageUploadOptions): Promise<StorageUploadResult>;
24
52
  download(key: string, options?: StorageDownloadOptions): Promise<StorageObject>;
@@ -27,6 +55,7 @@ export declare class FilesSdkStorageDriver<AdapterType extends Adapter = Adapter
27
55
  delete(key: string, options?: StorageOperationOptions): Promise<void>;
28
56
  copy(sourceKey: string, destinationKey: string, options?: StorageOperationOptions): Promise<void>;
29
57
  move(sourceKey: string, destinationKey: string, options?: StorageOperationOptions): Promise<void>;
58
+ promote(sourceKey: string, destinationKey: string, options: StoragePromotionOptions): Promise<void>;
30
59
  list(options?: StorageListOptions): Promise<StorageListResult>;
31
60
  search(pattern: string | RegExp, options?: StorageSearchOptions): AsyncIterable<StorageObjectMetadata>;
32
61
  signDownload(key: string, options?: StorageSignedDownloadOptions): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"files-sdk.driver.d.ts","sourceRoot":"","sources":["../../src/files-sdk/files-sdk.driver.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,OAAO,EAGZ,KAAK,YAAY,EAUlB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EACV,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EAEvB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,qBAAqB,CAAC,WAAW,SAAS,OAAO,IAC3D,YAAY,CAAC,WAAW,CAAC,CAAC;AAyS5B,qBAAa,qBAAqB,CAChC,WAAW,SAAS,OAAO,GAAG,OAAO,CACrC,YAAW,aAAa;;gBAIZ,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC;IAKvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,YAAY;;;;;;;;;;;;;MAaf;IAEK,MAAM,CACV,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC;IAQzB,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,aAAa,CAAC;IAUnB,IAAI,CACR,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,qBAAqB,CAAC;IAMjC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrE,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC;IAMhB,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC;IAMV,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAapE,MAAM,CACJ,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,aAAa,CAAC,qBAAqB,CAAC;IAoBvC,YAAY,CACV,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,4BAA4B,GACrC,OAAO,CAAC,MAAM,CAAC;IAMlB,UAAU,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,mBAAmB,CAAC;CAahC;AAED,wBAAgB,oBAAoB,CAAC,WAAW,SAAS,OAAO,EAC9D,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAC1C,qBAAqB,CAAC,WAAW,CAAC,CAEpC"}
1
+ {"version":3,"file":"files-sdk.driver.d.ts","sourceRoot":"","sources":["../../src/files-sdk/files-sdk.driver.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,OAAO,EAGZ,KAAK,YAAY,EAUlB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,YAAY,EAGb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EACV,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,gCAAgC,EAChC,uBAAuB,EAEvB,oBAAoB,EACpB,mCAAmC,EACnC,qCAAqC,EACrC,4BAA4B,EAC5B,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,qBAAqB,CAAC,WAAW,SAAS,OAAO,IAC3D,YAAY,CAAC,WAAW,CAAC,CAAC;AAE5B;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,eAAe,EAAE,gCAAgC,CAAC;IAC3D,OAAO,CACL,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,kBAAkB,EAAE,mCAAmC,CAAC;CAClE;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,oBAAoB,EAAE,qCAAqC,CAAC;CACtE;AAwID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAwD7D;AAiPD,qBAAa,qBAAqB,CAChC,WAAW,SAAS,OAAO,GAAG,OAAO,CACrC,YAAW,aAAa;;gBAQZ,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC;IAQvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;MAwBf;IAEK,MAAM,CACV,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC;IAQzB,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,aAAa,CAAC;IAUnB,IAAI,CACR,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,qBAAqB,CAAC;IAMjC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrE,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC;IAMhB,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC;IAMhB,OAAO,CACL,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC;IAoBV,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAapE,MAAM,CACJ,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,aAAa,CAAC,qBAAqB,CAAC;IAoBvC,YAAY,CACV,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,4BAA4B,GACrC,OAAO,CAAC,MAAM,CAAC;IAMlB,UAAU,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,mBAAmB,CAAC;CAahC;AAED,wBAAgB,oBAAoB,CAAC,WAAW,SAAS,OAAO,EAC9D,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAC1C,qBAAqB,CAAC,WAAW,CAAC,CAEpC"}
@@ -1,29 +1,121 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import { Files, FilesError, } from 'files-sdk';
3
- import { StorageError, StorageErrorCode } from '../storage.error.js';
3
+ import { StorageError, StorageErrorCode, isStorageError, } from '../storage.error.js';
4
4
  import { getFilesSdkUploadControl } from '../storage-upload-control.js';
5
- function mapFilesError(error) {
6
- if (error instanceof StorageError) {
5
+ function conditionalCopyAdapterOf(adapter) {
6
+ if (typeof adapter !== 'object' ||
7
+ adapter === null ||
8
+ !('conditionalCopy' in adapter) ||
9
+ !('promote' in adapter)) {
10
+ return undefined;
11
+ }
12
+ 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' ||
18
+ typeof candidate.promote !== 'function') {
19
+ return undefined;
20
+ }
21
+ return candidate;
22
+ }
23
+ function signedUploadPolicyAdapterOf(adapter) {
24
+ if (typeof adapter !== 'object' ||
25
+ adapter === null ||
26
+ !('signedUploadPolicy' in adapter)) {
27
+ return undefined;
28
+ }
29
+ const candidate = adapter;
30
+ const capability = candidate.signedUploadPolicy;
31
+ if (capability === undefined ||
32
+ typeof capability.contentType !== 'boolean' ||
33
+ typeof capability.sizeRange !== 'boolean') {
34
+ return undefined;
35
+ }
36
+ return candidate;
37
+ }
38
+ function signedDownloadPolicyAdapterOf(adapter) {
39
+ if (typeof adapter !== 'object' ||
40
+ adapter === null ||
41
+ !('signedDownloadPolicy' in adapter)) {
42
+ return undefined;
43
+ }
44
+ const candidate = adapter;
45
+ const capability = candidate.signedDownloadPolicy;
46
+ if (capability === undefined || typeof capability.expiresIn !== 'boolean') {
47
+ return undefined;
48
+ }
49
+ return candidate;
50
+ }
51
+ function isFilesErrorCode(value) {
52
+ return (value === 'NotFound' ||
53
+ value === 'Unauthorized' ||
54
+ value === 'Conflict' ||
55
+ value === 'ReadOnly' ||
56
+ value === 'Provider');
57
+ }
58
+ function isFilesErrorLike(error) {
59
+ if (error instanceof FilesError) {
60
+ return true;
61
+ }
62
+ if (!(error instanceof Error)) {
63
+ return false;
64
+ }
65
+ try {
66
+ return (error.name === 'FilesError' &&
67
+ typeof error.message === 'string' &&
68
+ 'code' in error &&
69
+ isFilesErrorCode(error.code) &&
70
+ 'aborted' in error &&
71
+ typeof error.aborted === 'boolean' &&
72
+ 'timedOut' in error &&
73
+ typeof error.timedOut === 'boolean' &&
74
+ 'permanent' in error &&
75
+ typeof error.permanent === 'boolean');
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function unwrapFilesError(error) {
82
+ const seen = new Set();
83
+ let current = error;
84
+ while (current.code === 'Provider' &&
85
+ !current.aborted &&
86
+ !current.timedOut &&
87
+ !current.permanent &&
88
+ isFilesErrorLike(current.cause) &&
89
+ current.message === current.cause.message &&
90
+ !seen.has(current.cause)) {
91
+ seen.add(current);
92
+ current = current.cause;
93
+ }
94
+ return current;
95
+ }
96
+ export function mapFilesSdkError(error) {
97
+ if (isStorageError(error)) {
7
98
  return error;
8
99
  }
9
- if (!(error instanceof FilesError)) {
100
+ if (!isFilesErrorLike(error)) {
10
101
  return new StorageError(error instanceof Error ? error.message : String(error), {
11
102
  cause: error,
12
103
  code: StorageErrorCode.PROVIDER,
13
104
  });
14
105
  }
15
- if (error.cause instanceof StorageError) {
16
- return error.cause;
106
+ const filesError = unwrapFilesError(error);
107
+ if (isStorageError(filesError.cause)) {
108
+ return filesError.cause;
17
109
  }
18
110
  let code;
19
- if (error.timedOut) {
111
+ if (filesError.timedOut) {
20
112
  code = StorageErrorCode.TIMEOUT;
21
113
  }
22
- else if (error.aborted) {
114
+ else if (filesError.aborted) {
23
115
  code = StorageErrorCode.ABORTED;
24
116
  }
25
117
  else {
26
- switch (error.code) {
118
+ switch (filesError.code) {
27
119
  case 'NotFound':
28
120
  code = StorageErrorCode.NOT_FOUND;
29
121
  break;
@@ -37,18 +129,18 @@ function mapFilesError(error) {
37
129
  code = StorageErrorCode.READ_ONLY;
38
130
  break;
39
131
  case 'Provider':
40
- code = /(?:not supported|does not support|unsupported)/iu.test(error.message)
132
+ code = /(?:not supported|does not support|unsupported)/iu.test(filesError.message)
41
133
  ? StorageErrorCode.NOT_SUPPORTED
42
134
  : StorageErrorCode.PROVIDER;
43
135
  break;
44
136
  }
45
137
  }
46
- return new StorageError(error.message, {
47
- aborted: error.aborted,
48
- cause: error.cause ?? error,
138
+ return new StorageError(filesError.message, {
139
+ aborted: filesError.aborted,
140
+ cause: filesError.cause ?? error,
49
141
  code,
50
- permanent: error.permanent,
51
- timedOut: error.timedOut,
142
+ permanent: filesError.permanent,
143
+ timedOut: filesError.timedOut,
52
144
  });
53
145
  }
54
146
  function mapRetryOptions(retries) {
@@ -60,7 +152,7 @@ function mapRetryOptions(retries) {
60
152
  ...(retries.backoff !== undefined && {
61
153
  backoff: ({ attempt, error }) => retries.backoff?.({
62
154
  attempt,
63
- error: mapFilesError(error),
155
+ error: mapFilesSdkError(error),
64
156
  }) ?? 0,
65
157
  }),
66
158
  };
@@ -114,7 +206,7 @@ function normalizeDownloadStream(source) {
114
206
  await activeReader.cancel(reason);
115
207
  }
116
208
  catch (error) {
117
- throw mapFilesError(error);
209
+ throw mapFilesSdkError(error);
118
210
  }
119
211
  finally {
120
212
  release();
@@ -137,7 +229,7 @@ function normalizeDownloadStream(source) {
137
229
  }
138
230
  catch (error) {
139
231
  release();
140
- controller.error(mapFilesError(error));
232
+ controller.error(mapFilesSdkError(error));
141
233
  }
142
234
  },
143
235
  });
@@ -256,9 +348,15 @@ function uploadResultOf(result) {
256
348
  export class FilesSdkStorageDriver {
257
349
  #files;
258
350
  #name;
351
+ #conditionalCopy;
352
+ #signedUploadPolicy;
353
+ #signedDownloadPolicy;
259
354
  constructor(options) {
260
355
  this.#files = new Files(options);
261
356
  this.#name = options.adapter.name;
357
+ this.#conditionalCopy = conditionalCopyAdapterOf(options.adapter);
358
+ this.#signedUploadPolicy = signedUploadPolicyAdapterOf(options.adapter);
359
+ this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(options.adapter);
262
360
  }
263
361
  get name() {
264
362
  return this.#name;
@@ -272,8 +370,19 @@ export class FilesSdkStorageDriver {
272
370
  rangeRead: capabilities.rangeRead,
273
371
  resumableUpload: capabilities.multipart,
274
372
  serverSideCopy: capabilities.serverSideCopy,
373
+ ...(this.#conditionalCopy !== undefined && {
374
+ conditionalCopy: { ...this.#conditionalCopy.conditionalCopy },
375
+ }),
275
376
  signedDownload: { ...capabilities.signedUrl },
377
+ ...(this.#signedDownloadPolicy !== undefined && {
378
+ signedDownloadPolicy: {
379
+ ...this.#signedDownloadPolicy.signedDownloadPolicy,
380
+ },
381
+ }),
276
382
  signedUpload: 'runtime',
383
+ ...(this.#signedUploadPolicy !== undefined && {
384
+ signedUploadPolicy: { ...this.#signedUploadPolicy.signedUploadPolicy },
385
+ }),
277
386
  nativeUploadProgress: capabilities.uploadProgress,
278
387
  };
279
388
  }
@@ -306,6 +415,18 @@ export class FilesSdkStorageDriver {
306
415
  move(sourceKey, destinationKey, options) {
307
416
  return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
308
417
  }
418
+ promote(sourceKey, destinationKey, options) {
419
+ const adapter = this.#conditionalCopy;
420
+ if (adapter === undefined || !adapter.conditionalCopy.supported) {
421
+ return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
422
+ code: StorageErrorCode.NOT_SUPPORTED,
423
+ key: sourceKey,
424
+ operation: 'promote',
425
+ permanent: true,
426
+ }));
427
+ }
428
+ return this.#call(() => adapter.promote(sourceKey, destinationKey, options));
429
+ }
309
430
  async list(options) {
310
431
  return this.#call(async () => {
311
432
  const result = await this.#files.list(listOptions(options));
@@ -328,7 +449,7 @@ export class FilesSdkStorageDriver {
328
449
  }
329
450
  }
330
451
  catch (error) {
331
- throw mapFilesError(error);
452
+ throw mapFilesSdkError(error);
332
453
  }
333
454
  }
334
455
  signDownload(key, options) {
@@ -342,7 +463,7 @@ export class FilesSdkStorageDriver {
342
463
  return await operation();
343
464
  }
344
465
  catch (error) {
345
- throw mapFilesError(error);
466
+ throw mapFilesSdkError(error);
346
467
  }
347
468
  }
348
469
  }