@nestm/storage 0.1.0-alpha.13 → 0.1.0-alpha.14
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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @nestm/storage
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 79609f1: Use conditional multipart completion for staged content streams when the provider supports create-only completion. This enables encrypted file writes on AWS S3 without buffering the entire content or weakening immutable creation and exact ETag reads.
|
|
8
|
+
|
|
3
9
|
## 0.1.0-alpha.13
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -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,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,
|
|
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,CAqE5B;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"}
|
|
@@ -47,6 +47,11 @@ export class StorageStagedContentStore {
|
|
|
47
47
|
const result = await this.#client.uploadConditional(this.#key(scope, payloadId), measured, {
|
|
48
48
|
condition: { type: 'create' },
|
|
49
49
|
contentType: 'application/octet-stream',
|
|
50
|
+
// Unknown-length streams need bounded parts on providers such as S3.
|
|
51
|
+
// Only opt in when the provider can enforce create at completion.
|
|
52
|
+
...(capabilities.conditionalMultipartCompletion?.create === true && {
|
|
53
|
+
multipart: true,
|
|
54
|
+
}),
|
|
50
55
|
retries: 0,
|
|
51
56
|
...(signal === undefined ? {} : { signal }),
|
|
52
57
|
});
|
|
@@ -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 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"]}
|
|
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 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 // Unknown-length streams need bounded parts on providers such as S3.\n // Only opt in when the provider can enforce create at completion.\n ...(capabilities.conditionalMultipartCompletion?.create === true && {\n multipart: true,\n }),\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"]}
|
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.14",
|
|
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",
|