@nestm/storage 0.1.0-alpha.2 → 0.1.0-alpha.20

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.
Files changed (198) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/README.md +975 -29
  3. package/SECURITY.md +128 -0
  4. package/dist/ai-sdk/ai-sdk-file-workflow-tools.d.ts +53 -0
  5. package/dist/ai-sdk/ai-sdk-file-workflow-tools.d.ts.map +1 -0
  6. package/dist/ai-sdk/ai-sdk-file-workflow-tools.js +370 -0
  7. package/dist/ai-sdk/ai-sdk-file-workflow-tools.js.map +1 -0
  8. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +91 -0
  9. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
  10. package/dist/ai-sdk/ai-sdk-workspace-tools.js +497 -0
  11. package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
  12. package/dist/ai-sdk/index.d.ts +3 -0
  13. package/dist/ai-sdk/index.d.ts.map +1 -0
  14. package/dist/ai-sdk/index.js +3 -0
  15. package/dist/ai-sdk/index.js.map +1 -0
  16. package/dist/bytes/index.d.ts +30 -0
  17. package/dist/bytes/index.d.ts.map +1 -0
  18. package/dist/bytes/index.js +96 -0
  19. package/dist/bytes/index.js.map +1 -0
  20. package/dist/core/index.d.ts +5 -0
  21. package/dist/core/index.d.ts.map +1 -1
  22. package/dist/core/index.js +5 -0
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/core/storage-staged-content.d.ts +39 -0
  25. package/dist/core/storage-staged-content.d.ts.map +1 -0
  26. package/dist/core/storage-staged-content.js +109 -0
  27. package/dist/core/storage-staged-content.js.map +1 -0
  28. package/dist/core/storage-streams.d.ts +19 -0
  29. package/dist/core/storage-streams.d.ts.map +1 -0
  30. package/dist/core/storage-streams.js +95 -0
  31. package/dist/core/storage-streams.js.map +1 -0
  32. package/dist/core/storage-text-edit.d.ts +38 -0
  33. package/dist/core/storage-text-edit.d.ts.map +1 -0
  34. package/dist/core/storage-text-edit.js +86 -0
  35. package/dist/core/storage-text-edit.js.map +1 -0
  36. package/dist/core/storage-text-stream-edit.d.ts +8 -0
  37. package/dist/core/storage-text-stream-edit.d.ts.map +1 -0
  38. package/dist/core/storage-text-stream-edit.js +114 -0
  39. package/dist/core/storage-text-stream-edit.js.map +1 -0
  40. package/dist/core/storage-text.d.ts +22 -0
  41. package/dist/core/storage-text.d.ts.map +1 -0
  42. package/dist/core/storage-text.js +69 -0
  43. package/dist/core/storage-text.js.map +1 -0
  44. package/dist/crypto/index.d.ts +2 -0
  45. package/dist/crypto/index.d.ts.map +1 -0
  46. package/dist/crypto/index.js +2 -0
  47. package/dist/crypto/index.js.map +1 -0
  48. package/dist/crypto/storage-encrypted-content.d.ts +49 -0
  49. package/dist/crypto/storage-encrypted-content.d.ts.map +1 -0
  50. package/dist/crypto/storage-encrypted-content.js +212 -0
  51. package/dist/crypto/storage-encrypted-content.js.map +1 -0
  52. package/dist/files-sdk/azure/azure-conditional.d.ts +5 -0
  53. package/dist/files-sdk/azure/azure-conditional.d.ts.map +1 -0
  54. package/dist/files-sdk/azure/azure-conditional.js +184 -0
  55. package/dist/files-sdk/azure/azure-conditional.js.map +1 -0
  56. package/dist/files-sdk/azure/index.d.ts +10 -0
  57. package/dist/files-sdk/azure/index.d.ts.map +1 -0
  58. package/dist/files-sdk/azure/index.js +34 -0
  59. package/dist/files-sdk/azure/index.js.map +1 -0
  60. package/dist/files-sdk/files-sdk.driver.d.ts +85 -3
  61. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  62. package/dist/files-sdk/files-sdk.driver.js +1437 -37
  63. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  64. package/dist/files-sdk/fs/index.d.ts +42 -0
  65. package/dist/files-sdk/fs/index.d.ts.map +1 -0
  66. package/dist/files-sdk/fs/index.js +1117 -0
  67. package/dist/files-sdk/fs/index.js.map +1 -0
  68. package/dist/files-sdk/index.d.ts +1 -1
  69. package/dist/files-sdk/index.d.ts.map +1 -1
  70. package/dist/files-sdk/index.js.map +1 -1
  71. package/dist/files-sdk/memory.d.ts +7 -0
  72. package/dist/files-sdk/memory.d.ts.map +1 -0
  73. package/dist/files-sdk/memory.js +72 -0
  74. package/dist/files-sdk/memory.js.map +1 -0
  75. package/dist/files-sdk/provider/index.d.ts +58 -0
  76. package/dist/files-sdk/provider/index.d.ts.map +1 -0
  77. package/dist/files-sdk/provider/index.js +169 -0
  78. package/dist/files-sdk/provider/index.js.map +1 -0
  79. package/dist/files-sdk/s3/construction-metadata.d.ts +9 -0
  80. package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
  81. package/dist/files-sdk/s3/construction-metadata.js +24 -0
  82. package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
  83. package/dist/files-sdk/s3/index.d.ts +54 -0
  84. package/dist/files-sdk/s3/index.d.ts.map +1 -0
  85. package/dist/files-sdk/s3/index.js +1505 -0
  86. package/dist/files-sdk/s3/index.js.map +1 -0
  87. package/dist/gateway/index.d.ts +1 -1
  88. package/dist/gateway/index.d.ts.map +1 -1
  89. package/dist/gateway/index.js.map +1 -1
  90. package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -1
  91. package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -1
  92. package/dist/gateway/storage-gateway.controller.d.ts +12 -11
  93. package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
  94. package/dist/gateway/storage-gateway.controller.js +240 -70
  95. package/dist/gateway/storage-gateway.controller.js.map +1 -1
  96. package/dist/gateway/storage-gateway.guard.d.ts.map +1 -1
  97. package/dist/gateway/storage-gateway.guard.js.map +1 -1
  98. package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
  99. package/dist/gateway/storage-gateway.module.js +60 -1
  100. package/dist/gateway/storage-gateway.module.js.map +1 -1
  101. package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
  102. package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
  103. package/dist/gateway/storage-gateway.tokens.js +1 -0
  104. package/dist/gateway/storage-gateway.tokens.js.map +1 -1
  105. package/dist/gateway/storage-gateway.types.d.ts +53 -10
  106. package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
  107. package/dist/gateway/storage-gateway.types.js.map +1 -1
  108. package/dist/inject-storage.decorator.js.map +1 -1
  109. package/dist/storage-etag.d.ts +13 -0
  110. package/dist/storage-etag.d.ts.map +1 -0
  111. package/dist/storage-etag.js +31 -0
  112. package/dist/storage-etag.js.map +1 -0
  113. package/dist/storage-upload-control.d.ts.map +1 -1
  114. package/dist/storage.client.d.ts +14 -1
  115. package/dist/storage.client.d.ts.map +1 -1
  116. package/dist/storage.client.js +204 -2
  117. package/dist/storage.client.js.map +1 -1
  118. package/dist/storage.driver.d.ts +28 -1
  119. package/dist/storage.driver.d.ts.map +1 -1
  120. package/dist/storage.driver.js.map +1 -1
  121. package/dist/storage.error.d.ts +17 -10
  122. package/dist/storage.error.d.ts.map +1 -1
  123. package/dist/storage.error.js +47 -1
  124. package/dist/storage.error.js.map +1 -1
  125. package/dist/storage.module.d.ts.map +1 -1
  126. package/dist/storage.module.js.map +1 -1
  127. package/dist/storage.service.d.ts.map +1 -1
  128. package/dist/storage.service.js +28 -3
  129. package/dist/storage.service.js.map +1 -1
  130. package/dist/storage.tokens.js.map +1 -1
  131. package/dist/storage.types.d.ts +150 -1
  132. package/dist/storage.types.d.ts.map +1 -1
  133. package/dist/storage.types.js.map +1 -1
  134. package/dist/testing/index.d.ts +1 -0
  135. package/dist/testing/index.d.ts.map +1 -1
  136. package/dist/testing/index.js +3 -1
  137. package/dist/testing/index.js.map +1 -1
  138. package/dist/testing/provider-conformance.d.ts +65 -0
  139. package/dist/testing/provider-conformance.d.ts.map +1 -0
  140. package/dist/testing/provider-conformance.js +869 -0
  141. package/dist/testing/provider-conformance.js.map +1 -0
  142. package/dist/workspace/index.d.ts +12 -0
  143. package/dist/workspace/index.d.ts.map +1 -0
  144. package/dist/workspace/index.js +10 -0
  145. package/dist/workspace/index.js.map +1 -0
  146. package/dist/workspace/storage-catalog-checkout.d.ts +7 -0
  147. package/dist/workspace/storage-catalog-checkout.d.ts.map +1 -0
  148. package/dist/workspace/storage-catalog-checkout.js +13 -0
  149. package/dist/workspace/storage-catalog-checkout.js.map +1 -0
  150. package/dist/workspace/storage-draft-stream.d.ts +3 -0
  151. package/dist/workspace/storage-draft-stream.d.ts.map +1 -0
  152. package/dist/workspace/storage-draft-stream.js +57 -0
  153. package/dist/workspace/storage-draft-stream.js.map +1 -0
  154. package/dist/workspace/storage-file-catalog.types.d.ts +73 -0
  155. package/dist/workspace/storage-file-catalog.types.d.ts.map +1 -0
  156. package/dist/workspace/storage-file-catalog.types.js +2 -0
  157. package/dist/workspace/storage-file-catalog.types.js.map +1 -0
  158. package/dist/workspace/storage-file-workflow.d.ts +7 -0
  159. package/dist/workspace/storage-file-workflow.d.ts.map +1 -0
  160. package/dist/workspace/storage-file-workflow.js +657 -0
  161. package/dist/workspace/storage-file-workflow.js.map +1 -0
  162. package/dist/workspace/storage-file-workflow.protection.d.ts +31 -0
  163. package/dist/workspace/storage-file-workflow.protection.d.ts.map +1 -0
  164. package/dist/workspace/storage-file-workflow.protection.js +339 -0
  165. package/dist/workspace/storage-file-workflow.protection.js.map +1 -0
  166. package/dist/workspace/storage-file-workflow.types.d.ts +184 -0
  167. package/dist/workspace/storage-file-workflow.types.d.ts.map +1 -0
  168. package/dist/workspace/storage-file-workflow.types.js +10 -0
  169. package/dist/workspace/storage-file-workflow.types.js.map +1 -0
  170. package/dist/workspace/storage-text-draft.d.ts +21 -0
  171. package/dist/workspace/storage-text-draft.d.ts.map +1 -0
  172. package/dist/workspace/storage-text-draft.js +77 -0
  173. package/dist/workspace/storage-text-draft.js.map +1 -0
  174. package/dist/workspace/storage-working-files.d.ts +34 -0
  175. package/dist/workspace/storage-working-files.d.ts.map +1 -0
  176. package/dist/workspace/storage-working-files.js +326 -0
  177. package/dist/workspace/storage-working-files.js.map +1 -0
  178. package/dist/workspace/storage-workspace.cursor.d.ts +71 -0
  179. package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
  180. package/dist/workspace/storage-workspace.cursor.js +408 -0
  181. package/dist/workspace/storage-workspace.cursor.js.map +1 -0
  182. package/dist/workspace/storage-workspace.d.ts +6 -0
  183. package/dist/workspace/storage-workspace.d.ts.map +1 -0
  184. package/dist/workspace/storage-workspace.error.d.ts +32 -0
  185. package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
  186. package/dist/workspace/storage-workspace.error.js +75 -0
  187. package/dist/workspace/storage-workspace.error.js.map +1 -0
  188. package/dist/workspace/storage-workspace.js +849 -0
  189. package/dist/workspace/storage-workspace.js.map +1 -0
  190. package/dist/workspace/storage-workspace.path.d.ts +9 -0
  191. package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
  192. package/dist/workspace/storage-workspace.path.js +68 -0
  193. package/dist/workspace/storage-workspace.path.js.map +1 -0
  194. package/dist/workspace/storage-workspace.types.d.ts +123 -0
  195. package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
  196. package/dist/workspace/storage-workspace.types.js +22 -0
  197. package/dist/workspace/storage-workspace.types.js.map +1 -0
  198. package/package.json +99 -20
@@ -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"]}