Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/restore-generic-storage-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@nestm/storage': minor
---

Remove the product-specific artifact protocol, encryption codec, and Nest composition entry
points. `@nestm/storage` remains a generic storage library; applications should compose domain
protocols over its clients and provider drivers in their own packages.
114 changes: 0 additions & 114 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ Install only the native SDKs required by the chosen provider. For example:
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-presigned-post \
@aws-sdk/s3-request-presigner @aws-sdk/lib-storage

# Encrypted artifact storage with AWS KMS
pnpm add @aws-sdk/client-kms

# Google Cloud Storage
pnpm add @google-cloud/storage google-auth-library

Expand Down Expand Up @@ -91,117 +88,6 @@ storage errors and operation types, and `StorageUploadControl`. It has no NestJS
runtime or declaration imports. Provider adapters remain available through
`@nestm/storage/files-sdk`.

## Encrypted artifact storage

The optional `@nestm/storage/artifacts` entry point adds a framework-neutral
artifact facade over the same provider drivers. Every write is a self-contained
CAE1 AES-256-GCM envelope; plaintext mode does not exist. The authenticated
context binds each object to its tenant scope, artifact id, version, path, and
content type so moving ciphertext to another address fails closed.

Install `@aws-sdk/client-kms` in every artifact-storage consumer. It is an
optional peer so core-only applications do not download AWS KMS, while artifact
applications can choose either the local key provider or KMS at runtime.

Set `ARTIFACT_KEY_PROVIDER=local` with a base64-encoded 32-byte `ARTIFACT_KEK`
(`ARTIFACT_KEK_NAME` is optional), or set `ARTIFACT_KEY_PROVIDER=kms` with
`ARTIFACT_KMS_KEY_ID` (`ARTIFACT_KMS_REGION` is optional). Invalid or missing
key configuration aborts startup. `ARTIFACT_ENCRYPTION_READ_LEGACY=true` is a
temporary, explicit plaintext-read migration flag and never enables plaintext
writes.

```ts
import {
artifactScope,
artifactStorageConfigFromEnv,
createArtifactStorage,
storageCryptoFromEnv,
} from '@nestm/storage/artifacts';

const storage = await createArtifactStorage(
artifactStorageConfigFromEnv(process.env),
storageCryptoFromEnv(process.env),
);

const ref = artifactScope({
organizationId: 'org-1',
ownerUserId: 'user-1',
});

await storage.writeHtml('artifact-1', Buffer.from('<h1>Encrypted</h1>'), {
scope: ref,
});

const html = await storage.read('artifact-1', 'index.html', {
scope: ref,
});
```

`writeBundle()` expands a zip into separately authenticated objects, skips
traversal and reserved-namespace entries, and rejects case-folded duplicate
paths, more than 1,000 entries, entries larger than 64 MiB, or more than 256 MiB
total expanded data. Callers may lower those limits per write. `read()` and
`readWithInfo()` support directory-to-`index.html` resolution, optional
plaintext-size limits, and version-bound paths.

`ObjectStore` exposes the same encryption guarantees for non-artifact objects
such as upload staging and organization media. Keys must begin with a configured
top-level namespace; the defaults are `_staging` and `org-logos`. Those names
are reserved as artifact ids, case-insensitively, so artifact and object
operations cannot address one another even when an object-store provider uses a
shared bucket root. Override the top-level
`ArtifactStorageConfig.objectNamespaces` field when an application owns
different object families.

Keep `objectNamespaces` identical across every reader and writer for a deployed
store; changing the set is a storage-protocol migration, not a per-process
preference. On case-insensitive filesystems, use case-stable artifact ids,
version ids, and object keys (Concepta's lowercase UUID ids satisfy this).

The CAE1 framing and its `{ scope, artifactId, version, path }` context are a
compatibility contract. Existing envelopes remain readable. Filesystems retain
the layouts `<root>/<artifactId>/<path>` and `<root>/_objects/<key>`; S3 and
other object-store providers retain their deployed unprefixed object keys, so a
rolling upgrade does not split old and new writers across keyspaces. Artifact
and version ids must each be one safe storage segment. Legacy plaintext reads
are available only through the explicit
`ARTIFACT_ENCRYPTION_READ_LEGACY=true` migration flag; writes are always
encrypted.

The convenience `createArtifactStorage()` and `createObjectStore()` factories
are ideal for scripts. Long-running framework-neutral processes that need an
explicit shutdown path should create the raw clients with
`createArtifactStorageClient()` / `createObjectStorageClient()`, compose them
with the corresponding `create*WithClient()` function, call each client's
`onApplicationShutdown()`, and finally call `crypto.keyProvider.clear()`. The
Nest module owns that lifecycle automatically.

Nest applications can register both adapters with one dynamic module:

```ts
import { Module } from '@nestjs/common';
import {
artifactStorageConfigFromEnv,
storageCryptoFromEnv,
} from '@nestm/storage/artifacts';
import { ArtifactStorageModule } from '@nestm/storage/artifacts/nest';

@Module({
imports: [
ArtifactStorageModule.forRoot({
config: artifactStorageConfigFromEnv(process.env),
crypto: storageCryptoFromEnv(process.env),
}),
],
})
export class AppModule {}
```

Inject `ArtifactStorage` with `@InjectArtifactStorage()` and `ObjectStore` with
`@InjectObjectStore()`. The module owns named raw clients (`artifacts` and
`objects`) and clears cached data keys during application shutdown. Set
`isGlobal: true` only when both adapters are intentionally application-wide.

## Configure named stores

Use the package-owned S3 factory when applicable. For other providers, create a
Expand Down
16 changes: 0 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./artifacts": {
"types": "./dist/artifacts/index.d.ts",
"import": "./dist/artifacts/index.js"
},
"./artifacts/nest": {
"types": "./dist/artifacts/nest/index.d.ts",
"import": "./dist/artifacts/nest/index.js"
},
"./core": {
"types": "./dist/core/index.d.ts",
"import": "./dist/core/index.js"
Expand Down Expand Up @@ -106,11 +98,9 @@
"release": "node scripts/publish.mjs"
},
"dependencies": {
"adm-zip": "0.5.18",
"files-sdk": "2.2.3"
},
"peerDependencies": {
"@aws-sdk/client-kms": "^3.700.0",
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/lib-storage": "^3.700.0",
"@aws-sdk/s3-presigned-post": "^3.700.0",
Expand All @@ -121,9 +111,6 @@
"rxjs": "^7.8.1"
},
"peerDependenciesMeta": {
"@aws-sdk/client-kms": {
"optional": true
},
"@aws-sdk/client-s3": {
"optional": true
},
Expand All @@ -150,7 +137,6 @@
}
},
"devDependencies": {
"@aws-sdk/client-kms": "3.1103.0",
"@aws-sdk/client-s3": "3.1103.0",
"@aws-sdk/lib-storage": "3.1103.0",
"@aws-sdk/s3-presigned-post": "3.1103.0",
Expand All @@ -161,11 +147,9 @@
"@nestjs/platform-express": "12.0.0-alpha.5",
"@nestjs/platform-fastify": "12.0.0-alpha.5",
"@nestjs/testing": "12.0.0-alpha.5",
"@types/adm-zip": "0.5.8",
"@types/node": "26.1.2",
"@types/supertest": "7.2.1",
"@vitest/coverage-v8": "4.1.10",
"aws-sdk-client-mock": "4.1.0",
"fastify": "5.11.2",
"oxlint": "1.77.0",
"prettier": "3.9.6",
Expand Down
Loading