@evomap/evolver-proxy 2.0.0-beta.1 → 2.0.0-beta.4

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.
Files changed (37) hide show
  1. package/dist/bin/evolver-proxy.d.ts +67 -6
  2. package/dist/bin/evolver-proxy.js +389 -75
  3. package/dist/bin/proxySettings.d.ts +2 -0
  4. package/dist/bin/proxySettings.js +8 -1
  5. package/dist/daemon/collaborationFacade.d.ts +56 -0
  6. package/dist/daemon/collaborationFacade.js +877 -0
  7. package/dist/daemon/proxyDaemon.d.ts +3 -0
  8. package/dist/daemon/proxyDaemon.js +111 -0
  9. package/dist/daemon/selectHub.js +17 -1
  10. package/dist/llm/traceControl.js +1 -1
  11. package/dist/private/adapterLoader.d.ts +6 -1
  12. package/dist/private/adapterLoader.js +14 -3
  13. package/dist/router/messagesRoute.d.ts +13 -0
  14. package/dist/router/messagesRoute.js +56 -0
  15. package/dist/selfUpdate/executor.d.ts +10 -5
  16. package/dist/selfUpdate/executor.js +81 -6
  17. package/dist/selfUpdate/failureCodes.d.ts +6 -0
  18. package/dist/selfUpdate/failureCodes.js +6 -0
  19. package/dist/selfUpdate/index.d.ts +4 -1
  20. package/dist/selfUpdate/index.js +4 -1
  21. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  22. package/dist/selfUpdate/lastUpdate.js +37 -6
  23. package/dist/selfUpdate/releaseBinary.d.ts +10 -0
  24. package/dist/selfUpdate/releaseBinary.js +43 -6
  25. package/dist/selfUpdate/transaction.d.ts +109 -0
  26. package/dist/selfUpdate/transaction.js +1174 -0
  27. package/dist/selfUpdate/unixController.d.ts +15 -0
  28. package/dist/selfUpdate/unixController.js +186 -0
  29. package/dist/selfUpdate/version.d.ts +6 -2
  30. package/dist/selfUpdate/version.js +5 -3
  31. package/dist/selfUpdate/windowsController.d.ts +23 -0
  32. package/dist/selfUpdate/windowsController.js +274 -0
  33. package/dist/selfUpdate/windowsUpdater.d.ts +79 -0
  34. package/dist/selfUpdate/windowsUpdater.js +715 -0
  35. package/dist/sync/engine.d.ts +6 -5
  36. package/dist/sync/engine.js +102 -58
  37. package/package.json +8 -3
@@ -86,6 +86,37 @@ export function reportPendingSelfUpdateLastUpdate(store, directive, opts = { fro
86
86
  ...(directive.directive_id ? { directive_id: String(directive.directive_id) } : {}),
87
87
  }, now);
88
88
  }
89
+ export function finalizeSelfUpdateRecoveryLastUpdate(store, recovery, now = Date.now()) {
90
+ if (recovery.outcome !== 'confirmed'
91
+ && recovery.outcome !== 'rolled_back'
92
+ && recovery.outcome !== 'blocked')
93
+ return false;
94
+ const toVersion = concreteVersion(recovery.targetVersion);
95
+ const fromVersion = concreteVersion(recovery.fromVersion);
96
+ if (!toVersion)
97
+ return false;
98
+ const current = readPendingLastUpdate(store, now);
99
+ if (current && current.to_version !== toVersion)
100
+ return false;
101
+ const common = {
102
+ to_version: toVersion,
103
+ finished_at: Math.max(now, FINISHED_AT_MIN_MS),
104
+ ...(current?.directive_id ? { directive_id: current.directive_id } : {}),
105
+ ...(current?.from_version ? { from_version: current.from_version } : fromVersion ? { from_version: fromVersion } : {}),
106
+ };
107
+ if (recovery.outcome === 'confirmed') {
108
+ return writeLastUpdate(store, {
109
+ ...common,
110
+ status: 'success',
111
+ ...(current?.applied_via ? { applied_via: current.applied_via } : {}),
112
+ }, now);
113
+ }
114
+ return writeLastUpdate(store, {
115
+ ...common,
116
+ status: 'failed',
117
+ error: clampString(hubNs.redactString(`${recovery.failureCode ?? 'self_update_recovery_failed'}: ${recovery.outcome}`), LAST_UPDATE_ERROR_MAX),
118
+ }, now);
119
+ }
89
120
  export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
90
121
  if (result.outcome === 'already_in_progress' || result.outcome === 'disabled')
91
122
  return undefined;
@@ -94,11 +125,11 @@ export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
94
125
  return undefined;
95
126
  const base = {
96
127
  to_version: toVersion,
97
- status: statusForOutcome(result.outcome),
128
+ status: statusForResult(result),
98
129
  finished_at: Math.max(opts.now, FINISHED_AT_MIN_MS),
99
130
  ...(directive.directive_id ? { directive_id: String(directive.directive_id) } : {}),
100
131
  };
101
- if (base.status === 'success') {
132
+ if (base.status === 'success' || base.status === 'pending') {
102
133
  return {
103
134
  ...base,
104
135
  from_version: clampString(opts.fromVersion, LAST_UPDATE_FROM_VERSION_MAX),
@@ -114,10 +145,10 @@ export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
114
145
  }
115
146
  return base;
116
147
  }
117
- function statusForOutcome(outcome) {
118
- if (outcome === 'applied')
119
- return 'success';
120
- if (outcome === 'noop')
148
+ function statusForResult(result) {
149
+ if (result.outcome === 'applied')
150
+ return result.confirmationPending ? 'pending' : 'success';
151
+ if (result.outcome === 'noop')
121
152
  return 'skipped';
122
153
  return 'failed';
123
154
  }
@@ -11,8 +11,18 @@ export interface ReleaseBinaryOptions {
11
11
  targetPath?: string;
12
12
  processExecPath?: string;
13
13
  requireSignedManifest?: boolean;
14
+ maxPrimaryBinaryBytes?: number;
14
15
  maxExtractedTarballBytes?: number;
15
16
  }
17
+ /**
18
+ * Hard ceiling for primary release binaries. The binary is buffered before its
19
+ * manifest hash is verified, so an unbounded response could exhaust memory
20
+ * before the verification gate runs. 128MiB leaves headroom above current
21
+ * single-platform binaries while bounding that pre-verification allocation.
22
+ */
23
+ export declare const MAX_PRIMARY_BINARY_BYTES: number;
24
+ /** Release metadata is untrusted and buffered before parsing or verification. */
25
+ export declare const MAX_RELEASE_METADATA_BYTES: number;
16
26
  /**
17
27
  * Hard ceiling for Channel 1b tarball downloads. A compromised/corrupt release
18
28
  * could advertise a multi-GB tar.gz and OOM us because tarballBytes is buffered
@@ -8,6 +8,15 @@ import { SELF_UPDATE_FAILURE_CODES, SelfUpdateFailureError, selfUpdateFailure }
8
8
  const DEFAULT_RELEASES_URL = 'https://github.com/EvoMap/evolver/releases';
9
9
  const SIGNED_MANIFEST_ASSET = 'evolver-update-manifest.json';
10
10
  const RELEASE_DOWNLOAD_TIMEOUT_MS = 60_000;
11
+ /**
12
+ * Hard ceiling for primary release binaries. The binary is buffered before its
13
+ * manifest hash is verified, so an unbounded response could exhaust memory
14
+ * before the verification gate runs. 128MiB leaves headroom above current
15
+ * single-platform binaries while bounding that pre-verification allocation.
16
+ */
17
+ export const MAX_PRIMARY_BINARY_BYTES = 128 * 1024 * 1024;
18
+ /** Release metadata is untrusted and buffered before parsing or verification. */
19
+ export const MAX_RELEASE_METADATA_BYTES = 256 * 1024;
11
20
  /**
12
21
  * Hard ceiling for Channel 1b tarball downloads. A compromised/corrupt release
13
22
  * could advertise a multi-GB tar.gz and OOM us because tarballBytes is buffered
@@ -75,7 +84,7 @@ export async function downloadGithubReleaseArtifact(targetVersion, directive, op
75
84
  }
76
85
  async function downloadBinaryAsset(version, assetName, directive, opts) {
77
86
  const assetUrl = releaseDownloadUrl(directive.release_url, version, assetName);
78
- const bytes = Buffer.from(await fetchBytes(assetUrl, opts.fetchFn));
87
+ const bytes = Buffer.from(await fetchBytes(assetUrl, opts.fetchFn, resolvedPrimaryBinaryLimit(opts.maxPrimaryBinaryBytes)));
79
88
  if (bytes.byteLength === 0) {
80
89
  throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `empty release asset:${assetName}`);
81
90
  }
@@ -192,7 +201,7 @@ export async function atomicReplaceExecutable(stagedPath, opts = {}) {
192
201
  }
193
202
  }
194
203
  export function resolveSelfUpdateTarget(opts = {}) {
195
- const explicitTarget = opts.targetPath ?? opts.env?.['EVOLVER_SELF_UPDATE_TARGET_PATH'];
204
+ const explicitTarget = opts.targetPath ?? opts.env?.['EVOLVER_SELF_UPDATE_TARGET_PATH']?.trim();
196
205
  if (explicitTarget)
197
206
  return { path: explicitTarget, explicit: true };
198
207
  const execPath = opts.processExecPath ?? process.execPath;
@@ -207,7 +216,10 @@ function releaseDownloadUrl(releaseUrl, version, assetName) {
207
216
  base = new URL(releaseUrl && releaseUrl.trim() ? releaseUrl : DEFAULT_RELEASES_URL);
208
217
  }
209
218
  catch (err) {
210
- throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, errorDetail(err), { cause: err });
219
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, 'invalid_release_url', { cause: err });
220
+ }
221
+ if (base.username || base.password) {
222
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, 'invalid_release_url_credentials');
211
223
  }
212
224
  if (base.protocol !== 'https:' || base.hostname !== 'github.com') {
213
225
  throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED, 'invalid_release_url_origin');
@@ -242,8 +254,8 @@ async function fetchSignedReleaseManifest(releaseUrl, version, assetName, fetchF
242
254
  if (!manifestVersion) {
243
255
  throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, 'signed_manifest_invalid_version');
244
256
  }
245
- if (!ops.currentSatisfiesRequiredVersion(manifestVersion, version)) {
246
- throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOADED_VERSION_MISMATCH, 'signed_manifest_below_required');
257
+ if (manifestVersion !== version) {
258
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOADED_VERSION_MISMATCH, 'signed_manifest_version_mismatch');
247
259
  }
248
260
  const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
249
261
  if (!artifacts.some((artifact) => artifact && typeof artifact.path === 'string' && basename(artifact.path) === assetName)) {
@@ -253,7 +265,8 @@ async function fetchSignedReleaseManifest(releaseUrl, version, assetName, fetchF
253
265
  }
254
266
  async function fetchText(url, fetchFn) {
255
267
  const fetched = await fetchWith(url, fetchFn);
256
- return readBodyWithTimeout(url, 'text', () => fetched.response.text(), fetched.abort);
268
+ const bytes = await readBodyWithTimeout(url, 'text', () => readBytesWithLimit(fetched.response, MAX_RELEASE_METADATA_BYTES, fetched.abort), fetched.abort);
269
+ return Buffer.from(bytes).toString('utf8');
257
270
  }
258
271
  async function fetchBytes(url, fetchFn, maxBytes) {
259
272
  const fetched = await fetchWith(url, fetchFn);
@@ -267,6 +280,12 @@ async function readBytesWithLimit(response, maxBytes, abort) {
267
280
  }
268
281
  return arr;
269
282
  }
283
+ const declaredBytes = parseContentLength(response.headers.get('content-length'));
284
+ if (declaredBytes !== undefined && declaredBytes > maxBytes) {
285
+ abort();
286
+ await response.body.cancel().catch(() => { });
287
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE, `release_body_too_large:${declaredBytes} > ${maxBytes}`);
288
+ }
270
289
  const reader = response.body.getReader();
271
290
  const chunks = [];
272
291
  let totalBytes = 0;
@@ -297,6 +316,15 @@ async function readBytesWithLimit(response, maxBytes, abort) {
297
316
  }
298
317
  return out.buffer;
299
318
  }
319
+ function parseContentLength(raw) {
320
+ const trimmed = raw?.trim();
321
+ if (!trimmed || !/^\d+$/.test(trimmed))
322
+ return undefined;
323
+ const parsed = Number(trimmed);
324
+ if (!Number.isSafeInteger(parsed))
325
+ return Number.POSITIVE_INFINITY;
326
+ return parsed;
327
+ }
300
328
  async function fetchWith(url, fetchFn) {
301
329
  const fn = fetchFn ?? globalThis.fetch;
302
330
  if (!fn)
@@ -472,6 +500,15 @@ function resolvedExtractedTarballLimit(requested) {
472
500
  return MAX_EXTRACTED_TARBALL_BYTES;
473
501
  return Math.min(Math.floor(requested), MAX_EXTRACTED_TARBALL_BYTES);
474
502
  }
503
+ function resolvedPrimaryBinaryLimit(requested) {
504
+ if (requested === undefined)
505
+ return MAX_PRIMARY_BINARY_BYTES;
506
+ if (!Number.isFinite(requested) || requested <= 0)
507
+ return MAX_PRIMARY_BINARY_BYTES;
508
+ // Tests may lower the cap without providing a production escape hatch that
509
+ // could raise or disable the hard safety boundary.
510
+ return Math.min(Math.floor(requested), MAX_PRIMARY_BINARY_BYTES);
511
+ }
475
512
  function readTarString(block, start, length) {
476
513
  let end = start;
477
514
  const max = start + length;
@@ -0,0 +1,109 @@
1
+ import type { DownloadResult } from './executor.js';
2
+ import { type ReleaseBinaryOptions } from './releaseBinary.js';
3
+ export type SelfUpdateJournalStage = 'preparing' | 'downloaded' | 'verified' | 'backed_up' | 'install_pending' | 'installed' | 'restarted' | 'health_check_pending' | 'rolling_back' | 'rollback_pending' | 'confirmed' | 'rolled_back' | 'rollback_failed';
4
+ export interface SelfUpdateJournal {
5
+ schema_version: 2;
6
+ transaction_id: string;
7
+ stage: SelfUpdateJournalStage;
8
+ from_version: string;
9
+ target_version: string;
10
+ platform: NodeJS.Platform;
11
+ arch: NodeJS.Architecture;
12
+ installing_pid: number;
13
+ created_at: string;
14
+ updated_at: string;
15
+ recovery_attempts: number;
16
+ /** Canonical logical install path: real parent directory plus the target leaf name. */
17
+ target_path: string;
18
+ /** Normalized operator-configured spelling used only when its parent can no longer be resolved. */
19
+ configured_target_path?: string;
20
+ staged_name?: string;
21
+ backup_name?: string;
22
+ failure_code?: string;
23
+ verified_sha256?: string;
24
+ }
25
+ export interface DurableSelfUpdateSession {
26
+ adoptDownloaded(download: DownloadResult): Promise<DownloadResult>;
27
+ markVerified(artifacts: readonly {
28
+ bytes?: Uint8Array;
29
+ sha256?: string;
30
+ }[]): Promise<void>;
31
+ install(): Promise<void>;
32
+ markRestartRequested(): Promise<void>;
33
+ abort(failureCode: string): Promise<void>;
34
+ rollback(failureCode: string): Promise<void>;
35
+ release(): Promise<void>;
36
+ }
37
+ export interface SelfUpdateRecoveryResult {
38
+ outcome: 'none' | 'pending_health' | 'rollback_pending' | 'confirmed' | 'rolled_back' | 'blocked';
39
+ stage?: SelfUpdateJournalStage;
40
+ targetVersion?: string;
41
+ fromVersion?: string;
42
+ restartRequired?: boolean;
43
+ failureCode?: string;
44
+ }
45
+ export interface StagedBinaryProbeOptions {
46
+ cwd: string;
47
+ env: NodeJS.ProcessEnv;
48
+ timeout: number;
49
+ windowsHide: boolean;
50
+ maxBuffer: number;
51
+ }
52
+ export type StagedBinaryProbe = (targetPath: string, args: readonly string[], options: StagedBinaryProbeOptions) => Promise<{
53
+ stdout: string;
54
+ }>;
55
+ export interface DurableSelfUpdateOptions extends ReleaseBinaryOptions {
56
+ stateDir?: string;
57
+ currentVersion: string;
58
+ platform?: NodeJS.Platform;
59
+ arch?: NodeJS.Architecture;
60
+ pid?: number;
61
+ now?: () => Date;
62
+ readBackVersion?: (targetPath: string) => Promise<string>;
63
+ stagedBinaryProbe?: StagedBinaryProbe;
64
+ /** Test hook invoked after a stale lock generation is observed and before its successor is published. */
65
+ beforeStaleLockReclaim?: () => void | Promise<void>;
66
+ }
67
+ export type SelfUpdateRecoveryOptions = Omit<DurableSelfUpdateOptions, 'currentVersion'> & {
68
+ currentVersion?: string;
69
+ /** Runs after a durable journal is loaded and before recovery changes the journal, target, or managed artifacts. */
70
+ beforeJournalMutation?: () => void | Promise<void>;
71
+ };
72
+ export interface StableUnixRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
73
+ platform?: NodeJS.Platform;
74
+ }
75
+ export interface StableWindowsRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
76
+ platform?: NodeJS.Platform;
77
+ }
78
+ export declare function inspectDurableSelfUpdate(options: SelfUpdateRecoveryOptions): Promise<SelfUpdateRecoveryResult>;
79
+ export declare function resolveStableUnixRecoveryControllerPath(options: StableUnixRecoveryControllerOptions): Promise<string>;
80
+ export declare function stableUnixRecoveryControllerPathForTarget(targetPath: string, stateDir?: string): string;
81
+ /**
82
+ * Installs an executable copy outside the mutable target path. The transaction
83
+ * lock and the existing no-follow file primitives keep service installation
84
+ * from racing an update or copying through a symlink.
85
+ */
86
+ export declare function provisionStableUnixRecoveryController(options: StableUnixRecoveryControllerOptions): Promise<string>;
87
+ export declare function bindStableUnixRecoveryController(options: StableUnixRecoveryControllerOptions, processExecPath: string): Promise<{
88
+ controllerPath: string;
89
+ targetPath: string;
90
+ }>;
91
+ export declare function bindStableWindowsRecoveryController(options: StableWindowsRecoveryControllerOptions, processExecPath: string): Promise<{
92
+ controllerPath: string;
93
+ stateDir: string;
94
+ targetPath: string;
95
+ }>;
96
+ export declare function stableWindowsRecoveryControllerPathForStateDir(stateDir: string): string;
97
+ /**
98
+ * Provision or refresh the long-lived controller while it is not running.
99
+ * Service installation stops the Scheduled Task before calling this command;
100
+ * each self-update only replaces the separate windows-updater worker path.
101
+ */
102
+ export declare function provisionStableWindowsRecoveryController(options: StableWindowsRecoveryControllerOptions, processExecPath: string): Promise<string>;
103
+ export declare function beginDurableSelfUpdate(targetVersion: string, options: DurableSelfUpdateOptions): Promise<DurableSelfUpdateSession>;
104
+ export declare function recoverDurableSelfUpdate(options: SelfUpdateRecoveryOptions): Promise<SelfUpdateRecoveryResult>;
105
+ export declare function markWindowsInstallApplied(options: SelfUpdateRecoveryOptions): Promise<SelfUpdateRecoveryResult>;
106
+ export declare function confirmDurableSelfUpdate(options: SelfUpdateRecoveryOptions): Promise<SelfUpdateRecoveryResult>;
107
+ export declare function rollbackDurableSelfUpdate(options: SelfUpdateRecoveryOptions, failureCode: string): Promise<SelfUpdateRecoveryResult>;
108
+ export declare function normalizeCanonicalSelfUpdateTargetPath(targetPath: string, platform?: NodeJS.Platform): string;
109
+ export declare function preflightManagedStagedBinary(targetPath: string, expectedVersion: string, probe?: StagedBinaryProbe): Promise<void>;