@tangle-network/agent-app 0.45.59 → 0.45.61

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.
@@ -50,13 +50,16 @@ export type ReadAttachmentFn = (scopeId: string, path: string) => Promise<Attach
50
50
  * routes create a unique path for each attempt, and the product adapter stores
51
51
  * this id with it. */
52
52
  export interface AttachmentWriteOwnership {
53
- id: string;
54
- path: string;
53
+ readonly id: string;
54
+ readonly path: string;
55
55
  }
56
56
  /** Public-safe outcome for a storage outage. Backend details belong in logs. */
57
57
  export declare const ATTACHMENT_STORAGE_FAILURE_MESSAGE = "Attachment storage is temporarily unavailable. Please try again.";
58
- /** Add an ownership id to a logical path and return an immutable store key.
59
- * Ownership ids are path-safe because the key is also returned to clients. */
58
+ /** Stable client error code when compensating cleanup did not complete. */
59
+ export declare const ATTACHMENT_ROLLBACK_FAILURE_CODE = "rollback_failed";
60
+ /** Public-safe outcome when compensating cleanup did not complete. */
61
+ export declare const ATTACHMENT_ROLLBACK_FAILURE_MESSAGE = "Attachment cleanup failed. Please try again.";
62
+ /** Add a path-safe ownership id to a logical path and return an immutable store key. */
60
63
  export declare function immutableAttachmentPath(logicalPath: string, ownershipId: string): string;
61
64
  /** Compensation for one attachment write. The adapter MUST compare the stored
62
65
  * ownership id before deleting, so an older rollback cannot delete a newer
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  redactErrorMessage
3
- } from "../chunk-CDC5HFKL.js";
3
+ } from "../chunk-MH74DY2I.js";
4
4
  import {
5
5
  createSandboxFileIndexRoute
6
6
  } from "../chunk-QY4BRKRJ.js";
@@ -2267,6 +2267,8 @@ function createUploadRoute(options) {
2267
2267
 
2268
2268
  // src/chat-routes/attachment-store.ts
2269
2269
  var ATTACHMENT_STORAGE_FAILURE_MESSAGE = "Attachment storage is temporarily unavailable. Please try again.";
2270
+ var ATTACHMENT_ROLLBACK_FAILURE_CODE = "rollback_failed";
2271
+ var ATTACHMENT_ROLLBACK_FAILURE_MESSAGE = "Attachment cleanup failed. Please try again.";
2270
2272
  function immutableAttachmentPath(logicalPath, ownershipId) {
2271
2273
  if (!/^[A-Za-z0-9_-]{1,128}$/.test(ownershipId)) {
2272
2274
  throw new Error("attachment ownership id must be a path-safe identifier");
@@ -2462,27 +2464,27 @@ function base64ToBytes(base64) {
2462
2464
  return bytes;
2463
2465
  }
2464
2466
  function parseDataUrl(url) {
2465
- const match = /^data:[^,]*,([\s\S]*)$/.exec(url);
2467
+ const match = /^data:[^,]*,([\s\S]*)$/i.exec(url);
2466
2468
  if (!match) return null;
2467
2469
  return { base64: /;base64,/i.test(url), data: match[1] ?? "" };
2468
2470
  }
2469
2471
  function dataUrlMime(url) {
2470
2472
  if (!url) return void 0;
2471
- const match = /^data:([^;,]+)[;,]/.exec(url);
2473
+ const match = /^data:([^;,]+)[;,]/i.exec(url);
2472
2474
  return match ? match[1] : void 0;
2473
2475
  }
2474
2476
  function basenameFromUrl(url) {
2475
- if (!url || url.startsWith("data:")) return void 0;
2477
+ if (!url || /^data:/i.test(url)) return void 0;
2476
2478
  const withoutQuery = url.split(/[?#]/)[0] ?? url;
2477
2479
  const segments = withoutQuery.split("/").filter(Boolean);
2478
2480
  return segments[segments.length - 1] || void 0;
2479
2481
  }
2480
2482
  function resolveFileUrlPath(url) {
2481
- const withoutScheme = url.startsWith("file://") ? url.slice("file://".length) : url;
2483
+ const withoutScheme = /^file:\/\//i.test(url) ? url.slice("file://".length) : url;
2482
2484
  try {
2483
2485
  return { succeeded: true, path: decodeURIComponent(withoutScheme) };
2484
2486
  } catch (err) {
2485
- return { succeeded: false, reason: `malformed file path: ${err instanceof Error ? err.message : String(err)}` };
2487
+ return { succeeded: false, reason: `malformed file path: ${redactErrorMessage(err)}` };
2486
2488
  }
2487
2489
  }
2488
2490
  function oversizeReason(filename, actual, limit) {
@@ -2495,7 +2497,7 @@ function resolveDataUrlBytes(url, filename, maxBytes) {
2495
2497
  try {
2496
2498
  bytes = parsed.base64 ? base64ToBytes(parsed.data) : new TextEncoder().encode(decodeURIComponent(parsed.data));
2497
2499
  } catch (err) {
2498
- return { succeeded: false, reason: `failed to decode data URI: ${err instanceof Error ? err.message : String(err)}` };
2500
+ return { succeeded: false, reason: `failed to decode data URI: ${redactErrorMessage(err)}` };
2499
2501
  }
2500
2502
  if (bytes.byteLength > maxBytes) {
2501
2503
  return { succeeded: false, reason: oversizeReason(filename, bytes.byteLength, maxBytes) };
@@ -2505,23 +2507,26 @@ function resolveDataUrlBytes(url, filename, maxBytes) {
2505
2507
  async function resolveSandboxFileBytes(input) {
2506
2508
  const stat = await statSandboxFileSize(input.box, input.path, { sessionId: input.sessionId });
2507
2509
  if (!stat.succeeded) {
2508
- return { succeeded: false, reason: `could not stat agent file: ${stat.error}` };
2510
+ return { succeeded: false, reason: `could not stat agent file: ${redactErrorMessage(stat.error)}` };
2509
2511
  }
2510
2512
  if (stat.value > input.maxBytes) {
2511
2513
  return { succeeded: false, reason: oversizeReason(input.filename, stat.value, input.maxBytes) };
2512
2514
  }
2513
2515
  const read = await readSandboxBinaryBytes(input.box, input.path, stat.value, { sessionId: input.sessionId });
2514
2516
  if (!read.succeeded) {
2515
- return { succeeded: false, reason: `could not read agent file: ${read.error}` };
2517
+ return { succeeded: false, reason: `could not read agent file: ${redactErrorMessage(read.error)}` };
2516
2518
  }
2517
2519
  return { succeeded: true, bytes: read.value.bytes };
2518
2520
  }
2519
2521
  async function resolveBytes(input) {
2520
2522
  const url = input.raw.url;
2521
2523
  if (!url) return { succeeded: false, reason: "the file part carries no url" };
2522
- if (url.startsWith("data:")) return resolveDataUrlBytes(url, input.filename, input.maxBytes);
2523
- const isSandboxPath = url.startsWith("file://") || url.startsWith("/");
2524
- if (!isSandboxPath) return { succeeded: false, reason: `unsupported file URL scheme: ${url}` };
2524
+ if (/^data:/i.test(url)) return resolveDataUrlBytes(url, input.filename, input.maxBytes);
2525
+ const isSandboxPath = /^file:\/\//i.test(url) || url.startsWith("/");
2526
+ if (!isSandboxPath) {
2527
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(url)?.[1]?.toLowerCase() ?? "unknown";
2528
+ return { succeeded: false, reason: `unsupported file URL scheme: ${scheme}` };
2529
+ }
2525
2530
  if (!input.box) return { succeeded: false, reason: "no sandbox to read agent file" };
2526
2531
  const resolvedPath = resolveFileUrlPath(url);
2527
2532
  if (!resolvedPath.succeeded) return resolvedPath;
@@ -2545,7 +2550,13 @@ function defaultBuildAttachmentPath(args) {
2545
2550
  }
2546
2551
  function logPromotionStorageError(logger, message, fields) {
2547
2552
  try {
2548
- logger.error(message, fields);
2553
+ const safeFields = Object.fromEntries(
2554
+ Object.entries(fields).map(([key, value]) => [
2555
+ key,
2556
+ typeof value === "string" ? redactErrorMessage(value) : value
2557
+ ])
2558
+ );
2559
+ logger.error(message, safeFields);
2549
2560
  } catch {
2550
2561
  }
2551
2562
  }
@@ -2589,11 +2600,21 @@ async function promoteAgentFilePart(options) {
2589
2600
  maxBytes
2590
2601
  });
2591
2602
  if (!resolved.succeeded) return { succeeded: false, filename, reason: resolved.reason };
2592
- const mediaType = options.raw.mediaType ?? options.raw.mime ?? dataUrlMime(options.raw.url) ?? sniffMime(filename);
2593
- const kind = attachmentKindForMime(mediaType);
2594
- const digest = await hash8(options.raw.id ?? options.raw.url ?? filename);
2595
- const date = now().toISOString().split("T")[0] ?? "";
2596
- const logicalPath = buildAttachmentPath({ filename, hash8: digest, date, mediaType, kind });
2603
+ let mediaType;
2604
+ let kind;
2605
+ let logicalPath;
2606
+ try {
2607
+ mediaType = (options.raw.mediaType ?? options.raw.mime ?? dataUrlMime(options.raw.url) ?? sniffMime(filename)).toLowerCase();
2608
+ kind = attachmentKindForMime(mediaType);
2609
+ const digest = await hash8(options.raw.id ?? options.raw.url ?? filename);
2610
+ const date = now().toISOString().split("T")[0] ?? "";
2611
+ logicalPath = buildAttachmentPath({ filename, hash8: digest, date, mediaType, kind });
2612
+ } catch (error) {
2613
+ logPromotionStorageError(logger, "[promote-file-part] path planning failed", {
2614
+ error: redactErrorMessage(error)
2615
+ });
2616
+ return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
2617
+ }
2597
2618
  if (!atomic) {
2598
2619
  let written2;
2599
2620
  try {
@@ -2649,7 +2670,7 @@ async function promoteAgentFilePart(options) {
2649
2670
  });
2650
2671
  return { succeeded: false, filename, reason: ATTACHMENT_STORAGE_FAILURE_MESSAGE };
2651
2672
  }
2652
- const ownership = { id: ownershipId, path };
2673
+ const ownership = Object.freeze({ id: ownershipId, path });
2653
2674
  let written;
2654
2675
  try {
2655
2676
  written = await options.attachmentWriter.write(options.scopeId, path, resolved.bytes, {
@@ -2720,7 +2741,13 @@ async function promoteAgentFilePart(options) {
2720
2741
  // src/chat-routes/attachment-upload.ts
2721
2742
  function logAttachmentUploadError(logger, message, fields) {
2722
2743
  try {
2723
- logger.error(message, fields);
2744
+ const safeFields = Object.fromEntries(
2745
+ Object.entries(fields).map(([key, value]) => [
2746
+ key,
2747
+ typeof value === "string" ? redactErrorMessage(value) : value
2748
+ ])
2749
+ );
2750
+ logger.error(message, safeFields);
2724
2751
  } catch {
2725
2752
  }
2726
2753
  }
@@ -2730,6 +2757,14 @@ function attachmentUploadError(status, code, message, path) {
2730
2757
  { status }
2731
2758
  );
2732
2759
  }
2760
+ function atomicAttachmentFailure(path, cleanupFailures = 0) {
2761
+ return attachmentUploadError(
2762
+ 503,
2763
+ cleanupFailures > 0 ? ATTACHMENT_ROLLBACK_FAILURE_CODE : "attachment_store_unavailable",
2764
+ cleanupFailures > 0 ? ATTACHMENT_ROLLBACK_FAILURE_MESSAGE : ATTACHMENT_STORAGE_FAILURE_MESSAGE,
2765
+ path
2766
+ );
2767
+ }
2733
2768
  async function rollbackSuccessfulWrites(writes, scopeId, writer, logger) {
2734
2769
  let failures = 0;
2735
2770
  for (let index = writes.length - 1; index >= 0; index -= 1) {
@@ -2955,7 +2990,7 @@ function createAttachmentUploadRoute(options) {
2955
2990
  if (!ownershipId) {
2956
2991
  return attachmentUploadError(503, "attachment_store_unavailable", ATTACHMENT_STORAGE_FAILURE_MESSAGE, input.path);
2957
2992
  }
2958
- const ownership = { id: ownershipId, path: input.path };
2993
+ const ownership = Object.freeze({ id: ownershipId, path: input.path });
2959
2994
  let written;
2960
2995
  try {
2961
2996
  written = await atomicWriter.write(auth.scopeId, input.path, input.bytes, {
@@ -2973,12 +3008,7 @@ function createAttachmentUploadRoute(options) {
2973
3008
  cleanupFailures,
2974
3009
  error: redactErrorMessage(error)
2975
3010
  });
2976
- return attachmentUploadError(
2977
- 503,
2978
- "attachment_store_unavailable",
2979
- ATTACHMENT_STORAGE_FAILURE_MESSAGE,
2980
- input.path
2981
- );
3011
+ return atomicAttachmentFailure(input.path, cleanupFailures);
2982
3012
  }
2983
3013
  const result = inspectAtomicAttachmentWriteResult(written, ownership);
2984
3014
  if (!result) {
@@ -2988,7 +3018,7 @@ function createAttachmentUploadRoute(options) {
2988
3018
  ownershipId: ownership.id,
2989
3019
  cleanupFailures
2990
3020
  });
2991
- return attachmentUploadError(503, "attachment_store_unavailable", ATTACHMENT_STORAGE_FAILURE_MESSAGE, input.path);
3021
+ return atomicAttachmentFailure(input.path, cleanupFailures);
2992
3022
  }
2993
3023
  if (!result.ok) {
2994
3024
  const cleanupFailures = await rollbackSuccessfulWrites([
@@ -3005,12 +3035,7 @@ function createAttachmentUploadRoute(options) {
3005
3035
  cleanupFailures,
3006
3036
  error: redactErrorMessage(result.reason)
3007
3037
  });
3008
- return attachmentUploadError(
3009
- 503,
3010
- "attachment_store_unavailable",
3011
- ATTACHMENT_STORAGE_FAILURE_MESSAGE,
3012
- input.path
3013
- );
3038
+ return atomicAttachmentFailure(input.path, cleanupFailures);
3014
3039
  }
3015
3040
  successfulWrites.push({ path: input.path, ownership, receipt: result.receipt });
3016
3041
  uploaded.push({
@@ -3245,6 +3270,8 @@ export {
3245
3270
  ALLOWED_ATTACHMENT_SNIFFED_MIMES,
3246
3271
  ATTACHMENT_ACCEPT,
3247
3272
  ATTACHMENT_MAX_COUNT,
3273
+ ATTACHMENT_ROLLBACK_FAILURE_CODE,
3274
+ ATTACHMENT_ROLLBACK_FAILURE_MESSAGE,
3248
3275
  ATTACHMENT_STORAGE_FAILURE_MESSAGE,
3249
3276
  ChatTurnInputError,
3250
3277
  DEFAULT_MODEL_FIRST_RESPONSE_TIMEOUT_MS,