@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.
Files changed (82) hide show
  1. package/README.md +49 -0
  2. package/dist/bin/envFile.d.ts +10 -0
  3. package/dist/bin/envFile.js +68 -0
  4. package/dist/bin/evolver-llm-proxy.d.ts +2 -0
  5. package/dist/bin/evolver-llm-proxy.js +111 -0
  6. package/dist/bin/evolver-proxy.d.ts +83 -0
  7. package/dist/bin/evolver-proxy.js +511 -0
  8. package/dist/bin/proxySettings.d.ts +15 -0
  9. package/dist/bin/proxySettings.js +84 -0
  10. package/dist/bin/proxyStorePath.d.ts +1 -0
  11. package/dist/bin/proxyStorePath.js +16 -0
  12. package/dist/daemon/atpConsent.d.ts +13 -0
  13. package/dist/daemon/atpConsent.js +60 -0
  14. package/dist/daemon/ipcConfig.d.ts +1 -0
  15. package/dist/daemon/ipcConfig.js +13 -0
  16. package/dist/daemon/proxyDaemon.d.ts +191 -0
  17. package/dist/daemon/proxyDaemon.js +1015 -0
  18. package/dist/daemon/selectHub.d.ts +16 -0
  19. package/dist/daemon/selectHub.js +30 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.js +8 -0
  22. package/dist/lifecycle/deployGuard.d.ts +46 -0
  23. package/dist/lifecycle/deployGuard.js +53 -0
  24. package/dist/lifecycle/legacyNodeId.d.ts +96 -0
  25. package/dist/lifecycle/legacyNodeId.js +163 -0
  26. package/dist/lifecycle/manager.d.ts +106 -0
  27. package/dist/lifecycle/manager.js +390 -0
  28. package/dist/llm/bodyCapture.d.ts +31 -0
  29. package/dist/llm/bodyCapture.js +293 -0
  30. package/dist/llm/index.d.ts +3 -0
  31. package/dist/llm/index.js +3 -0
  32. package/dist/llm/server.d.ts +35 -0
  33. package/dist/llm/server.js +359 -0
  34. package/dist/llm/traceBackfill.d.ts +53 -0
  35. package/dist/llm/traceBackfill.js +525 -0
  36. package/dist/llm/traceConfig.d.ts +7 -0
  37. package/dist/llm/traceConfig.js +44 -0
  38. package/dist/llm/traceControl.d.ts +16 -0
  39. package/dist/llm/traceControl.js +85 -0
  40. package/dist/llm/traceEnvelope.d.ts +75 -0
  41. package/dist/llm/traceEnvelope.js +286 -0
  42. package/dist/llm/traceSink.d.ts +61 -0
  43. package/dist/llm/traceSink.js +278 -0
  44. package/dist/llm/traceUploadPayload.d.ts +14 -0
  45. package/dist/llm/traceUploadPayload.js +27 -0
  46. package/dist/llm/upstream.d.ts +68 -0
  47. package/dist/llm/upstream.js +491 -0
  48. package/dist/private/adapterLoader.d.ts +46 -0
  49. package/dist/private/adapterLoader.js +62 -0
  50. package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
  51. package/dist/private/privateRuntimeSmokeOptions.js +132 -0
  52. package/dist/router/cachePassthrough.d.ts +3 -0
  53. package/dist/router/cachePassthrough.js +13 -0
  54. package/dist/router/features.d.ts +9 -0
  55. package/dist/router/features.js +52 -0
  56. package/dist/router/index.d.ts +6 -0
  57. package/dist/router/index.js +6 -0
  58. package/dist/router/messagesRoute.d.ts +138 -0
  59. package/dist/router/messagesRoute.js +753 -0
  60. package/dist/router/modelRouter.d.ts +49 -0
  61. package/dist/router/modelRouter.js +66 -0
  62. package/dist/router/providerRoutes.d.ts +42 -0
  63. package/dist/router/providerRoutes.js +1579 -0
  64. package/dist/router/sseScan.d.ts +56 -0
  65. package/dist/router/sseScan.js +543 -0
  66. package/dist/selfUpdate/executor.d.ts +90 -0
  67. package/dist/selfUpdate/executor.js +179 -0
  68. package/dist/selfUpdate/failureCodes.d.ts +33 -0
  69. package/dist/selfUpdate/failureCodes.js +84 -0
  70. package/dist/selfUpdate/index.d.ts +5 -0
  71. package/dist/selfUpdate/index.js +5 -0
  72. package/dist/selfUpdate/lastUpdate.d.ts +43 -0
  73. package/dist/selfUpdate/lastUpdate.js +195 -0
  74. package/dist/selfUpdate/policy.d.ts +3 -0
  75. package/dist/selfUpdate/policy.js +9 -0
  76. package/dist/selfUpdate/releaseBinary.d.ts +56 -0
  77. package/dist/selfUpdate/releaseBinary.js +498 -0
  78. package/dist/selfUpdate/version.d.ts +2 -0
  79. package/dist/selfUpdate/version.js +14 -0
  80. package/dist/sync/engine.d.ts +65 -0
  81. package/dist/sync/engine.js +461 -0
  82. package/package.json +41 -0
@@ -0,0 +1,90 @@
1
+ import { ops } from '@evomap/evolver-core';
2
+ import { type SelfUpdateFailureCode } from './failureCodes.js';
3
+ type DownloadedArtifact = ops.DownloadedArtifact;
4
+ /** Structured outcome codes — reported to telemetry; the hub sees WHY an update did/didn't apply. */
5
+ export type SelfUpdateOutcome = 'applied' | 'noop' | 'rejected_decision' | 'rejected_verification' | 'download_failed' | 'replace_failed' | 'already_in_progress' | 'disabled';
6
+ export interface SelfUpdateResult {
7
+ outcome: SelfUpdateOutcome;
8
+ reason: string;
9
+ /** Fine-grained stable failure taxonomy for telemetry aggregation. */
10
+ failureCode?: SelfUpdateFailureCode;
11
+ /** The version that was (or would have been) applied. */
12
+ targetVersion?: string;
13
+ /**
14
+ * Which download path produced the staged binary: `'binary'` is the normal
15
+ * precompiled-asset happy path, `'tarball'` means the binary download failed
16
+ * and Channel 1b (release `.tar.gz`) fallback was used. The verifyManifest
17
+ * gate ran in BOTH cases, so apply-semantics are identical, but "tarball
18
+ * used in production" is a useful CDN/rate-limit signal for the hub.
19
+ * Persisted into `last_update.json` (lastUpdate.LastUpdatePayload.applied_via)
20
+ * on success so the hub can observe the channel directly.
21
+ */
22
+ appliedVia?: 'binary' | 'tarball';
23
+ }
24
+ /** The hub's force_update directive (inbound message payload). */
25
+ export interface ForceUpdateDirective {
26
+ required_version?: string;
27
+ manifest?: unknown;
28
+ reason?: string;
29
+ release_url?: string;
30
+ update_channels?: readonly string[];
31
+ directive_id?: string;
32
+ deadline_ms?: number;
33
+ stagger_window_ms?: number;
34
+ }
35
+ /** Result of downloading the release for `targetVersion`: the staged path + the artifacts to verify. */
36
+ export interface DownloadResult {
37
+ /** Where the new version was staged (e.g. a tmp dir). Passed to atomicReplace on success. */
38
+ stagedPath: string;
39
+ /** The downloaded artifacts (bytes or precomputed sha256) for verifyManifest. */
40
+ artifacts: readonly DownloadedArtifact[];
41
+ /**
42
+ * Which channel actually produced the staged bytes — defaults to `'binary'`
43
+ * when the precompiled asset succeeded, `'tarball'` when Channel 1b fallback
44
+ * (release `.tar.gz`) had to extract the binary. The executor threads this
45
+ * onto the SelfUpdateResult as `appliedVia` for telemetry. Optional so
46
+ * legacy/test seams without a notion of channels stay valid.
47
+ */
48
+ appliedVia?: 'binary' | 'tarball';
49
+ }
50
+ /**
51
+ * Injected I/O seams. Defaults are NOT provided here — the proxy bin wires real implementations (degit/tarball
52
+ * download, fs atomic rename, process.exit(78)); tests pass fakes. Keeping them required makes "did we actually
53
+ * call restart?" trivially observable in tests and stops a real restart leaking into a unit run.
54
+ */
55
+ export interface SelfUpdateDeps {
56
+ /** Self-update policy. Only 'auto' applies; 'prompt' needs an approval path before this executor runs. */
57
+ policy: 'off' | 'prompt' | 'auto';
58
+ /** Current installed version (read from package.json by the caller). */
59
+ currentVersion: string;
60
+ /** Resolve a trusted manifest when the hub directive does not carry one. */
61
+ resolveManifest?: (directive: ForceUpdateDirective, currentVersion: string) => Promise<unknown> | unknown;
62
+ /** Download the staged release for the target version. Throws/rejects on failure. */
63
+ download: (targetVersion: string, directive: ForceUpdateDirective) => Promise<DownloadResult>;
64
+ /** Atomically replace the install tree with the staged path (preserving node_modules/.env/etc). Throws on fail. */
65
+ atomicReplace: (stagedPath: string) => Promise<void>;
66
+ /** Signal a restart so the supervisor relaunches the new version. v1 convention: process.exit(78). */
67
+ restart: () => void;
68
+ /** Optional Ed25519 public key (PEM / raw base64). When set, an unsigned/badly-signed manifest is REJECTED. */
69
+ publicKey?: string;
70
+ /** Best-effort telemetry sink for the structured outcome (never throws into the update path). */
71
+ onTelemetry?: (result: SelfUpdateResult) => void;
72
+ }
73
+ /** Test-only: reset the mutex between cases. Not part of the public update path. */
74
+ export declare function _resetSelfUpdateMutex(): void;
75
+ /**
76
+ * Execute a force_update directive end to end: decide → (mutex) → download → VERIFY → atomic replace → restart.
77
+ *
78
+ * Order is load-bearing:
79
+ * 1. policy off → do nothing (the default-off risk gate; a half-built channel must not auto-apply).
80
+ * 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
81
+ * 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
82
+ * 4. download the staged release.
83
+ * 5. verifyManifest (pure) — THE GATE. Fail → no write, no restart, `rejected_verification`.
84
+ * 6. atomicReplace, then restart(). Only reached after verification passed.
85
+ *
86
+ * Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
87
+ * on the old (intact) version.
88
+ */
89
+ export declare function executeForceUpdate(directive: ForceUpdateDirective, deps: SelfUpdateDeps): Promise<SelfUpdateResult>;
90
+ export {};
@@ -0,0 +1,179 @@
1
+ // Force-update EXECUTOR (the I/O side, ported from v1 src/forceUpdate.js executeForceUpdate). This is the only
2
+ // place in the codebase that downloads a release, replaces files on disk, and signals a restart. Every risky
3
+ // operation is an INJECTED seam (same IoC discipline as exec/selfPr) so tests drive it with fakes and it never
4
+ // shells out or touches a real install tree by accident.
5
+ //
6
+ // THE HARD GATE (verify-before-apply, non-negotiable): after download and BEFORE any filesystem write or restart,
7
+ // the executor calls the PURE core verifyManifest. If verification fails — bad sha256, missing/invalid signature
8
+ // when a key is configured, anything — the executor writes NOTHING, restarts NOTHING, and returns a structured
9
+ // failure. The old version stays intact and runnable. A compromised hub cannot turn this channel into fleet RCE.
10
+ //
11
+ // Concurrency: a process-level mutex guarantees that concurrent force_update messages execute the update EXACTLY
12
+ // once. The second caller short-circuits with `already_in_progress` and performs no I/O.
13
+ import { ops } from '@evomap/evolver-core';
14
+ import { SELF_UPDATE_FAILURE_CODES, classifySelfUpdateError, codeForDecisionReject, } from './failureCodes.js';
15
+ const { decideUpdate, verifyManifest } = ops;
16
+ // Process-level mutex. Module scope is correct: there is one daemon per process, and v1's _forceUpdateInFlight had
17
+ // the same lifetime. Guards against two force_update envelopes (or a heartbeat-driven + mailbox-driven trigger)
18
+ // racing the same upgrade and replacing files twice / double-restarting.
19
+ let inFlight = false;
20
+ /** Test-only: reset the mutex between cases. Not part of the public update path. */
21
+ export function _resetSelfUpdateMutex() {
22
+ inFlight = false;
23
+ }
24
+ function report(deps, result) {
25
+ try {
26
+ deps.onTelemetry?.(result);
27
+ }
28
+ catch {
29
+ /* telemetry must never break the update path */
30
+ }
31
+ return result;
32
+ }
33
+ /**
34
+ * Execute a force_update directive end to end: decide → (mutex) → download → VERIFY → atomic replace → restart.
35
+ *
36
+ * Order is load-bearing:
37
+ * 1. policy off → do nothing (the default-off risk gate; a half-built channel must not auto-apply).
38
+ * 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
39
+ * 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
40
+ * 4. download the staged release.
41
+ * 5. verifyManifest (pure) — THE GATE. Fail → no write, no restart, `rejected_verification`.
42
+ * 6. atomicReplace, then restart(). Only reached after verification passed.
43
+ *
44
+ * Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
45
+ * on the old (intact) version.
46
+ */
47
+ export async function executeForceUpdate(directive, deps) {
48
+ if (deps.policy !== 'auto') {
49
+ return report(deps, {
50
+ outcome: 'disabled',
51
+ reason: deps.policy === 'prompt' ? 'self_update_policy_prompt_requires_approval' : 'self_update_policy_off',
52
+ });
53
+ }
54
+ const requiredFloor = directive.required_version !== undefined
55
+ ? ops.normalizeRequiredVersion(directive.required_version)
56
+ : undefined;
57
+ if (directive.required_version !== undefined && !requiredFloor) {
58
+ return report(deps, {
59
+ outcome: 'rejected_decision',
60
+ reason: 'required_version_invalid',
61
+ failureCode: SELF_UPDATE_FAILURE_CODES.BAD_REQUIRED_VERSION,
62
+ });
63
+ }
64
+ if (requiredFloor && ops.currentSatisfiesRequiredVersion(deps.currentVersion, requiredFloor)) {
65
+ return report(deps, { outcome: 'noop', reason: 'already_satisfied', targetVersion: requiredFloor });
66
+ }
67
+ let manifest = directive.manifest;
68
+ if (deps.resolveManifest && shouldResolveManifest(directive, manifest, Boolean(deps.publicKey))) {
69
+ try {
70
+ manifest = await deps.resolveManifest(directive, deps.currentVersion);
71
+ }
72
+ catch (err) {
73
+ const targetVersion = ops.normalizeRequiredVersion(directive.required_version);
74
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
75
+ return report(deps, {
76
+ outcome: 'download_failed',
77
+ reason: `manifest_resolve_failed: ${classified.detail}`,
78
+ failureCode: classified.failureCode,
79
+ ...(targetVersion ? { targetVersion } : {}),
80
+ });
81
+ }
82
+ }
83
+ const effectiveDirective = { ...directive, manifest };
84
+ const decision = decideUpdate({
85
+ current: deps.currentVersion,
86
+ ...(requiredFloor ? { required: requiredFloor } : {}),
87
+ manifest,
88
+ });
89
+ if (decision.action === 'reject') {
90
+ return report(deps, {
91
+ outcome: 'rejected_decision',
92
+ reason: decision.reason,
93
+ failureCode: codeForDecisionReject(decision.reason),
94
+ ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
95
+ });
96
+ }
97
+ if (decision.action === 'noop') {
98
+ return report(deps, { outcome: 'noop', reason: decision.reason, ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}) });
99
+ }
100
+ // action === 'proceed'. Take the mutex; a concurrent force_update short-circuits here with NO I/O.
101
+ if (inFlight) {
102
+ return report(deps, { outcome: 'already_in_progress', reason: 'another_update_in_flight' });
103
+ }
104
+ inFlight = true;
105
+ const targetVersion = decision.targetVersion ?? manifest?.version ?? '';
106
+ try {
107
+ // 4. Download the staged release.
108
+ let dl;
109
+ try {
110
+ dl = await deps.download(targetVersion, effectiveDirective);
111
+ }
112
+ catch (err) {
113
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
114
+ return report(deps, {
115
+ outcome: 'download_failed',
116
+ reason: classified.detail,
117
+ failureCode: classified.failureCode,
118
+ targetVersion,
119
+ });
120
+ }
121
+ // 5. THE GATE: verify the downloaded bytes against the (optionally signed) manifest BEFORE any write.
122
+ const verification = verifyManifest(manifest, dl.artifacts, ...(deps.publicKey ? [deps.publicKey] : []));
123
+ if (!verification.ok) {
124
+ // Verification failed → write NOTHING, restart NOTHING. Old version stays intact and runnable.
125
+ return report(deps, {
126
+ outcome: 'rejected_verification',
127
+ reason: verification.reason,
128
+ failureCode: SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION,
129
+ targetVersion,
130
+ });
131
+ }
132
+ // 6. Verified. Atomic replace, then signal restart. A replace failure leaves the old version intact.
133
+ try {
134
+ await deps.atomicReplace(dl.stagedPath);
135
+ }
136
+ catch (err) {
137
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.COPY_FAILED);
138
+ return report(deps, {
139
+ outcome: 'replace_failed',
140
+ reason: classified.detail,
141
+ failureCode: classified.failureCode,
142
+ targetVersion,
143
+ });
144
+ }
145
+ const result = {
146
+ outcome: 'applied',
147
+ reason: 'verified_and_replaced',
148
+ targetVersion,
149
+ appliedVia: dl.appliedVia ?? 'binary',
150
+ };
151
+ report(deps, result);
152
+ deps.restart(); // v1 convention: exit(78) → supervisor relaunches the new version.
153
+ return result;
154
+ }
155
+ finally {
156
+ // Released so a later legitimate update (after a failed attempt) can proceed. On the success path the process
157
+ // is exiting anyway; releasing is harmless and keeps the mutex honest if restart() is a test fake that returns.
158
+ inFlight = false;
159
+ }
160
+ }
161
+ function shouldResolveManifest(directive, manifest, signatureRequired) {
162
+ if (manifest === undefined)
163
+ return true;
164
+ const hasGithubReleaseHint = typeof directive.release_url === 'string'
165
+ || directive.update_channels?.includes('github') === true;
166
+ if (!hasGithubReleaseHint)
167
+ return false;
168
+ if (signatureRequired)
169
+ return !hasSignedManifest(manifest);
170
+ return true;
171
+ }
172
+ function hasSignedManifest(manifest) {
173
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
174
+ return false;
175
+ const input = manifest;
176
+ return typeof input.signature === 'string'
177
+ && input.signature.length > 0
178
+ && input.signatureAlg === 'ed25519';
179
+ }
@@ -0,0 +1,33 @@
1
+ export declare const SELF_UPDATE_FAILURE_CODES: Readonly<{
2
+ readonly INSTALL_GUARD_NAME_MISMATCH: "install_guard_name_mismatch";
3
+ readonly INSTALL_GUARD_UNREADABLE: "install_guard_unreadable";
4
+ readonly BAD_REQUIRED_VERSION: "bad_required_version";
5
+ readonly CURRENT_VERSION_UNPARSABLE: "current_version_unparsable";
6
+ readonly NPX_NOT_FOUND: "npx_not_found";
7
+ readonly DEGIT_TIMEOUT: "degit_timeout";
8
+ readonly DEGIT_FAILED: "degit_failed";
9
+ readonly DOWNLOAD_INCOMPLETE: "download_incomplete";
10
+ readonly DOWNLOADED_VERSION_MISMATCH: "downloaded_version_mismatch";
11
+ readonly COPY_FAILED: "copy_failed";
12
+ readonly ALL_CHANNELS_EXHAUSTED: "all_channels_exhausted";
13
+ readonly DOWNLOAD_FAILED: "download_failed";
14
+ readonly REJECTED_DECISION: "rejected_decision";
15
+ readonly REJECTED_VERIFICATION: "rejected_verification";
16
+ readonly REPLACE_FAILED: "replace_failed";
17
+ readonly FALLBACK_DOWNLOAD_FAILED: "fallback_download_failed";
18
+ readonly FALLBACK_EXTRACT_FAILED: "fallback_extract_failed";
19
+ readonly FALLBACK_MISSING_BINARY: "fallback_missing_binary";
20
+ }>;
21
+ export type SelfUpdateFailureCode = typeof SELF_UPDATE_FAILURE_CODES[keyof typeof SELF_UPDATE_FAILURE_CODES];
22
+ export interface ClassifiedSelfUpdateError {
23
+ failureCode: SelfUpdateFailureCode;
24
+ detail: string;
25
+ }
26
+ export declare class SelfUpdateFailureError extends Error {
27
+ readonly failureCode: SelfUpdateFailureCode;
28
+ constructor(failureCode: SelfUpdateFailureCode, detail: string, options?: ErrorOptions);
29
+ }
30
+ export declare function selfUpdateFailure(failureCode: SelfUpdateFailureCode, detail: string, options?: ErrorOptions): SelfUpdateFailureError;
31
+ export declare function classifySelfUpdateError(err: unknown, fallback: SelfUpdateFailureCode): ClassifiedSelfUpdateError;
32
+ export declare function codeForDecisionReject(reason: string): SelfUpdateFailureCode;
33
+ export declare function renderFailureError(code: SelfUpdateFailureCode | undefined, detail: string): string;
@@ -0,0 +1,84 @@
1
+ export const SELF_UPDATE_FAILURE_CODES = Object.freeze({
2
+ INSTALL_GUARD_NAME_MISMATCH: 'install_guard_name_mismatch',
3
+ INSTALL_GUARD_UNREADABLE: 'install_guard_unreadable',
4
+ BAD_REQUIRED_VERSION: 'bad_required_version',
5
+ CURRENT_VERSION_UNPARSABLE: 'current_version_unparsable',
6
+ NPX_NOT_FOUND: 'npx_not_found',
7
+ DEGIT_TIMEOUT: 'degit_timeout',
8
+ DEGIT_FAILED: 'degit_failed',
9
+ DOWNLOAD_INCOMPLETE: 'download_incomplete',
10
+ DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
11
+ COPY_FAILED: 'copy_failed',
12
+ ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
13
+ DOWNLOAD_FAILED: 'download_failed',
14
+ REJECTED_DECISION: 'rejected_decision',
15
+ REJECTED_VERIFICATION: 'rejected_verification',
16
+ REPLACE_FAILED: 'replace_failed',
17
+ // V1 telemetry convention: tarball-fallback leg surfaces as `fallback_<reason>`
18
+ // so the hub can distinguish primary-channel failures from Channel 1b failures
19
+ // without inspecting the detail string. The enum string values are the wire
20
+ // contract; renaming the TS identifier without renaming the value would silently
21
+ // break aggregation. See V1 #282.
22
+ FALLBACK_DOWNLOAD_FAILED: 'fallback_download_failed',
23
+ FALLBACK_EXTRACT_FAILED: 'fallback_extract_failed',
24
+ FALLBACK_MISSING_BINARY: 'fallback_missing_binary',
25
+ });
26
+ export class SelfUpdateFailureError extends Error {
27
+ failureCode;
28
+ constructor(failureCode, detail, options) {
29
+ super(detail, options);
30
+ this.name = 'SelfUpdateFailureError';
31
+ this.failureCode = failureCode;
32
+ }
33
+ }
34
+ export function selfUpdateFailure(failureCode, detail, options) {
35
+ return new SelfUpdateFailureError(failureCode, detail, options);
36
+ }
37
+ export function classifySelfUpdateError(err, fallback) {
38
+ if (err instanceof SelfUpdateFailureError) {
39
+ return { failureCode: err.failureCode, detail: err.message };
40
+ }
41
+ if (isTimeoutError(err)) {
42
+ return { failureCode: SELF_UPDATE_FAILURE_CODES.DEGIT_TIMEOUT, detail: errorDetail(err) };
43
+ }
44
+ return { failureCode: fallback, detail: errorDetail(err) };
45
+ }
46
+ export function codeForDecisionReject(reason) {
47
+ if (reason === 'required_version_invalid')
48
+ return SELF_UPDATE_FAILURE_CODES.BAD_REQUIRED_VERSION;
49
+ if (reason === 'current_version_invalid')
50
+ return SELF_UPDATE_FAILURE_CODES.CURRENT_VERSION_UNPARSABLE;
51
+ if (reason === 'manifest_below_required')
52
+ return SELF_UPDATE_FAILURE_CODES.DOWNLOADED_VERSION_MISMATCH;
53
+ if (reason === 'manifest_missing'
54
+ || reason === 'manifest_no_version'
55
+ || reason === 'manifest_bad_version'
56
+ || reason === 'manifest_no_artifacts'
57
+ || reason === 'manifest_bad_artifact_path'
58
+ || reason === 'manifest_bad_artifact_sha256') {
59
+ return SELF_UPDATE_FAILURE_CODES.DOWNLOAD_INCOMPLETE;
60
+ }
61
+ return SELF_UPDATE_FAILURE_CODES.REJECTED_DECISION;
62
+ }
63
+ export function renderFailureError(code, detail) {
64
+ if (!code)
65
+ return detail;
66
+ const trimmed = detail.trim();
67
+ return trimmed.length > 0 ? `${code}: ${trimmed}` : code;
68
+ }
69
+ function isTimeoutError(err) {
70
+ if (!err || typeof err !== 'object')
71
+ return false;
72
+ const input = err;
73
+ const code = typeof input.code === 'string' ? input.code : '';
74
+ if (input.name === 'AbortError' || input.killed === true || input.signal === 'SIGTERM')
75
+ return true;
76
+ if (code === 'ETIMEDOUT' || code === 'UND_ERR_HEADERS_TIMEOUT' || code === 'UND_ERR_CONNECT_TIMEOUT')
77
+ return true;
78
+ return isTimeoutError(input.cause);
79
+ }
80
+ function errorDetail(err) {
81
+ if (err instanceof Error)
82
+ return err.message;
83
+ return String(err);
84
+ }
@@ -0,0 +1,5 @@
1
+ export * from './executor.js';
2
+ export * from './version.js';
3
+ export * from './policy.js';
4
+ export * from './releaseBinary.js';
5
+ export * from './failureCodes.js';
@@ -0,0 +1,5 @@
1
+ export * from './executor.js';
2
+ export * from './version.js';
3
+ export * from './policy.js';
4
+ export * from './releaseBinary.js';
5
+ export * from './failureCodes.js';
@@ -0,0 +1,43 @@
1
+ import { mailbox } from '@evomap/evolver-core';
2
+ import type { ForceUpdateDirective, SelfUpdateResult } from './executor.js';
3
+ type MailboxStore = mailbox.MailboxStore;
4
+ type LastUpdateStatus = 'success' | 'failed' | 'skipped' | 'pending';
5
+ export interface LastUpdatePayload {
6
+ to_version: string;
7
+ status: LastUpdateStatus;
8
+ finished_at: number;
9
+ from_version?: string;
10
+ directive_id?: string;
11
+ error?: string;
12
+ /**
13
+ * Which download channel produced the bytes that got applied: `'binary'` is
14
+ * the precompiled-asset happy path, `'tarball'` means Channel 1b fallback
15
+ * (release `.tar.gz`) was used. Persisted on success so the hub can see
16
+ * "primary CDN is degraded — fallback carrying production" without having
17
+ * to mine telemetry. Absent on non-success or when the executor predates
18
+ * the appliedVia field.
19
+ */
20
+ applied_via?: 'binary' | 'tarball';
21
+ }
22
+ export interface LastUpdateAck {
23
+ ok?: boolean;
24
+ reason?: string;
25
+ }
26
+ export declare function readPendingLastUpdate(store: MailboxStore, now?: number): LastUpdatePayload | undefined;
27
+ export declare function clearLastUpdateOnAck(store: MailboxStore, sent: LastUpdatePayload, now?: number): boolean;
28
+ export declare function writeLastUpdate(store: MailboxStore, payload: LastUpdatePayload, now?: number): boolean;
29
+ export declare function shouldClearForLastUpdateAck(ack: LastUpdateAck | undefined): boolean;
30
+ export declare function isLastUpdateRelatedError(value: unknown): boolean;
31
+ export declare function reportSelfUpdateLastUpdate(store: MailboxStore, directive: ForceUpdateDirective, result: SelfUpdateResult, opts?: {
32
+ fromVersion: string;
33
+ now?: number;
34
+ }): boolean;
35
+ export declare function reportPendingSelfUpdateLastUpdate(store: MailboxStore, directive: ForceUpdateDirective, opts?: {
36
+ fromVersion: string;
37
+ now?: number;
38
+ }): boolean;
39
+ export declare function lastUpdateFromSelfUpdateResult(directive: ForceUpdateDirective, result: SelfUpdateResult, opts: {
40
+ fromVersion: string;
41
+ now: number;
42
+ }): LastUpdatePayload | undefined;
43
+ export {};
@@ -0,0 +1,195 @@
1
+ import { hub as hubNs, mailbox, ops } from '@evomap/evolver-core';
2
+ import { renderFailureError } from './failureCodes.js';
3
+ const LAST_UPDATE_STATE_KEY = 'self_update:last_update';
4
+ const FINISHED_AT_MIN_MS = 1_700_000_000_000;
5
+ const LAST_UPDATE_TTL_MS = 7 * 24 * 60 * 60_000;
6
+ const LAST_UPDATE_TO_VERSION_MAX = 32;
7
+ const LAST_UPDATE_FROM_VERSION_MAX = 32;
8
+ const LAST_UPDATE_DIRECTIVE_ID_MAX = 64;
9
+ const LAST_UPDATE_ERROR_MAX = 1000;
10
+ const STATUS_SET = new Set(['success', 'failed', 'skipped', 'pending']);
11
+ export function readPendingLastUpdate(store, now = Date.now()) {
12
+ const raw = store.getState(LAST_UPDATE_STATE_KEY);
13
+ if (!raw)
14
+ return undefined;
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ clearPendingLastUpdate(store);
21
+ return undefined;
22
+ }
23
+ const payload = normalizeLastUpdate(parsed);
24
+ if (!payload) {
25
+ clearPendingLastUpdate(store);
26
+ return undefined;
27
+ }
28
+ if (payload.finished_at >= FINISHED_AT_MIN_MS && now - payload.finished_at > LAST_UPDATE_TTL_MS) {
29
+ clearPendingLastUpdate(store);
30
+ return undefined;
31
+ }
32
+ return payload;
33
+ }
34
+ function clearPendingLastUpdate(store) {
35
+ store.setState(LAST_UPDATE_STATE_KEY, '');
36
+ }
37
+ export function clearLastUpdateOnAck(store, sent, now = Date.now()) {
38
+ const current = readPendingLastUpdate(store, now);
39
+ if (!current)
40
+ return false;
41
+ if (!sameIdentity(current, sent))
42
+ return false;
43
+ clearPendingLastUpdate(store);
44
+ return true;
45
+ }
46
+ export function writeLastUpdate(store, payload, now = Date.now()) {
47
+ const normalized = normalizeLastUpdate(payload);
48
+ if (!normalized)
49
+ return false;
50
+ if (normalized.status === 'skipped' || normalized.status === 'pending') {
51
+ const pending = readPendingLastUpdate(store, now);
52
+ if (pending?.status === 'success' || pending?.status === 'failed')
53
+ return false;
54
+ }
55
+ store.setState(LAST_UPDATE_STATE_KEY, JSON.stringify(normalized));
56
+ return true;
57
+ }
58
+ export function shouldClearForLastUpdateAck(ack) {
59
+ if (!ack || typeof ack !== 'object')
60
+ return false;
61
+ return ack.ok === true;
62
+ }
63
+ export function isLastUpdateRelatedError(value) {
64
+ const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
65
+ return /last[_\-.]?update/i.test(text);
66
+ }
67
+ export function reportSelfUpdateLastUpdate(store, directive, result, opts = { fromVersion: '0.0.0' }) {
68
+ const payload = lastUpdateFromSelfUpdateResult(directive, result, {
69
+ fromVersion: opts.fromVersion,
70
+ now: opts.now ?? Date.now(),
71
+ });
72
+ if (!payload)
73
+ return false;
74
+ return writeLastUpdate(store, payload, opts.now ?? Date.now());
75
+ }
76
+ export function reportPendingSelfUpdateLastUpdate(store, directive, opts = { fromVersion: '0.0.0' }) {
77
+ const now = opts.now ?? Date.now();
78
+ const toVersion = targetVersionForDirective(directive);
79
+ if (!toVersion)
80
+ return false;
81
+ return writeLastUpdate(store, {
82
+ to_version: toVersion,
83
+ status: 'pending',
84
+ finished_at: Math.max(now, FINISHED_AT_MIN_MS),
85
+ from_version: clampString(opts.fromVersion, LAST_UPDATE_FROM_VERSION_MAX),
86
+ ...(directive.directive_id ? { directive_id: String(directive.directive_id) } : {}),
87
+ }, now);
88
+ }
89
+ export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
90
+ if (result.outcome === 'already_in_progress' || result.outcome === 'disabled')
91
+ return undefined;
92
+ const toVersion = targetVersionForReport(directive, result);
93
+ if (!toVersion)
94
+ return undefined;
95
+ const base = {
96
+ to_version: toVersion,
97
+ status: statusForOutcome(result.outcome),
98
+ finished_at: Math.max(opts.now, FINISHED_AT_MIN_MS),
99
+ ...(directive.directive_id ? { directive_id: String(directive.directive_id) } : {}),
100
+ };
101
+ if (base.status === 'success') {
102
+ return {
103
+ ...base,
104
+ from_version: clampString(opts.fromVersion, LAST_UPDATE_FROM_VERSION_MAX),
105
+ ...(result.appliedVia ? { applied_via: result.appliedVia } : {}),
106
+ };
107
+ }
108
+ if (base.status === 'failed') {
109
+ return {
110
+ ...base,
111
+ from_version: clampString(opts.fromVersion, LAST_UPDATE_FROM_VERSION_MAX),
112
+ error: clampString(hubNs.redactString(renderFailureError(result.failureCode, result.reason)), LAST_UPDATE_ERROR_MAX),
113
+ };
114
+ }
115
+ return base;
116
+ }
117
+ function statusForOutcome(outcome) {
118
+ if (outcome === 'applied')
119
+ return 'success';
120
+ if (outcome === 'noop')
121
+ return 'skipped';
122
+ return 'failed';
123
+ }
124
+ function targetVersionForReport(directive, result) {
125
+ const fromResult = reportableVersion(result.targetVersion);
126
+ if (fromResult)
127
+ return fromResult;
128
+ if (result.outcome !== 'applied' && result.outcome !== 'noop')
129
+ return requiredVersionForReport(directive);
130
+ return targetVersionForDirective(directive);
131
+ }
132
+ function targetVersionForDirective(directive) {
133
+ const manifest = directive.manifest;
134
+ if (manifest && typeof manifest === 'object' && !Array.isArray(manifest)) {
135
+ const version = concreteVersion(manifest.version);
136
+ if (version)
137
+ return version;
138
+ }
139
+ return ops.normalizeRequiredVersion(directive.required_version);
140
+ }
141
+ function requiredVersionForReport(directive) {
142
+ const normalized = ops.normalizeRequiredVersion(directive.required_version);
143
+ if (!normalized || normalized.length > LAST_UPDATE_TO_VERSION_MAX)
144
+ return undefined;
145
+ return normalized;
146
+ }
147
+ function normalizeLastUpdate(value) {
148
+ if (!value || typeof value !== 'object' || Array.isArray(value))
149
+ return undefined;
150
+ const input = value;
151
+ const toVersion = concreteVersion(input['to_version']);
152
+ const status = typeof input['status'] === 'string' && STATUS_SET.has(input['status'])
153
+ ? input['status']
154
+ : undefined;
155
+ const finishedAt = typeof input['finished_at'] === 'number' && Number.isFinite(input['finished_at'])
156
+ ? Math.max(Math.trunc(input['finished_at']), FINISHED_AT_MIN_MS)
157
+ : undefined;
158
+ if (!toVersion || !status || finishedAt === undefined)
159
+ return undefined;
160
+ const appliedVia = input['applied_via'];
161
+ return {
162
+ to_version: toVersion,
163
+ status,
164
+ finished_at: finishedAt,
165
+ ...(typeof input['from_version'] === 'string' ? { from_version: clampString(input['from_version'], LAST_UPDATE_FROM_VERSION_MAX) } : {}),
166
+ ...(typeof input['directive_id'] === 'string' && input['directive_id'].length > 0
167
+ ? { directive_id: clampString(input['directive_id'], LAST_UPDATE_DIRECTIVE_ID_MAX) }
168
+ : {}),
169
+ ...(typeof input['error'] === 'string' && input['error'].length > 0
170
+ ? { error: clampString(hubNs.redactString(input['error']), LAST_UPDATE_ERROR_MAX) }
171
+ : {}),
172
+ ...(appliedVia === 'binary' || appliedVia === 'tarball' ? { applied_via: appliedVia } : {}),
173
+ };
174
+ }
175
+ function sameIdentity(a, b) {
176
+ return a.status === b.status
177
+ && a.finished_at === b.finished_at
178
+ && a.to_version === b.to_version
179
+ && (a.directive_id ?? '') === (b.directive_id ?? '');
180
+ }
181
+ function concreteVersion(value) {
182
+ const normalized = ops.normalizeConcreteVersion(value);
183
+ if (!normalized || normalized.length > LAST_UPDATE_TO_VERSION_MAX)
184
+ return undefined;
185
+ return normalized;
186
+ }
187
+ function reportableVersion(value) {
188
+ const normalized = ops.normalizeRequiredVersion(value) ?? ops.normalizeConcreteVersion(value);
189
+ if (!normalized || normalized.length > LAST_UPDATE_TO_VERSION_MAX)
190
+ return undefined;
191
+ return normalized;
192
+ }
193
+ function clampString(value, max) {
194
+ return value.length <= max ? value : value.slice(0, max);
195
+ }
@@ -0,0 +1,3 @@
1
+ export type SelfUpdatePolicy = 'off' | 'prompt' | 'auto';
2
+ /** Resolve EVOLVER_SELF_UPDATE to a policy. Unset/unrecognized → 'off' (fail-closed). */
3
+ export declare function resolveSelfUpdatePolicy(env?: NodeJS.ProcessEnv): SelfUpdatePolicy;
@@ -0,0 +1,9 @@
1
+ /** Resolve EVOLVER_SELF_UPDATE to a policy. Unset/unrecognized → 'off' (fail-closed). */
2
+ export function resolveSelfUpdatePolicy(env = process.env) {
3
+ const raw = (env['EVOLVER_SELF_UPDATE'] ?? '').trim().toLowerCase();
4
+ if (raw === 'prompt')
5
+ return 'prompt';
6
+ if (raw === 'auto')
7
+ return 'auto';
8
+ return 'off';
9
+ }