@tomflow/proflow-dev-tunnel 0.1.20 → 0.1.22
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/CHANGELOG.md +12 -0
- package/README.md +5 -3
- package/SETUP.md +1 -1
- package/dist/deployment/adapter.d.ts +24 -23
- package/dist/deployment/adapter.js +55 -36
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/cli-resolver.d.ts +7 -0
- package/dist/src/cli-resolver.js +145 -0
- package/dist/src/resource-adapter.d.ts +2 -1
- package/dist/src/resource-adapter.js +30 -14
- package/package.json +2 -3
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-dev-tunnel
|
|
2
2
|
|
|
3
|
+
## 0.1.22
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Accept Microsoft Dev Tunnel canonical cluster suffixes while preserving the deterministic workspace Tunnel identity.
|
|
8
|
+
|
|
9
|
+
## 0.1.21
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Integrate package-local Microsoft Dev Tunnel CLI ownership and deterministic Fresh Tunnel recovery.
|
|
14
|
+
|
|
3
15
|
## 0.1.20
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ Deployment Governance
|
|
|
23
23
|
|
|
24
24
|
## Purpose
|
|
25
25
|
|
|
26
|
-
Manages the Microsoft Dev Tunnel (`devtunnel`) public HTTPS ingress.
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
Manages the Microsoft Dev Tunnel (`devtunnel`) public HTTPS ingress. The Module
|
|
27
|
+
owns acquisition of the pinned official CLI into its own package-local `.devtunnel/`
|
|
28
|
+
directory and never resolves a system PATH `devtunnel`. Existing login is reused;
|
|
29
|
+
when login is unavailable, the only human action is GitHub browser authorization
|
|
30
|
+
initiated by the package-owned CLI. Account state is never faked.
|
package/SETUP.md
CHANGED
|
@@ -7,14 +7,15 @@ type SetupState = {
|
|
|
7
7
|
phase: SetupPhase;
|
|
8
8
|
gatewayPort?: number;
|
|
9
9
|
publicBaseUrl?: string;
|
|
10
|
-
cliPath?: string;
|
|
11
10
|
};
|
|
12
11
|
type RuntimeFactory = (input: {
|
|
13
12
|
command?: string;
|
|
14
13
|
tunnelId?: string;
|
|
15
14
|
publicBaseUrl?: string;
|
|
16
15
|
processStateFile?: string;
|
|
16
|
+
loginVerified?: boolean;
|
|
17
17
|
}) => DevTunnelRuntime;
|
|
18
|
+
export declare function workspaceTunnelId(workspaceRoot: string): string;
|
|
18
19
|
export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
19
20
|
automation?: DevTunnelAutomation;
|
|
20
21
|
createRuntime?: RuntimeFactory;
|
|
@@ -27,7 +28,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
27
28
|
readonly ok: true;
|
|
28
29
|
readonly status: "SUCCEEDED";
|
|
29
30
|
readonly moduleRef: "dev-tunnel";
|
|
30
|
-
readonly moduleVersion: "0.1.
|
|
31
|
+
readonly moduleVersion: "0.1.22";
|
|
31
32
|
};
|
|
32
33
|
observedEffects: never[];
|
|
33
34
|
}>;
|
|
@@ -37,14 +38,14 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
37
38
|
readonly ok: true;
|
|
38
39
|
readonly status: "SUCCEEDED";
|
|
39
40
|
readonly moduleRef: "dev-tunnel";
|
|
40
|
-
readonly moduleVersion: "0.1.
|
|
41
|
+
readonly moduleVersion: "0.1.22";
|
|
41
42
|
};
|
|
42
43
|
observedEffects: string[];
|
|
43
44
|
} | {
|
|
44
45
|
result: {
|
|
45
46
|
contract: "deployment.result.v1";
|
|
46
47
|
moduleRef: "dev-tunnel";
|
|
47
|
-
moduleVersion: "0.1.
|
|
48
|
+
moduleVersion: "0.1.22";
|
|
48
49
|
ok: false;
|
|
49
50
|
status: "FAILED";
|
|
50
51
|
error: {
|
|
@@ -61,7 +62,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
61
62
|
ok: true;
|
|
62
63
|
status: "SUCCEEDED";
|
|
63
64
|
moduleRef: "dev-tunnel";
|
|
64
|
-
moduleVersion: "0.1.
|
|
65
|
+
moduleVersion: "0.1.22";
|
|
65
66
|
data: {
|
|
66
67
|
setupStatus: "ACTION_REQUIRED" | "READY";
|
|
67
68
|
runtimeStatus: "FAILED" | "RUNNING" | "STOPPED";
|
|
@@ -88,7 +89,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
88
89
|
ok: true;
|
|
89
90
|
status: "SUCCEEDED";
|
|
90
91
|
moduleRef: "dev-tunnel";
|
|
91
|
-
moduleVersion: "0.1.
|
|
92
|
+
moduleVersion: "0.1.22";
|
|
92
93
|
data: {
|
|
93
94
|
docs: string;
|
|
94
95
|
};
|
|
@@ -99,7 +100,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
99
100
|
result: {
|
|
100
101
|
contract: "deployment.result.v1";
|
|
101
102
|
moduleRef: "dev-tunnel";
|
|
102
|
-
moduleVersion: "0.1.
|
|
103
|
+
moduleVersion: "0.1.22";
|
|
103
104
|
ok: false;
|
|
104
105
|
status: "FAILED";
|
|
105
106
|
error: {
|
|
@@ -115,7 +116,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
115
116
|
ok: true;
|
|
116
117
|
status: "SUCCEEDED";
|
|
117
118
|
moduleRef: "dev-tunnel";
|
|
118
|
-
moduleVersion: "0.1.
|
|
119
|
+
moduleVersion: "0.1.22";
|
|
119
120
|
data: import("../src/resource-adapter.ts").DevTunnelObservation;
|
|
120
121
|
};
|
|
121
122
|
observedEffects: string[];
|
|
@@ -126,14 +127,14 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
126
127
|
readonly ok: true;
|
|
127
128
|
readonly status: "SUCCEEDED";
|
|
128
129
|
readonly moduleRef: "dev-tunnel";
|
|
129
|
-
readonly moduleVersion: "0.1.
|
|
130
|
+
readonly moduleVersion: "0.1.22";
|
|
130
131
|
};
|
|
131
132
|
observedEffects: string[];
|
|
132
133
|
} | {
|
|
133
134
|
result: {
|
|
134
135
|
contract: "deployment.result.v1";
|
|
135
136
|
moduleRef: "dev-tunnel";
|
|
136
|
-
moduleVersion: "0.1.
|
|
137
|
+
moduleVersion: "0.1.22";
|
|
137
138
|
ok: false;
|
|
138
139
|
status: "FAILED";
|
|
139
140
|
error: {
|
|
@@ -148,7 +149,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
148
149
|
result: {
|
|
149
150
|
contract: "deployment.result.v1";
|
|
150
151
|
moduleRef: "dev-tunnel";
|
|
151
|
-
moduleVersion: "0.1.
|
|
152
|
+
moduleVersion: "0.1.22";
|
|
152
153
|
ok: false;
|
|
153
154
|
status: "FAILED";
|
|
154
155
|
error: {
|
|
@@ -164,7 +165,7 @@ export declare function createDevTunnelBehaviorAdapter(dependencies?: {
|
|
|
164
165
|
ok: true;
|
|
165
166
|
status: "SUCCEEDED";
|
|
166
167
|
moduleRef: "dev-tunnel";
|
|
167
|
-
moduleVersion: "0.1.
|
|
168
|
+
moduleVersion: "0.1.22";
|
|
168
169
|
data: SetupState;
|
|
169
170
|
};
|
|
170
171
|
observedEffects: string[];
|
|
@@ -177,7 +178,7 @@ export declare const behaviorAdapter: {
|
|
|
177
178
|
readonly ok: true;
|
|
178
179
|
readonly status: "SUCCEEDED";
|
|
179
180
|
readonly moduleRef: "dev-tunnel";
|
|
180
|
-
readonly moduleVersion: "0.1.
|
|
181
|
+
readonly moduleVersion: "0.1.22";
|
|
181
182
|
};
|
|
182
183
|
observedEffects: never[];
|
|
183
184
|
}>;
|
|
@@ -187,14 +188,14 @@ export declare const behaviorAdapter: {
|
|
|
187
188
|
readonly ok: true;
|
|
188
189
|
readonly status: "SUCCEEDED";
|
|
189
190
|
readonly moduleRef: "dev-tunnel";
|
|
190
|
-
readonly moduleVersion: "0.1.
|
|
191
|
+
readonly moduleVersion: "0.1.22";
|
|
191
192
|
};
|
|
192
193
|
observedEffects: string[];
|
|
193
194
|
} | {
|
|
194
195
|
result: {
|
|
195
196
|
contract: "deployment.result.v1";
|
|
196
197
|
moduleRef: "dev-tunnel";
|
|
197
|
-
moduleVersion: "0.1.
|
|
198
|
+
moduleVersion: "0.1.22";
|
|
198
199
|
ok: false;
|
|
199
200
|
status: "FAILED";
|
|
200
201
|
error: {
|
|
@@ -211,7 +212,7 @@ export declare const behaviorAdapter: {
|
|
|
211
212
|
ok: true;
|
|
212
213
|
status: "SUCCEEDED";
|
|
213
214
|
moduleRef: "dev-tunnel";
|
|
214
|
-
moduleVersion: "0.1.
|
|
215
|
+
moduleVersion: "0.1.22";
|
|
215
216
|
data: {
|
|
216
217
|
setupStatus: "ACTION_REQUIRED" | "READY";
|
|
217
218
|
runtimeStatus: "FAILED" | "RUNNING" | "STOPPED";
|
|
@@ -238,7 +239,7 @@ export declare const behaviorAdapter: {
|
|
|
238
239
|
ok: true;
|
|
239
240
|
status: "SUCCEEDED";
|
|
240
241
|
moduleRef: "dev-tunnel";
|
|
241
|
-
moduleVersion: "0.1.
|
|
242
|
+
moduleVersion: "0.1.22";
|
|
242
243
|
data: {
|
|
243
244
|
docs: string;
|
|
244
245
|
};
|
|
@@ -249,7 +250,7 @@ export declare const behaviorAdapter: {
|
|
|
249
250
|
result: {
|
|
250
251
|
contract: "deployment.result.v1";
|
|
251
252
|
moduleRef: "dev-tunnel";
|
|
252
|
-
moduleVersion: "0.1.
|
|
253
|
+
moduleVersion: "0.1.22";
|
|
253
254
|
ok: false;
|
|
254
255
|
status: "FAILED";
|
|
255
256
|
error: {
|
|
@@ -265,7 +266,7 @@ export declare const behaviorAdapter: {
|
|
|
265
266
|
ok: true;
|
|
266
267
|
status: "SUCCEEDED";
|
|
267
268
|
moduleRef: "dev-tunnel";
|
|
268
|
-
moduleVersion: "0.1.
|
|
269
|
+
moduleVersion: "0.1.22";
|
|
269
270
|
data: import("../src/resource-adapter.ts").DevTunnelObservation;
|
|
270
271
|
};
|
|
271
272
|
observedEffects: string[];
|
|
@@ -276,14 +277,14 @@ export declare const behaviorAdapter: {
|
|
|
276
277
|
readonly ok: true;
|
|
277
278
|
readonly status: "SUCCEEDED";
|
|
278
279
|
readonly moduleRef: "dev-tunnel";
|
|
279
|
-
readonly moduleVersion: "0.1.
|
|
280
|
+
readonly moduleVersion: "0.1.22";
|
|
280
281
|
};
|
|
281
282
|
observedEffects: string[];
|
|
282
283
|
} | {
|
|
283
284
|
result: {
|
|
284
285
|
contract: "deployment.result.v1";
|
|
285
286
|
moduleRef: "dev-tunnel";
|
|
286
|
-
moduleVersion: "0.1.
|
|
287
|
+
moduleVersion: "0.1.22";
|
|
287
288
|
ok: false;
|
|
288
289
|
status: "FAILED";
|
|
289
290
|
error: {
|
|
@@ -298,7 +299,7 @@ export declare const behaviorAdapter: {
|
|
|
298
299
|
result: {
|
|
299
300
|
contract: "deployment.result.v1";
|
|
300
301
|
moduleRef: "dev-tunnel";
|
|
301
|
-
moduleVersion: "0.1.
|
|
302
|
+
moduleVersion: "0.1.22";
|
|
302
303
|
ok: false;
|
|
303
304
|
status: "FAILED";
|
|
304
305
|
error: {
|
|
@@ -314,7 +315,7 @@ export declare const behaviorAdapter: {
|
|
|
314
315
|
ok: true;
|
|
315
316
|
status: "SUCCEEDED";
|
|
316
317
|
moduleRef: "dev-tunnel";
|
|
317
|
-
moduleVersion: "0.1.
|
|
318
|
+
moduleVersion: "0.1.22";
|
|
318
319
|
data: SetupState;
|
|
319
320
|
};
|
|
320
321
|
observedEffects: string[];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { resolveDevTunnelCli } from "@tomflow/proflow-devtunnel-cli";
|
|
5
5
|
import { readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
|
|
6
6
|
import { createDevTunnelAutomation, createDevTunnelRuntime, verifyProvisionedPublicBaseUrl, } from "../src/resource-adapter.js";
|
|
7
|
+
import { devTunnelCliPath, resolveDevTunnelCli } from "../src/cli-resolver.js";
|
|
7
8
|
import { descriptor } from "./descriptor.js";
|
|
8
9
|
const base = {
|
|
9
10
|
contract: "deployment.result.v1",
|
|
@@ -47,7 +48,6 @@ async function readState(context) {
|
|
|
47
48
|
Number(gatewayPort) > 65_535))
|
|
48
49
|
return undefined;
|
|
49
50
|
const publicBaseUrl = raw.publicBaseUrl;
|
|
50
|
-
const cliPath = raw.cliPath;
|
|
51
51
|
if (raw.phase === "READY") {
|
|
52
52
|
if (typeof publicBaseUrl !== "string")
|
|
53
53
|
return undefined;
|
|
@@ -61,7 +61,6 @@ async function readState(context) {
|
|
|
61
61
|
phase: raw.phase,
|
|
62
62
|
...(typeof gatewayPort === "number" ? { gatewayPort } : {}),
|
|
63
63
|
...(typeof publicBaseUrl === "string" ? { publicBaseUrl } : {}),
|
|
64
|
-
...(typeof cliPath === "string" ? { cliPath } : {}),
|
|
65
64
|
};
|
|
66
65
|
}
|
|
67
66
|
catch {
|
|
@@ -77,7 +76,7 @@ async function writeState(context, state) {
|
|
|
77
76
|
}
|
|
78
77
|
function runtime(context, state) {
|
|
79
78
|
return createDevTunnelRuntime({
|
|
80
|
-
|
|
79
|
+
command: devTunnelCliPath(),
|
|
81
80
|
...(state
|
|
82
81
|
? { tunnelId: state.tunnelId, publicBaseUrl: state.publicBaseUrl }
|
|
83
82
|
: {}),
|
|
@@ -235,22 +234,22 @@ const baseBehaviorAdapter = {
|
|
|
235
234
|
observedEffects: [],
|
|
236
235
|
};
|
|
237
236
|
const rt = runtime(context, state);
|
|
238
|
-
if ((await rt.loginStatus()) !== "LOGGED_IN")
|
|
239
|
-
return {
|
|
240
|
-
result: {
|
|
241
|
-
...base,
|
|
242
|
-
ok: false,
|
|
243
|
-
status: "FAILED",
|
|
244
|
-
error: {
|
|
245
|
-
code: "START_FAILED",
|
|
246
|
-
message: "Microsoft Dev Tunnel login is not ready",
|
|
247
|
-
retryable: true,
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
observedEffects: [],
|
|
251
|
-
};
|
|
252
237
|
try {
|
|
253
238
|
const observed = await rt.start();
|
|
239
|
+
if (observed.login !== "LOGGED_IN")
|
|
240
|
+
return {
|
|
241
|
+
result: {
|
|
242
|
+
...base,
|
|
243
|
+
ok: false,
|
|
244
|
+
status: "FAILED",
|
|
245
|
+
error: {
|
|
246
|
+
code: "START_FAILED",
|
|
247
|
+
message: "Microsoft Dev Tunnel login is not ready",
|
|
248
|
+
retryable: true,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
observedEffects: [],
|
|
252
|
+
};
|
|
254
253
|
return observed.state === "RUNNING"
|
|
255
254
|
? {
|
|
256
255
|
result: { ...base, data: observed },
|
|
@@ -343,6 +342,13 @@ function gatewayPort(facts) {
|
|
|
343
342
|
throw new Error("agent-gateway localBaseUrl shared fact is invalid");
|
|
344
343
|
return port;
|
|
345
344
|
}
|
|
345
|
+
export function workspaceTunnelId(workspaceRoot) {
|
|
346
|
+
const digest = createHash("sha256")
|
|
347
|
+
.update(resolve(workspaceRoot))
|
|
348
|
+
.digest("hex")
|
|
349
|
+
.slice(0, 24);
|
|
350
|
+
return `proflow-${digest}`;
|
|
351
|
+
}
|
|
346
352
|
const setupFailed = (error) => ({
|
|
347
353
|
result: {
|
|
348
354
|
...base,
|
|
@@ -364,11 +370,10 @@ export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
|
364
370
|
setup: async (context) => {
|
|
365
371
|
try {
|
|
366
372
|
const previous = await readState(context);
|
|
367
|
-
const cliPath =
|
|
368
|
-
(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
(async (workspaceRoot) => (await resolveDevTunnelCli({ workspaceRoot })).command))(context.workspaceRoot));
|
|
373
|
+
const cliPath = dependencies?.automation
|
|
374
|
+
? devTunnelCliPath()
|
|
375
|
+
: await (dependencies?.resolveCli ??
|
|
376
|
+
(async () => (await resolveDevTunnelCli()).command))(context.workspaceRoot);
|
|
372
377
|
const automation = dependencies?.automation ??
|
|
373
378
|
createDevTunnelAutomation({ command: cliPath });
|
|
374
379
|
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
@@ -376,20 +381,33 @@ export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
|
376
381
|
await automation.ensureLogin();
|
|
377
382
|
let tunnelId;
|
|
378
383
|
let safeToStartNewHost = false;
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
throw new Error("
|
|
384
|
+
const stableTunnelId = workspaceTunnelId(context.workspaceRoot);
|
|
385
|
+
const createAndVerifyTunnel = async (targetTunnelId) => {
|
|
386
|
+
const createdTunnelId = await automation.createTunnel(targetTunnelId);
|
|
387
|
+
if (createdTunnelId !== targetTunnelId)
|
|
388
|
+
throw new Error("Dev Tunnel create returned an unexpected tunnelId");
|
|
384
389
|
await writeState(context, {
|
|
385
390
|
contract: "proflow.dev-tunnel-setup.v2",
|
|
386
391
|
tunnelId: createdTunnelId,
|
|
387
392
|
phase: "PENDING_CREATED",
|
|
388
393
|
gatewayPort: port,
|
|
389
|
-
cliPath,
|
|
390
394
|
});
|
|
395
|
+
const created = await automation.inspectTunnel(createdTunnelId);
|
|
396
|
+
if (created.state !== "EXISTS")
|
|
397
|
+
throw new Error("new Dev Tunnel could not be verified by show --json");
|
|
391
398
|
return createdTunnelId;
|
|
392
399
|
};
|
|
400
|
+
const resolveStableTunnel = async () => {
|
|
401
|
+
const inspected = await automation.inspectTunnel(stableTunnelId);
|
|
402
|
+
if (inspected.state === "UNKNOWN")
|
|
403
|
+
throw new Error("workspace Tunnel remote state is UNKNOWN");
|
|
404
|
+
if (inspected.state === "EXISTS")
|
|
405
|
+
return { tunnelId: stableTunnelId, hostState: inspected.hostState };
|
|
406
|
+
return {
|
|
407
|
+
tunnelId: await createAndVerifyTunnel(stableTunnelId),
|
|
408
|
+
hostState: "STOPPED",
|
|
409
|
+
};
|
|
410
|
+
};
|
|
393
411
|
if (previous) {
|
|
394
412
|
const inspected = await automation.inspectTunnel(previous.tunnelId);
|
|
395
413
|
if (inspected.state === "UNKNOWN")
|
|
@@ -399,13 +417,15 @@ export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
|
399
417
|
safeToStartNewHost = inspected.hostState === "STOPPED";
|
|
400
418
|
}
|
|
401
419
|
else {
|
|
402
|
-
|
|
403
|
-
|
|
420
|
+
const resolved = await resolveStableTunnel();
|
|
421
|
+
tunnelId = resolved.tunnelId;
|
|
422
|
+
safeToStartNewHost = resolved.hostState === "STOPPED";
|
|
404
423
|
}
|
|
405
424
|
}
|
|
406
425
|
else {
|
|
407
|
-
|
|
408
|
-
|
|
426
|
+
const resolved = await resolveStableTunnel();
|
|
427
|
+
tunnelId = resolved.tunnelId;
|
|
428
|
+
safeToStartNewHost = resolved.hostState === "STOPPED";
|
|
409
429
|
}
|
|
410
430
|
await automation.ensurePort(tunnelId, port);
|
|
411
431
|
await writeState(context, {
|
|
@@ -413,11 +433,12 @@ export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
|
413
433
|
tunnelId,
|
|
414
434
|
phase: "PORT_READY",
|
|
415
435
|
gatewayPort: port,
|
|
416
|
-
cliPath,
|
|
417
436
|
});
|
|
418
437
|
const host = createRuntime({
|
|
438
|
+
command: cliPath,
|
|
419
439
|
tunnelId,
|
|
420
440
|
processStateFile: processFile(context),
|
|
441
|
+
loginVerified: true,
|
|
421
442
|
});
|
|
422
443
|
const observed = await host.status();
|
|
423
444
|
let started = false;
|
|
@@ -434,7 +455,6 @@ export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
|
434
455
|
tunnelId,
|
|
435
456
|
phase: "HOST_READY",
|
|
436
457
|
gatewayPort: port,
|
|
437
|
-
cliPath,
|
|
438
458
|
});
|
|
439
459
|
const publicBaseUrl = await automation.discoverPublicBaseUrl(tunnelId, port);
|
|
440
460
|
const url = new URL(publicBaseUrl);
|
|
@@ -447,7 +467,6 @@ export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
|
447
467
|
phase: "READY",
|
|
448
468
|
gatewayPort: port,
|
|
449
469
|
publicBaseUrl: url.href,
|
|
450
|
-
cliPath,
|
|
451
470
|
};
|
|
452
471
|
await writeState(context, state);
|
|
453
472
|
await writeModuleSharedFacts(context, descriptor.moduleRef, {
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "dev-tunnel";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-dev-tunnel";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.22";
|
|
7
7
|
readonly kind: "external-resource";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "dev-tunnel",
|
|
5
5
|
packageName: "@tomflow/proflow-dev-tunnel",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.22",
|
|
7
7
|
kind: "external-resource",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { arch, platform } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
export const MANAGED_DEV_TUNNEL_VERSION = "1.0.2030";
|
|
9
|
+
const VERSION_PROBE_TIMEOUT_MS = 30_000;
|
|
10
|
+
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
11
|
+
const COMPATIBLE_VERSION = new RegExp(`^${MANAGED_DEV_TUNNEL_VERSION.replaceAll(".", "\\.")}$`);
|
|
12
|
+
function artifact() {
|
|
13
|
+
const key = `${platform()}-${arch()}`;
|
|
14
|
+
const artifacts = {
|
|
15
|
+
"darwin-x64": {
|
|
16
|
+
url: "https://aka.ms/TunnelsCliDownload/osx-x64-zip",
|
|
17
|
+
archive: "zip",
|
|
18
|
+
executable: "devtunnel",
|
|
19
|
+
sha256: "4a7d426df3edb63441f24f0b465d3642da284c982e88eed9a410033379fad33f",
|
|
20
|
+
},
|
|
21
|
+
"darwin-arm64": {
|
|
22
|
+
url: "https://aka.ms/TunnelsCliDownload/osx-arm64-zip",
|
|
23
|
+
archive: "zip",
|
|
24
|
+
executable: "devtunnel",
|
|
25
|
+
sha256: "41794a24ccee6c5dc2821fadc97285ee39894fdbd96ad9869ee62ae0d7c5c8ed",
|
|
26
|
+
},
|
|
27
|
+
"linux-x64": {
|
|
28
|
+
url: "https://aka.ms/TunnelsCliDownload/linux-x64",
|
|
29
|
+
archive: "binary",
|
|
30
|
+
executable: "devtunnel",
|
|
31
|
+
sha256: "ff6911548907b5abaea4ed5baa36b2420be7c5debcb637a4f50f7a4002b10b60",
|
|
32
|
+
},
|
|
33
|
+
"win32-x64": {
|
|
34
|
+
url: "https://aka.ms/TunnelsCliDownload/win-x64",
|
|
35
|
+
archive: "binary",
|
|
36
|
+
executable: "devtunnel.exe",
|
|
37
|
+
sha256: "78190ac81c664828858de2390fd9c15eeec0e4edfe4e7b0d6a323dc49df625db",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const selected = artifacts[key];
|
|
41
|
+
if (!selected)
|
|
42
|
+
throw new Error(`DEV_TUNNEL_PLATFORM_UNSUPPORTED: ${key}`);
|
|
43
|
+
return selected;
|
|
44
|
+
}
|
|
45
|
+
function packageRoot() {
|
|
46
|
+
const parent = dirname(import.meta.dirname);
|
|
47
|
+
return basename(parent) === "dist" ? dirname(parent) : parent;
|
|
48
|
+
}
|
|
49
|
+
export function devTunnelCliPath() {
|
|
50
|
+
const selected = artifact();
|
|
51
|
+
return join(packageRoot(), ".devtunnel", MANAGED_DEV_TUNNEL_VERSION, `${platform()}-${arch()}`, selected.executable);
|
|
52
|
+
}
|
|
53
|
+
async function version(command) {
|
|
54
|
+
try {
|
|
55
|
+
const result = await execute(command, ["--version"], {
|
|
56
|
+
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
57
|
+
});
|
|
58
|
+
return `${result.stdout}\n${result.stderr}`.match(/\d+\.\d+\.\d+/)?.[0];
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function sha256(path) {
|
|
65
|
+
return createHash("sha256").update(await readFile(path)).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
async function validCached(path) {
|
|
68
|
+
try {
|
|
69
|
+
const metadata = JSON.parse(await readFile(`${path}.json`, "utf8"));
|
|
70
|
+
if (typeof metadata !== "object" || metadata === null)
|
|
71
|
+
return false;
|
|
72
|
+
const expected = Reflect.get(metadata, "sha256");
|
|
73
|
+
return (typeof expected === "string" &&
|
|
74
|
+
expected === (await sha256(path)) &&
|
|
75
|
+
COMPATIBLE_VERSION.test((await version(path)) ?? ""));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function downloadManaged() {
|
|
82
|
+
const selected = artifact();
|
|
83
|
+
const target = devTunnelCliPath();
|
|
84
|
+
const directory = dirname(target);
|
|
85
|
+
if (await validCached(target))
|
|
86
|
+
return target;
|
|
87
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
88
|
+
let response;
|
|
89
|
+
try {
|
|
90
|
+
response = await fetch(selected.url, {
|
|
91
|
+
redirect: "follow",
|
|
92
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new Error(`DEV_TUNNEL_DOWNLOAD_FAILED: ${error instanceof Error ? error.message : "request failed"}`);
|
|
97
|
+
}
|
|
98
|
+
if (!response.ok)
|
|
99
|
+
throw new Error(`DEV_TUNNEL_DOWNLOAD_FAILED: HTTP ${response.status}`);
|
|
100
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
101
|
+
const temporary = `${target}.${process.pid}.download`;
|
|
102
|
+
const extraction = `${directory}/extract-${process.pid}`;
|
|
103
|
+
try {
|
|
104
|
+
await writeFile(temporary, bytes, { mode: 0o600 });
|
|
105
|
+
if ((await sha256(temporary)) !== selected.sha256)
|
|
106
|
+
throw new Error("DEV_TUNNEL_DOWNLOAD_CHECKSUM_MISMATCH");
|
|
107
|
+
if (selected.archive === "zip") {
|
|
108
|
+
await mkdir(extraction, { recursive: true, mode: 0o700 });
|
|
109
|
+
await execute("unzip", ["-oq", temporary, "-d", extraction], {
|
|
110
|
+
timeout: 30_000,
|
|
111
|
+
});
|
|
112
|
+
const extracted = join(extraction, selected.executable);
|
|
113
|
+
await stat(extracted);
|
|
114
|
+
await rename(extracted, target);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await rename(temporary, target);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await rm(temporary, { force: true });
|
|
122
|
+
await rm(extraction, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
if (platform() !== "win32")
|
|
125
|
+
await chmod(target, 0o700);
|
|
126
|
+
const observedVersion = await version(target);
|
|
127
|
+
if (!observedVersion || !COMPATIBLE_VERSION.test(observedVersion))
|
|
128
|
+
throw new Error(`DEV_TUNNEL_VERSION_INCOMPATIBLE: ${observedVersion ?? "unknown"}`);
|
|
129
|
+
await writeFile(`${target}.json`, `${JSON.stringify({
|
|
130
|
+
source: selected.url,
|
|
131
|
+
version: observedVersion,
|
|
132
|
+
sha256: await sha256(target),
|
|
133
|
+
artifactSha256: selected.sha256,
|
|
134
|
+
file: basename(target),
|
|
135
|
+
directory: dirname(target),
|
|
136
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
137
|
+
return target;
|
|
138
|
+
}
|
|
139
|
+
export async function resolveDevTunnelCli() {
|
|
140
|
+
const command = await downloadManaged();
|
|
141
|
+
const managedVersion = await version(command);
|
|
142
|
+
if (!managedVersion)
|
|
143
|
+
throw new Error("DEV_TUNNEL_MANAGED_CLI_INVALID");
|
|
144
|
+
return { command, source: "package", version: managedVersion };
|
|
145
|
+
}
|
|
@@ -26,7 +26,7 @@ export type DevTunnelInspection = {
|
|
|
26
26
|
export interface DevTunnelAutomation {
|
|
27
27
|
ensureLogin(): Promise<"LOGGED_IN">;
|
|
28
28
|
inspectTunnel(tunnelId: string): Promise<DevTunnelInspection>;
|
|
29
|
-
createTunnel(): Promise<string>;
|
|
29
|
+
createTunnel(tunnelId: string): Promise<string>;
|
|
30
30
|
ensurePort(tunnelId: string, port: number): Promise<"REUSED" | "CREATED" | "UPDATED">;
|
|
31
31
|
discoverPublicBaseUrl(tunnelId: string, port: number): Promise<string>;
|
|
32
32
|
}
|
|
@@ -60,6 +60,7 @@ export declare function createDevTunnelRuntime(input: {
|
|
|
60
60
|
publicBaseUrl?: string;
|
|
61
61
|
runCommand?: CommandRunner;
|
|
62
62
|
processStateFile?: string;
|
|
63
|
+
loginVerified?: boolean;
|
|
63
64
|
}): DevTunnelRuntime;
|
|
64
65
|
export interface IngressCheck {
|
|
65
66
|
id: string;
|
|
@@ -6,6 +6,8 @@ import { z } from "zod";
|
|
|
6
6
|
const LOGIN_ARGS = ["user", "show", "--json"];
|
|
7
7
|
const LOGIN_TIMEOUT_MS = 30_000;
|
|
8
8
|
const REMOTE_COMMAND_TIMEOUT_MS = 30_000;
|
|
9
|
+
const REMOTE_QUERY_TIMEOUT_MS = 45_000;
|
|
10
|
+
const REMOTE_QUERY_RETRY_DELAY_MS = 250;
|
|
9
11
|
const START_CONFIRM_MS = 500;
|
|
10
12
|
function defaultCommandRunner(command, args, options) {
|
|
11
13
|
if (options?.interactive) {
|
|
@@ -92,6 +94,14 @@ function parseTunnel(input) {
|
|
|
92
94
|
return z.object({ tunnel: tunnelPayloadSchema }).passthrough().parse(input)
|
|
93
95
|
.tunnel;
|
|
94
96
|
}
|
|
97
|
+
function sameTunnelIdentity(requested, observed) {
|
|
98
|
+
if (observed === requested)
|
|
99
|
+
return true;
|
|
100
|
+
if (requested.includes("."))
|
|
101
|
+
return false;
|
|
102
|
+
const prefix = `${requested}.`;
|
|
103
|
+
return observed.startsWith(prefix) && !observed.slice(prefix.length).includes(".");
|
|
104
|
+
}
|
|
95
105
|
function parseJson(text, label) {
|
|
96
106
|
try {
|
|
97
107
|
return JSON.parse(text);
|
|
@@ -171,6 +181,16 @@ function assertCommandSucceeded(result, label) {
|
|
|
171
181
|
export function createDevTunnelAutomation(input) {
|
|
172
182
|
const command = input?.command ?? "devtunnel";
|
|
173
183
|
const run = input?.runCommand ?? defaultCommandRunner;
|
|
184
|
+
const runRemoteQuery = async (args, retryTimeout = true) => {
|
|
185
|
+
let result = await run(command, args, { timeoutMs: REMOTE_QUERY_TIMEOUT_MS });
|
|
186
|
+
if (!retryTimeout ||
|
|
187
|
+
result.exitCode !== null ||
|
|
188
|
+
!/timed out/i.test(commandText(result)))
|
|
189
|
+
return result;
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, REMOTE_QUERY_RETRY_DELAY_MS));
|
|
191
|
+
result = await run(command, args, { timeoutMs: REMOTE_QUERY_TIMEOUT_MS });
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
174
194
|
const loginStatus = async () => {
|
|
175
195
|
let result;
|
|
176
196
|
try {
|
|
@@ -205,9 +225,7 @@ export function createDevTunnelAutomation(input) {
|
|
|
205
225
|
return after;
|
|
206
226
|
},
|
|
207
227
|
async inspectTunnel(tunnelId) {
|
|
208
|
-
const result = await
|
|
209
|
-
timeoutMs: REMOTE_COMMAND_TIMEOUT_MS,
|
|
210
|
-
});
|
|
228
|
+
const result = await runRemoteQuery(["show", tunnelId, "--json"]);
|
|
211
229
|
if (result.exitCode !== 0) {
|
|
212
230
|
return /not found|does not exist|could not be found/i.test(commandText(result))
|
|
213
231
|
? { state: "MISSING", hostState: "UNKNOWN" }
|
|
@@ -215,7 +233,7 @@ export function createDevTunnelAutomation(input) {
|
|
|
215
233
|
}
|
|
216
234
|
try {
|
|
217
235
|
const tunnel = parseTunnel(parseJson(result.stdout, "devtunnel show --json"));
|
|
218
|
-
if (tunnel.tunnelId
|
|
236
|
+
if (!sameTunnelIdentity(tunnelId, tunnel.tunnelId))
|
|
219
237
|
return { state: "UNKNOWN", hostState: "UNKNOWN" };
|
|
220
238
|
const hostState = tunnel.hostConnections !== undefined
|
|
221
239
|
? tunnel.hostConnections > 0
|
|
@@ -232,16 +250,16 @@ export function createDevTunnelAutomation(input) {
|
|
|
232
250
|
return { state: "UNKNOWN", hostState: "UNKNOWN" };
|
|
233
251
|
}
|
|
234
252
|
},
|
|
235
|
-
async createTunnel() {
|
|
236
|
-
const result = await run(command, ["create", "--allow-anonymous", "--json"], { timeoutMs: REMOTE_COMMAND_TIMEOUT_MS });
|
|
253
|
+
async createTunnel(tunnelId) {
|
|
254
|
+
const result = await run(command, ["create", tunnelId, "--allow-anonymous", "--json"], { timeoutMs: REMOTE_COMMAND_TIMEOUT_MS });
|
|
237
255
|
assertCommandSucceeded(result, "devtunnel create --json");
|
|
238
256
|
const tunnel = parseTunnel(parseJson(result.stdout, "devtunnel create --json"));
|
|
239
|
-
|
|
257
|
+
if (!sameTunnelIdentity(tunnelId, tunnel.tunnelId))
|
|
258
|
+
throw new Error("devtunnel create returned an unexpected tunnelId");
|
|
259
|
+
return tunnelId;
|
|
240
260
|
},
|
|
241
261
|
async ensurePort(tunnelId, port) {
|
|
242
|
-
const listed = await
|
|
243
|
-
timeoutMs: REMOTE_COMMAND_TIMEOUT_MS,
|
|
244
|
-
});
|
|
262
|
+
const listed = await runRemoteQuery(["port", "list", tunnelId, "--json"]);
|
|
245
263
|
assertCommandSucceeded(listed, "devtunnel port list --json");
|
|
246
264
|
const existing = parsePorts(parseJson(listed.stdout, "devtunnel port list --json")).find((item) => item.portNumber === port);
|
|
247
265
|
if (existing?.protocol?.toLowerCase() === "http")
|
|
@@ -271,9 +289,7 @@ export function createDevTunnelAutomation(input) {
|
|
|
271
289
|
async discoverPublicBaseUrl(tunnelId, port) {
|
|
272
290
|
let lastError;
|
|
273
291
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
274
|
-
const shown = await
|
|
275
|
-
timeoutMs: REMOTE_COMMAND_TIMEOUT_MS,
|
|
276
|
-
});
|
|
292
|
+
const shown = await runRemoteQuery(["show", tunnelId, "--json"], false);
|
|
277
293
|
assertCommandSucceeded(shown, "devtunnel show --json");
|
|
278
294
|
try {
|
|
279
295
|
return discoverPublicBaseUrl(parseJson(shown.stdout, "devtunnel show --json"), port);
|
|
@@ -432,7 +448,7 @@ export function createDevTunnelRuntime(input) {
|
|
|
432
448
|
loginStatus: () => observeLogin(),
|
|
433
449
|
publicBaseUrl: () => publicBaseUrl,
|
|
434
450
|
async start() {
|
|
435
|
-
const login = await observeLogin();
|
|
451
|
+
const login = input.loginVerified ? "LOGGED_IN" : await observeLogin();
|
|
436
452
|
if (login !== "LOGGED_IN")
|
|
437
453
|
return observation("UNKNOWN", login);
|
|
438
454
|
if (tunnelId === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-dev-tunnel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"zod": "4.1.12",
|
|
26
|
-
"@tomflow/proflow-module-contract": "^0.1.13"
|
|
27
|
-
"@tomflow/proflow-devtunnel-cli": "^0.1.1"
|
|
26
|
+
"@tomflow/proflow-module-contract": "^0.1.13"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
29
|
"@tomflow/proflow-deployment-conformance": "^0.1.13"
|
package/proflow.module.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"contractVersion": "1.0.0",
|
|
4
4
|
"moduleRef": "dev-tunnel",
|
|
5
5
|
"packageName": "@tomflow/proflow-dev-tunnel",
|
|
6
|
-
"moduleVersion": "0.1.
|
|
6
|
+
"moduleVersion": "0.1.22",
|
|
7
7
|
"kind": "external-resource",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|