@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 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. Existing
27
- login is reused; when login is unavailable, the only human action is GitHub
28
- browser authorization initiated by the provider CLI. Account state is never faked.
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
@@ -15,7 +15,7 @@ Human action: 仅当现有登录无效时,在 `devtunnel user login --github -
15
15
  Automatic flow:
16
16
 
17
17
  ```text
18
- 检查 CLI 和登录
18
+ 准备/复用 dev-tunnel 包目录内的官方 CLI,并检查登录
19
19
  → 必要时完成 GitHub browser auth 并复核登录
20
20
  → 复用 workspace-owned Tunnel 或自动创建
21
21
  → 从 agent-gateway shared facts 读取本地端口
@@ -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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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.20";
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
- ...(state?.cliPath ? { command: state.cliPath } : {}),
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 = previous?.cliPath ??
368
- (dependencies?.automation
369
- ? "devtunnel"
370
- : await (dependencies?.resolveCli ??
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 createAndVerifyTunnel = async () => {
380
- const createdTunnelId = await automation.createTunnel();
381
- const created = await automation.inspectTunnel(createdTunnelId);
382
- if (created.state !== "EXISTS")
383
- throw new Error("new Dev Tunnel could not be verified by show --json");
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
- tunnelId = await createAndVerifyTunnel();
403
- safeToStartNewHost = true;
420
+ const resolved = await resolveStableTunnel();
421
+ tunnelId = resolved.tunnelId;
422
+ safeToStartNewHost = resolved.hostState === "STOPPED";
404
423
  }
405
424
  }
406
425
  else {
407
- tunnelId = await createAndVerifyTunnel();
408
- safeToStartNewHost = true;
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.20";
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.20",
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,7 @@
1
+ export declare const MANAGED_DEV_TUNNEL_VERSION = "1.0.2030";
2
+ export declare function devTunnelCliPath(): string;
3
+ export declare function resolveDevTunnelCli(): Promise<{
4
+ command: string;
5
+ source: "package";
6
+ version: string;
7
+ }>;
@@ -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 run(command, ["show", tunnelId, "--json"], {
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 !== 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
- return tunnel.tunnelId;
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 run(command, ["port", "list", tunnelId, "--json"], {
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 run(command, ["show", tunnelId, "--json"], {
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.20",
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"
@@ -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.20",
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",