@mycadet/cli 0.12.7-dev.202610111049.d2b52a8 → 0.12.8
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/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +2 -2
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/package.json +4 -3
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/ps-cloud-status.test.ts +53 -0
- package/src/__tests__/retire.test.ts +37 -178
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
- package/src/__tests__/tunnel.test.ts +127 -80
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/wake.test.ts +16 -9
- package/src/commands/client.ts +1 -1
- package/src/commands/ps.ts +181 -72
- package/src/commands/retire.ts +28 -137
- package/src/commands/tunnel.ts +76 -118
- package/src/commands/upgrade.ts +2 -9
- package/src/commands/wake.ts +5 -9
- package/src/lib/assistant-config.ts +53 -0
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/ingress-config.ts +7 -34
- package/src/lib/local.ts +6 -27
- package/src/lib/nginx-ingress.ts +29 -8
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +200 -29
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
package/src/lib/nginx-ingress.ts
CHANGED
|
@@ -19,8 +19,10 @@ import { join } from "node:path";
|
|
|
19
19
|
import { cloudAssistantHubUrl } from "@mycadet/environments";
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
+
clearRootProcess,
|
|
22
23
|
getAssistantDisplayName,
|
|
23
24
|
lookupAssistantByIdentifier,
|
|
25
|
+
stampRootProcess,
|
|
24
26
|
type AssistantEntry,
|
|
25
27
|
} from "./assistant-config.js";
|
|
26
28
|
import type { RemoteHost } from "./constants.js";
|
|
@@ -360,7 +362,7 @@ http {
|
|
|
360
362
|
# remote client cannot smuggle one. Every caller reaches this loopback-only
|
|
361
363
|
# listener through the TLS-terminating front (tunnel agent), so the raw peer
|
|
362
364
|
# is always 127.0.0.1; the front records the real client as the RIGHTMOST
|
|
363
|
-
# X-Forwarded-For entry (
|
|
365
|
+
# X-Forwarded-For entry (tunnel providers append one; tailscale serve sets it),
|
|
364
366
|
# which the remote client cannot control. Fall back to the raw peer when the
|
|
365
367
|
# front sets no X-Forwarded-For.
|
|
366
368
|
map $http_x_forwarded_for $cadet_edge_client_ip {
|
|
@@ -494,7 +496,8 @@ export function getNginxVersion(): string | null {
|
|
|
494
496
|
* process.ts helpers: stopProcessByPidFile's isCadetProcess() guard only
|
|
495
497
|
* matches command lines containing a cadet path, which fails for a custom
|
|
496
498
|
* CADET_WORKSPACE_DIR and would silently leave nginx running (the same
|
|
497
|
-
* reason local.ts
|
|
499
|
+
* reason local.ts stops the tunnel agent directly). This module is also
|
|
500
|
+
* imported by
|
|
498
501
|
* sleep/retire, whose tests mock.module() process.js process-globally —
|
|
499
502
|
* depending on it here would couple this lib's behavior to those mocks.
|
|
500
503
|
*/
|
|
@@ -625,13 +628,15 @@ function clearIngressState(workspaceDir: string): void {
|
|
|
625
628
|
function clearStoppedIngress(workspaceDir: string, pidPath: string): void {
|
|
626
629
|
clearIngressState(workspaceDir);
|
|
627
630
|
rmSync(pidPath, { force: true });
|
|
631
|
+
// The edge is gone; drop the root lockfile's process entry with it.
|
|
632
|
+
clearRootProcess("app");
|
|
628
633
|
}
|
|
629
634
|
|
|
630
635
|
/**
|
|
631
|
-
* Write the nginx config and spawn nginx detached (
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
636
|
+
* Write the nginx config and spawn nginx detached (stdout/stderr to a log
|
|
637
|
+
* file, fd closed after spawn, caller unrefs). nginx runs with `daemon off`
|
|
638
|
+
* so the spawned process is the master; it writes its pid to nginx.pid under
|
|
639
|
+
* the prefix dir.
|
|
635
640
|
*/
|
|
636
641
|
export function startIngressNginx(opts: {
|
|
637
642
|
workspaceDir: string;
|
|
@@ -1035,8 +1040,22 @@ export async function stopContainerTunnelEdge(
|
|
|
1035
1040
|
return stopIngressNginx(workspaceDir);
|
|
1036
1041
|
}
|
|
1037
1042
|
|
|
1038
|
-
/**
|
|
1039
|
-
|
|
1043
|
+
/** Record the running edge's pid in the root lockfile for `cadet ps`. */
|
|
1044
|
+
function stampAppProcessEntry(
|
|
1045
|
+
workspaceDir: string,
|
|
1046
|
+
assistantId?: string,
|
|
1047
|
+
): void {
|
|
1048
|
+
const pid = getIngressPid(workspaceDir);
|
|
1049
|
+
if (pid !== null) {
|
|
1050
|
+
stampRootProcess("app", {
|
|
1051
|
+
pid,
|
|
1052
|
+
startedAt: new Date().toISOString(),
|
|
1053
|
+
assistantId,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Resolved edge a tunnel (or bring-your-own HTTPS front) should target. */ export interface TunnelEdge {
|
|
1040
1059
|
/** Loopback listen port the HTTPS front should forward to. */
|
|
1041
1060
|
port: number;
|
|
1042
1061
|
/** True when this call started the edge, false when a running one was reused. */
|
|
@@ -1099,6 +1118,7 @@ export async function ensureTunnelEdge(opts: {
|
|
|
1099
1118
|
|
|
1100
1119
|
switch (result.status) {
|
|
1101
1120
|
case "started":
|
|
1121
|
+
stampAppProcessEntry(opts.workspaceDir, opts.assistantId);
|
|
1102
1122
|
return {
|
|
1103
1123
|
port: result.listenPort,
|
|
1104
1124
|
started: true,
|
|
@@ -1132,6 +1152,7 @@ export async function ensureTunnelEdge(opts: {
|
|
|
1132
1152
|
stuckEdgeRecoveryHint(opts.workspaceDir),
|
|
1133
1153
|
);
|
|
1134
1154
|
}
|
|
1155
|
+
stampAppProcessEntry(opts.workspaceDir, opts.assistantId);
|
|
1135
1156
|
return {
|
|
1136
1157
|
port: result.listenPort,
|
|
1137
1158
|
started: false,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { ChildProcess } from "child_process";
|
|
2
|
-
|
|
3
|
-
import { selectRestartTunnelRecord } from "@mycadet/service-contracts/ingress";
|
|
4
|
-
|
|
5
1
|
import {
|
|
2
|
+
clearRootProcess,
|
|
6
3
|
loadAllAssistants,
|
|
7
4
|
loadAllAssistantsAcrossEnvs,
|
|
5
|
+
readRootProcesses,
|
|
8
6
|
type AssistantEntry,
|
|
7
|
+
type RootProcessRecord,
|
|
9
8
|
} from "./assistant-config.js";
|
|
10
9
|
|
|
11
10
|
import {
|
|
@@ -14,7 +13,6 @@ import {
|
|
|
14
13
|
loadRawConfig,
|
|
15
14
|
parseGatewayPortFromEntryUrls,
|
|
16
15
|
} from "./ingress-config.js";
|
|
17
|
-
import { IngressConfigSchema } from "./ngrok.js";
|
|
18
16
|
import {
|
|
19
17
|
ensureTunnelEdge,
|
|
20
18
|
formatEdgeMode,
|
|
@@ -23,8 +21,187 @@ import {
|
|
|
23
21
|
type TunnelEdge,
|
|
24
22
|
} from "./nginx-ingress.js";
|
|
25
23
|
import { waitForDaemonReady } from "./http-client.js";
|
|
26
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
hasWebhookIntegrations,
|
|
26
|
+
IngressConfigSchema,
|
|
27
|
+
isNgrokProcess,
|
|
28
|
+
maybeStartNgrokTunnel,
|
|
29
|
+
NGROK_TUNNEL_META,
|
|
30
|
+
runNgrokTunnel,
|
|
31
|
+
saveNgrokDomain,
|
|
32
|
+
} from "./tunnels/ngrok.js";
|
|
33
|
+
import { stopProcess } from "./process.js";
|
|
27
34
|
import { shellArg } from "./shell-arg.js";
|
|
35
|
+
import { notifyGatewayOfPublicUrl } from "./tunnels/gateway-notify.js";
|
|
36
|
+
import {
|
|
37
|
+
selectRestartTunnelRecord,
|
|
38
|
+
type TunnelProviderName,
|
|
39
|
+
} from "@mycadet/service-contracts/ingress";
|
|
40
|
+
import { runCadetTunnel, CADET_TUNNEL_META } from "./tunnels/cadet.js";
|
|
41
|
+
import {
|
|
42
|
+
runCloudflareTunnel,
|
|
43
|
+
CLOUDFLARE_TUNNEL_META,
|
|
44
|
+
} from "./tunnels/cloudflare.js";
|
|
45
|
+
import {
|
|
46
|
+
runTailscaleTunnel,
|
|
47
|
+
TAILSCALE_TUNNEL_META,
|
|
48
|
+
} from "./tunnels/tailscale.js";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The provider the automatic tunnel paths start when no provider is named.
|
|
52
|
+
* ngrok today; the north star has the managed `cadet` tunnel as the eventual
|
|
53
|
+
* default, at which point only this constant flips.
|
|
54
|
+
*/
|
|
55
|
+
export const DEFAULT_TUNNEL_PROVIDER: TunnelProviderName = "ngrok";
|
|
56
|
+
|
|
57
|
+
/** Per-provider copy `cadet tunnel`'s help renders. */
|
|
58
|
+
export interface ProviderMeta {
|
|
59
|
+
/** How far the tunnel URL reaches. */
|
|
60
|
+
reach: string;
|
|
61
|
+
/** Install instruction line, rendered under the provider entry. */
|
|
62
|
+
install: string;
|
|
63
|
+
/** Extra behavior notes, rendered under the install line. */
|
|
64
|
+
notes?: string[];
|
|
65
|
+
/** Example reserved-domain value, for providers that support `--domain`. */
|
|
66
|
+
domainExample?: string;
|
|
67
|
+
/** Whether `--domain`/`--clear-domain` can bind a reserved domain. */
|
|
68
|
+
supportsReservedDomain: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Listed but not yet implemented: the command refuses it before starting
|
|
71
|
+
* any infrastructure, so `--provider <name>` fails fast with the
|
|
72
|
+
* coming-soon copy instead of building an edge nothing will use.
|
|
73
|
+
*/
|
|
74
|
+
comingSoon?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The registry: each provider's copy lives in its own `tunnels/<provider>.ts`
|
|
79
|
+
* module and is assembled here, so `cadet tunnel`'s help, `--provider`
|
|
80
|
+
* validation, and dispatch all read one shape.
|
|
81
|
+
*/
|
|
82
|
+
export const PROVIDER_META: Record<TunnelProviderName, ProviderMeta> = {
|
|
83
|
+
cadet: CADET_TUNNEL_META,
|
|
84
|
+
ngrok: NGROK_TUNNEL_META,
|
|
85
|
+
cloudflare: CLOUDFLARE_TUNNEL_META,
|
|
86
|
+
tailscale: TAILSCALE_TUNNEL_META,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registry check that tolerates any provider spelling: only providers whose
|
|
91
|
+
* meta claims reserved-domain support accept `--domain`/`--clear-domain`.
|
|
92
|
+
*/
|
|
93
|
+
export function providerSupportsReservedDomain(provider: string): boolean {
|
|
94
|
+
for (const [name, meta] of Object.entries(PROVIDER_META)) {
|
|
95
|
+
if (name === provider) return meta.supportsReservedDomain;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Options every provider tunnel accepts; `runProviderTunnel` maps them. */
|
|
101
|
+
export interface ProviderTunnelOptions {
|
|
102
|
+
port: number;
|
|
103
|
+
assistantId?: string;
|
|
104
|
+
workspaceDir: string;
|
|
105
|
+
/** Reserved domain to bind (providers that support one only). */
|
|
106
|
+
domain?: string;
|
|
107
|
+
/** Tailnet-only runs keep the webhook callback base as it is. */
|
|
108
|
+
preserveIngressUrl?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* What every tunnel provider's run hands back: the public URL it established
|
|
113
|
+
* or adopted, and the promise that settles when the run ends. Conformance is
|
|
114
|
+
* the point — the tunnel level can notify the gateway the same way no matter
|
|
115
|
+
* which provider ran.
|
|
116
|
+
*/
|
|
117
|
+
export interface ProviderTunnelResult {
|
|
118
|
+
publicUrl: string;
|
|
119
|
+
waitForExit: Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run a tunnel provider by name — the dispatch `cadet tunnel` uses, so the
|
|
124
|
+
* command layer never names a provider implementation directly. Notifies the
|
|
125
|
+
* gateway of the resulting public URL identically for every provider, then
|
|
126
|
+
* holds until the run ends.
|
|
127
|
+
*/
|
|
128
|
+
export async function runProviderTunnel(
|
|
129
|
+
provider: TunnelProviderName,
|
|
130
|
+
opts: ProviderTunnelOptions,
|
|
131
|
+
): Promise<{ publicUrl: string }> {
|
|
132
|
+
const result = await runProvider(provider, opts);
|
|
133
|
+
await notifyGatewayOfPublicUrl(result.publicUrl, opts.assistantId);
|
|
134
|
+
await result.waitForExit;
|
|
135
|
+
return { publicUrl: result.publicUrl };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Map the provider name onto its run — no gateway notification here. */
|
|
139
|
+
function runProvider(
|
|
140
|
+
provider: TunnelProviderName,
|
|
141
|
+
opts: ProviderTunnelOptions,
|
|
142
|
+
): Promise<ProviderTunnelResult> {
|
|
143
|
+
const base = {
|
|
144
|
+
port: opts.port,
|
|
145
|
+
assistantId: opts.assistantId,
|
|
146
|
+
workspaceDir: opts.workspaceDir,
|
|
147
|
+
};
|
|
148
|
+
if (provider === "ngrok") {
|
|
149
|
+
return runNgrokTunnel({
|
|
150
|
+
...base,
|
|
151
|
+
...(opts.domain ? { domain: opts.domain } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (provider === "cadet") {
|
|
155
|
+
return runCadetTunnel(opts);
|
|
156
|
+
}
|
|
157
|
+
if (provider === "cloudflare") {
|
|
158
|
+
return runCloudflareTunnel(base);
|
|
159
|
+
}
|
|
160
|
+
return runTailscaleTunnel({
|
|
161
|
+
...base,
|
|
162
|
+
...(opts.preserveIngressUrl
|
|
163
|
+
? { preserveIngressUrl: opts.preserveIngressUrl }
|
|
164
|
+
: {}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Drop the saved reserved-domain record (providers that keep one). */
|
|
169
|
+
export function clearReservedDomain(): void {
|
|
170
|
+
saveNgrokDomain(null);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Stop a tunnel agent this CLI started, per the root lockfile's `tunnel`
|
|
175
|
+
* entry. The pid is verified against the provider's process identity before
|
|
176
|
+
* signaling — a stale entry after PID reuse must not kill an unrelated
|
|
177
|
+
* process.
|
|
178
|
+
*/
|
|
179
|
+
export async function stopTunnelProcess(): Promise<void> {
|
|
180
|
+
const recorded: RootProcessRecord | undefined = readRootProcesses().tunnel;
|
|
181
|
+
if (!recorded) return;
|
|
182
|
+
if (isNgrokProcess(recorded.pid)) {
|
|
183
|
+
await stopProcess(recorded.pid, "tunnel");
|
|
184
|
+
}
|
|
185
|
+
clearRootProcess("tunnel");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The `cadet tunnel --detach` copy for a run that adopted an already-running
|
|
190
|
+
* agent: killing the supervisor does not stop a tunnel it did not spawn.
|
|
191
|
+
*/
|
|
192
|
+
export function adoptedTunnelStopNotice(pid: number | undefined): string {
|
|
193
|
+
return (
|
|
194
|
+
`That ngrok agent was already running outside this command, so \`kill ${pid}\` only ` +
|
|
195
|
+
"stops this supervisor, not the ngrok tunnel or its saved ingress URL. Stop the ngrok agent " +
|
|
196
|
+
"directly to end the tunnel."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
ADOPTED_TUNNEL_RE,
|
|
202
|
+
hasWebhookIntegrationsConfigured,
|
|
203
|
+
} from "./tunnels/ngrok.js";
|
|
204
|
+
export { cadetTunnelComingSoonError } from "./tunnels/cadet.js";
|
|
28
205
|
|
|
29
206
|
/** Matches the Docker hatch path's service-readiness allowance. */
|
|
30
207
|
export const DOCKER_GATEWAY_READY_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -221,8 +398,9 @@ function ownsSharedIngress(
|
|
|
221
398
|
* survived across sleep (a tailnet serve, a reserved ngrok domain) reaches the
|
|
222
399
|
* gateway again without a manual `cadet tunnel`.
|
|
223
400
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
401
|
+
* The auto-tunnel runs here too, matching the local wake path: the spawned
|
|
402
|
+
* agent records itself in the root lockfile under `tunnel`, so `cadet ps` and
|
|
403
|
+
* the stop paths can see it instead of it leaking past the next sleep.
|
|
226
404
|
*/
|
|
227
405
|
export async function restoreContainerTunnelEdge(
|
|
228
406
|
entry: AssistantEntry,
|
|
@@ -257,20 +435,30 @@ export async function restoreContainerTunnelEdge(
|
|
|
257
435
|
);
|
|
258
436
|
return;
|
|
259
437
|
}
|
|
260
|
-
await
|
|
438
|
+
await restoreTunnelEdgeAndAutoTunnel(
|
|
439
|
+
entry.assistantId,
|
|
440
|
+
gatewayPort,
|
|
441
|
+
workspaceDir,
|
|
442
|
+
);
|
|
261
443
|
}
|
|
262
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Restore the nginx edge, then start the default provider's tunnel at it
|
|
447
|
+
* (the gateway port when the edge is down, keeping webhook delivery alive).
|
|
448
|
+
* The spawned agent records itself in the root lockfile under `tunnel`;
|
|
449
|
+
* adopting an already-running one clears any stale entry.
|
|
450
|
+
*/
|
|
263
451
|
export async function restoreTunnelEdgeAndAutoTunnel(
|
|
264
452
|
assistantId: string,
|
|
265
453
|
gatewayPort: number,
|
|
266
454
|
workspaceDir: string,
|
|
267
|
-
): Promise<
|
|
455
|
+
): Promise<void> {
|
|
268
456
|
const edgePort = await restoreTunnelEdge(
|
|
269
457
|
assistantId,
|
|
270
458
|
gatewayPort,
|
|
271
459
|
workspaceDir,
|
|
272
460
|
);
|
|
273
|
-
|
|
461
|
+
await maybeStartNgrokTunnel(
|
|
274
462
|
edgePort ?? gatewayPort,
|
|
275
463
|
workspaceDir,
|
|
276
464
|
assistantId,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { STALE_CLI_UPDATE_HINT } from "../stale-cli-hint.js";
|
|
2
|
+
import type { ProviderMeta, ProviderTunnelOptions } from "../tunnel.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The managed `cadet` tunnel. It is listed in the shared registry so the
|
|
6
|
+
* command's help and `--provider` validation speak its name, but the tunnel
|
|
7
|
+
* itself ships with Cadet Cloud — until then every run refuses loudly.
|
|
8
|
+
*/
|
|
9
|
+
export const CADET_TUNNEL_META: ProviderMeta = {
|
|
10
|
+
reach: "Managed tunnel via Cadet Cloud (coming soon)",
|
|
11
|
+
install: "no install needed — included with Cadet Cloud",
|
|
12
|
+
supportsReservedDomain: false,
|
|
13
|
+
comingSoon: true,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Copy every refusing path shares, so the wording cannot drift apart. */
|
|
17
|
+
export function cadetTunnelComingSoonError(provider: string): Error {
|
|
18
|
+
return new Error(
|
|
19
|
+
`Tunnel provider '${provider}' is not yet implemented. ` +
|
|
20
|
+
`If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Refuse the run: the managed tunnel is not something the CLI can start. */
|
|
25
|
+
export async function runCadetTunnel(
|
|
26
|
+
_opts: ProviderTunnelOptions,
|
|
27
|
+
): Promise<never> {
|
|
28
|
+
throw cadetTunnelComingSoonError("cadet");
|
|
29
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { ProviderMeta, ProviderTunnelResult } from "../tunnel.js";
|
|
2
|
+
|
|
1
3
|
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
2
4
|
|
|
3
|
-
import { GATEWAY_PORT } from "
|
|
5
|
+
import { GATEWAY_PORT } from "../constants.js";
|
|
4
6
|
import {
|
|
5
7
|
clearIngressUrl,
|
|
6
8
|
getDefaultWorkspaceDir,
|
|
7
9
|
saveIngressUrl,
|
|
8
|
-
} from "
|
|
10
|
+
} from "../ingress-config.js";
|
|
9
11
|
|
|
10
12
|
// ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
|
|
11
13
|
|
|
@@ -135,7 +137,7 @@ export interface RunCloudflareTunnelOptions {
|
|
|
135
137
|
|
|
136
138
|
export async function runCloudflareTunnel(
|
|
137
139
|
opts: RunCloudflareTunnelOptions = {},
|
|
138
|
-
): Promise<
|
|
140
|
+
): Promise<ProviderTunnelResult> {
|
|
139
141
|
const version = getCloudflareTunnelVersion();
|
|
140
142
|
if (!version) {
|
|
141
143
|
console.error("Error: cloudflared is not installed.");
|
|
@@ -226,7 +228,16 @@ export async function runCloudflareTunnel(
|
|
|
226
228
|
console.log("Press Ctrl+C to stop the tunnel and clear the ingress URL.");
|
|
227
229
|
|
|
228
230
|
// Keep running until cloudflared exits (e.g., network error or user Ctrl+C)
|
|
229
|
-
|
|
231
|
+
const waitForExit = new Promise<void>((resolve) => {
|
|
230
232
|
child.on("exit", () => resolve());
|
|
231
233
|
});
|
|
234
|
+
return { publicUrl, waitForExit };
|
|
232
235
|
}
|
|
236
|
+
|
|
237
|
+
/** Per-provider copy `cadet tunnel`'s help renders for cloudflare. */
|
|
238
|
+
export const CLOUDFLARE_TUNNEL_META: ProviderMeta = {
|
|
239
|
+
reach: "Public tunnel that webhook integrations can reach",
|
|
240
|
+
install: "install: brew install cloudflare/cloudflare/cloudflared",
|
|
241
|
+
notes: ["No Cloudflare account required for quick tunnels."],
|
|
242
|
+
supportsReservedDomain: false,
|
|
243
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunnel-level gateway notification: once ANY tunnel provider has a public
|
|
3
|
+
* URL, the gateway learns it here — ngrok-agnostic on purpose, so every
|
|
4
|
+
* provider conforms by returning its `publicUrl` and the dispatch (`cadet
|
|
5
|
+
* tunnel`'s tunnel level, or the daemon's auto-tunnel) calls this.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { lookupAssistantByIdentifier } from "../assistant-config.js";
|
|
9
|
+
import { GATEWAY_PORT } from "../constants.js";
|
|
10
|
+
import { loadGuardianToken } from "../guardian-token.js";
|
|
11
|
+
import { loopbackSafeFetch } from "../loopback-fetch.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where the gateway for this run's assistant answers. The entry's recorded
|
|
15
|
+
* loopback URL when one resolves; the global gateway port otherwise.
|
|
16
|
+
*/
|
|
17
|
+
function gatewayUrlFor(assistantId: string | undefined): string {
|
|
18
|
+
if (assistantId) {
|
|
19
|
+
const result = lookupAssistantByIdentifier(assistantId);
|
|
20
|
+
if (result.status === "found") {
|
|
21
|
+
const url = result.entry.localUrl ?? result.entry.runtimeUrl;
|
|
22
|
+
if (url) return url;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return `http://127.0.0.1:${GATEWAY_PORT}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tell the gateway the tunnel's public URL so it records the ingress domain
|
|
30
|
+
* in its own config, the same way its own tunnel machinery does. Best-effort:
|
|
31
|
+
* a gateway that is down never fails the tunnel that fronts it.
|
|
32
|
+
*/
|
|
33
|
+
export async function notifyGatewayOfPublicUrl(
|
|
34
|
+
publicUrl: string,
|
|
35
|
+
assistantId: string | undefined,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
try {
|
|
38
|
+
const token = assistantId
|
|
39
|
+
? loadGuardianToken(assistantId)?.accessToken
|
|
40
|
+
: undefined;
|
|
41
|
+
const res = await loopbackSafeFetch(
|
|
42
|
+
`${gatewayUrlFor(assistantId)}/v1/ingress/domains`,
|
|
43
|
+
{
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({ publicBaseUrl: publicUrl }),
|
|
50
|
+
signal: AbortSignal.timeout(5_000),
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`gateway responded ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn(
|
|
58
|
+
` ⚠ Could not record the ingress domain with the gateway: ${err instanceof Error ? err.message : String(err)}. The tunnel still runs.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|