@nestm/storage 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +51 -0
- package/README.md +200 -3
- package/dist/artifacts/artifact-storage.d.ts +161 -0
- package/dist/artifacts/artifact-storage.d.ts.map +1 -0
- package/dist/artifacts/artifact-storage.js +452 -0
- package/dist/artifacts/artifact-storage.js.map +1 -0
- package/dist/artifacts/crypto/codec.d.ts +44 -0
- package/dist/artifacts/crypto/codec.d.ts.map +1 -0
- package/dist/artifacts/crypto/codec.js +273 -0
- package/dist/artifacts/crypto/codec.js.map +1 -0
- package/dist/artifacts/crypto/context.d.ts +48 -0
- package/dist/artifacts/crypto/context.d.ts.map +1 -0
- package/dist/artifacts/crypto/context.js +37 -0
- package/dist/artifacts/crypto/context.js.map +1 -0
- package/dist/artifacts/crypto/dek-cache.d.ts +38 -0
- package/dist/artifacts/crypto/dek-cache.d.ts.map +1 -0
- package/dist/artifacts/crypto/dek-cache.js +137 -0
- package/dist/artifacts/crypto/dek-cache.js.map +1 -0
- package/dist/artifacts/crypto/index.d.ts +25 -0
- package/dist/artifacts/crypto/index.d.ts.map +1 -0
- package/dist/artifacts/crypto/index.js +29 -0
- package/dist/artifacts/crypto/index.js.map +1 -0
- package/dist/artifacts/crypto/key-provider.d.ts +44 -0
- package/dist/artifacts/crypto/key-provider.d.ts.map +1 -0
- package/dist/artifacts/crypto/key-provider.js +36 -0
- package/dist/artifacts/crypto/key-provider.js.map +1 -0
- package/dist/artifacts/crypto/kms-provider.d.ts +16 -0
- package/dist/artifacts/crypto/kms-provider.d.ts.map +1 -0
- package/dist/artifacts/crypto/kms-provider.js +90 -0
- package/dist/artifacts/crypto/kms-provider.js.map +1 -0
- package/dist/artifacts/crypto/local-provider.d.ts +17 -0
- package/dist/artifacts/crypto/local-provider.d.ts.map +1 -0
- package/dist/artifacts/crypto/local-provider.js +58 -0
- package/dist/artifacts/crypto/local-provider.js.map +1 -0
- package/dist/artifacts/env-config.d.ts +15 -0
- package/dist/artifacts/env-config.d.ts.map +1 -0
- package/dist/artifacts/env-config.js +95 -0
- package/dist/artifacts/env-config.js.map +1 -0
- package/dist/artifacts/index.d.ts +6 -0
- package/dist/artifacts/index.d.ts.map +1 -0
- package/dist/artifacts/index.js +6 -0
- package/dist/artifacts/index.js.map +1 -0
- package/dist/artifacts/nest/index.d.ts +29 -0
- package/dist/artifacts/nest/index.d.ts.map +1 -0
- package/dist/artifacts/nest/index.js +129 -0
- package/dist/artifacts/nest/index.js.map +1 -0
- package/dist/artifacts/object-store.d.ts +27 -0
- package/dist/artifacts/object-store.d.ts.map +1 -0
- package/dist/artifacts/object-store.js +198 -0
- package/dist/artifacts/object-store.js.map +1 -0
- package/dist/artifacts/storage-driver.d.ts +48 -0
- package/dist/artifacts/storage-driver.d.ts.map +1 -0
- package/dist/artifacts/storage-driver.js +89 -0
- package/dist/artifacts/storage-driver.js.map +1 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +18 -18
- package/dist/files-sdk/files-sdk.driver.d.ts.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/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 +13 -2
- package/dist/files-sdk/s3/index.d.ts.map +1 -1
- package/dist/files-sdk/s3/index.js +19 -8
- package/dist/files-sdk/s3/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.map +1 -1
- 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.map +1 -1
- package/dist/gateway/storage-gateway.types.d.ts +10 -10
- package/dist/gateway/storage-gateway.types.d.ts.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.map +1 -1
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.error.d.ts +10 -10
- package/dist/storage.error.d.ts.map +1 -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/testing/index.js.map +1 -1
- package/package.json +34 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @nestm/storage
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.5
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 227df5b: Add byte-compatible encrypted artifact storage through the
|
|
8
|
+
`@nestm/storage/artifacts` and `@nestm/storage/artifacts/nest` entry points.
|
|
9
|
+
|
|
10
|
+
The framework-neutral facade stores self-contained CAE1 AES-256-GCM envelopes,
|
|
11
|
+
binds ciphertext to its tenant, artifact, version, and path context, supports
|
|
12
|
+
local and AWS KMS key providers, and preserves authenticated content types,
|
|
13
|
+
bounded reads, versioned bundles, legacy migration reads, and encrypted generic
|
|
14
|
+
objects. The Nest entry point composes artifact and object adapters over two
|
|
15
|
+
named storage clients and clears cached data keys on shutdown.
|
|
16
|
+
|
|
17
|
+
The public protocol keeps the deployed filesystem and object-provider layouts,
|
|
18
|
+
reserves configurable ObjectStore namespaces from artifact ids, validates all
|
|
19
|
+
artifact/version paths, and bounds ZIP entry count and expansion before any
|
|
20
|
+
write. CAE1 now rejects unauthenticated empty content types and writer-side
|
|
21
|
+
oversized headers, KMS verifies the stored key id, DEK cache lifetimes are
|
|
22
|
+
capped at five minutes, filesystem `.ct` collisions are rejected, and sweepers
|
|
23
|
+
skip objects without trustworthy timestamps.
|
|
24
|
+
|
|
25
|
+
## 0.1.0-alpha.4
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 79ad3af: Add runtime provider selection and a package-owned filesystem driver.
|
|
30
|
+
|
|
31
|
+
`@nestm/storage/files-sdk/provider` builds a driver from a provider slug carried
|
|
32
|
+
as data — `createProviderStorageDriver({ provider: 's3' | 'gcs' | 'azure' | 'r2'
|
|
33
|
+
| 'fs' | … })` — importing that provider's adapter, and only that one, on
|
|
34
|
+
demand. A deployment now selects its store with an environment variable and
|
|
35
|
+
installs a single native SDK instead of the application hard-coding a driver per
|
|
36
|
+
backend. The same entry point exposes the provider catalog (`listStorageProviders`,
|
|
37
|
+
`getStorageProvider`, `listStorageProviderEnvVars`,
|
|
38
|
+
`listStorageProviderSecretEnvVars`, `isStorageProvider`) as pure data, so config
|
|
39
|
+
validation and health checks can read a provider's env contract without loading
|
|
40
|
+
an adapter. An unknown slug fails closed with `INVALID_ARGUMENT` before anything
|
|
41
|
+
is imported.
|
|
42
|
+
|
|
43
|
+
`@nestm/storage/files-sdk/fs` adds `createFsStorageDriver` for local filesystem
|
|
44
|
+
storage, mirroring the S3 factory. Its adapter reaches only `node:fs`, so it adds
|
|
45
|
+
no native SDK to an install.
|
|
46
|
+
|
|
47
|
+
`@nestm/storage/files-sdk/s3` additionally exports `withS3Capabilities`, which
|
|
48
|
+
applies S3's conditional-copy promotion and signed-policy declarations to an
|
|
49
|
+
adapter built by `s3(...)`. The provider factory uses it so the `s3` slug keeps
|
|
50
|
+
those capabilities without re-deriving `S3AdapterOptions` from flat config.
|
|
51
|
+
`EnhancedS3Adapter` is now `S3StorageAdapter`; the type was not previously
|
|
52
|
+
exported.
|
|
53
|
+
|
|
3
54
|
## 0.1.0-alpha.3
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ pinned engine explicitly only when the application imports another adapter
|
|
|
33
33
|
such as `files-sdk/gcs`:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pnpm add files-sdk@2.2.
|
|
36
|
+
pnpm add files-sdk@2.2.3
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Install only the native SDKs required by the chosen provider. For example:
|
|
@@ -43,6 +43,9 @@ Install only the native SDKs required by the chosen provider. For example:
|
|
|
43
43
|
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-presigned-post \
|
|
44
44
|
@aws-sdk/s3-request-presigner @aws-sdk/lib-storage
|
|
45
45
|
|
|
46
|
+
# Encrypted artifact storage with AWS KMS
|
|
47
|
+
pnpm add @aws-sdk/client-kms
|
|
48
|
+
|
|
46
49
|
# Google Cloud Storage
|
|
47
50
|
pnpm add @google-cloud/storage google-auth-library
|
|
48
51
|
|
|
@@ -88,6 +91,117 @@ storage errors and operation types, and `StorageUploadControl`. It has no NestJS
|
|
|
88
91
|
runtime or declaration imports. Provider adapters remain available through
|
|
89
92
|
`@nestm/storage/files-sdk`.
|
|
90
93
|
|
|
94
|
+
## Encrypted artifact storage
|
|
95
|
+
|
|
96
|
+
The optional `@nestm/storage/artifacts` entry point adds a framework-neutral
|
|
97
|
+
artifact facade over the same provider drivers. Every write is a self-contained
|
|
98
|
+
CAE1 AES-256-GCM envelope; plaintext mode does not exist. The authenticated
|
|
99
|
+
context binds each object to its tenant scope, artifact id, version, path, and
|
|
100
|
+
content type so moving ciphertext to another address fails closed.
|
|
101
|
+
|
|
102
|
+
Install `@aws-sdk/client-kms` in every artifact-storage consumer. It is an
|
|
103
|
+
optional peer so core-only applications do not download AWS KMS, while artifact
|
|
104
|
+
applications can choose either the local key provider or KMS at runtime.
|
|
105
|
+
|
|
106
|
+
Set `ARTIFACT_KEY_PROVIDER=local` with a base64-encoded 32-byte `ARTIFACT_KEK`
|
|
107
|
+
(`ARTIFACT_KEK_NAME` is optional), or set `ARTIFACT_KEY_PROVIDER=kms` with
|
|
108
|
+
`ARTIFACT_KMS_KEY_ID` (`ARTIFACT_KMS_REGION` is optional). Invalid or missing
|
|
109
|
+
key configuration aborts startup. `ARTIFACT_ENCRYPTION_READ_LEGACY=true` is a
|
|
110
|
+
temporary, explicit plaintext-read migration flag and never enables plaintext
|
|
111
|
+
writes.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import {
|
|
115
|
+
artifactScope,
|
|
116
|
+
artifactStorageConfigFromEnv,
|
|
117
|
+
createArtifactStorage,
|
|
118
|
+
storageCryptoFromEnv,
|
|
119
|
+
} from '@nestm/storage/artifacts';
|
|
120
|
+
|
|
121
|
+
const storage = await createArtifactStorage(
|
|
122
|
+
artifactStorageConfigFromEnv(process.env),
|
|
123
|
+
storageCryptoFromEnv(process.env),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const ref = artifactScope({
|
|
127
|
+
organizationId: 'org-1',
|
|
128
|
+
ownerUserId: 'user-1',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await storage.writeHtml('artifact-1', Buffer.from('<h1>Encrypted</h1>'), {
|
|
132
|
+
scope: ref,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const html = await storage.read('artifact-1', 'index.html', {
|
|
136
|
+
scope: ref,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`writeBundle()` expands a zip into separately authenticated objects, skips
|
|
141
|
+
traversal and reserved-namespace entries, and rejects case-folded duplicate
|
|
142
|
+
paths, more than 1,000 entries, entries larger than 64 MiB, or more than 256 MiB
|
|
143
|
+
total expanded data. Callers may lower those limits per write. `read()` and
|
|
144
|
+
`readWithInfo()` support directory-to-`index.html` resolution, optional
|
|
145
|
+
plaintext-size limits, and version-bound paths.
|
|
146
|
+
|
|
147
|
+
`ObjectStore` exposes the same encryption guarantees for non-artifact objects
|
|
148
|
+
such as upload staging and organization media. Keys must begin with a configured
|
|
149
|
+
top-level namespace; the defaults are `_staging` and `org-logos`. Those names
|
|
150
|
+
are reserved as artifact ids, case-insensitively, so artifact and object
|
|
151
|
+
operations cannot address one another even when an object-store provider uses a
|
|
152
|
+
shared bucket root. Override the top-level
|
|
153
|
+
`ArtifactStorageConfig.objectNamespaces` field when an application owns
|
|
154
|
+
different object families.
|
|
155
|
+
|
|
156
|
+
Keep `objectNamespaces` identical across every reader and writer for a deployed
|
|
157
|
+
store; changing the set is a storage-protocol migration, not a per-process
|
|
158
|
+
preference. On case-insensitive filesystems, use case-stable artifact ids,
|
|
159
|
+
version ids, and object keys (Concepta's lowercase UUID ids satisfy this).
|
|
160
|
+
|
|
161
|
+
The CAE1 framing and its `{ scope, artifactId, version, path }` context are a
|
|
162
|
+
compatibility contract. Existing envelopes remain readable. Filesystems retain
|
|
163
|
+
the layouts `<root>/<artifactId>/<path>` and `<root>/_objects/<key>`; S3 and
|
|
164
|
+
other object-store providers retain their deployed unprefixed object keys, so a
|
|
165
|
+
rolling upgrade does not split old and new writers across keyspaces. Artifact
|
|
166
|
+
and version ids must each be one safe storage segment. Legacy plaintext reads
|
|
167
|
+
are available only through the explicit
|
|
168
|
+
`ARTIFACT_ENCRYPTION_READ_LEGACY=true` migration flag; writes are always
|
|
169
|
+
encrypted.
|
|
170
|
+
|
|
171
|
+
The convenience `createArtifactStorage()` and `createObjectStore()` factories
|
|
172
|
+
are ideal for scripts. Long-running framework-neutral processes that need an
|
|
173
|
+
explicit shutdown path should create the raw clients with
|
|
174
|
+
`createArtifactStorageClient()` / `createObjectStorageClient()`, compose them
|
|
175
|
+
with the corresponding `create*WithClient()` function, call each client's
|
|
176
|
+
`onApplicationShutdown()`, and finally call `crypto.keyProvider.clear()`. The
|
|
177
|
+
Nest module owns that lifecycle automatically.
|
|
178
|
+
|
|
179
|
+
Nest applications can register both adapters with one dynamic module:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { Module } from '@nestjs/common';
|
|
183
|
+
import {
|
|
184
|
+
artifactStorageConfigFromEnv,
|
|
185
|
+
storageCryptoFromEnv,
|
|
186
|
+
} from '@nestm/storage/artifacts';
|
|
187
|
+
import { ArtifactStorageModule } from '@nestm/storage/artifacts/nest';
|
|
188
|
+
|
|
189
|
+
@Module({
|
|
190
|
+
imports: [
|
|
191
|
+
ArtifactStorageModule.forRoot({
|
|
192
|
+
config: artifactStorageConfigFromEnv(process.env),
|
|
193
|
+
crypto: storageCryptoFromEnv(process.env),
|
|
194
|
+
}),
|
|
195
|
+
],
|
|
196
|
+
})
|
|
197
|
+
export class AppModule {}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Inject `ArtifactStorage` with `@InjectArtifactStorage()` and `ObjectStore` with
|
|
201
|
+
`@InjectObjectStore()`. The module owns named raw clients (`artifacts` and
|
|
202
|
+
`objects`) and clears cached data keys during application shutdown. Set
|
|
203
|
+
`isGlobal: true` only when both adapters are intentionally application-wide.
|
|
204
|
+
|
|
91
205
|
## Configure named stores
|
|
92
206
|
|
|
93
207
|
Use the package-owned S3 factory when applicable. For other providers, create a
|
|
@@ -225,6 +339,71 @@ modules retain their own DI scope rather than mutating an application-wide
|
|
|
225
339
|
registry. Names are case-sensitive and cannot contain leading or trailing
|
|
226
340
|
whitespace.
|
|
227
341
|
|
|
342
|
+
### Select the provider at runtime
|
|
343
|
+
|
|
344
|
+
An application that ships to more than one environment usually cannot name its
|
|
345
|
+
provider at build time. `createProviderStorageDriver` takes the slug as data and
|
|
346
|
+
imports that provider's adapter — and only that one — on demand, so a deployment
|
|
347
|
+
picks its store with an environment variable and installs one native SDK:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { createProviderStorageDriver } from '@nestm/storage/files-sdk/provider';
|
|
351
|
+
|
|
352
|
+
StorageModule.forRootAsync({
|
|
353
|
+
imports: [ConfigModule],
|
|
354
|
+
stores: [
|
|
355
|
+
{
|
|
356
|
+
name: 'media',
|
|
357
|
+
inject: [ConfigService],
|
|
358
|
+
useFactory: (config: ConfigService) =>
|
|
359
|
+
createProviderStorageDriver({
|
|
360
|
+
provider: config.getOrThrow('STORAGE_PROVIDER'),
|
|
361
|
+
prefix: config.get('STORAGE_PREFIX'),
|
|
362
|
+
config: {
|
|
363
|
+
bucket: config.get('STORAGE_BUCKET'),
|
|
364
|
+
region: config.get('STORAGE_REGION'),
|
|
365
|
+
root: config.get('STORAGE_ROOT'),
|
|
366
|
+
},
|
|
367
|
+
}),
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`config` is one flat bag of provider settings — `bucket` and `region` for an
|
|
374
|
+
object store, `root` for the filesystem, `accountName` and `container` for
|
|
375
|
+
Azure. Each provider reads what it needs and ignores the rest, so the same shape
|
|
376
|
+
survives a provider change. Credentials may be omitted wherever the provider's
|
|
377
|
+
SDK resolves its own chain (an IAM role, Application Default Credentials, a
|
|
378
|
+
shared profile).
|
|
379
|
+
|
|
380
|
+
An unknown slug fails closed with `INVALID_ARGUMENT` before anything is
|
|
381
|
+
imported. Validate untrusted input up front with `isStorageProvider`, and drive
|
|
382
|
+
config validation from the catalog rather than a hand-kept list:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import {
|
|
386
|
+
getStorageProvider,
|
|
387
|
+
isStorageProvider,
|
|
388
|
+
listStorageProviders,
|
|
389
|
+
listStorageProviderSecretEnvVars,
|
|
390
|
+
} from '@nestm/storage/files-sdk/provider';
|
|
391
|
+
|
|
392
|
+
listStorageProviders().map((provider) => provider.slug); // 'akamai', 'alibaba', …
|
|
393
|
+
getStorageProvider('gcs')?.peerDeps; // ['@google-cloud/storage', …]
|
|
394
|
+
listStorageProviderSecretEnvVars('s3').map((variable) => variable.key);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The catalog is pure data and pulls in no adapter, so it is safe in config UIs,
|
|
398
|
+
health checks, and startup validation.
|
|
399
|
+
|
|
400
|
+
The `s3` slug additionally carries the conditional-promotion and signed-policy
|
|
401
|
+
capabilities described under
|
|
402
|
+
[Race-free staged-object promotion](#race-free-staged-object-promotion); every
|
|
403
|
+
other provider exposes exactly what its adapter declares. When the provider _is_
|
|
404
|
+
known at build time, import `@nestm/storage/files-sdk/s3` or
|
|
405
|
+
`@nestm/storage/files-sdk/fs` directly and skip the indirection.
|
|
406
|
+
|
|
228
407
|
## Storage API
|
|
229
408
|
|
|
230
409
|
`StorageClient` exposes:
|
|
@@ -510,8 +689,26 @@ StorageModule.forRoot({
|
|
|
510
689
|
});
|
|
511
690
|
```
|
|
512
691
|
|
|
513
|
-
For local filesystem storage,
|
|
514
|
-
`
|
|
692
|
+
For local filesystem storage, use the package-owned factory. The adapter reaches
|
|
693
|
+
only `node:fs`, so it needs no native SDK:
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
import { createFsStorageDriver } from '@nestm/storage/files-sdk/fs';
|
|
697
|
+
|
|
698
|
+
StorageModule.forRoot({
|
|
699
|
+
stores: [
|
|
700
|
+
{
|
|
701
|
+
name: 'artifacts',
|
|
702
|
+
driver: createFsStorageDriver({ adapter: { root: './var/artifacts' } }),
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
});
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Bodies are written verbatim at `<root>/<key>`. A `<key>.meta.json` sidecar beside
|
|
709
|
+
each one carries the content type, ETag, and custom metadata a filesystem has
|
|
710
|
+
nowhere else to put; sidecars never surface as keys, and uploading a key ending
|
|
711
|
+
in `.meta.json` fails closed rather than colliding with one.
|
|
515
712
|
|
|
516
713
|
## License
|
|
517
714
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type StorageClient } from '../core/index.js';
|
|
2
|
+
import { SCOPE_FROM_HEADER, type StorageCrypto } from './crypto/index.js';
|
|
3
|
+
import { type ArtifactStorageConfig } from './storage-driver.js';
|
|
4
|
+
export { assertStorageProvider, defaultFsRoot, type ArtifactStorageConfig, } from './storage-driver.js';
|
|
5
|
+
/**
|
|
6
|
+
* Storage for uploaded artifact files, keyed `<artifactId>/<path>` (e.g. `<id>/index.html`,
|
|
7
|
+
* `<id>/assets/app.js`). The api writes; the sandbox reads. The `fs` provider keeps both pointed
|
|
8
|
+
* at one directory (fine for a single host / shared volume); any object-store provider — S3, GCS,
|
|
9
|
+
* Azure, R2, and the rest — lets the two services share storage without a shared volume, which is
|
|
10
|
+
* what running them on separate hosts (as on ECS) requires.
|
|
11
|
+
*
|
|
12
|
+
* Every object is stored as a CAE1 encryption envelope (ADR-0001, SEC-03): callers must say who
|
|
13
|
+
* owns what they write (`ScopeRef`) and what they expect to read (`ReadRef`) — the codec binds
|
|
14
|
+
* ciphertext to `{scope, artifactId, version, path}` so a swapped or replayed object fails closed.
|
|
15
|
+
*/
|
|
16
|
+
export interface ArtifactStorage {
|
|
17
|
+
/** Single self-contained HTML artifact → `<id>/index.html`. */
|
|
18
|
+
writeHtml(artifactId: string, buffer: Buffer, ref: ScopeRef): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Pre-built SPA bundle (a zip whose root contains index.html). Encrypted per extracted entry.
|
|
21
|
+
* With `opts.versionId` (VER-03), entries land under `v/<versionId>/…` and are sealed with
|
|
22
|
+
* `ctx.version = versionId` (the caller's `ref.version` is overridden to match — the path⇔version
|
|
23
|
+
* biconditional is derived, never trusted). Reserved-namespace screening applies to the zip
|
|
24
|
+
* ENTRY name in both modes: a bundle shipping `v/…` or `__…` files has those skipped, not nested.
|
|
25
|
+
*/
|
|
26
|
+
writeBundle(artifactId: string, zipBuffer: Buffer, ref: ScopeRef, opts?: WriteBundleOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Write an arbitrary file within the artifact (e.g. `__vendor/<hash>.js`, `__meta.json`).
|
|
29
|
+
* Rejects any `relPath` escaping the artifact directory. `contentType` rides the envelope
|
|
30
|
+
* header (authenticated) on both backends.
|
|
31
|
+
*/
|
|
32
|
+
writeFile(artifactId: string, relPath: string, buffer: Buffer, ref: ScopeRef, contentType?: string): Promise<void>;
|
|
33
|
+
/** Server-rendered PNG preview → reserved key `<id>/__thumb.png`. */
|
|
34
|
+
writeThumbnail(artifactId: string, pngBuffer: Buffer, ref: ScopeRef): Promise<void>;
|
|
35
|
+
/** Delete every file under the artifact (includes the thumbnail). */
|
|
36
|
+
remove(artifactId: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Read a file within the artifact; `relPath === ""` resolves to index.html. `null` if absent.
|
|
39
|
+
* Throws `EnvelopeError` on any decrypt failure — a corrupt or swapped object must never be
|
|
40
|
+
* mistaken for a missing one (no SPA fallback over tampering).
|
|
41
|
+
*/
|
|
42
|
+
read(artifactId: string, relPath: string, ref: ReadRef, options?: ArtifactReadOptions): Promise<Buffer | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Like `read`, but also returns the envelope's AUTHENTICATED content type (VER-03). This is the
|
|
45
|
+
* read primitive for publish/CoW copy loops: a decrypt→re-encrypt copy must re-seal with the
|
|
46
|
+
* same `ct` it opened (the type is AAD-bound and steers serving), and `read()` discards it.
|
|
47
|
+
* `contentType` is undefined only for legacy plaintext pass-throughs (nothing was verified).
|
|
48
|
+
*/
|
|
49
|
+
readWithInfo(artifactId: string, relPath: string, ref: ReadRef, options?: ArtifactReadOptions): Promise<StoredObject | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Delete specific files under the artifact (VER-03: publish prunes root paths removed by the
|
|
52
|
+
* newly-materialized version). Paths escaping the artifact directory and missing paths are
|
|
53
|
+
* ignored (idempotent — publish repair re-runs the same prune).
|
|
54
|
+
*/
|
|
55
|
+
deleteFiles(artifactId: string, relPaths: string[]): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* List stored objects under the artifact (optionally under `subPrefix`, e.g. `v/<versionId>/`).
|
|
58
|
+
* Returns artifact-relative forward-slash paths + last-modified stamps, sorted by path. Reads
|
|
59
|
+
* no object bodies and needs no crypto — it serves the sweepers/invariants (version-prefix
|
|
60
|
+
* existence, orphan-prefix age, canonical bundle enumeration), never the serving plane.
|
|
61
|
+
*/
|
|
62
|
+
listFiles(artifactId: string, subPrefix?: string): Promise<StoredFileInfo[]>;
|
|
63
|
+
}
|
|
64
|
+
/** One listed storage object (metadata only — the body stays sealed). */
|
|
65
|
+
export interface StoredFileInfo {
|
|
66
|
+
/** Artifact-relative path, forward slashes (e.g. `index.html`, `v/<vid>/index.html`). */
|
|
67
|
+
path: string;
|
|
68
|
+
lastModified: Date;
|
|
69
|
+
}
|
|
70
|
+
/** A decrypted object plus the authenticated content type it was sealed with (see readWithInfo). */
|
|
71
|
+
export interface StoredObject {
|
|
72
|
+
plain: Buffer;
|
|
73
|
+
/** From the envelope header (`ct`, AAD-bound); undefined for legacy plaintext pass-throughs. */
|
|
74
|
+
contentType?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Bounds decrypted data while also bounding the opaque envelope downloaded before decryption. */
|
|
77
|
+
export interface ArtifactReadOptions {
|
|
78
|
+
readonly maxPlainBytes?: number;
|
|
79
|
+
}
|
|
80
|
+
/** Permanent, address-independent read rejection safe for callers to map to a bounded 4xx. */
|
|
81
|
+
export declare class ArtifactReadLimitError extends Error {
|
|
82
|
+
readonly maxPlainBytes: number;
|
|
83
|
+
constructor(maxPlainBytes: number);
|
|
84
|
+
}
|
|
85
|
+
/** Permanent rejection for a ZIP whose declared expansion exceeds the public protocol limits. */
|
|
86
|
+
export declare class ArtifactBundleLimitError extends Error {
|
|
87
|
+
readonly reason: 'entries' | 'entry-bytes' | 'total-bytes';
|
|
88
|
+
constructor(reason: 'entries' | 'entry-bytes' | 'total-bytes');
|
|
89
|
+
}
|
|
90
|
+
export declare const ARTIFACT_BUNDLE_MAX_ENTRIES = 1000;
|
|
91
|
+
export declare const ARTIFACT_BUNDLE_MAX_ENTRY_BYTES: number;
|
|
92
|
+
export declare const ARTIFACT_BUNDLE_MAX_TOTAL_BYTES: number;
|
|
93
|
+
/** Reserved storage path for the generated PNG preview, served by the sandbox as `<id>/__thumb.png`. */
|
|
94
|
+
export declare const THUMBNAIL_KEY = "__thumb.png";
|
|
95
|
+
/** Options for writeBundle (VER-03 versioned bundle writes). */
|
|
96
|
+
export interface WriteBundleOptions {
|
|
97
|
+
/** Write entries under `v/<versionId>/…` with `ctx.version = versionId` instead of the root. */
|
|
98
|
+
versionId?: string;
|
|
99
|
+
/** Optional stricter limits; values may lower, but never raise, the protocol hard limits. */
|
|
100
|
+
limits?: {
|
|
101
|
+
maxEntries?: number;
|
|
102
|
+
maxEntryBytes?: number;
|
|
103
|
+
maxTotalBytes?: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Who owns an object being written; `version` stays null until VER-02 introduces versions. */
|
|
107
|
+
export interface ScopeRef {
|
|
108
|
+
/** `org:<organizationId>` | `user:<ownerUserId>` | `upload:<uploadId>` — see artifactScope(). */
|
|
109
|
+
scope: string;
|
|
110
|
+
version?: string | null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What the reader independently knows. The api asserts the scope it fetched from the DB; the
|
|
114
|
+
* sandbox — which only knows the artifactId — passes SCOPE_FROM_HEADER and relies on the wrap
|
|
115
|
+
* layer to enforce scope cryptographically (see crypto/context.ts).
|
|
116
|
+
*/
|
|
117
|
+
export interface ReadRef {
|
|
118
|
+
scope: string | typeof SCOPE_FROM_HEADER;
|
|
119
|
+
version?: string | null;
|
|
120
|
+
}
|
|
121
|
+
export declare function createArtifactStorage(cfg: ArtifactStorageConfig, crypto: StorageCrypto): Promise<ArtifactStorage>;
|
|
122
|
+
/** Framework-neutral domain adapter used by both direct consumers and the Nest composition entry. */
|
|
123
|
+
export declare function createArtifactStorageWithClient(cfg: ArtifactStorageConfig, crypto: StorageCrypto, client: StorageClient): ArtifactStorage;
|
|
124
|
+
/**
|
|
125
|
+
* Storage path prefix for a version's objects (AP-002): `""` for a backfilled `canonical_v1`
|
|
126
|
+
* version (its bytes ARE the canonical root objects) and `v/<versionId>/` for a `versioned` one.
|
|
127
|
+
*/
|
|
128
|
+
export declare function versionPrefix(layout: 'canonical_v1' | 'versioned', versionId: string): string;
|
|
129
|
+
/**
|
|
130
|
+
* Envelope `ctx.version` for a version's objects (AP-002): `null` for `canonical_v1` (root AAD —
|
|
131
|
+
* the serving plane is version-free forever) and the version UUID, byte-identical to the path
|
|
132
|
+
* segment, for `versioned`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function versionAad(layout: 'canonical_v1' | 'versioned', versionId: string): string | null;
|
|
135
|
+
/** Artifact ids are one storage segment; `_objects` is the filesystem object-store namespace. */
|
|
136
|
+
export declare function assertArtifactId(artifactId: string): void;
|
|
137
|
+
/** Version ids are one storage segment because they are interpolated under `v/<id>/`. */
|
|
138
|
+
export declare function assertVersionId(versionId: string): void;
|
|
139
|
+
/**
|
|
140
|
+
* Is this artifact-relative path inside the namespaces VER-02 reserves for the platform — the
|
|
141
|
+
* `v/` version prefix and the `__`-internal families (meta/thumb/source/snapshot/overflow) —
|
|
142
|
+
* with the one serving exemption, top-level single-segment `__vendor/<name>` pool assets?
|
|
143
|
+
* User-supplied bundle entries matching this are SKIPPED at write time (writeBundle), exactly
|
|
144
|
+
* like zip-slip entries: the sandbox deny-guard will never serve them, and letting them through
|
|
145
|
+
* to the seal chokepoint would turn a hostile/quirky zip into a mid-write throw.
|
|
146
|
+
*/
|
|
147
|
+
export declare function isReservedArtifactPath(relPath: string): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* AP-002 storage rule 2: `path.startsWith("v/" + version + "/") ⇔ version !== null`. Root objects
|
|
150
|
+
* never live under the reserved `v/` namespace and versioned objects never live anywhere else —
|
|
151
|
+
* asserted at the single ctx chokepoint (covers reads and writes, both backends), so a
|
|
152
|
+
* mis-addressed artifact object is impossible rather than unlikely. Throws (fail-closed): this is
|
|
153
|
+
* a programming error, never user input — every USER-named path (zip entries) is screened by
|
|
154
|
+
* `isReservedArtifactPath` before it can reach this assertion.
|
|
155
|
+
*/
|
|
156
|
+
export declare function assertVersionPath(path: string, version: string | null): void;
|
|
157
|
+
/** Structured, greppable trace of a legacy plaintext pass-through (SEC-04 migration window). */
|
|
158
|
+
export declare function warnLegacyRead(where: string, key: string): void;
|
|
159
|
+
/** content-type for an artifact file path, falling back to octet-stream. */
|
|
160
|
+
export declare function contentTypeFor(path: string): string;
|
|
161
|
+
//# sourceMappingURL=artifact-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact-storage.d.ts","sourceRoot":"","sources":["../../src/artifacts/artifact-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EAIL,iBAAiB,EAEjB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E;;;;;;OAMG;IACH,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,QAAQ,EACb,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;OAIG;IACH,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,QAAQ,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,qEAAqE;IACrE,cAAc,CACZ,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,qEAAqE;IACrE,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C;;;;OAIG;IACH,IAAI,CACF,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,OAAO,EACZ,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CACV,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,OAAO,EACZ,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAChC;;;;OAIG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE;;;;;OAKG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CAC9E;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;CACpB;AAED,oGAAoG;AACpG,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,kGAAkG;AAClG,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,8FAA8F;AAC9F,qBAAa,sBAAuB,SAAQ,KAAK;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM;IAA1C,YAAqB,aAAa,EAAE,MAAM,EAGzC;CACF;AAED,iGAAiG;AACjG,qBAAa,wBAAyB,SAAQ,KAAK;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,aAAa;IAAtE,YAAqB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,aAAa,EAGrE;CACF;AAED,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AACjD,eAAO,MAAM,+BAA+B,QAAmB,CAAC;AAChE,eAAO,MAAM,+BAA+B,QAAoB,CAAC;AAEjE,wGAAwG;AACxG,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,MAAM,CAAC,EAAE;QACP,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACvB,iGAAiG;IACjG,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,OAAO;IAItB,KAAK,EAAE,MAAM,GAAG,OAAO,iBAAiB,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,eAAe,CAAC,CAM1B;AAED,qGAAqG;AACrG,wBAAgB,+BAA+B,CAC7C,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,GACpB,eAAe,CAOjB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,GAAG,WAAW,EACpC,SAAS,EAAE,MAAM,GAChB,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,cAAc,GAAG,WAAW,EACpC,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAGf;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAKzD;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvD;AAoCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAgB/D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAgB5E;AA8GD,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAO/D;AAyVD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|