@nestm/storage 0.1.0-alpha.11 → 0.1.0-alpha.12
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 +6 -0
- package/README.md +8 -0
- package/dist/core/storage-staged-content.d.ts +2 -0
- package/dist/core/storage-staged-content.d.ts.map +1 -1
- package/dist/core/storage-staged-content.js +13 -3
- package/dist/core/storage-staged-content.js.map +1 -1
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +2 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/storage-encrypted-content.d.ts +49 -0
- package/dist/crypto/storage-encrypted-content.d.ts.map +1 -0
- package/dist/crypto/storage-encrypted-content.js +212 -0
- package/dist/crypto/storage-encrypted-content.js.map +1 -0
- package/package.json +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @nestm/storage
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 009a0ed: Add optional `/crypto` encrypted staged content coordination over FileCipherEngine, with host-owned addressing, AAD, persistence and reference-protected cleanup. Preserve detached metadata preparation, exact physical acknowledgements, immutable ETag reads, authenticated ranges and cancellation. Trusted staged writers can reserve an identity before metadata preparation with `writeReserved`.
|
|
8
|
+
|
|
3
9
|
## 0.1.0-alpha.11
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1236,3 +1236,11 @@ or direct filesystem calls.
|
|
|
1236
1236
|
## License
|
|
1237
1237
|
|
|
1238
1238
|
MIT
|
|
1239
|
+
|
|
1240
|
+
## Encrypted staged content
|
|
1241
|
+
|
|
1242
|
+
`@nestm/storage/crypto` exports `StorageEncryptedContentStore<Scope>`. Install the optional `@nestm/crypto` peer only for this entry point. The store uses a host-owned `FileCipherEngine`, object-address function, fresh AAD buffers, allowed key providers, and metadata callbacks. It prepares detached encryption metadata before uploading and completes metadata only after plaintext and ciphertext accounting agree. Reads pin physical ETags and authenticate full content or bounded frame ranges.
|
|
1243
|
+
|
|
1244
|
+
The host authorizes reads, serializes metadata, enforces key policy and atomically proves reference eligibility before `metadata.discard` removes anything. A missing upload receipt means completion is uncertain: keep prepared cleanup inventory for a later sweep. A rejected prepare does not authorize cleanup of another writer's reservation. Engine shutdown stays with its owner. See [the standalone archive consumer](scripts/fixtures/encrypted-content-consumer.ts).
|
|
1245
|
+
|
|
1246
|
+
`StorageStagedContentStore.writeReserved(scope, payloadId, stream, options)` accepts a fresh UUID from a trusted caller. It retains create-only semantics and returns that exact identity in the receipt; it never overwrites an existing payload. Ordinary writers continue to use `write`.
|
|
@@ -28,6 +28,8 @@ export declare class StorageStagedContentStore<Scope> implements StorageStagedCo
|
|
|
28
28
|
#private;
|
|
29
29
|
constructor(options: StorageStagedContentStoreOptions<Scope>);
|
|
30
30
|
write(scope: Scope, body: ReadableStream<Uint8Array>, options?: StorageStagedWriteOptions): Promise<StorageStagedBody>;
|
|
31
|
+
/** Trusted callers reserve a fresh identity before preparing detached metadata. */
|
|
32
|
+
writeReserved(scope: Scope, payloadId: string, body: ReadableStream<Uint8Array>, options?: StorageStagedWriteOptions): Promise<StorageStagedBody>;
|
|
31
33
|
read(scope: Scope, body: StorageStagedBody, options?: StorageStagedReadOptions): Promise<ReadableStream<Uint8Array>>;
|
|
32
34
|
/** Host must prove no durable/in-flight reference can be created before removal. */
|
|
33
35
|
remove(scope: Scope, body: StorageStagedBody, options?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-staged-content.d.ts","sourceRoot":"","sources":["../../src/core/storage-staged-content.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AACD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AACD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CAC/C;AACD,MAAM,WAAW,oBAAoB,CAAC,KAAK;IACzC,KAAK,CACH,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,IAAI,CACF,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;CACxC;AACD,MAAM,WAAW,gCAAgC,CAAC,KAAK;IACrD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3D;AAED,4FAA4F;AAC5F,qBAAa,yBAAyB,CACpC,KAAK,CACL,YAAW,oBAAoB,CAAC,KAAK,CAAC;;IAGtC,YAAY,OAAO,EAAE,gCAAgC,CAAC,KAAK,CAAC,EAG3D;IACK,KAAK,CACT,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAgE5B;IACK,IAAI,CACR,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgBrC;IACD,oFAAoF;IAC9E,MAAM,CACV,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAC9C,OAAO,CAAC,IAAI,CAAC,CAMf;CACF"}
|
|
1
|
+
{"version":3,"file":"storage-staged-content.d.ts","sourceRoot":"","sources":["../../src/core/storage-staged-content.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AACD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AACD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CAC/C;AACD,MAAM,WAAW,oBAAoB,CAAC,KAAK;IACzC,KAAK,CACH,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,IAAI,CACF,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;CACxC;AACD,MAAM,WAAW,gCAAgC,CAAC,KAAK;IACrD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3D;AAED,4FAA4F;AAC5F,qBAAa,yBAAyB,CACpC,KAAK,CACL,YAAW,oBAAoB,CAAC,KAAK,CAAC;;IAGtC,YAAY,OAAO,EAAE,gCAAgC,CAAC,KAAK,CAAC,EAG3D;IACK,KAAK,CACT,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAE5B;IACD,mFAAmF;IAC7E,aAAa,CACjB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAgE5B;IACK,IAAI,CACR,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgBrC;IACD,oFAAoF;IAC9E,MAAM,CACV,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAC9C,OAAO,CAAC,IAAI,CAAC,CAMf;CACF"}
|
|
@@ -10,6 +10,11 @@ export class StorageStagedContentStore {
|
|
|
10
10
|
this.#key = options.key;
|
|
11
11
|
}
|
|
12
12
|
async write(scope, body, options = {}) {
|
|
13
|
+
return this.writeReserved(scope, randomUUID(), body, options);
|
|
14
|
+
}
|
|
15
|
+
/** Trusted callers reserve a fresh identity before preparing detached metadata. */
|
|
16
|
+
async writeReserved(scope, payloadId, body, options = {}) {
|
|
17
|
+
validatePayloadId(payloadId);
|
|
13
18
|
const { signal } = options;
|
|
14
19
|
const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;
|
|
15
20
|
storageInteger(maxBytes, 'maxBytes');
|
|
@@ -19,7 +24,6 @@ export class StorageStagedContentStore {
|
|
|
19
24
|
await body.cancel().catch(() => { });
|
|
20
25
|
throw new StorageError('Staged content requires native create-only writes and exact ETag reads.', { code: 'NOT_SUPPORTED' });
|
|
21
26
|
}
|
|
22
|
-
const payloadId = randomUUID();
|
|
23
27
|
const hash = createHash('sha256');
|
|
24
28
|
let size = 0;
|
|
25
29
|
let completed = false;
|
|
@@ -84,11 +88,17 @@ export class StorageStagedContentStore {
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
function validateBody(body) {
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
validatePayloadId(body.payloadId);
|
|
92
|
+
if (!/^[0-9a-f]{64}$/u.test(body.sha256))
|
|
89
93
|
throw new StorageError('Invalid staged body receipt.', {
|
|
90
94
|
code: 'INVALID_ARGUMENT',
|
|
91
95
|
});
|
|
92
96
|
storageInteger(body.size, 'size');
|
|
93
97
|
}
|
|
98
|
+
function validatePayloadId(payloadId) {
|
|
99
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(payloadId))
|
|
100
|
+
throw new StorageError('Invalid staged payload identity.', {
|
|
101
|
+
code: 'INVALID_ARGUMENT',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
94
104
|
//# sourceMappingURL=storage-staged-content.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-staged-content.js","sourceRoot":"","sources":["../../src/core/storage-staged-content.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport { StorageError } from '../storage.error.js';\nimport type { StorageClient } from '../storage.client.js';\nimport type { StorageByteRange } from '../storage.types.js';\nimport { storageInteger } from './storage-streams.js';\n\nexport interface StorageStagedBody {\n readonly payloadId: string;\n readonly size: number;\n readonly sha256: string;\n readonly etag: string;\n}\nexport interface StorageStagedWriteOptions {\n readonly signal?: AbortSignal | undefined;\n readonly maxBytes?: number | undefined;\n}\nexport interface StorageStagedReadOptions {\n readonly signal?: AbortSignal | undefined;\n readonly range?: StorageByteRange | undefined;\n}\nexport interface StorageStagedContent<Scope> {\n write(\n scope: Scope,\n body: ReadableStream<Uint8Array>,\n options?: StorageStagedWriteOptions,\n ): Promise<StorageStagedBody>;\n read(\n scope: Scope,\n body: StorageStagedBody,\n options?: StorageStagedReadOptions,\n ): Promise<ReadableStream<Uint8Array>>;\n}\nexport interface StorageStagedContentStoreOptions<Scope> {\n readonly client: StorageClient;\n /** Trusted, injective scope mapping; never accept this function from a model. */\n readonly key: (scope: Scope, payloadId: string) => string;\n}\n\n/** Immutable create-only bodies. Retention eligibility and references remain host-owned. */\nexport class StorageStagedContentStore<\n Scope,\n> implements StorageStagedContent<Scope> {\n readonly #client: StorageClient;\n readonly #key: (scope: Scope, payloadId: string) => string;\n constructor(options: StorageStagedContentStoreOptions<Scope>) {\n this.#client = options.client;\n this.#key = options.key;\n }\n async write(\n scope: Scope,\n body: ReadableStream<Uint8Array>,\n options: StorageStagedWriteOptions = {},\n ): Promise<StorageStagedBody> {\n const { signal } = options;\n const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;\n storageInteger(maxBytes, 'maxBytes');\n const capabilities = this.#client.capabilities;\n if (\n !capabilities.conditionalCreate?.resultEtag ||\n !capabilities.conditionalRead?.etag\n ) {\n await body.cancel().catch(() => {});\n throw new StorageError(\n 'Staged content requires native create-only writes and exact ETag reads.',\n { code: 'NOT_SUPPORTED' },\n );\n }\n const payloadId = randomUUID();\n const hash = createHash('sha256');\n let size = 0;\n let completed = false;\n const measured = body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n signal?.throwIfAborted();\n size += chunk.byteLength;\n if (!Number.isSafeInteger(size) || size > maxBytes)\n throw new StorageError('Staged body exceeds its byte budget.', {\n code: 'LIMIT_EXCEEDED',\n });\n hash.update(chunk);\n controller.enqueue(chunk);\n },\n flush() {\n completed = true;\n },\n }),\n signal === undefined ? {} : { signal },\n );\n try {\n signal?.throwIfAborted();\n const result = await this.#client.uploadConditional(\n this.#key(scope, payloadId),\n measured,\n {\n condition: { type: 'create' },\n contentType: 'application/octet-stream',\n retries: 0,\n ...(signal === undefined ? {} : { signal }),\n },\n );\n if (!completed || result.size !== size || result.etag === undefined)\n throw new StorageError(\n 'Provider did not acknowledge the complete staged body.',\n { code: 'PROVIDER', applied: true },\n );\n return Object.freeze({\n payloadId,\n size,\n sha256: hash.digest('hex'),\n etag: result.etag,\n });\n } catch (error) {\n await measured.cancel(error).catch(() => {});\n throw error;\n }\n }\n async read(\n scope: Scope,\n body: StorageStagedBody,\n options: StorageStagedReadOptions = {},\n ): Promise<ReadableStream<Uint8Array>> {\n validateBody(body);\n options.signal?.throwIfAborted();\n if (options.range !== undefined && !this.#client.capabilities.rangeRead)\n throw new StorageError('Provider does not support byte ranges.', {\n code: 'NOT_SUPPORTED',\n });\n const result = await this.#client.downloadConditional(\n this.#key(scope, body.payloadId),\n {\n condition: { etag: body.etag },\n ...(options.range === undefined ? {} : { range: options.range }),\n ...(options.signal === undefined ? {} : { signal: options.signal }),\n },\n );\n return result.body;\n }\n /** Host must prove no durable/in-flight reference can be created before removal. */\n async remove(\n scope: Scope,\n body: StorageStagedBody,\n options: { readonly signal?: AbortSignal } = {},\n ): Promise<void> {\n validateBody(body);\n await this.#client.deleteConditional(this.#key(scope, body.payloadId), {\n condition: { etag: body.etag },\n ...options,\n });\n }\n}\nfunction validateBody(body: StorageStagedBody): void {\n if (\n !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(\n body.payloadId,\n ) ||\n !/^[0-9a-f]{64}$/u.test(body.sha256)\n )\n throw new StorageError('Invalid staged body receipt.', {\n code: 'INVALID_ARGUMENT',\n });\n storageInteger(body.size, 'size');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"storage-staged-content.js","sourceRoot":"","sources":["../../src/core/storage-staged-content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAkCtD,4FAA4F;AAC5F,MAAM,OAAO,yBAAyB;IAG3B,OAAO,CAAgB;IACvB,IAAI,CAA8C;IAC3D,YAAY,OAAgD;QAC1D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC;IAC1B,CAAC;IACD,KAAK,CAAC,KAAK,CACT,KAAY,EACZ,IAAgC,EAChC,OAAO,GAA8B,EAAE;QAEvC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAChE,CAAC;IACD,mFAAmF;IACnF,KAAK,CAAC,aAAa,CACjB,KAAY,EACZ,SAAiB,EACjB,IAAgC,EAChC,OAAO,GAA8B,EAAE;QAEvC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAC7B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,gBAAgB,CAAC;QAC7D,cAAc,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QACrC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;QAC/C,IACE,CAAC,YAAY,CAAC,iBAAiB,EAAE,UAAU;YAC3C,CAAC,YAAY,CAAC,eAAe,EAAE,IAAI,EACnC,CAAC;YACD,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACpC,MAAM,IAAI,YAAY,CACpB,yEAAyE,EACzE,EAAE,IAAI,EAAE,eAAe,EAAE,CAC1B,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAClC,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAC/B,IAAI,eAAe,CAAyB;YAC1C,SAAS,CAAC,KAAK,EAAE,UAAU;gBACzB,MAAM,EAAE,cAAc,EAAE,CAAC;gBACzB,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC;gBACzB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,QAAQ;oBAChD,MAAM,IAAI,YAAY,CAAC,sCAAsC,EAAE;wBAC7D,IAAI,EAAE,gBAAgB;qBACvB,CAAC,CAAC;gBACL,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;YACD,KAAK;gBACH,SAAS,GAAG,IAAI,CAAC;YACnB,CAAC;SACF,CAAC,EACF,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CACvC,CAAC;QACF,IAAI,CAAC;YACH,MAAM,EAAE,cAAc,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CACjD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,EAC3B,QAAQ,EACR;gBACE,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC7B,WAAW,EAAE,0BAA0B;gBACvC,OAAO,EAAE,CAAC;gBACV,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;aAC5C,CACF,CAAC;YACF,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;gBACjE,MAAM,IAAI,YAAY,CACpB,wDAAwD,EACxD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CACpC,CAAC;YACJ,OAAO,MAAM,CAAC,MAAM,CAAC;gBACnB,SAAS;gBACT,IAAI;gBACJ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC1B,IAAI,EAAE,MAAM,CAAC,IAAI;aAClB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC7C,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CACR,KAAY,EACZ,IAAuB,EACvB,OAAO,GAA6B,EAAE;QAEtC,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjC,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,SAAS;YACrE,MAAM,IAAI,YAAY,CAAC,wCAAwC,EAAE;gBAC/D,IAAI,EAAE,eAAe;aACtB,CAAC,CAAC;QACL,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,mBAAmB,CACnD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,EAChC;YACE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YAC9B,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;YAChE,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;SACpE,CACF,CAAC;QACF,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IACD,oFAAoF;IACpF,KAAK,CAAC,MAAM,CACV,KAAY,EACZ,IAAuB,EACvB,OAAO,GAAsC,EAAE;QAE/C,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE;YACrE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YAC9B,GAAG,OAAO;SACX,CAAC,CAAC;IACL,CAAC;CACF;AACD,SAAS,YAAY,CAAC,IAAuB;IAC3C,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QACtC,MAAM,IAAI,YAAY,CAAC,8BAA8B,EAAE;YACrD,IAAI,EAAE,kBAAkB;SACzB,CAAC,CAAC;IACL,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,IACE,CAAC,wEAAwE,CAAC,IAAI,CAC5E,SAAS,CACV;QAED,MAAM,IAAI,YAAY,CAAC,kCAAkC,EAAE;YACzD,IAAI,EAAE,kBAAkB;SACzB,CAAC,CAAC;AACP,CAAC","sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport { StorageError } from '../storage.error.js';\nimport type { StorageClient } from '../storage.client.js';\nimport type { StorageByteRange } from '../storage.types.js';\nimport { storageInteger } from './storage-streams.js';\n\nexport interface StorageStagedBody {\n readonly payloadId: string;\n readonly size: number;\n readonly sha256: string;\n readonly etag: string;\n}\nexport interface StorageStagedWriteOptions {\n readonly signal?: AbortSignal | undefined;\n readonly maxBytes?: number | undefined;\n}\nexport interface StorageStagedReadOptions {\n readonly signal?: AbortSignal | undefined;\n readonly range?: StorageByteRange | undefined;\n}\nexport interface StorageStagedContent<Scope> {\n write(\n scope: Scope,\n body: ReadableStream<Uint8Array>,\n options?: StorageStagedWriteOptions,\n ): Promise<StorageStagedBody>;\n read(\n scope: Scope,\n body: StorageStagedBody,\n options?: StorageStagedReadOptions,\n ): Promise<ReadableStream<Uint8Array>>;\n}\nexport interface StorageStagedContentStoreOptions<Scope> {\n readonly client: StorageClient;\n /** Trusted, injective scope mapping; never accept this function from a model. */\n readonly key: (scope: Scope, payloadId: string) => string;\n}\n\n/** Immutable create-only bodies. Retention eligibility and references remain host-owned. */\nexport class StorageStagedContentStore<\n Scope,\n> implements StorageStagedContent<Scope> {\n readonly #client: StorageClient;\n readonly #key: (scope: Scope, payloadId: string) => string;\n constructor(options: StorageStagedContentStoreOptions<Scope>) {\n this.#client = options.client;\n this.#key = options.key;\n }\n async write(\n scope: Scope,\n body: ReadableStream<Uint8Array>,\n options: StorageStagedWriteOptions = {},\n ): Promise<StorageStagedBody> {\n return this.writeReserved(scope, randomUUID(), body, options);\n }\n /** Trusted callers reserve a fresh identity before preparing detached metadata. */\n async writeReserved(\n scope: Scope,\n payloadId: string,\n body: ReadableStream<Uint8Array>,\n options: StorageStagedWriteOptions = {},\n ): Promise<StorageStagedBody> {\n validatePayloadId(payloadId);\n const { signal } = options;\n const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;\n storageInteger(maxBytes, 'maxBytes');\n const capabilities = this.#client.capabilities;\n if (\n !capabilities.conditionalCreate?.resultEtag ||\n !capabilities.conditionalRead?.etag\n ) {\n await body.cancel().catch(() => {});\n throw new StorageError(\n 'Staged content requires native create-only writes and exact ETag reads.',\n { code: 'NOT_SUPPORTED' },\n );\n }\n const hash = createHash('sha256');\n let size = 0;\n let completed = false;\n const measured = body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n signal?.throwIfAborted();\n size += chunk.byteLength;\n if (!Number.isSafeInteger(size) || size > maxBytes)\n throw new StorageError('Staged body exceeds its byte budget.', {\n code: 'LIMIT_EXCEEDED',\n });\n hash.update(chunk);\n controller.enqueue(chunk);\n },\n flush() {\n completed = true;\n },\n }),\n signal === undefined ? {} : { signal },\n );\n try {\n signal?.throwIfAborted();\n const result = await this.#client.uploadConditional(\n this.#key(scope, payloadId),\n measured,\n {\n condition: { type: 'create' },\n contentType: 'application/octet-stream',\n retries: 0,\n ...(signal === undefined ? {} : { signal }),\n },\n );\n if (!completed || result.size !== size || result.etag === undefined)\n throw new StorageError(\n 'Provider did not acknowledge the complete staged body.',\n { code: 'PROVIDER', applied: true },\n );\n return Object.freeze({\n payloadId,\n size,\n sha256: hash.digest('hex'),\n etag: result.etag,\n });\n } catch (error) {\n await measured.cancel(error).catch(() => {});\n throw error;\n }\n }\n async read(\n scope: Scope,\n body: StorageStagedBody,\n options: StorageStagedReadOptions = {},\n ): Promise<ReadableStream<Uint8Array>> {\n validateBody(body);\n options.signal?.throwIfAborted();\n if (options.range !== undefined && !this.#client.capabilities.rangeRead)\n throw new StorageError('Provider does not support byte ranges.', {\n code: 'NOT_SUPPORTED',\n });\n const result = await this.#client.downloadConditional(\n this.#key(scope, body.payloadId),\n {\n condition: { etag: body.etag },\n ...(options.range === undefined ? {} : { range: options.range }),\n ...(options.signal === undefined ? {} : { signal: options.signal }),\n },\n );\n return result.body;\n }\n /** Host must prove no durable/in-flight reference can be created before removal. */\n async remove(\n scope: Scope,\n body: StorageStagedBody,\n options: { readonly signal?: AbortSignal } = {},\n ): Promise<void> {\n validateBody(body);\n await this.#client.deleteConditional(this.#key(scope, body.payloadId), {\n condition: { etag: body.etag },\n ...options,\n });\n }\n}\nfunction validateBody(body: StorageStagedBody): void {\n validatePayloadId(body.payloadId);\n if (!/^[0-9a-f]{64}$/u.test(body.sha256))\n throw new StorageError('Invalid staged body receipt.', {\n code: 'INVALID_ARGUMENT',\n });\n storageInteger(body.size, 'size');\n}\n\nfunction validatePayloadId(payloadId: string): void {\n if (\n !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(\n payloadId,\n )\n )\n throw new StorageError('Invalid staged payload identity.', {\n code: 'INVALID_ARGUMENT',\n });\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/crypto/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,KAAK,mCAAmC,EACxC,KAAK,iCAAiC,EACtC,KAAK,6BAA6B,EAClC,KAAK,kCAAkC,GACxC,MAAM,gCAAgC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/crypto/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,GAK7B,MAAM,gCAAgC,CAAC","sourcesContent":["export {\n StorageEncryptedContentStore,\n type StorageEncryptedContentStoreOptions,\n type StorageEncryptedContentProtection,\n type StorageEncryptedContentRecord,\n type StorageEncryptedContentPersistence,\n} from './storage-encrypted-content.js';\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type FileCipherEngine, type DetachedFileKey } from '@nestm/crypto/files';
|
|
2
|
+
import type { StorageClient } from '../storage.client.js';
|
|
3
|
+
import { type StorageStagedContent, type StorageStagedBody, type StorageStagedReadOptions, type StorageStagedWriteOptions } from '../core/storage-staged-content.js';
|
|
4
|
+
/** Detached encryption metadata. Hosts own serialization, key policy and access control. */
|
|
5
|
+
export interface StorageEncryptedContentProtection {
|
|
6
|
+
readonly headerBytes: Uint8Array;
|
|
7
|
+
readonly detachedKey: DetachedFileKey;
|
|
8
|
+
readonly wrappingContextDigest: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StorageEncryptedContentRecord {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly protection: StorageEncryptedContentProtection;
|
|
13
|
+
readonly ciphertextBody: StorageStagedBody;
|
|
14
|
+
readonly size: number;
|
|
15
|
+
readonly contentDigest: string;
|
|
16
|
+
}
|
|
17
|
+
export interface StorageEncryptedContentPersistence<Scope> {
|
|
18
|
+
/** Atomic create-only reservation; persist before consuming any ciphertext. */
|
|
19
|
+
prepare(scope: Scope, id: string, protection: StorageEncryptedContentProtection): Promise<void>;
|
|
20
|
+
/** Atomically make the prepared body readable after both streams acknowledge completion. */
|
|
21
|
+
complete(scope: Scope, record: StorageEncryptedContentRecord): Promise<void>;
|
|
22
|
+
require(scope: Scope, id: string): Promise<StorageEncryptedContentRecord>;
|
|
23
|
+
/**
|
|
24
|
+
* Retire only after proving reference eligibility, then clean the exact object.
|
|
25
|
+
* An absent receipt means upload completion may be uncertain: retain cleanup inventory.
|
|
26
|
+
* Called only after this writer successfully prepared its fresh identity.
|
|
27
|
+
*/
|
|
28
|
+
discard(scope: Scope, id: string, ciphertextBody?: StorageStagedBody): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface StorageEncryptedContentStoreOptions<Scope> {
|
|
31
|
+
readonly client: StorageClient;
|
|
32
|
+
/** Caller owns the engine and its shutdown lifecycle. */
|
|
33
|
+
readonly cipher: FileCipherEngine;
|
|
34
|
+
readonly key: (scope: Scope, payloadId: string) => string;
|
|
35
|
+
/** Return a fresh context buffer; this store clears it after the engine copies it. */
|
|
36
|
+
readonly aad: (scope: Scope, payloadId: string) => Uint8Array;
|
|
37
|
+
readonly allowedProviders: readonly string[];
|
|
38
|
+
readonly provider?: string;
|
|
39
|
+
readonly metadata: StorageEncryptedContentPersistence<Scope>;
|
|
40
|
+
}
|
|
41
|
+
/** Coordinates immutable ciphertext I/O and authenticated plaintext accounting. */
|
|
42
|
+
export declare class StorageEncryptedContentStore<Scope> implements StorageStagedContent<Scope> {
|
|
43
|
+
#private;
|
|
44
|
+
constructor(options: StorageEncryptedContentStoreOptions<Scope>);
|
|
45
|
+
write(scope: Scope, body: ReadableStream<Uint8Array>, options?: StorageStagedWriteOptions): Promise<StorageStagedBody>;
|
|
46
|
+
read(scope: Scope, expected: StorageStagedBody, options?: StorageStagedReadOptions): Promise<ReadableStream<Uint8Array>>;
|
|
47
|
+
readById(scope: Scope, id: string, options?: StorageStagedReadOptions, expected?: StorageStagedBody): Promise<ReadableStream<Uint8Array>>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=storage-encrypted-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-encrypted-content.d.ts","sourceRoot":"","sources":["../../src/crypto/storage-encrypted-content.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { nmf1EncryptedFileSize, NMF1_FORMAT_MAX_PLAINTEXT_BYTES, } from '@nestm/crypto/files';
|
|
3
|
+
import { StorageError } from '../storage.error.js';
|
|
4
|
+
import { StorageStagedContentStore, } from '../core/storage-staged-content.js';
|
|
5
|
+
import { storageBytesStream, storageInteger } from '../core/storage-streams.js';
|
|
6
|
+
/** Coordinates immutable ciphertext I/O and authenticated plaintext accounting. */
|
|
7
|
+
export class StorageEncryptedContentStore {
|
|
8
|
+
#options;
|
|
9
|
+
#ciphertext;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.#options = {
|
|
12
|
+
...options,
|
|
13
|
+
allowedProviders: Object.freeze([...options.allowedProviders]),
|
|
14
|
+
};
|
|
15
|
+
this.#ciphertext = new StorageStagedContentStore({
|
|
16
|
+
client: options.client,
|
|
17
|
+
key: options.key,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async write(scope, body, options = {}) {
|
|
21
|
+
const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;
|
|
22
|
+
storageInteger(maxBytes, 'maxBytes');
|
|
23
|
+
const signal = options.signal;
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const writeSignal = signal === undefined
|
|
27
|
+
? controller.signal
|
|
28
|
+
: AbortSignal.any([signal, controller.signal]);
|
|
29
|
+
const id = randomUUID();
|
|
30
|
+
const hash = createHash('sha256');
|
|
31
|
+
let plaintextSize = 0;
|
|
32
|
+
const plaintext = body.pipeThrough(new TransformStream({
|
|
33
|
+
transform(chunk, stream) {
|
|
34
|
+
plaintextSize += chunk.byteLength;
|
|
35
|
+
if (!Number.isSafeInteger(plaintextSize) || plaintextSize > maxBytes)
|
|
36
|
+
throw new StorageError('Encrypted content exceeds its byte budget.', { code: 'LIMIT_EXCEEDED' });
|
|
37
|
+
hash.update(chunk);
|
|
38
|
+
stream.enqueue(chunk);
|
|
39
|
+
},
|
|
40
|
+
}), { signal: writeSignal });
|
|
41
|
+
let encryption;
|
|
42
|
+
let upload;
|
|
43
|
+
let stored;
|
|
44
|
+
let prepared = false;
|
|
45
|
+
let aad;
|
|
46
|
+
try {
|
|
47
|
+
aad = this.#options.aad(scope, id);
|
|
48
|
+
encryption = await this.#options.cipher.encrypt(plaintext, {
|
|
49
|
+
aad,
|
|
50
|
+
...(this.#options.provider === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { provider: this.#options.provider }),
|
|
53
|
+
signal: writeSignal,
|
|
54
|
+
});
|
|
55
|
+
const protection = {
|
|
56
|
+
headerBytes: encryption.headerBytes,
|
|
57
|
+
detachedKey: encryption.detachedKey,
|
|
58
|
+
wrappingContextDigest: encryption.wrappingContextDigest,
|
|
59
|
+
};
|
|
60
|
+
await this.#options.metadata.prepare(scope, id, protection);
|
|
61
|
+
prepared = true;
|
|
62
|
+
writeSignal.throwIfAborted();
|
|
63
|
+
const maximum = nmf1EncryptedFileSize(BigInt(maxBytes) < NMF1_FORMAT_MAX_PLAINTEXT_BYTES
|
|
64
|
+
? BigInt(maxBytes)
|
|
65
|
+
: NMF1_FORMAT_MAX_PLAINTEXT_BYTES);
|
|
66
|
+
if (maximum > BigInt(Number.MAX_SAFE_INTEGER))
|
|
67
|
+
throw new StorageError('Ciphertext exceeds safe storage accounting.', {
|
|
68
|
+
code: 'INVALID_ARGUMENT',
|
|
69
|
+
});
|
|
70
|
+
upload = this.#ciphertext
|
|
71
|
+
.writeReserved(scope, id, encryption.encrypted, {
|
|
72
|
+
signal: writeSignal,
|
|
73
|
+
maxBytes: Number(maximum),
|
|
74
|
+
})
|
|
75
|
+
.then((receipt) => {
|
|
76
|
+
stored = receipt;
|
|
77
|
+
return receipt;
|
|
78
|
+
});
|
|
79
|
+
const [ciphertext, summary] = await Promise.all([
|
|
80
|
+
upload,
|
|
81
|
+
encryption.completion,
|
|
82
|
+
]);
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
const size = Number(summary.plaintextBytes);
|
|
85
|
+
if (!Number.isSafeInteger(size) ||
|
|
86
|
+
size !== plaintextSize ||
|
|
87
|
+
size > maxBytes ||
|
|
88
|
+
summary.ciphertextBytes !== BigInt(ciphertext.size) ||
|
|
89
|
+
summary.ciphertextSha256 !== ciphertext.sha256)
|
|
90
|
+
throw new StorageError('The encrypted body was not fully acknowledged.', { code: 'PROVIDER' });
|
|
91
|
+
const contentDigest = hash.digest('hex');
|
|
92
|
+
await this.#options.metadata.complete(scope, {
|
|
93
|
+
id,
|
|
94
|
+
protection,
|
|
95
|
+
ciphertextBody: ciphertext,
|
|
96
|
+
size,
|
|
97
|
+
contentDigest,
|
|
98
|
+
});
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
payloadId: id,
|
|
101
|
+
size,
|
|
102
|
+
sha256: contentDigest,
|
|
103
|
+
etag: ciphertext.etag,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
controller.abort(error);
|
|
108
|
+
await encryption?.cancel(error);
|
|
109
|
+
await Promise.allSettled([upload, encryption?.completion]);
|
|
110
|
+
if (prepared)
|
|
111
|
+
await this.#options.metadata
|
|
112
|
+
.discard(scope, id, stored)
|
|
113
|
+
.catch(() => undefined);
|
|
114
|
+
throw protectedFailure(signal);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
aad?.fill(0);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
read(scope, expected, options) {
|
|
121
|
+
return this.readById(scope, expected.payloadId, options, expected);
|
|
122
|
+
}
|
|
123
|
+
async readById(scope, id, options = {}, expected) {
|
|
124
|
+
const signal = options.signal;
|
|
125
|
+
let aad;
|
|
126
|
+
try {
|
|
127
|
+
signal?.throwIfAborted();
|
|
128
|
+
const record = await this.#options.metadata.require(scope, id);
|
|
129
|
+
if (record.id !== id ||
|
|
130
|
+
record.ciphertextBody.payloadId !== id ||
|
|
131
|
+
(expected !== undefined &&
|
|
132
|
+
(expected.etag !== record.ciphertextBody.etag ||
|
|
133
|
+
expected.sha256 !== record.contentDigest ||
|
|
134
|
+
expected.size !== record.size)))
|
|
135
|
+
throw new StorageError('The encrypted content receipt changed.', {
|
|
136
|
+
code: 'CONFLICT',
|
|
137
|
+
});
|
|
138
|
+
storageInteger(record.size, 'size');
|
|
139
|
+
aad = this.#options.aad(scope, id);
|
|
140
|
+
const input = {
|
|
141
|
+
aad,
|
|
142
|
+
detachedKey: record.protection.detachedKey,
|
|
143
|
+
allowedProviders: this.#options.allowedProviders,
|
|
144
|
+
expectedHeaderBytes: record.protection.headerBytes,
|
|
145
|
+
expectedPlaintextBytes: BigInt(record.size),
|
|
146
|
+
expectedCiphertextBytes: BigInt(record.ciphertextBody.size),
|
|
147
|
+
...(signal === undefined ? {} : { signal }),
|
|
148
|
+
};
|
|
149
|
+
if (options.range !== undefined) {
|
|
150
|
+
const start = options.range.start;
|
|
151
|
+
const end = options.range.end ?? record.size - 1;
|
|
152
|
+
if (!Number.isSafeInteger(start) ||
|
|
153
|
+
start < 0 ||
|
|
154
|
+
!Number.isSafeInteger(end) ||
|
|
155
|
+
end < start ||
|
|
156
|
+
end >= record.size)
|
|
157
|
+
throw new StorageError('The requested content range is invalid.', {
|
|
158
|
+
code: 'INVALID_ARGUMENT',
|
|
159
|
+
});
|
|
160
|
+
const bytes = await this.#options.cipher.decryptRange((range, readSignal) => this.#ciphertext.read(scope, record.ciphertextBody, {
|
|
161
|
+
signal: readSignal,
|
|
162
|
+
range: {
|
|
163
|
+
start: Number(range.start),
|
|
164
|
+
...(range.end === undefined ? {} : { end: Number(range.end) }),
|
|
165
|
+
},
|
|
166
|
+
}), {
|
|
167
|
+
...input,
|
|
168
|
+
offset: BigInt(start),
|
|
169
|
+
length: end - start + 1,
|
|
170
|
+
maxRangeBytes: end - start + 1,
|
|
171
|
+
});
|
|
172
|
+
return storageBytesStream(bytes);
|
|
173
|
+
}
|
|
174
|
+
const decrypted = await this.#options.cipher.decrypt(await this.#ciphertext.read(scope, record.ciphertextBody, options), { ...input, expectedCiphertextSha256: record.ciphertextBody.sha256 });
|
|
175
|
+
const hash = createHash('sha256');
|
|
176
|
+
let size = 0;
|
|
177
|
+
return decrypted.plaintext.pipeThrough(new TransformStream({
|
|
178
|
+
transform(chunk, stream) {
|
|
179
|
+
size += chunk.byteLength;
|
|
180
|
+
hash.update(chunk);
|
|
181
|
+
stream.enqueue(chunk);
|
|
182
|
+
},
|
|
183
|
+
async flush() {
|
|
184
|
+
try {
|
|
185
|
+
await decrypted.verification;
|
|
186
|
+
if (size !== record.size ||
|
|
187
|
+
hash.digest('hex') !== record.contentDigest)
|
|
188
|
+
throw protectedFailure(signal);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw protectedFailure(signal);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
}), signal === undefined ? {} : { signal });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof StorageError &&
|
|
198
|
+
['NOT_FOUND', 'CONFLICT', 'INVALID_ARGUMENT'].includes(error.code))
|
|
199
|
+
throw error;
|
|
200
|
+
throw protectedFailure(signal);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
aad?.fill(0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function protectedFailure(signal) {
|
|
208
|
+
return new StorageError(signal?.aborted
|
|
209
|
+
? 'The encrypted content operation was cancelled.'
|
|
210
|
+
: 'The encrypted content could not be authenticated.', { code: signal?.aborted ? 'ABORTED' : 'PROVIDER' });
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=storage-encrypted-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-encrypted-content.js","sourceRoot":"","sources":["../../src/crypto/storage-encrypted-content.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport {\n type FileCipherEngine,\n nmf1EncryptedFileSize,\n NMF1_FORMAT_MAX_PLAINTEXT_BYTES,\n type DetachedFileKey,\n type FileEncryptResult,\n} from '@nestm/crypto/files';\nimport { StorageError } from '../storage.error.js';\nimport type { StorageClient } from '../storage.client.js';\nimport {\n StorageStagedContentStore,\n type StorageStagedContent,\n type StorageStagedBody,\n type StorageStagedReadOptions,\n type StorageStagedWriteOptions,\n} from '../core/storage-staged-content.js';\nimport { storageBytesStream, storageInteger } from '../core/storage-streams.js';\n\n/** Detached encryption metadata. Hosts own serialization, key policy and access control. */\nexport interface StorageEncryptedContentProtection {\n readonly headerBytes: Uint8Array;\n readonly detachedKey: DetachedFileKey;\n readonly wrappingContextDigest: string;\n}\nexport interface StorageEncryptedContentRecord {\n readonly id: string;\n readonly protection: StorageEncryptedContentProtection;\n readonly ciphertextBody: StorageStagedBody;\n readonly size: number;\n readonly contentDigest: string;\n}\nexport interface StorageEncryptedContentPersistence<Scope> {\n /** Atomic create-only reservation; persist before consuming any ciphertext. */\n prepare(\n scope: Scope,\n id: string,\n protection: StorageEncryptedContentProtection,\n ): Promise<void>;\n /** Atomically make the prepared body readable after both streams acknowledge completion. */\n complete(scope: Scope, record: StorageEncryptedContentRecord): Promise<void>;\n require(scope: Scope, id: string): Promise<StorageEncryptedContentRecord>;\n /**\n * Retire only after proving reference eligibility, then clean the exact object.\n * An absent receipt means upload completion may be uncertain: retain cleanup inventory.\n * Called only after this writer successfully prepared its fresh identity.\n */\n discard(\n scope: Scope,\n id: string,\n ciphertextBody?: StorageStagedBody,\n ): Promise<void>;\n}\nexport interface StorageEncryptedContentStoreOptions<Scope> {\n readonly client: StorageClient;\n /** Caller owns the engine and its shutdown lifecycle. */\n readonly cipher: FileCipherEngine;\n readonly key: (scope: Scope, payloadId: string) => string;\n /** Return a fresh context buffer; this store clears it after the engine copies it. */\n readonly aad: (scope: Scope, payloadId: string) => Uint8Array;\n readonly allowedProviders: readonly string[];\n readonly provider?: string;\n readonly metadata: StorageEncryptedContentPersistence<Scope>;\n}\n\n/** Coordinates immutable ciphertext I/O and authenticated plaintext accounting. */\nexport class StorageEncryptedContentStore<\n Scope,\n> implements StorageStagedContent<Scope> {\n readonly #options: StorageEncryptedContentStoreOptions<Scope>;\n readonly #ciphertext: StorageStagedContentStore<Scope>;\n constructor(options: StorageEncryptedContentStoreOptions<Scope>) {\n this.#options = {\n ...options,\n allowedProviders: Object.freeze([...options.allowedProviders]),\n };\n this.#ciphertext = new StorageStagedContentStore({\n client: options.client,\n key: options.key,\n });\n }\n\n async write(\n scope: Scope,\n body: ReadableStream<Uint8Array>,\n options: StorageStagedWriteOptions = {},\n ): Promise<StorageStagedBody> {\n const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;\n storageInteger(maxBytes, 'maxBytes');\n const signal = options.signal;\n signal?.throwIfAborted();\n const controller = new AbortController();\n const writeSignal =\n signal === undefined\n ? controller.signal\n : AbortSignal.any([signal, controller.signal]);\n const id = randomUUID();\n const hash = createHash('sha256');\n let plaintextSize = 0;\n const plaintext = body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, stream) {\n plaintextSize += chunk.byteLength;\n if (!Number.isSafeInteger(plaintextSize) || plaintextSize > maxBytes)\n throw new StorageError(\n 'Encrypted content exceeds its byte budget.',\n { code: 'LIMIT_EXCEEDED' },\n );\n hash.update(chunk);\n stream.enqueue(chunk);\n },\n }),\n { signal: writeSignal },\n );\n let encryption: FileEncryptResult | undefined;\n let upload: Promise<StorageStagedBody> | undefined;\n let stored: StorageStagedBody | undefined;\n let prepared = false;\n let aad: Uint8Array | undefined;\n try {\n aad = this.#options.aad(scope, id);\n encryption = await this.#options.cipher.encrypt(plaintext, {\n aad,\n ...(this.#options.provider === undefined\n ? {}\n : { provider: this.#options.provider }),\n signal: writeSignal,\n });\n const protection = {\n headerBytes: encryption.headerBytes,\n detachedKey: encryption.detachedKey,\n wrappingContextDigest: encryption.wrappingContextDigest,\n };\n await this.#options.metadata.prepare(scope, id, protection);\n prepared = true;\n writeSignal.throwIfAborted();\n const maximum = nmf1EncryptedFileSize(\n BigInt(maxBytes) < NMF1_FORMAT_MAX_PLAINTEXT_BYTES\n ? BigInt(maxBytes)\n : NMF1_FORMAT_MAX_PLAINTEXT_BYTES,\n );\n if (maximum > BigInt(Number.MAX_SAFE_INTEGER))\n throw new StorageError('Ciphertext exceeds safe storage accounting.', {\n code: 'INVALID_ARGUMENT',\n });\n upload = this.#ciphertext\n .writeReserved(scope, id, encryption.encrypted, {\n signal: writeSignal,\n maxBytes: Number(maximum),\n })\n .then((receipt) => {\n stored = receipt;\n return receipt;\n });\n const [ciphertext, summary] = await Promise.all([\n upload,\n encryption.completion,\n ]);\n signal?.throwIfAborted();\n const size = Number(summary.plaintextBytes);\n if (\n !Number.isSafeInteger(size) ||\n size !== plaintextSize ||\n size > maxBytes ||\n summary.ciphertextBytes !== BigInt(ciphertext.size) ||\n summary.ciphertextSha256 !== ciphertext.sha256\n )\n throw new StorageError(\n 'The encrypted body was not fully acknowledged.',\n { code: 'PROVIDER' },\n );\n const contentDigest = hash.digest('hex');\n await this.#options.metadata.complete(scope, {\n id,\n protection,\n ciphertextBody: ciphertext,\n size,\n contentDigest,\n });\n return Object.freeze({\n payloadId: id,\n size,\n sha256: contentDigest,\n etag: ciphertext.etag,\n });\n } catch (error: unknown) {\n controller.abort(error);\n await encryption?.cancel(error);\n await Promise.allSettled([upload, encryption?.completion]);\n if (prepared)\n await this.#options.metadata\n .discard(scope, id, stored)\n .catch(() => undefined);\n throw protectedFailure(signal);\n } finally {\n aad?.fill(0);\n }\n }\n\n read(\n scope: Scope,\n expected: StorageStagedBody,\n options?: StorageStagedReadOptions,\n ): Promise<ReadableStream<Uint8Array>> {\n return this.readById(scope, expected.payloadId, options, expected);\n }\n\n async readById(\n scope: Scope,\n id: string,\n options: StorageStagedReadOptions = {},\n expected?: StorageStagedBody,\n ): Promise<ReadableStream<Uint8Array>> {\n const signal = options.signal;\n let aad: Uint8Array | undefined;\n try {\n signal?.throwIfAborted();\n const record = await this.#options.metadata.require(scope, id);\n if (\n record.id !== id ||\n record.ciphertextBody.payloadId !== id ||\n (expected !== undefined &&\n (expected.etag !== record.ciphertextBody.etag ||\n expected.sha256 !== record.contentDigest ||\n expected.size !== record.size))\n )\n throw new StorageError('The encrypted content receipt changed.', {\n code: 'CONFLICT',\n });\n storageInteger(record.size, 'size');\n aad = this.#options.aad(scope, id);\n const input = {\n aad,\n detachedKey: record.protection.detachedKey,\n allowedProviders: this.#options.allowedProviders,\n expectedHeaderBytes: record.protection.headerBytes,\n expectedPlaintextBytes: BigInt(record.size),\n expectedCiphertextBytes: BigInt(record.ciphertextBody.size),\n ...(signal === undefined ? {} : { signal }),\n };\n if (options.range !== undefined) {\n const start = options.range.start;\n const end = options.range.end ?? record.size - 1;\n if (\n !Number.isSafeInteger(start) ||\n start < 0 ||\n !Number.isSafeInteger(end) ||\n end < start ||\n end >= record.size\n )\n throw new StorageError('The requested content range is invalid.', {\n code: 'INVALID_ARGUMENT',\n });\n const bytes = await this.#options.cipher.decryptRange(\n (range, readSignal) =>\n this.#ciphertext.read(scope, record.ciphertextBody, {\n signal: readSignal,\n range: {\n start: Number(range.start),\n ...(range.end === undefined ? {} : { end: Number(range.end) }),\n },\n }),\n {\n ...input,\n offset: BigInt(start),\n length: end - start + 1,\n maxRangeBytes: end - start + 1,\n },\n );\n return storageBytesStream(bytes);\n }\n const decrypted = await this.#options.cipher.decrypt(\n await this.#ciphertext.read(scope, record.ciphertextBody, options),\n { ...input, expectedCiphertextSha256: record.ciphertextBody.sha256 },\n );\n const hash = createHash('sha256');\n let size = 0;\n return decrypted.plaintext.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, stream) {\n size += chunk.byteLength;\n hash.update(chunk);\n stream.enqueue(chunk);\n },\n async flush() {\n try {\n await decrypted.verification;\n if (\n size !== record.size ||\n hash.digest('hex') !== record.contentDigest\n )\n throw protectedFailure(signal);\n } catch {\n throw protectedFailure(signal);\n }\n },\n }),\n signal === undefined ? {} : { signal },\n );\n } catch (error: unknown) {\n if (\n error instanceof StorageError &&\n ['NOT_FOUND', 'CONFLICT', 'INVALID_ARGUMENT'].includes(error.code)\n )\n throw error;\n throw protectedFailure(signal);\n } finally {\n aad?.fill(0);\n }\n }\n}\n\nfunction protectedFailure(signal?: AbortSignal): StorageError {\n return new StorageError(\n signal?.aborted\n ? 'The encrypted content operation was cancelled.'\n : 'The encrypted content could not be authenticated.',\n { code: signal?.aborted ? 'ABORTED' : 'PROVIDER' },\n );\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestm/storage",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"description": "Framework-neutral storage clients, capability-scoped agent workspaces, NestJS 12 integration, and guarded HTTP access.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kauan Guesser",
|
|
@@ -59,7 +59,11 @@
|
|
|
59
59
|
"types": "./dist/testing/index.d.ts",
|
|
60
60
|
"import": "./dist/testing/index.js"
|
|
61
61
|
},
|
|
62
|
-
"./package.json": "./package.json"
|
|
62
|
+
"./package.json": "./package.json",
|
|
63
|
+
"./crypto": {
|
|
64
|
+
"types": "./dist/crypto/index.d.ts",
|
|
65
|
+
"import": "./dist/crypto/index.js"
|
|
66
|
+
}
|
|
63
67
|
},
|
|
64
68
|
"main": "./dist/index.js",
|
|
65
69
|
"types": "./dist/index.d.ts",
|
|
@@ -103,7 +107,8 @@
|
|
|
103
107
|
"ai": ">=7.0.0 <8.0.0",
|
|
104
108
|
"reflect-metadata": "^0.2.2",
|
|
105
109
|
"rxjs": "^7.8.1",
|
|
106
|
-
"zod": "^4.1.8"
|
|
110
|
+
"zod": "^4.1.8",
|
|
111
|
+
"@nestm/crypto": "0.1.0-alpha.9"
|
|
107
112
|
},
|
|
108
113
|
"peerDependenciesMeta": {
|
|
109
114
|
"@aws-sdk/client-s3": {
|
|
@@ -135,6 +140,9 @@
|
|
|
135
140
|
},
|
|
136
141
|
"zod": {
|
|
137
142
|
"optional": true
|
|
143
|
+
},
|
|
144
|
+
"@nestm/crypto": {
|
|
145
|
+
"optional": true
|
|
138
146
|
}
|
|
139
147
|
},
|
|
140
148
|
"devDependencies": {
|
|
@@ -161,7 +169,8 @@
|
|
|
161
169
|
"supertest": "7.2.2",
|
|
162
170
|
"typescript": "7.0.2",
|
|
163
171
|
"vitest": "4.1.11",
|
|
164
|
-
"zod": "4.4.3"
|
|
172
|
+
"zod": "4.4.3",
|
|
173
|
+
"@nestm/crypto": "0.1.0-alpha.9"
|
|
165
174
|
},
|
|
166
175
|
"scripts": {
|
|
167
176
|
"build": "node scripts/clean.mjs && tsc -p tsconfig.build.json",
|