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.
- package/package.json +18 -10
- package/scripts/a2a-cli.mjs +21 -4
- package/scripts/share-cli.mjs +322 -41
- package/scripts/viveworker-bridge.mjs +582 -67
- package/scripts/viveworker.mjs +11 -7
- package/web/app.css +195 -0
- package/web/app.js +1027 -239
- package/web/build-id.js +1 -1
- package/web/i18n.js +114 -18
- package/web/remote-pairing/transport.js +15 -3
- package/web/sw.js +4 -0
|
@@ -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
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
14546
|
+
lastError = candidateError;
|
|
14398
14547
|
console.log(
|
|
14399
|
-
`[completion-reply] failed candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} error=${normalizeIpcErrorMessage(
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
16855
|
+
paymentCapabilities,
|
|
16856
|
+
agentPaymentDefaults,
|
|
16857
|
+
supportedPayments,
|
|
16667
16858
|
supportedChains,
|
|
16668
|
-
defaultPaymentNetwork
|
|
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
|
|
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
|
|
16684
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|