instar 1.2.34 → 1.2.36

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 (33) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +20 -67
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/tunnel/CloudflareNamedProvider.d.ts +51 -0
  5. package/dist/tunnel/CloudflareNamedProvider.d.ts.map +1 -0
  6. package/dist/tunnel/CloudflareNamedProvider.js +258 -0
  7. package/dist/tunnel/CloudflareNamedProvider.js.map +1 -0
  8. package/dist/tunnel/CloudflareQuickProvider.d.ts +48 -0
  9. package/dist/tunnel/CloudflareQuickProvider.d.ts.map +1 -0
  10. package/dist/tunnel/CloudflareQuickProvider.js +201 -0
  11. package/dist/tunnel/CloudflareQuickProvider.js.map +1 -0
  12. package/dist/tunnel/TunnelLifecycle.d.ts +190 -0
  13. package/dist/tunnel/TunnelLifecycle.d.ts.map +1 -0
  14. package/dist/tunnel/TunnelLifecycle.js +222 -0
  15. package/dist/tunnel/TunnelLifecycle.js.map +1 -0
  16. package/dist/tunnel/TunnelManager.d.ts +99 -62
  17. package/dist/tunnel/TunnelManager.d.ts.map +1 -1
  18. package/dist/tunnel/TunnelManager.js +340 -395
  19. package/dist/tunnel/TunnelManager.js.map +1 -1
  20. package/dist/tunnel/TunnelNotifier.d.ts +110 -0
  21. package/dist/tunnel/TunnelNotifier.d.ts.map +1 -0
  22. package/dist/tunnel/TunnelNotifier.js +268 -0
  23. package/dist/tunnel/TunnelNotifier.js.map +1 -0
  24. package/dist/tunnel/TunnelProvider.d.ts +80 -0
  25. package/dist/tunnel/TunnelProvider.d.ts.map +1 -0
  26. package/dist/tunnel/TunnelProvider.js +28 -0
  27. package/dist/tunnel/TunnelProvider.js.map +1 -0
  28. package/package.json +1 -1
  29. package/src/data/builtin-manifest.json +2 -2
  30. package/upgrades/1.2.35.md +96 -0
  31. package/upgrades/1.2.36.md +111 -0
  32. package/upgrades/side-effects/tunnel-failure-resilience.md +118 -0
  33. package/upgrades/side-effects/tunnel-manager-rewrite.md +129 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * TunnelLifecycle — single-owner state machine for the tunnel layer.
3
+ *
4
+ * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 2.
5
+ *
6
+ * This module owns the detect → attempt → fall-back → notify → self-heal
7
+ * lifecycle. The existing server.ts startup-retry ladder, background-
8
+ * retry ladder, and Lifeline failure message are retired in favor of
9
+ * routing all retry through here.
10
+ *
11
+ * Design properties:
12
+ * - Single-writer transitions: every state change goes through
13
+ * `transition(expectedFrom, to)`, a compare-and-swap that REJECTS
14
+ * any transition whose expectedFrom doesn't match the current
15
+ * state. This prevents the error+exit double-handler race that the
16
+ * concurrency reviewer surfaced.
17
+ * - Monotonic epoch: each guarded transition carries a `transition.id`
18
+ * for downstream notification dedup. Notifications are emitted ONLY
19
+ * from inside the guarded mutation, tagged with the epoch.
20
+ * - Episode model: each contiguous failure→recovery cycle is an
21
+ * `episode` with its own id and one-time consent nonce. All consent
22
+ * state binds to (episodeId, provider, ownerId, chatId, messageId,
23
+ * issuedAt).
24
+ *
25
+ * This module is provider-agnostic and notification-channel-agnostic.
26
+ * Providers are injected as a pool; the notifier is injected as a sink.
27
+ * Both responsibilities live OUTSIDE this module per the spec's
28
+ * separation of concerns.
29
+ */
30
+ import { EventEmitter } from 'node:events';
31
+ import type { ProviderFailureReason, ProviderName } from './TunnelProvider.js';
32
+ export type TunnelLifecycleState =
33
+ /** No tunnel running and not currently trying. */
34
+ 'idle'
35
+ /** A start attempt is in flight on some provider. */
36
+ | 'starting'
37
+ /** A provider is up; URL is being served. */
38
+ | 'active'
39
+ /** A Tier-1 provider failed and we are backing off / trying the next one. */
40
+ | 'retrying'
41
+ /** All Tier-1 providers exhausted; awaiting owner consent for a Tier-2 relay. */
42
+ | 'awaiting-consent'
43
+ /** A Tier-2 relay provider is up; URL being served via the relay. */
44
+ | 'relay-active'
45
+ /** Cloudflare recovery probe verifying Tier-1 is back; switch-back imminent. */
46
+ | 'self-healing'
47
+ /** All providers exhausted (or consent declined); background self-heal is the only retry mechanism. */
48
+ | 'exhausted';
49
+ export interface Episode {
50
+ /** Stable per-failure-cycle id. Resets each time the manager enters `starting` from `idle`/`active`. */
51
+ episodeId: string;
52
+ /** When this episode began (ISO timestamp). */
53
+ startedAt: string;
54
+ /** Number of Tier-1 providers attempted in this episode. */
55
+ tier1Attempts: number;
56
+ /** Last classified failure reason (the most recent provider failure). */
57
+ lastFailureReason: ProviderFailureReason | null;
58
+ /** Which provider names have been attempted in this episode (in order). */
59
+ attemptedProviders: ProviderName[];
60
+ }
61
+ export interface ConsentRecord {
62
+ /** Tied to the episode that issued the prompt. */
63
+ episodeId: string;
64
+ /** Which Tier-2 provider the consent authorizes. Single-use, per-provider. */
65
+ provider: 'localtunnel' | 'bore';
66
+ /** Owner principal id (Telegram user id). Non-owner clicks are rejected. */
67
+ ownerId: string;
68
+ /** Telegram chat the consent was issued in (DM chat id). */
69
+ chatId: string;
70
+ /** Telegram message id of the inline-button prompt. */
71
+ messageId: string;
72
+ /** 128-bit CSPRNG nonce carried in callback_data. Atomic compare-and-delete on use. */
73
+ nonce: string;
74
+ /** Issued-at (ms epoch). Combined with consentTimeoutMs to expire. */
75
+ issuedAt: number;
76
+ }
77
+ export interface ConsentCooldownState {
78
+ /** Count of consecutive declines/timeouts feeding the exponential backoff. */
79
+ consecutiveRefusals: number;
80
+ /** When the cooldown was last extended (ms epoch). */
81
+ lastExtendedAt: number;
82
+ /** Until when the cooldown is active (ms epoch). 0 = no cooldown. */
83
+ activeUntil: number;
84
+ }
85
+ export interface PersistedTunnelState {
86
+ /** Schema version for safe future migrations. */
87
+ version: 1;
88
+ /** Last observed state. Used by the boot-recovery path. */
89
+ lastState: TunnelLifecycleState;
90
+ /** Last active URL (if any) — for diagnostic only; never trusted as live. */
91
+ lastUrl: string | null;
92
+ /** Name of the provider that was active when state was persisted. */
93
+ activeProvider: ProviderName | null;
94
+ /**
95
+ * Set to true the moment a Tier-2 relay-active state is entered. Cleared
96
+ * only after `authToken`/PIN rotation succeeds. If the agent crashes
97
+ * mid-relay-episode, this flag tells the boot-recovery path "rotate
98
+ * BEFORE accepting any API traffic on the new boot" — per spec Part 6.
99
+ */
100
+ rotationPending: boolean;
101
+ /** Cross-episode consent cooldown counter. */
102
+ consentCooldown: ConsentCooldownState;
103
+ /** Current episode, if any. */
104
+ episode: Episode | null;
105
+ /** Saved-at timestamp (ISO). */
106
+ savedAt: string;
107
+ }
108
+ export interface TransitionEvent {
109
+ /** Monotonic per-manager-instance epoch — never reset. Used by notifiers for dedup. */
110
+ epoch: number;
111
+ /** State BEFORE this transition. */
112
+ from: TunnelLifecycleState;
113
+ /** State AFTER this transition. */
114
+ to: TunnelLifecycleState;
115
+ /** Episode in effect at the time of the transition. */
116
+ episode: Episode | null;
117
+ /** Last classified failure (when relevant). */
118
+ lastFailureReason: ProviderFailureReason | null;
119
+ /** When (ms epoch). */
120
+ at: number;
121
+ }
122
+ export type TunnelLifecycleEvents = {
123
+ /** Fires on every successful guarded transition. */
124
+ transition: (e: TransitionEvent) => void;
125
+ };
126
+ /** Classify a provider failure based on the Error's `message`. */
127
+ export declare function classifyFailure(err: unknown): ProviderFailureReason;
128
+ /** Generate a fresh 128-bit CSPRNG nonce as hex (32 chars). */
129
+ export declare function generateNonce(): string;
130
+ /** Generate a fresh episode id (16-byte hex, no PII). */
131
+ export declare function generateEpisodeId(): string;
132
+ export declare function isValidTransition(from: TunnelLifecycleState, to: TunnelLifecycleState): boolean;
133
+ export declare class TunnelLifecycle extends EventEmitter {
134
+ private _state;
135
+ private _epoch;
136
+ private _episode;
137
+ private _lastFailureReason;
138
+ private _activeProvider;
139
+ private _consentCooldown;
140
+ private _rotationPending;
141
+ get state(): TunnelLifecycleState;
142
+ get epoch(): number;
143
+ get episode(): Episode | null;
144
+ get activeProvider(): ProviderName | null;
145
+ get rotationPending(): boolean;
146
+ get consentCooldown(): ConsentCooldownState;
147
+ get lastFailureReason(): ProviderFailureReason | null;
148
+ /**
149
+ * Compare-and-swap transition. Returns true if the transition was
150
+ * accepted; false if the current state didn't match `expectedFrom`
151
+ * (caller MUST treat false as a race — the lifecycle moved
152
+ * underneath them — and re-read state).
153
+ *
154
+ * `to` must be reachable from `expectedFrom` per VALID_TRANSITIONS;
155
+ * an invalid pair throws (programming error, not a runtime race).
156
+ */
157
+ transition(expectedFrom: TunnelLifecycleState, to: TunnelLifecycleState, ctx?: {
158
+ episode?: Episode | null;
159
+ activeProvider?: ProviderName | null;
160
+ lastFailureReason?: ProviderFailureReason | null;
161
+ rotationPending?: boolean;
162
+ }): boolean;
163
+ /** Begin a new failure-recovery episode. Called when entering `starting` from `idle` or `active`. */
164
+ startEpisode(): Episode;
165
+ /** Close out the current episode (called when returning to `active` or `idle`). */
166
+ endEpisode(): void;
167
+ /** Record a Tier-1 provider failure attempt against the current episode. */
168
+ recordAttempt(provider: ProviderName, reason: ProviderFailureReason): void;
169
+ /** Apply an exponential consent cooldown after a decline / timeout. */
170
+ recordConsentRefusal(): ConsentCooldownState;
171
+ /** Clear cooldown — called on explicit owner opt-in or fresh post-cooldown episode. */
172
+ clearConsentCooldown(): void;
173
+ /** True if the consent prompt is currently suppressed by the cooldown. */
174
+ isConsentSuppressed(now?: number): boolean;
175
+ /** Snapshot current state for persistence to tunnel.json. */
176
+ snapshot(): PersistedTunnelState;
177
+ /**
178
+ * Restore from a persisted snapshot. Called at boot ONLY for the
179
+ * cooldown counter + rotationPending flag — the state itself is NOT
180
+ * resumed (every boot starts at `idle` and re-enters `starting` from
181
+ * scratch). The rotationPending flag triggers a rotation-before-
182
+ * traffic in the boot-recovery path; rotation clears the flag.
183
+ */
184
+ restoreFrom(snapshot: PersistedTunnelState): void;
185
+ /** Set the rotation-pending flag (entered on `relay-active`; cleared after rotation). */
186
+ setRotationPending(value: boolean): void;
187
+ /** Used by callers (manager) that need to validate a consent-reply against the active state. */
188
+ isAwaitingConsent(): boolean;
189
+ }
190
+ //# sourceMappingURL=TunnelLifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TunnelLifecycle.d.ts","sourceRoot":"","sources":["../../src/tunnel/TunnelLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAkB,qBAAqB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAI/F,MAAM,MAAM,oBAAoB;AAC9B,kDAAkD;AAChD,MAAM;AACR,qDAAqD;GACnD,UAAU;AACZ,6CAA6C;GAC3C,QAAQ;AACV,6EAA6E;GAC3E,UAAU;AACZ,iFAAiF;GAC/E,kBAAkB;AACpB,qEAAqE;GACnE,cAAc;AAChB,gFAAgF;GAC9E,cAAc;AAChB,uGAAuG;GACrG,WAAW,CAAC;AAEhB,MAAM,WAAW,OAAO;IACtB,wGAAwG;IACxG,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,iBAAiB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,kBAAkB,EAAE,YAAY,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,QAAQ,EAAE,aAAa,GAAG,MAAM,CAAC;IACjC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,8EAA8E;IAC9E,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sDAAsD;IACtD,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,OAAO,EAAE,CAAC,CAAC;IACX,2DAA2D;IAC3D,SAAS,EAAE,oBAAoB,CAAC;IAChC,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,qEAAqE;IACrE,cAAc,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC;;;;;OAKG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB,8CAA8C;IAC9C,eAAe,EAAE,oBAAoB,CAAC;IACtC,+BAA+B;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,MAAM,WAAW,eAAe;IAC9B,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,mCAAmC;IACnC,EAAE,EAAE,oBAAoB,CAAC;IACzB,uDAAuD;IACvD,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,+CAA+C;IAC/C,iBAAiB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,uBAAuB;IACvB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,oDAAoD;IACpD,UAAU,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;CAC1C,CAAC;AAIF,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,qBAAqB,CAqBnE;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,yDAAyD;AACzD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAgBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,EAAE,oBAAoB,GAAG,OAAO,CAI/F;AAED,qBAAa,eAAgB,SAAQ,YAAY;IAC/C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,kBAAkB,CAAsC;IAChE,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,gBAAgB,CAItB;IACF,OAAO,CAAC,gBAAgB,CAAS;IAEjC,IAAI,KAAK,IAAI,oBAAoB,CAAwB;IACzD,IAAI,KAAK,IAAI,MAAM,CAAwB;IAC3C,IAAI,OAAO,IAAI,OAAO,GAAG,IAAI,CAA0B;IACvD,IAAI,cAAc,IAAI,YAAY,GAAG,IAAI,CAAiC;IAC1E,IAAI,eAAe,IAAI,OAAO,CAAkC;IAChE,IAAI,eAAe,IAAI,oBAAoB,CAAyC;IACpF,IAAI,iBAAiB,IAAI,qBAAqB,GAAG,IAAI,CAAoC;IAEzF;;;;;;;;OAQG;IACH,UAAU,CAAC,YAAY,EAAE,oBAAoB,EAAE,EAAE,EAAE,oBAAoB,EAAE,GAAG,CAAC,EAAE;QAC7E,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QACzB,cAAc,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;QACrC,iBAAiB,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;QACjD,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,GAAG,OAAO;IA4BX,qGAAqG;IACrG,YAAY,IAAI,OAAO;IAYvB,mFAAmF;IACnF,UAAU,IAAI,IAAI;IAIlB,4EAA4E;IAC5E,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI;IAQ1E,uEAAuE;IACvE,oBAAoB,IAAI,oBAAoB;IAa5C,uFAAuF;IACvF,oBAAoB,IAAI,IAAI;IAI5B,0EAA0E;IAC1E,mBAAmB,CAAC,GAAG,SAAa,GAAG,OAAO;IAI9C,6DAA6D;IAC7D,QAAQ,IAAI,oBAAoB;IAahC;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAOjD,yFAAyF;IACzF,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAIxC,gGAAgG;IAChG,iBAAiB,IAAI,OAAO;CAG7B"}
@@ -0,0 +1,222 @@
1
+ /**
2
+ * TunnelLifecycle — single-owner state machine for the tunnel layer.
3
+ *
4
+ * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 2.
5
+ *
6
+ * This module owns the detect → attempt → fall-back → notify → self-heal
7
+ * lifecycle. The existing server.ts startup-retry ladder, background-
8
+ * retry ladder, and Lifeline failure message are retired in favor of
9
+ * routing all retry through here.
10
+ *
11
+ * Design properties:
12
+ * - Single-writer transitions: every state change goes through
13
+ * `transition(expectedFrom, to)`, a compare-and-swap that REJECTS
14
+ * any transition whose expectedFrom doesn't match the current
15
+ * state. This prevents the error+exit double-handler race that the
16
+ * concurrency reviewer surfaced.
17
+ * - Monotonic epoch: each guarded transition carries a `transition.id`
18
+ * for downstream notification dedup. Notifications are emitted ONLY
19
+ * from inside the guarded mutation, tagged with the epoch.
20
+ * - Episode model: each contiguous failure→recovery cycle is an
21
+ * `episode` with its own id and one-time consent nonce. All consent
22
+ * state binds to (episodeId, provider, ownerId, chatId, messageId,
23
+ * issuedAt).
24
+ *
25
+ * This module is provider-agnostic and notification-channel-agnostic.
26
+ * Providers are injected as a pool; the notifier is injected as a sink.
27
+ * Both responsibilities live OUTSIDE this module per the spec's
28
+ * separation of concerns.
29
+ */
30
+ import crypto from 'node:crypto';
31
+ import { EventEmitter } from 'node:events';
32
+ // ── Helpers ──────────────────────────────────────────────────────────
33
+ /** Classify a provider failure based on the Error's `message`. */
34
+ export function classifyFailure(err) {
35
+ const msg = (err instanceof Error ? err.message : String(err ?? '')).toLowerCase();
36
+ if (msg.startsWith('rate-limited') || msg.includes('rate limit') || msg.includes('429') || msg.includes('1015')) {
37
+ return 'rate-limited';
38
+ }
39
+ if (msg.startsWith('binary-missing') || msg.includes('not installed') || msg.includes('enoent')) {
40
+ return 'binary-missing';
41
+ }
42
+ if (msg.startsWith('network') || msg.includes('dns') || msg.includes('econnrefused') || msg.includes('eai_again')) {
43
+ return 'network';
44
+ }
45
+ if (msg.startsWith('timeout') || msg.includes('timed out')) {
46
+ return 'timeout';
47
+ }
48
+ if (msg.startsWith('reachability-failed')) {
49
+ return 'reachability-failed';
50
+ }
51
+ if (msg.includes('process-exit') || msg.includes('exited')) {
52
+ return 'process-exit';
53
+ }
54
+ return 'unknown';
55
+ }
56
+ /** Generate a fresh 128-bit CSPRNG nonce as hex (32 chars). */
57
+ export function generateNonce() {
58
+ return crypto.randomBytes(16).toString('hex');
59
+ }
60
+ /** Generate a fresh episode id (16-byte hex, no PII). */
61
+ export function generateEpisodeId() {
62
+ return crypto.randomBytes(8).toString('hex');
63
+ }
64
+ // ── Single-writer state machine ─────────────────────────────────────
65
+ /** Valid transitions per the spec's state machine. */
66
+ const VALID_TRANSITIONS = new Map([
67
+ ['idle', new Set(['starting'])],
68
+ ['starting', new Set(['active', 'retrying', 'awaiting-consent', 'exhausted', 'idle'])],
69
+ ['active', new Set(['retrying', 'idle'])],
70
+ ['retrying', new Set(['active', 'starting', 'awaiting-consent', 'exhausted', 'idle'])],
71
+ ['awaiting-consent', new Set(['relay-active', 'starting', 'active', 'exhausted', 'idle'])],
72
+ ['relay-active', new Set(['self-healing', 'idle', 'exhausted'])],
73
+ ['self-healing', new Set(['active', 'relay-active', 'exhausted', 'idle'])],
74
+ ['exhausted', new Set(['self-healing', 'starting', 'idle'])],
75
+ ]);
76
+ export function isValidTransition(from, to) {
77
+ if (from === to)
78
+ return false;
79
+ const allowed = VALID_TRANSITIONS.get(from);
80
+ return !!allowed && allowed.has(to);
81
+ }
82
+ export class TunnelLifecycle extends EventEmitter {
83
+ _state = 'idle';
84
+ _epoch = 0;
85
+ _episode = null;
86
+ _lastFailureReason = null;
87
+ _activeProvider = null;
88
+ _consentCooldown = {
89
+ consecutiveRefusals: 0,
90
+ lastExtendedAt: 0,
91
+ activeUntil: 0,
92
+ };
93
+ _rotationPending = false;
94
+ get state() { return this._state; }
95
+ get epoch() { return this._epoch; }
96
+ get episode() { return this._episode; }
97
+ get activeProvider() { return this._activeProvider; }
98
+ get rotationPending() { return this._rotationPending; }
99
+ get consentCooldown() { return { ...this._consentCooldown }; }
100
+ get lastFailureReason() { return this._lastFailureReason; }
101
+ /**
102
+ * Compare-and-swap transition. Returns true if the transition was
103
+ * accepted; false if the current state didn't match `expectedFrom`
104
+ * (caller MUST treat false as a race — the lifecycle moved
105
+ * underneath them — and re-read state).
106
+ *
107
+ * `to` must be reachable from `expectedFrom` per VALID_TRANSITIONS;
108
+ * an invalid pair throws (programming error, not a runtime race).
109
+ */
110
+ transition(expectedFrom, to, ctx) {
111
+ if (this._state !== expectedFrom) {
112
+ return false; // CAS lost
113
+ }
114
+ if (!isValidTransition(expectedFrom, to)) {
115
+ throw new Error(`invalid transition ${expectedFrom} → ${to}`);
116
+ }
117
+ // Apply mutation atomically (Node is single-threaded; this synchronous
118
+ // block IS the critical section).
119
+ this._state = to;
120
+ this._epoch += 1;
121
+ if (ctx?.episode !== undefined)
122
+ this._episode = ctx.episode;
123
+ if (ctx?.activeProvider !== undefined)
124
+ this._activeProvider = ctx.activeProvider;
125
+ if (ctx?.lastFailureReason !== undefined)
126
+ this._lastFailureReason = ctx.lastFailureReason;
127
+ if (ctx?.rotationPending !== undefined)
128
+ this._rotationPending = ctx.rotationPending;
129
+ const event = {
130
+ epoch: this._epoch,
131
+ from: expectedFrom,
132
+ to,
133
+ episode: this._episode,
134
+ lastFailureReason: this._lastFailureReason,
135
+ at: Date.now(),
136
+ };
137
+ this.emit('transition', event);
138
+ return true;
139
+ }
140
+ /** Begin a new failure-recovery episode. Called when entering `starting` from `idle` or `active`. */
141
+ startEpisode() {
142
+ const ep = {
143
+ episodeId: generateEpisodeId(),
144
+ startedAt: new Date().toISOString(),
145
+ tier1Attempts: 0,
146
+ lastFailureReason: null,
147
+ attemptedProviders: [],
148
+ };
149
+ this._episode = ep;
150
+ return ep;
151
+ }
152
+ /** Close out the current episode (called when returning to `active` or `idle`). */
153
+ endEpisode() {
154
+ this._episode = null;
155
+ }
156
+ /** Record a Tier-1 provider failure attempt against the current episode. */
157
+ recordAttempt(provider, reason) {
158
+ if (!this._episode)
159
+ return;
160
+ this._episode.tier1Attempts += 1;
161
+ this._episode.lastFailureReason = reason;
162
+ this._episode.attemptedProviders.push(provider);
163
+ this._lastFailureReason = reason;
164
+ }
165
+ /** Apply an exponential consent cooldown after a decline / timeout. */
166
+ recordConsentRefusal() {
167
+ this._consentCooldown.consecutiveRefusals += 1;
168
+ const n = this._consentCooldown.consecutiveRefusals;
169
+ // 1h → 4h → 24h, then 24h forever
170
+ const COOLDOWNS_MS = [60 * 60_000, 4 * 60 * 60_000, 24 * 60 * 60_000];
171
+ const idx = Math.min(n - 1, COOLDOWNS_MS.length - 1);
172
+ const ms = COOLDOWNS_MS[idx] ?? COOLDOWNS_MS[COOLDOWNS_MS.length - 1];
173
+ if (ms === undefined)
174
+ throw new Error('unreachable: cooldown table empty');
175
+ this._consentCooldown.lastExtendedAt = Date.now();
176
+ this._consentCooldown.activeUntil = Date.now() + ms;
177
+ return this.consentCooldown;
178
+ }
179
+ /** Clear cooldown — called on explicit owner opt-in or fresh post-cooldown episode. */
180
+ clearConsentCooldown() {
181
+ this._consentCooldown = { consecutiveRefusals: 0, lastExtendedAt: 0, activeUntil: 0 };
182
+ }
183
+ /** True if the consent prompt is currently suppressed by the cooldown. */
184
+ isConsentSuppressed(now = Date.now()) {
185
+ return this._consentCooldown.activeUntil > now;
186
+ }
187
+ /** Snapshot current state for persistence to tunnel.json. */
188
+ snapshot() {
189
+ return {
190
+ version: 1,
191
+ lastState: this._state,
192
+ lastUrl: null, // populated by the manager that knows the URL
193
+ activeProvider: this._activeProvider,
194
+ rotationPending: this._rotationPending,
195
+ consentCooldown: this.consentCooldown,
196
+ episode: this._episode ? { ...this._episode, attemptedProviders: [...this._episode.attemptedProviders] } : null,
197
+ savedAt: new Date().toISOString(),
198
+ };
199
+ }
200
+ /**
201
+ * Restore from a persisted snapshot. Called at boot ONLY for the
202
+ * cooldown counter + rotationPending flag — the state itself is NOT
203
+ * resumed (every boot starts at `idle` and re-enters `starting` from
204
+ * scratch). The rotationPending flag triggers a rotation-before-
205
+ * traffic in the boot-recovery path; rotation clears the flag.
206
+ */
207
+ restoreFrom(snapshot) {
208
+ this._consentCooldown = { ...snapshot.consentCooldown };
209
+ this._rotationPending = snapshot.rotationPending;
210
+ // _state stays 'idle' by design — boot recovery rotation runs before
211
+ // the new lifecycle starts.
212
+ }
213
+ /** Set the rotation-pending flag (entered on `relay-active`; cleared after rotation). */
214
+ setRotationPending(value) {
215
+ this._rotationPending = value;
216
+ }
217
+ /** Used by callers (manager) that need to validate a consent-reply against the active state. */
218
+ isAwaitingConsent() {
219
+ return this._state === 'awaiting-consent';
220
+ }
221
+ }
222
+ //# sourceMappingURL=TunnelLifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TunnelLifecycle.js","sourceRoot":"","sources":["../../src/tunnel/TunnelLifecycle.ts"],"names":[],"mappings":"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"}
@@ -1,46 +1,68 @@
1
1
  /**
2
- * Cloudflare Tunnel manager for Instar agents.
2
+ * TunnelManager — single owner of the tunnel lifecycle.
3
3
  *
4
- * Manages cloudflared tunnel lifecycle — quick tunnels (zero-config,
5
- * ephemeral) and named tunnels (persistent, custom domain).
4
+ * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md.
6
5
  *
7
- * Quick tunnels require no Cloudflare account. Named tunnels require
8
- * a tunnel token from the Cloudflare dashboard.
6
+ * Rewritten on top of the foundation modules:
7
+ * - TunnelProvider — backend abstraction; concrete providers under
8
+ * src/tunnel/Cloudflare*Provider.ts.
9
+ * - TunnelLifecycle — single-writer CAS-guarded state machine.
10
+ * - TunnelNotifier — two-channel routing (group / owner-DM) of
11
+ * transition events.
9
12
  *
10
- * The tunnel exposes the agent's local HTTP server to the internet,
11
- * enabling:
12
- * - Private content viewing (auth-gated rendered markdown)
13
- * - Remote API access from anywhere
14
- * - File serving (logs, reports, exports)
15
- * - Webhook endpoints for external integrations
13
+ * The manager is the SOLE owner of the detect → attempt → fall-back →
14
+ * notify → self-heal lifecycle. The previous server.ts startup-retry
15
+ * ladder + background-retry ladder + Lifeline failure message are
16
+ * retired in favor of routing all retry through here (one backoff
17
+ * engine, not two).
18
+ *
19
+ * Scope of this rewrite (PR 2 of the chain):
20
+ * - Tier-1 provider pool (Cloudflare named → quick) with internal
21
+ * backoff between retries within an episode.
22
+ * - Post-start reachability probe (HTTP /health through the public
23
+ * URL) before declaring `active` — prevents broadcasting a "back
24
+ * online" link that doesn't actually serve traffic.
25
+ * - Backward-compatible public API: start(), stop(), forceStop(),
26
+ * enableAutoReconnect(), disableAutoReconnect(), getExternalUrl(),
27
+ * url/isRunning/state. Plus the existing events.
28
+ * - Notifier sink optional; when telegram adapter is plumbed,
29
+ * transition events route to the group topic.
30
+ *
31
+ * Out of scope for THIS PR (future PRs in the chain):
32
+ * - Tier-2 consent flow + relay providers (PR 4).
33
+ * - Owner-DM channel + inline-button consent UX (PR 3).
34
+ * - authToken/PIN rotation + boot recovery (PR 5).
35
+ * - Self-heal probe with N-consecutive-success stability gate (PR 6).
36
+ * - The /tunnel route (PR 7).
37
+ *
38
+ * The lifecycle state machine already supports these states; the
39
+ * manager simply doesn't transition into them yet in PR 2.
16
40
  */
17
41
  import { EventEmitter } from 'node:events';
42
+ import { type PersistedTunnelState } from './TunnelLifecycle.js';
43
+ import type { TunnelProvider, ProviderFailureReason } from './TunnelProvider.js';
44
+ import type { NotifierSink } from './TunnelNotifier.js';
18
45
  export interface TunnelConfig {
19
- /** Whether tunnel is enabled */
46
+ /** Whether tunnel is enabled. */
20
47
  enabled: boolean;
21
- /** Tunnel type: 'quick' (ephemeral, no account) or 'named' (persistent, requires token) */
48
+ /** Tunnel type: 'quick' (ephemeral, no account) or 'named' (persistent, requires token). */
22
49
  type: 'quick' | 'named';
23
- /** Cloudflare tunnel token (required for named tunnels using token auth) */
50
+ /** Cloudflare tunnel token (named, token-auth). */
24
51
  token?: string;
25
- /** Config file path for named tunnels using credentials file auth */
52
+ /** Config file path (named, config-file-auth). */
26
53
  configFile?: string;
27
- /** Public hostname for named tunnels (e.g., echo.dawn-tunnel.dev) */
54
+ /** Public hostname for named tunnels. */
28
55
  hostname?: string;
29
- /** Local port to tunnel to */
56
+ /** Local port to tunnel to. */
30
57
  port: number;
31
- /** State directory for persisting tunnel info */
58
+ /** State directory for persisting tunnel.json. */
32
59
  stateDir: string;
33
60
  }
34
61
  export interface TunnelState {
35
- /** Current tunnel URL (null if not connected) */
36
62
  url: string | null;
37
- /** Tunnel type */
38
63
  type: 'quick' | 'named';
39
- /** When the tunnel was started */
40
64
  startedAt: string | null;
41
- /** Connection info from cloudflared */
42
65
  connectionId?: string;
43
- /** Connection location */
44
66
  connectionLocation?: string;
45
67
  }
46
68
  export interface TunnelEvents {
@@ -54,60 +76,75 @@ export interface TunnelEvents {
54
76
  error: (error: Error) => void;
55
77
  stopped: () => void;
56
78
  }
79
+ /** Optional injections for testability. */
80
+ export interface TunnelManagerInjections {
81
+ providers?: TunnelProvider[];
82
+ notifierSink?: NotifierSink;
83
+ fetch?: typeof fetch;
84
+ }
57
85
  export declare class TunnelManager extends EventEmitter {
58
- private config;
59
- private tunnel;
60
- private stateFile;
61
- private _state;
62
- private _stopped;
86
+ private readonly config;
87
+ private readonly stateFile;
88
+ private readonly lifecycle;
89
+ private readonly providers;
90
+ private readonly notifier;
91
+ private readonly fetcher;
92
+ private currentHandle;
93
+ private currentProviderName;
94
+ private _legacyState;
63
95
  private _autoReconnect;
64
- private _reconnectTimer;
65
- private _reconnectAttempt;
96
+ private _stopped;
66
97
  private _startPromise;
67
- private static readonly MAX_RECONNECT_ATTEMPTS;
68
- private static readonly BASE_RECONNECT_DELAY_MS;
69
- private static readonly MAX_RECONNECT_DELAY_MS;
70
- constructor(config: TunnelConfig);
71
- /** Current tunnel URL, or null if not connected */
98
+ private _backoffAttempt;
99
+ private _backoffTimer;
100
+ private _postExhaustedTimer;
101
+ constructor(config: TunnelConfig, injections?: TunnelManagerInjections);
72
102
  get url(): string | null;
73
- /** Whether the tunnel is currently running */
74
103
  get isRunning(): boolean;
75
- /** Current tunnel state */
76
104
  get state(): TunnelState;
105
+ /** Additive new accessor — lifecycle snapshot. */
106
+ get lifecycleState(): PersistedTunnelState;
77
107
  /**
78
- * Start the tunnel. Ensures the cloudflared binary is installed,
79
- * then starts the appropriate tunnel type.
108
+ * Start the tunnel. Drives the full Tier-1 ladder internally — the
109
+ * caller does NOT wrap with its own retry loop (the old server.ts
110
+ * startup ladder is RETIRED).
111
+ *
112
+ * Resolves with the URL of the first provider that reaches `active`.
113
+ * Rejects when all Tier-1 providers fail and backoff is exhausted.
80
114
  */
81
115
  start(): Promise<string>;
82
- /**
83
- * Stop the tunnel gracefully. Intentional stops disable auto-reconnect.
84
- */
85
116
  stop(): Promise<void>;
86
- /**
87
- * Force-stop the tunnel with a timeout. If cloudflared doesn't respond to
88
- * SIGINT within `timeoutMs`, escalate to SIGKILL. Essential for sleep/wake
89
- * recovery where cloudflared may be a zombie process.
90
- */
91
- forceStop(timeoutMs?: number): Promise<void>;
92
- /**
93
- * Enable automatic reconnection when the tunnel disconnects unexpectedly.
94
- * Uses exponential backoff: 5s, 10s, 20s, ... up to 5 minutes, max 10 attempts.
95
- */
117
+ /** Force-stop with escalation. Providers internally do SIGINT → SIGKILL. */
118
+ forceStop(_timeoutMs?: number): Promise<void>;
96
119
  enableAutoReconnect(): void;
97
120
  disableAutoReconnect(): void;
121
+ getExternalUrl(localPath: string): string | null;
122
+ private buildDefaultPool;
123
+ private doStart;
124
+ private driveTier1;
125
+ private exhaustedOrBackoff;
98
126
  /**
99
- * Attempt to reconnect the tunnel with exponential backoff.
127
+ * Background retry — runs the bounded exponential ladder off the
128
+ * start() promise so initial start() resolves/rejects fast. After
129
+ * the ladder exhausts, hands off to the indefinite post-exhausted
130
+ * placeholder (the PR 2 self-heal; PR 6 replaces with the spec's
131
+ * N-consecutive-success probe).
100
132
  */
101
- private attemptReconnect;
133
+ private scheduleBackgroundRetry;
134
+ private clearBackoffTimer;
135
+ private clearPostExhaustedTimer;
102
136
  /**
103
- * Get the full external URL for a local path.
104
- * Returns null if tunnel is not connected.
137
+ * Minimum-viable post-exhausted retry (PR 2 self-heal placeholder).
138
+ * Schedules a single low-frequency probe; on completion (success or
139
+ * failure) it re-arms itself, so the agent keeps trying to recover
140
+ * even after the bounded startup-reconnect ladder gives up. PR 6
141
+ * replaces this with the spec's N-consecutive-success stability gate.
105
142
  */
106
- getExternalUrl(localPath: string): string | null;
107
- private ensureBinary;
108
- private startQuickTunnel;
109
- private startNamedTunnel;
110
- private startConfigFileTunnel;
111
- private saveState;
143
+ private schedulePostExhaustedRetry;
144
+ /** HTTP probe through the public URL — confirms the link actually serves. */
145
+ private probeReachability;
146
+ private persist;
147
+ private restorePersisted;
112
148
  }
149
+ export type { ProviderFailureReason };
113
150
  //# sourceMappingURL=TunnelManager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TunnelManager.d.ts","sourceRoot":"","sources":["../../src/tunnel/TunnelManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQ3C,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,2FAA2F;IAC3F,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,kBAAkB;IAClB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,kCAAkC;IAClC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,SAAS,EAAE,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAID,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAM;IACpD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAc;gBAEhD,MAAM,EAAE,YAAY;IAWhC,mDAAmD;IACnD,IAAI,GAAG,IAAI,MAAM,GAAG,IAAI,CAEvB;IAED,8CAA8C;IAC9C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,2BAA2B;IAC3B,IAAI,KAAK,IAAI,WAAW,CAEvB;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAyC9B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;;;OAIG;IACG,SAAS,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAuDhD;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAI3B,oBAAoB,IAAI,IAAI;IAS5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA+BxB;;;OAGG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;YASlC,YAAY;IAM1B,OAAO,CAAC,gBAAgB;IA2ExB,OAAO,CAAC,gBAAgB;IAuExB,OAAO,CAAC,qBAAqB;IAyF7B,OAAO,CAAC,SAAS;CAWlB"}
1
+ {"version":3,"file":"TunnelManager.d.ts","sourceRoot":"","sources":["../../src/tunnel/TunnelManager.ts"],"names":[],"mappings":"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"}