@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,657 @@
1
+ import { searchStorageText } from '../core/storage-text.js';
2
+ import { editStorageTextStream } from '../core/storage-text-stream-edit.js';
3
+ import { stageStorageTextDraft } from './storage-text-draft.js';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { StorageError } from '../storage.error.js';
6
+ import { assertWorkspacePath } from './storage-workspace.path.js';
7
+ import { collectStorageBytes, readStorageTextWindow, storageBytesStream, storageInteger, } from '../core/storage-streams.js';
8
+ import { DEFAULT_STORAGE_FILE_WORKFLOW_LIMITS, } from './storage-file-workflow.types.js';
9
+ export class StorageFileWorkflow {
10
+ #options;
11
+ constructor(options) {
12
+ this.#options = options;
13
+ }
14
+ mount(scope, options = {}) {
15
+ const limits = Object.freeze({
16
+ ...DEFAULT_STORAGE_FILE_WORKFLOW_LIMITS,
17
+ ...options.limits,
18
+ });
19
+ for (const [name, value] of Object.entries(limits))
20
+ storageInteger(value, name, name === 'maxReadBytes' ? 4 : 1);
21
+ const permissions = new Set(options.permissions ?? ['read', 'write', 'commit']);
22
+ const mutations = new Set(options.mutations ?? ['create', 'replace']);
23
+ const boundSignal = options.signal;
24
+ const requireMutation = (expectedEtag) => {
25
+ if (!mutations.has(expectedEtag == null ? 'create' : 'replace'))
26
+ throw new StorageError('Draft mutation is not permitted by this capability.', { code: 'UNAUTHORIZED' });
27
+ };
28
+ const operationPermissions = new WeakMap();
29
+ const allows = (permission) => permissions.has(permission) &&
30
+ (permission === 'read' || mutations.size > 0);
31
+ const operation = (permission, input) => {
32
+ if (!allows(permission))
33
+ throw new StorageError('File workflow operation is not permitted.', {
34
+ code: 'UNAUTHORIZED',
35
+ });
36
+ const signals = [boundSignal, input.signal].filter((signal) => signal !== undefined);
37
+ const signal = AbortSignal.any(signals);
38
+ signal.throwIfAborted();
39
+ operationPermissions.set(signal, permission);
40
+ return signal;
41
+ };
42
+ const transaction = (signal, work) => this.#transaction(scope, signal, operationPermissions.get(signal), work);
43
+ const content = this.#options.content;
44
+ const workflow = {
45
+ kind: 'storage-file-workflow',
46
+ version: 1,
47
+ limits,
48
+ allows,
49
+ restrict: (restriction) => {
50
+ const narrowed = { ...limits };
51
+ for (const name of Object.keys(limits)) {
52
+ const requested = restriction.limits?.[name] ?? limits[name];
53
+ storageInteger(requested, name, name === 'maxReadBytes' ? 4 : 1);
54
+ narrowed[name] = Math.min(limits[name], requested);
55
+ }
56
+ return this.mount(scope, {
57
+ limits: narrowed,
58
+ permissions: [...(restriction.permissions ?? permissions)].filter((permission) => permissions.has(permission)),
59
+ mutations: [...(restriction.mutations ?? mutations)].filter((mutation) => mutations.has(mutation)),
60
+ signal: AbortSignal.any([boundSignal, restriction.signal].filter((signal) => signal !== undefined)),
61
+ });
62
+ },
63
+ lookup: async (input) => {
64
+ const signal = operation('read', input);
65
+ validateIdentity(input.idempotencyKey);
66
+ return transaction(signal, async (tx) => {
67
+ const draft = await tx.findDraftByKey(input.idempotencyKey);
68
+ return draft === null ? null : summary(draft);
69
+ });
70
+ },
71
+ begin: async (input) => {
72
+ const signal = operation('write', input);
73
+ requireMutation(input.expectedEtag);
74
+ assertWorkspacePath(input.path, limits.maxPathBytes, {
75
+ allowRoot: false,
76
+ });
77
+ validateIdentity(input.idempotencyKey);
78
+ if (typeof input.text !== 'boolean' ||
79
+ (input.expectedEtag !== undefined &&
80
+ (typeof input.expectedEtag !== 'string' ||
81
+ input.expectedEtag.length === 0 ||
82
+ input.expectedEtag.length > 1024)))
83
+ invalid('Invalid draft input.');
84
+ const fingerprint = digest(new TextEncoder().encode(JSON.stringify([
85
+ input.path,
86
+ input.expectedEtag ?? null,
87
+ input.text,
88
+ ])));
89
+ return transaction(signal, async (tx) => {
90
+ const prior = await tx.findDraftByKey(input.idempotencyKey);
91
+ if (prior !== null) {
92
+ if (prior.requestFingerprint !== fingerprint)
93
+ conflict('Draft key was already used for different input.');
94
+ return summary(prior);
95
+ }
96
+ const record = {
97
+ id: randomUUID(),
98
+ path: input.path,
99
+ expectedEtag: input.expectedEtag ?? null,
100
+ sourceDraftId: null,
101
+ text: input.text,
102
+ status: 'open',
103
+ size: 0,
104
+ result: null,
105
+ createdAt: new Date().toISOString(),
106
+ idempotencyKey: input.idempotencyKey,
107
+ requestFingerprint: fingerprint,
108
+ body: null,
109
+ };
110
+ await tx.saveDraft(record);
111
+ return summary(record);
112
+ });
113
+ },
114
+ stageText: async (request) => {
115
+ const input = { ...request };
116
+ const signal = operation('write', input);
117
+ requireMutation(input.expectedEtag);
118
+ validateIdentity(input.idempotencyKey);
119
+ assertWorkspacePath(input.path, limits.maxPathBytes, {
120
+ allowRoot: false,
121
+ });
122
+ if (input.expectedEtag !== undefined &&
123
+ (input.expectedEtag.length < 1 || input.expectedEtag.length > 1024))
124
+ invalid('Invalid ETag.');
125
+ if (new TextEncoder().encode(input.content).byteLength >
126
+ limits.maxTextBytes)
127
+ throw new StorageError('Text checkpoint exceeds the buffered text limit.', { code: 'LIMIT_EXCEEDED' });
128
+ return summary(await stageStorageTextDraft({
129
+ scope,
130
+ signal,
131
+ limits,
132
+ content,
133
+ requireMutation,
134
+ transaction: (work) => transaction(signal, work),
135
+ idempotencyKey: input.idempotencyKey,
136
+ fingerprint: digest(new TextEncoder().encode(JSON.stringify([
137
+ 'stage-text',
138
+ input.path,
139
+ input.expectedEtag ?? null,
140
+ input.content,
141
+ ]))),
142
+ load: async () => ({
143
+ content: input.content,
144
+ path: input.path,
145
+ expectedEtag: input.expectedEtag ?? null,
146
+ source: null,
147
+ }),
148
+ }));
149
+ },
150
+ stageStream: async (request) => {
151
+ const input = { ...request };
152
+ if (input.sourceDraftId !== undefined)
153
+ operation('read', input);
154
+ const signal = operation('write', input);
155
+ requireMutation(input.expectedEtag);
156
+ validateIdentity(input.idempotencyKey);
157
+ assertWorkspacePath(input.path, limits.maxPathBytes, {
158
+ allowRoot: false,
159
+ });
160
+ if (typeof input.text !== 'boolean' ||
161
+ typeof input.contentIdentity !== 'string' ||
162
+ !input.contentIdentity.length ||
163
+ input.contentIdentity.length > 1024)
164
+ invalid('Invalid streaming draft identity.');
165
+ if (input.expectedEtag !== undefined &&
166
+ (!input.expectedEtag.length || input.expectedEtag.length > 1024))
167
+ invalid('Invalid ETag.');
168
+ return summary(await stageStorageTextDraft({
169
+ scope,
170
+ signal,
171
+ limits,
172
+ content,
173
+ requireMutation,
174
+ transaction: (work) => transaction(signal, work),
175
+ idempotencyKey: input.idempotencyKey,
176
+ fingerprint: digest(new TextEncoder().encode(JSON.stringify([
177
+ 'stage-stream',
178
+ input.path,
179
+ input.expectedEtag ?? null,
180
+ input.text,
181
+ input.contentIdentity,
182
+ input.sourceDraftId ?? null,
183
+ input.expectedSize ?? null,
184
+ ]))),
185
+ load: async () => {
186
+ const source = input.sourceDraftId === undefined
187
+ ? null
188
+ : await transaction(signal, (tx) => requireDraft(tx, input.sourceDraftId));
189
+ if (source !== null &&
190
+ (source.path !== input.path ||
191
+ source.expectedEtag !== (input.expectedEtag ?? null) ||
192
+ source.size !== input.expectedSize ||
193
+ !['open', 'sealed'].includes(source.status)))
194
+ conflict('The source checkpoint changed.');
195
+ return {
196
+ path: input.path,
197
+ expectedEtag: input.expectedEtag ?? null,
198
+ source,
199
+ text: input.text,
200
+ content: await input.body(),
201
+ };
202
+ },
203
+ }));
204
+ },
205
+ reviseText: async (request) => {
206
+ const input = {
207
+ ...request,
208
+ changes: request.changes.map((change) => ({ ...change })),
209
+ };
210
+ operation('read', input);
211
+ const signal = operation('write', input);
212
+ validateIdentity(input.idempotencyKey);
213
+ validateIdentity(input.draftId);
214
+ if (input.requestIdentity !== undefined &&
215
+ input.requestIdentity.length > 4096)
216
+ invalid('Invalid edit request identity.');
217
+ storageInteger(input.expectedSize, 'expectedSize');
218
+ if (input.changes.length < 1 || input.changes.length > limits.maxEdits)
219
+ throw new StorageError('Text editing exceeds the configured limits.', { code: 'LIMIT_EXCEEDED' });
220
+ return summary(await stageStorageTextDraft({
221
+ scope,
222
+ signal,
223
+ limits,
224
+ content,
225
+ requireMutation,
226
+ transaction: (work) => transaction(signal, work),
227
+ idempotencyKey: input.idempotencyKey,
228
+ fingerprint: digest(new TextEncoder().encode(JSON.stringify([
229
+ 'revise-text',
230
+ input.draftId,
231
+ input.expectedSize,
232
+ input.changes,
233
+ ...(input.requestIdentity === undefined
234
+ ? []
235
+ : [input.requestIdentity]),
236
+ ]))),
237
+ load: async () => {
238
+ const source = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
239
+ requireMutation(source.expectedEtag);
240
+ if (!source.text ||
241
+ !['open', 'sealed'].includes(source.status) ||
242
+ source.size !== input.expectedSize)
243
+ conflict('The source must be an unfinished text draft at the expected size.');
244
+ return {
245
+ source,
246
+ path: source.path,
247
+ expectedEtag: source.expectedEtag,
248
+ content: editStorageTextStream(this.#stream(scope, source, limits, signal), input.changes, {
249
+ maxEditBytes: limits.maxTextBytes,
250
+ maxEdits: limits.maxEdits,
251
+ signal,
252
+ }),
253
+ };
254
+ },
255
+ }));
256
+ },
257
+ list: async (input = {}) => {
258
+ const signal = operation('read', input);
259
+ const offset = input.offset ?? 0;
260
+ storageInteger(offset, 'offset');
261
+ return transaction(signal, async (tx) => {
262
+ const records = await tx.listDrafts(offset, limits.maxPageSize + 1);
263
+ if (records.length > limits.maxPageSize + 1)
264
+ provider('Persistence returned an oversized page.');
265
+ return {
266
+ items: records.slice(0, limits.maxPageSize).map(summary),
267
+ nextOffset: records.length > limits.maxPageSize
268
+ ? offset + limits.maxPageSize
269
+ : null,
270
+ };
271
+ });
272
+ },
273
+ readStream: async (input) => {
274
+ const signal = operation('read', input);
275
+ const draft = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
276
+ if (draft.status === 'cancelled' || draft.size !== input.expectedSize)
277
+ conflict('The source checkpoint changed.');
278
+ const start = input.start ?? 0;
279
+ const end = input.end ?? draft.size;
280
+ storageInteger(start, 'start');
281
+ storageInteger(end, 'end');
282
+ if (start > end || end > draft.size)
283
+ invalid('Invalid stream range.');
284
+ return {
285
+ ...summary(draft),
286
+ body: this.#stream(scope, draft, limits, signal, start, end),
287
+ };
288
+ },
289
+ read: async (input) => {
290
+ const signal = operation('read', input);
291
+ const draft = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
292
+ if (draft.status === 'cancelled')
293
+ conflict('Draft is cancelled.');
294
+ const offset = input.offset ?? 0;
295
+ storageInteger(offset, 'offset');
296
+ if (offset > draft.size)
297
+ invalid('Offset exceeds draft size.');
298
+ const window = draft.text
299
+ ? await readStorageTextWindow(async (range) => this.#stream(scope, draft, limits, signal, range.start, range.end + 1), {
300
+ size: draft.size,
301
+ offset,
302
+ maxBytes: limits.maxReadBytes,
303
+ signal,
304
+ })
305
+ : { content: null, offset, nextOffset: null };
306
+ return { ...summary(draft), ...window };
307
+ },
308
+ searchText: async (input) => {
309
+ const signal = operation('read', input);
310
+ const draft = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
311
+ if (!draft.text ||
312
+ draft.status === 'cancelled' ||
313
+ draft.size !== input.expectedSize)
314
+ conflict('The source checkpoint changed.');
315
+ const result = await searchStorageText(async (range) => this.#stream(scope, draft, limits, signal, range.start, range.end + 1), {
316
+ size: draft.size,
317
+ query: input.query,
318
+ offset: input.offset,
319
+ maxScanBytes: 262144,
320
+ maxMatches: 12,
321
+ maxSnippetCharacters: 320,
322
+ maxReadBytes: limits.maxReadBytes,
323
+ signal,
324
+ });
325
+ return { ...result, path: draft.path, etag: draft.id };
326
+ },
327
+ readText: async (request) => {
328
+ const input = { ...request };
329
+ const signal = operation('read', input);
330
+ storageInteger(input.expectedSize, 'expectedSize');
331
+ if (input.expectedSize > limits.maxTextBytes)
332
+ throw new StorageError('Text read exceeds the buffered text limit.', {
333
+ code: 'LIMIT_EXCEEDED',
334
+ });
335
+ const draft = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
336
+ if (!draft.text ||
337
+ draft.status === 'cancelled' ||
338
+ draft.size !== input.expectedSize)
339
+ conflict('The source must be an available text draft at the expected size.');
340
+ const bytes = await collectStorageBytes(this.#stream(scope, draft, limits, signal), limits.maxTextBytes, signal);
341
+ const current = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
342
+ if (current.status === 'cancelled' ||
343
+ current.size !== input.expectedSize)
344
+ conflict('The source draft changed while reading.');
345
+ return {
346
+ ...summary(current),
347
+ content: new TextDecoder('utf-8', {
348
+ fatal: true,
349
+ ignoreBOM: true,
350
+ }).decode(bytes),
351
+ };
352
+ },
353
+ append: async (input) => {
354
+ const signal = operation('write', input);
355
+ storageInteger(input.offset, 'offset');
356
+ if (!(input.bytes instanceof Uint8Array) ||
357
+ input.bytes.byteLength < 1 ||
358
+ input.bytes.byteLength > limits.maxChunkBytes)
359
+ throw new StorageError('Append exceeds chunk limits.', {
360
+ code: 'LIMIT_EXCEEDED',
361
+ });
362
+ // Snapshot before any await: callers may reuse or mutate their upload buffer.
363
+ const bytes = input.bytes.slice();
364
+ const sha256 = digest(bytes);
365
+ const prior = await transaction(signal, async (tx) => {
366
+ const draft = await requireDraft(tx, input.draftId);
367
+ requireMutation(draft.expectedEtag);
368
+ return appendState(tx, draft, input.offset, bytes.byteLength, sha256);
369
+ });
370
+ if (prior.replay)
371
+ return summary(prior.draft);
372
+ if (prior.draft.text) {
373
+ try {
374
+ new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
375
+ }
376
+ catch {
377
+ invalid('Text chunks must contain complete UTF-8 characters.');
378
+ }
379
+ }
380
+ const body = await content.write(scope, storageBytesStream(bytes), {
381
+ maxBytes: limits.maxChunkBytes,
382
+ signal,
383
+ });
384
+ if (body.size !== bytes.byteLength || body.sha256 !== sha256)
385
+ provider('Staged chunk receipt does not match input.');
386
+ return transaction(signal, async (tx) => {
387
+ const current = await appendState(tx, await requireDraft(tx, input.draftId), input.offset, bytes.byteLength, sha256);
388
+ requireMutation(current.draft.expectedEtag);
389
+ if (current.replay)
390
+ return summary(current.draft);
391
+ const size = current.draft.size + bytes.byteLength;
392
+ storageInteger(size, 'size');
393
+ await tx.putPart(input.draftId, {
394
+ offset: input.offset,
395
+ size: body.size,
396
+ sha256,
397
+ body,
398
+ });
399
+ const draft = { ...current.draft, size };
400
+ await tx.saveDraft(draft);
401
+ return summary(draft);
402
+ });
403
+ },
404
+ parts: async (input) => {
405
+ const signal = operation('read', input);
406
+ const offset = input.offset ?? 0;
407
+ storageInteger(offset, 'offset');
408
+ return transaction(signal, async (tx) => {
409
+ const draft = await requireDraft(tx, input.draftId);
410
+ if (draft.status === 'cancelled')
411
+ conflict('Draft is cancelled.');
412
+ if (offset > draft.size)
413
+ invalid('Offset exceeds draft size.');
414
+ const parts = await tx.listParts(draft.id, offset, limits.maxPageSize);
415
+ if (parts.length > limits.maxPageSize)
416
+ provider('Persistence returned an oversized page.');
417
+ const items = parts
418
+ .filter((part) => part.offset + part.size > offset)
419
+ .map(({ offset, size, sha256 }) => ({ offset, size, sha256 }));
420
+ const last = items.at(-1);
421
+ const next = last === undefined ? draft.size : last.offset + last.size;
422
+ if (offset < draft.size && (last === undefined || next <= offset))
423
+ provider('Draft contains missing chunks.');
424
+ return { items, nextOffset: next < draft.size ? next : null };
425
+ });
426
+ },
427
+ cancel: async (input) => {
428
+ const signal = operation('write', input);
429
+ return transaction(signal, async (tx) => {
430
+ const draft = await requireDraft(tx, input.draftId);
431
+ requireMutation(draft.expectedEtag);
432
+ if (draft.status === 'committed')
433
+ conflict('Committed draft cannot be cancelled.');
434
+ const cancelled = { ...draft, status: 'cancelled' };
435
+ await tx.saveDraft(cancelled);
436
+ return summary(cancelled);
437
+ });
438
+ },
439
+ commit: async (input) => {
440
+ const signal = operation('commit', input);
441
+ if (input.drafts.length < 1 ||
442
+ input.drafts.length > limits.maxCommitFiles ||
443
+ new Set(input.drafts.map((draft) => draft.draftId)).size !==
444
+ input.drafts.length)
445
+ invalid('Commit requires distinct draft IDs within the batch limit.');
446
+ const requests = input.drafts.map((draft) => ({ ...draft }));
447
+ for (const request of requests) {
448
+ storageInteger(request.size, 'size');
449
+ if (request.sha256 !== undefined &&
450
+ !/^[0-9a-f]{64}$/u.test(request.sha256))
451
+ invalid('Invalid SHA-256.');
452
+ }
453
+ const sealed = await transaction(signal, async (tx) => {
454
+ const drafts = [];
455
+ for (const request of requests) {
456
+ const draft = await requireDraft(tx, request.draftId);
457
+ requireMutation(draft.expectedEtag);
458
+ if (draft.size !== request.size || draft.status === 'cancelled')
459
+ conflict('Draft is cancelled or has a different byte count.');
460
+ if (draft.status === 'committed' &&
461
+ (draft.result === null ||
462
+ draft.body === null ||
463
+ (request.sha256 !== undefined &&
464
+ request.sha256 !== draft.body.sha256)))
465
+ conflict('Committed draft does not match the requested digest.');
466
+ drafts.push(draft);
467
+ }
468
+ if (new Set(drafts.map((draft) => draft.path)).size !== drafts.length)
469
+ conflict('A path may appear only once per commit.');
470
+ if (drafts.every((draft) => draft.status === 'committed'))
471
+ return drafts;
472
+ if (drafts.some((draft) => draft.status === 'committed'))
473
+ conflict('Cannot mix committed and uncommitted drafts.');
474
+ const result = drafts.map((draft) => ({
475
+ ...draft,
476
+ status: 'sealed',
477
+ }));
478
+ for (const draft of result)
479
+ await tx.saveDraft(draft);
480
+ return result;
481
+ });
482
+ if (sealed.every((draft) => draft.status === 'committed'))
483
+ return sealed.map((draft) => draft.result);
484
+ const prepared = new Map();
485
+ for (const [index, draft] of sealed.entries()) {
486
+ const body = await content.write(scope, this.#stream(scope, draft, limits, signal, 0, draft.size, 'commit'), { maxBytes: draft.size, signal });
487
+ if (body.size !== draft.size ||
488
+ (requests[index].sha256 !== undefined &&
489
+ requests[index].sha256 !== body.sha256))
490
+ conflict('Completed body does not match the expected byte count or SHA-256.');
491
+ prepared.set(draft.id, body);
492
+ }
493
+ return transaction(signal, async (tx) => {
494
+ const drafts = [];
495
+ for (const request of requests) {
496
+ const draft = await requireDraft(tx, request.draftId);
497
+ requireMutation(draft.expectedEtag);
498
+ drafts.push(draft);
499
+ }
500
+ // A competing identical commit may have completed while bodies streamed.
501
+ if (drafts.every((draft) => draft.status === 'committed')) {
502
+ for (const [index, draft] of drafts.entries())
503
+ if (draft.result === null ||
504
+ draft.body?.sha256 !== prepared.get(draft.id)?.sha256 ||
505
+ draft.size !== requests[index].size)
506
+ conflict('Concurrent commit differs from the prepared body.');
507
+ return drafts.map((draft) => draft.result);
508
+ }
509
+ if (drafts.some((draft) => draft.status !== 'sealed'))
510
+ conflict('Every draft must remain sealed until commit.');
511
+ const ordered = [...drafts].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
512
+ const results = await tx.commitHeads(ordered.map((draft) => ({ draft, body: prepared.get(draft.id) })));
513
+ if (results.length !== ordered.length ||
514
+ results.some((receipt) => receipt === null || receipt === undefined))
515
+ provider('Host must return one non-null receipt per head.');
516
+ const byId = new Map();
517
+ for (const [index, draft] of ordered.entries()) {
518
+ const result = results[index];
519
+ byId.set(draft.id, result);
520
+ await tx.saveDraft({
521
+ ...draft,
522
+ status: 'committed',
523
+ result,
524
+ body: prepared.get(draft.id),
525
+ });
526
+ }
527
+ return requests.map((request) => byId.get(request.draftId));
528
+ });
529
+ },
530
+ };
531
+ return Object.freeze(workflow);
532
+ }
533
+ #transaction(scope, signal, permission, work) {
534
+ signal.throwIfAborted();
535
+ return this.#options.persistence.transaction(scope, { signal, permission }, async (tx) => {
536
+ signal.throwIfAborted();
537
+ const result = await work(tx);
538
+ signal.throwIfAborted();
539
+ return result;
540
+ });
541
+ }
542
+ #stream(scope, draft, limits, parentSignal, start = 0, end = draft.size, permission = 'read') {
543
+ const controller = new AbortController();
544
+ const signal = AbortSignal.any([parentSignal, controller.signal]);
545
+ const content = this.#options.content;
546
+ const transaction = (work) => this.#transaction(scope, signal, permission, work);
547
+ const iterator = (async function* () {
548
+ let cursor = start;
549
+ while (cursor < end) {
550
+ const parts = await transaction(async (tx) => {
551
+ const current = await requireDraft(tx, draft.id);
552
+ if (current.status === 'cancelled')
553
+ conflict('Draft was cancelled.');
554
+ return tx.listParts(draft.id, cursor, limits.maxPageSize);
555
+ });
556
+ if (parts.length > limits.maxPageSize)
557
+ provider('Persistence returned an oversized page.');
558
+ let advanced = false;
559
+ for (const part of parts) {
560
+ if (cursor >= end)
561
+ break;
562
+ storageInteger(part.offset, 'part.offset');
563
+ storageInteger(part.size, 'part.size', 1);
564
+ if (part.size > limits.maxChunkBytes ||
565
+ part.size !== part.body.size ||
566
+ part.sha256 !== part.body.sha256)
567
+ provider('Invalid chunk receipt.');
568
+ if (part.offset + part.size <= cursor)
569
+ continue;
570
+ if (part.offset > cursor)
571
+ provider('Draft contains a missing chunk.');
572
+ const bytes = await collectStorageBytes(await content.read(scope, part.body, { signal }), part.size, signal);
573
+ if (bytes.byteLength !== part.size || digest(bytes) !== part.sha256)
574
+ conflict('Draft chunk failed integrity verification.');
575
+ const next = Math.min(end, part.offset + part.size);
576
+ yield bytes.subarray(cursor - part.offset, next - part.offset);
577
+ cursor = next;
578
+ advanced = true;
579
+ }
580
+ if (!advanced)
581
+ provider('Draft is incomplete.');
582
+ }
583
+ })();
584
+ return new ReadableStream({
585
+ async pull(output) {
586
+ try {
587
+ const next = await iterator.next();
588
+ if (next.done)
589
+ output.close();
590
+ else
591
+ output.enqueue(next.value);
592
+ }
593
+ catch (error) {
594
+ output.error(error);
595
+ await iterator.return(undefined);
596
+ }
597
+ },
598
+ async cancel(reason) {
599
+ controller.abort(reason);
600
+ await iterator.return(undefined);
601
+ },
602
+ }, { highWaterMark: 0 });
603
+ }
604
+ }
605
+ async function requireDraft(tx, id) {
606
+ validateIdentity(id);
607
+ const draft = await tx.getDraft(id);
608
+ if (draft === null)
609
+ throw new StorageError('Draft is unavailable.', { code: 'NOT_FOUND' });
610
+ return draft;
611
+ }
612
+ async function appendState(tx, draft, offset, size, sha256) {
613
+ if (draft.status !== 'open')
614
+ conflict('Draft is closed for writes.');
615
+ if (offset < draft.size) {
616
+ const previous = (await tx.listParts(draft.id, offset, 1))[0];
617
+ if (previous?.offset === offset &&
618
+ previous.size === size &&
619
+ previous.sha256 === sha256)
620
+ return { draft, replay: true };
621
+ conflict('Different bytes were already appended at this offset.');
622
+ }
623
+ if (offset !== draft.size)
624
+ conflict('Draft offset changed. Read the current size before appending.');
625
+ return { draft, replay: false };
626
+ }
627
+ function summary(draft) {
628
+ const { id, path, expectedEtag, sourceDraftId, text, status, size, result, createdAt, } = draft;
629
+ return Object.freeze({
630
+ id,
631
+ path,
632
+ expectedEtag,
633
+ sourceDraftId,
634
+ text,
635
+ status,
636
+ size,
637
+ result,
638
+ createdAt,
639
+ });
640
+ }
641
+ function validateIdentity(value) {
642
+ if (typeof value !== 'string' || value.length < 1 || value.length > 256)
643
+ invalid('Invalid workflow identity.');
644
+ }
645
+ function digest(bytes) {
646
+ return createHash('sha256').update(bytes).digest('hex');
647
+ }
648
+ function invalid(message) {
649
+ throw new StorageError(message, { code: 'INVALID_ARGUMENT' });
650
+ }
651
+ function conflict(message) {
652
+ throw new StorageError(message, { code: 'CONFLICT' });
653
+ }
654
+ function provider(message) {
655
+ throw new StorageError(message, { code: 'PROVIDER' });
656
+ }
657
+ //# sourceMappingURL=storage-file-workflow.js.map