viveworker 0.7.0-beta.2 → 0.7.0
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/README.md +10 -0
- package/package.json +2 -1
- package/scripts/share-cli.mjs +553 -3
- package/scripts/viveworker-bridge.mjs +1147 -148
- package/viveworker.env.example +4 -0
- package/web/app.css +126 -4
- package/web/app.js +207 -57
- package/web/i18n.js +42 -8
- package/web/index.html +84 -0
- package/web/sw.js +2 -2
|
@@ -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);
|
|
@@ -673,7 +680,7 @@ function withNotificationIcon(kind, title) {
|
|
|
673
680
|
|
|
674
681
|
function normalizeTimelineOutcome(value) {
|
|
675
682
|
const normalized = cleanText(value || "").toLowerCase();
|
|
676
|
-
return ["pending", "approved", "rejected", "implemented", "dismissed", "submitted"].includes(normalized)
|
|
683
|
+
return ["pending", "approved", "rejected", "failed", "implemented", "dismissed", "submitted"].includes(normalized)
|
|
677
684
|
? normalized
|
|
678
685
|
: "";
|
|
679
686
|
}
|
|
@@ -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 (
|
|
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
|
|
2914
|
+
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
2907
2915
|
}
|
|
2908
2916
|
|
|
2909
2917
|
function historyToken(stableId) {
|
|
@@ -2985,6 +2993,70 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2985
2993
|
return deduped;
|
|
2986
2994
|
}
|
|
2987
2995
|
|
|
2996
|
+
// Cheap structural diff used by record* helpers to decide whether a rebuilt
|
|
2997
|
+
// timeline / history / code-events array actually changed in a way worth
|
|
2998
|
+
// persisting. The previous implementation `JSON.stringify(a) !== JSON.stringify(b)`
|
|
2999
|
+
// allocated several megabytes of intermediate strings on every call when the
|
|
3000
|
+
// projection included `diffText` (5+ KB per file_event × ~250 entries × 2
|
|
3001
|
+
// sides), which dominated scavenger GC time in busy sessions and showed up as
|
|
3002
|
+
// a 25% CPU sink in `JsonStringifier::Serialize_*` under V8 sampling. Walking
|
|
3003
|
+
// the arrays in lockstep and bailing on the first mismatched primitive lets
|
|
3004
|
+
// the common "fresh entry prepended" case exit on the very first stableId
|
|
3005
|
+
// compare with zero allocations.
|
|
3006
|
+
//
|
|
3007
|
+
// Field semantics:
|
|
3008
|
+
// - String / number / boolean fields use value-equality via `===` (works
|
|
3009
|
+
// across object reconstruction by `normalizeTimelineEntry`).
|
|
3010
|
+
// - `Array` fields (e.g. `previousFileRefs`) shallow-compare lengths and the
|
|
3011
|
+
// `path` of each element rather than serializing.
|
|
3012
|
+
// - `null` / `undefined` are treated as equal to each other but distinct from
|
|
3013
|
+
// any defined value.
|
|
3014
|
+
function timelineProjectionChanged(nextItems, previousItems, fieldKeys) {
|
|
3015
|
+
const a = Array.isArray(nextItems) ? nextItems : [];
|
|
3016
|
+
const b = Array.isArray(previousItems) ? previousItems : [];
|
|
3017
|
+
if (a.length !== b.length) {
|
|
3018
|
+
return true;
|
|
3019
|
+
}
|
|
3020
|
+
for (let i = 0; i < a.length; i++) {
|
|
3021
|
+
const ai = a[i];
|
|
3022
|
+
const bi = b[i];
|
|
3023
|
+
if (ai === bi) continue;
|
|
3024
|
+
if (!ai || !bi) return true;
|
|
3025
|
+
for (let k = 0; k < fieldKeys.length; k++) {
|
|
3026
|
+
const key = fieldKeys[k];
|
|
3027
|
+
const av = ai[key];
|
|
3028
|
+
const bv = bi[key];
|
|
3029
|
+
if (av === bv) continue;
|
|
3030
|
+
if (av == null && bv == null) continue;
|
|
3031
|
+
if (Array.isArray(av) && Array.isArray(bv)) {
|
|
3032
|
+
if (av.length !== bv.length) return true;
|
|
3033
|
+
let arrayDiffers = false;
|
|
3034
|
+
for (let j = 0; j < av.length; j++) {
|
|
3035
|
+
const aj = av[j];
|
|
3036
|
+
const bj = bv[j];
|
|
3037
|
+
if (aj === bj) continue;
|
|
3038
|
+
if (!aj || !bj) {
|
|
3039
|
+
arrayDiffers = true;
|
|
3040
|
+
break;
|
|
3041
|
+
}
|
|
3042
|
+
// Most array-of-object fields here are fileRef-shaped: identity is
|
|
3043
|
+
// captured by `path`. Falling back to JSON for unrecognised shapes
|
|
3044
|
+
// is unnecessary — a path mismatch (or a null path either side) is
|
|
3045
|
+
// sufficient to mark the projection changed.
|
|
3046
|
+
if (aj.path !== bj.path) {
|
|
3047
|
+
arrayDiffers = true;
|
|
3048
|
+
break;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
if (arrayDiffers) return true;
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
return true;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return false;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2988
3060
|
function isCodeEventEntry(raw) {
|
|
2989
3061
|
if (!isPlainObject(raw)) {
|
|
2990
3062
|
return false;
|
|
@@ -3043,7 +3115,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
3043
3115
|
if (!isPlainObject(raw)) {
|
|
3044
3116
|
return null;
|
|
3045
3117
|
}
|
|
3046
|
-
if (
|
|
3118
|
+
if (shouldHideInternalTimelineItem(raw)) {
|
|
3047
3119
|
return null;
|
|
3048
3120
|
}
|
|
3049
3121
|
|
|
@@ -3137,7 +3209,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
3137
3209
|
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
3138
3210
|
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
3139
3211
|
};
|
|
3140
|
-
return
|
|
3212
|
+
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
3141
3213
|
}
|
|
3142
3214
|
|
|
3143
3215
|
function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
@@ -3150,35 +3222,21 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
|
3150
3222
|
[normalized, ...runtime.recentTimelineEntries.filter((item) => item.stableId !== normalized.stableId)],
|
|
3151
3223
|
config.maxTimelineEntries
|
|
3152
3224
|
);
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
JSON.stringify(
|
|
3169
|
-
runtime.recentTimelineEntries.map((item) => [
|
|
3170
|
-
item.stableId,
|
|
3171
|
-
item.title,
|
|
3172
|
-
item.createdAtMs,
|
|
3173
|
-
item.diffAvailable,
|
|
3174
|
-
item.diffSource,
|
|
3175
|
-
item.diffAddedLines,
|
|
3176
|
-
item.diffRemovedLines,
|
|
3177
|
-
item.diffText,
|
|
3178
|
-
item.previousFileRefs,
|
|
3179
|
-
item.cwd,
|
|
3180
|
-
])
|
|
3181
|
-
);
|
|
3225
|
+
// Note: diffText is intentionally omitted from the projection — the
|
|
3226
|
+
// line-count fields (`diffAddedLines` / `diffRemovedLines`) are a sufficient
|
|
3227
|
+
// proxy for "diff content changed" and avoid materialising a megabyte-class
|
|
3228
|
+
// intermediate string on every record call.
|
|
3229
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentTimelineEntries, [
|
|
3230
|
+
"stableId",
|
|
3231
|
+
"title",
|
|
3232
|
+
"createdAtMs",
|
|
3233
|
+
"diffAvailable",
|
|
3234
|
+
"diffSource",
|
|
3235
|
+
"diffAddedLines",
|
|
3236
|
+
"diffRemovedLines",
|
|
3237
|
+
"previousFileRefs",
|
|
3238
|
+
"cwd",
|
|
3239
|
+
]);
|
|
3182
3240
|
runtime.recentTimelineEntries = nextItems;
|
|
3183
3241
|
state.recentTimelineEntries = nextItems;
|
|
3184
3242
|
return changed;
|
|
@@ -3197,35 +3255,19 @@ function recordCodeEvent({ config, runtime, state, entry }) {
|
|
|
3197
3255
|
[normalized, ...runtime.recentCodeEvents.filter((item) => item.stableId !== normalized.stableId)],
|
|
3198
3256
|
config.maxCodeEvents
|
|
3199
3257
|
);
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
])
|
|
3214
|
-
) !==
|
|
3215
|
-
JSON.stringify(
|
|
3216
|
-
runtime.recentCodeEvents.map((item) => [
|
|
3217
|
-
item.stableId,
|
|
3218
|
-
item.title,
|
|
3219
|
-
item.createdAtMs,
|
|
3220
|
-
item.diffAvailable,
|
|
3221
|
-
item.diffSource,
|
|
3222
|
-
item.diffAddedLines,
|
|
3223
|
-
item.diffRemovedLines,
|
|
3224
|
-
item.diffText,
|
|
3225
|
-
item.previousFileRefs,
|
|
3226
|
-
item.cwd,
|
|
3227
|
-
])
|
|
3228
|
-
);
|
|
3258
|
+
// See `recordTimelineEntry` for the rationale on dropping `diffText` from
|
|
3259
|
+
// the projection — same megabyte-stringify hot path; same line-count proxy.
|
|
3260
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentCodeEvents, [
|
|
3261
|
+
"stableId",
|
|
3262
|
+
"title",
|
|
3263
|
+
"createdAtMs",
|
|
3264
|
+
"diffAvailable",
|
|
3265
|
+
"diffSource",
|
|
3266
|
+
"diffAddedLines",
|
|
3267
|
+
"diffRemovedLines",
|
|
3268
|
+
"previousFileRefs",
|
|
3269
|
+
"cwd",
|
|
3270
|
+
]);
|
|
3229
3271
|
runtime.recentCodeEvents = nextItems;
|
|
3230
3272
|
state.recentCodeEvents = nextItems;
|
|
3231
3273
|
if (changed) {
|
|
@@ -3249,37 +3291,19 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
|
|
|
3249
3291
|
],
|
|
3250
3292
|
config.maxCodeEvents
|
|
3251
3293
|
);
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
item.cwd,
|
|
3266
|
-
])
|
|
3267
|
-
) !==
|
|
3268
|
-
JSON.stringify(
|
|
3269
|
-
runtime.recentCodeEvents.map((item) => [
|
|
3270
|
-
item.stableId,
|
|
3271
|
-
item.title,
|
|
3272
|
-
item.createdAtMs,
|
|
3273
|
-
item.threadLabel,
|
|
3274
|
-
item.diffAvailable,
|
|
3275
|
-
item.diffSource,
|
|
3276
|
-
item.diffAddedLines,
|
|
3277
|
-
item.diffRemovedLines,
|
|
3278
|
-
item.diffText,
|
|
3279
|
-
item.previousFileRefs,
|
|
3280
|
-
item.cwd,
|
|
3281
|
-
])
|
|
3282
|
-
);
|
|
3294
|
+
// See `recordTimelineEntry` for the rationale on dropping `diffText`.
|
|
3295
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentCodeEvents, [
|
|
3296
|
+
"stableId",
|
|
3297
|
+
"title",
|
|
3298
|
+
"createdAtMs",
|
|
3299
|
+
"threadLabel",
|
|
3300
|
+
"diffAvailable",
|
|
3301
|
+
"diffSource",
|
|
3302
|
+
"diffAddedLines",
|
|
3303
|
+
"diffRemovedLines",
|
|
3304
|
+
"previousFileRefs",
|
|
3305
|
+
"cwd",
|
|
3306
|
+
]);
|
|
3283
3307
|
runtime.recentCodeEvents = nextItems;
|
|
3284
3308
|
state.recentCodeEvents = nextItems;
|
|
3285
3309
|
if (changed) {
|
|
@@ -3345,9 +3369,11 @@ function recordHistoryItem({ config, runtime, state, item }) {
|
|
|
3345
3369
|
[normalized, ...runtime.recentHistoryItems.filter((entry) => entry.stableId !== normalized.stableId)],
|
|
3346
3370
|
config.maxHistoryItems
|
|
3347
3371
|
);
|
|
3348
|
-
const changed =
|
|
3349
|
-
|
|
3350
|
-
|
|
3372
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentHistoryItems, [
|
|
3373
|
+
"stableId",
|
|
3374
|
+
"title",
|
|
3375
|
+
"createdAtMs",
|
|
3376
|
+
]);
|
|
3351
3377
|
runtime.recentHistoryItems = nextItems;
|
|
3352
3378
|
state.recentHistoryItems = nextItems;
|
|
3353
3379
|
return changed;
|
|
@@ -3776,7 +3802,7 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3776
3802
|
if (config.webUiEnabled) {
|
|
3777
3803
|
let claudeTranscriptChanged = false;
|
|
3778
3804
|
if (now - runtime.lastClaudeScanAt >= config.directoryScanIntervalMs) {
|
|
3779
|
-
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir);
|
|
3805
|
+
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir, config.claudeTranscriptMaxAgeMs);
|
|
3780
3806
|
runtime.lastClaudeScanAt = now;
|
|
3781
3807
|
}
|
|
3782
3808
|
let claudeSessionTitlesChanged = false;
|
|
@@ -4114,7 +4140,16 @@ async function refreshClaudeSessionTitles(runtime) {
|
|
|
4114
4140
|
return changed;
|
|
4115
4141
|
}
|
|
4116
4142
|
|
|
4117
|
-
async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
4143
|
+
async function listClaudeTranscriptFiles(claudeProjectsDir, maxAgeMs = 0) {
|
|
4144
|
+
// ~/.claude/projects can accumulate thousands of .jsonl files (one per
|
|
4145
|
+
// session, never garbage-collected by Claude Code itself). Returning all of
|
|
4146
|
+
// them means processClaudeTranscriptFile fs.stat()s each every poll
|
|
4147
|
+
// iteration — for a long-time user that's 3000+ stats every 2.5 s, all but
|
|
4148
|
+
// a handful of which are guaranteed-cold. Filter by mtime here so the
|
|
4149
|
+
// per-iteration working set tracks "active sessions", not "total sessions
|
|
4150
|
+
// ever". maxAgeMs=0 disables filtering for callers who explicitly want
|
|
4151
|
+
// everything (none in the bridge today).
|
|
4152
|
+
const cutoffMs = maxAgeMs > 0 ? Date.now() - maxAgeMs : 0;
|
|
4118
4153
|
try {
|
|
4119
4154
|
const result = [];
|
|
4120
4155
|
const entries = await fs.readdir(claudeProjectsDir, { withFileTypes: true });
|
|
@@ -4125,7 +4160,17 @@ async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
|
4125
4160
|
const files = await fs.readdir(projectPath, { withFileTypes: true });
|
|
4126
4161
|
for (const file of files) {
|
|
4127
4162
|
if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
|
|
4128
|
-
|
|
4163
|
+
const fullPath = path.join(projectPath, file.name);
|
|
4164
|
+
if (cutoffMs > 0) {
|
|
4165
|
+
try {
|
|
4166
|
+
const stat = await fs.stat(fullPath);
|
|
4167
|
+
if (stat.mtimeMs < cutoffMs) continue;
|
|
4168
|
+
} catch {
|
|
4169
|
+
// skip unreadable file (vanished or permissioned out)
|
|
4170
|
+
continue;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
result.push(fullPath);
|
|
4129
4174
|
}
|
|
4130
4175
|
} catch {
|
|
4131
4176
|
// skip unreadable project dirs
|
|
@@ -5669,6 +5714,12 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
5669
5714
|
return false;
|
|
5670
5715
|
}
|
|
5671
5716
|
|
|
5717
|
+
if (shouldSuppressInternalScannedEvent(event)) {
|
|
5718
|
+
state.seenEvents[event.id] = event.timestampMs || Date.now();
|
|
5719
|
+
trimSeenEvents(state.seenEvents, config.maxSeenEvents);
|
|
5720
|
+
return true;
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5672
5723
|
let dirty = false;
|
|
5673
5724
|
|
|
5674
5725
|
if (event.kind === "task_complete") {
|
|
@@ -6528,6 +6579,150 @@ async function createNativeApproval({ config, runtime, conversationId, request,
|
|
|
6528
6579
|
};
|
|
6529
6580
|
}
|
|
6530
6581
|
|
|
6582
|
+
function createX402PaymentApproval({ config, body, now = Date.now() }) {
|
|
6583
|
+
const payment = normalizeX402PaymentApprovalBody(body);
|
|
6584
|
+
if (!payment) {
|
|
6585
|
+
return null;
|
|
6586
|
+
}
|
|
6587
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
6588
|
+
const requestId = cleanText(body.paymentRequestId || body.requestId || crypto.randomUUID());
|
|
6589
|
+
const requestKey = `payment:${requestId}`;
|
|
6590
|
+
const title = payment.amountUsdc
|
|
6591
|
+
? `Payment approval — ${payment.amountUsdc} USDC`
|
|
6592
|
+
: "Payment approval";
|
|
6593
|
+
return {
|
|
6594
|
+
token,
|
|
6595
|
+
requestKey,
|
|
6596
|
+
conversationId: "payments",
|
|
6597
|
+
requestId,
|
|
6598
|
+
ownerClientId: null,
|
|
6599
|
+
kind: "payment",
|
|
6600
|
+
threadLabel: "x402 payment",
|
|
6601
|
+
title,
|
|
6602
|
+
messageText: formatX402PaymentApprovalMessage(payment),
|
|
6603
|
+
reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
|
|
6604
|
+
rawParams: payment,
|
|
6605
|
+
cwd: "",
|
|
6606
|
+
workspaceRoot: "",
|
|
6607
|
+
fileRefs: [],
|
|
6608
|
+
diffText: "",
|
|
6609
|
+
diffAvailable: false,
|
|
6610
|
+
diffSource: "",
|
|
6611
|
+
diffAddedLines: 0,
|
|
6612
|
+
diffRemovedLines: 0,
|
|
6613
|
+
createdAtMs: now,
|
|
6614
|
+
resolved: false,
|
|
6615
|
+
resolving: false,
|
|
6616
|
+
resolveClaudeWaiter: null,
|
|
6617
|
+
provider: "viveworker",
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6621
|
+
|
|
6622
|
+
function createHazbaseWalletPaymentApproval({ config, body, now = Date.now() }) {
|
|
6623
|
+
const payment = normalizeX402PaymentApprovalBody(body);
|
|
6624
|
+
if (!payment) return null;
|
|
6625
|
+
const paymentRequestId = cleanText(body.paymentRequestId || body.requestId || "");
|
|
6626
|
+
if (!/^[a-zA-Z0-9:_-]{8,160}$/u.test(paymentRequestId)) return null;
|
|
6627
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
6628
|
+
const requestKey = `hazbase_wallet_payment:${paymentRequestId}`;
|
|
6629
|
+
const title = payment.amountUsdc
|
|
6630
|
+
? `Hazbase wallet payment — ${payment.amountUsdc} USDC`
|
|
6631
|
+
: "Hazbase wallet payment";
|
|
6632
|
+
return {
|
|
6633
|
+
token,
|
|
6634
|
+
requestKey,
|
|
6635
|
+
conversationId: "payments",
|
|
6636
|
+
requestId: paymentRequestId,
|
|
6637
|
+
paymentRequestId,
|
|
6638
|
+
ownerClientId: null,
|
|
6639
|
+
kind: "hazbase_wallet_payment",
|
|
6640
|
+
threadLabel: "x402 payment",
|
|
6641
|
+
title,
|
|
6642
|
+
messageText: formatHazbaseWalletPaymentApprovalMessage(payment),
|
|
6643
|
+
reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
|
|
6644
|
+
rawParams: payment,
|
|
6645
|
+
cwd: "",
|
|
6646
|
+
workspaceRoot: "",
|
|
6647
|
+
fileRefs: [],
|
|
6648
|
+
diffText: "",
|
|
6649
|
+
diffAvailable: false,
|
|
6650
|
+
diffSource: "",
|
|
6651
|
+
diffAddedLines: 0,
|
|
6652
|
+
diffRemovedLines: 0,
|
|
6653
|
+
createdAtMs: now,
|
|
6654
|
+
resolved: false,
|
|
6655
|
+
resolving: false,
|
|
6656
|
+
resolveClaudeWaiter: null,
|
|
6657
|
+
provider: "viveworker",
|
|
6658
|
+
};
|
|
6659
|
+
}
|
|
6660
|
+
|
|
6661
|
+
function formatHazbaseWalletPaymentApprovalMessage(payment) {
|
|
6662
|
+
const lines = [
|
|
6663
|
+
"Hazbase Smart Wallet payment requested.",
|
|
6664
|
+
"",
|
|
6665
|
+
`Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
|
|
6666
|
+
`Network: ${payment.network} (chainId ${payment.chainId})`,
|
|
6667
|
+
`Pay to: ${payment.payTo}`,
|
|
6668
|
+
`Asset: ${payment.asset}`,
|
|
6669
|
+
`Resource: ${payment.resource}`,
|
|
6670
|
+
"",
|
|
6671
|
+
"Approving will ask for your passkey, then hazBase will submit a gasless Smart Wallet payment.",
|
|
6672
|
+
];
|
|
6673
|
+
if (payment.description) lines.splice(7, 0, `Description: ${payment.description}`);
|
|
6674
|
+
if (payment.url && payment.url !== payment.resource) lines.splice(-1, 0, `URL: ${payment.url}`);
|
|
6675
|
+
return lines.join("\n");
|
|
6676
|
+
}
|
|
6677
|
+
|
|
6678
|
+
function normalizeX402PaymentApprovalBody(body) {
|
|
6679
|
+
if (!isPlainObject(body)) return null;
|
|
6680
|
+
const payment = isPlainObject(body.payment) ? body.payment : {};
|
|
6681
|
+
const network = cleanText(payment.network || "");
|
|
6682
|
+
const chainId = Number(payment.chainId) || 0;
|
|
6683
|
+
const amountAtomic = cleanText(payment.amountAtomic || "");
|
|
6684
|
+
const amountUsdc = cleanText(payment.amountUsdc || "");
|
|
6685
|
+
const payTo = cleanText(payment.payTo || "");
|
|
6686
|
+
const asset = cleanText(payment.asset || "");
|
|
6687
|
+
const resource = cleanText(payment.resource || body.url || "");
|
|
6688
|
+
const description = cleanText(payment.description || "");
|
|
6689
|
+
const url = cleanText(body.url || resource);
|
|
6690
|
+
if (!network || !chainId || !amountAtomic || !payTo || !asset || !resource) {
|
|
6691
|
+
return null;
|
|
6692
|
+
}
|
|
6693
|
+
return {
|
|
6694
|
+
url,
|
|
6695
|
+
network,
|
|
6696
|
+
chainId,
|
|
6697
|
+
amountAtomic,
|
|
6698
|
+
amountUsdc,
|
|
6699
|
+
payTo,
|
|
6700
|
+
asset,
|
|
6701
|
+
resource,
|
|
6702
|
+
description,
|
|
6703
|
+
};
|
|
6704
|
+
}
|
|
6705
|
+
|
|
6706
|
+
function formatX402PaymentApprovalMessage(payment) {
|
|
6707
|
+
const lines = [
|
|
6708
|
+
"x402 payment approval requested.",
|
|
6709
|
+
"",
|
|
6710
|
+
`Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
|
|
6711
|
+
`Network: ${payment.network} (chainId ${payment.chainId})`,
|
|
6712
|
+
`Pay to: ${payment.payTo}`,
|
|
6713
|
+
`Asset: ${payment.asset}`,
|
|
6714
|
+
`Resource: ${payment.resource}`,
|
|
6715
|
+
];
|
|
6716
|
+
if (payment.description) {
|
|
6717
|
+
lines.push(`Description: ${payment.description}`);
|
|
6718
|
+
}
|
|
6719
|
+
if (payment.url && payment.url !== payment.resource) {
|
|
6720
|
+
lines.push(`URL: ${payment.url}`);
|
|
6721
|
+
}
|
|
6722
|
+
lines.push("", "Approve only if the amount, recipient, network, and resource match what you expect.");
|
|
6723
|
+
return lines.join("\n");
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6531
6726
|
async function buildNativeApprovalPayload({ config, runtime, conversationId, request, token }) {
|
|
6532
6727
|
const kind = nativeApprovalKind(request.method);
|
|
6533
6728
|
if (!kind) {
|
|
@@ -8767,6 +8962,18 @@ function isHiddenClaudeInternalScoringText(text) {
|
|
|
8767
8962
|
return /^You are scoring Moltbook posts? for an AI agent\b/iu.test(single);
|
|
8768
8963
|
}
|
|
8769
8964
|
|
|
8965
|
+
function isHiddenCodexApprovalAssessmentText(text) {
|
|
8966
|
+
const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
|
|
8967
|
+
if (!single) {
|
|
8968
|
+
return false;
|
|
8969
|
+
}
|
|
8970
|
+
return /\bThe following is the Codex agent history(?: added since your last approval assessment)?\b/iu.test(single);
|
|
8971
|
+
}
|
|
8972
|
+
|
|
8973
|
+
function shouldHideInternalTimelineItem(item) {
|
|
8974
|
+
return shouldHideClaudeInternalItem(item) || shouldHideCodexInternalApprovalItem(item);
|
|
8975
|
+
}
|
|
8976
|
+
|
|
8770
8977
|
function shouldHideClaudeInternalItem(item) {
|
|
8771
8978
|
if (!isPlainObject(item)) {
|
|
8772
8979
|
return false;
|
|
@@ -8786,6 +8993,48 @@ function shouldHideClaudeInternalItem(item) {
|
|
|
8786
8993
|
);
|
|
8787
8994
|
}
|
|
8788
8995
|
|
|
8996
|
+
function shouldSuppressInternalScannedEvent(event) {
|
|
8997
|
+
if (!isPlainObject(event)) {
|
|
8998
|
+
return false;
|
|
8999
|
+
}
|
|
9000
|
+
const provider = normalizeProvider(event.provider || "codex");
|
|
9001
|
+
if (provider !== "codex") {
|
|
9002
|
+
return false;
|
|
9003
|
+
}
|
|
9004
|
+
|
|
9005
|
+
return shouldHideCodexInternalApprovalItem({
|
|
9006
|
+
provider,
|
|
9007
|
+
kind: cleanText(event.kind || ""),
|
|
9008
|
+
title: event.title,
|
|
9009
|
+
threadLabel: event.threadLabel,
|
|
9010
|
+
summary: event.summary ?? event.message,
|
|
9011
|
+
messageText: event.messageText ?? event.detailText ?? event.message,
|
|
9012
|
+
detailText: event.detailText,
|
|
9013
|
+
message: event.message,
|
|
9014
|
+
});
|
|
9015
|
+
}
|
|
9016
|
+
|
|
9017
|
+
function shouldHideCodexInternalApprovalItem(item) {
|
|
9018
|
+
if (!isPlainObject(item)) {
|
|
9019
|
+
return false;
|
|
9020
|
+
}
|
|
9021
|
+
if (normalizeProvider(item.provider) !== "codex") {
|
|
9022
|
+
return false;
|
|
9023
|
+
}
|
|
9024
|
+
|
|
9025
|
+
const title = cleanText(item.title ?? "");
|
|
9026
|
+
const threadLabel = cleanText(item.threadLabel ?? "");
|
|
9027
|
+
if (isHiddenCodexApprovalAssessmentText(title) || isHiddenCodexApprovalAssessmentText(threadLabel)) {
|
|
9028
|
+
return true;
|
|
9029
|
+
}
|
|
9030
|
+
return (
|
|
9031
|
+
isHiddenCodexApprovalAssessmentText(item.messageText) ||
|
|
9032
|
+
isHiddenCodexApprovalAssessmentText(item.summary) ||
|
|
9033
|
+
isHiddenCodexApprovalAssessmentText(item.detailText) ||
|
|
9034
|
+
isHiddenCodexApprovalAssessmentText(item.message)
|
|
9035
|
+
);
|
|
9036
|
+
}
|
|
9037
|
+
|
|
8789
9038
|
function threadStateArchiveStatus(threadState) {
|
|
8790
9039
|
if (!isPlainObject(threadState)) {
|
|
8791
9040
|
return "";
|
|
@@ -10499,7 +10748,9 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
10499
10748
|
token: approval.token,
|
|
10500
10749
|
threadId: cleanText(approval.conversationId || ""),
|
|
10501
10750
|
threadLabel: approval.threadLabel || "",
|
|
10502
|
-
title:
|
|
10751
|
+
title: cleanText(approval.kind || "") === "payment"
|
|
10752
|
+
? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
|
|
10753
|
+
: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
10503
10754
|
summary: formatNotificationBody(approval.messageText, 100) || approval.messageText,
|
|
10504
10755
|
primaryLabel: t(locale, "server.action.review"),
|
|
10505
10756
|
createdAtMs: Number(approval.createdAtMs) || now,
|
|
@@ -10851,7 +11102,9 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
10851
11102
|
kind: "approval",
|
|
10852
11103
|
threadId: cleanText(approval.conversationId || ""),
|
|
10853
11104
|
threadLabel: approval.threadLabel,
|
|
10854
|
-
title:
|
|
11105
|
+
title: cleanText(approval.kind || "") === "payment"
|
|
11106
|
+
? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
|
|
11107
|
+
: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
10855
11108
|
summary: formatNotificationBody(approval.messageText, 180) || approval.messageText,
|
|
10856
11109
|
messageText: approval.messageText,
|
|
10857
11110
|
outcome: "pending",
|
|
@@ -11058,7 +11311,9 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
|
11058
11311
|
approvalKind,
|
|
11059
11312
|
provider: normalizeProvider(approval.provider),
|
|
11060
11313
|
token: approval.token,
|
|
11061
|
-
title:
|
|
11314
|
+
title: approvalKind === "payment"
|
|
11315
|
+
? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
|
|
11316
|
+
: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
11062
11317
|
threadLabel: approval.threadLabel || "",
|
|
11063
11318
|
createdAtMs: Number(approval.createdAtMs) || 0,
|
|
11064
11319
|
messageHtml: renderMessageHtml(approval.messageText, `<p>${escapeHtml(t(locale, "detail.approvalRequested"))}</p>`),
|
|
@@ -11073,10 +11328,15 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
|
11073
11328
|
readOnly: approval.readOnly === true,
|
|
11074
11329
|
actions: approval.readOnly === true
|
|
11075
11330
|
? []
|
|
11076
|
-
:
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11331
|
+
: approvalKind === "hazbase_wallet_payment"
|
|
11332
|
+
? [
|
|
11333
|
+
{ label: t(locale, "server.action.payWithWallet"), tone: "primary", url: `/api/payments/x402/hazbase-wallet/${encodeURIComponent(approval.token)}/pay`, body: { hazbaseReauth: true } },
|
|
11334
|
+
{ label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
|
|
11335
|
+
]
|
|
11336
|
+
: [
|
|
11337
|
+
{ label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
|
|
11338
|
+
{ label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
|
|
11339
|
+
],
|
|
11080
11340
|
};
|
|
11081
11341
|
if (approvalKind === "plan") {
|
|
11082
11342
|
const planText = String(approval.planText || "");
|
|
@@ -13258,9 +13518,93 @@ function buildWebAppHtml({ pairToken }) {
|
|
|
13258
13518
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
13259
13519
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
13260
13520
|
<link rel="stylesheet" href="/app.css">
|
|
13521
|
+
<style>
|
|
13522
|
+
.boot-splash {
|
|
13523
|
+
position: fixed;
|
|
13524
|
+
inset: 0;
|
|
13525
|
+
z-index: 9999;
|
|
13526
|
+
display: grid;
|
|
13527
|
+
place-items: center;
|
|
13528
|
+
padding: max(1.2rem, env(safe-area-inset-top)) 1rem max(1.2rem, env(safe-area-inset-bottom));
|
|
13529
|
+
color: #f5fbff;
|
|
13530
|
+
background:
|
|
13531
|
+
radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%),
|
|
13532
|
+
radial-gradient(circle at 78% 78%, rgba(79, 131, 216, 0.14), transparent 28%),
|
|
13533
|
+
linear-gradient(180deg, #081015 0%, #091015 100%);
|
|
13534
|
+
transition: opacity 220ms ease, visibility 220ms ease;
|
|
13535
|
+
}
|
|
13536
|
+
.boot-splash__card {
|
|
13537
|
+
width: min(20rem, 82vw);
|
|
13538
|
+
display: grid;
|
|
13539
|
+
justify-items: center;
|
|
13540
|
+
gap: 0.9rem;
|
|
13541
|
+
text-align: center;
|
|
13542
|
+
}
|
|
13543
|
+
.boot-splash__logo {
|
|
13544
|
+
width: clamp(5.4rem, 28vw, 7rem);
|
|
13545
|
+
height: clamp(5.4rem, 28vw, 7rem);
|
|
13546
|
+
border-radius: 28%;
|
|
13547
|
+
background:
|
|
13548
|
+
radial-gradient(circle at 76% 24%, rgba(125, 211, 252, 0.22), transparent 30%),
|
|
13549
|
+
linear-gradient(180deg, rgba(23, 52, 72, 0.96), rgba(9, 17, 23, 0.96));
|
|
13550
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
|
|
13551
|
+
}
|
|
13552
|
+
.boot-splash__title {
|
|
13553
|
+
margin: 0.25rem 0 0;
|
|
13554
|
+
font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
13555
|
+
font-size: clamp(1.65rem, 8vw, 2.4rem);
|
|
13556
|
+
line-height: 1;
|
|
13557
|
+
letter-spacing: -0.04em;
|
|
13558
|
+
}
|
|
13559
|
+
.boot-splash__status {
|
|
13560
|
+
margin: 0;
|
|
13561
|
+
color: rgba(205, 220, 231, 0.72);
|
|
13562
|
+
font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
13563
|
+
font-size: 0.9rem;
|
|
13564
|
+
letter-spacing: 0.02em;
|
|
13565
|
+
}
|
|
13566
|
+
.boot-splash__dots {
|
|
13567
|
+
display: inline-grid;
|
|
13568
|
+
grid-auto-flow: column;
|
|
13569
|
+
gap: 0.34rem;
|
|
13570
|
+
margin-top: 0.15rem;
|
|
13571
|
+
}
|
|
13572
|
+
.boot-splash__dots span {
|
|
13573
|
+
width: 0.42rem;
|
|
13574
|
+
height: 0.42rem;
|
|
13575
|
+
border-radius: 999px;
|
|
13576
|
+
background: rgba(142, 215, 255, 0.88);
|
|
13577
|
+
animation: viveworker-boot-dot 980ms ease-in-out infinite;
|
|
13578
|
+
}
|
|
13579
|
+
.boot-splash__dots span:nth-child(2) { animation-delay: 140ms; }
|
|
13580
|
+
.boot-splash__dots span:nth-child(3) { animation-delay: 280ms; }
|
|
13581
|
+
.viveworker-ready .boot-splash {
|
|
13582
|
+
opacity: 0;
|
|
13583
|
+
visibility: hidden;
|
|
13584
|
+
}
|
|
13585
|
+
@keyframes viveworker-boot-dot {
|
|
13586
|
+
0%, 80%, 100% { transform: translateY(0); opacity: 0.42; }
|
|
13587
|
+
40% { transform: translateY(-0.28rem); opacity: 1; }
|
|
13588
|
+
}
|
|
13589
|
+
@media (prefers-reduced-motion: reduce) {
|
|
13590
|
+
.boot-splash,
|
|
13591
|
+
.boot-splash__dots span {
|
|
13592
|
+
transition: none;
|
|
13593
|
+
animation: none;
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
</style>
|
|
13261
13597
|
<title>viveworker</title>
|
|
13262
13598
|
</head>
|
|
13263
13599
|
<body>
|
|
13600
|
+
<div id="boot-splash" class="boot-splash" role="status" aria-live="polite" aria-label="viveworker is starting">
|
|
13601
|
+
<div class="boot-splash__card">
|
|
13602
|
+
<img class="boot-splash__logo" src="/icons/viveworker-v-pulse.svg" alt="" width="112" height="112" decoding="async">
|
|
13603
|
+
<h1 class="boot-splash__title">viveworker</h1>
|
|
13604
|
+
<p class="boot-splash__status">Starting</p>
|
|
13605
|
+
<span class="boot-splash__dots" aria-hidden="true"><span></span><span></span><span></span></span>
|
|
13606
|
+
</div>
|
|
13607
|
+
</div>
|
|
13264
13608
|
<div id="app"></div>
|
|
13265
13609
|
<script type="module" src="/app.js"></script>
|
|
13266
13610
|
</body>
|
|
@@ -13472,6 +13816,12 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13472
13816
|
return writeJson(res, 200, buildSessionPayload({ config, state, session }));
|
|
13473
13817
|
}
|
|
13474
13818
|
|
|
13819
|
+
if (url.pathname === "/api/version/status" && req.method === "GET") {
|
|
13820
|
+
const session = requireApiSession(req, res, config, state);
|
|
13821
|
+
if (!session) return;
|
|
13822
|
+
return writeJson(res, 200, await getNpmVersionStatus(runtime, config));
|
|
13823
|
+
}
|
|
13824
|
+
|
|
13475
13825
|
// Collapses the PWA's boot-time fan-out (session + inbox + timeline
|
|
13476
13826
|
// + devices) into a single HTTPS round-trip. iOS Safari tears down
|
|
13477
13827
|
// connections aggressively, so each parallel fetch pays its own
|
|
@@ -13929,10 +14279,15 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
|
|
|
13929
14279
|
8453: resolveHazbaseAccountForChain(hazbase.accounts, 8453)?.smartAccountAddress || "",
|
|
13930
14280
|
84532: resolveHazbaseAccountForChain(hazbase.accounts, 84532)?.smartAccountAddress || "",
|
|
13931
14281
|
};
|
|
14282
|
+
const signedIn = Boolean(hazbase.accessToken) && !hazbase.sessionInvalid;
|
|
13932
14283
|
return writeJson(res, 200, {
|
|
13933
14284
|
enabled: true,
|
|
13934
14285
|
apiUrl: config.hazbaseApiUrl,
|
|
13935
|
-
signedIn
|
|
14286
|
+
signedIn,
|
|
14287
|
+
sessionStatus: hazbase.sessionInvalid ? "expired" : signedIn ? "signed_in" : "signed_out",
|
|
14288
|
+
sessionInvalid: hazbase.sessionInvalid,
|
|
14289
|
+
sessionInvalidReason: hazbase.sessionInvalidReason,
|
|
14290
|
+
sessionInvalidAt: hazbase.sessionInvalidAt,
|
|
13936
14291
|
email: hazbase.email,
|
|
13937
14292
|
userId: hazbase.userId,
|
|
13938
14293
|
sessionId: hazbase.sessionId,
|
|
@@ -13995,15 +14350,19 @@ if (url.pathname === "/api/hazbase/verify-otp" && req.method === "POST") {
|
|
|
13995
14350
|
const code = cleanText(body?.code || "");
|
|
13996
14351
|
if (!email || !code) return writeJson(res, 400, { error: "otp-required" });
|
|
13997
14352
|
const result = await verifyHazbaseEmailOtp({ email, code });
|
|
14353
|
+
const previousHazbase = normalizeHazbaseState(state.hazbase);
|
|
13998
14354
|
state.hazbase = {
|
|
13999
|
-
...
|
|
14355
|
+
...previousHazbase,
|
|
14000
14356
|
email: result.email || email,
|
|
14001
14357
|
accessToken: cleanText(result.accessToken || ""),
|
|
14002
14358
|
sessionId: cleanText(result.sessionId || ""),
|
|
14003
14359
|
userId: cleanText(result.userId || ""),
|
|
14004
|
-
accounts: Array.isArray(result.accounts) ? result.accounts : [],
|
|
14360
|
+
accounts: mergeHazbaseAccountSummaries(previousHazbase.accounts, Array.isArray(result.accounts) ? result.accounts : []),
|
|
14005
14361
|
highTrustToken: "",
|
|
14006
14362
|
highTrustExpiresAt: "",
|
|
14363
|
+
sessionInvalid: false,
|
|
14364
|
+
sessionInvalidReason: "",
|
|
14365
|
+
sessionInvalidAt: "",
|
|
14007
14366
|
};
|
|
14008
14367
|
await saveState(config.stateFile, state);
|
|
14009
14368
|
return writeJson(res, 200, result);
|
|
@@ -14024,11 +14383,17 @@ if (url.pathname === "/api/hazbase/passkey/register/challenge" && req.method ===
|
|
|
14024
14383
|
message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
|
|
14025
14384
|
});
|
|
14026
14385
|
}
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14386
|
+
let result;
|
|
14387
|
+
try {
|
|
14388
|
+
result = await requestPasskeyRegistrationChallenge({
|
|
14389
|
+
emailSession: hazbase.accessToken,
|
|
14390
|
+
deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
|
|
14391
|
+
partnerOrigin,
|
|
14392
|
+
});
|
|
14393
|
+
} catch (error) {
|
|
14394
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14395
|
+
throw error;
|
|
14396
|
+
}
|
|
14032
14397
|
return writeJson(res, 200, result);
|
|
14033
14398
|
}
|
|
14034
14399
|
|
|
@@ -14038,16 +14403,25 @@ if (url.pathname === "/api/hazbase/passkey/register/complete" && req.method ===
|
|
|
14038
14403
|
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14039
14404
|
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14040
14405
|
const body = await parseJsonBody(req);
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14406
|
+
let result;
|
|
14407
|
+
try {
|
|
14408
|
+
result = await completePasskeyRegistration({
|
|
14409
|
+
emailSession: hazbase.accessToken,
|
|
14410
|
+
challengeId: cleanText(body?.challengeId || ""),
|
|
14411
|
+
credential: body?.credential,
|
|
14412
|
+
deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
|
|
14413
|
+
});
|
|
14414
|
+
} catch (error) {
|
|
14415
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14416
|
+
throw error;
|
|
14417
|
+
}
|
|
14047
14418
|
state.hazbase = {
|
|
14048
14419
|
...hazbase,
|
|
14049
14420
|
deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
|
|
14050
14421
|
credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
|
|
14422
|
+
sessionInvalid: false,
|
|
14423
|
+
sessionInvalidReason: "",
|
|
14424
|
+
sessionInvalidAt: "",
|
|
14051
14425
|
};
|
|
14052
14426
|
await saveState(config.stateFile, state);
|
|
14053
14427
|
return writeJson(res, 200, result);
|
|
@@ -14068,12 +14442,18 @@ if (url.pathname === "/api/hazbase/passkey/assert/challenge" && req.method === "
|
|
|
14068
14442
|
message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
|
|
14069
14443
|
});
|
|
14070
14444
|
}
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14445
|
+
let result;
|
|
14446
|
+
try {
|
|
14447
|
+
result = await requestPasskeyAssertionChallenge({
|
|
14448
|
+
emailSession: hazbase.accessToken,
|
|
14449
|
+
purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
|
|
14450
|
+
deviceBindingId: hazbase.deviceBindingId || undefined,
|
|
14451
|
+
partnerOrigin,
|
|
14452
|
+
});
|
|
14453
|
+
} catch (error) {
|
|
14454
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14455
|
+
throw error;
|
|
14456
|
+
}
|
|
14077
14457
|
return writeJson(res, 200, result);
|
|
14078
14458
|
}
|
|
14079
14459
|
|
|
@@ -14083,19 +14463,28 @@ if (url.pathname === "/api/hazbase/passkey/assert/complete" && req.method === "P
|
|
|
14083
14463
|
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14084
14464
|
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14085
14465
|
const body = await parseJsonBody(req);
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14466
|
+
let result;
|
|
14467
|
+
try {
|
|
14468
|
+
result = await completePasskeyAssertion({
|
|
14469
|
+
emailSession: hazbase.accessToken,
|
|
14470
|
+
challengeId: cleanText(body?.challengeId || ""),
|
|
14471
|
+
credential: body?.credential,
|
|
14472
|
+
purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
|
|
14473
|
+
deviceBindingId: hazbase.deviceBindingId || undefined,
|
|
14474
|
+
});
|
|
14475
|
+
} catch (error) {
|
|
14476
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14477
|
+
throw error;
|
|
14478
|
+
}
|
|
14093
14479
|
state.hazbase = {
|
|
14094
14480
|
...hazbase,
|
|
14095
14481
|
deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
|
|
14096
14482
|
credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
|
|
14097
14483
|
highTrustToken: cleanText(result.highTrustToken || ""),
|
|
14098
14484
|
highTrustExpiresAt: cleanText(result.highTrustExpiresAt || ""),
|
|
14485
|
+
sessionInvalid: false,
|
|
14486
|
+
sessionInvalidReason: "",
|
|
14487
|
+
sessionInvalidAt: "",
|
|
14099
14488
|
};
|
|
14100
14489
|
await saveState(config.stateFile, state);
|
|
14101
14490
|
return writeJson(res, 200, result);
|
|
@@ -14113,12 +14502,18 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
|
|
|
14113
14502
|
if (chainId !== 8453 && chainId !== 84532) {
|
|
14114
14503
|
return writeJson(res, 400, { error: "unsupported-chain" });
|
|
14115
14504
|
}
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14505
|
+
let result;
|
|
14506
|
+
try {
|
|
14507
|
+
result = await bootstrapPasskeyAccount({
|
|
14508
|
+
emailSession: hazbase.accessToken,
|
|
14509
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
14510
|
+
highTrustToken: hazbase.highTrustToken,
|
|
14511
|
+
chainId,
|
|
14512
|
+
});
|
|
14513
|
+
} catch (error) {
|
|
14514
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14515
|
+
throw error;
|
|
14516
|
+
}
|
|
14122
14517
|
const nextAccounts = mergeHazbaseAccountSummaries(hazbase.accounts, [{
|
|
14123
14518
|
smartAccountAddress: result.smartAccountAddress,
|
|
14124
14519
|
chainId: result.chainId,
|
|
@@ -14129,11 +14524,29 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
|
|
|
14129
14524
|
state.hazbase = {
|
|
14130
14525
|
...hazbase,
|
|
14131
14526
|
accounts: nextAccounts,
|
|
14527
|
+
sessionInvalid: false,
|
|
14528
|
+
sessionInvalidReason: "",
|
|
14529
|
+
sessionInvalidAt: "",
|
|
14132
14530
|
};
|
|
14133
14531
|
await saveState(config.stateFile, state);
|
|
14134
14532
|
return writeJson(res, 200, result);
|
|
14135
14533
|
}
|
|
14136
14534
|
|
|
14535
|
+
if (url.pathname === "/api/hazbase/session/refresh" && req.method === "POST") {
|
|
14536
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
14537
|
+
const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
|
|
14538
|
+
if (!hookAuth) {
|
|
14539
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14540
|
+
if (!session) return;
|
|
14541
|
+
}
|
|
14542
|
+
markHazbaseSessionInvalid(state, "manual_refresh_requested");
|
|
14543
|
+
await saveState(config.stateFile, state);
|
|
14544
|
+
return writeJson(res, 200, {
|
|
14545
|
+
ok: true,
|
|
14546
|
+
status: "session_refresh_required",
|
|
14547
|
+
});
|
|
14548
|
+
}
|
|
14549
|
+
|
|
14137
14550
|
if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
14138
14551
|
const session = requireMutatingApiSession(req, res, config, state);
|
|
14139
14552
|
if (!session) return;
|
|
@@ -14656,6 +15069,168 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
14656
15069
|
});
|
|
14657
15070
|
}
|
|
14658
15071
|
|
|
15072
|
+
|
|
15073
|
+
if (url.pathname === "/api/payments/x402/hazbase-wallet" && req.method === "POST") {
|
|
15074
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
15075
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
15076
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
15077
|
+
}
|
|
15078
|
+
|
|
15079
|
+
let body;
|
|
15080
|
+
try {
|
|
15081
|
+
body = await parseJsonBody(req);
|
|
15082
|
+
} catch {
|
|
15083
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
15084
|
+
}
|
|
15085
|
+
|
|
15086
|
+
const approval = createHazbaseWalletPaymentApproval({ config, body });
|
|
15087
|
+
if (!approval) {
|
|
15088
|
+
return writeJson(res, 400, { error: "invalid-hazbase-wallet-payment-request" });
|
|
15089
|
+
}
|
|
15090
|
+
if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
|
|
15091
|
+
return writeJson(res, 409, { error: "hazbase-wallet-payment-already-pending" });
|
|
15092
|
+
}
|
|
15093
|
+
|
|
15094
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
15095
|
+
runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
|
|
15096
|
+
|
|
15097
|
+
deliverWebPushItem({
|
|
15098
|
+
config,
|
|
15099
|
+
state,
|
|
15100
|
+
kind: "approval",
|
|
15101
|
+
tab: "inbox",
|
|
15102
|
+
subtab: "pending",
|
|
15103
|
+
token: approval.token,
|
|
15104
|
+
stableId: pendingApprovalStableId(approval),
|
|
15105
|
+
title: approval.title,
|
|
15106
|
+
body: approval.messageText,
|
|
15107
|
+
buildLocalizedContent: () => ({ title: approval.title, body: approval.messageText }),
|
|
15108
|
+
}).catch((error) => {
|
|
15109
|
+
console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
|
|
15110
|
+
});
|
|
15111
|
+
|
|
15112
|
+
const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
|
|
15113
|
+
const decisionPromise = new Promise((resolve) => {
|
|
15114
|
+
approval.resolveClaudeWaiter = resolve;
|
|
15115
|
+
});
|
|
15116
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
15117
|
+
setTimeout(() => resolve({ paid: false, decision: "timeout", error: "hazbase-wallet-payment-timeout" }), waitMs);
|
|
15118
|
+
});
|
|
15119
|
+
const decision = await Promise.race([decisionPromise, timeoutPromise]);
|
|
15120
|
+
|
|
15121
|
+
if (!approval.resolved) {
|
|
15122
|
+
approval.resolved = true;
|
|
15123
|
+
approval.resolving = false;
|
|
15124
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
15125
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
15126
|
+
}
|
|
15127
|
+
|
|
15128
|
+
if (decision && typeof decision === "object" && decision.paid === true) {
|
|
15129
|
+
return writeJson(res, 200, decision);
|
|
15130
|
+
}
|
|
15131
|
+
const error = decision?.error || decision?.decision || "hazbase-wallet-payment-declined";
|
|
15132
|
+
const statusCode = error === "hazbase-wallet-payment-timeout" ? 408 : 403;
|
|
15133
|
+
return writeJson(res, statusCode, { paid: false, error, decision: decision?.decision || "decline" });
|
|
15134
|
+
}
|
|
15135
|
+
|
|
15136
|
+
const hazbaseWalletPayMatch = url.pathname.match(/^\/api\/payments\/x402\/hazbase-wallet\/([a-f0-9]+)\/pay$/u);
|
|
15137
|
+
if (hazbaseWalletPayMatch && req.method === "POST") {
|
|
15138
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
15139
|
+
if (!session) return;
|
|
15140
|
+
const token = hazbaseWalletPayMatch[1];
|
|
15141
|
+
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
15142
|
+
if (!approval || approval.kind !== "hazbase_wallet_payment") {
|
|
15143
|
+
return writeJson(res, 404, { error: "approval-not-found" });
|
|
15144
|
+
}
|
|
15145
|
+
if (approval.resolved || approval.resolving) {
|
|
15146
|
+
return writeJson(res, 409, { error: "approval-already-handled" });
|
|
15147
|
+
}
|
|
15148
|
+
approval.resolving = true;
|
|
15149
|
+
try {
|
|
15150
|
+
const result = await completeHazbaseWalletPaymentApproval({ config, runtime, state, approval });
|
|
15151
|
+
return writeJson(res, 200, result);
|
|
15152
|
+
} catch (error) {
|
|
15153
|
+
const failure = await finalizeHazbaseWalletPaymentFailure({ config, runtime, state, approval, error });
|
|
15154
|
+
console.error(`[hazbase-wallet-payment-error] ${approval.requestKey} | ${error?.stack || error?.message || error}`);
|
|
15155
|
+
return writeJson(res, Number(error?.statusCode) || 500, {
|
|
15156
|
+
error: failure.error,
|
|
15157
|
+
approvalFinalized: true,
|
|
15158
|
+
retryable: true,
|
|
15159
|
+
});
|
|
15160
|
+
}
|
|
15161
|
+
}
|
|
15162
|
+
|
|
15163
|
+
if (url.pathname === "/api/payments/x402/approval" && req.method === "POST") {
|
|
15164
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
15165
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
15166
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
15167
|
+
}
|
|
15168
|
+
|
|
15169
|
+
let body;
|
|
15170
|
+
try {
|
|
15171
|
+
body = await parseJsonBody(req);
|
|
15172
|
+
} catch {
|
|
15173
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
15174
|
+
}
|
|
15175
|
+
|
|
15176
|
+
const approval = createX402PaymentApproval({ config, body });
|
|
15177
|
+
if (!approval) {
|
|
15178
|
+
return writeJson(res, 400, { error: "invalid-payment-approval-request" });
|
|
15179
|
+
}
|
|
15180
|
+
if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
|
|
15181
|
+
return writeJson(res, 409, { error: "payment-approval-already-pending" });
|
|
15182
|
+
}
|
|
15183
|
+
|
|
15184
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
15185
|
+
runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
|
|
15186
|
+
|
|
15187
|
+
deliverWebPushItem({
|
|
15188
|
+
config,
|
|
15189
|
+
state,
|
|
15190
|
+
kind: "approval",
|
|
15191
|
+
tab: "inbox",
|
|
15192
|
+
subtab: "pending",
|
|
15193
|
+
token: approval.token,
|
|
15194
|
+
stableId: pendingApprovalStableId(approval),
|
|
15195
|
+
title: approval.title,
|
|
15196
|
+
body: approval.messageText,
|
|
15197
|
+
buildLocalizedContent: () => ({
|
|
15198
|
+
title: approval.title,
|
|
15199
|
+
body: approval.messageText,
|
|
15200
|
+
}),
|
|
15201
|
+
}).catch((error) => {
|
|
15202
|
+
console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
15203
|
+
});
|
|
15204
|
+
|
|
15205
|
+
const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
|
|
15206
|
+
const decisionPromise = new Promise((resolve) => {
|
|
15207
|
+
approval.resolveClaudeWaiter = resolve;
|
|
15208
|
+
});
|
|
15209
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
15210
|
+
setTimeout(() => resolve("timeout"), waitMs);
|
|
15211
|
+
});
|
|
15212
|
+
const decision = await Promise.race([decisionPromise, timeoutPromise]);
|
|
15213
|
+
|
|
15214
|
+
if (!approval.resolved) {
|
|
15215
|
+
approval.resolved = true;
|
|
15216
|
+
approval.resolving = false;
|
|
15217
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
15218
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
15219
|
+
}
|
|
15220
|
+
|
|
15221
|
+
if (decision === "accept") {
|
|
15222
|
+
return writeJson(res, 200, {
|
|
15223
|
+
approved: true,
|
|
15224
|
+
decision,
|
|
15225
|
+
approvedPayment: approval.rawParams,
|
|
15226
|
+
});
|
|
15227
|
+
}
|
|
15228
|
+
if (decision === "timeout") {
|
|
15229
|
+
return writeJson(res, 408, { approved: false, decision, error: "payment-approval-timeout" });
|
|
15230
|
+
}
|
|
15231
|
+
return writeJson(res, 403, { approved: false, decision: "decline", error: "payment-approval-declined" });
|
|
15232
|
+
}
|
|
15233
|
+
|
|
14659
15234
|
if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
|
|
14660
15235
|
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
14661
15236
|
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
@@ -15473,6 +16048,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15473
16048
|
if (approval.resolved || approval.resolving) {
|
|
15474
16049
|
return writeJson(res, 409, { error: "approval-already-handled" });
|
|
15475
16050
|
}
|
|
16051
|
+
if (approval.kind === "hazbase_wallet_payment" && decision !== "decline") {
|
|
16052
|
+
console.warn(`[hazbase-wallet-payment-generic-decision-blocked] ${approval.requestKey} | ${decision}`);
|
|
16053
|
+
return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
|
|
16054
|
+
}
|
|
15476
16055
|
|
|
15477
16056
|
approval.resolving = true;
|
|
15478
16057
|
try {
|
|
@@ -15483,6 +16062,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15483
16062
|
return writeJson(res, 200, { ok: true, decision });
|
|
15484
16063
|
} catch (error) {
|
|
15485
16064
|
approval.resolving = false;
|
|
16065
|
+
if (res.headersSent || res.writableEnded) {
|
|
16066
|
+
console.error(`[approval-decision-post-write-error] ${approval.requestKey} | ${error.message}`);
|
|
16067
|
+
return;
|
|
16068
|
+
}
|
|
15486
16069
|
return writeJson(res, 500, { error: error.message });
|
|
15487
16070
|
}
|
|
15488
16071
|
}
|
|
@@ -16067,6 +16650,21 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16067
16650
|
if (approval.resolved || approval.resolving) {
|
|
16068
16651
|
return writeApprovalHandled(res, req, approval.title, 409, config.defaultLocale);
|
|
16069
16652
|
}
|
|
16653
|
+
if (approval.kind === "hazbase_wallet_payment" && decision !== "decline") {
|
|
16654
|
+
console.warn(`[hazbase-wallet-payment-native-decision-blocked] ${approval.requestKey} | ${decision}`);
|
|
16655
|
+
if (requestWantsHtml(req)) {
|
|
16656
|
+
return writeHtml(
|
|
16657
|
+
res,
|
|
16658
|
+
409,
|
|
16659
|
+
renderStatusPage({
|
|
16660
|
+
title: approval.title,
|
|
16661
|
+
body: "This approval must be completed from the viveworker app with the wallet payment button.",
|
|
16662
|
+
tone: "warn",
|
|
16663
|
+
})
|
|
16664
|
+
);
|
|
16665
|
+
}
|
|
16666
|
+
return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
|
|
16667
|
+
}
|
|
16070
16668
|
|
|
16071
16669
|
approval.resolving = true;
|
|
16072
16670
|
try {
|
|
@@ -16091,6 +16689,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16091
16689
|
});
|
|
16092
16690
|
} catch (error) {
|
|
16093
16691
|
approval.resolving = false;
|
|
16692
|
+
if (res.headersSent || res.writableEnded) {
|
|
16693
|
+
console.error(`[native-approval-decision-post-write-error] ${approval.requestKey} | ${error.message}`);
|
|
16694
|
+
return;
|
|
16695
|
+
}
|
|
16094
16696
|
if (requestWantsHtml(req)) {
|
|
16095
16697
|
return writeHtml(
|
|
16096
16698
|
res,
|
|
@@ -16105,6 +16707,9 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16105
16707
|
return writeJson(res, 500, { error: error.message });
|
|
16106
16708
|
}
|
|
16107
16709
|
} catch (error) {
|
|
16710
|
+
if (res.headersSent || res.writableEnded) {
|
|
16711
|
+
return;
|
|
16712
|
+
}
|
|
16108
16713
|
if (requestWantsHtml(req)) {
|
|
16109
16714
|
return writeHtml(
|
|
16110
16715
|
res,
|
|
@@ -17453,6 +18058,9 @@ function buildConfig(cli) {
|
|
|
17453
18058
|
hazbaseApiUrl: stripTrailingSlash(process.env.HAZBASE_API_URL || "https://api.hazbase.com"),
|
|
17454
18059
|
hazbaseClientKey: cleanText(process.env.HAZBASE_CLIENT_KEY || "viveworker-internal"),
|
|
17455
18060
|
hazbaseDeviceLabel: cleanText(process.env.HAZBASE_DEVICE_LABEL || "viveworker"),
|
|
18061
|
+
npmPackageName: cleanText(process.env.VIVEWORKER_NPM_PACKAGE || "viveworker"),
|
|
18062
|
+
npmDistTag: cleanText(process.env.VIVEWORKER_NPM_DIST_TAG || "latest"),
|
|
18063
|
+
npmRegistryUrl: stripTrailingSlash(process.env.NPM_REGISTRY_URL || "https://registry.npmjs.org"),
|
|
17456
18064
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
17457
18065
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
17458
18066
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -17498,6 +18106,12 @@ function buildConfig(cli) {
|
|
|
17498
18106
|
maxCodeEvents: numberEnv("MAX_CODE_EVENTS", 1000),
|
|
17499
18107
|
maxTimelineThreads: numberEnv("MAX_TIMELINE_THREADS", 20),
|
|
17500
18108
|
maxReadBytes: numberEnv("MAX_READ_BYTES", 2 * 1024 * 1024),
|
|
18109
|
+
// Cap how far back the bridge looks for Claude transcripts. Default 7 days
|
|
18110
|
+
// covers a long pause without dragging the long tail of dead sessions
|
|
18111
|
+
// (which can easily reach thousands of files) into the per-iteration
|
|
18112
|
+
// stat loop. Set to 0 to disable filtering and scan everything (the
|
|
18113
|
+
// legacy behavior).
|
|
18114
|
+
claudeTranscriptMaxAgeMs: numberEnv("CLAUDE_TRANSCRIPT_MAX_AGE_DAYS", 7) * 24 * 60 * 60 * 1000,
|
|
17501
18115
|
maxMessageChars: numberEnv("MAX_MESSAGE_CHARS", 320),
|
|
17502
18116
|
maxCommandChars: numberEnv("MAX_COMMAND_CHARS", 220),
|
|
17503
18117
|
maxJustificationChars: numberEnv("MAX_JUSTIFICATION_CHARS", 220),
|
|
@@ -17541,21 +18155,92 @@ function buildConfig(cli) {
|
|
|
17541
18155
|
}
|
|
17542
18156
|
|
|
17543
18157
|
|
|
18158
|
+
function inferHazbaseDeviceBindingIdFromAccounts(accounts) {
|
|
18159
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
18160
|
+
for (const entry of list) {
|
|
18161
|
+
const deviceBindingId = cleanText(
|
|
18162
|
+
entry?.primaryDeviceBindingId || entry?.deviceBindingId || entry?.ownerDeviceBindingId || ""
|
|
18163
|
+
);
|
|
18164
|
+
if (deviceBindingId) return deviceBindingId;
|
|
18165
|
+
}
|
|
18166
|
+
return "";
|
|
18167
|
+
}
|
|
18168
|
+
|
|
17544
18169
|
function normalizeHazbaseState(raw) {
|
|
17545
18170
|
const value = raw && typeof raw === "object" ? raw : {};
|
|
18171
|
+
const accounts = Array.isArray(value.accounts) ? value.accounts : [];
|
|
18172
|
+
const deviceBindingId = cleanText(value.deviceBindingId ?? "") || inferHazbaseDeviceBindingIdFromAccounts(accounts);
|
|
17546
18173
|
return {
|
|
17547
18174
|
email: cleanText(value.email ?? ""),
|
|
17548
18175
|
accessToken: cleanText(value.accessToken ?? ""),
|
|
17549
18176
|
sessionId: cleanText(value.sessionId ?? ""),
|
|
17550
18177
|
userId: cleanText(value.userId ?? ""),
|
|
17551
|
-
deviceBindingId
|
|
18178
|
+
deviceBindingId,
|
|
17552
18179
|
credentialId: cleanText(value.credentialId ?? ""),
|
|
17553
18180
|
highTrustToken: cleanText(value.highTrustToken ?? ""),
|
|
17554
18181
|
highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
|
|
17555
|
-
accounts
|
|
18182
|
+
accounts,
|
|
18183
|
+
sessionInvalid: value.sessionInvalid === true,
|
|
18184
|
+
sessionInvalidReason: cleanText(value.sessionInvalidReason ?? ""),
|
|
18185
|
+
sessionInvalidAt: cleanText(value.sessionInvalidAt ?? ""),
|
|
17556
18186
|
};
|
|
17557
18187
|
}
|
|
17558
18188
|
|
|
18189
|
+
function isHazbaseInvalidAppSessionError(error) {
|
|
18190
|
+
const details = error?.details && typeof error.details === "object" ? error.details : {};
|
|
18191
|
+
const rawText = [
|
|
18192
|
+
error?.code,
|
|
18193
|
+
error?.message,
|
|
18194
|
+
details.errorCode,
|
|
18195
|
+
details.code,
|
|
18196
|
+
details.error,
|
|
18197
|
+
details.message,
|
|
18198
|
+
typeof error?.details === "string" ? error.details : "",
|
|
18199
|
+
]
|
|
18200
|
+
.map((entry) => cleanText(entry || ""))
|
|
18201
|
+
.filter(Boolean)
|
|
18202
|
+
.join(" ");
|
|
18203
|
+
const statusCode = Number(error?.statusCode || error?.status || details.statusCode || 0) ||
|
|
18204
|
+
(/statusCode["']?\s*[:=]\s*401/u.test(rawText) || /\b401\b/u.test(rawText) ? 401 : 0);
|
|
18205
|
+
const text = [
|
|
18206
|
+
rawText,
|
|
18207
|
+
]
|
|
18208
|
+
.map((entry) => cleanText(entry || "").toLowerCase())
|
|
18209
|
+
.filter(Boolean)
|
|
18210
|
+
.join(" ");
|
|
18211
|
+
return statusCode === 401 && (
|
|
18212
|
+
text.includes("invalid app session") ||
|
|
18213
|
+
text.includes("invalid_app_session")
|
|
18214
|
+
);
|
|
18215
|
+
}
|
|
18216
|
+
|
|
18217
|
+
function markHazbaseSessionInvalid(state, reason = "invalid_app_session") {
|
|
18218
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
18219
|
+
state.hazbase = {
|
|
18220
|
+
...hazbase,
|
|
18221
|
+
accessToken: "",
|
|
18222
|
+
sessionId: "",
|
|
18223
|
+
highTrustToken: "",
|
|
18224
|
+
highTrustExpiresAt: "",
|
|
18225
|
+
sessionInvalid: true,
|
|
18226
|
+
sessionInvalidReason: cleanText(reason || "invalid_app_session"),
|
|
18227
|
+
sessionInvalidAt: new Date().toISOString(),
|
|
18228
|
+
};
|
|
18229
|
+
return state.hazbase;
|
|
18230
|
+
}
|
|
18231
|
+
|
|
18232
|
+
async function maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res }) {
|
|
18233
|
+
if (!isHazbaseInvalidAppSessionError(error)) {
|
|
18234
|
+
return false;
|
|
18235
|
+
}
|
|
18236
|
+
markHazbaseSessionInvalid(state);
|
|
18237
|
+
await saveState(config.stateFile, state);
|
|
18238
|
+
return writeJson(res, 401, {
|
|
18239
|
+
error: "hazbase-session-expired",
|
|
18240
|
+
message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
|
|
18241
|
+
});
|
|
18242
|
+
}
|
|
18243
|
+
|
|
17559
18244
|
function hazbasePasskeyLocalHostError() {
|
|
17560
18245
|
const error = new Error("Open viveworker on its .local hostname to use hazBase passkeys.");
|
|
17561
18246
|
error.code = "hazbase-passkey-local-host-required";
|
|
@@ -17627,6 +18312,117 @@ async function fetchHazbaseMetadata(config, endpoint, timeoutMs = HAZBASE_METADA
|
|
|
17627
18312
|
}
|
|
17628
18313
|
}
|
|
17629
18314
|
|
|
18315
|
+
async function getNpmVersionStatus(runtime, config) {
|
|
18316
|
+
const now = Date.now();
|
|
18317
|
+
const cached = runtime.npmVersionStatusCache;
|
|
18318
|
+
if (cached && now - Number(cached.checkedAtMs || 0) < NPM_VERSION_CHECK_CACHE_TTL_MS) {
|
|
18319
|
+
return cached;
|
|
18320
|
+
}
|
|
18321
|
+
if (runtime.npmVersionStatusPending) {
|
|
18322
|
+
return runtime.npmVersionStatusPending;
|
|
18323
|
+
}
|
|
18324
|
+
runtime.npmVersionStatusPending = fetchNpmVersionStatus(config)
|
|
18325
|
+
.then((status) => {
|
|
18326
|
+
runtime.npmVersionStatusCache = status;
|
|
18327
|
+
return status;
|
|
18328
|
+
})
|
|
18329
|
+
.catch((error) => {
|
|
18330
|
+
const fallback = {
|
|
18331
|
+
packageName: config.npmPackageName || "viveworker",
|
|
18332
|
+
distTag: config.npmDistTag || "latest",
|
|
18333
|
+
currentVersion: appPackageVersion,
|
|
18334
|
+
latestVersion: "",
|
|
18335
|
+
updateAvailable: false,
|
|
18336
|
+
checkedAtMs: Date.now(),
|
|
18337
|
+
error: cleanText(error?.message || "npm version check failed"),
|
|
18338
|
+
};
|
|
18339
|
+
runtime.npmVersionStatusCache = fallback;
|
|
18340
|
+
return fallback;
|
|
18341
|
+
})
|
|
18342
|
+
.finally(() => {
|
|
18343
|
+
runtime.npmVersionStatusPending = null;
|
|
18344
|
+
});
|
|
18345
|
+
return runtime.npmVersionStatusPending;
|
|
18346
|
+
}
|
|
18347
|
+
|
|
18348
|
+
async function fetchNpmVersionStatus(config) {
|
|
18349
|
+
const packageName = cleanText(config.npmPackageName || "viveworker") || "viveworker";
|
|
18350
|
+
const distTag = cleanText(config.npmDistTag || "latest") || "latest";
|
|
18351
|
+
const registryBaseUrl = stripTrailingSlash(config.npmRegistryUrl || "https://registry.npmjs.org");
|
|
18352
|
+
const endpoint = `${registryBaseUrl}/${encodeURIComponent(packageName).replace(/^%40/u, "@")}/${encodeURIComponent(distTag)}`;
|
|
18353
|
+
const controller = new AbortController();
|
|
18354
|
+
const timer = setTimeout(() => controller.abort(), NPM_VERSION_CHECK_TIMEOUT_MS);
|
|
18355
|
+
timer.unref?.();
|
|
18356
|
+
try {
|
|
18357
|
+
const response = await fetch(endpoint, {
|
|
18358
|
+
method: "GET",
|
|
18359
|
+
headers: { Accept: "application/json" },
|
|
18360
|
+
signal: controller.signal,
|
|
18361
|
+
});
|
|
18362
|
+
if (!response.ok) {
|
|
18363
|
+
throw new Error(`npm registry returned ${response.status}`);
|
|
18364
|
+
}
|
|
18365
|
+
const payload = await response.json().catch(() => null);
|
|
18366
|
+
const latestVersion = cleanText(payload?.version || "");
|
|
18367
|
+
if (!latestVersion) {
|
|
18368
|
+
throw new Error("npm registry response did not include a version");
|
|
18369
|
+
}
|
|
18370
|
+
return {
|
|
18371
|
+
packageName,
|
|
18372
|
+
distTag,
|
|
18373
|
+
currentVersion: appPackageVersion,
|
|
18374
|
+
latestVersion,
|
|
18375
|
+
updateAvailable: comparePackageVersions(latestVersion, appPackageVersion) > 0,
|
|
18376
|
+
checkedAtMs: Date.now(),
|
|
18377
|
+
error: "",
|
|
18378
|
+
};
|
|
18379
|
+
} catch (error) {
|
|
18380
|
+
if (error?.name === "AbortError") {
|
|
18381
|
+
throw new Error("npm version check timed out");
|
|
18382
|
+
}
|
|
18383
|
+
throw error;
|
|
18384
|
+
} finally {
|
|
18385
|
+
clearTimeout(timer);
|
|
18386
|
+
}
|
|
18387
|
+
}
|
|
18388
|
+
|
|
18389
|
+
function comparePackageVersions(left, right) {
|
|
18390
|
+
const a = parsePackageVersion(left);
|
|
18391
|
+
const b = parsePackageVersion(right);
|
|
18392
|
+
for (let index = 0; index < 3; index += 1) {
|
|
18393
|
+
if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1;
|
|
18394
|
+
}
|
|
18395
|
+
if (!a.prerelease.length && b.prerelease.length) return 1;
|
|
18396
|
+
if (a.prerelease.length && !b.prerelease.length) return -1;
|
|
18397
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
18398
|
+
for (let index = 0; index < length; index += 1) {
|
|
18399
|
+
const av = a.prerelease[index];
|
|
18400
|
+
const bv = b.prerelease[index];
|
|
18401
|
+
if (av == null && bv == null) return 0;
|
|
18402
|
+
if (av == null) return -1;
|
|
18403
|
+
if (bv == null) return 1;
|
|
18404
|
+
const an = /^\d+$/u.test(av) ? Number(av) : null;
|
|
18405
|
+
const bn = /^\d+$/u.test(bv) ? Number(bv) : null;
|
|
18406
|
+
if (an != null && bn != null && an !== bn) return an > bn ? 1 : -1;
|
|
18407
|
+
if (an != null && bn == null) return -1;
|
|
18408
|
+
if (an == null && bn != null) return 1;
|
|
18409
|
+
const cmp = av.localeCompare(bv);
|
|
18410
|
+
if (cmp !== 0) return cmp > 0 ? 1 : -1;
|
|
18411
|
+
}
|
|
18412
|
+
return 0;
|
|
18413
|
+
}
|
|
18414
|
+
|
|
18415
|
+
function parsePackageVersion(value) {
|
|
18416
|
+
const normalized = cleanText(value || "").replace(/^v/iu, "").split("+")[0];
|
|
18417
|
+
const [core, prerelease = ""] = normalized.split("-", 2);
|
|
18418
|
+
const parts = core.split(".").map((part) => Number(part)).map((part) => (Number.isFinite(part) && part >= 0 ? part : 0));
|
|
18419
|
+
while (parts.length < 3) parts.push(0);
|
|
18420
|
+
return {
|
|
18421
|
+
parts: parts.slice(0, 3),
|
|
18422
|
+
prerelease: prerelease ? prerelease.split(".").map((part) => cleanText(part)).filter(Boolean) : [],
|
|
18423
|
+
};
|
|
18424
|
+
}
|
|
18425
|
+
|
|
17630
18426
|
function mergeHazbaseAccountSummaries(existing, incoming) {
|
|
17631
18427
|
const merged = new Map();
|
|
17632
18428
|
for (const entry of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(incoming) ? incoming : [])]) {
|
|
@@ -17644,6 +18440,169 @@ function resolveHazbaseAccountForChain(accounts, chainId) {
|
|
|
17644
18440
|
return list.find((entry) => Number(entry?.chainId) === normalizedChainId && cleanText(entry?.smartAccountAddress || "")) || null;
|
|
17645
18441
|
}
|
|
17646
18442
|
|
|
18443
|
+
|
|
18444
|
+
async function finalizeHazbaseWalletPaymentFailure({ config, runtime, state, approval, error }) {
|
|
18445
|
+
const errorCode = cleanText(error?.code || error?.message || "hazbase-wallet-payment-failed") || "hazbase-wallet-payment-failed";
|
|
18446
|
+
if (approval.resolved) {
|
|
18447
|
+
return { paid: false, decision: "failed", error: errorCode };
|
|
18448
|
+
}
|
|
18449
|
+
|
|
18450
|
+
const payload = {
|
|
18451
|
+
paid: false,
|
|
18452
|
+
decision: "failed",
|
|
18453
|
+
error: errorCode,
|
|
18454
|
+
paymentRequestId: approval.paymentRequestId || approval.requestId || "",
|
|
18455
|
+
};
|
|
18456
|
+
|
|
18457
|
+
approval.resolved = true;
|
|
18458
|
+
approval.resolving = false;
|
|
18459
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
18460
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
18461
|
+
approval.resolveClaudeWaiter?.(payload);
|
|
18462
|
+
|
|
18463
|
+
const statusMessage = t(config.defaultLocale, "server.message.paymentFailed", { reason: errorCode });
|
|
18464
|
+
const stateChanged = recordActionHistoryItem({
|
|
18465
|
+
config,
|
|
18466
|
+
runtime,
|
|
18467
|
+
state,
|
|
18468
|
+
kind: "approval",
|
|
18469
|
+
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
18470
|
+
token: approval.token,
|
|
18471
|
+
title: approval.title,
|
|
18472
|
+
threadLabel: approval.threadLabel || "",
|
|
18473
|
+
messageText: `${statusMessage}\n\n${approval.messageText}`,
|
|
18474
|
+
summary: statusMessage,
|
|
18475
|
+
fileRefs: [],
|
|
18476
|
+
diffText: "",
|
|
18477
|
+
diffSource: "",
|
|
18478
|
+
diffAvailable: false,
|
|
18479
|
+
diffAddedLines: 0,
|
|
18480
|
+
diffRemovedLines: 0,
|
|
18481
|
+
outcome: "failed",
|
|
18482
|
+
provider: approval.provider || "viveworker",
|
|
18483
|
+
});
|
|
18484
|
+
if (stateChanged) await saveState(config.stateFile, state);
|
|
18485
|
+
console.log(`[hazbase-wallet-payment-failed] ${approval.requestKey} | ${errorCode}`);
|
|
18486
|
+
return payload;
|
|
18487
|
+
}
|
|
18488
|
+
|
|
18489
|
+
async function completeHazbaseWalletPaymentApproval({ config, runtime, state, approval }) {
|
|
18490
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
18491
|
+
if (!hazbase.accessToken) throw codedError("hazbase-auth-required", 401);
|
|
18492
|
+
if (!hazbase.deviceBindingId) throw codedError("hazbase-passkey-required", 409);
|
|
18493
|
+
if (!hazbase.highTrustToken) throw codedError("hazbase-reauth-required", 409);
|
|
18494
|
+
const chainId = Number(approval.rawParams?.chainId || 0);
|
|
18495
|
+
const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
|
|
18496
|
+
if (!account?.smartAccountAddress) throw codedError("hazbase-wallet-account-missing", 409);
|
|
18497
|
+
|
|
18498
|
+
let result;
|
|
18499
|
+
try {
|
|
18500
|
+
result = await postHazbaseJson(config, "/api/payments/x402/hazbase-wallet/pay", {
|
|
18501
|
+
paymentRequestId: approval.paymentRequestId,
|
|
18502
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
18503
|
+
highTrustToken: hazbase.highTrustToken,
|
|
18504
|
+
smartAccountAddress: account.smartAccountAddress,
|
|
18505
|
+
waitForReceipt: true,
|
|
18506
|
+
}, hazbase.accessToken, 130_000);
|
|
18507
|
+
} catch (error) {
|
|
18508
|
+
if (isHazbaseInvalidAppSessionError(error)) {
|
|
18509
|
+
markHazbaseSessionInvalid(state);
|
|
18510
|
+
await saveState(config.stateFile, state);
|
|
18511
|
+
throw codedError("hazbase-session-expired", 401, {
|
|
18512
|
+
message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
|
|
18513
|
+
});
|
|
18514
|
+
}
|
|
18515
|
+
throw error;
|
|
18516
|
+
}
|
|
18517
|
+
|
|
18518
|
+
if (!result?.xPayment) {
|
|
18519
|
+
throw codedError(result?.errorCode || result?.error || "hazbase-wallet-payment-failed", 502);
|
|
18520
|
+
}
|
|
18521
|
+
|
|
18522
|
+
const payload = {
|
|
18523
|
+
paid: true,
|
|
18524
|
+
decision: "accept",
|
|
18525
|
+
paymentRequestId: approval.paymentRequestId,
|
|
18526
|
+
xPayment: cleanText(result.xPayment || ""),
|
|
18527
|
+
payer: cleanText(result.payer || account.smartAccountAddress || ""),
|
|
18528
|
+
chainId: Number(result.chainId || chainId),
|
|
18529
|
+
network: cleanText(result.network || approval.rawParams?.network || ""),
|
|
18530
|
+
relayMode: cleanText(result.relayMode || ""),
|
|
18531
|
+
submittedUserOpHash: cleanText(result.submittedUserOpHash || ""),
|
|
18532
|
+
transactionHash: cleanText(result.transactionHash || ""),
|
|
18533
|
+
payment: result,
|
|
18534
|
+
};
|
|
18535
|
+
|
|
18536
|
+
approval.resolved = true;
|
|
18537
|
+
approval.resolving = false;
|
|
18538
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
18539
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
18540
|
+
approval.resolveClaudeWaiter?.(payload);
|
|
18541
|
+
const stateChanged = recordActionHistoryItem({
|
|
18542
|
+
config,
|
|
18543
|
+
runtime,
|
|
18544
|
+
state,
|
|
18545
|
+
kind: "approval",
|
|
18546
|
+
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
18547
|
+
token: approval.token,
|
|
18548
|
+
title: approval.title,
|
|
18549
|
+
threadLabel: approval.threadLabel || "",
|
|
18550
|
+
messageText: `${approvalDecisionMessage("accept", config.defaultLocale, approval.provider)}\n\n${approval.messageText}\n\nTransaction: ${payload.transactionHash || payload.submittedUserOpHash}`,
|
|
18551
|
+
summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage("accept", config.defaultLocale, approval.provider),
|
|
18552
|
+
fileRefs: [],
|
|
18553
|
+
diffText: "",
|
|
18554
|
+
diffSource: "",
|
|
18555
|
+
diffAvailable: false,
|
|
18556
|
+
diffAddedLines: 0,
|
|
18557
|
+
diffRemovedLines: 0,
|
|
18558
|
+
outcome: "approved",
|
|
18559
|
+
provider: approval.provider || "viveworker",
|
|
18560
|
+
});
|
|
18561
|
+
if (stateChanged) await saveState(config.stateFile, state);
|
|
18562
|
+
console.log(`[hazbase-wallet-payment] ${approval.requestKey} | ${payload.transactionHash || payload.submittedUserOpHash}`);
|
|
18563
|
+
return { ok: true, ...payload };
|
|
18564
|
+
}
|
|
18565
|
+
|
|
18566
|
+
async function postHazbaseJson(config, endpoint, body, bearerToken, timeoutMs = 30_000) {
|
|
18567
|
+
const baseUrl = stripTrailingSlash(config?.hazbaseApiUrl || "");
|
|
18568
|
+
if (!baseUrl) throw codedError("hazbase-api-not-configured", 503);
|
|
18569
|
+
const controller = new AbortController();
|
|
18570
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18571
|
+
timer.unref?.();
|
|
18572
|
+
try {
|
|
18573
|
+
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
18574
|
+
method: "POST",
|
|
18575
|
+
headers: {
|
|
18576
|
+
"content-type": "application/json",
|
|
18577
|
+
accept: "application/json",
|
|
18578
|
+
authorization: `Bearer ${bearerToken}`,
|
|
18579
|
+
"x-request-id": `viveworker_${crypto.randomUUID()}`,
|
|
18580
|
+
},
|
|
18581
|
+
body: JSON.stringify(body),
|
|
18582
|
+
signal: controller.signal,
|
|
18583
|
+
});
|
|
18584
|
+
const payload = await response.json().catch(() => null);
|
|
18585
|
+
const data = payload?.data || payload || {};
|
|
18586
|
+
if (!response.ok) {
|
|
18587
|
+
throw codedError(data?.errorCode || data?.code || data?.error || `hazbase-api-${response.status}`, response.status, data);
|
|
18588
|
+
}
|
|
18589
|
+
return data;
|
|
18590
|
+
} catch (error) {
|
|
18591
|
+
if (error?.name === "AbortError") throw codedError("hazbase-api-timeout", 504);
|
|
18592
|
+
throw error;
|
|
18593
|
+
} finally {
|
|
18594
|
+
clearTimeout(timer);
|
|
18595
|
+
}
|
|
18596
|
+
}
|
|
18597
|
+
|
|
18598
|
+
function codedError(code, statusCode = 500, details = null) {
|
|
18599
|
+
const error = new Error(code);
|
|
18600
|
+
error.code = code;
|
|
18601
|
+
error.statusCode = statusCode;
|
|
18602
|
+
error.details = details;
|
|
18603
|
+
return error;
|
|
18604
|
+
}
|
|
18605
|
+
|
|
17647
18606
|
function validateConfig(config) {
|
|
17648
18607
|
let publicBaseUrl = null;
|
|
17649
18608
|
try {
|
|
@@ -18436,8 +19395,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
18436
19395
|
);
|
|
18437
19396
|
|
|
18438
19397
|
if (
|
|
18439
|
-
|
|
18440
|
-
|
|
19398
|
+
timelineProjectionChanged(nextHistoryItems, runtime.recentHistoryItems, [
|
|
19399
|
+
"stableId",
|
|
19400
|
+
"title",
|
|
19401
|
+
"threadLabel",
|
|
19402
|
+
])
|
|
18441
19403
|
) {
|
|
18442
19404
|
runtime.recentHistoryItems = nextHistoryItems;
|
|
18443
19405
|
state.recentHistoryItems = nextHistoryItems;
|
|
@@ -18478,8 +19440,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
18478
19440
|
);
|
|
18479
19441
|
|
|
18480
19442
|
if (
|
|
18481
|
-
|
|
18482
|
-
|
|
19443
|
+
timelineProjectionChanged(nextTimelineEntries, runtime.recentTimelineEntries, [
|
|
19444
|
+
"stableId",
|
|
19445
|
+
"title",
|
|
19446
|
+
"threadLabel",
|
|
19447
|
+
])
|
|
18483
19448
|
) {
|
|
18484
19449
|
runtime.recentTimelineEntries = nextTimelineEntries;
|
|
18485
19450
|
state.recentTimelineEntries = nextTimelineEntries;
|
|
@@ -18514,8 +19479,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
18514
19479
|
);
|
|
18515
19480
|
|
|
18516
19481
|
if (
|
|
18517
|
-
|
|
18518
|
-
|
|
19482
|
+
timelineProjectionChanged(nextCodeEvents, runtime.recentCodeEvents, [
|
|
19483
|
+
"stableId",
|
|
19484
|
+
"title",
|
|
19485
|
+
"threadLabel",
|
|
19486
|
+
])
|
|
18519
19487
|
) {
|
|
18520
19488
|
runtime.recentCodeEvents = nextCodeEvents;
|
|
18521
19489
|
state.recentCodeEvents = nextCodeEvents;
|
|
@@ -18950,6 +19918,37 @@ async function main() {
|
|
|
18950
19918
|
process.on("SIGINT", handleSignal);
|
|
18951
19919
|
process.on("SIGTERM", handleSignal);
|
|
18952
19920
|
|
|
19921
|
+
// Belt-and-suspenders: a stray ERR_HTTP_HEADERS_SENT inside an async
|
|
19922
|
+
// request handler used to escape as an UnhandledPromiseRejection and crash
|
|
19923
|
+
// the process under launchd, triggering a restart loop that then re-ran all
|
|
19924
|
+
// the startup backfills (state.json parse, rollout/transcript replay) and
|
|
19925
|
+
// burned CPU + memory for ~30s per cycle. The targeted catch-handler guards
|
|
19926
|
+
// around handleNativeApprovalDecision cover the known sites; this top-level
|
|
19927
|
+
// filter swallows the same class of error from any new site we haven't
|
|
19928
|
+
// audited yet so the process keeps serving instead of dying.
|
|
19929
|
+
const isBenignHttpError = (err) =>
|
|
19930
|
+
err && typeof err === "object" && (
|
|
19931
|
+
err.code === "ERR_HTTP_HEADERS_SENT" ||
|
|
19932
|
+
err.code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
19933
|
+
err.code === "ERR_STREAM_DESTROYED" ||
|
|
19934
|
+
err.code === "ECONNRESET" ||
|
|
19935
|
+
err.code === "EPIPE"
|
|
19936
|
+
);
|
|
19937
|
+
process.on("uncaughtException", (err) => {
|
|
19938
|
+
if (isBenignHttpError(err)) {
|
|
19939
|
+
console.warn(`[uncaught-http] ${err.code} ${err.message}`);
|
|
19940
|
+
return;
|
|
19941
|
+
}
|
|
19942
|
+
console.error(`[uncaught] ${err?.stack || err}`);
|
|
19943
|
+
});
|
|
19944
|
+
process.on("unhandledRejection", (reason) => {
|
|
19945
|
+
if (isBenignHttpError(reason)) {
|
|
19946
|
+
console.warn(`[unhandled-http] ${reason.code} ${reason.message}`);
|
|
19947
|
+
return;
|
|
19948
|
+
}
|
|
19949
|
+
console.error(`[unhandled] ${reason?.stack || reason}`);
|
|
19950
|
+
});
|
|
19951
|
+
|
|
18953
19952
|
try {
|
|
18954
19953
|
if (config.webPushEnabled) {
|
|
18955
19954
|
webPush.setVapidDetails(
|