@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,56 @@
|
|
|
1
|
+
import { ops } from '@evomap/evolver-core';
|
|
2
|
+
import type { DownloadResult, ForceUpdateDirective } from './executor.js';
|
|
3
|
+
type UpdateManifest = ops.UpdateManifest;
|
|
4
|
+
type FetchFn = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
export interface ReleaseBinaryOptions {
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
7
|
+
fetchFn?: FetchFn;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
arch?: NodeJS.Architecture;
|
|
10
|
+
tmpDir?: string;
|
|
11
|
+
targetPath?: string;
|
|
12
|
+
processExecPath?: string;
|
|
13
|
+
requireSignedManifest?: boolean;
|
|
14
|
+
maxExtractedTarballBytes?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Hard ceiling for Channel 1b tarball downloads. A compromised/corrupt release
|
|
18
|
+
* could advertise a multi-GB tar.gz and OOM us because tarballBytes is buffered
|
|
19
|
+
* before extraction. 64MB is well above any real single-platform precompiled
|
|
20
|
+
* release tarball today.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_TARBALL_BYTES: number;
|
|
23
|
+
/**
|
|
24
|
+
* Hard ceiling for the decompressed tar stream. This is intentionally larger
|
|
25
|
+
* than the compressed-body cap so normal binary tarballs still fit, while gzip
|
|
26
|
+
* bombs fail before the extractor accumulates unbounded output in memory.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MAX_EXTRACTED_TARBALL_BYTES: number;
|
|
29
|
+
export declare function requiredVersionForRelease(raw: unknown): string;
|
|
30
|
+
export declare function releaseAssetName(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): string;
|
|
31
|
+
export declare function resolveGithubReleaseManifest(directive: ForceUpdateDirective, opts?: ReleaseBinaryOptions): Promise<UpdateManifest>;
|
|
32
|
+
export declare function downloadGithubReleaseArtifact(targetVersion: string, directive: ForceUpdateDirective, opts?: ReleaseBinaryOptions): Promise<DownloadResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Channel 1b: download the same release's `<assetName>.tar.gz` and extract the
|
|
35
|
+
* binary file matching `assetName` (or its basename). The tarball MUST contain
|
|
36
|
+
* an entry that matches the asset name — anything else and we fail closed with
|
|
37
|
+
* `fallback_missing_binary` (no half-extracted state).
|
|
38
|
+
*
|
|
39
|
+
* Note: we do NOT use `codeload.github.com/.../tar.gz` here. Codeload returns
|
|
40
|
+
* the SOURCE tree, which has no compiled binary; V2 ships a single binary, so
|
|
41
|
+
* the fallback only restores the DOWNLOAD leg, not an "install from source"
|
|
42
|
+
* leg. If a deployment wants the source-tree fallback V1 had, that is a
|
|
43
|
+
* separate, signed channel that doesn't exist yet in V2.
|
|
44
|
+
*
|
|
45
|
+
* The returned DownloadResult is shape-compatible with the primary path: same
|
|
46
|
+
* `artifacts[].path` (assetName) and a sha256 over the EXTRACTED binary bytes,
|
|
47
|
+
* so the executor's verifyManifest gate still enforces signature + hash before
|
|
48
|
+
* any atomicReplace.
|
|
49
|
+
*/
|
|
50
|
+
export declare function downloadGithubReleaseTarball(version: string, assetName: string, directive: ForceUpdateDirective, opts: ReleaseBinaryOptions, primaryError: unknown): Promise<DownloadResult>;
|
|
51
|
+
export declare function atomicReplaceExecutable(stagedPath: string, opts?: ReleaseBinaryOptions): Promise<void>;
|
|
52
|
+
export declare function resolveSelfUpdateTarget(opts?: ReleaseBinaryOptions): {
|
|
53
|
+
path: string;
|
|
54
|
+
explicit: boolean;
|
|
55
|
+
};
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { chmod, copyFile, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { createGunzip } from 'node:zlib';
|
|
6
|
+
import { ops } from '@evomap/evolver-core';
|
|
7
|
+
import { SELF_UPDATE_FAILURE_CODES, SelfUpdateFailureError, selfUpdateFailure } from './failureCodes.js';
|
|
8
|
+
const DEFAULT_RELEASES_URL = 'https://github.com/EvoMap/evolver/releases';
|
|
9
|
+
const SIGNED_MANIFEST_ASSET = 'evolver-update-manifest.json';
|
|
10
|
+
const RELEASE_DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling for Channel 1b tarball downloads. A compromised/corrupt release
|
|
13
|
+
* could advertise a multi-GB tar.gz and OOM us because tarballBytes is buffered
|
|
14
|
+
* before extraction. 64MB is well above any real single-platform precompiled
|
|
15
|
+
* release tarball today.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* Hard ceiling for the decompressed tar stream. This is intentionally larger
|
|
20
|
+
* than the compressed-body cap so normal binary tarballs still fit, while gzip
|
|
21
|
+
* bombs fail before the extractor accumulates unbounded output in memory.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_EXTRACTED_TARBALL_BYTES = 128 * 1024 * 1024;
|
|
24
|
+
export function requiredVersionForRelease(raw) {
|
|
25
|
+
try {
|
|
26
|
+
return ops.requireSelfUpdateVersion(raw, 'required_version');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.BAD_REQUIRED_VERSION, errorDetail(err), { cause: err });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function releaseAssetName(platform = process.platform, arch = process.arch) {
|
|
33
|
+
if (platform === 'darwin' && arch === 'arm64')
|
|
34
|
+
return 'evolver-darwin-arm64';
|
|
35
|
+
if (platform === 'darwin' && arch === 'x64')
|
|
36
|
+
return 'evolver-darwin-x64';
|
|
37
|
+
if (platform === 'linux' && arch === 'arm64')
|
|
38
|
+
return 'evolver-linux-arm64';
|
|
39
|
+
if (platform === 'linux' && arch === 'x64')
|
|
40
|
+
return 'evolver-linux-x64';
|
|
41
|
+
if (platform === 'win32' && arch === 'x64')
|
|
42
|
+
return 'evolver-windows-x64.exe';
|
|
43
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, `unsupported_release_platform:${platform}:${arch}`);
|
|
44
|
+
}
|
|
45
|
+
export async function resolveGithubReleaseManifest(directive, opts = {}) {
|
|
46
|
+
assertGithubUpdateChannelAvailable(directive);
|
|
47
|
+
const version = requiredVersionForRelease(directive.required_version);
|
|
48
|
+
const assetName = releaseAssetName(opts.platform, opts.arch);
|
|
49
|
+
if (opts.requireSignedManifest) {
|
|
50
|
+
return fetchSignedReleaseManifest(directive.release_url, version, assetName, opts.fetchFn);
|
|
51
|
+
}
|
|
52
|
+
const sumsUrl = releaseDownloadUrl(directive.release_url, version, 'SHA256SUMS.txt');
|
|
53
|
+
const text = await fetchText(sumsUrl, opts.fetchFn);
|
|
54
|
+
const sha256 = shaForAsset(text, assetName);
|
|
55
|
+
return { version, artifacts: [{ path: assetName, sha256 }] };
|
|
56
|
+
}
|
|
57
|
+
export async function downloadGithubReleaseArtifact(targetVersion, directive, opts = {}) {
|
|
58
|
+
assertGithubUpdateChannelAvailable(directive);
|
|
59
|
+
const version = requiredVersionForRelease(targetVersion);
|
|
60
|
+
const assetName = releaseAssetName(opts.platform, opts.arch);
|
|
61
|
+
// Channel 1a: try the precompiled binary asset first (the normal happy path).
|
|
62
|
+
// Channel 1b: if 1a fails — release rate-limited / asset missing / geo-blocked —
|
|
63
|
+
// fall through to a same-release `.tar.gz` form and extract the binary out of it.
|
|
64
|
+
// The signed-manifest sha256 gate (verifyManifest) is unchanged either way: the
|
|
65
|
+
// staged bytes still have to hash to the manifest's art.sha256 for assetName.
|
|
66
|
+
// See PORT v1 #282: "force-update: add tarball fallback for self-update".
|
|
67
|
+
let primaryError;
|
|
68
|
+
try {
|
|
69
|
+
return await downloadBinaryAsset(version, assetName, directive, opts);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
primaryError = err;
|
|
73
|
+
}
|
|
74
|
+
return downloadGithubReleaseTarball(version, assetName, directive, opts, primaryError);
|
|
75
|
+
}
|
|
76
|
+
async function downloadBinaryAsset(version, assetName, directive, opts) {
|
|
77
|
+
const assetUrl = releaseDownloadUrl(directive.release_url, version, assetName);
|
|
78
|
+
const bytes = Buffer.from(await fetchBytes(assetUrl, opts.fetchFn));
|
|
79
|
+
if (bytes.byteLength === 0) {
|
|
80
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `empty release asset:${assetName}`);
|
|
81
|
+
}
|
|
82
|
+
return stageStagedBinary(bytes, assetName, opts, 'binary');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Channel 1b: download the same release's `<assetName>.tar.gz` and extract the
|
|
86
|
+
* binary file matching `assetName` (or its basename). The tarball MUST contain
|
|
87
|
+
* an entry that matches the asset name — anything else and we fail closed with
|
|
88
|
+
* `fallback_missing_binary` (no half-extracted state).
|
|
89
|
+
*
|
|
90
|
+
* Note: we do NOT use `codeload.github.com/.../tar.gz` here. Codeload returns
|
|
91
|
+
* the SOURCE tree, which has no compiled binary; V2 ships a single binary, so
|
|
92
|
+
* the fallback only restores the DOWNLOAD leg, not an "install from source"
|
|
93
|
+
* leg. If a deployment wants the source-tree fallback V1 had, that is a
|
|
94
|
+
* separate, signed channel that doesn't exist yet in V2.
|
|
95
|
+
*
|
|
96
|
+
* The returned DownloadResult is shape-compatible with the primary path: same
|
|
97
|
+
* `artifacts[].path` (assetName) and a sha256 over the EXTRACTED binary bytes,
|
|
98
|
+
* so the executor's verifyManifest gate still enforces signature + hash before
|
|
99
|
+
* any atomicReplace.
|
|
100
|
+
*/
|
|
101
|
+
export async function downloadGithubReleaseTarball(version, assetName, directive, opts, primaryError) {
|
|
102
|
+
const tarballName = `${assetName}.tar.gz`;
|
|
103
|
+
const tarballUrl = releaseDownloadUrl(directive.release_url, version, tarballName);
|
|
104
|
+
let tarballBytes;
|
|
105
|
+
try {
|
|
106
|
+
const arr = await fetchBytes(tarballUrl, opts.fetchFn, MAX_TARBALL_BYTES);
|
|
107
|
+
tarballBytes = Buffer.from(arr);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_DOWNLOAD_FAILED, `${tarballName}:${primaryDetail(primaryError)}|tarball:${errorDetail(err)}`, { cause: err });
|
|
111
|
+
}
|
|
112
|
+
if (tarballBytes.byteLength === 0) {
|
|
113
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_DOWNLOAD_FAILED, `${tarballName}:${primaryDetail(primaryError)}|empty_release_tarball`);
|
|
114
|
+
}
|
|
115
|
+
// OOM guard: a malicious or corrupt release could advertise a multi-GB tarball
|
|
116
|
+
// that we'd otherwise buffer entirely in memory before rejecting at extract
|
|
117
|
+
// time. Cap the body length BEFORE extraction so the worst case is bounded.
|
|
118
|
+
// 64MB is comfortably above any real precompiled evolver binary (single-binary
|
|
119
|
+
// build is < 100MB compressed across all platforms today), and small enough
|
|
120
|
+
// that bin-only nodes don't OOM on it.
|
|
121
|
+
if (tarballBytes.byteLength > MAX_TARBALL_BYTES) {
|
|
122
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_DOWNLOAD_FAILED, `${tarballName}:${primaryDetail(primaryError)}|tarball_too_large:${tarballBytes.byteLength} > ${MAX_TARBALL_BYTES}`);
|
|
123
|
+
}
|
|
124
|
+
let binaryBytes;
|
|
125
|
+
try {
|
|
126
|
+
binaryBytes = await extractBinaryFromTarball(tarballBytes, assetName, resolvedExtractedTarballLimit(opts.maxExtractedTarballBytes));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof SelfUpdateFailureError)
|
|
130
|
+
throw err;
|
|
131
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_EXTRACT_FAILED, `${tarballName}:${primaryDetail(primaryError)}|extract:${errorDetail(err)}`, { cause: err });
|
|
132
|
+
}
|
|
133
|
+
return stageStagedBinary(binaryBytes, assetName, opts, 'tarball');
|
|
134
|
+
}
|
|
135
|
+
async function stageStagedBinary(bytes, assetName, opts, appliedVia) {
|
|
136
|
+
const root = join(opts.tmpDir ?? tmpdir(), `evolver-update-${process.pid}-${randomBytes(6).toString('hex')}`);
|
|
137
|
+
await mkdir(root, { recursive: true });
|
|
138
|
+
const stagedPath = join(root, assetName);
|
|
139
|
+
await writeFile(stagedPath, bytes, { mode: 0o755 });
|
|
140
|
+
await chmod(stagedPath, 0o755);
|
|
141
|
+
return {
|
|
142
|
+
stagedPath,
|
|
143
|
+
artifacts: [{ path: assetName, sha256: createHash('sha256').update(bytes).digest('hex') }],
|
|
144
|
+
appliedVia,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function primaryDetail(err) {
|
|
148
|
+
if (err instanceof SelfUpdateFailureError) {
|
|
149
|
+
const codePrefix = err.failureCode ? `${err.failureCode}:` : '';
|
|
150
|
+
return `primary=${codePrefix}${err.message}`;
|
|
151
|
+
}
|
|
152
|
+
return `primary=${errorDetail(err)}`;
|
|
153
|
+
}
|
|
154
|
+
export async function atomicReplaceExecutable(stagedPath, opts = {}) {
|
|
155
|
+
// The staged download must always be cleaned up, even when target resolution
|
|
156
|
+
// or the install guard rejects BEFORE the copy: a verified release that then
|
|
157
|
+
// fails the guard must not leave its temp staging file on disk. So the whole
|
|
158
|
+
// body runs inside this try/finally, not just the copy/rename step.
|
|
159
|
+
try {
|
|
160
|
+
const target = resolveSelfUpdateTarget(opts);
|
|
161
|
+
const targetPath = target.path;
|
|
162
|
+
assertSafeExecutableTarget(targetPath, target.explicit);
|
|
163
|
+
let st;
|
|
164
|
+
try {
|
|
165
|
+
st = await stat(stagedPath);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
// The staged file existed right after download; a missing/unreadable file
|
|
169
|
+
// here is a replace-phase precondition failure, so the failureCode must
|
|
170
|
+
// line up with the executor's `replace_failed` coarse outcome rather than
|
|
171
|
+
// misreport a download problem.
|
|
172
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, errorDetail(err), { cause: err });
|
|
173
|
+
}
|
|
174
|
+
if (!st.isFile()) {
|
|
175
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'staged_release_not_file');
|
|
176
|
+
}
|
|
177
|
+
const tmpPath = join(dirname(targetPath), `.${basename(targetPath)}.${process.pid}.${randomBytes(6).toString('hex')}.evolver-tmp`);
|
|
178
|
+
try {
|
|
179
|
+
await copyFile(stagedPath, tmpPath);
|
|
180
|
+
await chmod(tmpPath, 0o755);
|
|
181
|
+
if (process.platform === 'win32')
|
|
182
|
+
await rm(targetPath, { force: true });
|
|
183
|
+
await rename(tmpPath, targetPath);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
await rm(tmpPath, { force: true }).catch(() => { });
|
|
187
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.COPY_FAILED, errorDetail(err), { cause: err });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await rm(stagedPath, { force: true }).catch(() => { });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export function resolveSelfUpdateTarget(opts = {}) {
|
|
195
|
+
const explicitTarget = opts.targetPath ?? opts.env?.['EVOLVER_SELF_UPDATE_TARGET_PATH'];
|
|
196
|
+
if (explicitTarget)
|
|
197
|
+
return { path: explicitTarget, explicit: true };
|
|
198
|
+
const execPath = opts.processExecPath ?? process.execPath;
|
|
199
|
+
const name = basename(execPath).toLowerCase();
|
|
200
|
+
if (name.startsWith('evolver'))
|
|
201
|
+
return { path: execPath, explicit: false };
|
|
202
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.INSTALL_GUARD_UNREADABLE, `self_update_target_required:${execPath}`);
|
|
203
|
+
}
|
|
204
|
+
function releaseDownloadUrl(releaseUrl, version, assetName) {
|
|
205
|
+
let base;
|
|
206
|
+
try {
|
|
207
|
+
base = new URL(releaseUrl && releaseUrl.trim() ? releaseUrl : DEFAULT_RELEASES_URL);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, errorDetail(err), { cause: err });
|
|
211
|
+
}
|
|
212
|
+
if (base.protocol !== 'https:' || base.hostname !== 'github.com') {
|
|
213
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, 'invalid_release_url_origin');
|
|
214
|
+
}
|
|
215
|
+
const parts = base.pathname.split('/').filter(Boolean);
|
|
216
|
+
if (parts[0] !== 'EvoMap' || parts[1] !== 'evolver' || parts[2] !== 'releases') {
|
|
217
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, 'invalid_release_url_repo');
|
|
218
|
+
}
|
|
219
|
+
return `https://github.com/EvoMap/evolver/releases/download/v${version}/${assetName}`;
|
|
220
|
+
}
|
|
221
|
+
async function fetchSignedReleaseManifest(releaseUrl, version, assetName, fetchFn) {
|
|
222
|
+
const manifestUrl = releaseDownloadUrl(releaseUrl, version, SIGNED_MANIFEST_ASSET);
|
|
223
|
+
const raw = await fetchText(manifestUrl, fetchFn);
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(raw);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `signed_manifest_invalid_json:${errorDetail(err)}`, { cause: err });
|
|
230
|
+
}
|
|
231
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
232
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, 'signed_manifest_invalid');
|
|
233
|
+
}
|
|
234
|
+
const manifest = parsed;
|
|
235
|
+
if (typeof manifest.signature !== 'string' || manifest.signature.length === 0) {
|
|
236
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION, 'signed_manifest_missing_signature');
|
|
237
|
+
}
|
|
238
|
+
if (manifest.signatureAlg !== 'ed25519') {
|
|
239
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION, 'signed_manifest_unsupported_signature_alg');
|
|
240
|
+
}
|
|
241
|
+
const manifestVersion = ops.normalizeConcreteVersion(manifest.version);
|
|
242
|
+
if (!manifestVersion) {
|
|
243
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, 'signed_manifest_invalid_version');
|
|
244
|
+
}
|
|
245
|
+
if (!ops.currentSatisfiesRequiredVersion(manifestVersion, version)) {
|
|
246
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOADED_VERSION_MISMATCH, 'signed_manifest_below_required');
|
|
247
|
+
}
|
|
248
|
+
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
249
|
+
if (!artifacts.some((artifact) => artifact && typeof artifact.path === 'string' && basename(artifact.path) === assetName)) {
|
|
250
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `signed_manifest_asset_missing:${assetName}`);
|
|
251
|
+
}
|
|
252
|
+
return manifest;
|
|
253
|
+
}
|
|
254
|
+
async function fetchText(url, fetchFn) {
|
|
255
|
+
const fetched = await fetchWith(url, fetchFn);
|
|
256
|
+
return readBodyWithTimeout(url, 'text', () => fetched.response.text(), fetched.abort);
|
|
257
|
+
}
|
|
258
|
+
async function fetchBytes(url, fetchFn, maxBytes) {
|
|
259
|
+
const fetched = await fetchWith(url, fetchFn);
|
|
260
|
+
return readBodyWithTimeout(url, 'arrayBuffer', () => readBytesWithLimit(fetched.response, maxBytes, fetched.abort), fetched.abort);
|
|
261
|
+
}
|
|
262
|
+
async function readBytesWithLimit(response, maxBytes, abort) {
|
|
263
|
+
if (maxBytes === undefined || !response.body) {
|
|
264
|
+
const arr = await response.arrayBuffer();
|
|
265
|
+
if (maxBytes !== undefined && arr.byteLength > maxBytes) {
|
|
266
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `release_body_too_large:${arr.byteLength} > ${maxBytes}`);
|
|
267
|
+
}
|
|
268
|
+
return arr;
|
|
269
|
+
}
|
|
270
|
+
const reader = response.body.getReader();
|
|
271
|
+
const chunks = [];
|
|
272
|
+
let totalBytes = 0;
|
|
273
|
+
try {
|
|
274
|
+
for (;;) {
|
|
275
|
+
const { done, value } = await reader.read();
|
|
276
|
+
if (done)
|
|
277
|
+
break;
|
|
278
|
+
if (!value)
|
|
279
|
+
continue;
|
|
280
|
+
totalBytes += value.byteLength;
|
|
281
|
+
if (totalBytes > maxBytes) {
|
|
282
|
+
abort();
|
|
283
|
+
await reader.cancel().catch(() => { });
|
|
284
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `release_body_too_large:${totalBytes} > ${maxBytes}`);
|
|
285
|
+
}
|
|
286
|
+
chunks.push(value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
reader.releaseLock();
|
|
291
|
+
}
|
|
292
|
+
const out = new Uint8Array(totalBytes);
|
|
293
|
+
let offset = 0;
|
|
294
|
+
for (const chunk of chunks) {
|
|
295
|
+
out.set(chunk, offset);
|
|
296
|
+
offset += chunk.byteLength;
|
|
297
|
+
}
|
|
298
|
+
return out.buffer;
|
|
299
|
+
}
|
|
300
|
+
async function fetchWith(url, fetchFn) {
|
|
301
|
+
const fn = fetchFn ?? globalThis.fetch;
|
|
302
|
+
if (!fn)
|
|
303
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, 'fetch_unavailable');
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
const res = await withReleaseDownloadTimeout(() => fn(url, { signal: controller.signal }), `release_fetch_timeout:${url}`, () => controller.abort());
|
|
306
|
+
if (!res.ok) {
|
|
307
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DEGIT_FAILED, `release_fetch_failed:${res.status}:${url}`);
|
|
308
|
+
}
|
|
309
|
+
return { response: res, abort: () => controller.abort() };
|
|
310
|
+
}
|
|
311
|
+
async function readBodyWithTimeout(url, bodyKind, read, abort) {
|
|
312
|
+
try {
|
|
313
|
+
return await withReleaseDownloadTimeout(read, `release_body_timeout:${bodyKind}:${url}`, abort);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (isSelfUpdateFailureCode(err, SELF_UPDATE_FAILURE_CODES.DEGIT_TIMEOUT))
|
|
317
|
+
throw err;
|
|
318
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `release_body_read_failed:${bodyKind}:${errorDetail(err)}`, { cause: err });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function withReleaseDownloadTimeout(operation, timeoutDetail, onTimeout) {
|
|
322
|
+
let timeoutId;
|
|
323
|
+
const timeout = new Promise((_, reject) => {
|
|
324
|
+
timeoutId = setTimeout(() => {
|
|
325
|
+
onTimeout?.();
|
|
326
|
+
reject(selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DEGIT_TIMEOUT, timeoutDetail));
|
|
327
|
+
}, RELEASE_DOWNLOAD_TIMEOUT_MS);
|
|
328
|
+
unrefTimer(timeoutId);
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
return await Promise.race([operation(), timeout]);
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
clearTimeout(timeoutId);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function unrefTimer(timeoutId) {
|
|
338
|
+
if (timeoutId
|
|
339
|
+
&& typeof timeoutId === 'object'
|
|
340
|
+
&& 'unref' in timeoutId
|
|
341
|
+
&& typeof timeoutId.unref === 'function') {
|
|
342
|
+
timeoutId.unref();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isSelfUpdateFailureCode(err, failureCode) {
|
|
346
|
+
return err instanceof SelfUpdateFailureError && err.failureCode === failureCode;
|
|
347
|
+
}
|
|
348
|
+
function assertGithubUpdateChannelAvailable(directive) {
|
|
349
|
+
if (directive.update_channels && !directive.update_channels.includes('github')) {
|
|
350
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.ALL_CHANNELS_EXHAUSTED, 'github_update_channel_unavailable');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function shaForAsset(sums, assetName) {
|
|
354
|
+
for (const line of sums.split(/\r?\n/)) {
|
|
355
|
+
const match = /^([0-9a-fA-F]{64})\s+\*?(.+)$/.exec(line.trim());
|
|
356
|
+
if (match && basename(match[2] ?? '') === assetName)
|
|
357
|
+
return (match[1] ?? '').toLowerCase();
|
|
358
|
+
}
|
|
359
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `release_sha_missing:${assetName}`);
|
|
360
|
+
}
|
|
361
|
+
function assertSafeExecutableTarget(targetPath, explicitTarget) {
|
|
362
|
+
const name = basename(targetPath).toLowerCase();
|
|
363
|
+
if ((name === 'node' || name === 'node.exe' || !name.startsWith('evolver')) && !explicitTarget) {
|
|
364
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.INSTALL_GUARD_NAME_MISMATCH, `unsafe_self_update_target:${targetPath}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function errorDetail(err) {
|
|
368
|
+
if (err instanceof Error)
|
|
369
|
+
return err.message;
|
|
370
|
+
return String(err);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Extract the binary `assetName` (matched by basename) out of a gzipped tar.
|
|
374
|
+
* Inline parser — V2 has no `tar` dep and inheriting V1 #282's approach keeps
|
|
375
|
+
* the install surface single-binary, no native deps. The parser is intentionally
|
|
376
|
+
* minimal: it reads ustar/POSIX headers, joins prefix+name, and only accepts
|
|
377
|
+
* regular file entries (type '0' / '\0'). Anything weird (symlinks, hard links,
|
|
378
|
+
* device nodes, longlink GNU extensions for the binary name we care about) is
|
|
379
|
+
* skipped — a release that hides the binary behind a symlink is treated the
|
|
380
|
+
* same as one that omitted it, i.e. fallback_missing_binary, fail-closed.
|
|
381
|
+
*
|
|
382
|
+
* Path safety (basename-match is the safety mechanism, not a bug):
|
|
383
|
+
* We match entries by `basename(name)`, NOT by the full archived path. That
|
|
384
|
+
* means `./evolver-linux-x64`, `nested/dir/evolver-linux-x64`, AND a hostile
|
|
385
|
+
* `../../../etc/evolver-linux-x64` all resolve to the same matched entry —
|
|
386
|
+
* this is intentional. Legitimate goreleaser/release-it tarballs vary in how
|
|
387
|
+
* they nest the binary, so allowing any path with the right basename keeps
|
|
388
|
+
* the fallback robust. The hostile `../` case is harmless HERE because we
|
|
389
|
+
* NEVER use the archive-supplied `name` as a write target: we return the
|
|
390
|
+
* bytes in memory and `stageStagedBinary` writes them to its OWN tmpdir
|
|
391
|
+
* under `assetName` (a constant from `releaseAssetName()`), not under any
|
|
392
|
+
* archive-controlled path. Then the executor's verifyManifest gate hashes
|
|
393
|
+
* those bytes against the signed manifest before atomicReplace runs.
|
|
394
|
+
*
|
|
395
|
+
* The V1 "unsafe tar entry" class of bugs comes from extracting EVERY entry
|
|
396
|
+
* to disk using its declared path. We don't extract to disk and we only ever
|
|
397
|
+
* touch the one entry whose basename matches — so there is no extraction
|
|
398
|
+
* tempdir to traverse out of.
|
|
399
|
+
*/
|
|
400
|
+
async function extractBinaryFromTarball(archive, assetName, maxExtractedBytes) {
|
|
401
|
+
const raw = await gunzipWithExtractedLimit(archive, maxExtractedBytes);
|
|
402
|
+
const targetName = basename(assetName);
|
|
403
|
+
let offset = 0;
|
|
404
|
+
while (offset + 512 <= raw.length) {
|
|
405
|
+
const header = raw.subarray(offset, offset + 512);
|
|
406
|
+
offset += 512;
|
|
407
|
+
if (isZeroTarBlock(header))
|
|
408
|
+
break;
|
|
409
|
+
let name = readTarString(header, 0, 100);
|
|
410
|
+
const prefix = readTarString(header, 345, 155);
|
|
411
|
+
if (prefix)
|
|
412
|
+
name = `${prefix}/${name}`;
|
|
413
|
+
const size = readTarOctal(header, 124, 12);
|
|
414
|
+
const typeFlag = readTarString(header, 156, 1) || '0';
|
|
415
|
+
if (offset + size > raw.length) {
|
|
416
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_EXTRACT_FAILED, `truncated_tar_entry:${name}`);
|
|
417
|
+
}
|
|
418
|
+
const isRegularFile = typeFlag === '0' || typeFlag === '\0';
|
|
419
|
+
if (isRegularFile && basename(name) === targetName) {
|
|
420
|
+
return Buffer.from(raw.subarray(offset, offset + size));
|
|
421
|
+
}
|
|
422
|
+
// Advance past the entry body, rounded up to a 512-byte boundary.
|
|
423
|
+
offset += Math.ceil(size / 512) * 512;
|
|
424
|
+
}
|
|
425
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_MISSING_BINARY, `fallback_missing_binary:${targetName}`);
|
|
426
|
+
}
|
|
427
|
+
function gunzipWithExtractedLimit(archive, maxExtractedBytes) {
|
|
428
|
+
return new Promise((resolve, reject) => {
|
|
429
|
+
const gunzip = createGunzip();
|
|
430
|
+
const chunks = [];
|
|
431
|
+
let totalBytes = 0;
|
|
432
|
+
let settled = false;
|
|
433
|
+
const fail = (err) => {
|
|
434
|
+
if (settled)
|
|
435
|
+
return;
|
|
436
|
+
settled = true;
|
|
437
|
+
gunzip.destroy();
|
|
438
|
+
reject(err);
|
|
439
|
+
};
|
|
440
|
+
gunzip.on('data', (chunk) => {
|
|
441
|
+
if (settled)
|
|
442
|
+
return;
|
|
443
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
444
|
+
totalBytes += bytes.byteLength;
|
|
445
|
+
if (totalBytes > maxExtractedBytes) {
|
|
446
|
+
fail(selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_EXTRACT_FAILED, `tarball_extracted_too_large:${totalBytes} > ${maxExtractedBytes}`));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
chunks.push(bytes);
|
|
450
|
+
});
|
|
451
|
+
gunzip.once('error', (err) => {
|
|
452
|
+
fail(selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_EXTRACT_FAILED, `gunzip:${errorDetail(err)}`, { cause: err }));
|
|
453
|
+
});
|
|
454
|
+
gunzip.once('end', () => {
|
|
455
|
+
if (settled)
|
|
456
|
+
return;
|
|
457
|
+
settled = true;
|
|
458
|
+
resolve(Buffer.concat(chunks, totalBytes));
|
|
459
|
+
});
|
|
460
|
+
try {
|
|
461
|
+
gunzip.end(archive);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
fail(selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_EXTRACT_FAILED, `gunzip:${errorDetail(err)}`, { cause: err }));
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function resolvedExtractedTarballLimit(requested) {
|
|
469
|
+
if (requested === undefined)
|
|
470
|
+
return MAX_EXTRACTED_TARBALL_BYTES;
|
|
471
|
+
if (!Number.isFinite(requested) || requested <= 0)
|
|
472
|
+
return MAX_EXTRACTED_TARBALL_BYTES;
|
|
473
|
+
return Math.min(Math.floor(requested), MAX_EXTRACTED_TARBALL_BYTES);
|
|
474
|
+
}
|
|
475
|
+
function readTarString(block, start, length) {
|
|
476
|
+
let end = start;
|
|
477
|
+
const max = start + length;
|
|
478
|
+
while (end < max && block[end] !== 0)
|
|
479
|
+
end++;
|
|
480
|
+
return block.toString('utf8', start, end);
|
|
481
|
+
}
|
|
482
|
+
function readTarOctal(block, start, length) {
|
|
483
|
+
const trimmed = readTarString(block, start, length).trim();
|
|
484
|
+
if (!trimmed)
|
|
485
|
+
return 0;
|
|
486
|
+
const parsed = parseInt(trimmed, 8);
|
|
487
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
488
|
+
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.FALLBACK_EXTRACT_FAILED, `invalid_tar_numeric_field:${trimmed}`);
|
|
489
|
+
}
|
|
490
|
+
return parsed;
|
|
491
|
+
}
|
|
492
|
+
function isZeroTarBlock(block) {
|
|
493
|
+
for (let i = 0; i < block.length; i++) {
|
|
494
|
+
if (block[i] !== 0)
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Current-version reader (ported from v1 forceUpdate.js getCurrentVersion). Reads package metadata so the daemon can:
|
|
2
|
+
// (a) report it on heartbeat (the hub sees each node's version), and (b) feed it to decideUpdate as `current`.
|
|
3
|
+
// Private workspace package.json files intentionally keep version 0.0.0; in that case we append git metadata so
|
|
4
|
+
// fleet inventory can still distinguish deployed builds. Best-effort fallback remains 0.0.0 and never throws.
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { util } from '@evomap/evolver-core';
|
|
8
|
+
/** Resolve EVOLVER's own version. Walks up from this module to the nearest package.json named @evomap/evolver-proxy. */
|
|
9
|
+
export function getCurrentVersion() {
|
|
10
|
+
return util.resolveRuntimeVersion({
|
|
11
|
+
startDir: dirname(fileURLToPath(import.meta.url)),
|
|
12
|
+
isPackage: (pkg) => pkg.name === '@evomap/evolver-proxy' || pkg.name === '@evomap/evolver',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { mailbox, hub as hubNs } from '@evomap/evolver-core';
|
|
2
|
+
type MailboxStore = mailbox.MailboxStore;
|
|
3
|
+
type Envelope = mailbox.Envelope;
|
|
4
|
+
type HubCapability = hubNs.HubCapability;
|
|
5
|
+
/** 双线程轮询节奏(沿用 v1 常量). */
|
|
6
|
+
export declare const SYNC_INTERVALS: {
|
|
7
|
+
readonly outboundIdle: 5000;
|
|
8
|
+
readonly outboundPending: 1000;
|
|
9
|
+
readonly inboundActive: 10000;
|
|
10
|
+
readonly inboundIdle: 60000;
|
|
11
|
+
};
|
|
12
|
+
export declare const MAX_BATCH = 50;
|
|
13
|
+
export declare const IDLE_THRESHOLD_MS: number;
|
|
14
|
+
export interface SyncEngineDeps {
|
|
15
|
+
store: MailboxStore;
|
|
16
|
+
hub: HubCapability;
|
|
17
|
+
/** = makeHubBindings(hub).asProxyHandler(); outbound 推送复用 Dispatcher 同一 handler. */
|
|
18
|
+
proxyHandler: (e: Envelope) => Promise<unknown>;
|
|
19
|
+
now: () => number;
|
|
20
|
+
runtimeNamespace?: string;
|
|
21
|
+
leaseMs?: number;
|
|
22
|
+
onOutboundFlushed?: (result: OutboundResult) => void | Promise<void>;
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
}
|
|
25
|
+
export interface OutboundResult {
|
|
26
|
+
sent: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
terminal: number;
|
|
29
|
+
deferred: number;
|
|
30
|
+
authFailed?: boolean;
|
|
31
|
+
authErrorMessage?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface InboundResult {
|
|
34
|
+
received: number;
|
|
35
|
+
enqueued: number;
|
|
36
|
+
nextPollAfterMs?: number;
|
|
37
|
+
hasMore: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* SyncEngine(M6-2): proxy↔hub 双向同步. 移植 v1 sync/{engine,outbound,inbound} 到 TS,
|
|
41
|
+
* hub I/O 全走 HubCapability.mailbox(非裸 hubFetch). tick 方法纯逻辑(注入 now), 可对 FakeHubCapability 确定性测;
|
|
42
|
+
* 定时器循环(start/stop)是薄包装, 由 M6-4 daemon 装配驱动.
|
|
43
|
+
*/
|
|
44
|
+
export declare class SyncEngine {
|
|
45
|
+
private readonly deps;
|
|
46
|
+
private lastActivityAt;
|
|
47
|
+
constructor(deps: SyncEngineDeps);
|
|
48
|
+
/** 出站: claim proxy 消息 → 经 proxyHandler 推 hub → complete; 终态(publish reject)直进 DLQ 不重试(money-safety). */
|
|
49
|
+
syncOutbound(limit?: number): Promise<OutboundResult>;
|
|
50
|
+
/** 入站: poll hub → 去重 enqueue 到 MailboxStore → ack; 游标存 kv. 遵守 #1195 nextPollAfterMs. */
|
|
51
|
+
syncInbound(): Promise<InboundResult>;
|
|
52
|
+
/** #1195 背压: hub 给 nextPollAfterMs 优先(有 backlog→drain 立即, 否则遵守); 缺省回退 idle/active. */
|
|
53
|
+
nextInboundDelay(last: InboundResult): number;
|
|
54
|
+
nextOutboundDelay(): number;
|
|
55
|
+
private isIdle;
|
|
56
|
+
private markOk;
|
|
57
|
+
private markError;
|
|
58
|
+
private notifyOutboundFlushed;
|
|
59
|
+
private normalizeOutbound;
|
|
60
|
+
private outboundDedupKey;
|
|
61
|
+
private safeAck;
|
|
62
|
+
/** AgentEvent → core Envelope; 未知类型(不在目录)跳过. */
|
|
63
|
+
private toEnvelope;
|
|
64
|
+
}
|
|
65
|
+
export {};
|