@cueai/omni-reader-mcp 1.0.0

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.
@@ -0,0 +1,720 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { chmod, link, lstat, mkdir, open, readdir, realpath, unlink, } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { ARTIFACT_TTL_MS, INLINE_RESULT_MAX_BYTES, RESULT_CHUNK_MAX_BYTES } from "./constants.js";
7
+ import { CursorCodec } from "./cursor.js";
8
+ import { OmniBridgeError } from "./errors.js";
9
+ const PREVIEW_MAX_BYTES = 2048;
10
+ const METADATA_VERSION = 1;
11
+ const RESULT_ID_PATTERN = /^result_[A-Za-z0-9_-]{16,64}$/;
12
+ const TEMP_NAME_PATTERN = /^\.tmp-[A-Za-z0-9_-]+$/;
13
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
14
+ function artifactError(code, message, retryable = false) {
15
+ return new OmniBridgeError({
16
+ code,
17
+ message,
18
+ fileUploaded: true,
19
+ billed: true,
20
+ contentReleased: true,
21
+ retryable,
22
+ });
23
+ }
24
+ function cacheError(code, message) {
25
+ return new OmniBridgeError({
26
+ code,
27
+ message,
28
+ fileUploaded: false,
29
+ billed: false,
30
+ contentReleased: false,
31
+ retryable: false,
32
+ });
33
+ }
34
+ function errno(error, code) {
35
+ return error instanceof Error && error.code === code;
36
+ }
37
+ function containsPath(parent, child) {
38
+ const relative = path.relative(parent, child);
39
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
40
+ }
41
+ async function removeIfPresent(filePath) {
42
+ try {
43
+ await unlink(filePath);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ if (errno(error, "ENOENT"))
48
+ return false;
49
+ throw error;
50
+ }
51
+ }
52
+ async function syncDirectory(directory) {
53
+ let handle;
54
+ try {
55
+ handle = await open(directory, "r");
56
+ await handle.sync();
57
+ }
58
+ catch (error) {
59
+ if (!errno(error, "EINVAL") && !errno(error, "ENOTSUP") && !errno(error, "EISDIR")) {
60
+ throw error;
61
+ }
62
+ }
63
+ finally {
64
+ await handle?.close();
65
+ }
66
+ }
67
+ async function openNoFollow(filePath) {
68
+ return open(filePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
69
+ }
70
+ async function readPrivateFile(filePath, missingCode, missingMessage) {
71
+ let handle;
72
+ try {
73
+ handle = await openNoFollow(filePath);
74
+ }
75
+ catch (error) {
76
+ if (errno(error, "ENOENT"))
77
+ throw artifactError(missingCode, missingMessage);
78
+ if (errno(error, "ELOOP") || errno(error, "EMLINK")) {
79
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result path is unsafe.");
80
+ }
81
+ throw error;
82
+ }
83
+ try {
84
+ const details = await handle.stat();
85
+ if (!details.isFile()) {
86
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result path is unsafe.");
87
+ }
88
+ return await handle.readFile();
89
+ }
90
+ finally {
91
+ await handle.close();
92
+ }
93
+ }
94
+ async function openArtifactForRead(filePath) {
95
+ try {
96
+ return await openNoFollow(filePath);
97
+ }
98
+ catch (error) {
99
+ if (errno(error, "ENOENT")) {
100
+ throw artifactError("RESULT_NOT_FOUND", "The local result artifact is unavailable.");
101
+ }
102
+ if (errno(error, "ELOOP") || errno(error, "EMLINK")) {
103
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact path is unsafe.");
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+ async function installPrivateBytes(directory, finalPath, bytes) {
109
+ const temporaryPath = path.join(directory, `.tmp-${randomBytes(18).toString("base64url")}`);
110
+ let handle;
111
+ try {
112
+ handle = await open(temporaryPath, "wx", 0o600);
113
+ await handle.writeFile(bytes);
114
+ await handle.sync();
115
+ await handle.close();
116
+ handle = undefined;
117
+ try {
118
+ await link(temporaryPath, finalPath);
119
+ }
120
+ catch (error) {
121
+ if (errno(error, "EEXIST"))
122
+ return false;
123
+ throw error;
124
+ }
125
+ await chmod(finalPath, 0o600);
126
+ await syncDirectory(directory);
127
+ return true;
128
+ }
129
+ finally {
130
+ await handle?.close();
131
+ await removeIfPresent(temporaryPath);
132
+ }
133
+ }
134
+ async function readCursorKey(keyPath) {
135
+ let handle;
136
+ try {
137
+ handle = await openNoFollow(keyPath);
138
+ }
139
+ catch (error) {
140
+ if (errno(error, "ENOENT"))
141
+ return undefined;
142
+ if (errno(error, "ELOOP") || errno(error, "EMLINK")) {
143
+ throw cacheError("ARTIFACT_CACHE_UNSAFE", "The local artifact cursor key path is unsafe.");
144
+ }
145
+ throw error;
146
+ }
147
+ try {
148
+ const details = await handle.stat();
149
+ if (!details.isFile() || details.size !== 32) {
150
+ throw cacheError("ARTIFACT_KEY_INVALID", "The local artifact cursor key is invalid.");
151
+ }
152
+ const key = await handle.readFile();
153
+ if (key.length !== 32) {
154
+ throw cacheError("ARTIFACT_KEY_INVALID", "The local artifact cursor key is invalid.");
155
+ }
156
+ await handle.chmod(0o600);
157
+ return key;
158
+ }
159
+ finally {
160
+ await handle.close();
161
+ }
162
+ }
163
+ async function loadOrCreateKey(rootDirectory) {
164
+ const keyPath = path.join(rootDirectory, "cursor.key");
165
+ const existing = await readCursorKey(keyPath);
166
+ if (existing !== undefined)
167
+ return existing;
168
+ const generated = randomBytes(32);
169
+ if (await installPrivateBytes(rootDirectory, keyPath, generated))
170
+ return generated;
171
+ const winner = await readCursorKey(keyPath);
172
+ if (winner === undefined) {
173
+ throw cacheError("ARTIFACT_KEY_INVALID", "The local artifact cursor key is unavailable.");
174
+ }
175
+ return winner;
176
+ }
177
+ function decodeUtf8(bytes) {
178
+ try {
179
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
180
+ }
181
+ catch {
182
+ throw artifactError("RESULT_ENCODING_INVALID", "The released result is not valid UTF-8.");
183
+ }
184
+ }
185
+ async function readUtf8Slice(handle, offset, maximumBytes, totalBytes) {
186
+ const requested = Math.min(maximumBytes, totalBytes - offset);
187
+ if (requested <= 0)
188
+ return { text: "", bytesRead: 0 };
189
+ const buffer = Buffer.alloc(requested);
190
+ let filled = 0;
191
+ while (filled < requested) {
192
+ const result = await handle.read(buffer, filled, requested - filled, offset + filled);
193
+ if (result.bytesRead === 0)
194
+ break;
195
+ filled += result.bytesRead;
196
+ }
197
+ if (filled !== requested) {
198
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact is incomplete.");
199
+ }
200
+ for (let end = filled; end >= 0; end -= 1) {
201
+ try {
202
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, end));
203
+ return { text, bytesRead: end };
204
+ }
205
+ catch {
206
+ // Backtrack only at the requested byte boundary.
207
+ }
208
+ }
209
+ throw artifactError("RESULT_ENCODING_INVALID", "The local result artifact is not valid UTF-8.");
210
+ }
211
+ function parseMetadata(value, expectedResultId) {
212
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
213
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result metadata is invalid.");
214
+ }
215
+ const record = value;
216
+ if (Object.keys(record).sort().join(",") !== "artifactName,createdAt,expiresAt,mediaType,resultBytes,resultDigest,resultId,version"
217
+ || record.version !== METADATA_VERSION
218
+ || record.resultId !== expectedResultId
219
+ || typeof record.resultBytes !== "number"
220
+ || !Number.isSafeInteger(record.resultBytes)
221
+ || record.resultBytes <= INLINE_RESULT_MAX_BYTES
222
+ || typeof record.mediaType !== "string"
223
+ || record.mediaType.length === 0
224
+ || typeof record.resultDigest !== "string"
225
+ || !SHA256_PATTERN.test(record.resultDigest)
226
+ || record.artifactName !== `${expectedResultId}.data`
227
+ || typeof record.createdAt !== "string"
228
+ || !Number.isFinite(Date.parse(record.createdAt))
229
+ || typeof record.expiresAt !== "string"
230
+ || !Number.isFinite(Date.parse(record.expiresAt))) {
231
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result metadata is invalid.");
232
+ }
233
+ return record;
234
+ }
235
+ export function defaultArtifactRoot(options = {}) {
236
+ const platform = options.platform ?? process.platform;
237
+ const homeDirectory = options.homeDirectory ?? os.homedir();
238
+ const env = options.env ?? process.env;
239
+ if (platform === "darwin") {
240
+ return path.posix.join(homeDirectory, "Library", "Caches", "cue", "omni-reader-mcp");
241
+ }
242
+ if (platform === "win32") {
243
+ const localAppData = env.LOCALAPPDATA;
244
+ if (localAppData === undefined || localAppData.length === 0) {
245
+ return path.win32.join(homeDirectory, "AppData", "Local", "Cue", "omni-reader-mcp", "Cache");
246
+ }
247
+ return path.win32.join(localAppData, "Cue", "omni-reader-mcp", "Cache");
248
+ }
249
+ const xdgCacheHome = env.XDG_CACHE_HOME;
250
+ const cacheHome = xdgCacheHome !== undefined && path.posix.isAbsolute(xdgCacheHome)
251
+ ? xdgCacheHome
252
+ : path.posix.join(homeDirectory, ".cache");
253
+ return path.posix.join(cacheHome, "cue", "omni-reader-mcp");
254
+ }
255
+ export class ArtifactStore {
256
+ #rootDirectory;
257
+ #resultsDirectory;
258
+ #retentionMs;
259
+ #now;
260
+ #cursor;
261
+ #closed = false;
262
+ constructor(rootDirectory, retentionMs, now, cursor) {
263
+ this.#rootDirectory = rootDirectory;
264
+ this.#resultsDirectory = path.join(rootDirectory, "results");
265
+ this.#retentionMs = retentionMs;
266
+ this.#now = now;
267
+ this.#cursor = cursor;
268
+ }
269
+ static async open(options = {}) {
270
+ const rootDirectory = path.resolve(options.rootDirectory ?? defaultArtifactRoot());
271
+ const projectDirectory = path.resolve(options.projectDirectory ?? process.cwd());
272
+ const retentionMs = options.retentionMs ?? ARTIFACT_TTL_MS;
273
+ const now = options.now ?? (() => new Date());
274
+ if (!Number.isSafeInteger(retentionMs) || retentionMs <= 0) {
275
+ throw new Error("artifact retention must be a positive integer");
276
+ }
277
+ await mkdir(rootDirectory, { recursive: true, mode: 0o700 });
278
+ const rootDetails = await lstat(rootDirectory);
279
+ if (!rootDetails.isDirectory() || rootDetails.isSymbolicLink()) {
280
+ throw cacheError("ARTIFACT_CACHE_UNSAFE", "The local artifact cache path is unsafe.");
281
+ }
282
+ const canonicalRoot = await realpath(rootDirectory);
283
+ const canonicalProject = await realpath(projectDirectory);
284
+ if (containsPath(canonicalProject, canonicalRoot) || containsPath(canonicalRoot, canonicalProject)) {
285
+ throw cacheError("ARTIFACT_CACHE_UNSAFE", "The local artifact cache must be outside the project directory.");
286
+ }
287
+ await chmod(canonicalRoot, 0o700);
288
+ const resultsDirectory = path.join(canonicalRoot, "results");
289
+ await mkdir(resultsDirectory, { recursive: true, mode: 0o700 });
290
+ const resultsDetails = await lstat(resultsDirectory);
291
+ if (!resultsDetails.isDirectory() || resultsDetails.isSymbolicLink()) {
292
+ throw cacheError("ARTIFACT_CACHE_UNSAFE", "The local artifact results path is unsafe.");
293
+ }
294
+ const canonicalResults = await realpath(resultsDirectory);
295
+ if (canonicalResults !== resultsDirectory || !containsPath(canonicalRoot, canonicalResults)) {
296
+ throw cacheError("ARTIFACT_CACHE_UNSAFE", "The local artifact results path is unsafe.");
297
+ }
298
+ await chmod(canonicalResults, 0o700);
299
+ const key = await loadOrCreateKey(canonicalRoot);
300
+ const store = new ArtifactStore(canonicalRoot, retentionMs, now, new CursorCodec(key, { now }));
301
+ await store.cleanupExpired();
302
+ return store;
303
+ }
304
+ get rootDirectory() {
305
+ return this.#rootDirectory;
306
+ }
307
+ createRetention() {
308
+ this.#requireOpen();
309
+ return new LocalResultRetention(this);
310
+ }
311
+ async read(resultId, cursor, maxBytes = RESULT_CHUNK_MAX_BYTES) {
312
+ this.#requireOpen();
313
+ const metadata = await this.#loadMetadata(resultId);
314
+ if (Date.parse(metadata.expiresAt) <= this.#now().getTime()) {
315
+ await this.discard(resultId);
316
+ throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
317
+ }
318
+ let offset = 0;
319
+ if (cursor !== undefined) {
320
+ const payload = this.#cursor.decode(cursor);
321
+ if (payload.resultId !== resultId || payload.expiresAt !== metadata.expiresAt) {
322
+ throw artifactError("RESULT_CURSOR_MISMATCH", "The result cursor does not match this artifact.");
323
+ }
324
+ offset = payload.offset;
325
+ }
326
+ if (offset > metadata.resultBytes) {
327
+ throw artifactError("INVALID_RESULT_CURSOR", "The result cursor offset is invalid.");
328
+ }
329
+ const normalizedMaxBytes = Number.isFinite(maxBytes)
330
+ ? Math.trunc(maxBytes)
331
+ : RESULT_CHUNK_MAX_BYTES;
332
+ const requested = Math.min(RESULT_CHUNK_MAX_BYTES, Math.max(1, normalizedMaxBytes));
333
+ const artifactPath = path.join(this.#resultsDirectory, metadata.artifactName);
334
+ const handle = await openArtifactForRead(artifactPath);
335
+ let chunk;
336
+ try {
337
+ const artifactStat = await handle.stat();
338
+ if (!artifactStat.isFile() || artifactStat.size !== metadata.resultBytes) {
339
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact is invalid.");
340
+ }
341
+ chunk = await readUtf8Slice(handle, offset, requested, metadata.resultBytes);
342
+ }
343
+ finally {
344
+ await handle.close();
345
+ }
346
+ if (chunk.bytesRead === 0 && offset < metadata.resultBytes) {
347
+ throw artifactError("RESULT_CHUNK_TOO_SMALL", "The requested result chunk cannot fit the next UTF-8 character.");
348
+ }
349
+ const nextOffset = offset + chunk.bytesRead;
350
+ return {
351
+ resultId,
352
+ resultBytes: metadata.resultBytes,
353
+ expiresAt: metadata.expiresAt,
354
+ text: chunk.text,
355
+ ...(nextOffset < metadata.resultBytes
356
+ ? { nextCursor: this.#cursor.encode({ resultId, offset: nextOffset, expiresAt: metadata.expiresAt }) }
357
+ : {}),
358
+ };
359
+ }
360
+ async discard(resultId) {
361
+ this.#requireOpen();
362
+ if (!RESULT_ID_PATTERN.test(resultId))
363
+ return false;
364
+ const metadataPath = path.join(this.#resultsDirectory, `${resultId}.json`);
365
+ const artifactPath = path.join(this.#resultsDirectory, `${resultId}.data`);
366
+ const removedMetadata = await removeIfPresent(metadataPath);
367
+ const removedArtifact = await removeIfPresent(artifactPath);
368
+ if (removedMetadata || removedArtifact)
369
+ await syncDirectory(this.#resultsDirectory);
370
+ return removedMetadata || removedArtifact;
371
+ }
372
+ async cleanupExpired() {
373
+ this.#requireOpen();
374
+ const entries = await readdir(this.#resultsDirectory, { withFileTypes: true });
375
+ const now = this.#now().getTime();
376
+ let removed = 0;
377
+ const liveArtifacts = new Set();
378
+ for (const entry of entries) {
379
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
380
+ continue;
381
+ const resultId = entry.name.slice(0, -5);
382
+ if (!RESULT_ID_PATTERN.test(resultId))
383
+ continue;
384
+ try {
385
+ const metadata = await this.#loadMetadata(resultId, true);
386
+ if (Date.parse(metadata.expiresAt) <= now) {
387
+ if (await this.discard(resultId))
388
+ removed += 1;
389
+ }
390
+ else {
391
+ liveArtifacts.add(metadata.artifactName);
392
+ }
393
+ }
394
+ catch {
395
+ if (await this.discard(resultId))
396
+ removed += 1;
397
+ }
398
+ }
399
+ for (const entry of entries) {
400
+ if (!entry.isFile())
401
+ continue;
402
+ const entryPath = path.join(this.#resultsDirectory, entry.name);
403
+ if (TEMP_NAME_PATTERN.test(entry.name)) {
404
+ let details;
405
+ try {
406
+ details = await lstat(entryPath);
407
+ }
408
+ catch (error) {
409
+ if (errno(error, "ENOENT"))
410
+ continue;
411
+ throw error;
412
+ }
413
+ if (details.mtimeMs + this.#retentionMs <= now && await removeIfPresent(entryPath))
414
+ removed += 1;
415
+ }
416
+ else if (entry.name.endsWith(".data") && !liveArtifacts.has(entry.name)) {
417
+ let details;
418
+ try {
419
+ details = await lstat(entryPath);
420
+ }
421
+ catch (error) {
422
+ if (errno(error, "ENOENT"))
423
+ continue;
424
+ throw error;
425
+ }
426
+ if (details.mtimeMs + this.#retentionMs <= now && await removeIfPresent(entryPath))
427
+ removed += 1;
428
+ }
429
+ }
430
+ if (removed > 0)
431
+ await syncDirectory(this.#resultsDirectory);
432
+ return removed;
433
+ }
434
+ async close() {
435
+ if (this.#closed)
436
+ return;
437
+ await this.cleanupExpired();
438
+ this.#closed = true;
439
+ }
440
+ async _finalizeArtifact(temporaryPath, metadata) {
441
+ this.#requireOpen();
442
+ const now = this.#now();
443
+ const createdAt = now.toISOString();
444
+ const expiresAt = new Date(now.getTime() + this.#retentionMs).toISOString();
445
+ const artifactName = `${metadata.resultId}.data`;
446
+ const artifactPath = path.join(this.#resultsDirectory, artifactName);
447
+ const metadataPath = path.join(this.#resultsDirectory, `${metadata.resultId}.json`);
448
+ try {
449
+ if (!await this.#installTemporary(temporaryPath, artifactPath)) {
450
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "The local result artifact could not be installed.");
451
+ }
452
+ const stored = {
453
+ version: METADATA_VERSION,
454
+ resultId: metadata.resultId,
455
+ resultBytes: metadata.resultBytes,
456
+ mediaType: metadata.mediaType,
457
+ resultDigest: metadata.resultDigest,
458
+ artifactName,
459
+ createdAt,
460
+ expiresAt,
461
+ };
462
+ const installed = await installPrivateBytes(this.#resultsDirectory, metadataPath, Buffer.from(JSON.stringify(stored), "utf8"));
463
+ if (!installed) {
464
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "The local result metadata could not be installed.");
465
+ }
466
+ const previewHandle = await openArtifactForRead(artifactPath);
467
+ let previewChunk;
468
+ try {
469
+ const artifactStat = await previewHandle.stat();
470
+ if (!artifactStat.isFile() || artifactStat.size !== metadata.resultBytes) {
471
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact is invalid.");
472
+ }
473
+ previewChunk = await readUtf8Slice(previewHandle, 0, PREVIEW_MAX_BYTES, metadata.resultBytes);
474
+ }
475
+ finally {
476
+ await previewHandle.close();
477
+ }
478
+ return {
479
+ kind: "artifact",
480
+ operationId: metadata.operationId,
481
+ resultId: metadata.resultId,
482
+ resultBytes: metadata.resultBytes,
483
+ mediaType: metadata.mediaType,
484
+ resultDigest: metadata.resultDigest,
485
+ source: metadata.source,
486
+ artifactPath,
487
+ expiresAt,
488
+ preview: previewChunk.text,
489
+ ...(previewChunk.bytesRead < metadata.resultBytes
490
+ ? { nextCursor: this.#cursor.encode({ resultId: metadata.resultId, offset: previewChunk.bytesRead, expiresAt }) }
491
+ : {}),
492
+ };
493
+ }
494
+ catch (error) {
495
+ await removeIfPresent(metadataPath);
496
+ await removeIfPresent(artifactPath);
497
+ await removeIfPresent(temporaryPath);
498
+ throw error;
499
+ }
500
+ }
501
+ async _newTemporaryArtifact() {
502
+ this.#requireOpen();
503
+ for (let attempt = 0; attempt < 8; attempt += 1) {
504
+ const resultId = `result_${randomBytes(18).toString("base64url")}`;
505
+ const temporaryPath = path.join(this.#resultsDirectory, `.tmp-${randomBytes(18).toString("base64url")}`);
506
+ try {
507
+ const handle = await open(temporaryPath, "wx", 0o600);
508
+ return { resultId, temporaryPath, handle };
509
+ }
510
+ catch (error) {
511
+ if (!errno(error, "EEXIST"))
512
+ throw error;
513
+ }
514
+ }
515
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "A private local result artifact could not be created.", true);
516
+ }
517
+ async #installTemporary(temporaryPath, artifactPath) {
518
+ try {
519
+ await link(temporaryPath, artifactPath);
520
+ }
521
+ catch (error) {
522
+ if (errno(error, "EEXIST"))
523
+ return false;
524
+ throw error;
525
+ }
526
+ await chmod(artifactPath, 0o600);
527
+ await removeIfPresent(temporaryPath);
528
+ await syncDirectory(this.#resultsDirectory);
529
+ return true;
530
+ }
531
+ async #loadMetadata(resultId, allowExpired = false) {
532
+ if (!RESULT_ID_PATTERN.test(resultId)) {
533
+ throw artifactError("RESULT_NOT_FOUND", "The local result artifact is unavailable.");
534
+ }
535
+ const metadataPath = path.join(this.#resultsDirectory, `${resultId}.json`);
536
+ let value;
537
+ try {
538
+ const bytes = await readPrivateFile(metadataPath, "RESULT_NOT_FOUND", "The local result artifact is unavailable.");
539
+ value = JSON.parse(bytes.toString("utf8"));
540
+ }
541
+ catch (error) {
542
+ if (error instanceof SyntaxError) {
543
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result metadata is invalid.");
544
+ }
545
+ throw error;
546
+ }
547
+ const metadata = parseMetadata(value, resultId);
548
+ if (!allowExpired && Date.parse(metadata.expiresAt) <= this.#now().getTime()) {
549
+ await this.discard(resultId);
550
+ throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
551
+ }
552
+ return metadata;
553
+ }
554
+ #requireOpen() {
555
+ if (this.#closed)
556
+ throw cacheError("ARTIFACT_STORE_CLOSED", "The local artifact store is closed.");
557
+ }
558
+ }
559
+ export class LocalResultRetention {
560
+ #store;
561
+ #start;
562
+ #hash;
563
+ #decoder;
564
+ #encodingInvalid = false;
565
+ #received = 0;
566
+ #inlineChunks = [];
567
+ #artifact;
568
+ #result;
569
+ constructor(store) {
570
+ this.#store = store;
571
+ }
572
+ async reset() {
573
+ await this.abort();
574
+ this.#start = undefined;
575
+ this.#result = undefined;
576
+ }
577
+ async begin(metadata) {
578
+ if (this.#start !== undefined || this.#result !== undefined) {
579
+ throw artifactError("LOCAL_RESULT_STATE_INVALID", "Local result retention has already started.");
580
+ }
581
+ if (!Number.isSafeInteger(metadata.resultBytes)
582
+ || metadata.resultBytes < 0
583
+ || metadata.operationId.length === 0
584
+ || metadata.mediaType.length === 0) {
585
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The released result metadata is invalid.");
586
+ }
587
+ this.#start = metadata;
588
+ this.#hash = createHash("sha256");
589
+ this.#decoder = new TextDecoder("utf-8", { fatal: true });
590
+ this.#encodingInvalid = false;
591
+ this.#received = 0;
592
+ this.#inlineChunks = [];
593
+ if (metadata.resultBytes > INLINE_RESULT_MAX_BYTES) {
594
+ const temporary = await this.#store._newTemporaryArtifact();
595
+ this.#artifact = temporary;
596
+ }
597
+ }
598
+ async write(chunk) {
599
+ if (this.#start === undefined || this.#hash === undefined || this.#decoder === undefined) {
600
+ throw artifactError("LOCAL_RESULT_STATE_INVALID", "Local result retention has not started.");
601
+ }
602
+ if (chunk.byteLength === 0)
603
+ return;
604
+ if (this.#received + chunk.byteLength > this.#start.resultBytes) {
605
+ await this.abort();
606
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The released result exceeded its declared length.");
607
+ }
608
+ this.#hash.update(chunk);
609
+ if (!this.#encodingInvalid) {
610
+ try {
611
+ this.#decoder.decode(chunk, { stream: true });
612
+ }
613
+ catch {
614
+ this.#encodingInvalid = true;
615
+ }
616
+ }
617
+ if (this.#artifact === undefined) {
618
+ this.#inlineChunks.push(Buffer.from(chunk));
619
+ }
620
+ else {
621
+ const handle = this.#artifact.handle;
622
+ if (handle === undefined) {
623
+ throw artifactError("LOCAL_RESULT_STATE_INVALID", "The local artifact is not writable.");
624
+ }
625
+ let written = 0;
626
+ while (written < chunk.byteLength) {
627
+ const result = await handle.write(chunk, written, chunk.byteLength - written, null);
628
+ if (result.bytesWritten === 0) {
629
+ await this.abort();
630
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "The local result artifact write stalled.", true);
631
+ }
632
+ written += result.bytesWritten;
633
+ }
634
+ }
635
+ this.#received += chunk.byteLength;
636
+ }
637
+ async complete(metadata) {
638
+ const start = this.#start;
639
+ const hash = this.#hash;
640
+ const decoder = this.#decoder;
641
+ if (start === undefined || hash === undefined || decoder === undefined || this.#result !== undefined) {
642
+ throw artifactError("LOCAL_RESULT_STATE_INVALID", "Local result retention cannot be completed.");
643
+ }
644
+ try {
645
+ if (metadata.operationId !== start.operationId
646
+ || metadata.resultBytes !== start.resultBytes
647
+ || metadata.mediaType !== start.mediaType
648
+ || metadata.source !== start.source
649
+ || this.#received !== start.resultBytes
650
+ || !SHA256_PATTERN.test(metadata.resultDigest)) {
651
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained result metadata did not match.");
652
+ }
653
+ if (!this.#encodingInvalid) {
654
+ try {
655
+ decoder.decode();
656
+ }
657
+ catch {
658
+ this.#encodingInvalid = true;
659
+ }
660
+ }
661
+ if (this.#encodingInvalid) {
662
+ throw artifactError("RESULT_ENCODING_INVALID", "The released result is not valid UTF-8.");
663
+ }
664
+ const computedDigest = `sha256:${hash.digest("hex")}`;
665
+ if (computedDigest !== metadata.resultDigest) {
666
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained result digest did not match.");
667
+ }
668
+ if (this.#artifact === undefined) {
669
+ const bytes = Buffer.concat(this.#inlineChunks);
670
+ this.#result = {
671
+ kind: "inline",
672
+ ...metadata,
673
+ text: decodeUtf8(bytes),
674
+ };
675
+ }
676
+ else {
677
+ const handle = this.#artifact.handle;
678
+ if (handle === undefined) {
679
+ throw artifactError("LOCAL_RESULT_STATE_INVALID", "The local artifact is not writable.");
680
+ }
681
+ await handle.sync();
682
+ await handle.close();
683
+ this.#artifact.handle = undefined;
684
+ this.#result = await this.#store._finalizeArtifact(this.#artifact.temporaryPath, {
685
+ ...metadata,
686
+ resultId: this.#artifact.resultId,
687
+ });
688
+ }
689
+ }
690
+ catch (error) {
691
+ await this.abort();
692
+ throw error;
693
+ }
694
+ }
695
+ result() {
696
+ if (this.#result === undefined) {
697
+ throw artifactError("LOCAL_RESULT_NOT_DURABLE", "The local result has not been retained durably.");
698
+ }
699
+ return this.#result;
700
+ }
701
+ async abort() {
702
+ const artifact = this.#artifact;
703
+ if (artifact?.handle !== undefined) {
704
+ await artifact.handle.close();
705
+ artifact.handle = undefined;
706
+ }
707
+ if (artifact !== undefined) {
708
+ await removeIfPresent(artifact.temporaryPath);
709
+ if (this.#result?.kind === "artifact") {
710
+ await this.#store.discard(this.#result.resultId);
711
+ }
712
+ }
713
+ this.#artifact = undefined;
714
+ this.#inlineChunks = [];
715
+ this.#hash = undefined;
716
+ this.#decoder = undefined;
717
+ this.#received = 0;
718
+ this.#result = undefined;
719
+ }
720
+ }