@nestm/storage 0.1.0-alpha.2 → 0.1.0-alpha.4
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.
- package/CHANGELOG.md +44 -0
- package/README.md +196 -15
- package/SECURITY.md +23 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +31 -2
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +141 -20
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +21 -0
- package/dist/files-sdk/fs/index.d.ts.map +1 -0
- package/dist/files-sdk/fs/index.js +20 -0
- package/dist/files-sdk/fs/index.js.map +1 -0
- package/dist/files-sdk/index.d.ts +1 -1
- package/dist/files-sdk/index.d.ts.map +1 -1
- package/dist/files-sdk/index.js.map +1 -1
- package/dist/files-sdk/provider/index.d.ts +54 -0
- package/dist/files-sdk/provider/index.d.ts.map +1 -0
- package/dist/files-sdk/provider/index.js +80 -0
- package/dist/files-sdk/provider/index.js.map +1 -0
- package/dist/files-sdk/s3/index.d.ts +25 -0
- package/dist/files-sdk/s3/index.d.ts.map +1 -0
- package/dist/files-sdk/s3/index.js +119 -0
- package/dist/files-sdk/s3/index.js.map +1 -0
- package/dist/gateway/index.d.ts +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -1
- package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -1
- package/dist/gateway/storage-gateway.controller.d.ts +12 -11
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.controller.js +209 -65
- package/dist/gateway/storage-gateway.controller.js.map +1 -1
- package/dist/gateway/storage-gateway.guard.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.guard.js.map +1 -1
- package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.module.js +60 -1
- package/dist/gateway/storage-gateway.module.js.map +1 -1
- package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
- package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.tokens.js +1 -0
- package/dist/gateway/storage-gateway.tokens.js.map +1 -1
- package/dist/gateway/storage-gateway.types.d.ts +53 -10
- package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.types.js.map +1 -1
- package/dist/inject-storage.decorator.js.map +1 -1
- package/dist/storage-upload-control.d.ts.map +1 -1
- package/dist/storage.client.d.ts +8 -1
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +40 -0
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +6 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.error.d.ts +13 -10
- package/dist/storage.error.d.ts.map +1 -1
- package/dist/storage.error.js +33 -1
- package/dist/storage.error.js.map +1 -1
- package/dist/storage.module.d.ts.map +1 -1
- package/dist/storage.module.js.map +1 -1
- package/dist/storage.service.d.ts.map +1 -1
- package/dist/storage.service.js.map +1 -1
- package/dist/storage.tokens.js.map +1 -1
- package/dist/storage.types.d.ts +39 -1
- package/dist/storage.types.d.ts.map +1 -1
- package/dist/storage.types.js.map +1 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +38 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @nestm/storage
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.4
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 79ad3af: Add runtime provider selection and a package-owned filesystem driver.
|
|
8
|
+
|
|
9
|
+
`@nestm/storage/files-sdk/provider` builds a driver from a provider slug carried
|
|
10
|
+
as data — `createProviderStorageDriver({ provider: 's3' | 'gcs' | 'azure' | 'r2'
|
|
11
|
+
| 'fs' | … })` — importing that provider's adapter, and only that one, on
|
|
12
|
+
demand. A deployment now selects its store with an environment variable and
|
|
13
|
+
installs a single native SDK instead of the application hard-coding a driver per
|
|
14
|
+
backend. The same entry point exposes the provider catalog (`listStorageProviders`,
|
|
15
|
+
`getStorageProvider`, `listStorageProviderEnvVars`,
|
|
16
|
+
`listStorageProviderSecretEnvVars`, `isStorageProvider`) as pure data, so config
|
|
17
|
+
validation and health checks can read a provider's env contract without loading
|
|
18
|
+
an adapter. An unknown slug fails closed with `INVALID_ARGUMENT` before anything
|
|
19
|
+
is imported.
|
|
20
|
+
|
|
21
|
+
`@nestm/storage/files-sdk/fs` adds `createFsStorageDriver` for local filesystem
|
|
22
|
+
storage, mirroring the S3 factory. Its adapter reaches only `node:fs`, so it adds
|
|
23
|
+
no native SDK to an install.
|
|
24
|
+
|
|
25
|
+
`@nestm/storage/files-sdk/s3` additionally exports `withS3Capabilities`, which
|
|
26
|
+
applies S3's conditional-copy promotion and signed-policy declarations to an
|
|
27
|
+
adapter built by `s3(...)`. The provider factory uses it so the `s3` slug keeps
|
|
28
|
+
those capabilities without re-deriving `S3AdapterOptions` from flat config.
|
|
29
|
+
`EnhancedS3Adapter` is now `S3StorageAdapter`; the type was not previously
|
|
30
|
+
exported.
|
|
31
|
+
|
|
32
|
+
## 0.1.0-alpha.3
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- b947634: Harden storage integration boundaries with cross-copy-safe `StorageError`
|
|
37
|
+
detection, a package-owned S3 driver factory, conditional staged-object
|
|
38
|
+
promotion, and a mandatory parsed key policy plus signed-transfer limits for
|
|
39
|
+
the optional HTTP gateway.
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- b947634: Map structurally branded `files-sdk` errors, including errors wrapped across
|
|
44
|
+
duplicate package copies, so missing objects retain the `NOT_FOUND` storage
|
|
45
|
+
error code.
|
|
46
|
+
|
|
3
47
|
## 0.1.0-alpha.2
|
|
4
48
|
|
|
5
49
|
### 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
|
|
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.3
|
|
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
|
-
|
|
86
|
-
bridge, and register the
|
|
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:
|
|
108
|
-
adapter:
|
|
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
|
-
|
|
198
|
-
adapter:
|
|
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
|
],
|
|
@@ -216,11 +225,77 @@ modules retain their own DI scope rather than mutating an application-wide
|
|
|
216
225
|
registry. Names are case-sensitive and cannot contain leading or trailing
|
|
217
226
|
whitespace.
|
|
218
227
|
|
|
228
|
+
### Select the provider at runtime
|
|
229
|
+
|
|
230
|
+
An application that ships to more than one environment usually cannot name its
|
|
231
|
+
provider at build time. `createProviderStorageDriver` takes the slug as data and
|
|
232
|
+
imports that provider's adapter — and only that one — on demand, so a deployment
|
|
233
|
+
picks its store with an environment variable and installs one native SDK:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { createProviderStorageDriver } from '@nestm/storage/files-sdk/provider';
|
|
237
|
+
|
|
238
|
+
StorageModule.forRootAsync({
|
|
239
|
+
imports: [ConfigModule],
|
|
240
|
+
stores: [
|
|
241
|
+
{
|
|
242
|
+
name: 'media',
|
|
243
|
+
inject: [ConfigService],
|
|
244
|
+
useFactory: (config: ConfigService) =>
|
|
245
|
+
createProviderStorageDriver({
|
|
246
|
+
provider: config.getOrThrow('STORAGE_PROVIDER'),
|
|
247
|
+
prefix: config.get('STORAGE_PREFIX'),
|
|
248
|
+
config: {
|
|
249
|
+
bucket: config.get('STORAGE_BUCKET'),
|
|
250
|
+
region: config.get('STORAGE_REGION'),
|
|
251
|
+
root: config.get('STORAGE_ROOT'),
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`config` is one flat bag of provider settings — `bucket` and `region` for an
|
|
260
|
+
object store, `root` for the filesystem, `accountName` and `container` for
|
|
261
|
+
Azure. Each provider reads what it needs and ignores the rest, so the same shape
|
|
262
|
+
survives a provider change. Credentials may be omitted wherever the provider's
|
|
263
|
+
SDK resolves its own chain (an IAM role, Application Default Credentials, a
|
|
264
|
+
shared profile).
|
|
265
|
+
|
|
266
|
+
An unknown slug fails closed with `INVALID_ARGUMENT` before anything is
|
|
267
|
+
imported. Validate untrusted input up front with `isStorageProvider`, and drive
|
|
268
|
+
config validation from the catalog rather than a hand-kept list:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import {
|
|
272
|
+
getStorageProvider,
|
|
273
|
+
isStorageProvider,
|
|
274
|
+
listStorageProviders,
|
|
275
|
+
listStorageProviderSecretEnvVars,
|
|
276
|
+
} from '@nestm/storage/files-sdk/provider';
|
|
277
|
+
|
|
278
|
+
listStorageProviders().map((provider) => provider.slug); // 'akamai', 'alibaba', …
|
|
279
|
+
getStorageProvider('gcs')?.peerDeps; // ['@google-cloud/storage', …]
|
|
280
|
+
listStorageProviderSecretEnvVars('s3').map((variable) => variable.key);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The catalog is pure data and pulls in no adapter, so it is safe in config UIs,
|
|
284
|
+
health checks, and startup validation.
|
|
285
|
+
|
|
286
|
+
The `s3` slug additionally carries the conditional-promotion and signed-policy
|
|
287
|
+
capabilities described under
|
|
288
|
+
[Race-free staged-object promotion](#race-free-staged-object-promotion); every
|
|
289
|
+
other provider exposes exactly what its adapter declares. When the provider _is_
|
|
290
|
+
known at build time, import `@nestm/storage/files-sdk/s3` or
|
|
291
|
+
`@nestm/storage/files-sdk/fs` directly and skip the indirection.
|
|
292
|
+
|
|
219
293
|
## Storage API
|
|
220
294
|
|
|
221
295
|
`StorageClient` exposes:
|
|
222
296
|
|
|
223
297
|
- `upload`, `downloadStream`, `head`, `exists`, `delete`, `copy`, and `move`;
|
|
298
|
+
- conditional staged-object `promote` when the driver advertises it;
|
|
224
299
|
- `list`, cursor-aware `listAll`, and lazy `search`;
|
|
225
300
|
- `signDownload` and discriminated PUT/POST `signUpload`;
|
|
226
301
|
- `uploadMany`, `downloadMany`, `headMany`, `existsMany`, and `deleteMany`;
|
|
@@ -250,6 +325,37 @@ Node `Readable` uploads are accepted and converted to Web streams without
|
|
|
250
325
|
buffering. Provider capability gaps fail closed with `StorageError` rather than
|
|
251
326
|
silently discarding a range, metadata, or cache-control request.
|
|
252
327
|
|
|
328
|
+
### Race-free staged-object promotion
|
|
329
|
+
|
|
330
|
+
The S3 bridge advertises ETag- and version-conditional server-side copy. This
|
|
331
|
+
lets an application validate a staged object and copy that exact source to its
|
|
332
|
+
final key instead of re-reading whichever bytes occupy the staging key later:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { StorageError, StorageErrorCode } from '@nestm/storage';
|
|
336
|
+
|
|
337
|
+
const staged = await media.head(stagingKey);
|
|
338
|
+
if (staged.etag === undefined) {
|
|
339
|
+
throw new StorageError('Provider did not return a source ETag.', {
|
|
340
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Validate size, declared MIME, and magic bytes before this call.
|
|
345
|
+
await media.file(stagingKey).promoteTo(finalKey, {
|
|
346
|
+
sourceEtag: staged.etag,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Commit ready metadata first. Promotion deliberately retains the staged
|
|
350
|
+
// object so a failed database commit remains recoverable.
|
|
351
|
+
await media.delete(stagingKey);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`sourceVersion` can select an immutable S3 version and may be combined with
|
|
355
|
+
`sourceEtag`. A promotion without either identity is rejected. Drivers that do
|
|
356
|
+
not publish `capabilities.conditionalCopy` fail with `NOT_SUPPORTED` rather
|
|
357
|
+
than falling back to an unsafe ordinary copy.
|
|
358
|
+
|
|
253
359
|
### Resumable uploads
|
|
254
360
|
|
|
255
361
|
```ts
|
|
@@ -301,20 +407,42 @@ The gateway lives at `@nestm/storage/gateway` and is never mounted by
|
|
|
301
407
|
`StorageModule`.
|
|
302
408
|
|
|
303
409
|
```ts
|
|
304
|
-
import { Module } from '@nestjs/common';
|
|
410
|
+
import { Injectable, Module } from '@nestjs/common';
|
|
305
411
|
import {
|
|
306
412
|
StorageGatewayModule,
|
|
307
413
|
StorageGatewayOperation,
|
|
414
|
+
type StorageGatewayKeyPolicy,
|
|
308
415
|
} from '@nestm/storage/gateway';
|
|
309
416
|
|
|
417
|
+
@Injectable()
|
|
418
|
+
class TenantStorageKeyPolicy implements StorageGatewayKeyPolicy {
|
|
419
|
+
resolve({ input, request, target }) {
|
|
420
|
+
const tenantId = tenantIdFromAuthenticatedRequest(request);
|
|
421
|
+
const root = `tenants/${base64url(tenantId)}`;
|
|
422
|
+
if (target === 'pattern') {
|
|
423
|
+
// Search is already constrained by the separately resolved prefix.
|
|
424
|
+
return input?.value ?? '*';
|
|
425
|
+
}
|
|
426
|
+
return `${root}/${input?.value ?? ''}`;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@Module({
|
|
431
|
+
providers: [TenantStorageKeyPolicy],
|
|
432
|
+
exports: [TenantStorageKeyPolicy],
|
|
433
|
+
})
|
|
434
|
+
class StoragePolicyModule {}
|
|
435
|
+
|
|
310
436
|
@Module({
|
|
311
437
|
imports: [
|
|
312
438
|
AppStorageModule,
|
|
313
439
|
AuthModule,
|
|
440
|
+
StoragePolicyModule,
|
|
314
441
|
StorageGatewayModule.register({
|
|
315
|
-
imports: [AppStorageModule, AuthModule],
|
|
442
|
+
imports: [AppStorageModule, AuthModule, StoragePolicyModule],
|
|
316
443
|
store: 'media',
|
|
317
444
|
guards: [JwtAuthGuard],
|
|
445
|
+
keyPolicy: TenantStorageKeyPolicy,
|
|
318
446
|
mode: 'hybrid',
|
|
319
447
|
operations: [
|
|
320
448
|
StorageGatewayOperation.DOWNLOAD,
|
|
@@ -325,6 +453,9 @@ import {
|
|
|
325
453
|
StorageGatewayOperation.SIGN_UPLOAD,
|
|
326
454
|
],
|
|
327
455
|
maxUploadBytes: 100 * 1024 * 1024,
|
|
456
|
+
maxSignedUploadBytes: 10 * 1024 * 1024,
|
|
457
|
+
signedUploadContentTypes: ['image/jpeg', 'image/png'],
|
|
458
|
+
maxSignedUrlExpiresIn: 900,
|
|
328
459
|
maxListResults: 1000,
|
|
329
460
|
maxSearchResults: 1000,
|
|
330
461
|
proxyInlineContentTypes: ['image/jpeg', 'image/png'],
|
|
@@ -338,12 +469,39 @@ Registration fails without at least one existing Nest guard. The only bypass is
|
|
|
338
469
|
the explicit `allowUnauthenticated: true` development escape hatch. Operations
|
|
339
470
|
are deny-by-default and must be allowlisted individually.
|
|
340
471
|
|
|
472
|
+
Registration also fails without a `keyPolicy`. Guards answer whether a request
|
|
473
|
+
may reach the gateway; the key policy independently resolves every parsed
|
|
474
|
+
`key`, `prefix`, search `pattern`, `from`, and `to` value to the exact provider
|
|
475
|
+
path. It runs even when a list/search prefix was omitted, so the policy can
|
|
476
|
+
always impose a tenant root. Key-policy providers may be request scoped.
|
|
477
|
+
Returned paths are parsed again and reject absolute paths, backslashes, control
|
|
478
|
+
characters, empty segments, and dot/parent segments.
|
|
479
|
+
|
|
480
|
+
Existing single-tenant applications can temporarily set
|
|
481
|
+
`unsafeAllowUnscopedKeys: true` instead of `keyPolicy`. The name is intentional:
|
|
482
|
+
it preserves caller-controlled provider keys and must not be used on an exposed
|
|
483
|
+
or multi-tenant gateway. It cannot be combined with `keyPolicy`.
|
|
484
|
+
|
|
341
485
|
Proxy downloads default to `Content-Disposition: attachment` and always send
|
|
342
486
|
`X-Content-Type-Options: nosniff`. Add only trusted, non-active MIME types to
|
|
343
487
|
`proxyInlineContentTypes` when browser rendering is required. Search responses
|
|
344
488
|
are capped by `maxSearchResults`, and list pages by `maxListResults` (both
|
|
345
489
|
1,000 by default).
|
|
346
490
|
|
|
491
|
+
Every signed URL is capped by `maxSignedUrlExpiresIn` (3,600 seconds by
|
|
492
|
+
default). Signed uploads always carry a provider-enforced maximum size, capped
|
|
493
|
+
by `maxSignedUploadBytes`, and require an exact lowercase MIME type from
|
|
494
|
+
`signedUploadContentTypes`. The default direct-upload allowlist contains only
|
|
495
|
+
`application/octet-stream`. Gateway callers may request only the literal
|
|
496
|
+
`attachment` or `inline` response disposition; arbitrary response-header text
|
|
497
|
+
and filenames are rejected. A driver must also advertise
|
|
498
|
+
`signedUploadPolicy.contentType` and `signedUploadPolicy.sizeRange`; otherwise
|
|
499
|
+
the gateway refuses to mint the URL. `createS3StorageDriver()` advertises both
|
|
500
|
+
and uses S3 POST policy conditions. Signed downloads similarly require
|
|
501
|
+
`signedDownloadPolicy.expiresIn`. The S3 factory advertises it only when no
|
|
502
|
+
permanent `publicBaseUrl` was configured, preventing a configured TTL from
|
|
503
|
+
silently returning a non-expiring public link.
|
|
504
|
+
|
|
347
505
|
The fixed gateway prefix is `/storage`:
|
|
348
506
|
|
|
349
507
|
| Method | Path | Operation |
|
|
@@ -394,9 +552,14 @@ try {
|
|
|
394
552
|
}
|
|
395
553
|
```
|
|
396
554
|
|
|
555
|
+
`files-sdk` `NotFound` failures retain `StorageErrorCode.NOT_FOUND`, including
|
|
556
|
+
when a provider adapter and this driver resolve separate copies of `files-sdk`.
|
|
557
|
+
`isStorageError()` likewise recognizes branded and exact legacy structural
|
|
558
|
+
errors produced by a duplicated `@nestm/storage` package copy.
|
|
559
|
+
|
|
397
560
|
Capability flags cover range reads, native byte-level upload progress,
|
|
398
561
|
delimiter listing, metadata, cache control, resumable uploads, server-side
|
|
399
|
-
copy, and signed transfers.
|
|
562
|
+
copy, conditional promotion, and signed transfers.
|
|
400
563
|
Provider-specific native clients are intentionally not exposed from the root
|
|
401
564
|
package.
|
|
402
565
|
|
|
@@ -412,8 +575,26 @@ StorageModule.forRoot({
|
|
|
412
575
|
});
|
|
413
576
|
```
|
|
414
577
|
|
|
415
|
-
For local filesystem storage,
|
|
416
|
-
`
|
|
578
|
+
For local filesystem storage, use the package-owned factory. The adapter reaches
|
|
579
|
+
only `node:fs`, so it needs no native SDK:
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
import { createFsStorageDriver } from '@nestm/storage/files-sdk/fs';
|
|
583
|
+
|
|
584
|
+
StorageModule.forRoot({
|
|
585
|
+
stores: [
|
|
586
|
+
{
|
|
587
|
+
name: 'artifacts',
|
|
588
|
+
driver: createFsStorageDriver({ adapter: { root: './var/artifacts' } }),
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
Bodies are written verbatim at `<root>/<key>`. A `<key>.meta.json` sidecar beside
|
|
595
|
+
each one carries the content type, ETag, and custom metadata a filesystem has
|
|
596
|
+
nowhere else to put; sidecars never surface as keys, and uploading a key ending
|
|
597
|
+
in `.meta.json` fails closed rather than colliding with one.
|
|
417
598
|
|
|
418
599
|
## License
|
|
419
600
|
|
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,7 +1,23 @@
|
|
|
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>);
|
|
@@ -13,11 +29,23 @@ export declare class FilesSdkStorageDriver<AdapterType extends Adapter = Adapter
|
|
|
13
29
|
rangeRead: boolean;
|
|
14
30
|
resumableUpload: boolean;
|
|
15
31
|
serverSideCopy: boolean;
|
|
32
|
+
conditionalCopy?: {
|
|
33
|
+
supported: boolean;
|
|
34
|
+
etag: boolean;
|
|
35
|
+
version: boolean;
|
|
36
|
+
};
|
|
16
37
|
signedDownload: {
|
|
17
38
|
supported: boolean;
|
|
18
39
|
maxExpiresIn?: number;
|
|
19
40
|
};
|
|
20
|
-
|
|
41
|
+
signedDownloadPolicy?: {
|
|
42
|
+
expiresIn: boolean;
|
|
43
|
+
};
|
|
44
|
+
signedUpload: 'runtime';
|
|
45
|
+
signedUploadPolicy?: {
|
|
46
|
+
contentType: boolean;
|
|
47
|
+
sizeRange: boolean;
|
|
48
|
+
};
|
|
21
49
|
nativeUploadProgress: boolean;
|
|
22
50
|
};
|
|
23
51
|
upload(key: string, body: StorageBody, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"files-sdk.driver.d.ts","sourceRoot":"","sources":["../../src/files-sdk/files-sdk.driver.ts"],"names":[],"mappings":"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"}
|