@tomflow/proflow-dev-tunnel 0.1.2 → 0.1.4
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/dist/deployment/adapter.d.ts +11 -0
- package/dist/deployment/adapter.js +39 -0
- package/dist/deployment/descriptor.d.ts +6 -1
- package/dist/deployment/descriptor.js +7 -1
- package/dist/src/resource-adapter.d.ts +1 -0
- package/dist/src/resource-adapter.js +145 -14
- package/package.json +1 -1
- package/proflow.module.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-dev-tunnel
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Detach the managed dev-tunnel host from the short-lived Platform CLI process and persist an ownership-checked process record so later status/stop/restart invocations can manage the same tunnel without turning `platform start` into a daemon.
|
|
8
|
+
|
|
9
|
+
## 0.1.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Bind production dev-tunnel verification to an explicit evidence file for the frozen file-relay and 429/5xx proofs, while retaining live HTTPS/TLS/latency/size checks.
|
|
14
|
+
|
|
3
15
|
## 0.1.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { ModuleOperationResult } from "@tomflow/proflow-module-contract";
|
|
2
2
|
import type { DevTunnelRuntime, ErrorSemanticsProof, FileRelayProof } from "../src/resource-adapter.ts";
|
|
3
|
+
type DevTunnelVerificationEvidence = {
|
|
4
|
+
contract: "proflow.dev-tunnel-verification.v1";
|
|
5
|
+
moduleVersion: string;
|
|
6
|
+
publicBaseUrl: string;
|
|
7
|
+
observedAt: string;
|
|
8
|
+
fileRelay: FileRelayProof;
|
|
9
|
+
errorSemantics: ErrorSemanticsProof;
|
|
10
|
+
};
|
|
11
|
+
export declare function readDevTunnelVerificationEvidence(file: string | undefined, publicBaseUrl: string): Promise<DevTunnelVerificationEvidence | undefined>;
|
|
3
12
|
export declare function createBehaviorAdapter(input?: {
|
|
4
13
|
runtime: DevTunnelRuntime;
|
|
5
14
|
verifyErrorSemantics?: () => Promise<ErrorSemanticsProof>;
|
|
@@ -125,6 +134,8 @@ export declare const behaviorAdapter: {
|
|
|
125
134
|
export declare function createProductionBinding(input: {
|
|
126
135
|
moduleRef: string;
|
|
127
136
|
config: Record<string, string>;
|
|
137
|
+
workspaceRoot: string;
|
|
128
138
|
}): Promise<{
|
|
129
139
|
behaviorAdapter: Record<string, unknown>;
|
|
130
140
|
} | undefined>;
|
|
141
|
+
export {};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { verifyPublicIngress } from "../src/resource-adapter.js";
|
|
2
4
|
import { descriptor } from "./descriptor.js";
|
|
3
5
|
const success = (data) => ({
|
|
@@ -16,6 +18,32 @@ const actionRequired = (action, description) => ({
|
|
|
16
18
|
moduleVersion: descriptor.moduleVersion,
|
|
17
19
|
actionRequired: { action, description },
|
|
18
20
|
});
|
|
21
|
+
export async function readDevTunnelVerificationEvidence(file, publicBaseUrl) {
|
|
22
|
+
if (!file)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
26
|
+
if (raw.contract !== "proflow.dev-tunnel-verification.v1" ||
|
|
27
|
+
raw.moduleVersion !== descriptor.moduleVersion ||
|
|
28
|
+
raw.publicBaseUrl !== publicBaseUrl ||
|
|
29
|
+
typeof raw.observedAt !== "string" ||
|
|
30
|
+
Number.isNaN(Date.parse(raw.observedAt)) ||
|
|
31
|
+
typeof raw.fileRelay !== "object" ||
|
|
32
|
+
raw.fileRelay === null ||
|
|
33
|
+
typeof raw.fileRelay.verified !== "boolean" ||
|
|
34
|
+
typeof raw.fileRelay.message !== "string" ||
|
|
35
|
+
typeof raw.errorSemantics !== "object" ||
|
|
36
|
+
raw.errorSemantics === null ||
|
|
37
|
+
typeof raw.errorSemantics.rateLimit429Verified !== "boolean" ||
|
|
38
|
+
typeof raw.errorSemantics.server5xxVerified !== "boolean" ||
|
|
39
|
+
typeof raw.errorSemantics.message !== "string")
|
|
40
|
+
return undefined;
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
19
47
|
export function createBehaviorAdapter(input) {
|
|
20
48
|
return {
|
|
21
49
|
describe: () => ({
|
|
@@ -257,12 +285,23 @@ export async function createProductionBinding(input) {
|
|
|
257
285
|
if (!publicBaseUrl)
|
|
258
286
|
return undefined;
|
|
259
287
|
const { createDevTunnelRuntime } = await import("../src/resource-adapter.js");
|
|
288
|
+
const readEvidence = () => readDevTunnelVerificationEvidence(input.config.verificationEvidenceFile, publicBaseUrl);
|
|
260
289
|
return {
|
|
261
290
|
behaviorAdapter: createBehaviorAdapter({
|
|
262
291
|
runtime: createDevTunnelRuntime({
|
|
263
292
|
publicBaseUrl,
|
|
264
293
|
...(input.config.tunnelId ? { tunnelId: input.config.tunnelId } : {}),
|
|
294
|
+
processStateFile: join(input.workspaceRoot, ".proflow", "runtime", "external-resources", "dev-tunnel", "process.json"),
|
|
265
295
|
}),
|
|
296
|
+
verifyFileRelay: async () => (await readEvidence())?.fileRelay ?? {
|
|
297
|
+
verified: false,
|
|
298
|
+
message: "dev-tunnel verification evidence is missing, stale, or invalid",
|
|
299
|
+
},
|
|
300
|
+
verifyErrorSemantics: async () => (await readEvidence())?.errorSemantics ?? {
|
|
301
|
+
rateLimit429Verified: false,
|
|
302
|
+
server5xxVerified: false,
|
|
303
|
+
message: "dev-tunnel verification evidence is missing, stale, or invalid",
|
|
304
|
+
},
|
|
266
305
|
}),
|
|
267
306
|
};
|
|
268
307
|
}
|
|
@@ -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.4";
|
|
7
7
|
readonly kind: "external-resource";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -31,6 +31,11 @@ export declare const descriptor: {
|
|
|
31
31
|
readonly type: "string";
|
|
32
32
|
readonly required: false;
|
|
33
33
|
readonly description: "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop";
|
|
34
|
+
}, {
|
|
35
|
+
readonly key: "verificationEvidenceFile";
|
|
36
|
+
readonly type: "path";
|
|
37
|
+
readonly required: false;
|
|
38
|
+
readonly description: "JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress";
|
|
34
39
|
}];
|
|
35
40
|
readonly lifecycle: {
|
|
36
41
|
readonly supported: readonly ["describe", "preflight", "status", "verify", "doctor", "start", "stop", "restart", "uninstall"];
|
|
@@ -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.4",
|
|
7
7
|
kind: "external-resource",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -34,6 +34,12 @@ export const descriptor = {
|
|
|
34
34
|
required: false,
|
|
35
35
|
description: "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop",
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
key: "verificationEvidenceFile",
|
|
39
|
+
type: "path",
|
|
40
|
+
required: false,
|
|
41
|
+
description: "JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress",
|
|
42
|
+
},
|
|
37
43
|
],
|
|
38
44
|
lifecycle: {
|
|
39
45
|
supported: [
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
2
4
|
import { connect } from "node:tls";
|
|
3
5
|
const LOGIN_ARGS = ["user", "show"];
|
|
4
6
|
const LOGIN_TIMEOUT_MS = 10_000;
|
|
@@ -31,10 +33,83 @@ function defaultCommandRunner(command, args, options) {
|
|
|
31
33
|
});
|
|
32
34
|
});
|
|
33
35
|
}
|
|
36
|
+
async function readProcessRecord(file) {
|
|
37
|
+
if (!file)
|
|
38
|
+
return undefined;
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
41
|
+
if (raw.contract !== "proflow.dev-tunnel-process.v1" ||
|
|
42
|
+
typeof raw.pid !== "number" ||
|
|
43
|
+
!Number.isInteger(raw.pid) ||
|
|
44
|
+
raw.pid <= 0 ||
|
|
45
|
+
typeof raw.command !== "string" ||
|
|
46
|
+
raw.command.length === 0 ||
|
|
47
|
+
typeof raw.tunnelId !== "string" ||
|
|
48
|
+
raw.tunnelId.length === 0 ||
|
|
49
|
+
typeof raw.startedAt !== "string" ||
|
|
50
|
+
Number.isNaN(Date.parse(raw.startedAt)))
|
|
51
|
+
return undefined;
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function writeProcessRecord(file, record) {
|
|
59
|
+
await mkdir(dirname(file), { recursive: true });
|
|
60
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
61
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
mode: 0o600,
|
|
64
|
+
});
|
|
65
|
+
await rename(temporary, file);
|
|
66
|
+
}
|
|
67
|
+
function processAlive(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function processCommandLine(pid) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const command = process.platform === "win32" ? "powershell.exe" : "ps";
|
|
79
|
+
const args = process.platform === "win32"
|
|
80
|
+
? [
|
|
81
|
+
"-NoProfile",
|
|
82
|
+
"-Command",
|
|
83
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
84
|
+
]
|
|
85
|
+
: ["-p", String(pid), "-o", "command="];
|
|
86
|
+
execFile(command, args, { timeout: 2_000 }, (error, stdout) => {
|
|
87
|
+
resolve(error ? undefined : String(stdout));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function processRecordIsOwned(record, command, tunnelId) {
|
|
92
|
+
if (record.command !== command ||
|
|
93
|
+
record.tunnelId !== tunnelId ||
|
|
94
|
+
!processAlive(record.pid))
|
|
95
|
+
return false;
|
|
96
|
+
const commandLine = await processCommandLine(record.pid);
|
|
97
|
+
return (commandLine?.includes(command) === true && commandLine.includes(tunnelId));
|
|
98
|
+
}
|
|
99
|
+
async function waitForProcessExit(pid, timeoutMs = 2_000) {
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
if (!processAlive(pid))
|
|
103
|
+
return true;
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
105
|
+
}
|
|
106
|
+
return !processAlive(pid);
|
|
107
|
+
}
|
|
34
108
|
export function createDevTunnelRuntime(input) {
|
|
35
109
|
const command = input.command ?? "devtunnel";
|
|
36
110
|
const tunnelId = input.tunnelId;
|
|
37
111
|
const publicBaseUrl = input.publicBaseUrl;
|
|
112
|
+
const processStateFile = input.processStateFile;
|
|
38
113
|
const run = input.runCommand ?? defaultCommandRunner;
|
|
39
114
|
let child;
|
|
40
115
|
const observeLogin = async () => {
|
|
@@ -58,12 +133,26 @@ export function createDevTunnelRuntime(input) {
|
|
|
58
133
|
...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
|
|
59
134
|
};
|
|
60
135
|
};
|
|
136
|
+
const ownedPersistedPid = async () => {
|
|
137
|
+
if (!processStateFile || !tunnelId)
|
|
138
|
+
return undefined;
|
|
139
|
+
const record = await readProcessRecord(processStateFile);
|
|
140
|
+
if (!record)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (await processRecordIsOwned(record, command, tunnelId))
|
|
143
|
+
return record.pid;
|
|
144
|
+
await rm(processStateFile, { force: true });
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
61
147
|
return {
|
|
62
148
|
command,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
149
|
+
async status() {
|
|
150
|
+
if (await ownedPersistedPid())
|
|
151
|
+
return observe("RUNNING");
|
|
152
|
+
if (child?.pid && processAlive(child.pid))
|
|
153
|
+
return observe("RUNNING");
|
|
154
|
+
return observe("UNKNOWN");
|
|
155
|
+
},
|
|
67
156
|
loginStatus: () => observeLogin(),
|
|
68
157
|
publicBaseUrl: () => publicBaseUrl,
|
|
69
158
|
async start() {
|
|
@@ -73,20 +162,24 @@ export function createDevTunnelRuntime(input) {
|
|
|
73
162
|
if (tunnelId === undefined) {
|
|
74
163
|
throw new TypeError("tunnelId is required to host the configured persistent tunnel");
|
|
75
164
|
}
|
|
76
|
-
if (
|
|
165
|
+
if (await ownedPersistedPid())
|
|
166
|
+
return observe("RUNNING");
|
|
167
|
+
if (child?.pid && processAlive(child.pid))
|
|
77
168
|
return observe("RUNNING");
|
|
78
|
-
|
|
169
|
+
const spawned = spawn(command, ["host", tunnelId], {
|
|
170
|
+
stdio: "ignore",
|
|
171
|
+
detached: true,
|
|
172
|
+
});
|
|
79
173
|
const confirmed = await new Promise((resolve) => {
|
|
80
174
|
let settled = false;
|
|
81
175
|
const fail = () => {
|
|
82
176
|
if (settled)
|
|
83
177
|
return;
|
|
84
178
|
settled = true;
|
|
85
|
-
child = undefined;
|
|
86
179
|
resolve("FAILED");
|
|
87
180
|
};
|
|
88
|
-
|
|
89
|
-
|
|
181
|
+
spawned.once("error", fail);
|
|
182
|
+
spawned.once("exit", fail);
|
|
90
183
|
setTimeout(() => {
|
|
91
184
|
if (settled)
|
|
92
185
|
return;
|
|
@@ -94,18 +187,56 @@ export function createDevTunnelRuntime(input) {
|
|
|
94
187
|
resolve("RUNNING");
|
|
95
188
|
}, START_CONFIRM_MS);
|
|
96
189
|
});
|
|
97
|
-
if (confirmed === "FAILED") {
|
|
190
|
+
if (confirmed === "FAILED" || spawned.pid === undefined) {
|
|
98
191
|
throw new Error("devtunnel host process failed to start");
|
|
99
192
|
}
|
|
193
|
+
child = spawned;
|
|
194
|
+
spawned.once("exit", () => {
|
|
195
|
+
if (child === spawned)
|
|
196
|
+
child = undefined;
|
|
197
|
+
if (processStateFile)
|
|
198
|
+
void rm(processStateFile, { force: true });
|
|
199
|
+
});
|
|
200
|
+
if (processStateFile) {
|
|
201
|
+
try {
|
|
202
|
+
await writeProcessRecord(processStateFile, {
|
|
203
|
+
contract: "proflow.dev-tunnel-process.v1",
|
|
204
|
+
pid: spawned.pid,
|
|
205
|
+
command,
|
|
206
|
+
tunnelId,
|
|
207
|
+
startedAt: new Date().toISOString(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
spawned.kill();
|
|
212
|
+
child = undefined;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
spawned.unref();
|
|
100
217
|
return observe("RUNNING");
|
|
101
218
|
},
|
|
102
219
|
async stop() {
|
|
103
|
-
|
|
104
|
-
|
|
220
|
+
const persistedPid = await ownedPersistedPid();
|
|
221
|
+
const pid = persistedPid ?? child?.pid;
|
|
222
|
+
if (!pid || !processAlive(pid)) {
|
|
223
|
+
if (processStateFile)
|
|
224
|
+
await rm(processStateFile, { force: true });
|
|
105
225
|
child = undefined;
|
|
106
|
-
return observe("
|
|
226
|
+
return observe("UNKNOWN");
|
|
107
227
|
}
|
|
108
|
-
|
|
228
|
+
try {
|
|
229
|
+
process.kill(pid, "SIGTERM");
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return observe("UNKNOWN");
|
|
233
|
+
}
|
|
234
|
+
if (!(await waitForProcessExit(pid)))
|
|
235
|
+
return observe("UNKNOWN");
|
|
236
|
+
if (processStateFile)
|
|
237
|
+
await rm(processStateFile, { force: true });
|
|
238
|
+
child = undefined;
|
|
239
|
+
return observe("STOPPED");
|
|
109
240
|
},
|
|
110
241
|
async restart() {
|
|
111
242
|
const stopped = await this.stop();
|
package/package.json
CHANGED
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.4",
|
|
7
7
|
"kind": "external-resource",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"type": "string",
|
|
37
37
|
"required": false,
|
|
38
38
|
"description": "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"key": "verificationEvidenceFile",
|
|
42
|
+
"type": "path",
|
|
43
|
+
"required": false,
|
|
44
|
+
"description": "JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress"
|
|
39
45
|
}
|
|
40
46
|
],
|
|
41
47
|
"lifecycle": {
|