viveworker 0.8.5 → 0.8.7

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.
@@ -37,6 +37,7 @@ const {
37
37
  bootstrapPasskeyAccount,
38
38
  completePasskeyAssertion,
39
39
  completePasskeyRegistration,
40
+ listPasskeyDevices,
40
41
  listSupportedChains,
41
42
  requestEmailOtp: requestHazbaseEmailOtp,
42
43
  requestPasskeyAssertionChallenge,
@@ -65,12 +66,21 @@ const REMOTE_PAIRING_RELAY_TOKEN_ROTATION_MS = 30 * 24 * 60 * 60 * 1000;
65
66
  const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
66
67
  const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
67
68
  const DEFAULT_COMPLETION_REPLY_ACK_TIMEOUT_MS = 1200;
69
+ const DEFAULT_COMPLETION_REPLY_THREAD_REOPEN_WAIT_MS = 1800;
68
70
  const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
69
71
  const COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS = 2 * 60 * 1000;
70
72
  const HAZBASE_METADATA_TIMEOUT_MS = 1500;
71
73
  const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
72
74
  const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
73
75
  const TIMELINE_ACTIVITY_TTL_MS = 2 * 60 * 1000;
76
+ const PAYMENT_NETWORKS = {
77
+ base: { family: "evm", chainId: 8453, asset: "usdc", assets: ["usdc"], scheme: "exact", label: "Base", releaseStatus: "comingSoon" },
78
+ "base-sepolia": { family: "evm", chainId: 84532, asset: "usdc", assets: ["usdc"], scheme: "exact", label: "Base Sepolia" },
79
+ polygon: { family: "evm", chainId: 137, asset: "usdc", assets: ["usdc", "jpyc"], scheme: "exact", label: "Polygon", releaseStatus: "comingSoon" },
80
+ "polygon-amoy": { family: "evm", chainId: 80002, asset: "usdc", assets: ["usdc", "jpyc"], scheme: "exact", label: "Polygon Amoy" },
81
+ liquidtestnet: { family: "liquid", chainId: null, asset: "usdt", assets: ["usdt", "lbtc"], scheme: "exact-liquid-pset", label: "Liquid Testnet" },
82
+ liquidv1: { family: "liquid", chainId: null, asset: "usdt", assets: ["usdt", "lbtc"], scheme: "exact-liquid-pset", label: "Liquid", releaseStatus: "comingSoon" },
83
+ };
74
84
 
75
85
  // Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
76
86
  // per tracked repo (`git diff --name-status`, `git status --porcelain`,
@@ -806,7 +816,7 @@ function localizedMcpApprovalPushContent(locale, approval) {
806
816
 
807
817
  function localizedPaymentApprovalPushContent(locale, approval) {
808
818
  const payment = isPlainObject(approval?.rawParams) ? approval.rawParams : {};
809
- const amount = cleanText(payment.amountUsdc || payment.amountAtomic || "");
819
+ const amount = cleanText(`${payment.amountUsdc || payment.amountAtomic || ""} ${payment.assetLabel || ""}`.trim());
810
820
  const network = cleanText(payment.network || "");
811
821
  const resource = cleanText(payment.resource || payment.url || "");
812
822
  const titleKey = cleanText(approval?.kind || "") === "hazbase_wallet_payment"
@@ -1540,11 +1550,19 @@ function normalizeTrustedReadTokens(tokens) {
1540
1550
  }
1541
1551
  if (strippedTokens.some((token) => {
1542
1552
  const value = String(token || "");
1543
- return (
1544
- ["||", "&&", ";", "&"].includes(value) ||
1545
- value.includes("`") ||
1546
- value.includes("$(")
1547
- );
1553
+ if (value === "|") {
1554
+ // A standalone pipe is the only operator allowed here; the pipe-stage
1555
+ // split below validates that each downstream stage is an allow-listed
1556
+ // post-processor (head/tail/wc).
1557
+ return false;
1558
+ }
1559
+ // Reject any token that *contains* a command separator or command
1560
+ // substitution, not just tokens that equal one. The whitespace tokenizer
1561
+ // keeps separators attached to adjacent text (e.g. "README.md;id" or
1562
+ // "README.md|sh"), so a whole-token equality check let the shell run an
1563
+ // injected second command the classifier never saw. A substring check on
1564
+ // ; & | and backtick (plus "$(") closes that gap.
1565
+ return /[;&|`]/u.test(value) || value.includes("$(");
1548
1566
  })) {
1549
1567
  return null;
1550
1568
  }
@@ -1578,6 +1596,14 @@ function evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot }) {
1578
1596
  const normalizedCwd = cleanText(cwd || "");
1579
1597
  const normalizedWorkspaceRoot = cleanText(workspaceRoot || normalizedCwd);
1580
1598
  const normalizedCommandText = unwrapShellCommand(commandText);
1599
+ // A literal newline or carriage return is a shell command separator, but
1600
+ // tokenizeShellWords() treats it as ordinary whitespace — so an injected
1601
+ // second command (e.g. "rg foo\ntouch evil") would be split into harmless
1602
+ // looking word tokens and misclassified as a single safe read. Refuse to
1603
+ // auto-classify anything carrying these so it falls back to manual approval.
1604
+ if (/[\n\r]/u.test(normalizedCommandText)) {
1605
+ return null;
1606
+ }
1581
1607
  const rawTokens = tokenizeShellWords(normalizedCommandText);
1582
1608
  if (!normalizedCommandText || !normalizedCwd || !normalizedWorkspaceRoot || rawTokens.length === 0) {
1583
1609
  return null;
@@ -7962,7 +7988,7 @@ function createX402PaymentApproval({ config, body, now = Date.now() }) {
7962
7988
  const requestId = cleanText(body.paymentRequestId || body.requestId || crypto.randomUUID());
7963
7989
  const requestKey = `payment:${requestId}`;
7964
7990
  const title = payment.amountUsdc
7965
- ? `Payment approval — ${payment.amountUsdc} USDC`
7991
+ ? `Payment approval — ${payment.amountUsdc} ${payment.assetLabel || "USDC"}`
7966
7992
  : "Payment approval";
7967
7993
  return {
7968
7994
  token,
@@ -8001,7 +8027,7 @@ function createHazbaseWalletPaymentApproval({ config, body, now = Date.now() })
8001
8027
  const token = crypto.randomBytes(18).toString("hex");
8002
8028
  const requestKey = `hazbase_wallet_payment:${paymentRequestId}`;
8003
8029
  const title = payment.amountUsdc
8004
- ? `Hazbase wallet payment — ${payment.amountUsdc} USDC`
8030
+ ? `Hazbase wallet payment — ${payment.amountUsdc} ${payment.assetLabel || "USDC"}`
8005
8031
  : "Hazbase wallet payment";
8006
8032
  return {
8007
8033
  token,
@@ -8036,7 +8062,7 @@ function formatHazbaseWalletPaymentApprovalMessage(payment) {
8036
8062
  const lines = [
8037
8063
  "Hazbase Smart Wallet payment requested.",
8038
8064
  "",
8039
- `Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
8065
+ `Amount: ${payment.amountUsdc || payment.amountAtomic} ${payment.assetLabel || "USDC"}`,
8040
8066
  `Network: ${payment.network} (chainId ${payment.chainId})`,
8041
8067
  `Pay to: ${payment.payTo}`,
8042
8068
  `Asset: ${payment.asset}`,
@@ -8053,15 +8079,16 @@ function normalizeX402PaymentApprovalBody(body) {
8053
8079
  if (!isPlainObject(body)) return null;
8054
8080
  const payment = isPlainObject(body.payment) ? body.payment : {};
8055
8081
  const network = cleanText(payment.network || "");
8056
- const chainId = Number(payment.chainId) || 0;
8082
+ const chainId = payment.chainId === null ? null : Number(payment.chainId) || 0;
8057
8083
  const amountAtomic = cleanText(payment.amountAtomic || "");
8058
- const amountUsdc = cleanText(payment.amountUsdc || "");
8084
+ const amountUsdc = cleanText(payment.amountUsdc || payment.amount || "");
8085
+ const assetLabel = cleanText(payment.assetLabel || "");
8059
8086
  const payTo = cleanText(payment.payTo || "");
8060
8087
  const asset = cleanText(payment.asset || "");
8061
8088
  const resource = cleanText(payment.resource || body.url || "");
8062
8089
  const description = cleanText(payment.description || "");
8063
8090
  const url = cleanText(body.url || resource);
8064
- if (!network || !chainId || !amountAtomic || !payTo || !asset || !resource) {
8091
+ if (!network || (chainId !== null && !chainId) || !amountAtomic || !payTo || !asset || !resource) {
8065
8092
  return null;
8066
8093
  }
8067
8094
  return {
@@ -8070,6 +8097,7 @@ function normalizeX402PaymentApprovalBody(body) {
8070
8097
  chainId,
8071
8098
  amountAtomic,
8072
8099
  amountUsdc,
8100
+ assetLabel,
8073
8101
  payTo,
8074
8102
  asset,
8075
8103
  resource,
@@ -8081,8 +8109,8 @@ function formatX402PaymentApprovalMessage(payment) {
8081
8109
  const lines = [
8082
8110
  "x402 payment approval requested.",
8083
8111
  "",
8084
- `Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
8085
- `Network: ${payment.network} (chainId ${payment.chainId})`,
8112
+ `Amount: ${payment.amountUsdc || payment.amountAtomic} ${payment.assetLabel || "USDC"}`,
8113
+ `Network: ${payment.network}${payment.chainId === null ? "" : ` (chainId ${payment.chainId})`}`,
8086
8114
  `Pay to: ${payment.payTo}`,
8087
8115
  `Asset: ${payment.asset}`,
8088
8116
  `Resource: ${payment.resource}`,
@@ -9640,6 +9668,50 @@ function isIpcNoClientFoundError(errorValue) {
9640
9668
  message.includes("client not found");
9641
9669
  }
9642
9670
 
9671
+ function codexThreadDeepLink(conversationId) {
9672
+ const id = cleanText(conversationId || "");
9673
+ if (!id || !/^[0-9a-z-]{16,}$/iu.test(id)) {
9674
+ return "";
9675
+ }
9676
+ return `codex://local/${encodeURIComponent(id)}`;
9677
+ }
9678
+
9679
+ function openCodexThreadBestEffort(conversationId) {
9680
+ if (process.platform !== "darwin") {
9681
+ return false;
9682
+ }
9683
+ const url = codexThreadDeepLink(conversationId);
9684
+ if (!url) {
9685
+ return false;
9686
+ }
9687
+ try {
9688
+ const child = spawn("open", [url], {
9689
+ detached: true,
9690
+ stdio: "ignore",
9691
+ });
9692
+ child.unref?.();
9693
+ console.log(`[completion-reply] reopened Codex thread via ${url}`);
9694
+ return true;
9695
+ } catch (error) {
9696
+ console.warn(
9697
+ `[completion-reply] failed to reopen Codex thread=${cleanText(conversationId || "") || "unknown"} ` +
9698
+ `error=${cleanText(error?.message || error || "") || "unknown"}`
9699
+ );
9700
+ return false;
9701
+ }
9702
+ }
9703
+
9704
+ async function waitForCodexThreadOwner(runtime, conversationId, timeoutMs) {
9705
+ const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
9706
+ while (Date.now() <= deadline) {
9707
+ if (runtime.threadOwnerClientIds.has(conversationId)) {
9708
+ return true;
9709
+ }
9710
+ await sleep(100);
9711
+ }
9712
+ return false;
9713
+ }
9714
+
9643
9715
  function buildDefaultCollaborationMode(threadState) {
9644
9716
  // Fallback turns must leave Plan mode unless the caller explicitly opts in.
9645
9717
  return buildRequestedCollaborationMode(threadState, "default");
@@ -10431,6 +10503,45 @@ function isHiddenCodexApprovalAssessmentText(text) {
10431
10503
  return /\bThe following is the Codex agent history(?: added since your last approval assessment)?\b/iu.test(single);
10432
10504
  }
10433
10505
 
10506
+ function isHiddenCodexApprovalDecisionJsonText(text) {
10507
+ const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
10508
+ if (!single || !single.startsWith("{") || !single.endsWith("}")) {
10509
+ return false;
10510
+ }
10511
+
10512
+ const parsed = safeJsonParse(single);
10513
+ if (!isPlainObject(parsed)) {
10514
+ return false;
10515
+ }
10516
+
10517
+ const allowedKeys = new Set(["risk_level", "user_authorization", "outcome", "rationale"]);
10518
+ const keys = Object.keys(parsed);
10519
+ if (!keys.includes("outcome") || keys.some((key) => !allowedKeys.has(key))) {
10520
+ return false;
10521
+ }
10522
+
10523
+ const outcome = cleanText(parsed.outcome || "");
10524
+ if (outcome !== "allow" && outcome !== "deny") {
10525
+ return false;
10526
+ }
10527
+
10528
+ const riskLevel = cleanText(parsed.risk_level || "");
10529
+ if (riskLevel && !["low", "medium", "high", "critical"].includes(riskLevel)) {
10530
+ return false;
10531
+ }
10532
+
10533
+ const userAuthorization = cleanText(parsed.user_authorization || "");
10534
+ if (userAuthorization && !["unknown", "low", "medium", "high"].includes(userAuthorization)) {
10535
+ return false;
10536
+ }
10537
+
10538
+ if (parsed.rationale != null && typeof parsed.rationale !== "string") {
10539
+ return false;
10540
+ }
10541
+
10542
+ return keys.length === 1 || keys.some((key) => key === "risk_level" || key === "user_authorization" || key === "rationale");
10543
+ }
10544
+
10434
10545
  function shouldHideInternalTimelineItem(item) {
10435
10546
  return shouldHideClaudeInternalItem(item) || shouldHideCodexInternalApprovalItem(item) || shouldHideMoltbookHarnessItem(item);
10436
10547
  }
@@ -10508,14 +10619,23 @@ function shouldHideCodexInternalApprovalItem(item) {
10508
10619
 
10509
10620
  const title = cleanText(item.title ?? "");
10510
10621
  const threadLabel = cleanText(item.threadLabel ?? "");
10511
- if (isHiddenCodexApprovalAssessmentText(title) || isHiddenCodexApprovalAssessmentText(threadLabel)) {
10622
+ if (
10623
+ isHiddenCodexApprovalAssessmentText(title) ||
10624
+ isHiddenCodexApprovalAssessmentText(threadLabel) ||
10625
+ isHiddenCodexApprovalDecisionJsonText(title) ||
10626
+ isHiddenCodexApprovalDecisionJsonText(threadLabel)
10627
+ ) {
10512
10628
  return true;
10513
10629
  }
10514
10630
  return (
10515
10631
  isHiddenCodexApprovalAssessmentText(item.messageText) ||
10516
10632
  isHiddenCodexApprovalAssessmentText(item.summary) ||
10517
10633
  isHiddenCodexApprovalAssessmentText(item.detailText) ||
10518
- isHiddenCodexApprovalAssessmentText(item.message)
10634
+ isHiddenCodexApprovalAssessmentText(item.message) ||
10635
+ isHiddenCodexApprovalDecisionJsonText(item.messageText) ||
10636
+ isHiddenCodexApprovalDecisionJsonText(item.summary) ||
10637
+ isHiddenCodexApprovalDecisionJsonText(item.detailText) ||
10638
+ isHiddenCodexApprovalDecisionJsonText(item.message)
10519
10639
  );
10520
10640
  }
10521
10641
 
@@ -14344,7 +14464,6 @@ async function handleCompletionReply({
14344
14464
  stagedWorkspaceImagePaths
14345
14465
  );
14346
14466
  let lastError = null;
14347
- const ownerClientId = runtime.threadOwnerClientIds.get(conversationId) ?? null;
14348
14467
  const finalizeReplyAccepted = async () => {
14349
14468
  if (timelineImageAliases.length > 0) {
14350
14469
  const aliases = isPlainObject(state.timelineImagePathAliases)
@@ -14357,46 +14476,76 @@ async function handleCompletionReply({
14357
14476
  }
14358
14477
  scheduleBestEffortFileCleanup(stagedWorkspaceImagePaths);
14359
14478
  };
14479
+ const sendReplyCandidate = async (candidate, attemptLabel = "initial") => {
14480
+ const currentOwnerClientId = runtime.threadOwnerClientIds.get(conversationId) ?? null;
14481
+ console.log(
14482
+ `[completion-reply] try candidate=${candidate.name} attempt=${attemptLabel} transport=${cleanText(candidate.transport || "thread-follower")} owner=${cleanText(currentOwnerClientId || "") || "none"} images=${normalizedLocalImagePaths.length} workspaceImages=${stagedWorkspaceImagePaths.length} cwd=${cleanText(resolvedCwd || "") || "none"}`
14483
+ );
14484
+ if (candidate.transport === "direct-turn-start" && currentOwnerClientId) {
14485
+ return runtime.ipcClient.startTurnDirect(
14486
+ conversationId,
14487
+ candidate.turnStartParams,
14488
+ currentOwnerClientId,
14489
+ { timeoutMs: config.completionReplyAckTimeoutMs }
14490
+ );
14491
+ }
14492
+ return runtime.ipcClient.startTurn(
14493
+ conversationId,
14494
+ candidate.turnStartParams,
14495
+ currentOwnerClientId,
14496
+ { timeoutMs: config.completionReplyAckTimeoutMs }
14497
+ );
14498
+ };
14499
+ let reopenedCodexThread = false;
14360
14500
 
14361
14501
  for (const candidate of turnCandidates) {
14362
14502
  try {
14363
- console.log(
14364
- `[completion-reply] try candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} owner=${cleanText(ownerClientId || "") || "none"} images=${normalizedLocalImagePaths.length} workspaceImages=${stagedWorkspaceImagePaths.length} cwd=${cleanText(resolvedCwd || "") || "none"}`
14365
- );
14366
- if (candidate.transport === "direct-turn-start" && ownerClientId) {
14367
- await runtime.ipcClient.startTurnDirect(
14368
- conversationId,
14369
- candidate.turnStartParams,
14370
- ownerClientId,
14371
- { timeoutMs: config.completionReplyAckTimeoutMs }
14372
- );
14373
- } else {
14374
- await runtime.ipcClient.startTurn(
14375
- conversationId,
14376
- candidate.turnStartParams,
14377
- ownerClientId,
14378
- { timeoutMs: config.completionReplyAckTimeoutMs }
14379
- );
14380
- }
14503
+ await sendReplyCandidate(candidate);
14381
14504
  console.log(
14382
14505
  `[completion-reply] success at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
14383
14506
  );
14384
14507
  await finalizeReplyAccepted();
14385
14508
  return { alreadyAccepted: false };
14386
14509
  } catch (error) {
14387
- if (isStartTurnAckTimeout(error)) {
14510
+ let candidateError = error;
14511
+ if (isIpcNoClientFoundError(candidateError) && !reopenedCodexThread) {
14512
+ reopenedCodexThread = true;
14513
+ runtime.threadOwnerClientIds.delete(conversationId);
14514
+ const reopened = openCodexThreadBestEffort(conversationId);
14515
+ if (reopened) {
14516
+ const ownerReady = await waitForCodexThreadOwner(
14517
+ runtime,
14518
+ conversationId,
14519
+ config.completionReplyThreadReopenWaitMs
14520
+ );
14521
+ console.log(
14522
+ `[completion-reply] retry after Codex thread reopen candidate=${candidate.name} ownerReady=${ownerReady ? 1 : 0}`
14523
+ );
14524
+ try {
14525
+ await sendReplyCandidate(candidate, "after-reopen");
14526
+ console.log(
14527
+ `[completion-reply] success at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} reopened=1`
14528
+ );
14529
+ await finalizeReplyAccepted();
14530
+ return { alreadyAccepted: false, reopenedCodexThread: true };
14531
+ } catch (retryError) {
14532
+ candidateError = retryError;
14533
+ }
14534
+ }
14535
+ }
14536
+ if (isStartTurnAckTimeout(candidateError)) {
14388
14537
  // Codex can create the turn but fail to send the bridge an ACK before
14389
14538
  // its internal follower timeout fires. Retrying risks duplicate user
14390
14539
  // turns, so treat this as accepted and let the timeline catch up.
14391
14540
  console.log(
14392
- `[completion-reply] accepted at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} ack-timeout=${normalizeIpcErrorMessage(error)}`
14541
+ `[completion-reply] accepted at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} ack-timeout=${normalizeIpcErrorMessage(candidateError)}`
14393
14542
  );
14394
14543
  await finalizeReplyAccepted();
14395
14544
  return { alreadyAccepted: false, ackTimeout: true };
14396
14545
  }
14397
- lastError = error;
14546
+ lastError = candidateError;
14398
14547
  console.log(
14399
- `[completion-reply] failed candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} error=${normalizeIpcErrorMessage(error)} raw=${inspect(error?.ipcError ?? error, { depth: 6, breakLength: 160 })}`
14548
+ `[completion-reply] failed candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} error=${normalizeIpcErrorMessage(candidateError)} raw=${inspect(candidateError?.ipcError ?? candidateError, { depth: 6, breakLength: 160 })}`
14400
14549
  );
14401
14550
  }
14402
14551
  }
@@ -15572,7 +15721,7 @@ function resolveManifestPairingToken({ config, state, requestedToken }) {
15572
15721
  if (!isPairingAvailable(config)) {
15573
15722
  return "";
15574
15723
  }
15575
- return cleanText(config.pairingToken) === token ? token : "";
15724
+ return constantTimeStringEqual(cleanText(config.pairingToken), token) ? token : "";
15576
15725
  }
15577
15726
 
15578
15727
  function buildWebManifest({ pairToken }) {
@@ -15607,7 +15756,7 @@ function buildWebManifest({ pairToken }) {
15607
15756
  );
15608
15757
  }
15609
15758
 
15610
- function buildWebAppHtml({ pairToken }) {
15759
+ function buildWebAppHtml({ pairToken, nonce }) {
15611
15760
  const manifestHref = pairToken
15612
15761
  ? `/manifest.webmanifest?pairToken=${encodeURIComponent(pairToken)}`
15613
15762
  : "/manifest.webmanifest";
@@ -15730,7 +15879,7 @@ function buildWebAppHtml({ pairToken }) {
15730
15879
  </div>
15731
15880
  </div>
15732
15881
  <div id="app"></div>
15733
- <script>
15882
+ <script nonce="${nonce}">
15734
15883
  (() => {
15735
15884
  const isJa = (navigator.language || "").toLowerCase().startsWith("ja");
15736
15885
  const message = isJa ? "同じWi-Fi内のPCを確認中..." : "Checking your trusted Wi-Fi...";
@@ -16015,10 +16164,22 @@ function createNativeApprovalServer({ config, runtime, state }) {
16015
16164
  state,
16016
16165
  requestedToken: url.searchParams.get("pairToken"),
16017
16166
  });
16167
+ const cspNonce = crypto.randomBytes(16).toString("base64url");
16018
16168
  res.statusCode = 200;
16019
16169
  res.setHeader("Content-Type", "text/html; charset=utf-8");
16020
16170
  res.setHeader("Cache-Control", "no-store, max-age=0");
16021
- res.end(buildWebAppHtml({ pairToken }));
16171
+ // Defense-in-depth against XSS in the control-plane PWA: this document
16172
+ // renders server-built HTML that can contain untrusted agent content,
16173
+ // so lock script execution to same-origin plus the single nonce'd inline
16174
+ // boot script. Without 'unsafe-inline', an injected <script>, onerror=
16175
+ // handler, or javascript: URL cannot run even if it slips past the
16176
+ // markdown sanitizer. img/style/connect are intentionally left to the
16177
+ // browser default so remote connection, wallet, and images keep working.
16178
+ res.setHeader(
16179
+ "Content-Security-Policy",
16180
+ `script-src 'self' 'nonce-${cspNonce}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'`
16181
+ );
16182
+ res.end(buildWebAppHtml({ pairToken, nonce: cspNonce }));
16022
16183
  return;
16023
16184
  }
16024
16185
 
@@ -16149,7 +16310,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
16149
16310
  const apiA2ATaskStatus = url.pathname.match(/^\/api\/providers\/a2a\/tasks\/([^/]+)\/status$/u);
16150
16311
  if (apiA2ATaskStatus && req.method === "GET") {
16151
16312
  const apiKey = req.headers["x-a2a-key"] || "";
16152
- if (!config.a2aApiKey || apiKey !== config.a2aApiKey) {
16313
+ if (!config.a2aApiKey || !constantTimeStringEqual(apiKey, config.a2aApiKey)) {
16153
16314
  return writeJson(res, 401, { error: "unauthorized" });
16154
16315
  }
16155
16316
  const token = decodeURIComponent(apiA2ATaskStatus[1]);
@@ -16618,7 +16779,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
16618
16779
 
16619
16780
  if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
16620
16781
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
16621
- const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
16782
+ const hookAuth = config.sessionSecret && constantTimeStringEqual(hookSecret, config.sessionSecret);
16622
16783
  if (!hookAuth) {
16623
16784
  const session = requireApiSession(req, res, config, state);
16624
16785
  if (!session) return;
@@ -16627,12 +16788,20 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
16627
16788
  return writeJson(res, 200, { enabled: false });
16628
16789
  }
16629
16790
  const hazbase = normalizeHazbaseState(state.hazbase);
16630
- const [paymentsResult, chainsResult] = await Promise.allSettled([
16791
+ const localPasskeyRegistered = Boolean(hazbase.deviceBindingId || hazbase.credentialId);
16792
+ const devicesPromise = hazbase.accessToken && !hazbase.sessionInvalid && !localPasskeyRegistered
16793
+ ? listPasskeyDevices({ emailSession: hazbase.accessToken })
16794
+ : Promise.resolve(null);
16795
+ const [paymentsResult, chainsResult, devicesResult] = await Promise.allSettled([
16631
16796
  fetchHazbaseMetadata(config, "/api/meta/payments"),
16632
16797
  fetchHazbaseMetadata(config, "/api/meta/chains"),
16798
+ devicesPromise,
16633
16799
  ]);
16634
16800
  const payments = paymentsResult.status === "fulfilled" ? paymentsResult.value : null;
16635
16801
  const chains = chainsResult.status === "fulfilled" ? chainsResult.value : null;
16802
+ const passkeyDevices = devicesResult.status === "fulfilled" && Array.isArray(devicesResult.value?.devices)
16803
+ ? devicesResult.value.devices
16804
+ : [];
16636
16805
  const errors = [];
16637
16806
  if (paymentsResult.status === "rejected") {
16638
16807
  errors.push(paymentsResult.reason?.message || String(paymentsResult.reason || "payments metadata unavailable"));
@@ -16640,13 +16809,31 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
16640
16809
  if (chainsResult.status === "rejected") {
16641
16810
  errors.push(chainsResult.reason?.message || String(chainsResult.reason || "chains metadata unavailable"));
16642
16811
  }
16812
+ if (devicesResult.status === "rejected") {
16813
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error: devicesResult.reason, config, state, res })) return;
16814
+ errors.push(devicesResult.reason?.message || String(devicesResult.reason || "passkey devices unavailable"));
16815
+ }
16643
16816
  const supportedChains = Array.isArray(chains?.chains)
16644
- ? chains.chains.filter((entry) => Number(entry.chainId) === 8453 || Number(entry.chainId) === 84532)
16817
+ ? chains.chains.filter((entry) => Boolean(paymentNetworkForChainId(Number(entry.chainId))))
16818
+ .filter((entry) => isPaymentNetworkAvailable(paymentNetworkForChainId(Number(entry.chainId))))
16645
16819
  : [];
16646
16820
  const payoutAddresses = {
16647
- 8453: resolveHazbaseAccountForChain(hazbase.accounts, 8453)?.smartAccountAddress || "",
16648
16821
  84532: resolveHazbaseAccountForChain(hazbase.accounts, 84532)?.smartAccountAddress || "",
16822
+ 80002: resolveHazbaseAccountForChain(hazbase.accounts, 80002)?.smartAccountAddress || "",
16649
16823
  };
16824
+ const rawPaymentCapabilities = buildPaymentCapabilities({ hazbase, payments });
16825
+ const paymentCapabilities = applyAgentPaymentDefaults(rawPaymentCapabilities, hazbase.agentPaymentDefaults);
16826
+ const agentPaymentDefaults = agentPaymentDefaultsStatus(hazbase.agentPaymentDefaults, rawPaymentCapabilities);
16827
+ const supportedPayments = (Array.isArray(payments?.networks) ? payments.networks : [])
16828
+ .filter((entry) => isPaymentNetworkAvailable(normalizePaymentNetworkKey(entry?.network || entry?.id || "")));
16829
+ const defaultPaymentNetwork = isPaymentNetworkAvailable(normalizePaymentNetworkKey(payments?.defaultNetwork || ""))
16830
+ ? payments.defaultNetwork
16831
+ : "base-sepolia";
16832
+ for (const capability of paymentCapabilities) {
16833
+ if (capability.payTo) {
16834
+ payoutAddresses[capability.network] = capability.payTo;
16835
+ }
16836
+ }
16650
16837
  const signedIn = Boolean(hazbase.accessToken) && !hazbase.sessionInvalid;
16651
16838
  return writeJson(res, 200, {
16652
16839
  enabled: true,
@@ -16661,11 +16848,15 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
16661
16848
  sessionId: hazbase.sessionId,
16662
16849
  deviceBindingId: hazbase.deviceBindingId,
16663
16850
  credentialId: hazbase.credentialId,
16851
+ passkeyRegistered: Boolean(localPasskeyRegistered || passkeyDevices.length),
16852
+ passkeyDeviceCount: passkeyDevices.length,
16664
16853
  highTrustExpiresAt: hazbase.highTrustExpiresAt,
16665
16854
  accounts: hazbase.accounts,
16666
- supportedPayments: payments?.networks || [],
16855
+ paymentCapabilities,
16856
+ agentPaymentDefaults,
16857
+ supportedPayments,
16667
16858
  supportedChains,
16668
- defaultPaymentNetwork: payments?.defaultNetwork || "base-sepolia",
16859
+ defaultPaymentNetwork,
16669
16860
  payoutAddresses,
16670
16861
  error: errors.join(" | "),
16671
16862
  });
@@ -16673,23 +16864,55 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
16673
16864
 
16674
16865
  if (url.pathname === "/api/hazbase/payout-address" && req.method === "GET") {
16675
16866
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
16676
- const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
16867
+ const hookAuth = config.sessionSecret && constantTimeStringEqual(hookSecret, config.sessionSecret);
16677
16868
  if (!hookAuth) {
16678
16869
  const session = requireApiSession(req, res, config, state);
16679
16870
  if (!session) return;
16680
16871
  }
16681
16872
  const hazbase = normalizeHazbaseState(state.hazbase);
16682
16873
  if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
16683
- const chainId = Number(url.searchParams.get("chainId") || 0);
16684
- if (chainId !== 8453 && chainId !== 84532) {
16874
+ const networkParam = normalizePaymentNetworkKey(url.searchParams.get("network") || "");
16875
+ if (networkParam && PAYMENT_NETWORKS[networkParam]?.family === "liquid") {
16876
+ if (!isPaymentNetworkAvailable(networkParam)) {
16877
+ return writeJson(res, 409, { error: "payment-network-coming-soon" });
16878
+ }
16879
+ const capability = resolvePaymentCapability(
16880
+ buildPaymentCapabilities({ hazbase, payments: null }),
16881
+ networkParam,
16882
+ url.searchParams.get("asset") || "usdt",
16883
+ );
16884
+ if (!capability?.payTo) return writeJson(res, 409, { error: "hazbase-wallet-account-missing" });
16885
+ return writeJson(res, 200, {
16886
+ chainId: null,
16887
+ network: networkParam,
16888
+ asset: capability.asset,
16889
+ scheme: capability.scheme,
16890
+ payoutMethod: "external_liquid",
16891
+ payoutAddress: capability.payTo,
16892
+ payTo: capability.payTo,
16893
+ issuer: capability.issuer || "external",
16894
+ });
16895
+ }
16896
+ const chainId = networkParam && PAYMENT_NETWORKS[networkParam]?.family === "evm"
16897
+ ? Number(PAYMENT_NETWORKS[networkParam].chainId || 0)
16898
+ : Number(url.searchParams.get("chainId") || 0);
16899
+ if (!paymentNetworkForChainId(chainId)) {
16685
16900
  return writeJson(res, 400, { error: "unsupported-chain" });
16686
16901
  }
16902
+ if (!isPaymentNetworkAvailable(paymentNetworkForChainId(chainId))) {
16903
+ return writeJson(res, 409, { error: "payment-network-coming-soon" });
16904
+ }
16905
+ const evmNetwork = paymentNetworkForChainId(chainId);
16906
+ const asset = normalizePaymentAssetKey(url.searchParams.get("asset") || PAYMENT_NETWORKS[evmNetwork]?.asset, evmNetwork);
16907
+ if (!asset) return writeJson(res, 400, { error: "unsupported-payment-asset" });
16687
16908
  const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
16688
16909
  if (!account?.smartAccountAddress) {
16689
16910
  return writeJson(res, 409, { error: "hazbase-wallet-account-missing" });
16690
16911
  }
16691
16912
  return writeJson(res, 200, {
16692
16913
  chainId,
16914
+ network: evmNetwork,
16915
+ asset,
16693
16916
  payoutMethod: "hazbase_wallet",
16694
16917
  payoutAddress: account.smartAccountAddress,
16695
16918
  smartAccountAddress: account.smartAccountAddress,
@@ -16867,9 +17090,12 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
16867
17090
  }
16868
17091
  const body = await parseJsonBody(req);
16869
17092
  const chainId = Number(body?.chainId || 0);
16870
- if (chainId !== 8453 && chainId !== 84532) {
17093
+ if (!paymentNetworkForChainId(chainId)) {
16871
17094
  return writeJson(res, 400, { error: "unsupported-chain" });
16872
17095
  }
17096
+ if (!isPaymentNetworkAvailable(paymentNetworkForChainId(chainId))) {
17097
+ return writeJson(res, 409, { error: "payment-network-coming-soon" });
17098
+ }
16873
17099
  let result;
16874
17100
  try {
16875
17101
  result = await bootstrapPasskeyAccount({
@@ -16900,9 +17126,91 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
16900
17126
  return writeJson(res, 200, result);
16901
17127
  }
16902
17128
 
17129
+ if (url.pathname === "/api/hazbase/payment-capability" && req.method === "POST") {
17130
+ const session = requireMutatingApiSession(req, res, config, state);
17131
+ if (!session) return;
17132
+ const hazbase = normalizeHazbaseState(state.hazbase);
17133
+ if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
17134
+ const body = await parseJsonBody(req);
17135
+ const network = normalizePaymentNetworkKey(body?.network);
17136
+ if (!network || PAYMENT_NETWORKS[network]?.family !== "liquid") {
17137
+ return writeJson(res, 400, { error: "unsupported-payment-network" });
17138
+ }
17139
+ if (!isPaymentNetworkAvailable(network)) {
17140
+ return writeJson(res, 409, { error: "payment-network-coming-soon" });
17141
+ }
17142
+ const asset = normalizePaymentAssetKey(body?.asset || "usdt", network);
17143
+ if (!asset) return writeJson(res, 400, { error: "unsupported-payment-asset" });
17144
+ const payTo = cleanText(body?.payTo || body?.payoutAddress || "").toLowerCase();
17145
+ if (!isValidLiquidAddressForNetwork(payTo, network)) {
17146
+ return writeJson(res, 400, { error: "invalid-liquid-address" });
17147
+ }
17148
+ const nextCapability = {
17149
+ network,
17150
+ asset,
17151
+ scheme: PAYMENT_NETWORKS[network].scheme,
17152
+ payoutMethod: "external_liquid",
17153
+ payTo,
17154
+ configured: true,
17155
+ issuer: "external",
17156
+ };
17157
+ const existing = normalizeStoredPaymentCapabilities(hazbase.paymentCapabilities)
17158
+ .filter((entry) => !(entry.network === network && entry.asset === asset));
17159
+ state.hazbase = {
17160
+ ...hazbase,
17161
+ paymentCapabilities: [...existing, nextCapability],
17162
+ sessionInvalid: false,
17163
+ sessionInvalidReason: "",
17164
+ sessionInvalidAt: "",
17165
+ };
17166
+ await saveState(config.stateFile, state);
17167
+ return writeJson(res, 200, { ok: true, capability: nextCapability });
17168
+ }
17169
+
17170
+ if (url.pathname === "/api/hazbase/agent-payment-defaults" && req.method === "POST") {
17171
+ const session = requireMutatingApiSession(req, res, config, state);
17172
+ if (!session) return;
17173
+ const hazbase = normalizeHazbaseState(state.hazbase);
17174
+ const body = await parseJsonBody(req);
17175
+ const mode = cleanText(body?.mode || "").toLowerCase() === "configured" ? "configured" : "custom";
17176
+ let nextDefaults;
17177
+ if (mode === "configured") {
17178
+ nextDefaults = { mode: "configured", accepts: [] };
17179
+ } else {
17180
+ const capabilities = buildPaymentCapabilities({ hazbase, payments: null });
17181
+ const configuredKeys = new Set(
17182
+ capabilities
17183
+ .filter((entry) => entry?.configured && entry?.enabled !== false && isPaymentNetworkAvailable(entry.network))
17184
+ .map((entry) => paymentCapabilityKey(entry))
17185
+ .filter(Boolean)
17186
+ );
17187
+ const rawAccepts = Array.isArray(body?.accepts) ? body.accepts : [];
17188
+ const accepts = [];
17189
+ const seen = new Set();
17190
+ for (const entry of rawAccepts) {
17191
+ const ref = paymentCapabilityRef(entry);
17192
+ if (!ref) return writeJson(res, 400, { error: "unsupported-payment-capability" });
17193
+ const key = `${ref.network}:${ref.asset}`;
17194
+ if (!configuredKeys.has(key)) {
17195
+ return writeJson(res, 409, { error: "payment-capability-not-configured", network: ref.network, asset: ref.asset });
17196
+ }
17197
+ if (seen.has(key)) continue;
17198
+ seen.add(key);
17199
+ accepts.push(ref);
17200
+ }
17201
+ nextDefaults = { mode: "custom", accepts };
17202
+ }
17203
+ state.hazbase = {
17204
+ ...hazbase,
17205
+ agentPaymentDefaults: nextDefaults,
17206
+ };
17207
+ await saveState(config.stateFile, state);
17208
+ return writeJson(res, 200, { ok: true, agentPaymentDefaults: nextDefaults });
17209
+ }
17210
+
16903
17211
  if (url.pathname === "/api/hazbase/session/refresh" && req.method === "POST") {
16904
17212
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
16905
- const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
17213
+ const hookAuth = config.sessionSecret && constantTimeStringEqual(hookSecret, config.sessionSecret);
16906
17214
  if (!hookAuth) {
16907
17215
  const session = requireMutatingApiSession(req, res, config, state);
16908
17216
  if (!session) return;
@@ -17511,7 +17819,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
17511
17819
  // List known threads (active + registry) for the share target picker.
17512
17820
  if (url.pathname === "/api/threads/list" && req.method === "GET") {
17513
17821
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
17514
- const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
17822
+ const hookAuth = config.sessionSecret && constantTimeStringEqual(hookSecret, config.sessionSecret);
17515
17823
  if (!hookAuth) { const session = requireApiSession(req, res, config, state); if (!session) return; }
17516
17824
  const codexConnected = Boolean(runtime.ipcClient?.clientId);
17517
17825
  const activeIds = new Set();
@@ -17555,7 +17863,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
17555
17863
  // Submit a thread share request (creates an approval item on the phone).
17556
17864
  if (url.pathname === "/api/threads/share" && req.method === "POST") {
17557
17865
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
17558
- const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
17866
+ const hookAuth = config.sessionSecret && constantTimeStringEqual(hookSecret, config.sessionSecret);
17559
17867
  if (!hookAuth) { const session = requireApiSession(req, res, config, state); if (!session) return; }
17560
17868
  const body = await parseJsonBody(req);
17561
17869
  const shareType = cleanText(body?.shareType || "message");
@@ -17805,7 +18113,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
17805
18113
  // time-slot-based compose. Defaults to full-day today.
17806
18114
  if (url.pathname === "/api/providers/moltbook/activity-summary" && req.method === "GET") {
17807
18115
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
17808
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18116
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
17809
18117
  return writeJson(res, 403, { error: "forbidden" });
17810
18118
  }
17811
18119
  const slot = String(url.searchParams.get("slot") || "").toLowerCase();
@@ -17980,7 +18288,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
17980
18288
 
17981
18289
  if (url.pathname === "/api/payments/x402/hazbase-wallet" && req.method === "POST") {
17982
18290
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
17983
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18291
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
17984
18292
  return writeJson(res, 401, { error: "unauthorized" });
17985
18293
  }
17986
18294
 
@@ -18070,7 +18378,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
18070
18378
 
18071
18379
  if (url.pathname === "/api/payments/x402/approval" && req.method === "POST") {
18072
18380
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
18073
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18381
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
18074
18382
  return writeJson(res, 401, { error: "unauthorized" });
18075
18383
  }
18076
18384
 
@@ -18138,7 +18446,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
18138
18446
 
18139
18447
  if (url.pathname === "/api/providers/mcp/events" && req.method === "POST") {
18140
18448
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
18141
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18449
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
18142
18450
  return writeJson(res, 401, { error: "unauthorized" });
18143
18451
  }
18144
18452
 
@@ -18154,7 +18462,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
18154
18462
 
18155
18463
  if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
18156
18464
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
18157
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18465
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
18158
18466
  return writeJson(res, 401, { error: "unauthorized" });
18159
18467
  }
18160
18468
 
@@ -18518,7 +18826,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
18518
18826
 
18519
18827
  if (url.pathname === "/api/providers/moltbook/events" && req.method === "POST") {
18520
18828
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
18521
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18829
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
18522
18830
  return writeJson(res, 401, { error: "unauthorized" });
18523
18831
  }
18524
18832
  let body;
@@ -18670,7 +18978,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
18670
18978
 
18671
18979
  if (url.pathname === "/api/providers/moltbook/draft" && req.method === "POST") {
18672
18980
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
18673
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
18981
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
18674
18982
  return writeJson(res, 401, { error: "unauthorized" });
18675
18983
  }
18676
18984
  let body;
@@ -18797,7 +19105,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
18797
19105
  );
18798
19106
  if (apiMoltbookDraftDecisionGet && req.method === "GET") {
18799
19107
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
18800
- if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
19108
+ if (!config.sessionSecret || !constantTimeStringEqual(hookSecret, config.sessionSecret)) {
18801
19109
  return writeJson(res, 401, { error: "unauthorized" });
18802
19110
  }
18803
19111
  const token = decodeURIComponent(apiMoltbookDraftDecisionGet[1]);
@@ -21246,6 +21554,10 @@ function buildConfig(cli) {
21246
21554
  "COMPLETION_REPLY_ACK_TIMEOUT_MS",
21247
21555
  DEFAULT_COMPLETION_REPLY_ACK_TIMEOUT_MS
21248
21556
  ),
21557
+ completionReplyThreadReopenWaitMs: numberEnv(
21558
+ "COMPLETION_REPLY_THREAD_REOPEN_WAIT_MS",
21559
+ DEFAULT_COMPLETION_REPLY_THREAD_REOPEN_WAIT_MS
21560
+ ),
21249
21561
  choicePageSize: numberEnv("CHOICE_PAGE_SIZE", 5),
21250
21562
  completionReplyImageMaxBytes: numberEnv(
21251
21563
  "COMPLETION_REPLY_IMAGE_MAX_BYTES",
@@ -21291,6 +21603,8 @@ function normalizeHazbaseState(raw) {
21291
21603
  highTrustToken: cleanText(value.highTrustToken ?? ""),
21292
21604
  highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
21293
21605
  accounts,
21606
+ paymentCapabilities: normalizeStoredPaymentCapabilities(value.paymentCapabilities),
21607
+ agentPaymentDefaults: normalizeAgentPaymentDefaults(value.agentPaymentDefaults),
21294
21608
  sessionInvalid: value.sessionInvalid === true,
21295
21609
  sessionInvalidReason: cleanText(value.sessionInvalidReason ?? ""),
21296
21610
  sessionInvalidAt: cleanText(value.sessionInvalidAt ?? ""),
@@ -21551,6 +21865,202 @@ function resolveHazbaseAccountForChain(accounts, chainId) {
21551
21865
  return list.find((entry) => Number(entry?.chainId) === normalizedChainId && cleanText(entry?.smartAccountAddress || "")) || null;
21552
21866
  }
21553
21867
 
21868
+ function normalizePaymentNetworkKey(raw) {
21869
+ const value = cleanText(raw || "").toLowerCase();
21870
+ if (value === "basesepolia") return "base-sepolia";
21871
+ if (value === "polygonamoy" || value === "amoy") return "polygon-amoy";
21872
+ if (value === "matic") return "polygon";
21873
+ if (value === "liquid" || value === "liquid-mainnet") return "liquidv1";
21874
+ return PAYMENT_NETWORKS[value] ? value : "";
21875
+ }
21876
+
21877
+ function isPaymentNetworkAvailable(network) {
21878
+ return PAYMENT_NETWORKS[network]?.releaseStatus !== "comingSoon";
21879
+ }
21880
+
21881
+ function paymentNetworkForChainId(chainId) {
21882
+ const id = Number(chainId || 0);
21883
+ return Object.entries(PAYMENT_NETWORKS).find(([, entry]) => entry.chainId === id)?.[0] || "";
21884
+ }
21885
+
21886
+ function normalizePaymentAssetKey(raw, network) {
21887
+ const networkInfo = PAYMENT_NETWORKS[network] || null;
21888
+ const value = cleanText(raw || networkInfo?.asset || "").toLowerCase();
21889
+ if (value === "usd-t" || value === "tether") return "usdt";
21890
+ if (value === "jpy" || value === "jpy-coin") return "jpyc";
21891
+ if (value === "bitcoin" || value === "btc") return "lbtc";
21892
+ const allowed = Array.isArray(networkInfo?.assets) ? networkInfo.assets : [networkInfo?.asset].filter(Boolean);
21893
+ if ((value === "usdc" || value === "jpyc" || value === "usdt" || value === "lbtc") && allowed.includes(value)) return value;
21894
+ return "";
21895
+ }
21896
+
21897
+ function isValidLiquidAddressForNetwork(address, network) {
21898
+ const value = cleanText(address || "").toLowerCase();
21899
+ if (network === "liquidtestnet") return /^(tlq1|tex1)[0-9a-z]{20,}$/u.test(value);
21900
+ if (network === "liquidv1") return /^(lq1|ex1)[0-9a-z]{20,}$/u.test(value);
21901
+ return false;
21902
+ }
21903
+
21904
+ function normalizeStoredPaymentCapabilities(value) {
21905
+ const list = Array.isArray(value) ? value : [];
21906
+ const out = [];
21907
+ for (const entry of list) {
21908
+ const network = normalizePaymentNetworkKey(entry?.network);
21909
+ if (!network || PAYMENT_NETWORKS[network]?.family !== "liquid") continue;
21910
+ const asset = normalizePaymentAssetKey(entry?.asset, network);
21911
+ const payTo = cleanText(entry?.payTo || entry?.payoutAddress || "").toLowerCase();
21912
+ if (!asset || !isValidLiquidAddressForNetwork(payTo, network)) continue;
21913
+ out.push({
21914
+ network,
21915
+ asset,
21916
+ scheme: PAYMENT_NETWORKS[network].scheme,
21917
+ payoutMethod: "external_liquid",
21918
+ payTo,
21919
+ configured: true,
21920
+ issuer: cleanText(entry?.issuer || "external") || "external",
21921
+ });
21922
+ }
21923
+ return out;
21924
+ }
21925
+
21926
+ function paymentCapabilityRef(entry) {
21927
+ const network = normalizePaymentNetworkKey(entry?.network);
21928
+ const asset = normalizePaymentAssetKey(entry?.asset, network);
21929
+ if (!network || !asset) return null;
21930
+ return { network, asset };
21931
+ }
21932
+
21933
+ function paymentCapabilityKey(entry) {
21934
+ const ref = paymentCapabilityRef(entry);
21935
+ return ref ? `${ref.network}:${ref.asset}` : "";
21936
+ }
21937
+
21938
+ function normalizeAgentPaymentDefaults(value) {
21939
+ const raw = value && typeof value === "object" ? value : {};
21940
+ const mode = cleanText(raw.mode || "").toLowerCase() === "custom" ? "custom" : "configured";
21941
+ const list = Array.isArray(raw.accepts) ? raw.accepts : Array.isArray(value) ? value : [];
21942
+ const accepts = [];
21943
+ const seen = new Set();
21944
+ for (const entry of list) {
21945
+ const ref = paymentCapabilityRef(entry);
21946
+ if (!ref || !isPaymentNetworkAvailable(ref.network)) continue;
21947
+ const key = `${ref.network}:${ref.asset}`;
21948
+ if (seen.has(key)) continue;
21949
+ seen.add(key);
21950
+ accepts.push(ref);
21951
+ }
21952
+ return { mode, accepts };
21953
+ }
21954
+
21955
+ function effectiveAgentPaymentDefaultRefs(defaults, capabilities) {
21956
+ const normalized = normalizeAgentPaymentDefaults(defaults);
21957
+ const configured = (Array.isArray(capabilities) ? capabilities : [])
21958
+ .filter((entry) => entry?.configured && entry?.enabled !== false && isPaymentNetworkAvailable(entry.network))
21959
+ .map((entry) => paymentCapabilityRef(entry))
21960
+ .filter(Boolean);
21961
+ if (normalized.mode !== "custom") {
21962
+ return configured;
21963
+ }
21964
+ const configuredKeys = new Set(configured.map((entry) => `${entry.network}:${entry.asset}`));
21965
+ return normalized.accepts.filter((entry) => configuredKeys.has(`${entry.network}:${entry.asset}`));
21966
+ }
21967
+
21968
+ function applyAgentPaymentDefaults(capabilities, defaults) {
21969
+ const effective = effectiveAgentPaymentDefaultRefs(defaults, capabilities);
21970
+ const enabledKeys = new Set(effective.map((entry) => `${entry.network}:${entry.asset}`));
21971
+ return (Array.isArray(capabilities) ? capabilities : []).map((entry) => ({
21972
+ ...entry,
21973
+ agentEnabled: enabledKeys.has(paymentCapabilityKey(entry)),
21974
+ }));
21975
+ }
21976
+
21977
+ function agentPaymentDefaultsStatus(defaults, capabilities) {
21978
+ const normalized = normalizeAgentPaymentDefaults(defaults);
21979
+ return {
21980
+ mode: normalized.mode,
21981
+ accepts: normalized.accepts,
21982
+ effectiveAccepts: effectiveAgentPaymentDefaultRefs(normalized, capabilities),
21983
+ };
21984
+ }
21985
+
21986
+ function liquidCapabilityFromEnv(network) {
21987
+ if (!isPaymentNetworkAvailable(network)) return null;
21988
+ const suffix = network === "liquidtestnet" ? "LIQUIDTESTNET" : "LIQUIDV1";
21989
+ const payTo = cleanText(process.env[`VIVEWORKER_${suffix}_PAYOUT_ADDRESS`] || process.env[`VIVEWORKER_${suffix}_PAY_TO`] || "").toLowerCase();
21990
+ if (!isValidLiquidAddressForNetwork(payTo, network)) return null;
21991
+ return {
21992
+ network,
21993
+ asset: "usdt",
21994
+ scheme: PAYMENT_NETWORKS[network].scheme,
21995
+ payoutMethod: "external_liquid",
21996
+ payTo,
21997
+ configured: true,
21998
+ issuer: "env",
21999
+ };
22000
+ }
22001
+
22002
+ function buildPaymentCapabilities({ hazbase, payments }) {
22003
+ const supported = Array.isArray(payments?.networks) ? payments.networks : [];
22004
+ const supportedByNetwork = new Map(supported.map((entry) => [cleanText(entry?.network || entry?.id || ""), entry]));
22005
+ const capabilities = [];
22006
+ for (const network of ["base-sepolia", "base", "polygon-amoy", "polygon"]) {
22007
+ const meta = PAYMENT_NETWORKS[network];
22008
+ const paymentMeta = supportedByNetwork.get(network) || null;
22009
+ const supportedAssets = Array.isArray(paymentMeta?.assets) && paymentMeta.assets.length > 0
22010
+ ? paymentMeta.assets.map((entry) => normalizePaymentAssetKey(entry?.asset, network)).filter(Boolean)
22011
+ : meta.assets || [meta.asset];
22012
+ const account = resolveHazbaseAccountForChain(hazbase.accounts, meta.chainId);
22013
+ const available = isPaymentNetworkAvailable(network);
22014
+ for (const asset of [...new Set(supportedAssets)]) {
22015
+ capabilities.push({
22016
+ network,
22017
+ family: "evm",
22018
+ chainId: meta.chainId,
22019
+ asset,
22020
+ scheme: "exact",
22021
+ payoutMethod: "hazbase_wallet",
22022
+ payTo: account?.smartAccountAddress || "",
22023
+ smartAccountAddress: account?.smartAccountAddress || "",
22024
+ configured: Boolean(account?.smartAccountAddress),
22025
+ enabled: available && (supportedByNetwork.has(network) || supported.length === 0),
22026
+ label: meta.label,
22027
+ issuer: "hazbase",
22028
+ releaseStatus: meta.releaseStatus || "available",
22029
+ });
22030
+ }
22031
+ }
22032
+ const stored = normalizeStoredPaymentCapabilities(hazbase.paymentCapabilities);
22033
+ for (const network of ["liquidtestnet", "liquidv1"]) {
22034
+ const meta = PAYMENT_NETWORKS[network];
22035
+ const existing = stored.find((entry) => entry.network === network) || liquidCapabilityFromEnv(network);
22036
+ const available = isPaymentNetworkAvailable(network);
22037
+ capabilities.push({
22038
+ network,
22039
+ family: "liquid",
22040
+ chainId: null,
22041
+ asset: existing?.asset || meta.asset,
22042
+ scheme: meta.scheme,
22043
+ payoutMethod: "external_liquid",
22044
+ payTo: existing?.payTo || "",
22045
+ configured: Boolean(existing?.payTo),
22046
+ enabled: available && (supportedByNetwork.has(network) || supported.length === 0),
22047
+ label: meta.label,
22048
+ issuer: existing?.issuer || "external",
22049
+ releaseStatus: meta.releaseStatus || "available",
22050
+ });
22051
+ }
22052
+ return capabilities;
22053
+ }
22054
+
22055
+ function resolvePaymentCapability(capabilities, network, asset = "") {
22056
+ const normalizedNetwork = normalizePaymentNetworkKey(network);
22057
+ const normalizedAsset = normalizePaymentAssetKey(asset, normalizedNetwork);
22058
+ return capabilities.find((entry) => (
22059
+ normalizePaymentNetworkKey(entry.network) === normalizedNetwork &&
22060
+ (!normalizedAsset || normalizePaymentAssetKey(entry.asset, normalizedNetwork) === normalizedAsset)
22061
+ )) || null;
22062
+ }
22063
+
21554
22064
 
21555
22065
  async function finalizeHazbaseWalletPaymentFailure({ config, runtime, state, approval, error }) {
21556
22066
  const errorCode = cleanText(error?.code || error?.message || "hazbase-wallet-payment-failed") || "hazbase-wallet-payment-failed";
@@ -22020,8 +22530,13 @@ async function saveState(stateFile, state) {
22020
22530
  // single newline at the end keeps diffs/hexdumps working on the off chance
22021
22531
  // someone cats the file.
22022
22532
  const output = JSON.stringify(state);
22023
- await fs.mkdir(path.dirname(stateFile), { recursive: true });
22024
- await fs.writeFile(stateFile, `${output}\n`, "utf8");
22533
+ await fs.mkdir(path.dirname(stateFile), { recursive: true, mode: 0o700 });
22534
+ // state.json holds hazbase wallet tokens, push subscriptions, and device
22535
+ // records — keep it owner-only so other local users can't read it. writeFile's
22536
+ // mode only applies on create, so chmod explicitly to also tighten any file
22537
+ // that was previously written world-readable.
22538
+ await fs.writeFile(stateFile, `${output}\n`, { mode: 0o600 });
22539
+ await fs.chmod(stateFile, 0o600).catch(() => {});
22025
22540
  }
22026
22541
 
22027
22542
  // ---------------------------------------------------------------------------