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.
- package/dist/core/WorktreeManager.d.ts +11 -0
- package/dist/core/WorktreeManager.d.ts.map +1 -1
- package/dist/core/WorktreeManager.js +69 -0
- package/dist/core/WorktreeManager.js.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +88 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js +181 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts +94 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -0
- package/dist/monitoring/SocketDisconnectSentinel.js +203 -0
- package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -0
- package/dist/tunnel/LocaltunnelProvider.d.ts +63 -0
- package/dist/tunnel/LocaltunnelProvider.d.ts.map +1 -0
- package/dist/tunnel/LocaltunnelProvider.js +130 -0
- package/dist/tunnel/LocaltunnelProvider.js.map +1 -0
- package/dist/tunnel/TunnelManager.d.ts +44 -1
- package/dist/tunnel/TunnelManager.d.ts.map +1 -1
- package/dist/tunnel/TunnelManager.js +199 -15
- package/dist/tunnel/TunnelManager.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.2.40.md +134 -0
- package/upgrades/1.2.41.md +39 -0
- package/upgrades/side-effects/consent-state-machine.md +135 -0
- package/upgrades/side-effects/localtunnel-provider.md +95 -0
- package/upgrades/side-effects/silently-stopped-trio.md +115 -0
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA4C3C,MAAM,cAAc,GAA6C;IAC/D,OAAO,EAAE,IAAI;IACb,iBAAiB,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,GAAG,MAAM,CAAC;IACjE,WAAW,EAAE,CAAC;IACd,cAAc,EAAE,MAAM;CACvB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IAC3D,wCAAwC;IACxC,2BAA2B;IAC3B,qBAAqB;IACrB,mCAAmC;CACpC,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,0BAA0B,EAAE,CAAC;QAC7C,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IAClC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,OAAO,wBAAyB,SAAQ,YAAY;IAK3B;IAJZ,GAAG,CAA2C;IAC9C,MAAM,GAAG,IAAI,GAAG,EAAiC,CAAC;IAClD,MAAM,GAAG,IAAI,GAAG,EAAyC,CAAC;IAE3E,YAA6B,IAAkC,EAAE,MAAsC,EAAE;QACvG,KAAK,EAAE,CAAC;QADmB,SAAI,GAAJ,IAAI,CAA8B;QAE7D,IAAI,CAAC,GAAG,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,iFAAiF;IACjF,WAAW,CAAC,WAAmB;QAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,OAAO;QAC9B,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,CAAC,mBAAmB;QAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACtD,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC;YAAE,OAAO;QAC5C,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,WAAmB;QACxB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,OAAO;QAC9B,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO;QACzC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,KAAK,GAA0B;YACnC,WAAW;YACX,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,CAAC;YACX,YAAY,EAAE,CAAC;YACf,MAAM,EAAE,UAAU;SACnB,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAEpC,oDAAoD;QACpD,KAAK,IAAI,CAAC,MAAM,CACd,WAAW,EACX,GAAG,YAAY,CAAC,WAAW,CAAC,wFAAwF,CACrH,CAAC;QAEF,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC;IAED,gBAAgB,CAAC,WAAmB;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC;IACrE,CAAC;IAED,UAAU;QACR,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,WAAmB;QACvB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvC,IAAI,CAAC;YAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IAED,uDAAuD;IACvD,QAAQ;QACN,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED,sEAAsE;IAE9D,mBAAmB,CAAC,WAAmB;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAC3C,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5E,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;QAC7C,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,WAAmB;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;QACpB,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC;QAEzB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;YAClD,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;QAED,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,iEAAiE;YACjE,yCAAyC;YACzC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,2BAA2B;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC7F,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;IAEO,aAAa,CAAC,WAAmB;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACtD,uEAAuE;QACvE,sDAAsD;QACtD,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,EAAE,CAAC;YACpC,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC;YAC3B,KAAK,IAAI,CAAC,MAAM,CACd,WAAW,EACX,GAAG,YAAY,CAAC,WAAW,CAAC,mCAAmC,CAChE,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;YACpC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YACxB,OAAO;QACT,CAAC;QACD,8BAA8B;QAC9B,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC;IAEO,QAAQ,CAAC,WAAmB;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;QACrG,KAAK,IAAI,CAAC,MAAM,CACd,WAAW,EACX,GAAG,YAAY,CAAC,WAAW,CAAC,oCAAoC,OAAO,sEAAsE,CAC9I,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QACpC,4EAA4E;IAC9E,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,WAAmB,EAAE,IAAY;QACpD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,EAAc,EAAE,EAAU;QACzC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;IAEO,UAAU,CAAC,MAAqC;QACtD,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;CACF;AAED,uEAAuE;AACvE,SAAS,YAAY,CAAC,WAAmB;IACvC,OAAO,WAAW,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACpG,CAAC"}
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,
|
|
1
|
+
{"version":3,"file":"TunnelManager.d.ts","sourceRoot":"","sources":["../../src/tunnel/TunnelManager.ts"],"names":[],"mappings":"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"}
|