viveworker 0.7.0-beta.2 → 0.7.0-beta.3

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.
@@ -56,6 +56,8 @@ const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
56
56
  const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
57
57
  const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
58
58
  const HAZBASE_METADATA_TIMEOUT_MS = 1500;
59
+ const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
60
+ const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
59
61
 
60
62
  // Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
61
63
  // per tracked repo (`git diff --name-status`, `git status --porcelain`,
@@ -265,6 +267,11 @@ const runtime = {
265
267
  // deliberately NOT persisted via `state` since the cache is worthless after
266
268
  // a restart and would just bloat the state file.
267
269
  a2aShareStatusCache: null,
270
+ // Latest npm version cache. Checked only from the technical settings page
271
+ // and cached here so a weak network or repeated settings renders never
272
+ // hammer registry.npmjs.org.
273
+ npmVersionStatusCache: null,
274
+ npmVersionStatusPending: null,
268
275
  };
269
276
  const state = await loadState(config.stateFile);
270
277
  runtime.threadRegistry = await loadThreadRegistry(config.threadRegistryFile);
@@ -2809,6 +2816,7 @@ function providerDisplayName(locale, provider) {
2809
2816
  if (p === "claude") return t(locale, "common.claude");
2810
2817
  if (p === "moltbook") return "Moltbook";
2811
2818
  if (p === "a2a") return "A2A";
2819
+ if (p === "viveworker") return t(locale, "common.appName");
2812
2820
  return t(locale, "common.codex");
2813
2821
  }
2814
2822
 
@@ -2848,7 +2856,7 @@ function normalizeHistoryItem(raw) {
2848
2856
  if (!isPlainObject(raw)) {
2849
2857
  return null;
2850
2858
  }
2851
- if (shouldHideClaudeInternalItem(raw)) {
2859
+ if (shouldHideInternalTimelineItem(raw)) {
2852
2860
  return null;
2853
2861
  }
2854
2862
 
@@ -2903,7 +2911,7 @@ function normalizeHistoryItem(raw) {
2903
2911
  ...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
2904
2912
  ...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
2905
2913
  };
2906
- return shouldHideClaudeInternalItem(normalized) ? null : normalized;
2914
+ return shouldHideInternalTimelineItem(normalized) ? null : normalized;
2907
2915
  }
2908
2916
 
2909
2917
  function historyToken(stableId) {
@@ -3043,7 +3051,7 @@ function normalizeTimelineEntry(raw) {
3043
3051
  if (!isPlainObject(raw)) {
3044
3052
  return null;
3045
3053
  }
3046
- if (shouldHideClaudeInternalItem(raw)) {
3054
+ if (shouldHideInternalTimelineItem(raw)) {
3047
3055
  return null;
3048
3056
  }
3049
3057
 
@@ -3137,7 +3145,7 @@ function normalizeTimelineEntry(raw) {
3137
3145
  ...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
3138
3146
  ...(suggestions.length > 0 ? { suggestions } : {}),
3139
3147
  };
3140
- return shouldHideClaudeInternalItem(normalized) ? null : normalized;
3148
+ return shouldHideInternalTimelineItem(normalized) ? null : normalized;
3141
3149
  }
3142
3150
 
3143
3151
  function recordTimelineEntry({ config, runtime, state, entry }) {
@@ -5669,6 +5677,12 @@ async function processScannedEvent({ config, runtime, state, event }) {
5669
5677
  return false;
5670
5678
  }
5671
5679
 
5680
+ if (shouldSuppressInternalScannedEvent(event)) {
5681
+ state.seenEvents[event.id] = event.timestampMs || Date.now();
5682
+ trimSeenEvents(state.seenEvents, config.maxSeenEvents);
5683
+ return true;
5684
+ }
5685
+
5672
5686
  let dirty = false;
5673
5687
 
5674
5688
  if (event.kind === "task_complete") {
@@ -6528,6 +6542,150 @@ async function createNativeApproval({ config, runtime, conversationId, request,
6528
6542
  };
6529
6543
  }
6530
6544
 
6545
+ function createX402PaymentApproval({ config, body, now = Date.now() }) {
6546
+ const payment = normalizeX402PaymentApprovalBody(body);
6547
+ if (!payment) {
6548
+ return null;
6549
+ }
6550
+ const token = crypto.randomBytes(18).toString("hex");
6551
+ const requestId = cleanText(body.paymentRequestId || body.requestId || crypto.randomUUID());
6552
+ const requestKey = `payment:${requestId}`;
6553
+ const title = payment.amountUsdc
6554
+ ? `Payment approval — ${payment.amountUsdc} USDC`
6555
+ : "Payment approval";
6556
+ return {
6557
+ token,
6558
+ requestKey,
6559
+ conversationId: "payments",
6560
+ requestId,
6561
+ ownerClientId: null,
6562
+ kind: "payment",
6563
+ threadLabel: "x402 payment",
6564
+ title,
6565
+ messageText: formatX402PaymentApprovalMessage(payment),
6566
+ reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
6567
+ rawParams: payment,
6568
+ cwd: "",
6569
+ workspaceRoot: "",
6570
+ fileRefs: [],
6571
+ diffText: "",
6572
+ diffAvailable: false,
6573
+ diffSource: "",
6574
+ diffAddedLines: 0,
6575
+ diffRemovedLines: 0,
6576
+ createdAtMs: now,
6577
+ resolved: false,
6578
+ resolving: false,
6579
+ resolveClaudeWaiter: null,
6580
+ provider: "viveworker",
6581
+ };
6582
+ }
6583
+
6584
+
6585
+ function createHazbaseWalletPaymentApproval({ config, body, now = Date.now() }) {
6586
+ const payment = normalizeX402PaymentApprovalBody(body);
6587
+ if (!payment) return null;
6588
+ const paymentRequestId = cleanText(body.paymentRequestId || body.requestId || "");
6589
+ if (!/^[a-zA-Z0-9:_-]{8,160}$/u.test(paymentRequestId)) return null;
6590
+ const token = crypto.randomBytes(18).toString("hex");
6591
+ const requestKey = `hazbase_wallet_payment:${paymentRequestId}`;
6592
+ const title = payment.amountUsdc
6593
+ ? `Hazbase wallet payment — ${payment.amountUsdc} USDC`
6594
+ : "Hazbase wallet payment";
6595
+ return {
6596
+ token,
6597
+ requestKey,
6598
+ conversationId: "payments",
6599
+ requestId: paymentRequestId,
6600
+ paymentRequestId,
6601
+ ownerClientId: null,
6602
+ kind: "hazbase_wallet_payment",
6603
+ threadLabel: "x402 payment",
6604
+ title,
6605
+ messageText: formatHazbaseWalletPaymentApprovalMessage(payment),
6606
+ reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
6607
+ rawParams: payment,
6608
+ cwd: "",
6609
+ workspaceRoot: "",
6610
+ fileRefs: [],
6611
+ diffText: "",
6612
+ diffAvailable: false,
6613
+ diffSource: "",
6614
+ diffAddedLines: 0,
6615
+ diffRemovedLines: 0,
6616
+ createdAtMs: now,
6617
+ resolved: false,
6618
+ resolving: false,
6619
+ resolveClaudeWaiter: null,
6620
+ provider: "viveworker",
6621
+ };
6622
+ }
6623
+
6624
+ function formatHazbaseWalletPaymentApprovalMessage(payment) {
6625
+ const lines = [
6626
+ "Hazbase Smart Wallet payment requested.",
6627
+ "",
6628
+ `Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
6629
+ `Network: ${payment.network} (chainId ${payment.chainId})`,
6630
+ `Pay to: ${payment.payTo}`,
6631
+ `Asset: ${payment.asset}`,
6632
+ `Resource: ${payment.resource}`,
6633
+ "",
6634
+ "Approving will ask for your passkey, then hazBase will submit a gasless Smart Wallet payment.",
6635
+ ];
6636
+ if (payment.description) lines.splice(7, 0, `Description: ${payment.description}`);
6637
+ if (payment.url && payment.url !== payment.resource) lines.splice(-1, 0, `URL: ${payment.url}`);
6638
+ return lines.join("\n");
6639
+ }
6640
+
6641
+ function normalizeX402PaymentApprovalBody(body) {
6642
+ if (!isPlainObject(body)) return null;
6643
+ const payment = isPlainObject(body.payment) ? body.payment : {};
6644
+ const network = cleanText(payment.network || "");
6645
+ const chainId = Number(payment.chainId) || 0;
6646
+ const amountAtomic = cleanText(payment.amountAtomic || "");
6647
+ const amountUsdc = cleanText(payment.amountUsdc || "");
6648
+ const payTo = cleanText(payment.payTo || "");
6649
+ const asset = cleanText(payment.asset || "");
6650
+ const resource = cleanText(payment.resource || body.url || "");
6651
+ const description = cleanText(payment.description || "");
6652
+ const url = cleanText(body.url || resource);
6653
+ if (!network || !chainId || !amountAtomic || !payTo || !asset || !resource) {
6654
+ return null;
6655
+ }
6656
+ return {
6657
+ url,
6658
+ network,
6659
+ chainId,
6660
+ amountAtomic,
6661
+ amountUsdc,
6662
+ payTo,
6663
+ asset,
6664
+ resource,
6665
+ description,
6666
+ };
6667
+ }
6668
+
6669
+ function formatX402PaymentApprovalMessage(payment) {
6670
+ const lines = [
6671
+ "x402 payment approval requested.",
6672
+ "",
6673
+ `Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
6674
+ `Network: ${payment.network} (chainId ${payment.chainId})`,
6675
+ `Pay to: ${payment.payTo}`,
6676
+ `Asset: ${payment.asset}`,
6677
+ `Resource: ${payment.resource}`,
6678
+ ];
6679
+ if (payment.description) {
6680
+ lines.push(`Description: ${payment.description}`);
6681
+ }
6682
+ if (payment.url && payment.url !== payment.resource) {
6683
+ lines.push(`URL: ${payment.url}`);
6684
+ }
6685
+ lines.push("", "Approve only if the amount, recipient, network, and resource match what you expect.");
6686
+ return lines.join("\n");
6687
+ }
6688
+
6531
6689
  async function buildNativeApprovalPayload({ config, runtime, conversationId, request, token }) {
6532
6690
  const kind = nativeApprovalKind(request.method);
6533
6691
  if (!kind) {
@@ -8767,6 +8925,18 @@ function isHiddenClaudeInternalScoringText(text) {
8767
8925
  return /^You are scoring Moltbook posts? for an AI agent\b/iu.test(single);
8768
8926
  }
8769
8927
 
8928
+ function isHiddenCodexApprovalAssessmentText(text) {
8929
+ const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
8930
+ if (!single) {
8931
+ return false;
8932
+ }
8933
+ return /\bThe following is the Codex agent history(?: added since your last approval assessment)?\b/iu.test(single);
8934
+ }
8935
+
8936
+ function shouldHideInternalTimelineItem(item) {
8937
+ return shouldHideClaudeInternalItem(item) || shouldHideCodexInternalApprovalItem(item);
8938
+ }
8939
+
8770
8940
  function shouldHideClaudeInternalItem(item) {
8771
8941
  if (!isPlainObject(item)) {
8772
8942
  return false;
@@ -8786,6 +8956,48 @@ function shouldHideClaudeInternalItem(item) {
8786
8956
  );
8787
8957
  }
8788
8958
 
8959
+ function shouldSuppressInternalScannedEvent(event) {
8960
+ if (!isPlainObject(event)) {
8961
+ return false;
8962
+ }
8963
+ const provider = normalizeProvider(event.provider || "codex");
8964
+ if (provider !== "codex") {
8965
+ return false;
8966
+ }
8967
+
8968
+ return shouldHideCodexInternalApprovalItem({
8969
+ provider,
8970
+ kind: cleanText(event.kind || ""),
8971
+ title: event.title,
8972
+ threadLabel: event.threadLabel,
8973
+ summary: event.summary ?? event.message,
8974
+ messageText: event.messageText ?? event.detailText ?? event.message,
8975
+ detailText: event.detailText,
8976
+ message: event.message,
8977
+ });
8978
+ }
8979
+
8980
+ function shouldHideCodexInternalApprovalItem(item) {
8981
+ if (!isPlainObject(item)) {
8982
+ return false;
8983
+ }
8984
+ if (normalizeProvider(item.provider) !== "codex") {
8985
+ return false;
8986
+ }
8987
+
8988
+ const title = cleanText(item.title ?? "");
8989
+ const threadLabel = cleanText(item.threadLabel ?? "");
8990
+ if (isHiddenCodexApprovalAssessmentText(title) || isHiddenCodexApprovalAssessmentText(threadLabel)) {
8991
+ return true;
8992
+ }
8993
+ return (
8994
+ isHiddenCodexApprovalAssessmentText(item.messageText) ||
8995
+ isHiddenCodexApprovalAssessmentText(item.summary) ||
8996
+ isHiddenCodexApprovalAssessmentText(item.detailText) ||
8997
+ isHiddenCodexApprovalAssessmentText(item.message)
8998
+ );
8999
+ }
9000
+
8789
9001
  function threadStateArchiveStatus(threadState) {
8790
9002
  if (!isPlainObject(threadState)) {
8791
9003
  return "";
@@ -10499,7 +10711,9 @@ function buildPendingInboxItems(runtime, state, config, locale) {
10499
10711
  token: approval.token,
10500
10712
  threadId: cleanText(approval.conversationId || ""),
10501
10713
  threadLabel: approval.threadLabel || "",
10502
- title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
10714
+ title: cleanText(approval.kind || "") === "payment"
10715
+ ? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
10716
+ : formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
10503
10717
  summary: formatNotificationBody(approval.messageText, 100) || approval.messageText,
10504
10718
  primaryLabel: t(locale, "server.action.review"),
10505
10719
  createdAtMs: Number(approval.createdAtMs) || now,
@@ -10851,7 +11065,9 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
10851
11065
  kind: "approval",
10852
11066
  threadId: cleanText(approval.conversationId || ""),
10853
11067
  threadLabel: approval.threadLabel,
10854
- title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
11068
+ title: cleanText(approval.kind || "") === "payment"
11069
+ ? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
11070
+ : formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
10855
11071
  summary: formatNotificationBody(approval.messageText, 180) || approval.messageText,
10856
11072
  messageText: approval.messageText,
10857
11073
  outcome: "pending",
@@ -11058,7 +11274,9 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
11058
11274
  approvalKind,
11059
11275
  provider: normalizeProvider(approval.provider),
11060
11276
  token: approval.token,
11061
- title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
11277
+ title: approvalKind === "payment"
11278
+ ? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
11279
+ : formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
11062
11280
  threadLabel: approval.threadLabel || "",
11063
11281
  createdAtMs: Number(approval.createdAtMs) || 0,
11064
11282
  messageHtml: renderMessageHtml(approval.messageText, `<p>${escapeHtml(t(locale, "detail.approvalRequested"))}</p>`),
@@ -11073,10 +11291,14 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
11073
11291
  readOnly: approval.readOnly === true,
11074
11292
  actions: approval.readOnly === true
11075
11293
  ? []
11076
- : [
11077
- { label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
11078
- { label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
11079
- ],
11294
+ : approvalKind === "hazbase_wallet_payment"
11295
+ ? [
11296
+ { label: t(locale, "server.action.payWithWallet"), tone: "primary", url: `/api/payments/x402/hazbase-wallet/${encodeURIComponent(approval.token)}/pay`, body: { hazbaseReauth: true } },
11297
+ ]
11298
+ : [
11299
+ { label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
11300
+ { label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
11301
+ ],
11080
11302
  };
11081
11303
  if (approvalKind === "plan") {
11082
11304
  const planText = String(approval.planText || "");
@@ -13472,6 +13694,12 @@ function createNativeApprovalServer({ config, runtime, state }) {
13472
13694
  return writeJson(res, 200, buildSessionPayload({ config, state, session }));
13473
13695
  }
13474
13696
 
13697
+ if (url.pathname === "/api/version/status" && req.method === "GET") {
13698
+ const session = requireApiSession(req, res, config, state);
13699
+ if (!session) return;
13700
+ return writeJson(res, 200, await getNpmVersionStatus(runtime, config));
13701
+ }
13702
+
13475
13703
  // Collapses the PWA's boot-time fan-out (session + inbox + timeline
13476
13704
  // + devices) into a single HTTPS round-trip. iOS Safari tears down
13477
13705
  // connections aggressively, so each parallel fetch pays its own
@@ -13929,10 +14157,15 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
13929
14157
  8453: resolveHazbaseAccountForChain(hazbase.accounts, 8453)?.smartAccountAddress || "",
13930
14158
  84532: resolveHazbaseAccountForChain(hazbase.accounts, 84532)?.smartAccountAddress || "",
13931
14159
  };
14160
+ const signedIn = Boolean(hazbase.accessToken) && !hazbase.sessionInvalid;
13932
14161
  return writeJson(res, 200, {
13933
14162
  enabled: true,
13934
14163
  apiUrl: config.hazbaseApiUrl,
13935
- signedIn: Boolean(hazbase.accessToken),
14164
+ signedIn,
14165
+ sessionStatus: hazbase.sessionInvalid ? "expired" : signedIn ? "signed_in" : "signed_out",
14166
+ sessionInvalid: hazbase.sessionInvalid,
14167
+ sessionInvalidReason: hazbase.sessionInvalidReason,
14168
+ sessionInvalidAt: hazbase.sessionInvalidAt,
13936
14169
  email: hazbase.email,
13937
14170
  userId: hazbase.userId,
13938
14171
  sessionId: hazbase.sessionId,
@@ -13995,15 +14228,19 @@ if (url.pathname === "/api/hazbase/verify-otp" && req.method === "POST") {
13995
14228
  const code = cleanText(body?.code || "");
13996
14229
  if (!email || !code) return writeJson(res, 400, { error: "otp-required" });
13997
14230
  const result = await verifyHazbaseEmailOtp({ email, code });
14231
+ const previousHazbase = normalizeHazbaseState(state.hazbase);
13998
14232
  state.hazbase = {
13999
- ...normalizeHazbaseState(state.hazbase),
14233
+ ...previousHazbase,
14000
14234
  email: result.email || email,
14001
14235
  accessToken: cleanText(result.accessToken || ""),
14002
14236
  sessionId: cleanText(result.sessionId || ""),
14003
14237
  userId: cleanText(result.userId || ""),
14004
- accounts: Array.isArray(result.accounts) ? result.accounts : [],
14238
+ accounts: mergeHazbaseAccountSummaries(previousHazbase.accounts, Array.isArray(result.accounts) ? result.accounts : []),
14005
14239
  highTrustToken: "",
14006
14240
  highTrustExpiresAt: "",
14241
+ sessionInvalid: false,
14242
+ sessionInvalidReason: "",
14243
+ sessionInvalidAt: "",
14007
14244
  };
14008
14245
  await saveState(config.stateFile, state);
14009
14246
  return writeJson(res, 200, result);
@@ -14024,11 +14261,17 @@ if (url.pathname === "/api/hazbase/passkey/register/challenge" && req.method ===
14024
14261
  message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
14025
14262
  });
14026
14263
  }
14027
- const result = await requestPasskeyRegistrationChallenge({
14028
- emailSession: hazbase.accessToken,
14029
- deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14030
- partnerOrigin,
14031
- });
14264
+ let result;
14265
+ try {
14266
+ result = await requestPasskeyRegistrationChallenge({
14267
+ emailSession: hazbase.accessToken,
14268
+ deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14269
+ partnerOrigin,
14270
+ });
14271
+ } catch (error) {
14272
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14273
+ throw error;
14274
+ }
14032
14275
  return writeJson(res, 200, result);
14033
14276
  }
14034
14277
 
@@ -14038,16 +14281,25 @@ if (url.pathname === "/api/hazbase/passkey/register/complete" && req.method ===
14038
14281
  const hazbase = normalizeHazbaseState(state.hazbase);
14039
14282
  if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
14040
14283
  const body = await parseJsonBody(req);
14041
- const result = await completePasskeyRegistration({
14042
- emailSession: hazbase.accessToken,
14043
- challengeId: cleanText(body?.challengeId || ""),
14044
- credential: body?.credential,
14045
- deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14046
- });
14284
+ let result;
14285
+ try {
14286
+ result = await completePasskeyRegistration({
14287
+ emailSession: hazbase.accessToken,
14288
+ challengeId: cleanText(body?.challengeId || ""),
14289
+ credential: body?.credential,
14290
+ deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14291
+ });
14292
+ } catch (error) {
14293
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14294
+ throw error;
14295
+ }
14047
14296
  state.hazbase = {
14048
14297
  ...hazbase,
14049
14298
  deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
14050
14299
  credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
14300
+ sessionInvalid: false,
14301
+ sessionInvalidReason: "",
14302
+ sessionInvalidAt: "",
14051
14303
  };
14052
14304
  await saveState(config.stateFile, state);
14053
14305
  return writeJson(res, 200, result);
@@ -14068,12 +14320,18 @@ if (url.pathname === "/api/hazbase/passkey/assert/challenge" && req.method === "
14068
14320
  message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
14069
14321
  });
14070
14322
  }
14071
- const result = await requestPasskeyAssertionChallenge({
14072
- emailSession: hazbase.accessToken,
14073
- purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14074
- deviceBindingId: hazbase.deviceBindingId || undefined,
14075
- partnerOrigin,
14076
- });
14323
+ let result;
14324
+ try {
14325
+ result = await requestPasskeyAssertionChallenge({
14326
+ emailSession: hazbase.accessToken,
14327
+ purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14328
+ deviceBindingId: hazbase.deviceBindingId || undefined,
14329
+ partnerOrigin,
14330
+ });
14331
+ } catch (error) {
14332
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14333
+ throw error;
14334
+ }
14077
14335
  return writeJson(res, 200, result);
14078
14336
  }
14079
14337
 
@@ -14083,19 +14341,28 @@ if (url.pathname === "/api/hazbase/passkey/assert/complete" && req.method === "P
14083
14341
  const hazbase = normalizeHazbaseState(state.hazbase);
14084
14342
  if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
14085
14343
  const body = await parseJsonBody(req);
14086
- const result = await completePasskeyAssertion({
14087
- emailSession: hazbase.accessToken,
14088
- challengeId: cleanText(body?.challengeId || ""),
14089
- credential: body?.credential,
14090
- purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14091
- deviceBindingId: hazbase.deviceBindingId || undefined,
14092
- });
14344
+ let result;
14345
+ try {
14346
+ result = await completePasskeyAssertion({
14347
+ emailSession: hazbase.accessToken,
14348
+ challengeId: cleanText(body?.challengeId || ""),
14349
+ credential: body?.credential,
14350
+ purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14351
+ deviceBindingId: hazbase.deviceBindingId || undefined,
14352
+ });
14353
+ } catch (error) {
14354
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14355
+ throw error;
14356
+ }
14093
14357
  state.hazbase = {
14094
14358
  ...hazbase,
14095
14359
  deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
14096
14360
  credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
14097
14361
  highTrustToken: cleanText(result.highTrustToken || ""),
14098
14362
  highTrustExpiresAt: cleanText(result.highTrustExpiresAt || ""),
14363
+ sessionInvalid: false,
14364
+ sessionInvalidReason: "",
14365
+ sessionInvalidAt: "",
14099
14366
  };
14100
14367
  await saveState(config.stateFile, state);
14101
14368
  return writeJson(res, 200, result);
@@ -14113,12 +14380,18 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
14113
14380
  if (chainId !== 8453 && chainId !== 84532) {
14114
14381
  return writeJson(res, 400, { error: "unsupported-chain" });
14115
14382
  }
14116
- const result = await bootstrapPasskeyAccount({
14117
- emailSession: hazbase.accessToken,
14118
- deviceBindingId: hazbase.deviceBindingId,
14119
- highTrustToken: hazbase.highTrustToken,
14120
- chainId,
14121
- });
14383
+ let result;
14384
+ try {
14385
+ result = await bootstrapPasskeyAccount({
14386
+ emailSession: hazbase.accessToken,
14387
+ deviceBindingId: hazbase.deviceBindingId,
14388
+ highTrustToken: hazbase.highTrustToken,
14389
+ chainId,
14390
+ });
14391
+ } catch (error) {
14392
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14393
+ throw error;
14394
+ }
14122
14395
  const nextAccounts = mergeHazbaseAccountSummaries(hazbase.accounts, [{
14123
14396
  smartAccountAddress: result.smartAccountAddress,
14124
14397
  chainId: result.chainId,
@@ -14129,11 +14402,29 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
14129
14402
  state.hazbase = {
14130
14403
  ...hazbase,
14131
14404
  accounts: nextAccounts,
14405
+ sessionInvalid: false,
14406
+ sessionInvalidReason: "",
14407
+ sessionInvalidAt: "",
14132
14408
  };
14133
14409
  await saveState(config.stateFile, state);
14134
14410
  return writeJson(res, 200, result);
14135
14411
  }
14136
14412
 
14413
+ if (url.pathname === "/api/hazbase/session/refresh" && req.method === "POST") {
14414
+ const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
14415
+ const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
14416
+ if (!hookAuth) {
14417
+ const session = requireMutatingApiSession(req, res, config, state);
14418
+ if (!session) return;
14419
+ }
14420
+ markHazbaseSessionInvalid(state, "manual_refresh_requested");
14421
+ await saveState(config.stateFile, state);
14422
+ return writeJson(res, 200, {
14423
+ ok: true,
14424
+ status: "session_refresh_required",
14425
+ });
14426
+ }
14427
+
14137
14428
  if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
14138
14429
  const session = requireMutatingApiSession(req, res, config, state);
14139
14430
  if (!session) return;
@@ -14656,6 +14947,164 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
14656
14947
  });
14657
14948
  }
14658
14949
 
14950
+
14951
+ if (url.pathname === "/api/payments/x402/hazbase-wallet" && req.method === "POST") {
14952
+ const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
14953
+ if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
14954
+ return writeJson(res, 401, { error: "unauthorized" });
14955
+ }
14956
+
14957
+ let body;
14958
+ try {
14959
+ body = await parseJsonBody(req);
14960
+ } catch {
14961
+ return writeJson(res, 400, { error: "invalid-json-body" });
14962
+ }
14963
+
14964
+ const approval = createHazbaseWalletPaymentApproval({ config, body });
14965
+ if (!approval) {
14966
+ return writeJson(res, 400, { error: "invalid-hazbase-wallet-payment-request" });
14967
+ }
14968
+ if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
14969
+ return writeJson(res, 409, { error: "hazbase-wallet-payment-already-pending" });
14970
+ }
14971
+
14972
+ runtime.nativeApprovalsByToken.set(approval.token, approval);
14973
+ runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
14974
+
14975
+ deliverWebPushItem({
14976
+ config,
14977
+ state,
14978
+ kind: "approval",
14979
+ tab: "inbox",
14980
+ subtab: "pending",
14981
+ token: approval.token,
14982
+ stableId: pendingApprovalStableId(approval),
14983
+ title: approval.title,
14984
+ body: approval.messageText,
14985
+ buildLocalizedContent: () => ({ title: approval.title, body: approval.messageText }),
14986
+ }).catch((error) => {
14987
+ console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
14988
+ });
14989
+
14990
+ const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
14991
+ const decisionPromise = new Promise((resolve) => {
14992
+ approval.resolveClaudeWaiter = resolve;
14993
+ });
14994
+ const timeoutPromise = new Promise((resolve) => {
14995
+ setTimeout(() => resolve({ paid: false, decision: "timeout", error: "hazbase-wallet-payment-timeout" }), waitMs);
14996
+ });
14997
+ const decision = await Promise.race([decisionPromise, timeoutPromise]);
14998
+
14999
+ if (!approval.resolved) {
15000
+ approval.resolved = true;
15001
+ approval.resolving = false;
15002
+ runtime.nativeApprovalsByToken.delete(approval.token);
15003
+ runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
15004
+ }
15005
+
15006
+ if (decision && typeof decision === "object" && decision.paid === true) {
15007
+ return writeJson(res, 200, decision);
15008
+ }
15009
+ const error = decision?.error || decision?.decision || "hazbase-wallet-payment-declined";
15010
+ const statusCode = error === "hazbase-wallet-payment-timeout" ? 408 : 403;
15011
+ return writeJson(res, statusCode, { paid: false, error, decision: decision?.decision || "decline" });
15012
+ }
15013
+
15014
+ const hazbaseWalletPayMatch = url.pathname.match(/^\/api\/payments\/x402\/hazbase-wallet\/([a-f0-9]+)\/pay$/u);
15015
+ if (hazbaseWalletPayMatch && req.method === "POST") {
15016
+ const session = requireMutatingApiSession(req, res, config, state);
15017
+ if (!session) return;
15018
+ const token = hazbaseWalletPayMatch[1];
15019
+ const approval = runtime.nativeApprovalsByToken.get(token);
15020
+ if (!approval || approval.kind !== "hazbase_wallet_payment") {
15021
+ return writeJson(res, 404, { error: "approval-not-found" });
15022
+ }
15023
+ if (approval.resolved || approval.resolving) {
15024
+ return writeJson(res, 409, { error: "approval-already-handled" });
15025
+ }
15026
+ approval.resolving = true;
15027
+ try {
15028
+ const result = await completeHazbaseWalletPaymentApproval({ config, runtime, state, approval });
15029
+ return writeJson(res, 200, result);
15030
+ } catch (error) {
15031
+ approval.resolving = false;
15032
+ console.error(`[hazbase-wallet-payment-error] ${approval.requestKey} | ${error?.stack || error?.message || error}`);
15033
+ return writeJson(res, Number(error?.statusCode) || 500, { error: error?.code || error?.message || "hazbase-wallet-payment-failed" });
15034
+ }
15035
+ }
15036
+
15037
+ if (url.pathname === "/api/payments/x402/approval" && req.method === "POST") {
15038
+ const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
15039
+ if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
15040
+ return writeJson(res, 401, { error: "unauthorized" });
15041
+ }
15042
+
15043
+ let body;
15044
+ try {
15045
+ body = await parseJsonBody(req);
15046
+ } catch {
15047
+ return writeJson(res, 400, { error: "invalid-json-body" });
15048
+ }
15049
+
15050
+ const approval = createX402PaymentApproval({ config, body });
15051
+ if (!approval) {
15052
+ return writeJson(res, 400, { error: "invalid-payment-approval-request" });
15053
+ }
15054
+ if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
15055
+ return writeJson(res, 409, { error: "payment-approval-already-pending" });
15056
+ }
15057
+
15058
+ runtime.nativeApprovalsByToken.set(approval.token, approval);
15059
+ runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
15060
+
15061
+ deliverWebPushItem({
15062
+ config,
15063
+ state,
15064
+ kind: "approval",
15065
+ tab: "inbox",
15066
+ subtab: "pending",
15067
+ token: approval.token,
15068
+ stableId: pendingApprovalStableId(approval),
15069
+ title: approval.title,
15070
+ body: approval.messageText,
15071
+ buildLocalizedContent: () => ({
15072
+ title: approval.title,
15073
+ body: approval.messageText,
15074
+ }),
15075
+ }).catch((error) => {
15076
+ console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
15077
+ });
15078
+
15079
+ const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
15080
+ const decisionPromise = new Promise((resolve) => {
15081
+ approval.resolveClaudeWaiter = resolve;
15082
+ });
15083
+ const timeoutPromise = new Promise((resolve) => {
15084
+ setTimeout(() => resolve("timeout"), waitMs);
15085
+ });
15086
+ const decision = await Promise.race([decisionPromise, timeoutPromise]);
15087
+
15088
+ if (!approval.resolved) {
15089
+ approval.resolved = true;
15090
+ approval.resolving = false;
15091
+ runtime.nativeApprovalsByToken.delete(approval.token);
15092
+ runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
15093
+ }
15094
+
15095
+ if (decision === "accept") {
15096
+ return writeJson(res, 200, {
15097
+ approved: true,
15098
+ decision,
15099
+ approvedPayment: approval.rawParams,
15100
+ });
15101
+ }
15102
+ if (decision === "timeout") {
15103
+ return writeJson(res, 408, { approved: false, decision, error: "payment-approval-timeout" });
15104
+ }
15105
+ return writeJson(res, 403, { approved: false, decision: "decline", error: "payment-approval-declined" });
15106
+ }
15107
+
14659
15108
  if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
14660
15109
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
14661
15110
  if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
@@ -15473,6 +15922,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
15473
15922
  if (approval.resolved || approval.resolving) {
15474
15923
  return writeJson(res, 409, { error: "approval-already-handled" });
15475
15924
  }
15925
+ if (approval.kind === "hazbase_wallet_payment") {
15926
+ console.warn(`[hazbase-wallet-payment-generic-decision-blocked] ${approval.requestKey} | ${decision}`);
15927
+ return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
15928
+ }
15476
15929
 
15477
15930
  approval.resolving = true;
15478
15931
  try {
@@ -16067,6 +16520,21 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
16067
16520
  if (approval.resolved || approval.resolving) {
16068
16521
  return writeApprovalHandled(res, req, approval.title, 409, config.defaultLocale);
16069
16522
  }
16523
+ if (approval.kind === "hazbase_wallet_payment") {
16524
+ console.warn(`[hazbase-wallet-payment-native-decision-blocked] ${approval.requestKey} | ${decision}`);
16525
+ if (requestWantsHtml(req)) {
16526
+ return writeHtml(
16527
+ res,
16528
+ 409,
16529
+ renderStatusPage({
16530
+ title: approval.title,
16531
+ body: "This approval must be completed from the viveworker app with the wallet payment button.",
16532
+ tone: "warn",
16533
+ })
16534
+ );
16535
+ }
16536
+ return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
16537
+ }
16070
16538
 
16071
16539
  approval.resolving = true;
16072
16540
  try {
@@ -17453,6 +17921,9 @@ function buildConfig(cli) {
17453
17921
  hazbaseApiUrl: stripTrailingSlash(process.env.HAZBASE_API_URL || "https://api.hazbase.com"),
17454
17922
  hazbaseClientKey: cleanText(process.env.HAZBASE_CLIENT_KEY || "viveworker-internal"),
17455
17923
  hazbaseDeviceLabel: cleanText(process.env.HAZBASE_DEVICE_LABEL || "viveworker"),
17924
+ npmPackageName: cleanText(process.env.VIVEWORKER_NPM_PACKAGE || "viveworker"),
17925
+ npmDistTag: cleanText(process.env.VIVEWORKER_NPM_DIST_TAG || "latest"),
17926
+ npmRegistryUrl: stripTrailingSlash(process.env.NPM_REGISTRY_URL || "https://registry.npmjs.org"),
17456
17927
  webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
17457
17928
  authRequired: boolEnv("AUTH_REQUIRED", true),
17458
17929
  webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
@@ -17553,7 +18024,54 @@ function normalizeHazbaseState(raw) {
17553
18024
  highTrustToken: cleanText(value.highTrustToken ?? ""),
17554
18025
  highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
17555
18026
  accounts: Array.isArray(value.accounts) ? value.accounts : [],
18027
+ sessionInvalid: value.sessionInvalid === true,
18028
+ sessionInvalidReason: cleanText(value.sessionInvalidReason ?? ""),
18029
+ sessionInvalidAt: cleanText(value.sessionInvalidAt ?? ""),
18030
+ };
18031
+ }
18032
+
18033
+ function isHazbaseInvalidAppSessionError(error) {
18034
+ const details = error?.details && typeof error.details === "object" ? error.details : {};
18035
+ const statusCode = Number(error?.statusCode || error?.status || details.statusCode || 0);
18036
+ const text = [
18037
+ error?.code,
18038
+ error?.message,
18039
+ details.errorCode,
18040
+ details.code,
18041
+ details.error,
18042
+ details.message,
18043
+ ]
18044
+ .map((entry) => cleanText(entry || "").toLowerCase())
18045
+ .filter(Boolean)
18046
+ .join(" ");
18047
+ return statusCode === 401 && text.includes("invalid app session");
18048
+ }
18049
+
18050
+ function markHazbaseSessionInvalid(state, reason = "invalid_app_session") {
18051
+ const hazbase = normalizeHazbaseState(state.hazbase);
18052
+ state.hazbase = {
18053
+ ...hazbase,
18054
+ accessToken: "",
18055
+ sessionId: "",
18056
+ highTrustToken: "",
18057
+ highTrustExpiresAt: "",
18058
+ sessionInvalid: true,
18059
+ sessionInvalidReason: cleanText(reason || "invalid_app_session"),
18060
+ sessionInvalidAt: new Date().toISOString(),
17556
18061
  };
18062
+ return state.hazbase;
18063
+ }
18064
+
18065
+ async function maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res }) {
18066
+ if (!isHazbaseInvalidAppSessionError(error)) {
18067
+ return false;
18068
+ }
18069
+ markHazbaseSessionInvalid(state);
18070
+ await saveState(config.stateFile, state);
18071
+ return writeJson(res, 401, {
18072
+ error: "hazbase-session-expired",
18073
+ message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
18074
+ });
17557
18075
  }
17558
18076
 
17559
18077
  function hazbasePasskeyLocalHostError() {
@@ -17627,6 +18145,117 @@ async function fetchHazbaseMetadata(config, endpoint, timeoutMs = HAZBASE_METADA
17627
18145
  }
17628
18146
  }
17629
18147
 
18148
+ async function getNpmVersionStatus(runtime, config) {
18149
+ const now = Date.now();
18150
+ const cached = runtime.npmVersionStatusCache;
18151
+ if (cached && now - Number(cached.checkedAtMs || 0) < NPM_VERSION_CHECK_CACHE_TTL_MS) {
18152
+ return cached;
18153
+ }
18154
+ if (runtime.npmVersionStatusPending) {
18155
+ return runtime.npmVersionStatusPending;
18156
+ }
18157
+ runtime.npmVersionStatusPending = fetchNpmVersionStatus(config)
18158
+ .then((status) => {
18159
+ runtime.npmVersionStatusCache = status;
18160
+ return status;
18161
+ })
18162
+ .catch((error) => {
18163
+ const fallback = {
18164
+ packageName: config.npmPackageName || "viveworker",
18165
+ distTag: config.npmDistTag || "latest",
18166
+ currentVersion: appPackageVersion,
18167
+ latestVersion: "",
18168
+ updateAvailable: false,
18169
+ checkedAtMs: Date.now(),
18170
+ error: cleanText(error?.message || "npm version check failed"),
18171
+ };
18172
+ runtime.npmVersionStatusCache = fallback;
18173
+ return fallback;
18174
+ })
18175
+ .finally(() => {
18176
+ runtime.npmVersionStatusPending = null;
18177
+ });
18178
+ return runtime.npmVersionStatusPending;
18179
+ }
18180
+
18181
+ async function fetchNpmVersionStatus(config) {
18182
+ const packageName = cleanText(config.npmPackageName || "viveworker") || "viveworker";
18183
+ const distTag = cleanText(config.npmDistTag || "latest") || "latest";
18184
+ const registryBaseUrl = stripTrailingSlash(config.npmRegistryUrl || "https://registry.npmjs.org");
18185
+ const endpoint = `${registryBaseUrl}/${encodeURIComponent(packageName).replace(/^%40/u, "@")}/${encodeURIComponent(distTag)}`;
18186
+ const controller = new AbortController();
18187
+ const timer = setTimeout(() => controller.abort(), NPM_VERSION_CHECK_TIMEOUT_MS);
18188
+ timer.unref?.();
18189
+ try {
18190
+ const response = await fetch(endpoint, {
18191
+ method: "GET",
18192
+ headers: { Accept: "application/json" },
18193
+ signal: controller.signal,
18194
+ });
18195
+ if (!response.ok) {
18196
+ throw new Error(`npm registry returned ${response.status}`);
18197
+ }
18198
+ const payload = await response.json().catch(() => null);
18199
+ const latestVersion = cleanText(payload?.version || "");
18200
+ if (!latestVersion) {
18201
+ throw new Error("npm registry response did not include a version");
18202
+ }
18203
+ return {
18204
+ packageName,
18205
+ distTag,
18206
+ currentVersion: appPackageVersion,
18207
+ latestVersion,
18208
+ updateAvailable: comparePackageVersions(latestVersion, appPackageVersion) > 0,
18209
+ checkedAtMs: Date.now(),
18210
+ error: "",
18211
+ };
18212
+ } catch (error) {
18213
+ if (error?.name === "AbortError") {
18214
+ throw new Error("npm version check timed out");
18215
+ }
18216
+ throw error;
18217
+ } finally {
18218
+ clearTimeout(timer);
18219
+ }
18220
+ }
18221
+
18222
+ function comparePackageVersions(left, right) {
18223
+ const a = parsePackageVersion(left);
18224
+ const b = parsePackageVersion(right);
18225
+ for (let index = 0; index < 3; index += 1) {
18226
+ if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1;
18227
+ }
18228
+ if (!a.prerelease.length && b.prerelease.length) return 1;
18229
+ if (a.prerelease.length && !b.prerelease.length) return -1;
18230
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
18231
+ for (let index = 0; index < length; index += 1) {
18232
+ const av = a.prerelease[index];
18233
+ const bv = b.prerelease[index];
18234
+ if (av == null && bv == null) return 0;
18235
+ if (av == null) return -1;
18236
+ if (bv == null) return 1;
18237
+ const an = /^\d+$/u.test(av) ? Number(av) : null;
18238
+ const bn = /^\d+$/u.test(bv) ? Number(bv) : null;
18239
+ if (an != null && bn != null && an !== bn) return an > bn ? 1 : -1;
18240
+ if (an != null && bn == null) return -1;
18241
+ if (an == null && bn != null) return 1;
18242
+ const cmp = av.localeCompare(bv);
18243
+ if (cmp !== 0) return cmp > 0 ? 1 : -1;
18244
+ }
18245
+ return 0;
18246
+ }
18247
+
18248
+ function parsePackageVersion(value) {
18249
+ const normalized = cleanText(value || "").replace(/^v/iu, "").split("+")[0];
18250
+ const [core, prerelease = ""] = normalized.split("-", 2);
18251
+ const parts = core.split(".").map((part) => Number(part)).map((part) => (Number.isFinite(part) && part >= 0 ? part : 0));
18252
+ while (parts.length < 3) parts.push(0);
18253
+ return {
18254
+ parts: parts.slice(0, 3),
18255
+ prerelease: prerelease ? prerelease.split(".").map((part) => cleanText(part)).filter(Boolean) : [],
18256
+ };
18257
+ }
18258
+
17630
18259
  function mergeHazbaseAccountSummaries(existing, incoming) {
17631
18260
  const merged = new Map();
17632
18261
  for (const entry of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(incoming) ? incoming : [])]) {
@@ -17644,6 +18273,124 @@ function resolveHazbaseAccountForChain(accounts, chainId) {
17644
18273
  return list.find((entry) => Number(entry?.chainId) === normalizedChainId && cleanText(entry?.smartAccountAddress || "")) || null;
17645
18274
  }
17646
18275
 
18276
+
18277
+ async function completeHazbaseWalletPaymentApproval({ config, runtime, state, approval }) {
18278
+ const hazbase = normalizeHazbaseState(state.hazbase);
18279
+ if (!hazbase.accessToken) throw codedError("hazbase-auth-required", 401);
18280
+ if (!hazbase.deviceBindingId) throw codedError("hazbase-passkey-required", 409);
18281
+ if (!hazbase.highTrustToken) throw codedError("hazbase-reauth-required", 409);
18282
+ const chainId = Number(approval.rawParams?.chainId || 0);
18283
+ const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
18284
+ if (!account?.smartAccountAddress) throw codedError("hazbase-wallet-account-missing", 409);
18285
+
18286
+ let result;
18287
+ try {
18288
+ result = await postHazbaseJson(config, "/api/payments/x402/hazbase-wallet/pay", {
18289
+ paymentRequestId: approval.paymentRequestId,
18290
+ deviceBindingId: hazbase.deviceBindingId,
18291
+ highTrustToken: hazbase.highTrustToken,
18292
+ smartAccountAddress: account.smartAccountAddress,
18293
+ waitForReceipt: true,
18294
+ }, hazbase.accessToken, 130_000);
18295
+ } catch (error) {
18296
+ if (isHazbaseInvalidAppSessionError(error)) {
18297
+ markHazbaseSessionInvalid(state);
18298
+ await saveState(config.stateFile, state);
18299
+ throw codedError("hazbase-session-expired", 401, {
18300
+ message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
18301
+ });
18302
+ }
18303
+ throw error;
18304
+ }
18305
+
18306
+ if (!result?.xPayment) {
18307
+ throw codedError(result?.errorCode || result?.error || "hazbase-wallet-payment-failed", 502);
18308
+ }
18309
+
18310
+ const payload = {
18311
+ paid: true,
18312
+ decision: "accept",
18313
+ paymentRequestId: approval.paymentRequestId,
18314
+ xPayment: cleanText(result.xPayment || ""),
18315
+ payer: cleanText(result.payer || account.smartAccountAddress || ""),
18316
+ chainId: Number(result.chainId || chainId),
18317
+ network: cleanText(result.network || approval.rawParams?.network || ""),
18318
+ relayMode: cleanText(result.relayMode || ""),
18319
+ submittedUserOpHash: cleanText(result.submittedUserOpHash || ""),
18320
+ transactionHash: cleanText(result.transactionHash || ""),
18321
+ payment: result,
18322
+ };
18323
+
18324
+ approval.resolved = true;
18325
+ approval.resolving = false;
18326
+ runtime.nativeApprovalsByToken.delete(approval.token);
18327
+ runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
18328
+ approval.resolveClaudeWaiter?.(payload);
18329
+ const stateChanged = recordActionHistoryItem({
18330
+ config,
18331
+ runtime,
18332
+ state,
18333
+ kind: "approval",
18334
+ stableId: `approval:${approval.requestKey}:${Date.now()}`,
18335
+ token: approval.token,
18336
+ title: approval.title,
18337
+ threadLabel: approval.threadLabel || "",
18338
+ messageText: `${approvalDecisionMessage("accept", config.defaultLocale, approval.provider)}\n\n${approval.messageText}\n\nTransaction: ${payload.transactionHash || payload.submittedUserOpHash}`,
18339
+ summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage("accept", config.defaultLocale, approval.provider),
18340
+ fileRefs: [],
18341
+ diffText: "",
18342
+ diffSource: "",
18343
+ diffAvailable: false,
18344
+ diffAddedLines: 0,
18345
+ diffRemovedLines: 0,
18346
+ outcome: "approved",
18347
+ provider: approval.provider || "viveworker",
18348
+ });
18349
+ if (stateChanged) await saveState(config.stateFile, state);
18350
+ console.log(`[hazbase-wallet-payment] ${approval.requestKey} | ${payload.transactionHash || payload.submittedUserOpHash}`);
18351
+ return { ok: true, ...payload };
18352
+ }
18353
+
18354
+ async function postHazbaseJson(config, endpoint, body, bearerToken, timeoutMs = 30_000) {
18355
+ const baseUrl = stripTrailingSlash(config?.hazbaseApiUrl || "");
18356
+ if (!baseUrl) throw codedError("hazbase-api-not-configured", 503);
18357
+ const controller = new AbortController();
18358
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
18359
+ timer.unref?.();
18360
+ try {
18361
+ const response = await fetch(`${baseUrl}${endpoint}`, {
18362
+ method: "POST",
18363
+ headers: {
18364
+ "content-type": "application/json",
18365
+ accept: "application/json",
18366
+ authorization: `Bearer ${bearerToken}`,
18367
+ "x-request-id": `viveworker_${crypto.randomUUID()}`,
18368
+ },
18369
+ body: JSON.stringify(body),
18370
+ signal: controller.signal,
18371
+ });
18372
+ const payload = await response.json().catch(() => null);
18373
+ const data = payload?.data || payload || {};
18374
+ if (!response.ok) {
18375
+ throw codedError(data?.errorCode || data?.code || data?.error || `hazbase-api-${response.status}`, response.status, data);
18376
+ }
18377
+ return data;
18378
+ } catch (error) {
18379
+ if (error?.name === "AbortError") throw codedError("hazbase-api-timeout", 504);
18380
+ throw error;
18381
+ } finally {
18382
+ clearTimeout(timer);
18383
+ }
18384
+ }
18385
+
18386
+ function codedError(code, statusCode = 500, details = null) {
18387
+ const error = new Error(code);
18388
+ error.code = code;
18389
+ error.statusCode = statusCode;
18390
+ error.details = details;
18391
+ return error;
18392
+ }
18393
+
17647
18394
  function validateConfig(config) {
17648
18395
  let publicBaseUrl = null;
17649
18396
  try {