@tangle-network/agent-app 0.45.57 → 0.45.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant/index.js +1 -1
- package/dist/chat-routes/attachment-store.d.ts +66 -26
- package/dist/chat-routes/attachment-upload.d.ts +59 -38
- package/dist/chat-routes/attachment-write-safety.d.ts +11 -0
- package/dist/chat-routes/index.js +387 -20
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-routes/promote-file-part.d.ts +28 -13
- package/dist/chat-store/index.js +1 -1
- package/dist/chunk-CDC5HFKL.js +165 -0
- package/dist/chunk-CDC5HFKL.js.map +1 -0
- package/dist/{chunk-5T5TSBM4.js → chunk-FOXGPGXF.js} +42 -4
- package/dist/chunk-FOXGPGXF.js.map +1 -0
- package/dist/{chunk-LRHVCVEW.js → chunk-Q4ZER4HI.js} +39 -1
- package/dist/chunk-Q4ZER4HI.js.map +1 -0
- package/dist/redact/index.d.ts +1 -0
- package/dist/redact/index.js +10 -116
- package/dist/redact/index.js.map +1 -1
- package/dist/store/index.d.ts +35 -0
- package/dist/store/index.js +3 -1
- package/dist/web-react/chat-composer.d.ts +6 -1
- package/dist/web-react/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5T5TSBM4.js.map +0 -1
- package/dist/chunk-LRHVCVEW.js.map +0 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redactErrorMessage
|
|
3
|
+
} from "../chunk-CDC5HFKL.js";
|
|
1
4
|
import {
|
|
2
5
|
createSandboxFileIndexRoute
|
|
3
6
|
} from "../chunk-QY4BRKRJ.js";
|
|
@@ -2262,6 +2265,21 @@ function createUploadRoute(options) {
|
|
|
2262
2265
|
};
|
|
2263
2266
|
}
|
|
2264
2267
|
|
|
2268
|
+
// src/chat-routes/attachment-store.ts
|
|
2269
|
+
var ATTACHMENT_STORAGE_FAILURE_MESSAGE = "Attachment storage is temporarily unavailable. Please try again.";
|
|
2270
|
+
function immutableAttachmentPath(logicalPath, ownershipId) {
|
|
2271
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(ownershipId)) {
|
|
2272
|
+
throw new Error("attachment ownership id must be a path-safe identifier");
|
|
2273
|
+
}
|
|
2274
|
+
return `${logicalPath}--${ownershipId}`;
|
|
2275
|
+
}
|
|
2276
|
+
function createAtomicAttachmentWriter(input) {
|
|
2277
|
+
return {
|
|
2278
|
+
write: input.write.bind(input),
|
|
2279
|
+
abort: input.abort.bind(input)
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2265
2283
|
// src/chat-routes/resolve-attachments.ts
|
|
2266
2284
|
var MAX_ATTACHMENT_NAME_LENGTH = 256;
|
|
2267
2285
|
function isAttachmentKind(value) {
|
|
@@ -2357,6 +2375,47 @@ async function resolveChatAttachments(value, options) {
|
|
|
2357
2375
|
return { succeeded: true, value: inputs.map(attachmentInputToPart) };
|
|
2358
2376
|
}
|
|
2359
2377
|
|
|
2378
|
+
// src/chat-routes/attachment-write-safety.ts
|
|
2379
|
+
function objectLike(value) {
|
|
2380
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
2381
|
+
}
|
|
2382
|
+
function inspectLegacyAttachmentWriteResult(value) {
|
|
2383
|
+
try {
|
|
2384
|
+
if (!objectLike(value)) return void 0;
|
|
2385
|
+
const ok = value.ok;
|
|
2386
|
+
if (ok === true) return { ok: true };
|
|
2387
|
+
if (ok === false && typeof value.reason === "string") {
|
|
2388
|
+
return { ok: false, reason: value.reason };
|
|
2389
|
+
}
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
return void 0;
|
|
2393
|
+
}
|
|
2394
|
+
function inspectAtomicAttachmentWriteResult(value, ownership) {
|
|
2395
|
+
try {
|
|
2396
|
+
if (!objectLike(value)) return void 0;
|
|
2397
|
+
const receiptValue = value.receipt;
|
|
2398
|
+
if (!objectLike(receiptValue)) return void 0;
|
|
2399
|
+
const receiptOwnership = receiptValue.ownership;
|
|
2400
|
+
if (!objectLike(receiptOwnership)) return void 0;
|
|
2401
|
+
if (receiptOwnership.id !== ownership.id || receiptOwnership.path !== ownership.path || typeof receiptOwnership.id !== "string" || typeof receiptOwnership.path !== "string" || typeof receiptValue.rollback !== "function") {
|
|
2402
|
+
return void 0;
|
|
2403
|
+
}
|
|
2404
|
+
if (value.ok === true) {
|
|
2405
|
+
return { ok: true, receipt: receiptValue };
|
|
2406
|
+
}
|
|
2407
|
+
if (value.ok === false && typeof value.reason === "string") {
|
|
2408
|
+
return {
|
|
2409
|
+
ok: false,
|
|
2410
|
+
reason: value.reason,
|
|
2411
|
+
receipt: receiptValue
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
} catch {
|
|
2415
|
+
}
|
|
2416
|
+
return void 0;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2360
2419
|
// src/chat-routes/promote-file-part.ts
|
|
2361
2420
|
var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
2362
2421
|
var EXT_TO_MIME = {
|
|
@@ -2484,11 +2543,41 @@ function defaultBuildAttachmentPath(args) {
|
|
|
2484
2543
|
const base = extension ? args.filename.slice(0, -extension.length) : args.filename;
|
|
2485
2544
|
return `uploads/agent/${args.date}/${base}-${args.hash8}${extension}`;
|
|
2486
2545
|
}
|
|
2546
|
+
function logPromotionStorageError(logger, message, fields) {
|
|
2547
|
+
try {
|
|
2548
|
+
logger.error(message, fields);
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
async function compensatePromotionWrite(writer, scopeId, ownership, receipt) {
|
|
2553
|
+
try {
|
|
2554
|
+
await receipt.rollback();
|
|
2555
|
+
return {};
|
|
2556
|
+
} catch (rollbackError) {
|
|
2557
|
+
try {
|
|
2558
|
+
await writer.abort(scopeId, ownership);
|
|
2559
|
+
return { rollbackError };
|
|
2560
|
+
} catch (abortError) {
|
|
2561
|
+
return { rollbackError, abortError };
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function isAtomicPromotionOptions(options) {
|
|
2566
|
+
return "attachmentWriter" in options;
|
|
2567
|
+
}
|
|
2487
2568
|
async function promoteAgentFilePart(options) {
|
|
2569
|
+
const atomic = isAtomicPromotionOptions(options);
|
|
2488
2570
|
const maxBytes = options.maxBytes ?? PROMOTE_MAX_FILE_BYTES;
|
|
2489
2571
|
const sniffMime = options.sniffMime ?? sniffMimeFromName;
|
|
2490
2572
|
const buildAttachmentPath = options.buildAttachmentPath ?? defaultBuildAttachmentPath;
|
|
2491
2573
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2574
|
+
const logger = options.logger ?? console;
|
|
2575
|
+
const createWriteId = atomic ? options.createWriteId ?? (() => {
|
|
2576
|
+
if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") {
|
|
2577
|
+
throw new Error("attachment promotion requires crypto.randomUUID");
|
|
2578
|
+
}
|
|
2579
|
+
return crypto.randomUUID();
|
|
2580
|
+
}) : void 0;
|
|
2492
2581
|
const filename = sanitizeAttachmentFileName(
|
|
2493
2582
|
options.raw.filename ?? basenameFromUrl(options.raw.url) ?? "agent-file"
|
|
2494
2583
|
);
|
|
@@ -2504,19 +2593,118 @@ async function promoteAgentFilePart(options) {
|
|
|
2504
2593
|
const kind = attachmentKindForMime(mediaType);
|
|
2505
2594
|
const digest = await hash8(options.raw.id ?? options.raw.url ?? filename);
|
|
2506
2595
|
const date = now().toISOString().split("T")[0] ?? "";
|
|
2507
|
-
const
|
|
2596
|
+
const logicalPath = buildAttachmentPath({ filename, hash8: digest, date, mediaType, kind });
|
|
2597
|
+
if (!atomic) {
|
|
2598
|
+
let written2;
|
|
2599
|
+
try {
|
|
2600
|
+
written2 = await options.writeAttachment(options.scopeId, logicalPath, resolved.bytes, {
|
|
2601
|
+
mediaType,
|
|
2602
|
+
name: filename,
|
|
2603
|
+
originalName: options.raw.filename ?? filename,
|
|
2604
|
+
size: resolved.bytes.byteLength
|
|
2605
|
+
});
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
const reason = redactErrorMessage(error);
|
|
2608
|
+
logPromotionStorageError(logger, "[promote-file-part] legacy writer failed", {
|
|
2609
|
+
path: logicalPath,
|
|
2610
|
+
error: reason
|
|
2611
|
+
});
|
|
2612
|
+
return { succeeded: false, filename, reason };
|
|
2613
|
+
}
|
|
2614
|
+
const result2 = inspectLegacyAttachmentWriteResult(written2);
|
|
2615
|
+
if (!result2) {
|
|
2616
|
+
logPromotionStorageError(logger, "[promote-file-part] legacy writer returned an invalid result", {
|
|
2617
|
+
path: logicalPath
|
|
2618
|
+
});
|
|
2619
|
+
return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
|
|
2620
|
+
}
|
|
2621
|
+
if (!result2.ok) {
|
|
2622
|
+
const reason = redactErrorMessage(result2.reason);
|
|
2623
|
+
logPromotionStorageError(logger, "[promote-file-part] legacy write rejected", {
|
|
2624
|
+
path: logicalPath,
|
|
2625
|
+
error: reason
|
|
2626
|
+
});
|
|
2627
|
+
return { succeeded: false, filename, reason };
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
succeeded: true,
|
|
2631
|
+
part: {
|
|
2632
|
+
type: kind,
|
|
2633
|
+
path: logicalPath,
|
|
2634
|
+
name: filename,
|
|
2635
|
+
size: resolved.bytes.byteLength,
|
|
2636
|
+
mediaType
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
let ownershipId;
|
|
2641
|
+
let path;
|
|
2642
|
+
try {
|
|
2643
|
+
ownershipId = createWriteId();
|
|
2644
|
+
path = immutableAttachmentPath(logicalPath, ownershipId);
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
logPromotionStorageError(logger, "[promote-file-part] could not allocate ownership key", {
|
|
2647
|
+
path: logicalPath,
|
|
2648
|
+
error: redactErrorMessage(error)
|
|
2649
|
+
});
|
|
2650
|
+
return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
|
|
2651
|
+
}
|
|
2652
|
+
const ownership = { id: ownershipId, path };
|
|
2508
2653
|
let written;
|
|
2509
2654
|
try {
|
|
2510
|
-
written = await options.
|
|
2655
|
+
written = await options.attachmentWriter.write(options.scopeId, path, resolved.bytes, {
|
|
2511
2656
|
mediaType,
|
|
2512
2657
|
name: filename,
|
|
2513
2658
|
originalName: options.raw.filename ?? filename,
|
|
2514
|
-
size: resolved.bytes.byteLength
|
|
2659
|
+
size: resolved.bytes.byteLength,
|
|
2660
|
+
ownership
|
|
2515
2661
|
});
|
|
2516
2662
|
} catch (err) {
|
|
2517
|
-
|
|
2663
|
+
let abortError;
|
|
2664
|
+
try {
|
|
2665
|
+
await options.attachmentWriter.abort(options.scopeId, ownership);
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
abortError = error;
|
|
2668
|
+
}
|
|
2669
|
+
logPromotionStorageError(logger, "[promote-file-part] write failed", {
|
|
2670
|
+
path,
|
|
2671
|
+
ownershipId: ownership.id,
|
|
2672
|
+
error: redactErrorMessage(err),
|
|
2673
|
+
...abortError === void 0 ? {} : { abortError: redactErrorMessage(abortError) }
|
|
2674
|
+
});
|
|
2675
|
+
return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
|
|
2676
|
+
}
|
|
2677
|
+
const result = inspectAtomicAttachmentWriteResult(written, ownership);
|
|
2678
|
+
if (!result) {
|
|
2679
|
+
let abortError;
|
|
2680
|
+
try {
|
|
2681
|
+
await options.attachmentWriter.abort(options.scopeId, ownership);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
abortError = error;
|
|
2684
|
+
}
|
|
2685
|
+
logPromotionStorageError(logger, "[promote-file-part] writer returned a mismatched ownership receipt", {
|
|
2686
|
+
path,
|
|
2687
|
+
ownershipId: ownership.id,
|
|
2688
|
+
...abortError === void 0 ? {} : { abortError: redactErrorMessage(abortError) }
|
|
2689
|
+
});
|
|
2690
|
+
return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
|
|
2691
|
+
}
|
|
2692
|
+
if (!result.ok) {
|
|
2693
|
+
const compensation = await compensatePromotionWrite(
|
|
2694
|
+
options.attachmentWriter,
|
|
2695
|
+
options.scopeId,
|
|
2696
|
+
ownership,
|
|
2697
|
+
result.receipt
|
|
2698
|
+
);
|
|
2699
|
+
logPromotionStorageError(logger, "[promote-file-part] write rejected", {
|
|
2700
|
+
path,
|
|
2701
|
+
ownershipId: ownership.id,
|
|
2702
|
+
error: redactErrorMessage(result.reason),
|
|
2703
|
+
...compensation.rollbackError === void 0 ? {} : { rollbackError: redactErrorMessage(compensation.rollbackError) },
|
|
2704
|
+
...compensation.abortError === void 0 ? {} : { abortError: redactErrorMessage(compensation.abortError) }
|
|
2705
|
+
});
|
|
2706
|
+
return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
|
|
2518
2707
|
}
|
|
2519
|
-
if (!written.ok) return { succeeded: false, filename, reason: written.reason };
|
|
2520
2708
|
return {
|
|
2521
2709
|
succeeded: true,
|
|
2522
2710
|
part: {
|
|
@@ -2530,13 +2718,54 @@ async function promoteAgentFilePart(options) {
|
|
|
2530
2718
|
}
|
|
2531
2719
|
|
|
2532
2720
|
// src/chat-routes/attachment-upload.ts
|
|
2721
|
+
function logAttachmentUploadError(logger, message, fields) {
|
|
2722
|
+
try {
|
|
2723
|
+
logger.error(message, fields);
|
|
2724
|
+
} catch {
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2533
2727
|
function attachmentUploadError(status, code, message, path) {
|
|
2534
2728
|
return Response.json(
|
|
2535
2729
|
{ error: path === void 0 ? { code, message } : { code, message, path } },
|
|
2536
2730
|
{ status }
|
|
2537
2731
|
);
|
|
2538
2732
|
}
|
|
2733
|
+
async function rollbackSuccessfulWrites(writes, scopeId, writer, logger) {
|
|
2734
|
+
let failures = 0;
|
|
2735
|
+
for (let index = writes.length - 1; index >= 0; index -= 1) {
|
|
2736
|
+
const write = writes[index];
|
|
2737
|
+
try {
|
|
2738
|
+
await write.receipt.rollback();
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
failures += 1;
|
|
2741
|
+
logAttachmentUploadError(logger, "[attachment-upload] cleanup failed", {
|
|
2742
|
+
path: write.path,
|
|
2743
|
+
ownershipId: write.ownership.id,
|
|
2744
|
+
error: redactErrorMessage(error)
|
|
2745
|
+
});
|
|
2746
|
+
failures += await abortOwnership(writer, scopeId, write.ownership, logger);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
return failures;
|
|
2750
|
+
}
|
|
2751
|
+
async function abortOwnership(writer, scopeId, ownership, logger) {
|
|
2752
|
+
try {
|
|
2753
|
+
await writer.abort(scopeId, ownership);
|
|
2754
|
+
return 0;
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
logAttachmentUploadError(logger, "[attachment-upload] ambiguous write cleanup failed", {
|
|
2757
|
+
path: ownership.path,
|
|
2758
|
+
ownershipId: ownership.id,
|
|
2759
|
+
error: redactErrorMessage(error)
|
|
2760
|
+
});
|
|
2761
|
+
return 1;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
function isAtomicRouteOptions(options) {
|
|
2765
|
+
return "attachmentWriter" in options;
|
|
2766
|
+
}
|
|
2539
2767
|
function createAttachmentUploadRoute(options) {
|
|
2768
|
+
const atomic = isAtomicRouteOptions(options);
|
|
2540
2769
|
const maxCount = options.limits?.maxCount ?? ATTACHMENT_MAX_COUNT;
|
|
2541
2770
|
const maxBinaryBytes = options.limits?.maxBinaryBytes ?? MAX_BINARY_ATTACHMENT_BYTES;
|
|
2542
2771
|
const maxBytesBySniffedMime = options.limits?.maxBytesBySniffedMime;
|
|
@@ -2547,10 +2776,21 @@ function createAttachmentUploadRoute(options) {
|
|
|
2547
2776
|
const pathFor = options.pathFor ?? ((name) => name);
|
|
2548
2777
|
const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
|
|
2549
2778
|
const sniffMime = options.sniffMime ?? sniffMimeFromName;
|
|
2779
|
+
const createWriteId = atomic ? options.createWriteId ?? (() => {
|
|
2780
|
+
if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") {
|
|
2781
|
+
throw new Error("attachment upload requires crypto.randomUUID");
|
|
2782
|
+
}
|
|
2783
|
+
return crypto.randomUUID();
|
|
2784
|
+
}) : void 0;
|
|
2785
|
+
const logger = options.logger ?? console;
|
|
2550
2786
|
return async function attachmentUpload(request) {
|
|
2551
2787
|
const auth = await options.authorize({ request });
|
|
2552
2788
|
if (!auth.ok) return auth.response;
|
|
2553
|
-
const
|
|
2789
|
+
const atomicWriter = atomic ? auth.attachmentWriter ?? options.attachmentWriter : void 0;
|
|
2790
|
+
const legacyWriter = atomic ? void 0 : auth.writeAttachment ?? options.writeAttachment;
|
|
2791
|
+
if (atomic && !atomicWriter) {
|
|
2792
|
+
return attachmentUploadError(503, "attachment_store_unavailable", ATTACHMENT_STORAGE_FAILURE_MESSAGE);
|
|
2793
|
+
}
|
|
2554
2794
|
let form;
|
|
2555
2795
|
try {
|
|
2556
2796
|
form = await request.formData();
|
|
@@ -2580,7 +2820,7 @@ function createAttachmentUploadRoute(options) {
|
|
|
2580
2820
|
);
|
|
2581
2821
|
}
|
|
2582
2822
|
const prepared = [];
|
|
2583
|
-
const
|
|
2823
|
+
const seenLogicalPaths = /* @__PURE__ */ new Set();
|
|
2584
2824
|
let totalBytes = 0;
|
|
2585
2825
|
for (const file of files) {
|
|
2586
2826
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
@@ -2613,20 +2853,40 @@ function createAttachmentUploadRoute(options) {
|
|
|
2613
2853
|
attachmentSizeErrorMessage(name, bytes.length, limit)
|
|
2614
2854
|
);
|
|
2615
2855
|
}
|
|
2616
|
-
const
|
|
2856
|
+
const logicalPath = pathFor(name);
|
|
2857
|
+
let ownershipId;
|
|
2858
|
+
let path = logicalPath;
|
|
2859
|
+
if (atomic) {
|
|
2860
|
+
try {
|
|
2861
|
+
ownershipId = createWriteId();
|
|
2862
|
+
path = immutableAttachmentPath(logicalPath, ownershipId);
|
|
2863
|
+
} catch (error) {
|
|
2864
|
+
logAttachmentUploadError(logger, "[attachment-upload] could not allocate ownership key", {
|
|
2865
|
+
path: logicalPath,
|
|
2866
|
+
error: redactErrorMessage(error)
|
|
2867
|
+
});
|
|
2868
|
+
return attachmentUploadError(
|
|
2869
|
+
503,
|
|
2870
|
+
"attachment_store_unavailable",
|
|
2871
|
+
ATTACHMENT_STORAGE_FAILURE_MESSAGE,
|
|
2872
|
+
logicalPath
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2617
2876
|
const pathCheck = validatePath(path);
|
|
2618
2877
|
if (!pathCheck.succeeded) {
|
|
2619
2878
|
return attachmentUploadError(400, "invalid_attachment_path", pathCheck.error, path);
|
|
2620
2879
|
}
|
|
2621
|
-
|
|
2880
|
+
const duplicatePath = atomic ? logicalPath : path;
|
|
2881
|
+
if (seenLogicalPaths.has(duplicatePath)) {
|
|
2622
2882
|
return attachmentUploadError(
|
|
2623
2883
|
400,
|
|
2624
2884
|
"attachment_duplicate_path",
|
|
2625
|
-
`attachments must not repeat a path within one upload: ${
|
|
2885
|
+
`attachments must not repeat a path within one upload: ${duplicatePath}`,
|
|
2626
2886
|
path
|
|
2627
2887
|
);
|
|
2628
2888
|
}
|
|
2629
|
-
|
|
2889
|
+
seenLogicalPaths.add(duplicatePath);
|
|
2630
2890
|
totalBytes += bytes.length;
|
|
2631
2891
|
if (totalBytes > maxTotalBytes) {
|
|
2632
2892
|
return attachmentUploadError(
|
|
@@ -2636,19 +2896,123 @@ function createAttachmentUploadRoute(options) {
|
|
|
2636
2896
|
);
|
|
2637
2897
|
}
|
|
2638
2898
|
const mediaType = sniff.mime ?? sniffMime(name);
|
|
2639
|
-
prepared.push({ path, name, bytes, originalName: file.name, size: bytes.length, mediaType, kind });
|
|
2899
|
+
prepared.push({ path, ownershipId, name, bytes, originalName: file.name, size: bytes.length, mediaType, kind });
|
|
2640
2900
|
}
|
|
2641
2901
|
const uploaded = [];
|
|
2902
|
+
const successfulWrites = [];
|
|
2642
2903
|
for (const input of prepared) {
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2904
|
+
if (!atomic) {
|
|
2905
|
+
let written2;
|
|
2906
|
+
try {
|
|
2907
|
+
written2 = await legacyWriter(auth.scopeId, input.path, input.bytes, {
|
|
2908
|
+
mediaType: input.mediaType,
|
|
2909
|
+
name: input.name,
|
|
2910
|
+
originalName: input.originalName,
|
|
2911
|
+
size: input.size
|
|
2912
|
+
});
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
logAttachmentUploadError(logger, "[attachment-upload] legacy writer failed", {
|
|
2915
|
+
path: input.path,
|
|
2916
|
+
error: redactErrorMessage(error)
|
|
2917
|
+
});
|
|
2918
|
+
return attachmentUploadError(
|
|
2919
|
+
503,
|
|
2920
|
+
"attachment_store_unavailable",
|
|
2921
|
+
ATTACHMENT_STORAGE_FAILURE_MESSAGE,
|
|
2922
|
+
input.path
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
const result2 = inspectLegacyAttachmentWriteResult(written2);
|
|
2926
|
+
if (!result2) {
|
|
2927
|
+
logAttachmentUploadError(logger, "[attachment-upload] legacy writer returned an invalid result", {
|
|
2928
|
+
path: input.path
|
|
2929
|
+
});
|
|
2930
|
+
return attachmentUploadError(
|
|
2931
|
+
503,
|
|
2932
|
+
"attachment_store_unavailable",
|
|
2933
|
+
ATTACHMENT_STORAGE_FAILURE_MESSAGE,
|
|
2934
|
+
input.path
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
if (!result2.ok) {
|
|
2938
|
+
const reason = redactErrorMessage(result2.reason);
|
|
2939
|
+
logAttachmentUploadError(logger, "[attachment-upload] legacy write rejected", {
|
|
2940
|
+
path: input.path,
|
|
2941
|
+
error: reason
|
|
2942
|
+
});
|
|
2943
|
+
return attachmentUploadError(413, "attachment_write_failed", reason, input.path);
|
|
2944
|
+
}
|
|
2945
|
+
uploaded.push({
|
|
2946
|
+
path: input.path,
|
|
2947
|
+
name: input.name,
|
|
2948
|
+
size: input.size,
|
|
2949
|
+
mediaType: input.mediaType,
|
|
2950
|
+
kind: input.kind
|
|
2951
|
+
});
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
const ownershipId = input.ownershipId;
|
|
2955
|
+
if (!ownershipId) {
|
|
2956
|
+
return attachmentUploadError(503, "attachment_store_unavailable", ATTACHMENT_STORAGE_FAILURE_MESSAGE, input.path);
|
|
2957
|
+
}
|
|
2958
|
+
const ownership = { id: ownershipId, path: input.path };
|
|
2959
|
+
let written;
|
|
2960
|
+
try {
|
|
2961
|
+
written = await atomicWriter.write(auth.scopeId, input.path, input.bytes, {
|
|
2962
|
+
mediaType: input.mediaType,
|
|
2963
|
+
name: input.name,
|
|
2964
|
+
originalName: input.originalName,
|
|
2965
|
+
size: input.size,
|
|
2966
|
+
ownership
|
|
2967
|
+
});
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
const cleanupFailures = await abortOwnership(atomicWriter, auth.scopeId, ownership, logger) + await rollbackSuccessfulWrites(successfulWrites, auth.scopeId, atomicWriter, logger);
|
|
2970
|
+
logAttachmentUploadError(logger, "[attachment-upload] write failed", {
|
|
2971
|
+
path: input.path,
|
|
2972
|
+
ownershipId: ownership.id,
|
|
2973
|
+
cleanupFailures,
|
|
2974
|
+
error: redactErrorMessage(error)
|
|
2975
|
+
});
|
|
2976
|
+
return attachmentUploadError(
|
|
2977
|
+
503,
|
|
2978
|
+
"attachment_store_unavailable",
|
|
2979
|
+
ATTACHMENT_STORAGE_FAILURE_MESSAGE,
|
|
2980
|
+
input.path
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
const result = inspectAtomicAttachmentWriteResult(written, ownership);
|
|
2984
|
+
if (!result) {
|
|
2985
|
+
const cleanupFailures = await abortOwnership(atomicWriter, auth.scopeId, ownership, logger) + await rollbackSuccessfulWrites(successfulWrites, auth.scopeId, atomicWriter, logger);
|
|
2986
|
+
logAttachmentUploadError(logger, "[attachment-upload] writer returned a mismatched ownership receipt", {
|
|
2987
|
+
path: input.path,
|
|
2988
|
+
ownershipId: ownership.id,
|
|
2989
|
+
cleanupFailures
|
|
2990
|
+
});
|
|
2991
|
+
return attachmentUploadError(503, "attachment_store_unavailable", ATTACHMENT_STORAGE_FAILURE_MESSAGE, input.path);
|
|
2992
|
+
}
|
|
2993
|
+
if (!result.ok) {
|
|
2994
|
+
const cleanupFailures = await rollbackSuccessfulWrites([
|
|
2995
|
+
{ path: input.path, ownership, receipt: result.receipt }
|
|
2996
|
+
], auth.scopeId, atomicWriter, logger) + await rollbackSuccessfulWrites(
|
|
2997
|
+
successfulWrites,
|
|
2998
|
+
auth.scopeId,
|
|
2999
|
+
atomicWriter,
|
|
3000
|
+
logger
|
|
3001
|
+
);
|
|
3002
|
+
logAttachmentUploadError(logger, "[attachment-upload] write rejected", {
|
|
3003
|
+
path: input.path,
|
|
3004
|
+
ownershipId: ownership.id,
|
|
3005
|
+
cleanupFailures,
|
|
3006
|
+
error: redactErrorMessage(result.reason)
|
|
3007
|
+
});
|
|
3008
|
+
return attachmentUploadError(
|
|
3009
|
+
503,
|
|
3010
|
+
"attachment_store_unavailable",
|
|
3011
|
+
ATTACHMENT_STORAGE_FAILURE_MESSAGE,
|
|
3012
|
+
input.path
|
|
3013
|
+
);
|
|
2651
3014
|
}
|
|
3015
|
+
successfulWrites.push({ path: input.path, ownership, receipt: result.receipt });
|
|
2652
3016
|
uploaded.push({
|
|
2653
3017
|
path: input.path,
|
|
2654
3018
|
name: input.name,
|
|
@@ -2881,6 +3245,7 @@ export {
|
|
|
2881
3245
|
ALLOWED_ATTACHMENT_SNIFFED_MIMES,
|
|
2882
3246
|
ATTACHMENT_ACCEPT,
|
|
2883
3247
|
ATTACHMENT_MAX_COUNT,
|
|
3248
|
+
ATTACHMENT_STORAGE_FAILURE_MESSAGE,
|
|
2884
3249
|
ChatTurnInputError,
|
|
2885
3250
|
DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS,
|
|
2886
3251
|
DEFAULT_MODEL_STREAM_OPEN_TIMEOUT_MS,
|
|
@@ -2920,6 +3285,7 @@ export {
|
|
|
2920
3285
|
checkAttachmentType,
|
|
2921
3286
|
classifyTerminalFailure,
|
|
2922
3287
|
createAssistantDraftWriter,
|
|
3288
|
+
createAtomicAttachmentWriter,
|
|
2923
3289
|
createAttachmentUploadRoute,
|
|
2924
3290
|
createChatTurnRoutes,
|
|
2925
3291
|
createSandboxChatProducer,
|
|
@@ -2928,6 +3294,7 @@ export {
|
|
|
2928
3294
|
defaultValidateAttachmentPath,
|
|
2929
3295
|
fileMentionsToParts,
|
|
2930
3296
|
formatBytes,
|
|
3297
|
+
immutableAttachmentPath,
|
|
2931
3298
|
isCommittingSandboxEvent,
|
|
2932
3299
|
isDraftContentEvent,
|
|
2933
3300
|
mediaTypeForMentionPath,
|