@nestm/storage 0.1.0-alpha.11 → 0.1.0-alpha.13

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,21 @@
1
1
  # @nestm/storage
2
2
 
3
+ ## 0.1.0-alpha.13
4
+
5
+ ### Minor Changes
6
+
7
+ - 211e8c4: Add native Azure Blob create-only uploads, ETag-conditional replacement and deletion,
8
+ and exact ETag reads with ranges through the Files SDK conditional pipeline. Expose
9
+ `createAzureStorageDriver` and decorate the named Azure provider with the same
10
+ primitives, allowing staged encrypted content to run on Azure without application
11
+ provider code.
12
+
13
+ ## 0.1.0-alpha.12
14
+
15
+ ### Patch Changes
16
+
17
+ - 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`.
18
+
3
19
  ## 0.1.0-alpha.11
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -528,6 +528,54 @@ ordering, bounds and the actual local SHA-256 before returning the next offset.
528
528
  Filename/size/mtime are insufficient proof of resumed content identity. Filename
529
529
  classification, browser session storage and HTTP transport remain host policy.
530
530
 
531
+ ### Azure Blob staged content
532
+
533
+ Use the Azure entrypoint for native create-only uploads, ETag-conditional
534
+ replacement/deletion, and exact ETag reads, including byte ranges. These operations
535
+ run through the Files SDK conditional pipeline, so hooks, retries, prefix policy,
536
+ readonly checks, and sanitized errors apply to them. The named `azure` provider
537
+ also receives these primitives.
538
+
539
+ ```ts
540
+ import { DefaultAzureCredential } from '@azure/identity';
541
+ import { createAzureStorageDriver } from '@nestm/storage/files-sdk/azure';
542
+
543
+ const driver = createAzureStorageDriver({
544
+ adapter: {
545
+ accountName: 'workspacestorage',
546
+ container: 'private-content',
547
+ credential: new DefaultAzureCredential(),
548
+ useUserDelegationSas: false,
549
+ },
550
+ });
551
+ ```
552
+
553
+ Install `@azure/storage-blob`, `@azure/core-auth`, and the credential provider used
554
+ by the host. The Blob container must already exist. Grant the workload identity
555
+ the required Blob data permissions; signed URLs are unnecessary for protected
556
+ server-side content operations. Explicit token credentials cannot be combined
557
+ with shared keys, SAS tokens, or connection strings, and cannot be overridden by
558
+ ambient shared-key environment variables.
559
+
560
+ Conditional uploads stream through bounded Azure SDK blocks and atomically apply
561
+ their predicate when committing. They return the committed ETag and actual byte
562
+ count without a separate metadata read. Failed uploads may leave uncommitted
563
+ blocks for Azure's own expiration; they never fall back to unconditional writes.
564
+ Exact reads verify the response ETag before exposing its body. The driver uses a
565
+ conservative 1024-byte UTF-8 physical-key budget within Azure's name limit.
566
+ Conditional server-side copy, version-ID predicates, and explicit conditional
567
+ multipart completion are not advertised. Workspace safe copy/move can use the
568
+ existing exact-read/create/delete workflow. The generic staged and encrypted
569
+ content stores require no Azure-specific changes.
570
+
571
+ Run `test/azure-content.e2e-spec.ts` with `STORAGE_AZURE_CONFORMANCE=true` and
572
+ `STORAGE_AZURE_TEST_CONNECTION_STRING` against a dedicated account or Azurite.
573
+ For live identity tests, supply `STORAGE_AZURE_TEST_ACCOUNT_NAME` instead; the
574
+ suite uses `DefaultAzureCredential`. It creates and deletes its own uniquely
575
+ named test container. The CI Azure job covers native predicates, competing
576
+ writes, bounded streams, ranges, encrypted content, and the provider conformance
577
+ suite on Azurite. Live Azure identity remains a separate deployment check.
578
+
531
579
  ## Configure named stores
532
580
 
533
581
  Use the package-owned S3 factory when applicable. For other providers, create a
@@ -1236,3 +1284,11 @@ or direct filesystem calls.
1236
1284
  ## License
1237
1285
 
1238
1286
  MIT
1287
+
1288
+ ## Encrypted staged content
1289
+
1290
+ `@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.
1291
+
1292
+ 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).
1293
+
1294
+ `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
- 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(body.payloadId) ||
88
- !/^[0-9a-f]{64}$/u.test(body.sha256))
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,2 @@
1
+ export { StorageEncryptedContentStore, type StorageEncryptedContentStoreOptions, type StorageEncryptedContentProtection, type StorageEncryptedContentRecord, type StorageEncryptedContentPersistence, } from './storage-encrypted-content.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { StorageEncryptedContentStore, } from './storage-encrypted-content.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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"]}
@@ -0,0 +1,5 @@
1
+ import { type AzureAdapter } from 'files-sdk/azure';
2
+ import type { FilesSdkPhysicalKeyAdapter } from '../files-sdk.driver.js';
3
+ /** Native Azure predicates execute inside Files SDK's conditional policy pipeline. */
4
+ export declare function withAzureConditionalOperations(adapter: AzureAdapter): AzureAdapter & FilesSdkPhysicalKeyAdapter;
5
+ //# sourceMappingURL=azure-conditional.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"azure-conditional.d.ts","sourceRoot":"","sources":["../../../src/files-sdk/azure/azure-conditional.ts"],"names":[],"mappings":"AASA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAMzE,sFAAsF;AACtF,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,YAAY,GACpB,YAAY,GAAG,0BAA0B,CAyK3C"}
@@ -0,0 +1,184 @@
1
+ import { Readable } from 'node:stream';
2
+ import { createStoredFile, FilesError, } from 'files-sdk';
3
+ import { mapAzureError } from 'files-sdk/azure';
4
+ import { normalizeProviderStorageEtag, storageEtagHeader, } from '../../storage-etag.js';
5
+ /** Native Azure predicates execute inside Files SDK's conditional policy pipeline. */
6
+ export function withAzureConditionalOperations(adapter) {
7
+ const container = adapter.raw.getContainerClient(adapter.bucket);
8
+ const upload = async (key, body, expectedEtag, options) => {
9
+ const conditions = expectedEtag === null
10
+ ? { ifNoneMatch: '*' }
11
+ : { ifMatch: etagHeader(expectedEtag) };
12
+ const contentType = options?.contentType ??
13
+ (body instanceof Blob && body.type
14
+ ? body.type
15
+ : 'application/octet-stream');
16
+ let size = 0;
17
+ const reader = bodyStream(body).getReader();
18
+ const node = Readable.from((async function* () {
19
+ for (;;) {
20
+ const next = await reader.read();
21
+ options?.signal?.throwIfAborted();
22
+ if (next.done)
23
+ break;
24
+ const chunk = next.value;
25
+ size += chunk.byteLength;
26
+ if (!Number.isSafeInteger(size))
27
+ throw new FilesError('Provider', 'Azure upload is too large.');
28
+ // Azure's upload scheduler requires Buffer.copy(), including when
29
+ // the source is a web stream of ordinary Uint8Array chunks.
30
+ yield Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
31
+ }
32
+ })(), { objectMode: false });
33
+ // Destroying the Node stream also cancels the web source, including a source
34
+ // blocked waiting for its next chunk when cancellation arrives.
35
+ const abort = () => {
36
+ void reader.cancel(options?.signal?.reason).catch(() => { });
37
+ };
38
+ options?.signal?.addEventListener('abort', abort, { once: true });
39
+ try {
40
+ options?.signal?.throwIfAborted();
41
+ const result = await container
42
+ .getBlockBlobClient(key)
43
+ .uploadStream(node, 4 * 1024 * 1024, 2, {
44
+ conditions,
45
+ blobHTTPHeaders: {
46
+ blobContentType: contentType,
47
+ ...(options?.cacheControl === undefined
48
+ ? {}
49
+ : { blobCacheControl: options.cacheControl }),
50
+ },
51
+ ...(options?.metadata === undefined
52
+ ? {}
53
+ : { metadata: options.metadata }),
54
+ ...(options?.signal === undefined
55
+ ? {}
56
+ : { abortSignal: options.signal }),
57
+ ...(options?.onProgress === undefined
58
+ ? {}
59
+ : {
60
+ onProgress: ({ loadedBytes }) => options.onProgress?.({ loaded: loadedBytes }),
61
+ }),
62
+ });
63
+ return {
64
+ key,
65
+ size,
66
+ contentType,
67
+ etag: requiredEtag(result.etag),
68
+ ...(result.lastModified === undefined
69
+ ? {}
70
+ : { lastModified: result.lastModified.getTime() }),
71
+ };
72
+ }
73
+ catch (error) {
74
+ throw mapAzureError(error);
75
+ }
76
+ finally {
77
+ options?.signal?.removeEventListener('abort', abort);
78
+ node.destroy();
79
+ await reader.cancel().catch(() => { });
80
+ reader.releaseLock();
81
+ }
82
+ };
83
+ const conditional = {
84
+ create: (key, body, options) => upload(key, body, null, options),
85
+ replace: (key, body, etag, options) => upload(key, body, etag, options),
86
+ exactRead: async (key, etag, options) => {
87
+ const ifMatch = etagHeader(etag);
88
+ options?.signal?.throwIfAborted();
89
+ try {
90
+ const range = options?.range;
91
+ const result = await container
92
+ .getBlobClient(key)
93
+ .download(range?.start ?? 0, range?.end === undefined ? undefined : range.end - range.start + 1, {
94
+ conditions: { ifMatch },
95
+ ...(options?.signal === undefined
96
+ ? {}
97
+ : { abortSignal: options.signal }),
98
+ });
99
+ const stream = result.readableStreamBody;
100
+ if (!stream)
101
+ throw new FilesError('Provider', 'Azure returned no download stream.');
102
+ let returnedEtag;
103
+ try {
104
+ returnedEtag = requiredEtag(result.etag);
105
+ if (returnedEtag !== etag)
106
+ throw new FilesError('Conflict', 'Azure object condition did not match.');
107
+ }
108
+ catch (error) {
109
+ stream.destroy();
110
+ throw error;
111
+ }
112
+ return createStoredFile({
113
+ key,
114
+ etag: returnedEtag,
115
+ size: result.contentLength ?? 0,
116
+ type: result.contentType ?? 'application/octet-stream',
117
+ ...(result.lastModified === undefined
118
+ ? {}
119
+ : { lastModified: result.lastModified.getTime() }),
120
+ ...(result.metadata === undefined
121
+ ? {}
122
+ : { metadata: result.metadata }),
123
+ }, {
124
+ kind: 'stream',
125
+ factory: () => Readable.toWeb(Readable.from(stream)),
126
+ });
127
+ }
128
+ catch (error) {
129
+ throw mapAzureError(error);
130
+ }
131
+ },
132
+ delete: async (key, etag, options) => {
133
+ const ifMatch = etagHeader(etag);
134
+ options?.signal?.throwIfAborted();
135
+ try {
136
+ await container.getBlobClient(key).delete({
137
+ conditions: { ifMatch },
138
+ ...(options?.signal === undefined
139
+ ? {}
140
+ : { abortSignal: options.signal }),
141
+ });
142
+ }
143
+ catch (error) {
144
+ throw mapAzureError(error);
145
+ }
146
+ },
147
+ };
148
+ return {
149
+ ...adapter,
150
+ conditional: Object.freeze(conditional),
151
+ // A conservative UTF-8 budget within Azure's 1024-character blob-name limit.
152
+ physicalKey: Object.freeze({ maxBytes: 1024 }),
153
+ };
154
+ }
155
+ function etagHeader(etag) {
156
+ const header = storageEtagHeader(etag);
157
+ if (header === undefined)
158
+ throw new FilesError('Provider', 'Azure requires one canonical strong ETag.');
159
+ return header;
160
+ }
161
+ function requiredEtag(value) {
162
+ const etag = normalizeProviderStorageEtag(value);
163
+ if (etag === undefined)
164
+ throw new FilesError('Provider', 'Azure returned no canonical strong ETag.');
165
+ return etag;
166
+ }
167
+ function bodyStream(body) {
168
+ if (body instanceof ReadableStream)
169
+ return body;
170
+ if (body instanceof Blob)
171
+ return body.stream();
172
+ const bytes = typeof body === 'string'
173
+ ? new TextEncoder().encode(body)
174
+ : ArrayBuffer.isView(body)
175
+ ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
176
+ : new Uint8Array(body);
177
+ return new ReadableStream({
178
+ start(controller) {
179
+ controller.enqueue(bytes);
180
+ controller.close();
181
+ },
182
+ });
183
+ }
184
+ //# sourceMappingURL=azure-conditional.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"azure-conditional.js","sourceRoot":"","sources":["../../../src/files-sdk/azure/azure-conditional.ts"],"names":[],"mappings":"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","sourcesContent":["import { Readable } from 'node:stream';\nimport {\n createStoredFile,\n FilesError,\n type AdapterConditionalOperations,\n type AdapterUploadOptions,\n type Body,\n type ConditionalUploadResult,\n} from 'files-sdk';\nimport { mapAzureError, type AzureAdapter } from 'files-sdk/azure';\nimport type { FilesSdkPhysicalKeyAdapter } from '../files-sdk.driver.js';\nimport {\n normalizeProviderStorageEtag,\n storageEtagHeader,\n} from '../../storage-etag.js';\n\n/** Native Azure predicates execute inside Files SDK's conditional policy pipeline. */\nexport function withAzureConditionalOperations(\n adapter: AzureAdapter,\n): AzureAdapter & FilesSdkPhysicalKeyAdapter {\n const container = adapter.raw.getContainerClient(adapter.bucket);\n const upload = async (\n key: string,\n body: Body,\n expectedEtag: string | null,\n options?: AdapterUploadOptions,\n ): Promise<ConditionalUploadResult> => {\n const conditions =\n expectedEtag === null\n ? { ifNoneMatch: '*' }\n : { ifMatch: etagHeader(expectedEtag) };\n const contentType =\n options?.contentType ??\n (body instanceof Blob && body.type\n ? body.type\n : 'application/octet-stream');\n let size = 0;\n const reader = bodyStream(body).getReader();\n const node = Readable.from(\n (async function* () {\n for (;;) {\n const next = await reader.read();\n options?.signal?.throwIfAborted();\n if (next.done) break;\n const chunk = next.value;\n size += chunk.byteLength;\n if (!Number.isSafeInteger(size))\n throw new FilesError('Provider', 'Azure upload is too large.');\n // Azure's upload scheduler requires Buffer.copy(), including when\n // the source is a web stream of ordinary Uint8Array chunks.\n yield Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\n }\n })(),\n { objectMode: false },\n );\n // Destroying the Node stream also cancels the web source, including a source\n // blocked waiting for its next chunk when cancellation arrives.\n const abort = () => {\n void reader.cancel(options?.signal?.reason).catch(() => {});\n };\n options?.signal?.addEventListener('abort', abort, { once: true });\n try {\n options?.signal?.throwIfAborted();\n const result = await container\n .getBlockBlobClient(key)\n .uploadStream(node, 4 * 1024 * 1024, 2, {\n conditions,\n blobHTTPHeaders: {\n blobContentType: contentType,\n ...(options?.cacheControl === undefined\n ? {}\n : { blobCacheControl: options.cacheControl }),\n },\n ...(options?.metadata === undefined\n ? {}\n : { metadata: options.metadata }),\n ...(options?.signal === undefined\n ? {}\n : { abortSignal: options.signal }),\n ...(options?.onProgress === undefined\n ? {}\n : {\n onProgress: ({ loadedBytes }) =>\n options.onProgress?.({ loaded: loadedBytes }),\n }),\n });\n return {\n key,\n size,\n contentType,\n etag: requiredEtag(result.etag),\n ...(result.lastModified === undefined\n ? {}\n : { lastModified: result.lastModified.getTime() }),\n };\n } catch (error) {\n throw mapAzureError(error);\n } finally {\n options?.signal?.removeEventListener('abort', abort);\n node.destroy();\n await reader.cancel().catch(() => {});\n reader.releaseLock();\n }\n };\n const conditional: AdapterConditionalOperations = {\n create: (key, body, options) => upload(key, body, null, options),\n replace: (key, body, etag, options) => upload(key, body, etag, options),\n exactRead: async (key, etag, options) => {\n const ifMatch = etagHeader(etag);\n options?.signal?.throwIfAborted();\n try {\n const range = options?.range;\n const result = await container\n .getBlobClient(key)\n .download(\n range?.start ?? 0,\n range?.end === undefined ? undefined : range.end - range.start + 1,\n {\n conditions: { ifMatch },\n ...(options?.signal === undefined\n ? {}\n : { abortSignal: options.signal }),\n },\n );\n const stream = result.readableStreamBody;\n if (!stream)\n throw new FilesError(\n 'Provider',\n 'Azure returned no download stream.',\n );\n let returnedEtag: string;\n try {\n returnedEtag = requiredEtag(result.etag);\n if (returnedEtag !== etag)\n throw new FilesError(\n 'Conflict',\n 'Azure object condition did not match.',\n );\n } catch (error) {\n stream.destroy();\n throw error;\n }\n return createStoredFile(\n {\n key,\n etag: returnedEtag,\n size: result.contentLength ?? 0,\n type: result.contentType ?? 'application/octet-stream',\n ...(result.lastModified === undefined\n ? {}\n : { lastModified: result.lastModified.getTime() }),\n ...(result.metadata === undefined\n ? {}\n : { metadata: result.metadata }),\n },\n {\n kind: 'stream',\n factory: () =>\n Readable.toWeb(\n Readable.from(stream),\n ) as ReadableStream<Uint8Array>,\n },\n );\n } catch (error) {\n throw mapAzureError(error);\n }\n },\n delete: async (key, etag, options) => {\n const ifMatch = etagHeader(etag);\n options?.signal?.throwIfAborted();\n try {\n await container.getBlobClient(key).delete({\n conditions: { ifMatch },\n ...(options?.signal === undefined\n ? {}\n : { abortSignal: options.signal }),\n });\n } catch (error) {\n throw mapAzureError(error);\n }\n },\n };\n return {\n ...adapter,\n conditional: Object.freeze(conditional),\n // A conservative UTF-8 budget within Azure's 1024-character blob-name limit.\n physicalKey: Object.freeze({ maxBytes: 1024 }),\n };\n}\n\nfunction etagHeader(etag: string): string {\n const header = storageEtagHeader(etag);\n if (header === undefined)\n throw new FilesError(\n 'Provider',\n 'Azure requires one canonical strong ETag.',\n );\n return header;\n}\n\nfunction requiredEtag(value: unknown): string {\n const etag = normalizeProviderStorageEtag(value);\n if (etag === undefined)\n throw new FilesError(\n 'Provider',\n 'Azure returned no canonical strong ETag.',\n );\n return etag;\n}\n\nfunction bodyStream(body: Body): ReadableStream<Uint8Array> {\n if (body instanceof ReadableStream) return body;\n if (body instanceof Blob) return body.stream();\n const bytes =\n typeof body === 'string'\n ? new TextEncoder().encode(body)\n : ArrayBuffer.isView(body)\n ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength)\n : new Uint8Array(body);\n return new ReadableStream({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { type AzureAdapter, type AzureAdapterOptions } from 'files-sdk/azure';
2
+ import { type FilesSdkDriverOptions, type FilesSdkStorageDriver } from '../files-sdk.driver.js';
3
+ export interface AzureStorageDriverOptions extends Omit<FilesSdkDriverOptions<AzureAdapter>, 'adapter'> {
4
+ adapter: AzureAdapterOptions;
5
+ }
6
+ /** Files SDK Azure adapter with native create/replace/read/delete predicates. */
7
+ export declare function azure(options: AzureAdapterOptions): AzureAdapter;
8
+ export declare function createAzureStorageDriver(options: AzureStorageDriverOptions): FilesSdkStorageDriver<AzureAdapter>;
9
+ export type { AzureAdapter, AzureAdapterOptions } from 'files-sdk/azure';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/files-sdk/azure/index.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAGhC,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CACrD,qBAAqB,CAAC,YAAY,CAAC,EACnC,SAAS,CACV;IACC,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,iFAAiF;AACjF,wBAAgB,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAyBhE;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GACjC,qBAAqB,CAAC,YAAY,CAAC,CAOrC;AAED,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { FilesError } from 'files-sdk';
2
+ import { azure as createFilesSdkAzureAdapter, } from 'files-sdk/azure';
3
+ import { createFilesSdkDriver, mapFilesSdkError, } from '../files-sdk.driver.js';
4
+ import { withAzureConditionalOperations } from './azure-conditional.js';
5
+ /** Files SDK Azure adapter with native create/replace/read/delete predicates. */
6
+ export function azure(options) {
7
+ if (options.credential !== undefined) {
8
+ if (!options.accountName ||
9
+ options.connectionString ||
10
+ options.accountKey ||
11
+ options.sasToken) {
12
+ throw new FilesError('Provider', 'Azure token authentication requires an account name and no competing credentials.');
13
+ }
14
+ // Explicit workload identity must not silently fall back to ambient shared
15
+ // keys or connection strings inherited from an unrelated Azure application.
16
+ return withAzureConditionalOperations(createFilesSdkAzureAdapter({
17
+ ...options,
18
+ connectionString: '',
19
+ accountKey: '',
20
+ sasToken: '',
21
+ }));
22
+ }
23
+ return withAzureConditionalOperations(createFilesSdkAzureAdapter(options));
24
+ }
25
+ export function createAzureStorageDriver(options) {
26
+ const { adapter, ...driver } = options;
27
+ try {
28
+ return createFilesSdkDriver({ ...driver, adapter: azure(adapter) });
29
+ }
30
+ catch (error) {
31
+ throw mapFilesSdkError(error);
32
+ }
33
+ }
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/files-sdk/azure/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EACL,KAAK,IAAI,0BAA0B,GAGpC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,GAGjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AASxE,iFAAiF;AACjF,MAAM,UAAU,KAAK,CAAC,OAA4B;IAChD,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,IACE,CAAC,OAAO,CAAC,WAAW;YACpB,OAAO,CAAC,gBAAgB;YACxB,OAAO,CAAC,UAAU;YAClB,OAAO,CAAC,QAAQ,EAChB,CAAC;YACD,MAAM,IAAI,UAAU,CAClB,UAAU,EACV,mFAAmF,CACpF,CAAC;QACJ,CAAC;QACD,2EAA2E;QAC3E,4EAA4E;QAC5E,OAAO,8BAA8B,CACnC,0BAA0B,CAAC;YACzB,GAAG,OAAO;YACV,gBAAgB,EAAE,EAAE;YACpB,UAAU,EAAE,EAAE;YACd,QAAQ,EAAE,EAAE;SACb,CAAC,CACH,CAAC;IACJ,CAAC;IACD,OAAO,8BAA8B,CAAC,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,OAAkC;IAElC,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IACvC,IAAI,CAAC;QACH,OAAO,oBAAoB,CAAC,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;AACH,CAAC","sourcesContent":["import { FilesError } from 'files-sdk';\nimport {\n azure as createFilesSdkAzureAdapter,\n type AzureAdapter,\n type AzureAdapterOptions,\n} from 'files-sdk/azure';\nimport {\n createFilesSdkDriver,\n mapFilesSdkError,\n type FilesSdkDriverOptions,\n type FilesSdkStorageDriver,\n} from '../files-sdk.driver.js';\nimport { withAzureConditionalOperations } from './azure-conditional.js';\n\nexport interface AzureStorageDriverOptions extends Omit<\n FilesSdkDriverOptions<AzureAdapter>,\n 'adapter'\n> {\n adapter: AzureAdapterOptions;\n}\n\n/** Files SDK Azure adapter with native create/replace/read/delete predicates. */\nexport function azure(options: AzureAdapterOptions): AzureAdapter {\n if (options.credential !== undefined) {\n if (\n !options.accountName ||\n options.connectionString ||\n options.accountKey ||\n options.sasToken\n ) {\n throw new FilesError(\n 'Provider',\n 'Azure token authentication requires an account name and no competing credentials.',\n );\n }\n // Explicit workload identity must not silently fall back to ambient shared\n // keys or connection strings inherited from an unrelated Azure application.\n return withAzureConditionalOperations(\n createFilesSdkAzureAdapter({\n ...options,\n connectionString: '',\n accountKey: '',\n sasToken: '',\n }),\n );\n }\n return withAzureConditionalOperations(createFilesSdkAzureAdapter(options));\n}\n\nexport function createAzureStorageDriver(\n options: AzureStorageDriverOptions,\n): FilesSdkStorageDriver<AzureAdapter> {\n const { adapter, ...driver } = options;\n try {\n return createFilesSdkDriver({ ...driver, adapter: azure(adapter) });\n } catch (error) {\n throw mapFilesSdkError(error);\n }\n}\n\nexport type { AzureAdapter, AzureAdapterOptions } from 'files-sdk/azure';\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/files-sdk/provider/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAMnE,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC;AAEvE,MAAM,WAAW,4BAA6B,SAAQ,IAAI,CACxD,qBAAqB,CAAC,OAAO,CAAC,EAC9B,SAAS,CACV;IACC,wFAAwF;IACxF,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,qBAAqB,CAAC,CAoBhC;AA2HD,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,mBAAmB,CAE7E;AAED,wDAAwD;AACxD,wBAAgB,oBAAoB,IAAI,SAAS,QAAQ,EAAE,CAE1D;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAErE;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjE;AAED,6EAA6E;AAC7E,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE;AAED,YAAY,EACV,MAAM,IAAI,qBAAqB,EAC/B,QAAQ,IAAI,mBAAmB,GAChC,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/files-sdk/provider/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAMnE,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC;AAEvE,MAAM,WAAW,4BAA6B,SAAQ,IAAI,CACxD,qBAAqB,CAAC,OAAO,CAAC,EAC9B,SAAS,CACV;IACC,wFAAwF;IACxF,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,qBAAqB,CAAC,CAoBhC;AAkID,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,mBAAmB,CAE7E;AAED,wDAAwD;AACxD,wBAAgB,oBAAoB,IAAI,SAAS,QAAQ,EAAE,CAE1D;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAErE;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjE;AAED,6EAA6E;AAC7E,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE;AAED,YAAY,EACV,MAAM,IAAI,qBAAqB,EAC/B,QAAQ,IAAI,mBAAmB,GAChC,MAAM,qBAAqB,CAAC"}
@@ -62,6 +62,10 @@ async function resolveAdapter(provider, config, s3ProviderProfile) {
62
62
  throw mapFilesSdkError(error);
63
63
  }
64
64
  const { adapter } = resolved.files;
65
+ if (provider === 'azure') {
66
+ const { withAzureConditionalOperations } = await import('../azure/azure-conditional.js');
67
+ return withAzureConditionalOperations(adapter);
68
+ }
65
69
  if (provider === 'memory') {
66
70
  const { withMemoryConditionalOperations } = await import('../memory.js');
67
71
  return withMemoryConditionalOperations(adapter);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/files-sdk/provider/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Adapter } from 'files-sdk';\nimport { loadFiles, type LoadFilesOptions } from 'files-sdk/loader';\nimport {\n PROVIDER_NAMES,\n getProvider,\n getSecretEnvVars,\n listEnvVars,\n type EnvVar,\n type Provider,\n type ProviderSlug,\n} from 'files-sdk/providers';\n\nimport { StorageError, StorageErrorCode } from '../../storage.error.js';\nimport {\n createFilesSdkDriver,\n mapFilesSdkError,\n type FilesSdkDriverOptions,\n type FilesSdkStorageDriver,\n} from '../files-sdk.driver.js';\nimport type { S3Adapter, S3ProviderProfile } from '../s3/index.js';\nimport {\n recordS3ConditionalRequestPermission,\n recordS3ConstructionMetadata,\n} from '../s3/construction-metadata.js';\n\n/** Slug of a storage provider this package can build a driver for. */\nexport type StorageProviderName = ProviderSlug;\n\n/**\n * Flat, provider-specific settings — `bucket` and `region` for an object store,\n * `root` for the filesystem, `accountName` and `container` for Azure, and so\n * on. Every provider reads what it needs and ignores the rest, so one config\n * shape serves a deployment that switches providers by name.\n *\n * Credentials may be omitted for any provider whose SDK resolves its own chain\n * (an IAM role, Application Default Credentials, a shared profile).\n */\nexport type StorageProviderConfig = Omit<LoadFilesOptions, 'provider'>;\n\nexport interface ProviderStorageDriverOptions extends Omit<\n FilesSdkDriverOptions<Adapter>,\n 'adapter'\n> {\n /** Which provider to build. Validate untrusted input with {@link isStorageProvider}. */\n provider: StorageProviderName;\n config?: StorageProviderConfig;\n /** Verified operation profile for the `s3` slug, especially custom endpoints. */\n s3ProviderProfile?: S3ProviderProfile;\n}\n\n/**\n * Builds a {@link FilesSdkStorageDriver} for a provider named at runtime,\n * importing that provider's adapter — and only that one — on demand. A\n * deployment selects its store with a string (`'s3'`, `'gcs'`, `'azure'`,\n * `'r2'`, `'fs'`, …) and installs one native SDK, instead of the application\n * hard-coding a driver per backend.\n *\n * The `s3` slug additionally gets the exact verified provider profile and\n * signed-policy capabilities {@link createS3StorageDriver} attaches. Providers\n * not backed by the AWS S3 SDK expose what their adapter declares;\n * noncanonical S3-backed providers default to an unverified, read-only profile.\n * Import `@nestm/storage/files-sdk/s3` directly when the provider is known at\n * build time and the extra indirection buys nothing.\n *\n * @throws StorageError `INVALID_ARGUMENT` for an unknown slug, and whatever the\n * adapter reports (mapped) when required config or credentials are missing.\n */\nexport async function createProviderStorageDriver(\n options: ProviderStorageDriverOptions,\n): Promise<FilesSdkStorageDriver> {\n const { provider, config, s3ProviderProfile, ...filesOptions } = options;\n if (!isStorageProvider(provider)) {\n throw unknownProvider(provider);\n }\n\n // `loadFiles` owns the slug → adapter mapping and keeps the import lazy; the\n // client it returns is discarded so the caller's own driver options (prefix,\n // hooks, plugins, readonly, retries) apply to the instance the bridge builds.\n if (s3ProviderProfile !== undefined && provider !== 's3') {\n throw new StorageError(\n 's3ProviderProfile can only be used with the \"s3\" provider.',\n { code: StorageErrorCode.INVALID_ARGUMENT, permanent: true },\n );\n }\n const adapter = await resolveAdapter(provider, config, s3ProviderProfile);\n return createFilesSdkDriver({\n ...filesOptions,\n adapter,\n });\n}\n\nfunction isS3BackedAdapter(adapter: Adapter): adapter is S3Adapter {\n try {\n const raw = adapter.raw;\n if (typeof raw !== 'object' || raw === null) return false;\n const candidate = raw as {\n readonly config?: unknown;\n readonly send?: unknown;\n };\n return (\n typeof candidate.send === 'function' &&\n typeof candidate.config === 'object' &&\n candidate.config !== null &&\n (candidate.config as { readonly serviceId?: unknown }).serviceId === 'S3'\n );\n } catch {\n return false;\n }\n}\n\nasync function resolveAdapter(\n provider: StorageProviderName,\n config: StorageProviderConfig | undefined,\n s3ProviderProfile: S3ProviderProfile | undefined,\n): Promise<Adapter> {\n const loaderConfig = withVerifiedS3ConditionalOptIn(\n provider,\n config,\n s3ProviderProfile,\n );\n let resolved;\n try {\n resolved = await loadFiles({ ...loaderConfig, provider });\n } catch (error) {\n throw mapFilesSdkError(error);\n }\n const { adapter } = resolved.files;\n if (provider === 'memory') {\n const { withMemoryConditionalOperations } = await import('../memory.js');\n return withMemoryConditionalOperations(\n adapter as Parameters<typeof withMemoryConditionalOperations>[0],\n );\n }\n if (provider === 'fs') {\n const { withFsConditionalMutation } = await import('../fs/index.js');\n return withFsConditionalMutation(\n adapter as Parameters<typeof withFsConditionalMutation>[0],\n );\n }\n if (!isS3BackedAdapter(adapter)) {\n return adapter;\n }\n // Every AWS-SDK-backed wrapper gets fail-closed S3 provenance and endpoint\n // hardening. Only the canonical `s3` slug may infer native AWS or accept an\n // explicit verified profile; named compatibles remain unverified/read-only.\n const { withS3Capabilities } = await import('../s3/index.js');\n const configJsonPublicBaseUrl = config?.configJson?.publicBaseUrl;\n const publicBaseUrl =\n config?.publicBaseUrl ??\n (typeof configJsonPublicBaseUrl === 'string'\n ? configJsonPublicBaseUrl\n : undefined);\n const s3Adapter = adapter as Parameters<typeof withS3Capabilities>[0];\n if (provider === 's3') {\n recordS3ConditionalRequestPermission(\n s3Adapter.raw,\n config?.configJson?.conditional !== false,\n );\n }\n recordS3ConstructionMetadata(s3Adapter.raw, {\n publicBaseUrlConfigured:\n provider !== 's3' ||\n config?.publicBaseUrl !== undefined ||\n configJsonPublicBaseUrl !== undefined,\n });\n return withS3Capabilities(s3Adapter, {\n ...(typeof config?.endpoint === 'string' && {\n endpoint: config.endpoint,\n }),\n ...(publicBaseUrl !== undefined && {\n publicBaseUrl,\n }),\n ...(s3ProviderProfile !== undefined && {\n providerProfile: s3ProviderProfile,\n }),\n });\n}\n\nfunction withVerifiedS3ConditionalOptIn(\n provider: StorageProviderName,\n config: StorageProviderConfig | undefined,\n s3ProviderProfile: S3ProviderProfile | undefined,\n): StorageProviderConfig | undefined {\n const configJsonEndpoint = config?.configJson?.endpoint;\n const hasCustomEndpoint =\n config?.endpoint !== undefined || configJsonEndpoint !== undefined;\n if (\n provider !== 's3' ||\n s3ProviderProfile === undefined ||\n config === undefined ||\n !hasCustomEndpoint ||\n config.configJson?.conditional !== undefined\n ) {\n return config;\n }\n\n // files-sdk requires this construction-time opt-in before it exposes native\n // conditional primitives for a custom endpoint. The branded provider profile\n // still narrows the exposed operations in withS3Capabilities below.\n return {\n ...config,\n configJson: { ...config.configJson, conditional: true },\n };\n}\n\nfunction unknownProvider(provider: string): StorageError {\n return new StorageError(\n `Unknown storage provider: ${JSON.stringify(provider)}. Known providers: ${PROVIDER_NAMES.join(', ')}.`,\n { code: StorageErrorCode.INVALID_ARGUMENT, permanent: true },\n );\n}\n\n/** Narrows an untrusted string — an env var, a config file — to a known slug. */\nexport function isStorageProvider(value: string): value is StorageProviderName {\n return getProvider(value) !== undefined;\n}\n\n/** Every provider that can be named, sorted by slug. */\nexport function listStorageProviders(): readonly Provider[] {\n return PROVIDER_NAMES.flatMap((slug) => getProvider(slug) ?? []);\n}\n\n/** One provider's display name, description, native SDKs, and env contract. */\nexport function getStorageProvider(slug: string): Provider | undefined {\n return getProvider(slug);\n}\n\n/**\n * Every environment variable a provider reads, flattened across required, all\n * credential modes, and optional. Useful for validating a deployment's config\n * before the first upload rather than at it.\n */\nexport function listStorageProviderEnvVars(slug: string): EnvVar[] {\n return listEnvVars(slug);\n}\n\n/** The subset of {@link listStorageProviderEnvVars} that carries secrets. */\nexport function listStorageProviderSecretEnvVars(slug: string): EnvVar[] {\n return getSecretEnvVars(slug);\n}\n\nexport type {\n EnvVar as StorageProviderEnvVar,\n Provider as StorageProviderInfo,\n} from 'files-sdk/providers';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/files-sdk/provider/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Adapter } from 'files-sdk';\nimport { loadFiles, type LoadFilesOptions } from 'files-sdk/loader';\nimport {\n PROVIDER_NAMES,\n getProvider,\n getSecretEnvVars,\n listEnvVars,\n type EnvVar,\n type Provider,\n type ProviderSlug,\n} from 'files-sdk/providers';\n\nimport { StorageError, StorageErrorCode } from '../../storage.error.js';\nimport {\n createFilesSdkDriver,\n mapFilesSdkError,\n type FilesSdkDriverOptions,\n type FilesSdkStorageDriver,\n} from '../files-sdk.driver.js';\nimport type { S3Adapter, S3ProviderProfile } from '../s3/index.js';\nimport {\n recordS3ConditionalRequestPermission,\n recordS3ConstructionMetadata,\n} from '../s3/construction-metadata.js';\n\n/** Slug of a storage provider this package can build a driver for. */\nexport type StorageProviderName = ProviderSlug;\n\n/**\n * Flat, provider-specific settings — `bucket` and `region` for an object store,\n * `root` for the filesystem, `accountName` and `container` for Azure, and so\n * on. Every provider reads what it needs and ignores the rest, so one config\n * shape serves a deployment that switches providers by name.\n *\n * Credentials may be omitted for any provider whose SDK resolves its own chain\n * (an IAM role, Application Default Credentials, a shared profile).\n */\nexport type StorageProviderConfig = Omit<LoadFilesOptions, 'provider'>;\n\nexport interface ProviderStorageDriverOptions extends Omit<\n FilesSdkDriverOptions<Adapter>,\n 'adapter'\n> {\n /** Which provider to build. Validate untrusted input with {@link isStorageProvider}. */\n provider: StorageProviderName;\n config?: StorageProviderConfig;\n /** Verified operation profile for the `s3` slug, especially custom endpoints. */\n s3ProviderProfile?: S3ProviderProfile;\n}\n\n/**\n * Builds a {@link FilesSdkStorageDriver} for a provider named at runtime,\n * importing that provider's adapter — and only that one — on demand. A\n * deployment selects its store with a string (`'s3'`, `'gcs'`, `'azure'`,\n * `'r2'`, `'fs'`, …) and installs one native SDK, instead of the application\n * hard-coding a driver per backend.\n *\n * The `s3` slug additionally gets the exact verified provider profile and\n * signed-policy capabilities {@link createS3StorageDriver} attaches. Providers\n * not backed by the AWS S3 SDK expose what their adapter declares;\n * noncanonical S3-backed providers default to an unverified, read-only profile.\n * Import `@nestm/storage/files-sdk/s3` directly when the provider is known at\n * build time and the extra indirection buys nothing.\n *\n * @throws StorageError `INVALID_ARGUMENT` for an unknown slug, and whatever the\n * adapter reports (mapped) when required config or credentials are missing.\n */\nexport async function createProviderStorageDriver(\n options: ProviderStorageDriverOptions,\n): Promise<FilesSdkStorageDriver> {\n const { provider, config, s3ProviderProfile, ...filesOptions } = options;\n if (!isStorageProvider(provider)) {\n throw unknownProvider(provider);\n }\n\n // `loadFiles` owns the slug → adapter mapping and keeps the import lazy; the\n // client it returns is discarded so the caller's own driver options (prefix,\n // hooks, plugins, readonly, retries) apply to the instance the bridge builds.\n if (s3ProviderProfile !== undefined && provider !== 's3') {\n throw new StorageError(\n 's3ProviderProfile can only be used with the \"s3\" provider.',\n { code: StorageErrorCode.INVALID_ARGUMENT, permanent: true },\n );\n }\n const adapter = await resolveAdapter(provider, config, s3ProviderProfile);\n return createFilesSdkDriver({\n ...filesOptions,\n adapter,\n });\n}\n\nfunction isS3BackedAdapter(adapter: Adapter): adapter is S3Adapter {\n try {\n const raw = adapter.raw;\n if (typeof raw !== 'object' || raw === null) return false;\n const candidate = raw as {\n readonly config?: unknown;\n readonly send?: unknown;\n };\n return (\n typeof candidate.send === 'function' &&\n typeof candidate.config === 'object' &&\n candidate.config !== null &&\n (candidate.config as { readonly serviceId?: unknown }).serviceId === 'S3'\n );\n } catch {\n return false;\n }\n}\n\nasync function resolveAdapter(\n provider: StorageProviderName,\n config: StorageProviderConfig | undefined,\n s3ProviderProfile: S3ProviderProfile | undefined,\n): Promise<Adapter> {\n const loaderConfig = withVerifiedS3ConditionalOptIn(\n provider,\n config,\n s3ProviderProfile,\n );\n let resolved;\n try {\n resolved = await loadFiles({ ...loaderConfig, provider });\n } catch (error) {\n throw mapFilesSdkError(error);\n }\n const { adapter } = resolved.files;\n if (provider === 'azure') {\n const { withAzureConditionalOperations } =\n await import('../azure/azure-conditional.js');\n return withAzureConditionalOperations(\n adapter as Parameters<typeof withAzureConditionalOperations>[0],\n );\n }\n if (provider === 'memory') {\n const { withMemoryConditionalOperations } = await import('../memory.js');\n return withMemoryConditionalOperations(\n adapter as Parameters<typeof withMemoryConditionalOperations>[0],\n );\n }\n if (provider === 'fs') {\n const { withFsConditionalMutation } = await import('../fs/index.js');\n return withFsConditionalMutation(\n adapter as Parameters<typeof withFsConditionalMutation>[0],\n );\n }\n if (!isS3BackedAdapter(adapter)) {\n return adapter;\n }\n // Every AWS-SDK-backed wrapper gets fail-closed S3 provenance and endpoint\n // hardening. Only the canonical `s3` slug may infer native AWS or accept an\n // explicit verified profile; named compatibles remain unverified/read-only.\n const { withS3Capabilities } = await import('../s3/index.js');\n const configJsonPublicBaseUrl = config?.configJson?.publicBaseUrl;\n const publicBaseUrl =\n config?.publicBaseUrl ??\n (typeof configJsonPublicBaseUrl === 'string'\n ? configJsonPublicBaseUrl\n : undefined);\n const s3Adapter = adapter as Parameters<typeof withS3Capabilities>[0];\n if (provider === 's3') {\n recordS3ConditionalRequestPermission(\n s3Adapter.raw,\n config?.configJson?.conditional !== false,\n );\n }\n recordS3ConstructionMetadata(s3Adapter.raw, {\n publicBaseUrlConfigured:\n provider !== 's3' ||\n config?.publicBaseUrl !== undefined ||\n configJsonPublicBaseUrl !== undefined,\n });\n return withS3Capabilities(s3Adapter, {\n ...(typeof config?.endpoint === 'string' && {\n endpoint: config.endpoint,\n }),\n ...(publicBaseUrl !== undefined && {\n publicBaseUrl,\n }),\n ...(s3ProviderProfile !== undefined && {\n providerProfile: s3ProviderProfile,\n }),\n });\n}\n\nfunction withVerifiedS3ConditionalOptIn(\n provider: StorageProviderName,\n config: StorageProviderConfig | undefined,\n s3ProviderProfile: S3ProviderProfile | undefined,\n): StorageProviderConfig | undefined {\n const configJsonEndpoint = config?.configJson?.endpoint;\n const hasCustomEndpoint =\n config?.endpoint !== undefined || configJsonEndpoint !== undefined;\n if (\n provider !== 's3' ||\n s3ProviderProfile === undefined ||\n config === undefined ||\n !hasCustomEndpoint ||\n config.configJson?.conditional !== undefined\n ) {\n return config;\n }\n\n // files-sdk requires this construction-time opt-in before it exposes native\n // conditional primitives for a custom endpoint. The branded provider profile\n // still narrows the exposed operations in withS3Capabilities below.\n return {\n ...config,\n configJson: { ...config.configJson, conditional: true },\n };\n}\n\nfunction unknownProvider(provider: string): StorageError {\n return new StorageError(\n `Unknown storage provider: ${JSON.stringify(provider)}. Known providers: ${PROVIDER_NAMES.join(', ')}.`,\n { code: StorageErrorCode.INVALID_ARGUMENT, permanent: true },\n );\n}\n\n/** Narrows an untrusted string — an env var, a config file — to a known slug. */\nexport function isStorageProvider(value: string): value is StorageProviderName {\n return getProvider(value) !== undefined;\n}\n\n/** Every provider that can be named, sorted by slug. */\nexport function listStorageProviders(): readonly Provider[] {\n return PROVIDER_NAMES.flatMap((slug) => getProvider(slug) ?? []);\n}\n\n/** One provider's display name, description, native SDKs, and env contract. */\nexport function getStorageProvider(slug: string): Provider | undefined {\n return getProvider(slug);\n}\n\n/**\n * Every environment variable a provider reads, flattened across required, all\n * credential modes, and optional. Useful for validating a deployment's config\n * before the first upload rather than at it.\n */\nexport function listStorageProviderEnvVars(slug: string): EnvVar[] {\n return listEnvVars(slug);\n}\n\n/** The subset of {@link listStorageProviderEnvVars} that carries secrets. */\nexport function listStorageProviderSecretEnvVars(slug: string): EnvVar[] {\n return getSecretEnvVars(slug);\n}\n\nexport type {\n EnvVar as StorageProviderEnvVar,\n Provider as StorageProviderInfo,\n} from 'files-sdk/providers';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestm/storage",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
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,15 @@
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
+ },
67
+ "./files-sdk/azure": {
68
+ "types": "./dist/files-sdk/azure/index.d.ts",
69
+ "import": "./dist/files-sdk/azure/index.js"
70
+ }
63
71
  },
64
72
  "main": "./dist/index.js",
65
73
  "types": "./dist/index.d.ts",
@@ -98,8 +106,11 @@
98
106
  "@aws-sdk/lib-storage": "^3.700.0",
99
107
  "@aws-sdk/s3-presigned-post": "^3.700.0",
100
108
  "@aws-sdk/s3-request-presigner": "^3.700.0",
109
+ "@azure/core-auth": "^1.11.0",
110
+ "@azure/storage-blob": "^12.33.0",
101
111
  "@nestjs/common": "^12.0.0",
102
112
  "@nestjs/core": "^12.0.0",
113
+ "@nestm/crypto": "0.1.0-alpha.9",
103
114
  "ai": ">=7.0.0 <8.0.0",
104
115
  "reflect-metadata": "^0.2.2",
105
116
  "rxjs": "^7.8.1",
@@ -135,6 +146,15 @@
135
146
  },
136
147
  "zod": {
137
148
  "optional": true
149
+ },
150
+ "@nestm/crypto": {
151
+ "optional": true
152
+ },
153
+ "@azure/storage-blob": {
154
+ "optional": true
155
+ },
156
+ "@azure/core-auth": {
157
+ "optional": true
138
158
  }
139
159
  },
140
160
  "devDependencies": {
@@ -142,12 +162,16 @@
142
162
  "@aws-sdk/lib-storage": "3.1120.0",
143
163
  "@aws-sdk/s3-presigned-post": "3.1120.0",
144
164
  "@aws-sdk/s3-request-presigner": "3.1120.0",
165
+ "@azure/core-auth": "1.11.0",
166
+ "@azure/identity": "4.13.2",
167
+ "@azure/storage-blob": "12.33.0",
145
168
  "@changesets/cli": "3.0.1",
146
169
  "@nestjs/common": "12.0.1",
147
170
  "@nestjs/core": "12.0.1",
148
171
  "@nestjs/platform-express": "12.0.1",
149
172
  "@nestjs/platform-fastify": "12.0.1",
150
173
  "@nestjs/testing": "12.0.1",
174
+ "@nestm/crypto": "0.1.0-alpha.9",
151
175
  "@types/node": "26.4.0",
152
176
  "@types/supertest": "7.2.1",
153
177
  "@vitest/coverage-v8": "4.1.11",