@nestm/storage 0.1.0-alpha.10 → 0.1.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +161 -0
  3. package/SECURITY.md +27 -0
  4. package/dist/ai-sdk/ai-sdk-file-workflow-tools.d.ts +37 -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 +258 -0
  7. package/dist/ai-sdk/ai-sdk-file-workflow-tools.js.map +1 -0
  8. package/dist/ai-sdk/index.d.ts +1 -0
  9. package/dist/ai-sdk/index.d.ts.map +1 -1
  10. package/dist/ai-sdk/index.js +1 -0
  11. package/dist/ai-sdk/index.js.map +1 -1
  12. package/dist/bytes/index.d.ts +30 -0
  13. package/dist/bytes/index.d.ts.map +1 -0
  14. package/dist/bytes/index.js +96 -0
  15. package/dist/bytes/index.js.map +1 -0
  16. package/dist/core/index.d.ts +3 -0
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +3 -0
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/storage-staged-content.d.ts +39 -0
  21. package/dist/core/storage-staged-content.d.ts.map +1 -0
  22. package/dist/core/storage-staged-content.js +104 -0
  23. package/dist/core/storage-staged-content.js.map +1 -0
  24. package/dist/core/storage-streams.d.ts +19 -0
  25. package/dist/core/storage-streams.d.ts.map +1 -0
  26. package/dist/core/storage-streams.js +95 -0
  27. package/dist/core/storage-streams.js.map +1 -0
  28. package/dist/core/storage-text.d.ts +33 -0
  29. package/dist/core/storage-text.d.ts.map +1 -0
  30. package/dist/core/storage-text.js +98 -0
  31. package/dist/core/storage-text.js.map +1 -0
  32. package/dist/crypto/index.d.ts +2 -0
  33. package/dist/crypto/index.d.ts.map +1 -0
  34. package/dist/crypto/index.js +2 -0
  35. package/dist/crypto/index.js.map +1 -0
  36. package/dist/crypto/storage-encrypted-content.d.ts +49 -0
  37. package/dist/crypto/storage-encrypted-content.d.ts.map +1 -0
  38. package/dist/crypto/storage-encrypted-content.js +212 -0
  39. package/dist/crypto/storage-encrypted-content.js.map +1 -0
  40. package/dist/files-sdk/memory.d.ts +7 -0
  41. package/dist/files-sdk/memory.d.ts.map +1 -0
  42. package/dist/files-sdk/memory.js +72 -0
  43. package/dist/files-sdk/memory.js.map +1 -0
  44. package/dist/files-sdk/provider/index.d.ts.map +1 -1
  45. package/dist/files-sdk/provider/index.js +4 -0
  46. package/dist/files-sdk/provider/index.js.map +1 -1
  47. package/dist/testing/index.d.ts.map +1 -1
  48. package/dist/testing/index.js +2 -1
  49. package/dist/testing/index.js.map +1 -1
  50. package/dist/workspace/index.d.ts +5 -0
  51. package/dist/workspace/index.d.ts.map +1 -1
  52. package/dist/workspace/index.js +3 -0
  53. package/dist/workspace/index.js.map +1 -1
  54. package/dist/workspace/storage-file-catalog.types.d.ts +65 -0
  55. package/dist/workspace/storage-file-catalog.types.d.ts.map +1 -0
  56. package/dist/workspace/storage-file-catalog.types.js +2 -0
  57. package/dist/workspace/storage-file-catalog.types.js.map +1 -0
  58. package/dist/workspace/storage-file-workflow.d.ts +7 -0
  59. package/dist/workspace/storage-file-workflow.d.ts.map +1 -0
  60. package/dist/workspace/storage-file-workflow.js +440 -0
  61. package/dist/workspace/storage-file-workflow.js.map +1 -0
  62. package/dist/workspace/storage-file-workflow.protection.d.ts +31 -0
  63. package/dist/workspace/storage-file-workflow.protection.d.ts.map +1 -0
  64. package/dist/workspace/storage-file-workflow.protection.js +258 -0
  65. package/dist/workspace/storage-file-workflow.protection.js.map +1 -0
  66. package/dist/workspace/storage-file-workflow.types.d.ts +128 -0
  67. package/dist/workspace/storage-file-workflow.types.d.ts.map +1 -0
  68. package/dist/workspace/storage-file-workflow.types.js +8 -0
  69. package/dist/workspace/storage-file-workflow.types.js.map +1 -0
  70. package/package.json +17 -4
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,oCAAoC,EACpC,gCAAgC,EAChC,kCAAkC,GAKnC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAuB9B,MAAM,8BAA8B,CAAC","sourcesContent":["export {\n StorageWorkspaceError,\n isStorageWorkspaceError,\n} from './storage-workspace.error.js';\nexport {\n Aes256GcmStorageWorkspaceCursorCodec,\n STORAGE_WORKSPACE_CURSOR_VERSION,\n STORAGE_WORKSPACE_MAX_CURSOR_BYTES,\n type Aes256GcmStorageWorkspaceCursorCodecOptions,\n type StorageWorkspaceCursorCodec,\n type StorageWorkspaceCursorConfiguration,\n type StorageWorkspaceCursorEncodeOptions,\n} from './storage-workspace.cursor.js';\nexport {\n createStorageWorkspace,\n mountStorageWorkspace,\n} from './storage-workspace.js';\nexport {\n DEFAULT_STORAGE_WORKSPACE_LIMITS,\n STORAGE_WORKSPACE_PERMISSIONS,\n type MountStorageWorkspaceOptions,\n type StorageWorkspaceBody,\n type StorageWorkspaceByteFile,\n type StorageWorkspaceCopyOptions,\n type StorageWorkspaceDeleteOptions,\n type StorageWorkspace,\n type StorageWorkspaceDirectory,\n type StorageWorkspaceEntry,\n type StorageWorkspaceFile,\n type StorageWorkspaceLimits,\n type StorageWorkspaceListOptions,\n type StorageWorkspaceMountOptions,\n type StorageWorkspaceMutationOptions,\n type StorageWorkspaceOverwriteOptions,\n type StorageWorkspacePage,\n type StorageWorkspacePermission,\n type StorageWorkspaceReadOptions,\n type StorageWorkspaceSearchMatch,\n type StorageWorkspaceSearchOptions,\n type StorageWorkspaceTextFile,\n type StorageWorkspaceUnconditionalDeleteOptions,\n type StorageWorkspaceWriteOptions,\n} from './storage-workspace.types.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,oCAAoC,EAAE,MAAM,kCAAkC,CAAC;AAGxF,OAAO,EACL,mCAAmC,EACnC,mCAAmC,EACnC,sBAAsB,EACtB,qBAAqB,GAItB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,oCAAoC,EACpC,gCAAgC,EAChC,kCAAkC,GAKnC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAuB9B,MAAM,8BAA8B,CAAC","sourcesContent":["export {\n StorageWorkspaceError,\n isStorageWorkspaceError,\n} from './storage-workspace.error.js';\nexport { StorageFileWorkflow } from './storage-file-workflow.js';\nexport { DEFAULT_STORAGE_FILE_WORKFLOW_LIMITS } from './storage-file-workflow.types.js';\nexport type * from './storage-file-workflow.types.js';\nexport type * from './storage-file-catalog.types.js';\nexport {\n protectStorageFileWorkflowWorkspace,\n protectStorageFileWorkflowOperation,\n getStorageFileWorkflow,\n getStorageFileCatalog,\n type StorageFileWorkflowProtection,\n type StorageFileWorkflowWorkspace,\n type ProtectStorageFileWorkflowWorkspaceOptions,\n} from './storage-file-workflow.protection.js';\nexport {\n Aes256GcmStorageWorkspaceCursorCodec,\n STORAGE_WORKSPACE_CURSOR_VERSION,\n STORAGE_WORKSPACE_MAX_CURSOR_BYTES,\n type Aes256GcmStorageWorkspaceCursorCodecOptions,\n type StorageWorkspaceCursorCodec,\n type StorageWorkspaceCursorConfiguration,\n type StorageWorkspaceCursorEncodeOptions,\n} from './storage-workspace.cursor.js';\nexport {\n createStorageWorkspace,\n mountStorageWorkspace,\n} from './storage-workspace.js';\nexport {\n DEFAULT_STORAGE_WORKSPACE_LIMITS,\n STORAGE_WORKSPACE_PERMISSIONS,\n type MountStorageWorkspaceOptions,\n type StorageWorkspaceBody,\n type StorageWorkspaceByteFile,\n type StorageWorkspaceCopyOptions,\n type StorageWorkspaceDeleteOptions,\n type StorageWorkspace,\n type StorageWorkspaceDirectory,\n type StorageWorkspaceEntry,\n type StorageWorkspaceFile,\n type StorageWorkspaceLimits,\n type StorageWorkspaceListOptions,\n type StorageWorkspaceMountOptions,\n type StorageWorkspaceMutationOptions,\n type StorageWorkspaceOverwriteOptions,\n type StorageWorkspacePage,\n type StorageWorkspacePermission,\n type StorageWorkspaceReadOptions,\n type StorageWorkspaceSearchMatch,\n type StorageWorkspaceSearchOptions,\n type StorageWorkspaceTextFile,\n type StorageWorkspaceUnconditionalDeleteOptions,\n type StorageWorkspaceWriteOptions,\n} from './storage-workspace.types.js';\n"]}
@@ -0,0 +1,65 @@
1
+ import type { StorageTextEdit, StorageTextSearchResult } from '../core/storage-text.js';
2
+ import type { StorageTextWindow } from '../core/storage-streams.js';
3
+ import type { StorageFileWorkflowOperation, StorageFileWorkflowPage } from './storage-file-workflow.types.js';
4
+ export interface StorageCatalogFile {
5
+ readonly path: string;
6
+ readonly fileId: string;
7
+ readonly etag: string;
8
+ readonly size: number;
9
+ readonly contentType: string;
10
+ }
11
+ export interface StorageFileCatalogLimits {
12
+ readonly maxReadBytes: number;
13
+ readonly maxWriteBytes: number;
14
+ readonly maxPageSize: number;
15
+ readonly maxSearchScanBytes: number;
16
+ readonly maxSearchMatches: number;
17
+ readonly maxPathBytes: number;
18
+ }
19
+ export interface StorageCatalogPath extends StorageFileWorkflowOperation {
20
+ readonly path: string;
21
+ readonly expectedEtag?: string | undefined;
22
+ }
23
+ export interface StorageCatalogPage extends StorageFileWorkflowOperation {
24
+ readonly offset?: number | undefined;
25
+ }
26
+ export interface StorageCatalogCommand extends StorageCatalogPath {
27
+ /** Opaque host-scoped command identity, stable across response-loss retries. */
28
+ readonly commandId: string;
29
+ }
30
+ /** Host catalog port. Implementations own persistence, conditional heads and replay. */
31
+ export interface StorageFileCatalogCapability<Receipt = unknown> {
32
+ readonly kind: 'storage-file-catalog';
33
+ readonly version: 1;
34
+ readonly limits: StorageFileCatalogLimits;
35
+ allows(permission: 'read' | 'write'): boolean;
36
+ list(input: StorageCatalogPage & {
37
+ readonly directory?: string | undefined;
38
+ }): Promise<StorageFileWorkflowPage<StorageCatalogFile>>;
39
+ stat(input: StorageCatalogPath): Promise<StorageCatalogFile>;
40
+ search(input: StorageCatalogPage & {
41
+ readonly query: string;
42
+ }): Promise<StorageFileWorkflowPage<StorageCatalogFile>>;
43
+ readWindow(input: StorageCatalogPath & StorageCatalogPage): Promise<StorageTextWindow & {
44
+ readonly path: string;
45
+ readonly fileId: string;
46
+ readonly etag: string;
47
+ readonly size: number;
48
+ readonly totalBytes: number;
49
+ }>;
50
+ searchContent(input: StorageCatalogPath & StorageCatalogPage & {
51
+ readonly expectedEtag: string;
52
+ readonly query: string;
53
+ }): Promise<StorageTextSearchResult & {
54
+ readonly path: string;
55
+ readonly etag: string;
56
+ }>;
57
+ write(input: StorageCatalogCommand & {
58
+ readonly content: string;
59
+ }): Promise<Receipt>;
60
+ edit(input: StorageCatalogCommand & {
61
+ readonly expectedEtag: string;
62
+ readonly change: StorageTextEdit;
63
+ }): Promise<Receipt>;
64
+ }
65
+ //# sourceMappingURL=storage-file-catalog.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-file-catalog.types.d.ts","sourceRoot":"","sources":["../../src/workspace/storage-file-catalog.types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=storage-file-catalog.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-file-catalog.types.js","sourceRoot":"","sources":["../../src/workspace/storage-file-catalog.types.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n StorageTextEdit,\n StorageTextSearchResult,\n} from '../core/storage-text.js';\nimport type { StorageTextWindow } from '../core/storage-streams.js';\nimport type {\n StorageFileWorkflowOperation,\n StorageFileWorkflowPage,\n} from './storage-file-workflow.types.js';\n\nexport interface StorageCatalogFile {\n readonly path: string;\n readonly fileId: string;\n readonly etag: string;\n readonly size: number;\n readonly contentType: string;\n}\nexport interface StorageFileCatalogLimits {\n readonly maxReadBytes: number;\n readonly maxWriteBytes: number;\n readonly maxPageSize: number;\n readonly maxSearchScanBytes: number;\n readonly maxSearchMatches: number;\n readonly maxPathBytes: number;\n}\nexport interface StorageCatalogPath extends StorageFileWorkflowOperation {\n readonly path: string;\n readonly expectedEtag?: string | undefined;\n}\nexport interface StorageCatalogPage extends StorageFileWorkflowOperation {\n readonly offset?: number | undefined;\n}\nexport interface StorageCatalogCommand extends StorageCatalogPath {\n /** Opaque host-scoped command identity, stable across response-loss retries. */\n readonly commandId: string;\n}\n/** Host catalog port. Implementations own persistence, conditional heads and replay. */\nexport interface StorageFileCatalogCapability<Receipt = unknown> {\n readonly kind: 'storage-file-catalog';\n readonly version: 1;\n readonly limits: StorageFileCatalogLimits;\n allows(permission: 'read' | 'write'): boolean;\n list(\n input: StorageCatalogPage & { readonly directory?: string | undefined },\n ): Promise<StorageFileWorkflowPage<StorageCatalogFile>>;\n stat(input: StorageCatalogPath): Promise<StorageCatalogFile>;\n search(\n input: StorageCatalogPage & { readonly query: string },\n ): Promise<StorageFileWorkflowPage<StorageCatalogFile>>;\n readWindow(input: StorageCatalogPath & StorageCatalogPage): Promise<\n StorageTextWindow & {\n readonly path: string;\n readonly fileId: string;\n readonly etag: string;\n readonly size: number;\n readonly totalBytes: number;\n }\n >;\n searchContent(\n input: StorageCatalogPath &\n StorageCatalogPage & {\n readonly expectedEtag: string;\n readonly query: string;\n },\n ): Promise<\n StorageTextSearchResult & { readonly path: string; readonly etag: string }\n >;\n write(\n input: StorageCatalogCommand & { readonly content: string },\n ): Promise<Receipt>;\n edit(\n input: StorageCatalogCommand & {\n readonly expectedEtag: string;\n readonly change: StorageTextEdit;\n },\n ): Promise<Receipt>;\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import { type MountStorageFileWorkflowOptions, type StorageFileWorkflowCapability, type StorageFileWorkflowOptions } from './storage-file-workflow.types.js';
2
+ export declare class StorageFileWorkflow<Scope, Receipt> {
3
+ #private;
4
+ constructor(options: StorageFileWorkflowOptions<Scope, Receipt>);
5
+ mount(scope: Scope, options?: MountStorageFileWorkflowOptions): StorageFileWorkflowCapability<Receipt>;
6
+ }
7
+ //# sourceMappingURL=storage-file-workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-file-workflow.d.ts","sourceRoot":"","sources":["../../src/workspace/storage-file-workflow.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,+BAA+B,EAGpC,KAAK,6BAA6B,EAGlC,KAAK,0BAA0B,EAIhC,MAAM,kCAAkC,CAAC;AAE1C,qBAAa,mBAAmB,CAAC,KAAK,EAAE,OAAO;;IAE7C,YAAY,OAAO,EAAE,0BAA0B,CAAC,KAAK,EAAE,OAAO,CAAC,EAE9D;IAED,KAAK,CACH,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,+BAAoC,GAC5C,6BAA6B,CAAC,OAAO,CAAC,CAgYxC;CA6FF"}
@@ -0,0 +1,440 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { StorageError } from '../storage.error.js';
3
+ import { assertWorkspacePath } from './storage-workspace.path.js';
4
+ import { collectStorageBytes, readStorageTextWindow, storageBytesStream, storageInteger, } from '../core/storage-streams.js';
5
+ import { DEFAULT_STORAGE_FILE_WORKFLOW_LIMITS, } from './storage-file-workflow.types.js';
6
+ export class StorageFileWorkflow {
7
+ #options;
8
+ constructor(options) {
9
+ this.#options = options;
10
+ }
11
+ mount(scope, options = {}) {
12
+ const limits = Object.freeze({
13
+ ...DEFAULT_STORAGE_FILE_WORKFLOW_LIMITS,
14
+ ...options.limits,
15
+ });
16
+ for (const [name, value] of Object.entries(limits))
17
+ storageInteger(value, name, name === 'maxReadBytes' ? 4 : 1);
18
+ const permissions = new Set(options.permissions ?? ['read', 'write', 'commit']);
19
+ const mutations = new Set(options.mutations ?? ['create', 'replace']);
20
+ const boundSignal = options.signal;
21
+ const requireMutation = (expectedEtag) => {
22
+ if (!mutations.has(expectedEtag == null ? 'create' : 'replace'))
23
+ throw new StorageError('Draft mutation is not permitted by this capability.', { code: 'UNAUTHORIZED' });
24
+ };
25
+ const operationPermissions = new WeakMap();
26
+ const allows = (permission) => permissions.has(permission) &&
27
+ (permission === 'read' || mutations.size > 0);
28
+ const operation = (permission, input) => {
29
+ if (!allows(permission))
30
+ throw new StorageError('File workflow operation is not permitted.', {
31
+ code: 'UNAUTHORIZED',
32
+ });
33
+ const signals = [boundSignal, input.signal].filter((signal) => signal !== undefined);
34
+ const signal = AbortSignal.any(signals);
35
+ signal.throwIfAborted();
36
+ operationPermissions.set(signal, permission);
37
+ return signal;
38
+ };
39
+ const transaction = (signal, work) => this.#transaction(scope, signal, operationPermissions.get(signal), work);
40
+ const content = this.#options.content;
41
+ const workflow = {
42
+ kind: 'storage-file-workflow',
43
+ version: 1,
44
+ limits,
45
+ allows,
46
+ restrict: (restriction) => {
47
+ const narrowed = { ...limits };
48
+ for (const name of Object.keys(limits)) {
49
+ const requested = restriction.limits?.[name] ?? limits[name];
50
+ storageInteger(requested, name, name === 'maxReadBytes' ? 4 : 1);
51
+ narrowed[name] = Math.min(limits[name], requested);
52
+ }
53
+ return this.mount(scope, {
54
+ limits: narrowed,
55
+ permissions: [...(restriction.permissions ?? permissions)].filter((permission) => permissions.has(permission)),
56
+ mutations: [...(restriction.mutations ?? mutations)].filter((mutation) => mutations.has(mutation)),
57
+ signal: AbortSignal.any([boundSignal, restriction.signal].filter((signal) => signal !== undefined)),
58
+ });
59
+ },
60
+ begin: async (input) => {
61
+ const signal = operation('write', input);
62
+ requireMutation(input.expectedEtag);
63
+ assertWorkspacePath(input.path, limits.maxPathBytes, {
64
+ allowRoot: false,
65
+ });
66
+ validateIdentity(input.idempotencyKey);
67
+ if (typeof input.text !== 'boolean' ||
68
+ (input.expectedEtag !== undefined &&
69
+ (typeof input.expectedEtag !== 'string' ||
70
+ input.expectedEtag.length === 0 ||
71
+ input.expectedEtag.length > 1024)))
72
+ invalid('Invalid draft input.');
73
+ const fingerprint = digest(new TextEncoder().encode(JSON.stringify([
74
+ input.path,
75
+ input.expectedEtag ?? null,
76
+ input.text,
77
+ ])));
78
+ return transaction(signal, async (tx) => {
79
+ const prior = await tx.findDraftByKey(input.idempotencyKey);
80
+ if (prior !== null) {
81
+ if (prior.requestFingerprint !== fingerprint)
82
+ conflict('Draft key was already used for different input.');
83
+ return summary(prior);
84
+ }
85
+ const record = {
86
+ id: randomUUID(),
87
+ path: input.path,
88
+ expectedEtag: input.expectedEtag ?? null,
89
+ text: input.text,
90
+ status: 'open',
91
+ size: 0,
92
+ result: null,
93
+ createdAt: new Date().toISOString(),
94
+ idempotencyKey: input.idempotencyKey,
95
+ requestFingerprint: fingerprint,
96
+ body: null,
97
+ };
98
+ await tx.saveDraft(record);
99
+ return summary(record);
100
+ });
101
+ },
102
+ list: async (input = {}) => {
103
+ const signal = operation('read', input);
104
+ const offset = input.offset ?? 0;
105
+ storageInteger(offset, 'offset');
106
+ return transaction(signal, async (tx) => {
107
+ const records = await tx.listDrafts(offset, limits.maxPageSize + 1);
108
+ if (records.length > limits.maxPageSize + 1)
109
+ provider('Persistence returned an oversized page.');
110
+ return {
111
+ items: records.slice(0, limits.maxPageSize).map(summary),
112
+ nextOffset: records.length > limits.maxPageSize
113
+ ? offset + limits.maxPageSize
114
+ : null,
115
+ };
116
+ });
117
+ },
118
+ read: async (input) => {
119
+ const signal = operation('read', input);
120
+ const draft = await transaction(signal, (tx) => requireDraft(tx, input.draftId));
121
+ if (draft.status === 'cancelled')
122
+ conflict('Draft is cancelled.');
123
+ const offset = input.offset ?? 0;
124
+ storageInteger(offset, 'offset');
125
+ if (offset > draft.size)
126
+ invalid('Offset exceeds draft size.');
127
+ const window = draft.text
128
+ ? await readStorageTextWindow(async (range) => this.#stream(scope, draft, limits, signal, range.start, range.end + 1), {
129
+ size: draft.size,
130
+ offset,
131
+ maxBytes: limits.maxReadBytes,
132
+ signal,
133
+ })
134
+ : { content: null, offset, nextOffset: null };
135
+ return { ...summary(draft), ...window };
136
+ },
137
+ append: async (input) => {
138
+ const signal = operation('write', input);
139
+ storageInteger(input.offset, 'offset');
140
+ if (!(input.bytes instanceof Uint8Array) ||
141
+ input.bytes.byteLength < 1 ||
142
+ input.bytes.byteLength > limits.maxChunkBytes)
143
+ throw new StorageError('Append exceeds chunk limits.', {
144
+ code: 'LIMIT_EXCEEDED',
145
+ });
146
+ // Snapshot before any await: callers may reuse or mutate their upload buffer.
147
+ const bytes = input.bytes.slice();
148
+ const sha256 = digest(bytes);
149
+ const prior = await transaction(signal, async (tx) => {
150
+ const draft = await requireDraft(tx, input.draftId);
151
+ requireMutation(draft.expectedEtag);
152
+ return appendState(tx, draft, input.offset, bytes.byteLength, sha256);
153
+ });
154
+ if (prior.replay)
155
+ return summary(prior.draft);
156
+ if (prior.draft.text) {
157
+ try {
158
+ new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
159
+ }
160
+ catch {
161
+ invalid('Text chunks must contain complete UTF-8 characters.');
162
+ }
163
+ }
164
+ const body = await content.write(scope, storageBytesStream(bytes), {
165
+ maxBytes: limits.maxChunkBytes,
166
+ signal,
167
+ });
168
+ if (body.size !== bytes.byteLength || body.sha256 !== sha256)
169
+ provider('Staged chunk receipt does not match input.');
170
+ return transaction(signal, async (tx) => {
171
+ const current = await appendState(tx, await requireDraft(tx, input.draftId), input.offset, bytes.byteLength, sha256);
172
+ requireMutation(current.draft.expectedEtag);
173
+ if (current.replay)
174
+ return summary(current.draft);
175
+ const size = current.draft.size + bytes.byteLength;
176
+ storageInteger(size, 'size');
177
+ await tx.putPart(input.draftId, {
178
+ offset: input.offset,
179
+ size: body.size,
180
+ sha256,
181
+ body,
182
+ });
183
+ const draft = { ...current.draft, size };
184
+ await tx.saveDraft(draft);
185
+ return summary(draft);
186
+ });
187
+ },
188
+ parts: async (input) => {
189
+ const signal = operation('read', input);
190
+ const offset = input.offset ?? 0;
191
+ storageInteger(offset, 'offset');
192
+ return transaction(signal, async (tx) => {
193
+ const draft = await requireDraft(tx, input.draftId);
194
+ if (draft.status === 'cancelled')
195
+ conflict('Draft is cancelled.');
196
+ if (offset > draft.size)
197
+ invalid('Offset exceeds draft size.');
198
+ const parts = await tx.listParts(draft.id, offset, limits.maxPageSize);
199
+ if (parts.length > limits.maxPageSize)
200
+ provider('Persistence returned an oversized page.');
201
+ const items = parts
202
+ .filter((part) => part.offset + part.size > offset)
203
+ .map(({ offset, size, sha256 }) => ({ offset, size, sha256 }));
204
+ const last = items.at(-1);
205
+ const next = last === undefined ? draft.size : last.offset + last.size;
206
+ if (offset < draft.size && (last === undefined || next <= offset))
207
+ provider('Draft contains missing chunks.');
208
+ return { items, nextOffset: next < draft.size ? next : null };
209
+ });
210
+ },
211
+ cancel: async (input) => {
212
+ const signal = operation('write', input);
213
+ return transaction(signal, async (tx) => {
214
+ const draft = await requireDraft(tx, input.draftId);
215
+ requireMutation(draft.expectedEtag);
216
+ if (draft.status === 'committed')
217
+ conflict('Committed draft cannot be cancelled.');
218
+ const cancelled = { ...draft, status: 'cancelled' };
219
+ await tx.saveDraft(cancelled);
220
+ return summary(cancelled);
221
+ });
222
+ },
223
+ commit: async (input) => {
224
+ const signal = operation('commit', input);
225
+ if (input.drafts.length < 1 ||
226
+ input.drafts.length > limits.maxCommitFiles ||
227
+ new Set(input.drafts.map((draft) => draft.draftId)).size !==
228
+ input.drafts.length)
229
+ invalid('Commit requires distinct draft IDs within the batch limit.');
230
+ const requests = input.drafts.map((draft) => ({ ...draft }));
231
+ for (const request of requests) {
232
+ storageInteger(request.size, 'size');
233
+ if (request.sha256 !== undefined &&
234
+ !/^[0-9a-f]{64}$/u.test(request.sha256))
235
+ invalid('Invalid SHA-256.');
236
+ }
237
+ const sealed = await transaction(signal, async (tx) => {
238
+ const drafts = [];
239
+ for (const request of requests) {
240
+ const draft = await requireDraft(tx, request.draftId);
241
+ requireMutation(draft.expectedEtag);
242
+ if (draft.size !== request.size || draft.status === 'cancelled')
243
+ conflict('Draft is cancelled or has a different byte count.');
244
+ if (draft.status === 'committed' &&
245
+ (draft.result === null ||
246
+ draft.body === null ||
247
+ (request.sha256 !== undefined &&
248
+ request.sha256 !== draft.body.sha256)))
249
+ conflict('Committed draft does not match the requested digest.');
250
+ drafts.push(draft);
251
+ }
252
+ if (new Set(drafts.map((draft) => draft.path)).size !== drafts.length)
253
+ conflict('A path may appear only once per commit.');
254
+ if (drafts.every((draft) => draft.status === 'committed'))
255
+ return drafts;
256
+ if (drafts.some((draft) => draft.status === 'committed'))
257
+ conflict('Cannot mix committed and uncommitted drafts.');
258
+ const result = drafts.map((draft) => ({
259
+ ...draft,
260
+ status: 'sealed',
261
+ }));
262
+ for (const draft of result)
263
+ await tx.saveDraft(draft);
264
+ return result;
265
+ });
266
+ if (sealed.every((draft) => draft.status === 'committed'))
267
+ return sealed.map((draft) => draft.result);
268
+ const prepared = new Map();
269
+ for (const [index, draft] of sealed.entries()) {
270
+ const body = await content.write(scope, this.#stream(scope, draft, limits, signal, 0, draft.size, 'commit'), { maxBytes: draft.size, signal });
271
+ if (body.size !== draft.size ||
272
+ (requests[index].sha256 !== undefined &&
273
+ requests[index].sha256 !== body.sha256))
274
+ conflict('Completed body does not match the expected byte count or SHA-256.');
275
+ prepared.set(draft.id, body);
276
+ }
277
+ return transaction(signal, async (tx) => {
278
+ const drafts = [];
279
+ for (const request of requests) {
280
+ const draft = await requireDraft(tx, request.draftId);
281
+ requireMutation(draft.expectedEtag);
282
+ drafts.push(draft);
283
+ }
284
+ // A competing identical commit may have completed while bodies streamed.
285
+ if (drafts.every((draft) => draft.status === 'committed')) {
286
+ for (const [index, draft] of drafts.entries())
287
+ if (draft.result === null ||
288
+ draft.body?.sha256 !== prepared.get(draft.id)?.sha256 ||
289
+ draft.size !== requests[index].size)
290
+ conflict('Concurrent commit differs from the prepared body.');
291
+ return drafts.map((draft) => draft.result);
292
+ }
293
+ if (drafts.some((draft) => draft.status !== 'sealed'))
294
+ conflict('Every draft must remain sealed until commit.');
295
+ const ordered = [...drafts].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
296
+ const results = await tx.commitHeads(ordered.map((draft) => ({ draft, body: prepared.get(draft.id) })));
297
+ if (results.length !== ordered.length ||
298
+ results.some((receipt) => receipt === null || receipt === undefined))
299
+ provider('Host must return one non-null receipt per head.');
300
+ const byId = new Map();
301
+ for (const [index, draft] of ordered.entries()) {
302
+ const result = results[index];
303
+ byId.set(draft.id, result);
304
+ await tx.saveDraft({
305
+ ...draft,
306
+ status: 'committed',
307
+ result,
308
+ body: prepared.get(draft.id),
309
+ });
310
+ }
311
+ return requests.map((request) => byId.get(request.draftId));
312
+ });
313
+ },
314
+ };
315
+ return Object.freeze(workflow);
316
+ }
317
+ #transaction(scope, signal, permission, work) {
318
+ signal.throwIfAborted();
319
+ return this.#options.persistence.transaction(scope, { signal, permission }, async (tx) => {
320
+ signal.throwIfAborted();
321
+ const result = await work(tx);
322
+ signal.throwIfAborted();
323
+ return result;
324
+ });
325
+ }
326
+ #stream(scope, draft, limits, parentSignal, start = 0, end = draft.size, permission = 'read') {
327
+ const controller = new AbortController();
328
+ const signal = AbortSignal.any([parentSignal, controller.signal]);
329
+ const content = this.#options.content;
330
+ const transaction = (work) => this.#transaction(scope, signal, permission, work);
331
+ const iterator = (async function* () {
332
+ let cursor = start;
333
+ while (cursor < end) {
334
+ const parts = await transaction(async (tx) => {
335
+ const current = await requireDraft(tx, draft.id);
336
+ if (current.status === 'cancelled')
337
+ conflict('Draft was cancelled.');
338
+ return tx.listParts(draft.id, cursor, limits.maxPageSize);
339
+ });
340
+ if (parts.length > limits.maxPageSize)
341
+ provider('Persistence returned an oversized page.');
342
+ let advanced = false;
343
+ for (const part of parts) {
344
+ if (cursor >= end)
345
+ break;
346
+ storageInteger(part.offset, 'part.offset');
347
+ storageInteger(part.size, 'part.size', 1);
348
+ if (part.size > limits.maxChunkBytes ||
349
+ part.size !== part.body.size ||
350
+ part.sha256 !== part.body.sha256)
351
+ provider('Invalid chunk receipt.');
352
+ if (part.offset + part.size <= cursor)
353
+ continue;
354
+ if (part.offset > cursor)
355
+ provider('Draft contains a missing chunk.');
356
+ const bytes = await collectStorageBytes(await content.read(scope, part.body, { signal }), part.size, signal);
357
+ if (bytes.byteLength !== part.size || digest(bytes) !== part.sha256)
358
+ conflict('Draft chunk failed integrity verification.');
359
+ const next = Math.min(end, part.offset + part.size);
360
+ yield bytes.subarray(cursor - part.offset, next - part.offset);
361
+ cursor = next;
362
+ advanced = true;
363
+ }
364
+ if (!advanced)
365
+ provider('Draft is incomplete.');
366
+ }
367
+ })();
368
+ return new ReadableStream({
369
+ async pull(output) {
370
+ try {
371
+ const next = await iterator.next();
372
+ if (next.done)
373
+ output.close();
374
+ else
375
+ output.enqueue(next.value);
376
+ }
377
+ catch (error) {
378
+ output.error(error);
379
+ await iterator.return(undefined);
380
+ }
381
+ },
382
+ async cancel(reason) {
383
+ controller.abort(reason);
384
+ await iterator.return(undefined);
385
+ },
386
+ }, { highWaterMark: 0 });
387
+ }
388
+ }
389
+ async function requireDraft(tx, id) {
390
+ validateIdentity(id);
391
+ const draft = await tx.getDraft(id);
392
+ if (draft === null)
393
+ throw new StorageError('Draft is unavailable.', { code: 'NOT_FOUND' });
394
+ return draft;
395
+ }
396
+ async function appendState(tx, draft, offset, size, sha256) {
397
+ if (draft.status !== 'open')
398
+ conflict('Draft is closed for writes.');
399
+ if (offset < draft.size) {
400
+ const previous = (await tx.listParts(draft.id, offset, 1))[0];
401
+ if (previous?.offset === offset &&
402
+ previous.size === size &&
403
+ previous.sha256 === sha256)
404
+ return { draft, replay: true };
405
+ conflict('Different bytes were already appended at this offset.');
406
+ }
407
+ if (offset !== draft.size)
408
+ conflict('Draft offset changed. Read the current size before appending.');
409
+ return { draft, replay: false };
410
+ }
411
+ function summary(draft) {
412
+ const { id, path, expectedEtag, text, status, size, result, createdAt } = draft;
413
+ return Object.freeze({
414
+ id,
415
+ path,
416
+ expectedEtag,
417
+ text,
418
+ status,
419
+ size,
420
+ result,
421
+ createdAt,
422
+ });
423
+ }
424
+ function validateIdentity(value) {
425
+ if (typeof value !== 'string' || value.length < 1 || value.length > 256)
426
+ invalid('Invalid workflow identity.');
427
+ }
428
+ function digest(bytes) {
429
+ return createHash('sha256').update(bytes).digest('hex');
430
+ }
431
+ function invalid(message) {
432
+ throw new StorageError(message, { code: 'INVALID_ARGUMENT' });
433
+ }
434
+ function conflict(message) {
435
+ throw new StorageError(message, { code: 'CONFLICT' });
436
+ }
437
+ function provider(message) {
438
+ throw new StorageError(message, { code: 'PROVIDER' });
439
+ }
440
+ //# sourceMappingURL=storage-file-workflow.js.map