instar 1.2.39 → 1.2.41

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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SocketDisconnectSentinel — detects Claude Code's "socket connection closed
3
+ * unexpectedly" message in tracked sessions and runs a bounded recovery loop.
4
+ *
5
+ * Closes the gap surfaced by the joint diagnosis on 2026-05-22: the instar
6
+ * repo had detectors for rate limits, quota, hundreds of patterns, but zero
7
+ * detectors for Claude Code's own connection-drop string. When it fires, the
8
+ * session freezes; nothing classifies it; no recovery runs; no user-visible
9
+ * alert reaches the user.
10
+ *
11
+ * Pattern mirrors `RateLimitSentinel.report` → backoff → `resumeFn` →
12
+ * verification → escalate, but the failure shape is simpler (no Claude
13
+ * Code internal retries to wait out — disconnect is immediate) so the
14
+ * lifecycle is shorter.
15
+ *
16
+ * Spec: docs/specs/silently-stopped-trio.md
17
+ *
18
+ * Signal-vs-authority: the regex + state machine are detectors. The
19
+ * notifyFn path is the existing MessagingToneGate authority (called through
20
+ * /attention). No new blocking authority introduced.
21
+ */
22
+ import { EventEmitter } from 'node:events';
23
+ export type SocketDisconnectStatus = 'detected' | 'recovering' | 'recovered' | 'escalated';
24
+ export interface SocketDisconnectState {
25
+ sessionName: string;
26
+ detectedAt: number;
27
+ attempts: number;
28
+ lastInjectAt: number;
29
+ status: SocketDisconnectStatus;
30
+ }
31
+ export interface SocketDisconnectSentinelDeps {
32
+ /**
33
+ * Inject a recovery nudge into the session: typically Ctrl+C then Enter.
34
+ * Returns whether the injection was accepted.
35
+ */
36
+ resumeFn: (sessionName: string) => Promise<boolean>;
37
+ /** Route a user-facing message; server.ts owns topic routing. */
38
+ notifyFn: (sessionName: string, text: string) => Promise<void>;
39
+ /** Peek at the session's most recent tmux output. Returns a string that
40
+ * may be empty if the session has no recent output. */
41
+ getRecentOutput: (sessionName: string) => string;
42
+ /** Override Date.now (tests). */
43
+ now?: () => number;
44
+ /** Override timer setters (tests). */
45
+ setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
46
+ clearTimer?: (handle: ReturnType<typeof setTimeout>) => void;
47
+ }
48
+ export interface SocketDisconnectSentinelConfig {
49
+ enabled?: boolean;
50
+ /** Backoff staircase in ms between recovery attempts. Last value repeats. */
51
+ backoffScheduleMs?: number[];
52
+ /** Max recovery attempts before escalating. Default 4. */
53
+ maxAttempts?: number;
54
+ /** How long to wait after a nudge before declaring recovery (ms). Default 60s. */
55
+ verifyWindowMs?: number;
56
+ }
57
+ /**
58
+ * Patterns Claude Code uses when its connection to Anthropic drops. Broad
59
+ * intentionally — false-positive cost is one harmless nudge; false-negative
60
+ * cost is the silently-stopped failure class this sentinel exists to close.
61
+ */
62
+ export declare const SOCKET_DISCONNECT_PATTERNS: readonly RegExp[];
63
+ /**
64
+ * Detector — true if `text` contains any known disconnect pattern.
65
+ */
66
+ export declare function detectSocketDisconnect(text: string): boolean;
67
+ export declare class SocketDisconnectSentinel extends EventEmitter {
68
+ private readonly deps;
69
+ private readonly cfg;
70
+ private readonly states;
71
+ private readonly timers;
72
+ constructor(deps: SocketDisconnectSentinelDeps, cfg?: SocketDisconnectSentinelConfig);
73
+ /** Called by the tick loop (or by an event trigger) for each tracked session. */
74
+ scanSession(sessionName: string): void;
75
+ /**
76
+ * Public entry: report a detected disconnect. Idempotent — second call
77
+ * for the same session within an active recovery is a no-op.
78
+ */
79
+ report(sessionName: string): void;
80
+ isRecoveryActive(sessionName: string): boolean;
81
+ listActive(): SocketDisconnectState[];
82
+ /** Cancel any in-flight recovery for this session (test + shutdown helper). */
83
+ clear(sessionName: string): void;
84
+ /** Cancel all in-flight recovery (server shutdown). */
85
+ shutdown(): void;
86
+ private scheduleNextAttempt;
87
+ private runAttempt;
88
+ private verifyAttempt;
89
+ private escalate;
90
+ private notify;
91
+ private setTimer;
92
+ private clearTimer;
93
+ }
94
+ //# sourceMappingURL=SocketDisconnectSentinel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SocketDisconnectSentinel.d.ts","sourceRoot":"","sources":["../../src/monitoring/SocketDisconnectSentinel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,YAAY,GACZ,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,iEAAiE;IACjE,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D;2DACuD;IACvD,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IACzE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,8BAA8B;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AASD;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAKvD,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM5D;AAED,qBAAa,wBAAyB,SAAQ,YAAY;IAK5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA2C;IAC/D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;IACnE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoD;gBAE9C,IAAI,EAAE,4BAA4B,EAAE,GAAG,GAAE,8BAAmC;IAKzG,iFAAiF;IACjF,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAQtC;;;OAGG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAsBjC,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAK9C,UAAU,IAAI,qBAAqB,EAAE;IAIrC,+EAA+E;IAC/E,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAOhC,uDAAuD;IACvD,QAAQ,IAAI,IAAI;IAQhB,OAAO,CAAC,mBAAmB;YAcb,UAAU;IA2BxB,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,QAAQ;YAaF,MAAM;IAQpB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,UAAU;CAGnB"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * SocketDisconnectSentinel — detects Claude Code's "socket connection closed
3
+ * unexpectedly" message in tracked sessions and runs a bounded recovery loop.
4
+ *
5
+ * Closes the gap surfaced by the joint diagnosis on 2026-05-22: the instar
6
+ * repo had detectors for rate limits, quota, hundreds of patterns, but zero
7
+ * detectors for Claude Code's own connection-drop string. When it fires, the
8
+ * session freezes; nothing classifies it; no recovery runs; no user-visible
9
+ * alert reaches the user.
10
+ *
11
+ * Pattern mirrors `RateLimitSentinel.report` → backoff → `resumeFn` →
12
+ * verification → escalate, but the failure shape is simpler (no Claude
13
+ * Code internal retries to wait out — disconnect is immediate) so the
14
+ * lifecycle is shorter.
15
+ *
16
+ * Spec: docs/specs/silently-stopped-trio.md
17
+ *
18
+ * Signal-vs-authority: the regex + state machine are detectors. The
19
+ * notifyFn path is the existing MessagingToneGate authority (called through
20
+ * /attention). No new blocking authority introduced.
21
+ */
22
+ import { EventEmitter } from 'node:events';
23
+ const DEFAULT_CONFIG = {
24
+ enabled: true,
25
+ backoffScheduleMs: [60_000, 5 * 60_000, 15 * 60_000, 30 * 60_000],
26
+ maxAttempts: 4,
27
+ verifyWindowMs: 60_000,
28
+ };
29
+ /**
30
+ * Patterns Claude Code uses when its connection to Anthropic drops. Broad
31
+ * intentionally — false-positive cost is one harmless nudge; false-negative
32
+ * cost is the silently-stopped failure class this sentinel exists to close.
33
+ */
34
+ export const SOCKET_DISCONNECT_PATTERNS = [
35
+ /socket connection closed unexpectedly/i,
36
+ /websocket.*reset by peer/i,
37
+ /ECONNRESET.*claude/i,
38
+ /connection.*closed.*unexpectedly/i,
39
+ ];
40
+ /**
41
+ * Detector — true if `text` contains any known disconnect pattern.
42
+ */
43
+ export function detectSocketDisconnect(text) {
44
+ if (!text)
45
+ return false;
46
+ for (const pat of SOCKET_DISCONNECT_PATTERNS) {
47
+ if (pat.test(text))
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ export class SocketDisconnectSentinel extends EventEmitter {
53
+ deps;
54
+ cfg;
55
+ states = new Map();
56
+ timers = new Map();
57
+ constructor(deps, cfg = {}) {
58
+ super();
59
+ this.deps = deps;
60
+ this.cfg = { ...DEFAULT_CONFIG, ...cfg };
61
+ }
62
+ /** Called by the tick loop (or by an event trigger) for each tracked session. */
63
+ scanSession(sessionName) {
64
+ if (!this.cfg.enabled)
65
+ return;
66
+ if (this.states.has(sessionName))
67
+ return; // already handling
68
+ const output = this.deps.getRecentOutput(sessionName);
69
+ if (!detectSocketDisconnect(output))
70
+ return;
71
+ this.report(sessionName);
72
+ }
73
+ /**
74
+ * Public entry: report a detected disconnect. Idempotent — second call
75
+ * for the same session within an active recovery is a no-op.
76
+ */
77
+ report(sessionName) {
78
+ if (!this.cfg.enabled)
79
+ return;
80
+ if (this.states.has(sessionName))
81
+ return;
82
+ const now = (this.deps.now ?? Date.now)();
83
+ const state = {
84
+ sessionName,
85
+ detectedAt: now,
86
+ attempts: 0,
87
+ lastInjectAt: 0,
88
+ status: 'detected',
89
+ };
90
+ this.states.set(sessionName, state);
91
+ // First user notice — tone-gated path via notifyFn.
92
+ void this.notify(sessionName, `${friendlyName(sessionName)} lost its connection to Claude Code. Trying to recover; will let you know if it can't.`);
93
+ this.scheduleNextAttempt(sessionName);
94
+ }
95
+ isRecoveryActive(sessionName) {
96
+ const s = this.states.get(sessionName);
97
+ return !!s && s.status !== 'recovered' && s.status !== 'escalated';
98
+ }
99
+ listActive() {
100
+ return Array.from(this.states.values());
101
+ }
102
+ /** Cancel any in-flight recovery for this session (test + shutdown helper). */
103
+ clear(sessionName) {
104
+ const t = this.timers.get(sessionName);
105
+ if (t)
106
+ this.clearTimer(t);
107
+ this.timers.delete(sessionName);
108
+ this.states.delete(sessionName);
109
+ }
110
+ /** Cancel all in-flight recovery (server shutdown). */
111
+ shutdown() {
112
+ for (const t of this.timers.values())
113
+ this.clearTimer(t);
114
+ this.timers.clear();
115
+ this.states.clear();
116
+ }
117
+ // ── Recovery loop ──────────────────────────────────────────────────
118
+ scheduleNextAttempt(sessionName) {
119
+ const state = this.states.get(sessionName);
120
+ if (!state)
121
+ return;
122
+ if (state.attempts >= this.cfg.maxAttempts) {
123
+ this.escalate(sessionName);
124
+ return;
125
+ }
126
+ const idx = Math.min(state.attempts, this.cfg.backoffScheduleMs.length - 1);
127
+ const wait = this.cfg.backoffScheduleMs[idx];
128
+ state.status = 'recovering';
129
+ const handle = this.setTimer(() => this.runAttempt(sessionName), wait);
130
+ this.timers.set(sessionName, handle);
131
+ }
132
+ async runAttempt(sessionName) {
133
+ const state = this.states.get(sessionName);
134
+ if (!state)
135
+ return;
136
+ const now = (this.deps.now ?? Date.now)();
137
+ state.attempts += 1;
138
+ state.lastInjectAt = now;
139
+ let injected = false;
140
+ try {
141
+ injected = await this.deps.resumeFn(sessionName);
142
+ }
143
+ catch (err) {
144
+ this.emit('recovery-error', { sessionName, err });
145
+ injected = false;
146
+ }
147
+ if (!injected) {
148
+ // Nudge couldn't be delivered — escalate immediately, this isn't
149
+ // something a backoff staircase can fix.
150
+ this.escalate(sessionName);
151
+ return;
152
+ }
153
+ // Schedule a verify check.
154
+ const handle = this.setTimer(() => this.verifyAttempt(sessionName), this.cfg.verifyWindowMs);
155
+ this.timers.set(sessionName, handle);
156
+ }
157
+ verifyAttempt(sessionName) {
158
+ const state = this.states.get(sessionName);
159
+ if (!state)
160
+ return;
161
+ const output = this.deps.getRecentOutput(sessionName);
162
+ // Heuristic: if the disconnect pattern no longer appears in the recent
163
+ // output, the session has progressed past the freeze.
164
+ if (!detectSocketDisconnect(output)) {
165
+ state.status = 'recovered';
166
+ void this.notify(sessionName, `${friendlyName(sessionName)} is reconnected and back to work.`);
167
+ this.emit('recovered', sessionName);
168
+ this.clear(sessionName);
169
+ return;
170
+ }
171
+ // Still stuck → next attempt.
172
+ this.scheduleNextAttempt(sessionName);
173
+ }
174
+ escalate(sessionName) {
175
+ const state = this.states.get(sessionName);
176
+ if (!state)
177
+ return;
178
+ state.status = 'escalated';
179
+ const minutes = Math.max(1, Math.round(((this.deps.now ?? Date.now)() - state.detectedAt) / 60_000));
180
+ void this.notify(sessionName, `${friendlyName(sessionName)} has been disconnected for about ${minutes} minutes and my recovery attempts haven't worked. Want me to dig in?`);
181
+ this.emit('escalated', sessionName);
182
+ // Keep state so a subsequent scanSession doesn't re-report the same outage.
183
+ }
184
+ async notify(sessionName, text) {
185
+ try {
186
+ await this.deps.notifyFn(sessionName, text);
187
+ }
188
+ catch (err) {
189
+ this.emit('notify-error', { sessionName, err });
190
+ }
191
+ }
192
+ setTimer(fn, ms) {
193
+ return (this.deps.setTimer ?? setTimeout)(fn, ms);
194
+ }
195
+ clearTimer(handle) {
196
+ (this.deps.clearTimer ?? clearTimeout)(handle);
197
+ }
198
+ }
199
+ /** Strip prefix conventions for user-facing copy. Keeps B12 simple. */
200
+ function friendlyName(sessionName) {
201
+ return sessionName.replace(/^ai\.instar\./, '').replace(/-server$/, '').replace(/-lifeline$/, '');
202
+ }
203
+ //# sourceMappingURL=SocketDisconnectSentinel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SocketDisconnectSentinel.js","sourceRoot":"","sources":["../../src/monitoring/SocketDisconnectSentinel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * LocaltunnelProvider — Tier-2 consent-gated relay tunnel via *.loca.lt.
3
+ *
4
+ * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 1
5
+ * (provider abstraction, Tier 2) and Part 7 (supply-chain hardening).
6
+ *
7
+ * SECURITY POSTURE:
8
+ * - localtunnel routes the agent's local server traffic through
9
+ * loca.lt servers. The relay operator (and anyone who logs the URL)
10
+ * can see dashboard PIN + signed view URLs while the relay is active.
11
+ * This is exactly why the spec gates Tier-2 providers behind owner
12
+ * consent — see Part 3 (consent flow) and Part 6 (mandatory
13
+ * credential rotation on relay-episode end).
14
+ * - The agent never spins this up without an explicit per-episode
15
+ * consent record bound to the owner principal — that's enforced in
16
+ * TunnelManager's state machine, not in this provider.
17
+ *
18
+ * Supply-chain hardening (Part 7):
19
+ * - The npm package `localtunnel` is pinned at exact version in
20
+ * package.json (no caret range).
21
+ * - The provider spawns the localtunnel client via its programmatic
22
+ * API; a future hardening PR can switch to child-process isolation
23
+ * once the spec's checksum-verified install path is added.
24
+ * - `isAvailable()` resolves false if the npm package cannot be
25
+ * loaded, so a missing dep degrades gracefully (provider skipped)
26
+ * instead of crashing.
27
+ *
28
+ * The provider does NOT own retry/reconnect/notification — those are
29
+ * the manager's responsibility per the single-owner mandate (PR 2).
30
+ */
31
+ import type { TunnelProvider, TunnelProviderHandle, ProviderName, ProviderTier } from './TunnelProvider.js';
32
+ export interface LocaltunnelProviderOptions {
33
+ /** Local port the relay should point at. */
34
+ port: number;
35
+ /** Start timeout in milliseconds (default: 20_000). */
36
+ startTimeoutMs?: number;
37
+ /**
38
+ * Optional subdomain hint passed to localtunnel. When set, the
39
+ * service will try to allocate the requested name; when unavailable
40
+ * (already taken or rate-limited), localtunnel falls back to a
41
+ * random subdomain. NEVER use the agent's identity here — the
42
+ * subdomain is publicly visible on *.loca.lt.
43
+ */
44
+ subdomain?: string;
45
+ }
46
+ export declare class LocaltunnelProvider implements TunnelProvider {
47
+ readonly name: ProviderName;
48
+ readonly tier: ProviderTier;
49
+ private readonly port;
50
+ private readonly startTimeoutMs;
51
+ private readonly subdomain;
52
+ /**
53
+ * Cached dynamic-import result. The first `isAvailable()` call
54
+ * detects whether the npm package is present; subsequent calls reuse
55
+ * the cached resolution.
56
+ */
57
+ private _moduleResolved;
58
+ constructor(opts: LocaltunnelProviderOptions);
59
+ isAvailable(): Promise<boolean>;
60
+ start(localPort: number): Promise<TunnelProviderHandle>;
61
+ private loadModule;
62
+ }
63
+ //# sourceMappingURL=LocaltunnelProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocaltunnelProvider.d.ts","sourceRoot":"","sources":["../../src/tunnel/LocaltunnelProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACb,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,0BAA0B;IACzC,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD,qBAAa,mBAAoB,YAAW,cAAc;IACxD,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAiB;IAC5C,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAK;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C;;;;OAIG;IACH,OAAO,CAAC,eAAe,CAAgD;gBAE3D,IAAI,EAAE,0BAA0B;IAMtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAK/B,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;YA8D/C,UAAU;CAezB"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * LocaltunnelProvider — Tier-2 consent-gated relay tunnel via *.loca.lt.
3
+ *
4
+ * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 1
5
+ * (provider abstraction, Tier 2) and Part 7 (supply-chain hardening).
6
+ *
7
+ * SECURITY POSTURE:
8
+ * - localtunnel routes the agent's local server traffic through
9
+ * loca.lt servers. The relay operator (and anyone who logs the URL)
10
+ * can see dashboard PIN + signed view URLs while the relay is active.
11
+ * This is exactly why the spec gates Tier-2 providers behind owner
12
+ * consent — see Part 3 (consent flow) and Part 6 (mandatory
13
+ * credential rotation on relay-episode end).
14
+ * - The agent never spins this up without an explicit per-episode
15
+ * consent record bound to the owner principal — that's enforced in
16
+ * TunnelManager's state machine, not in this provider.
17
+ *
18
+ * Supply-chain hardening (Part 7):
19
+ * - The npm package `localtunnel` is pinned at exact version in
20
+ * package.json (no caret range).
21
+ * - The provider spawns the localtunnel client via its programmatic
22
+ * API; a future hardening PR can switch to child-process isolation
23
+ * once the spec's checksum-verified install path is added.
24
+ * - `isAvailable()` resolves false if the npm package cannot be
25
+ * loaded, so a missing dep degrades gracefully (provider skipped)
26
+ * instead of crashing.
27
+ *
28
+ * The provider does NOT own retry/reconnect/notification — those are
29
+ * the manager's responsibility per the single-owner mandate (PR 2).
30
+ */
31
+ export class LocaltunnelProvider {
32
+ name = 'localtunnel';
33
+ tier = 2;
34
+ port;
35
+ startTimeoutMs;
36
+ subdomain;
37
+ /**
38
+ * Cached dynamic-import result. The first `isAvailable()` call
39
+ * detects whether the npm package is present; subsequent calls reuse
40
+ * the cached resolution.
41
+ */
42
+ _moduleResolved;
43
+ constructor(opts) {
44
+ this.port = opts.port;
45
+ this.startTimeoutMs = opts.startTimeoutMs ?? 20_000;
46
+ this.subdomain = opts.subdomain;
47
+ }
48
+ async isAvailable() {
49
+ const mod = await this.loadModule();
50
+ return mod !== 'unavailable';
51
+ }
52
+ async start(localPort) {
53
+ const mod = await this.loadModule();
54
+ if (mod === 'unavailable') {
55
+ throw new Error('binary-missing: localtunnel npm package is not installed');
56
+ }
57
+ const lt = mod.default;
58
+ if (typeof lt !== 'function') {
59
+ throw new Error('binary-missing: localtunnel module did not expose the expected factory');
60
+ }
61
+ const port = localPort || this.port;
62
+ return new Promise((resolve, reject) => {
63
+ let resolved = false;
64
+ const timeout = setTimeout(() => {
65
+ if (resolved)
66
+ return;
67
+ resolved = true;
68
+ reject(new Error('timeout: localtunnel did not emit a URL within the start budget'));
69
+ }, this.startTimeoutMs);
70
+ lt({ port, subdomain: this.subdomain }).then((client) => {
71
+ if (resolved) {
72
+ try {
73
+ client.close();
74
+ }
75
+ catch { /* best effort */ }
76
+ return;
77
+ }
78
+ resolved = true;
79
+ clearTimeout(timeout);
80
+ if (!client.url) {
81
+ reject(new Error('process-exit: localtunnel returned no URL'));
82
+ return;
83
+ }
84
+ client.on('error', () => { });
85
+ resolve({
86
+ url: client.url,
87
+ stop: async () => {
88
+ try {
89
+ client.close();
90
+ }
91
+ catch { /* already dead */ }
92
+ },
93
+ });
94
+ }, (err) => {
95
+ if (resolved)
96
+ return;
97
+ resolved = true;
98
+ clearTimeout(timeout);
99
+ const msg = (err?.message ?? '').toLowerCase();
100
+ if (msg.includes('rate') || msg.includes('429')) {
101
+ reject(new Error(`rate-limited: localtunnel: ${err.message}`));
102
+ return;
103
+ }
104
+ if (msg.includes('econnrefused') || msg.includes('dns')) {
105
+ reject(new Error(`network: localtunnel: ${err.message}`));
106
+ return;
107
+ }
108
+ reject(new Error(`process-exit: localtunnel start failed: ${err.message}`));
109
+ });
110
+ });
111
+ }
112
+ async loadModule() {
113
+ if (this._moduleResolved !== undefined)
114
+ return this._moduleResolved;
115
+ try {
116
+ // Dynamic specifier so TypeScript doesn't require an installed
117
+ // `@types/localtunnel`. The npm package itself is an optional
118
+ // peer-style dep — agents without it installed degrade
119
+ // gracefully via the catch below.
120
+ const specifier = 'localtunnel';
121
+ const mod = (await import(/* @vite-ignore */ specifier));
122
+ this._moduleResolved = mod;
123
+ }
124
+ catch {
125
+ this._moduleResolved = 'unavailable';
126
+ }
127
+ return this._moduleResolved;
128
+ }
129
+ }
130
+ //# sourceMappingURL=LocaltunnelProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocaltunnelProvider.js","sourceRoot":"","sources":["../../src/tunnel/LocaltunnelProvider.ts"],"names":[],"mappings":"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"}
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import { EventEmitter } from 'node:events';
42
42
  import { type PersistedTunnelState } from './TunnelLifecycle.js';
43
- import type { TunnelProvider, ProviderFailureReason } from './TunnelProvider.js';
43
+ import type { TunnelProvider, ProviderName, ProviderFailureReason } from './TunnelProvider.js';
44
44
  import type { NotifierSink } from './TunnelNotifier.js';
45
45
  export interface TunnelConfig {
46
46
  /** Whether tunnel is enabled. */
@@ -110,6 +110,13 @@ export declare class TunnelManager extends EventEmitter {
110
110
  private _backoffAttempt;
111
111
  private _backoffTimer;
112
112
  private _postExhaustedTimer;
113
+ /**
114
+ * Pending consent record — populated when the manager enters
115
+ * `awaiting-consent` and cleared on grant / decline / timeout / stop.
116
+ * The nonce is the CSPRNG token sent to the owner; matching it on
117
+ * `grantConsent()` is the security-load-bearing check.
118
+ */
119
+ private _pendingConsent;
113
120
  constructor(config: TunnelConfig, injections?: TunnelManagerInjections);
114
121
  get url(): string | null;
115
122
  get isRunning(): boolean;
@@ -154,6 +161,42 @@ export declare class TunnelManager extends EventEmitter {
154
161
  private doStart;
155
162
  private driveTier1;
156
163
  private exhaustedOrBackoff;
164
+ /** First Tier-2 provider that reports available, or null. */
165
+ private findAvailableTier2;
166
+ /**
167
+ * Internal — called after the lifecycle transitions to
168
+ * `awaiting-consent`. Generates the one-time nonce, stores the
169
+ * pending consent record, and arms the timeout.
170
+ */
171
+ private requestConsent;
172
+ private clearPendingConsent;
173
+ /**
174
+ * Public — called by the consent UX layer (Telegram callback handler
175
+ * in PR 6) after the owner approves. Validates the nonce matches the
176
+ * pending consent record, starts the Tier-2 provider, and transitions
177
+ * to `relay-active`. Returns true on success, false if the nonce
178
+ * didn't match (replay, race, stale click) or the state moved beyond
179
+ * awaiting-consent.
180
+ */
181
+ grantConsent(nonce: string): Promise<boolean>;
182
+ /**
183
+ * Public — called by the consent UX layer when the owner declines.
184
+ * Validates the nonce, applies the cross-episode cooldown, and
185
+ * transitions to `exhausted` + background retry.
186
+ */
187
+ declineConsent(nonce: string): boolean;
188
+ private recordConsentDecline;
189
+ /**
190
+ * Public — the active pending-consent record (or null). Used by the
191
+ * consent UX layer (PR 6) to know what nonce to embed in the inline
192
+ * button.
193
+ */
194
+ get pendingConsent(): {
195
+ episodeId: string;
196
+ provider: ProviderName;
197
+ nonce: string;
198
+ issuedAt: number;
199
+ } | null;
157
200
  /**
158
201
  * Background retry — runs the bounded exponential ladder off the
159
202
  * start() promise so initial start() resolves/rejects fast. After
@@ -1 +1 @@
1
- {"version":3,"file":"TunnelManager.d.ts","sourceRoot":"","sources":["../../src/tunnel/TunnelManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TunnelManager.d.ts","sourceRoot":"","sources":["../../src/tunnel/TunnelManager.ts"],"names":[],"mappings":"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"}