@evomap/evolver-proxy 2.0.0-beta.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 +49 -0
- package/dist/bin/envFile.d.ts +10 -0
- package/dist/bin/envFile.js +68 -0
- package/dist/bin/evolver-llm-proxy.d.ts +2 -0
- package/dist/bin/evolver-llm-proxy.js +111 -0
- package/dist/bin/evolver-proxy.d.ts +83 -0
- package/dist/bin/evolver-proxy.js +511 -0
- package/dist/bin/proxySettings.d.ts +15 -0
- package/dist/bin/proxySettings.js +84 -0
- package/dist/bin/proxyStorePath.d.ts +1 -0
- package/dist/bin/proxyStorePath.js +16 -0
- package/dist/daemon/atpConsent.d.ts +13 -0
- package/dist/daemon/atpConsent.js +60 -0
- package/dist/daemon/ipcConfig.d.ts +1 -0
- package/dist/daemon/ipcConfig.js +13 -0
- package/dist/daemon/proxyDaemon.d.ts +191 -0
- package/dist/daemon/proxyDaemon.js +1015 -0
- package/dist/daemon/selectHub.d.ts +16 -0
- package/dist/daemon/selectHub.js +30 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/lifecycle/deployGuard.d.ts +46 -0
- package/dist/lifecycle/deployGuard.js +53 -0
- package/dist/lifecycle/legacyNodeId.d.ts +96 -0
- package/dist/lifecycle/legacyNodeId.js +163 -0
- package/dist/lifecycle/manager.d.ts +106 -0
- package/dist/lifecycle/manager.js +390 -0
- package/dist/llm/bodyCapture.d.ts +31 -0
- package/dist/llm/bodyCapture.js +293 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/server.d.ts +35 -0
- package/dist/llm/server.js +359 -0
- package/dist/llm/traceBackfill.d.ts +53 -0
- package/dist/llm/traceBackfill.js +525 -0
- package/dist/llm/traceConfig.d.ts +7 -0
- package/dist/llm/traceConfig.js +44 -0
- package/dist/llm/traceControl.d.ts +16 -0
- package/dist/llm/traceControl.js +85 -0
- package/dist/llm/traceEnvelope.d.ts +75 -0
- package/dist/llm/traceEnvelope.js +286 -0
- package/dist/llm/traceSink.d.ts +61 -0
- package/dist/llm/traceSink.js +278 -0
- package/dist/llm/traceUploadPayload.d.ts +14 -0
- package/dist/llm/traceUploadPayload.js +27 -0
- package/dist/llm/upstream.d.ts +68 -0
- package/dist/llm/upstream.js +491 -0
- package/dist/private/adapterLoader.d.ts +46 -0
- package/dist/private/adapterLoader.js +62 -0
- package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
- package/dist/private/privateRuntimeSmokeOptions.js +132 -0
- package/dist/router/cachePassthrough.d.ts +3 -0
- package/dist/router/cachePassthrough.js +13 -0
- package/dist/router/features.d.ts +9 -0
- package/dist/router/features.js +52 -0
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +6 -0
- package/dist/router/messagesRoute.d.ts +138 -0
- package/dist/router/messagesRoute.js +753 -0
- package/dist/router/modelRouter.d.ts +49 -0
- package/dist/router/modelRouter.js +66 -0
- package/dist/router/providerRoutes.d.ts +42 -0
- package/dist/router/providerRoutes.js +1579 -0
- package/dist/router/sseScan.d.ts +56 -0
- package/dist/router/sseScan.js +543 -0
- package/dist/selfUpdate/executor.d.ts +90 -0
- package/dist/selfUpdate/executor.js +179 -0
- package/dist/selfUpdate/failureCodes.d.ts +33 -0
- package/dist/selfUpdate/failureCodes.js +84 -0
- package/dist/selfUpdate/index.d.ts +5 -0
- package/dist/selfUpdate/index.js +5 -0
- package/dist/selfUpdate/lastUpdate.d.ts +43 -0
- package/dist/selfUpdate/lastUpdate.js +195 -0
- package/dist/selfUpdate/policy.d.ts +3 -0
- package/dist/selfUpdate/policy.js +9 -0
- package/dist/selfUpdate/releaseBinary.d.ts +56 -0
- package/dist/selfUpdate/releaseBinary.js +498 -0
- package/dist/selfUpdate/version.d.ts +2 -0
- package/dist/selfUpdate/version.js +14 -0
- package/dist/sync/engine.d.ts +65 -0
- package/dist/sync/engine.js +461 -0
- package/package.json +41 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { mailbox, hub as hubNs } from '@evomap/evolver-core';
|
|
2
|
+
import { clearLastUpdateOnAck, isLastUpdateRelatedError, readPendingLastUpdate, shouldClearForLastUpdateAck, } from '../selfUpdate/lastUpdate.js';
|
|
3
|
+
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 360_000;
|
|
4
|
+
export const MIN_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
5
|
+
export const HEARTBEAT_BACKOFF_CAP_MS = 15 * 60_000;
|
|
6
|
+
export const MAX_REAUTH_ATTEMPTS = 2;
|
|
7
|
+
export const REAUTH_BACKOFF_BASE_MS = 30 * 60_000;
|
|
8
|
+
export const REAUTH_BACKOFF_MAX_MS = 4 * 60 * 60_000;
|
|
9
|
+
export const HUB_UNREACHABLE_BACKOFF_MS = 60_000;
|
|
10
|
+
export const HUB_UNREACHABLE_BACKOFF_MAX_MS = 10 * 60_000;
|
|
11
|
+
export const MIN_HUB_UNREACHABLE_RETRY_MS = 1_000;
|
|
12
|
+
export const MAX_LAST_ERROR_LENGTH = 1_000;
|
|
13
|
+
const K = {
|
|
14
|
+
nodeId: 'node_id',
|
|
15
|
+
reauthUntil: 'lifecycle:reauth_until',
|
|
16
|
+
reauthAttempts: 'lifecycle:reauth_attempts',
|
|
17
|
+
helloRlUntil: 'lifecycle:hello_rl_until',
|
|
18
|
+
hubUnreachableUntil: 'lifecycle:hub_unreachable_until',
|
|
19
|
+
authStatus: 'hub:auth_status',
|
|
20
|
+
lastError: 'sync:last_error',
|
|
21
|
+
nodeSecret: 'node_secret',
|
|
22
|
+
nodeSecretSource: 'node_secret_source',
|
|
23
|
+
nodeSecretVersion: 'node_secret_version',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* LifecycleManager(M6-3): hello/heartbeat/reauth 状态机, 移植 v1 lifecycle/manager.js.
|
|
27
|
+
* node_secret 解析下沉到 HubCapability.auth(M6-5 LegacyAuthShim 双轨); 本层只管:
|
|
28
|
+
* 注册(hello, 持久化 node_id)、心跳节奏、403/401→auth.rotate() 退避(30m→4h, MAX 2 次)、hello 限流尊重.
|
|
29
|
+
* 纯逻辑注入 now/hello/heartbeat → 对 FakeHubCapability 确定性测.
|
|
30
|
+
*/
|
|
31
|
+
export class LifecycleManager {
|
|
32
|
+
deps;
|
|
33
|
+
reauthInProgress = false;
|
|
34
|
+
constructor(deps) {
|
|
35
|
+
this.deps = deps;
|
|
36
|
+
}
|
|
37
|
+
get nodeId() { return this.deps.store.getState(K.nodeId); }
|
|
38
|
+
/** 当前上报版本(供 force_update 决策 / hub fleet 观测). */
|
|
39
|
+
get version() { return this.deps.evolverVersion; }
|
|
40
|
+
/** 注册. 尊重 hub hello 限流窗口; 成功持久化 node_id + 清 reauth 退避. 随报当前版本(hub 观测 fleet). */
|
|
41
|
+
async doHello(rotate = false) {
|
|
42
|
+
const now = this.deps.now();
|
|
43
|
+
const hubWait = this.hubUnreachableWaitMs(now);
|
|
44
|
+
if (hubWait > 0)
|
|
45
|
+
return { ok: false, error: 'hub_unreachable_backoff', retryAfterMs: hubWait };
|
|
46
|
+
const rlUntil = Number(this.deps.store.getState(K.helloRlUntil) ?? 0);
|
|
47
|
+
if (rlUntil > now) {
|
|
48
|
+
this.deps.store.setState(K.authStatus, 'rate_limited');
|
|
49
|
+
this.deps.store.setState(K.lastError, `hello_rate_limited:${rlUntil}`);
|
|
50
|
+
return { ok: false, error: 'hello_rate_limited' };
|
|
51
|
+
}
|
|
52
|
+
let res;
|
|
53
|
+
try {
|
|
54
|
+
res = await this.callHello(rotate);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// v1 parity (src/gep/a2aProtocol.js sendHelloToHub, src/proxy/lifecycle/manager.js#hello):
|
|
58
|
+
// hello never rethrows — every failure resolves to {ok:false}. A hub link
|
|
59
|
+
// drop backs off; any other failure (incl. 401/403) lands on the failure
|
|
60
|
+
// terminal in one shot. The hub never rejects auth on node_secret_version
|
|
61
|
+
// (it is a backfill hint only), so there is NO "strip version and retry
|
|
62
|
+
// hello" path. Version clearing is driven solely by the hub response
|
|
63
|
+
// (HubCapability.hello adopts version=undefined when the hub omits it) and
|
|
64
|
+
// by the reauth rotate prelude (clearLegacyNodeSecretVersion before rotate).
|
|
65
|
+
if (isHubUnreachable(err)) {
|
|
66
|
+
return { ok: false, error: 'hub_unreachable', retryAfterMs: this.recordHubUnreachable(err, now) };
|
|
67
|
+
}
|
|
68
|
+
if (isAuthError(err)) {
|
|
69
|
+
res = { ok: false, authError: true, error: safeErrorMessage(err), httpStatus: errorStatus(err) };
|
|
70
|
+
}
|
|
71
|
+
else if (isHubClientError(err)) {
|
|
72
|
+
res = { ok: false, error: safeErrorMessage(err), httpStatus: errorStatus(err) };
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (isHubUnreachableResult(res))
|
|
79
|
+
this.recordHubUnreachable(res, now);
|
|
80
|
+
if (res.rateLimitUntilMs)
|
|
81
|
+
this.deps.store.setState(K.helloRlUntil, String(res.rateLimitUntilMs));
|
|
82
|
+
if (res.ok && res.nodeId) {
|
|
83
|
+
this.deps.store.setState(K.nodeId, res.nodeId);
|
|
84
|
+
this.deps.store.setState(K.reauthAttempts, '0');
|
|
85
|
+
this.deps.store.setState(K.reauthUntil, '0');
|
|
86
|
+
this.deps.store.setState(K.hubUnreachableUntil, '0');
|
|
87
|
+
this.deps.store.setState(K.helloRlUntil, '0');
|
|
88
|
+
this.deps.store.setState(K.authStatus, 'ok');
|
|
89
|
+
this.deps.store.setState(K.lastError, '');
|
|
90
|
+
}
|
|
91
|
+
else if (!res.ok) {
|
|
92
|
+
// Secret divergence (HTTP 200, secret already cleared in-memory + durably by HubCapability):
|
|
93
|
+
// do NOT mark auth_failed, do NOT reauth, do NOT arm backoff. The next tick re-hellos
|
|
94
|
+
// unauthenticated and recovers (v1 a2aProtocol.js secret-divergence carve-out). Checked
|
|
95
|
+
// before authError so a diverged secret never enters the reauth-rotate ladder.
|
|
96
|
+
if (res.secretDiverged) {
|
|
97
|
+
this.deps.store.setState(K.authStatus, 'secret_diverged');
|
|
98
|
+
this.deps.store.setState(K.lastError, `hello:${res.error ?? 'secret_diverged'}`);
|
|
99
|
+
}
|
|
100
|
+
else if (res.authError) {
|
|
101
|
+
this.deps.store.setState(K.authStatus, 'auth_failed');
|
|
102
|
+
this.deps.store.setState(K.lastError, `hello:${safeErrorMessage(res.error ?? 'auth_error')}`);
|
|
103
|
+
if (!rotate)
|
|
104
|
+
await this.reauthenticate();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (res.rateLimitUntilMs)
|
|
108
|
+
this.deps.store.setState(K.authStatus, 'rate_limited');
|
|
109
|
+
this.deps.store.setState(K.lastError, `hello:${safeErrorMessage(res.error ?? 'failed')}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return res;
|
|
113
|
+
}
|
|
114
|
+
/** 心跳. authError → 触发 reauth 状态机. 随心跳上报当前版本(hub 观测 fleet, #108). */
|
|
115
|
+
async doHeartbeat() {
|
|
116
|
+
try {
|
|
117
|
+
if (this.hubUnreachableWaitMs(this.deps.now()) > 0)
|
|
118
|
+
return { ok: false, reauthed: false };
|
|
119
|
+
const hb = this.heartbeatOptions();
|
|
120
|
+
const sentLastUpdate = hb?.lastUpdate;
|
|
121
|
+
const res = await this.deps.heartbeat(hb);
|
|
122
|
+
// Read `now` AFTER the network round-trip so hub-unreachable backoff windows and lastUpdate ack
|
|
123
|
+
// timing are measured from when the response arrived, not from before the request was sent.
|
|
124
|
+
const now = this.deps.now();
|
|
125
|
+
this.handleLastUpdateAck(res, sentLastUpdate, now);
|
|
126
|
+
this.maybeTriggerForceUpdate(res);
|
|
127
|
+
if (res.ok) {
|
|
128
|
+
this.deps.store.setState(K.hubUnreachableUntil, '0');
|
|
129
|
+
this.deps.store.setState(K.authStatus, 'ok');
|
|
130
|
+
this.deps.store.setState(K.lastError, '');
|
|
131
|
+
return { ok: true, reauthed: false };
|
|
132
|
+
}
|
|
133
|
+
if (res.status === 'unknown_node') {
|
|
134
|
+
this.deps.store.setState(K.authStatus, 'unknown_node');
|
|
135
|
+
this.deps.store.setState(K.lastError, 'heartbeat:unknown_node');
|
|
136
|
+
await this.doHello(false);
|
|
137
|
+
return { ok: false, reauthed: false };
|
|
138
|
+
}
|
|
139
|
+
if (isHubUnreachableResult(res)) {
|
|
140
|
+
this.recordHubUnreachable(res, now);
|
|
141
|
+
return { ok: false, reauthed: false };
|
|
142
|
+
}
|
|
143
|
+
if (res.authError) {
|
|
144
|
+
this.deps.store.setState(K.authStatus, 'auth_failed');
|
|
145
|
+
this.deps.store.setState(K.lastError, `heartbeat:${res.error ?? 'auth_error'}`);
|
|
146
|
+
const reauthed = await this.reauthenticate();
|
|
147
|
+
return { ok: false, reauthed };
|
|
148
|
+
}
|
|
149
|
+
this.deps.store.setState(K.lastError, `heartbeat:${res.error ?? 'failed'}`);
|
|
150
|
+
return { ok: false, reauthed: false };
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (isHubUnreachable(err)) {
|
|
154
|
+
this.recordHubUnreachable(err, this.deps.now());
|
|
155
|
+
return { ok: false, reauthed: false };
|
|
156
|
+
}
|
|
157
|
+
const error = this.recordHeartbeatException(err);
|
|
158
|
+
return { ok: false, reauthed: false, error };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
nextHeartbeatDelay(consecutiveFailures = 0) {
|
|
162
|
+
try {
|
|
163
|
+
const hubWait = this.hubUnreachableWaitMs(this.deps.now());
|
|
164
|
+
if (hubWait > 0)
|
|
165
|
+
return Math.max(MIN_HUB_UNREACHABLE_RETRY_MS, hubWait);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Store read threw (corrupt state) — treat as a failure signal: force at least one backoff
|
|
169
|
+
// step (base*2) rather than retrying at the bare base interval against a broken local store.
|
|
170
|
+
return this.heartbeatFailureBackoffDelay(Math.max(1, consecutiveFailures));
|
|
171
|
+
}
|
|
172
|
+
return this.heartbeatFailureBackoffDelay(consecutiveFailures);
|
|
173
|
+
}
|
|
174
|
+
heartbeatFailureBackoffDelay(consecutiveFailures) {
|
|
175
|
+
const base = Math.max(MIN_HEARTBEAT_INTERVAL_MS, this.deps.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
176
|
+
if (consecutiveFailures <= 0)
|
|
177
|
+
return base;
|
|
178
|
+
const cap = Math.max(base, HEARTBEAT_BACKOFF_CAP_MS);
|
|
179
|
+
const multiplier = 2 ** Math.min(consecutiveFailures, 30);
|
|
180
|
+
return Math.min(base * multiplier, cap);
|
|
181
|
+
}
|
|
182
|
+
recordHeartbeatException(err) {
|
|
183
|
+
const message = safeErrorMessage(err);
|
|
184
|
+
try {
|
|
185
|
+
this.deps.store.setState(K.lastError, `heartbeat_exception:${message}`);
|
|
186
|
+
}
|
|
187
|
+
catch (stateErr) {
|
|
188
|
+
return `${message}; state_write_failed:${safeErrorMessage(stateErr)}`;
|
|
189
|
+
}
|
|
190
|
+
return message;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 403/401 重认证. 指数退避 30m→4h, 超 MAX_REAUTH_ATTEMPTS 长退避;
|
|
194
|
+
* auth.rotate() 成功后重 hello(rotate) 再注册. 退避状态持久化(进程重启不复位).
|
|
195
|
+
*/
|
|
196
|
+
async reauthenticate() {
|
|
197
|
+
const now = this.deps.now();
|
|
198
|
+
if (this.reauthInProgress)
|
|
199
|
+
return false;
|
|
200
|
+
if (this.hubUnreachableWaitMs(now) > 0)
|
|
201
|
+
return false;
|
|
202
|
+
const until = Number(this.deps.store.getState(K.reauthUntil) ?? 0);
|
|
203
|
+
if (until > now)
|
|
204
|
+
return false; // 退避中
|
|
205
|
+
this.reauthInProgress = true;
|
|
206
|
+
try {
|
|
207
|
+
const attempts = Number(this.deps.store.getState(K.reauthAttempts) ?? 0) + 1;
|
|
208
|
+
if (attempts > MAX_REAUTH_ATTEMPTS) {
|
|
209
|
+
this.deps.store.setState(K.reauthAttempts, String(attempts));
|
|
210
|
+
this.setBackoff(attempts, now);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
this.clearLegacyNodeSecretVersion();
|
|
215
|
+
await this.deps.auth.rotate();
|
|
216
|
+
const hello = await this.callHello(true);
|
|
217
|
+
if (hello.secretDiverged) {
|
|
218
|
+
// v1 parity (a2aProtocol.js L1842-1851): the hub rejected our cached secret as
|
|
219
|
+
// diverged and HubCapability already cleared it (in-memory + store + on-disk files).
|
|
220
|
+
// Arming the escalating reauth backoff here would block the natural recovery — the
|
|
221
|
+
// next tick sends an UNAUTHENTICATED hello the hub treats as a fresh registration.
|
|
222
|
+
// So reset the reauth ladder, surface the signal, and let the next tick re-hello.
|
|
223
|
+
this.deps.store.setState(K.reauthAttempts, '0');
|
|
224
|
+
this.deps.store.setState(K.reauthUntil, '0');
|
|
225
|
+
this.deps.store.setState(K.authStatus, 'secret_diverged');
|
|
226
|
+
this.deps.store.setState(K.lastError, 'hello:secret_diverged_cleared');
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (hello.ok) {
|
|
230
|
+
const verified = await this.verifyReauthHeartbeat(now);
|
|
231
|
+
if (verified === 'ok') {
|
|
232
|
+
this.deps.store.setState(K.reauthAttempts, '0');
|
|
233
|
+
this.deps.store.setState(K.reauthUntil, '0');
|
|
234
|
+
this.deps.store.setState(K.hubUnreachableUntil, '0');
|
|
235
|
+
this.deps.store.setState(K.authStatus, 'recovered');
|
|
236
|
+
this.deps.store.setState(K.lastError, '');
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (verified === 'hub_unreachable')
|
|
240
|
+
return false;
|
|
241
|
+
this.deps.store.setState(K.reauthAttempts, String(attempts));
|
|
242
|
+
this.setBackoff(attempts, now);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (isHubUnreachableResult(hello)) {
|
|
246
|
+
this.recordHubUnreachable(hello, now);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
this.deps.store.setState(K.reauthAttempts, String(attempts));
|
|
250
|
+
this.setBackoff(attempts, now);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
if (isHubUnreachable(err)) {
|
|
255
|
+
this.recordHubUnreachable(err, now);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
this.deps.store.setState(K.reauthAttempts, String(attempts));
|
|
259
|
+
this.setBackoff(attempts, now);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
this.reauthInProgress = false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
setBackoff(attempts, now) {
|
|
268
|
+
const backoff = Math.min(REAUTH_BACKOFF_BASE_MS * 2 ** (attempts - 1), REAUTH_BACKOFF_MAX_MS);
|
|
269
|
+
this.deps.store.setState(K.reauthUntil, String(now + backoff));
|
|
270
|
+
this.deps.store.setState(K.authStatus, 'backoff');
|
|
271
|
+
this.deps.store.setState(K.lastError, `reauth_backoff:${attempts}`);
|
|
272
|
+
}
|
|
273
|
+
hubUnreachableWaitMs(now) {
|
|
274
|
+
return Math.max(0, Number(this.deps.store.getState(K.hubUnreachableUntil) ?? 0) - now);
|
|
275
|
+
}
|
|
276
|
+
recordHubUnreachable(err, now) {
|
|
277
|
+
const retryAfter = retryAfterMs(err);
|
|
278
|
+
this.deps.store.setState(K.hubUnreachableUntil, String(now + retryAfter));
|
|
279
|
+
this.deps.store.setState(K.authStatus, 'hub_unreachable');
|
|
280
|
+
this.deps.store.setState(K.lastError, `hub_unreachable:${safeErrorMessage(err)}`);
|
|
281
|
+
return retryAfter;
|
|
282
|
+
}
|
|
283
|
+
clearLegacyNodeSecretVersion() {
|
|
284
|
+
if (this.deps.helloMode === 'enterprise_token')
|
|
285
|
+
return;
|
|
286
|
+
const auth = this.deps.auth;
|
|
287
|
+
this.deps.store.setState(K.nodeSecretVersion, '');
|
|
288
|
+
auth.adoptNodeSecretVersion?.(undefined);
|
|
289
|
+
}
|
|
290
|
+
async verifyReauthHeartbeat(now) {
|
|
291
|
+
try {
|
|
292
|
+
const opts = this.heartbeatOptions();
|
|
293
|
+
const hb = await this.deps.heartbeat(opts);
|
|
294
|
+
this.handleLastUpdateAck(hb, opts?.lastUpdate, now);
|
|
295
|
+
this.maybeTriggerForceUpdate(hb);
|
|
296
|
+
if (hb.ok)
|
|
297
|
+
return 'ok';
|
|
298
|
+
if (isHubUnreachableResult(hb)) {
|
|
299
|
+
this.recordHubUnreachable(hb, now);
|
|
300
|
+
return 'hub_unreachable';
|
|
301
|
+
}
|
|
302
|
+
return 'failed';
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (isHubUnreachable(err)) {
|
|
306
|
+
this.recordHubUnreachable(err, now);
|
|
307
|
+
return 'hub_unreachable';
|
|
308
|
+
}
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
heartbeatOptions() {
|
|
313
|
+
const opts = {};
|
|
314
|
+
if (this.deps.evolverVersion)
|
|
315
|
+
opts.evolverVersion = this.deps.evolverVersion;
|
|
316
|
+
const lastUpdate = readPendingLastUpdate(this.deps.store, this.deps.now());
|
|
317
|
+
if (lastUpdate)
|
|
318
|
+
opts.lastUpdate = lastUpdate;
|
|
319
|
+
return Object.keys(opts).length > 0 ? opts : undefined;
|
|
320
|
+
}
|
|
321
|
+
callHello(rotate) {
|
|
322
|
+
const safeRotate = this.deps.helloMode === 'enterprise_token' ? false : rotate;
|
|
323
|
+
return this.deps.hello({ rotate: safeRotate, ...(this.deps.evolverVersion ? { evolverVersion: this.deps.evolverVersion } : {}) });
|
|
324
|
+
}
|
|
325
|
+
handleLastUpdateAck(res, sent, now) {
|
|
326
|
+
if (!sent)
|
|
327
|
+
return;
|
|
328
|
+
if (shouldClearForLastUpdateAck(res.lastUpdateAck)) {
|
|
329
|
+
clearLastUpdateOnAck(this.deps.store, sent, now);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (res.lastUpdateAck?.reason === 'failed')
|
|
333
|
+
return;
|
|
334
|
+
if (res.httpStatus === 400 && (isLastUpdateRelatedError(res.error) || isLastUpdateRelatedError(res.details))) {
|
|
335
|
+
clearLastUpdateOnAck(this.deps.store, sent, now);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (res.ok && res.status !== 'unknown_node' && !res.lastUpdateAck) {
|
|
339
|
+
clearLastUpdateOnAck(this.deps.store, sent, now);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
maybeTriggerForceUpdate(res) {
|
|
343
|
+
if (!res.forceUpdate || typeof res.forceUpdate !== 'object')
|
|
344
|
+
return;
|
|
345
|
+
const source = res.httpStatus === 426 ? 'heartbeat_426' : 'heartbeat_200';
|
|
346
|
+
void this.deps.onForceUpdateDirective?.(res.forceUpdate, source);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function isHubUnreachable(err) {
|
|
350
|
+
const e = err;
|
|
351
|
+
return e?.name === 'HubUnreachableError' || e?.code === 'HUB_UNREACHABLE' || e?.error === 'hub_unreachable';
|
|
352
|
+
}
|
|
353
|
+
function isAuthError(err) {
|
|
354
|
+
const e = err;
|
|
355
|
+
return e?.name === 'AuthError' || e?.authError === true;
|
|
356
|
+
}
|
|
357
|
+
function isHubClientError(err) {
|
|
358
|
+
const e = err;
|
|
359
|
+
return e?.name === 'HubClientError' && typeof e.status === 'number';
|
|
360
|
+
}
|
|
361
|
+
function errorStatus(err) {
|
|
362
|
+
const status = err?.status;
|
|
363
|
+
return typeof status === 'number' && Number.isFinite(status) ? status : undefined;
|
|
364
|
+
}
|
|
365
|
+
function isHubUnreachableResult(res) {
|
|
366
|
+
return res?.error === 'hub_unreachable' || res?.error === 'hub_unreachable_backoff';
|
|
367
|
+
}
|
|
368
|
+
function errorMessage(err) {
|
|
369
|
+
return err instanceof Error ? err.message : String(err);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Redact secrets/PII then cap length before any durable, UI-exposed telemetry write to
|
|
373
|
+
* `sync:last_error` (mirrors ProxyDaemon's safeHeartbeatTickErrorMessage so the two layers
|
|
374
|
+
* apply the same floor). redactString is pure/total but wrapped defensively.
|
|
375
|
+
*/
|
|
376
|
+
function safeErrorMessage(err) {
|
|
377
|
+
const message = errorMessage(err);
|
|
378
|
+
try {
|
|
379
|
+
return hubNs.redactString(message).slice(0, MAX_LAST_ERROR_LENGTH);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return message.slice(0, MAX_LAST_ERROR_LENGTH);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function retryAfterMs(err) {
|
|
386
|
+
const retry = err?.retryAfterMs
|
|
387
|
+
?? err?.details?.retryAfterMs;
|
|
388
|
+
const delay = Math.max(MIN_HUB_UNREACHABLE_RETRY_MS, typeof retry === 'number' && Number.isFinite(retry) ? retry : HUB_UNREACHABLE_BACKOFF_MS);
|
|
389
|
+
return Math.min(delay, HUB_UNREACHABLE_BACKOFF_MAX_MS);
|
|
390
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bump when the redaction ruleset changes so downstream can tell which scrub a row went through. */
|
|
2
|
+
export declare const REDACTION_VERSION = "evolver-redact-v2";
|
|
3
|
+
export declare function legacyProxyTraceFullEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Native body capture switch. Defaults to the v1-compatible full trace mode, but explicit v2 capture flags win so
|
|
6
|
+
* operators can disable body capture without also setting the legacy EVOMAP_PROXY_TRACE knob.
|
|
7
|
+
*/
|
|
8
|
+
export declare function captureBodiesEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
9
|
+
/** Default per-body cap (bytes), aligned with the public Hub mailbox outbound envelope limit. A single trace may
|
|
10
|
+
* still exceed the final encrypted outbound envelope when it carries multiple large bodies; in that case the row
|
|
11
|
+
* is explicitly marked incomplete rather than silently downgraded to a preview. */
|
|
12
|
+
export declare const DEFAULT_TRACE_ENVELOPE_MAX_CHARS: number;
|
|
13
|
+
export declare function bodyMaxChars(env?: NodeJS.ProcessEnv): number;
|
|
14
|
+
export declare function traceEnvelopeMaxChars(env?: NodeJS.ProcessEnv): number;
|
|
15
|
+
export declare function positiveIntegerFromEnv(env: NodeJS.ProcessEnv, names: readonly string[], fallback: number): number;
|
|
16
|
+
/** Apply the redaction ruleset to a string. Pure; safe on arbitrary text. */
|
|
17
|
+
export declare function redactText(input: string): string;
|
|
18
|
+
export interface CapturedBody {
|
|
19
|
+
/** Redacted JSON string of the native payload, or an explicit incomplete envelope when it exceeds the cap. */
|
|
20
|
+
body: string;
|
|
21
|
+
/** True when the full body could not fit in the trace envelope cap. */
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
/** Redaction ruleset marker, so a consumer can tell the row actually went through a scrub. */
|
|
24
|
+
redaction: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function stableUserIdHash(value: unknown): string;
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a native request/response payload, run it through redaction, and cap its size. Never throws — capture
|
|
29
|
+
* must never break serving — returning a small error envelope instead. Returns undefined for empty/absent input.
|
|
30
|
+
*/
|
|
31
|
+
export declare function captureBody(value: unknown, env?: NodeJS.ProcessEnv): CapturedBody | undefined;
|