@tomflow/proflow-dev-tunnel 0.1.0

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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # dev-tunnel
2
+
3
+ Domain Owner: deployment-governance
4
+ Module Kind: external-resource
5
+ Service: none
6
+ Process: none (except the managed `devtunnel host` external CLI child)
7
+ Business Fact Owner: none
8
+
9
+ ## Owner
10
+
11
+ Deployment Governance
12
+
13
+ ## Consumers
14
+
15
+ - agent-gateway
16
+
17
+ ## Does NOT own
18
+
19
+ - Agent Gateway
20
+ - Gateway routing
21
+ - Gateway auth
22
+ - Task/Agent facts
23
+
24
+ ## Purpose
25
+
26
+ Manages the Microsoft Dev Tunnel (`devtunnel`) public HTTPS ingress. Login is a
27
+ user external-account fact and is never faked.
@@ -0,0 +1 @@
1
+ { "contract": "proflow.conformance.v1", "levels": ["C1", "C2", "C3"] }
@@ -0,0 +1,130 @@
1
+ import type { ModuleOperationResult } from "@tomflow/proflow-module-contract";
2
+ import type { DevTunnelRuntime, ErrorSemanticsProof, FileRelayProof } from "../src/resource-adapter.ts";
3
+ export declare function createBehaviorAdapter(input?: {
4
+ runtime: DevTunnelRuntime;
5
+ verifyErrorSemantics?: () => Promise<ErrorSemanticsProof>;
6
+ verifyFileRelay?: () => Promise<FileRelayProof>;
7
+ }): {
8
+ describe: () => {
9
+ result: ModuleOperationResult;
10
+ observedEffects: never[];
11
+ };
12
+ preflight: () => {
13
+ result: ModuleOperationResult;
14
+ observedEffects: never[];
15
+ };
16
+ status: () => Promise<{
17
+ result: ModuleOperationResult;
18
+ observedEffects: never[];
19
+ }>;
20
+ verify: () => Promise<{
21
+ result: ModuleOperationResult;
22
+ observedEffects: never[];
23
+ } | {
24
+ result: {
25
+ contract: "deployment.result.v1";
26
+ ok: boolean;
27
+ status: "ACTION_REQUIRED" | "BLOCKED" | "FAILED" | "SUCCEEDED";
28
+ moduleRef: string;
29
+ moduleVersion: string;
30
+ actionRequired?: {
31
+ action: string;
32
+ description: string;
33
+ } | undefined;
34
+ error?: {
35
+ code: "APPLY_FAILED" | "COMMAND_FAILED" | "COMPATIBILITY_MISMATCH" | "CONFIG_REQUIRED" | "CONFORMANCE_FAILED" | "CONTRACT_INVALID" | "CORE_PACKAGE_REQUIRED" | "DEPENDENCY_UNRESOLVED" | "DOCTOR_FAILED" | "EXTERNAL_RESOURCE_UNAVAILABLE" | "INVALID_REQUEST" | "LIFECYCLE_UNSUPPORTED" | "MODULE_NOT_FOUND" | "PLAN_INVALID" | "PLAN_STALE" | "REQUIREMENT_UNMET" | "UNINSTALL_FAILED" | "UPGRADE_FAILED" | "VERIFY_FAILED";
36
+ message: string;
37
+ retryable: boolean;
38
+ } | undefined;
39
+ resourceVersion?: string | undefined;
40
+ data?: unknown;
41
+ checks: import("../src/resource-adapter.ts").IngressCheck[];
42
+ };
43
+ observedEffects: string[];
44
+ }>;
45
+ doctor: () => Promise<{
46
+ result: ModuleOperationResult;
47
+ observedEffects: never[];
48
+ }>;
49
+ start: () => Promise<{
50
+ result: ModuleOperationResult;
51
+ observedEffects: string[];
52
+ }>;
53
+ stop: () => Promise<{
54
+ result: ModuleOperationResult;
55
+ observedEffects: string[];
56
+ }>;
57
+ uninstall: () => Promise<{
58
+ result: ModuleOperationResult;
59
+ observedEffects: string[];
60
+ }>;
61
+ restart: () => Promise<{
62
+ result: ModuleOperationResult;
63
+ observedEffects: string[];
64
+ }>;
65
+ };
66
+ export declare const behaviorAdapter: {
67
+ describe: () => {
68
+ result: ModuleOperationResult;
69
+ observedEffects: never[];
70
+ };
71
+ preflight: () => {
72
+ result: ModuleOperationResult;
73
+ observedEffects: never[];
74
+ };
75
+ status: () => Promise<{
76
+ result: ModuleOperationResult;
77
+ observedEffects: never[];
78
+ }>;
79
+ verify: () => Promise<{
80
+ result: ModuleOperationResult;
81
+ observedEffects: never[];
82
+ } | {
83
+ result: {
84
+ contract: "deployment.result.v1";
85
+ ok: boolean;
86
+ status: "ACTION_REQUIRED" | "BLOCKED" | "FAILED" | "SUCCEEDED";
87
+ moduleRef: string;
88
+ moduleVersion: string;
89
+ actionRequired?: {
90
+ action: string;
91
+ description: string;
92
+ } | undefined;
93
+ error?: {
94
+ code: "APPLY_FAILED" | "COMMAND_FAILED" | "COMPATIBILITY_MISMATCH" | "CONFIG_REQUIRED" | "CONFORMANCE_FAILED" | "CONTRACT_INVALID" | "CORE_PACKAGE_REQUIRED" | "DEPENDENCY_UNRESOLVED" | "DOCTOR_FAILED" | "EXTERNAL_RESOURCE_UNAVAILABLE" | "INVALID_REQUEST" | "LIFECYCLE_UNSUPPORTED" | "MODULE_NOT_FOUND" | "PLAN_INVALID" | "PLAN_STALE" | "REQUIREMENT_UNMET" | "UNINSTALL_FAILED" | "UPGRADE_FAILED" | "VERIFY_FAILED";
95
+ message: string;
96
+ retryable: boolean;
97
+ } | undefined;
98
+ resourceVersion?: string | undefined;
99
+ data?: unknown;
100
+ checks: import("../src/resource-adapter.ts").IngressCheck[];
101
+ };
102
+ observedEffects: string[];
103
+ }>;
104
+ doctor: () => Promise<{
105
+ result: ModuleOperationResult;
106
+ observedEffects: never[];
107
+ }>;
108
+ start: () => Promise<{
109
+ result: ModuleOperationResult;
110
+ observedEffects: string[];
111
+ }>;
112
+ stop: () => Promise<{
113
+ result: ModuleOperationResult;
114
+ observedEffects: string[];
115
+ }>;
116
+ uninstall: () => Promise<{
117
+ result: ModuleOperationResult;
118
+ observedEffects: string[];
119
+ }>;
120
+ restart: () => Promise<{
121
+ result: ModuleOperationResult;
122
+ observedEffects: string[];
123
+ }>;
124
+ };
125
+ export declare function createProductionBinding(input: {
126
+ moduleRef: string;
127
+ config: Record<string, string>;
128
+ }): Promise<{
129
+ behaviorAdapter: Record<string, unknown>;
130
+ } | undefined>;
@@ -0,0 +1,266 @@
1
+ import { verifyPublicIngress } from "../src/resource-adapter.js";
2
+ import { descriptor } from "./descriptor.js";
3
+ const success = (data) => ({
4
+ contract: "deployment.result.v1",
5
+ ok: true,
6
+ status: "SUCCEEDED",
7
+ moduleRef: descriptor.moduleRef,
8
+ moduleVersion: descriptor.moduleVersion,
9
+ ...(data === undefined ? {} : { data }),
10
+ });
11
+ const actionRequired = (action, description) => ({
12
+ contract: "deployment.result.v1",
13
+ ok: false,
14
+ status: "ACTION_REQUIRED",
15
+ moduleRef: descriptor.moduleRef,
16
+ moduleVersion: descriptor.moduleVersion,
17
+ actionRequired: { action, description },
18
+ });
19
+ export function createBehaviorAdapter(input) {
20
+ return {
21
+ describe: () => ({
22
+ result: success({
23
+ publicApi: ["status", "verify", "doctor", "start", "stop", "restart"],
24
+ }),
25
+ observedEffects: [],
26
+ }),
27
+ preflight: () => ({
28
+ result: input
29
+ ? success()
30
+ : actionRequired("configure-tunnel", "Bind a dev-tunnel resource with login status and a public HTTPS ingress"),
31
+ observedEffects: [],
32
+ }),
33
+ status: async () => {
34
+ if (!input) {
35
+ return {
36
+ result: actionRequired("configure-tunnel", "No dev-tunnel resource is bound"),
37
+ observedEffects: [],
38
+ };
39
+ }
40
+ const observation = await input.runtime.status();
41
+ return {
42
+ result: {
43
+ ...success(),
44
+ checks: [
45
+ {
46
+ id: "tunnel-status",
47
+ status: observation.state === "RUNNING"
48
+ ? "PASS"
49
+ : "WARN",
50
+ message: `dev-tunnel state is ${observation.state}`,
51
+ },
52
+ ],
53
+ },
54
+ observedEffects: [],
55
+ };
56
+ },
57
+ verify: async () => {
58
+ if (!input) {
59
+ return {
60
+ result: actionRequired("configure-tunnel", "A live dev-tunnel resource is required for public ingress verification"),
61
+ observedEffects: [],
62
+ };
63
+ }
64
+ if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
65
+ return {
66
+ result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before verifying public ingress"),
67
+ observedEffects: [],
68
+ };
69
+ }
70
+ const publicBaseUrl = input.runtime.publicBaseUrl();
71
+ if (publicBaseUrl === undefined) {
72
+ return {
73
+ result: actionRequired("configure-tunnel", "publicBaseUrl must be configured before public ingress verification"),
74
+ observedEffects: [],
75
+ };
76
+ }
77
+ const verification = await verifyPublicIngress(publicBaseUrl, {
78
+ ...(input.verifyErrorSemantics === undefined
79
+ ? {}
80
+ : { verifyErrorSemantics: input.verifyErrorSemantics }),
81
+ ...(input.verifyFileRelay === undefined
82
+ ? {}
83
+ : { verifyFileRelay: input.verifyFileRelay }),
84
+ });
85
+ return {
86
+ result: verification.ok
87
+ ? { ...success(), checks: verification.checks }
88
+ : {
89
+ ...actionRequired("repair-tunnel-ingress", failureReason(verification)),
90
+ checks: verification.checks,
91
+ },
92
+ observedEffects: verification.reachable
93
+ ? ["Probes the dev-tunnel public HTTPS ingress"]
94
+ : [],
95
+ };
96
+ },
97
+ doctor: async () => {
98
+ if (!input) {
99
+ return {
100
+ result: actionRequired("configure-tunnel", "dev-tunnel login and public ingress configuration are required for diagnostics"),
101
+ observedEffects: [],
102
+ };
103
+ }
104
+ const login = await input.runtime.loginStatus();
105
+ const status = await input.runtime.status();
106
+ const publicBaseUrl = input.runtime.publicBaseUrl();
107
+ const checks = [
108
+ {
109
+ id: "tunnel-login",
110
+ status: login === "LOGGED_IN" ? "PASS" : "FAIL",
111
+ message: `login status is ${login}`,
112
+ },
113
+ {
114
+ id: "tunnel-state",
115
+ status: status.state === "RUNNING"
116
+ ? "PASS"
117
+ : status.state === "STOPPED"
118
+ ? "WARN"
119
+ : "FAIL",
120
+ message: `tunnel state is ${status.state}`,
121
+ },
122
+ {
123
+ id: "tunnel-public-url",
124
+ status: publicBaseUrl === undefined ? "FAIL" : "PASS",
125
+ message: publicBaseUrl === undefined
126
+ ? "publicBaseUrl is not configured"
127
+ : `publicBaseUrl is ${publicBaseUrl}`,
128
+ },
129
+ ];
130
+ const healthy = login === "LOGGED_IN" &&
131
+ status.state === "RUNNING" &&
132
+ publicBaseUrl !== undefined;
133
+ return {
134
+ result: healthy
135
+ ? { ...success(), checks }
136
+ : {
137
+ ...actionRequired("repair-tunnel", "dev-tunnel resource is not healthy"),
138
+ checks,
139
+ },
140
+ observedEffects: [],
141
+ };
142
+ },
143
+ start: async () => {
144
+ if (!input) {
145
+ return {
146
+ result: actionRequired("configure-tunnel", "Cannot start without a bound dev-tunnel resource"),
147
+ observedEffects: [],
148
+ };
149
+ }
150
+ if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
151
+ return {
152
+ result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before starting the tunnel"),
153
+ observedEffects: [],
154
+ };
155
+ }
156
+ try {
157
+ const observation = await input.runtime.start();
158
+ return {
159
+ result: success(observation),
160
+ observedEffects: ["Manage the dev-tunnel public ingress process"],
161
+ };
162
+ }
163
+ catch (error) {
164
+ return {
165
+ result: actionRequired("start-tunnel", error instanceof Error
166
+ ? error.message
167
+ : "failed to start the dev-tunnel process"),
168
+ observedEffects: [],
169
+ };
170
+ }
171
+ },
172
+ stop: async () => {
173
+ if (!input) {
174
+ return {
175
+ result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to stop"),
176
+ observedEffects: [],
177
+ };
178
+ }
179
+ const stopped = await input.runtime.stop();
180
+ if (stopped.state === "STOPPED") {
181
+ return {
182
+ result: success(),
183
+ observedEffects: ["Manage the dev-tunnel public ingress process"],
184
+ };
185
+ }
186
+ return {
187
+ result: actionRequired("complete-tunnel-stop", "dev-tunnel stop state is UNKNOWN; cannot confirm the tunnel stopped"),
188
+ observedEffects: [],
189
+ };
190
+ },
191
+ uninstall: async () => {
192
+ if (!input) {
193
+ return {
194
+ result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to stop before package removal"),
195
+ observedEffects: [],
196
+ };
197
+ }
198
+ const stopped = await input.runtime.stop();
199
+ if (stopped.state === "STOPPED") {
200
+ return {
201
+ result: success(),
202
+ observedEffects: ["Manage the dev-tunnel public ingress process"],
203
+ };
204
+ }
205
+ return {
206
+ result: actionRequired("complete-tunnel-stop", "dev-tunnel stop state is UNKNOWN; package removal cannot continue"),
207
+ observedEffects: [],
208
+ };
209
+ },
210
+ restart: async () => {
211
+ if (!input) {
212
+ return {
213
+ result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to restart"),
214
+ observedEffects: [],
215
+ };
216
+ }
217
+ if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
218
+ return {
219
+ result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before restarting the tunnel"),
220
+ observedEffects: [],
221
+ };
222
+ }
223
+ const stopped = await input.runtime.stop();
224
+ if (stopped.state !== "STOPPED") {
225
+ return {
226
+ result: actionRequired("complete-tunnel-stop", "Cannot restart: dev-tunnel stop state is UNKNOWN"),
227
+ observedEffects: [],
228
+ };
229
+ }
230
+ try {
231
+ const observation = await input.runtime.start();
232
+ return {
233
+ result: success(observation),
234
+ observedEffects: ["Manage the dev-tunnel public ingress process"],
235
+ };
236
+ }
237
+ catch (error) {
238
+ return {
239
+ result: actionRequired("start-tunnel", error instanceof Error
240
+ ? error.message
241
+ : "failed to start the dev-tunnel process"),
242
+ observedEffects: [],
243
+ };
244
+ }
245
+ },
246
+ };
247
+ }
248
+ function failureReason(verification) {
249
+ const failed = verification.checks.find((check) => check.status === "FAIL");
250
+ return failed?.message ?? "public ingress verification did not pass";
251
+ }
252
+ export const behaviorAdapter = createBehaviorAdapter();
253
+ export async function createProductionBinding(input) {
254
+ const publicBaseUrl = input.config.publicBaseUrl;
255
+ if (!publicBaseUrl)
256
+ return undefined;
257
+ const { createDevTunnelRuntime } = await import("../src/resource-adapter.js");
258
+ return {
259
+ behaviorAdapter: createBehaviorAdapter({
260
+ runtime: createDevTunnelRuntime({
261
+ publicBaseUrl,
262
+ ...(input.config.tunnelId ? { tunnelId: input.config.tunnelId } : {}),
263
+ }),
264
+ }),
265
+ };
266
+ }
@@ -0,0 +1,75 @@
1
+ export declare const descriptor: {
2
+ readonly contract: "module";
3
+ readonly contractVersion: "1.0.0";
4
+ readonly moduleRef: "dev-tunnel";
5
+ readonly packageName: "@tomflow/proflow-dev-tunnel";
6
+ readonly moduleVersion: "0.1.0";
7
+ readonly kind: "external-resource";
8
+ readonly templateVersion: "1.0.0";
9
+ readonly platformCompatibility: ">=1.0.0 <2.0.0";
10
+ readonly installClass: "core";
11
+ readonly identity: {
12
+ readonly domain: "deployment-governance";
13
+ readonly summary: "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.";
14
+ };
15
+ readonly provides: readonly [];
16
+ readonly requires: readonly [];
17
+ readonly requirements: readonly [{
18
+ readonly kind: "executable";
19
+ readonly command: "devtunnel";
20
+ }, {
21
+ readonly kind: "human";
22
+ readonly action: "Complete Microsoft Dev Tunnel login when required";
23
+ }];
24
+ readonly configSlots: readonly [{
25
+ readonly key: "publicBaseUrl";
26
+ readonly type: "url";
27
+ readonly required: true;
28
+ readonly description: "Public HTTPS URL supplied by the dev-tunnel resource";
29
+ }, {
30
+ readonly key: "tunnelId";
31
+ readonly type: "string";
32
+ readonly required: false;
33
+ readonly description: "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop";
34
+ }];
35
+ readonly lifecycle: {
36
+ readonly supported: readonly ["describe", "preflight", "status", "verify", "doctor", "start", "stop", "restart", "uninstall"];
37
+ };
38
+ readonly verification: {
39
+ readonly checks: readonly [{
40
+ readonly id: "tunnel-status";
41
+ readonly description: "dev-tunnel process reports a real running or stopped state";
42
+ readonly lifecycle: "status";
43
+ }, {
44
+ readonly id: "tunnel-public-ingress";
45
+ readonly description: "public HTTPS ingress passes the frozen TLS/port/size/latency contract";
46
+ readonly lifecycle: "verify";
47
+ }, {
48
+ readonly id: "tunnel-file-relay";
49
+ readonly description: "file relay behind the public ingress is reachable";
50
+ readonly lifecycle: "verify";
51
+ }, {
52
+ readonly id: "tunnel-diagnostics";
53
+ readonly description: "dev-tunnel configuration has actionable diagnostics";
54
+ readonly lifecycle: "doctor";
55
+ }];
56
+ };
57
+ readonly effects: readonly [{
58
+ readonly kind: "process";
59
+ readonly description: "Manage the dev-tunnel public ingress process";
60
+ readonly retention: "remove";
61
+ }, {
62
+ readonly kind: "network";
63
+ readonly description: "Probes the dev-tunnel public HTTPS ingress";
64
+ readonly retention: "preserve";
65
+ }, {
66
+ readonly kind: "external-resource";
67
+ readonly description: "Exposes the local platform via a public HTTPS tunnel";
68
+ readonly retention: "preserve";
69
+ }];
70
+ readonly documentation: readonly [{
71
+ readonly id: "overview";
72
+ readonly path: "./README.md";
73
+ readonly description: "Dev Tunnel module overview";
74
+ }];
75
+ };
@@ -0,0 +1,99 @@
1
+ export const descriptor = {
2
+ contract: "module",
3
+ contractVersion: "1.0.0",
4
+ moduleRef: "dev-tunnel",
5
+ packageName: "@tomflow/proflow-dev-tunnel",
6
+ moduleVersion: "0.1.0",
7
+ kind: "external-resource",
8
+ templateVersion: "1.0.0",
9
+ platformCompatibility: ">=1.0.0 <2.0.0",
10
+ installClass: "core",
11
+ identity: {
12
+ domain: "deployment-governance",
13
+ summary: "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.",
14
+ },
15
+ provides: [],
16
+ requires: [],
17
+ requirements: [
18
+ { kind: "executable", command: "devtunnel" },
19
+ {
20
+ kind: "human",
21
+ action: "Complete Microsoft Dev Tunnel login when required",
22
+ },
23
+ ],
24
+ configSlots: [
25
+ {
26
+ key: "publicBaseUrl",
27
+ type: "url",
28
+ required: true,
29
+ description: "Public HTTPS URL supplied by the dev-tunnel resource",
30
+ },
31
+ {
32
+ key: "tunnelId",
33
+ type: "string",
34
+ required: false,
35
+ description: "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop",
36
+ },
37
+ ],
38
+ lifecycle: {
39
+ supported: [
40
+ "describe",
41
+ "preflight",
42
+ "status",
43
+ "verify",
44
+ "doctor",
45
+ "start",
46
+ "stop",
47
+ "restart",
48
+ "uninstall",
49
+ ],
50
+ },
51
+ verification: {
52
+ checks: [
53
+ {
54
+ id: "tunnel-status",
55
+ description: "dev-tunnel process reports a real running or stopped state",
56
+ lifecycle: "status",
57
+ },
58
+ {
59
+ id: "tunnel-public-ingress",
60
+ description: "public HTTPS ingress passes the frozen TLS/port/size/latency contract",
61
+ lifecycle: "verify",
62
+ },
63
+ {
64
+ id: "tunnel-file-relay",
65
+ description: "file relay behind the public ingress is reachable",
66
+ lifecycle: "verify",
67
+ },
68
+ {
69
+ id: "tunnel-diagnostics",
70
+ description: "dev-tunnel configuration has actionable diagnostics",
71
+ lifecycle: "doctor",
72
+ },
73
+ ],
74
+ },
75
+ effects: [
76
+ {
77
+ kind: "process",
78
+ description: "Manage the dev-tunnel public ingress process",
79
+ retention: "remove",
80
+ },
81
+ {
82
+ kind: "network",
83
+ description: "Probes the dev-tunnel public HTTPS ingress",
84
+ retention: "preserve",
85
+ },
86
+ {
87
+ kind: "external-resource",
88
+ description: "Exposes the local platform via a public HTTPS tunnel",
89
+ retention: "preserve",
90
+ },
91
+ ],
92
+ documentation: [
93
+ {
94
+ id: "overview",
95
+ path: "./README.md",
96
+ description: "Dev Tunnel module overview",
97
+ },
98
+ ],
99
+ };
@@ -0,0 +1,7 @@
1
+ export declare const requirements: readonly [{
2
+ readonly kind: "executable";
3
+ readonly command: "devtunnel";
4
+ }, {
5
+ readonly kind: "human";
6
+ readonly action: "Complete Microsoft Dev Tunnel login when required";
7
+ }];
@@ -0,0 +1,2 @@
1
+ import { descriptor } from "./descriptor.js";
2
+ export const requirements = descriptor.requirements;
@@ -0,0 +1,19 @@
1
+ export declare const verification: {
2
+ readonly checks: readonly [{
3
+ readonly id: "tunnel-status";
4
+ readonly description: "dev-tunnel process reports a real running or stopped state";
5
+ readonly lifecycle: "status";
6
+ }, {
7
+ readonly id: "tunnel-public-ingress";
8
+ readonly description: "public HTTPS ingress passes the frozen TLS/port/size/latency contract";
9
+ readonly lifecycle: "verify";
10
+ }, {
11
+ readonly id: "tunnel-file-relay";
12
+ readonly description: "file relay behind the public ingress is reachable";
13
+ readonly lifecycle: "verify";
14
+ }, {
15
+ readonly id: "tunnel-diagnostics";
16
+ readonly description: "dev-tunnel configuration has actionable diagnostics";
17
+ readonly lifecycle: "doctor";
18
+ }];
19
+ };
@@ -0,0 +1,2 @@
1
+ import { descriptor } from "./descriptor.js";
2
+ export const verification = descriptor.verification;
@@ -0,0 +1,2 @@
1
+ export { behaviorAdapter, createBehaviorAdapter, } from "../deployment/adapter.ts";
2
+ export { descriptor } from "../deployment/descriptor.ts";
@@ -0,0 +1,2 @@
1
+ export { behaviorAdapter, createBehaviorAdapter, } from "../deployment/adapter.js";
2
+ export { descriptor } from "../deployment/descriptor.js";
@@ -0,0 +1,57 @@
1
+ export type DevTunnelState = "STOPPED" | "RUNNING" | "UNKNOWN";
2
+ export type DevTunnelLoginStatus = "LOGGED_IN" | "NOT_LOGGED_IN" | "UNKNOWN";
3
+ export interface DevTunnelObservation {
4
+ state: DevTunnelState;
5
+ login: DevTunnelLoginStatus;
6
+ publicBaseUrl?: string;
7
+ }
8
+ export interface CommandResult {
9
+ exitCode: number | null;
10
+ stdout: string;
11
+ stderr: string;
12
+ }
13
+ export type CommandRunner = (command: string, args: string[], options?: {
14
+ timeoutMs?: number;
15
+ }) => Promise<CommandResult>;
16
+ export interface DevTunnelRuntime {
17
+ readonly command: string;
18
+ status(): Promise<DevTunnelObservation>;
19
+ loginStatus(): Promise<DevTunnelLoginStatus>;
20
+ publicBaseUrl(): string | undefined;
21
+ start(): Promise<DevTunnelObservation>;
22
+ stop(): Promise<DevTunnelObservation>;
23
+ restart(): Promise<DevTunnelObservation>;
24
+ }
25
+ export declare function createDevTunnelRuntime(input: {
26
+ command?: string;
27
+ tunnelId?: string;
28
+ publicBaseUrl?: string;
29
+ runCommand?: CommandRunner;
30
+ }): DevTunnelRuntime;
31
+ export interface IngressCheck {
32
+ id: string;
33
+ status: "PASS" | "FAIL" | "WARN" | "SKIP";
34
+ message: string;
35
+ }
36
+ export interface PublicIngressVerification {
37
+ reachable: boolean;
38
+ ok: boolean;
39
+ checks: IngressCheck[];
40
+ }
41
+ export interface ErrorSemanticsProof {
42
+ rateLimit429Verified: boolean;
43
+ server5xxVerified: boolean;
44
+ message: string;
45
+ }
46
+ export interface FileRelayProof {
47
+ verified: boolean;
48
+ message: string;
49
+ }
50
+ export interface PublicIngressVerifyOptions {
51
+ verifyErrorSemantics?: () => Promise<ErrorSemanticsProof>;
52
+ verifyFileRelay?: () => Promise<FileRelayProof>;
53
+ timeoutMs?: number;
54
+ }
55
+ export declare function probeTlsProtocol(host: string, port: number, timeoutMs: number): Promise<string | undefined>;
56
+ export declare function readResponseChars(response: Response, ceiling: number): Promise<number | undefined>;
57
+ export declare function verifyPublicIngress(publicBaseUrl: string, options?: PublicIngressVerifyOptions): Promise<PublicIngressVerification>;
@@ -0,0 +1,309 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { connect } from "node:tls";
3
+ const LOGIN_ARGS = ["user", "show"];
4
+ const LOGIN_TIMEOUT_MS = 10_000;
5
+ const START_CONFIRM_MS = 500;
6
+ function defaultCommandRunner(command, args, options) {
7
+ return new Promise((resolve) => {
8
+ execFile(command, args, { timeout: options?.timeoutMs ?? 10_000 }, (error, stdout, stderr) => {
9
+ if (error) {
10
+ const err = error;
11
+ if (err.killed === true) {
12
+ resolve({
13
+ exitCode: null,
14
+ stdout: String(stdout ?? ""),
15
+ stderr: "command timed out",
16
+ });
17
+ return;
18
+ }
19
+ resolve({
20
+ exitCode: typeof err.code === "number" ? err.code : null,
21
+ stdout: String(stdout ?? ""),
22
+ stderr: String(stderr ?? ""),
23
+ });
24
+ return;
25
+ }
26
+ resolve({
27
+ exitCode: 0,
28
+ stdout: String(stdout ?? ""),
29
+ stderr: String(stderr ?? ""),
30
+ });
31
+ });
32
+ });
33
+ }
34
+ export function createDevTunnelRuntime(input) {
35
+ const command = input.command ?? "devtunnel";
36
+ const tunnelId = input.tunnelId;
37
+ const publicBaseUrl = input.publicBaseUrl;
38
+ const run = input.runCommand ?? defaultCommandRunner;
39
+ let child;
40
+ const observeLogin = async () => {
41
+ let result;
42
+ try {
43
+ result = await run(command, LOGIN_ARGS, { timeoutMs: LOGIN_TIMEOUT_MS });
44
+ }
45
+ catch {
46
+ return "UNKNOWN";
47
+ }
48
+ if (result.exitCode === 0) {
49
+ return result.stdout.trim().length > 0 ? "LOGGED_IN" : "NOT_LOGGED_IN";
50
+ }
51
+ return result.exitCode === null ? "UNKNOWN" : "NOT_LOGGED_IN";
52
+ };
53
+ const observe = async (state) => {
54
+ const login = await observeLogin();
55
+ return {
56
+ state,
57
+ login,
58
+ ...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
59
+ };
60
+ };
61
+ return {
62
+ command,
63
+ // A live owned child is RUNNING; without one we cannot prove the
64
+ // external tunnel has stopped, so we report UNKNOWN rather than a
65
+ // deterministic STOPPED.
66
+ status: () => observe(child ? "RUNNING" : "UNKNOWN"),
67
+ loginStatus: () => observeLogin(),
68
+ publicBaseUrl: () => publicBaseUrl,
69
+ async start() {
70
+ const login = await observeLogin();
71
+ if (login !== "LOGGED_IN")
72
+ return observe("UNKNOWN");
73
+ if (tunnelId === undefined) {
74
+ throw new TypeError("tunnelId is required to host the configured persistent tunnel");
75
+ }
76
+ if (child)
77
+ return observe("RUNNING");
78
+ child = spawn(command, ["host", tunnelId], { stdio: "ignore" });
79
+ const confirmed = await new Promise((resolve) => {
80
+ let settled = false;
81
+ const fail = () => {
82
+ if (settled)
83
+ return;
84
+ settled = true;
85
+ child = undefined;
86
+ resolve("FAILED");
87
+ };
88
+ child?.once("error", fail);
89
+ child?.once("exit", fail);
90
+ setTimeout(() => {
91
+ if (settled)
92
+ return;
93
+ settled = true;
94
+ resolve("RUNNING");
95
+ }, START_CONFIRM_MS);
96
+ });
97
+ if (confirmed === "FAILED") {
98
+ throw new Error("devtunnel host process failed to start");
99
+ }
100
+ return observe("RUNNING");
101
+ },
102
+ async stop() {
103
+ if (child) {
104
+ child.kill();
105
+ child = undefined;
106
+ return observe("STOPPED");
107
+ }
108
+ return observe("UNKNOWN");
109
+ },
110
+ async restart() {
111
+ const stopped = await this.stop();
112
+ if (stopped.state !== "STOPPED")
113
+ return stopped;
114
+ return this.start();
115
+ },
116
+ };
117
+ }
118
+ const TLS_PROTOCOL_RANK = {
119
+ TLSv1: 10,
120
+ "TLSv1.1": 11,
121
+ "TLSv1.2": 12,
122
+ "TLSv1.3": 13,
123
+ };
124
+ function tlsProtocolAtLeast(protocol, minimum) {
125
+ return ((TLS_PROTOCOL_RANK[protocol] ?? 0) >= (TLS_PROTOCOL_RANK[minimum] ?? 0));
126
+ }
127
+ export function probeTlsProtocol(host, port, timeoutMs) {
128
+ return new Promise((resolve) => {
129
+ let settled = false;
130
+ let timer;
131
+ const finish = (protocol) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ if (timer !== undefined)
136
+ clearTimeout(timer);
137
+ socket.destroy();
138
+ resolve(protocol);
139
+ };
140
+ const socket = connect({ host, port, servername: host, rejectUnauthorized: true }, () => finish(socket.getProtocol() ?? undefined));
141
+ timer = setTimeout(() => finish(undefined), timeoutMs);
142
+ socket.once("error", () => finish(undefined));
143
+ });
144
+ }
145
+ export async function readResponseChars(response, ceiling) {
146
+ const body = response.body;
147
+ if (body === null)
148
+ return 0;
149
+ const reader = body.getReader();
150
+ const decoder = new TextDecoder();
151
+ let chars = 0;
152
+ try {
153
+ for (;;) {
154
+ const { done, value } = await reader.read();
155
+ if (done) {
156
+ chars += decoder.decode().length;
157
+ break;
158
+ }
159
+ chars += decoder.decode(value, { stream: true }).length;
160
+ if (chars >= ceiling) {
161
+ await reader.cancel();
162
+ break;
163
+ }
164
+ }
165
+ return chars;
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ }
171
+ export async function verifyPublicIngress(publicBaseUrl, options) {
172
+ const timeoutMs = options?.timeoutMs ?? 45_000;
173
+ const checks = [];
174
+ let url;
175
+ try {
176
+ url = new URL(publicBaseUrl);
177
+ }
178
+ catch {
179
+ return {
180
+ reachable: false,
181
+ ok: false,
182
+ checks: [
183
+ {
184
+ id: "public-https-url",
185
+ status: "FAIL",
186
+ message: `publicBaseUrl is not a valid URL: ${publicBaseUrl}`,
187
+ },
188
+ ],
189
+ };
190
+ }
191
+ const isHttps = url.protocol === "https:";
192
+ const port = url.port === "" ? 443 : Number(url.port);
193
+ const isPort443 = port === 443;
194
+ checks.push({
195
+ id: "public-https-port-443",
196
+ status: isHttps && isPort443 ? "PASS" : "FAIL",
197
+ message: isHttps && isPort443
198
+ ? "public ingress is public HTTPS on port 443"
199
+ : `public ingress must be HTTPS on port 443 (got ${url.protocol}// port ${port})`,
200
+ });
201
+ if (!isHttps) {
202
+ return { reachable: false, ok: false, checks };
203
+ }
204
+ const tlsProtocol = await probeTlsProtocol(url.hostname, port, timeoutMs);
205
+ const tlsOk = tlsProtocol !== undefined && tlsProtocolAtLeast(tlsProtocol, "TLSv1.2");
206
+ checks.push({
207
+ id: "tls-version",
208
+ status: tlsProtocol === undefined ? "FAIL" : tlsOk ? "PASS" : "FAIL",
209
+ message: tlsProtocol === undefined
210
+ ? "could not negotiate a TLS connection to the public ingress within the timeout"
211
+ : tlsOk
212
+ ? `TLS handshake negotiated ${tlsProtocol} (>= TLSv1.2)`
213
+ : `TLS handshake negotiated ${tlsProtocol} (< TLSv1.2)`,
214
+ });
215
+ let statusCode = 0;
216
+ let responseChars = 0;
217
+ let roundTripMs;
218
+ const controller = new AbortController();
219
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
220
+ const startedAt = Date.now();
221
+ try {
222
+ const response = await fetch(url, {
223
+ method: "GET",
224
+ signal: controller.signal,
225
+ });
226
+ statusCode = response.status;
227
+ const chars = await readResponseChars(response, 100_000);
228
+ if (chars === undefined) {
229
+ roundTripMs = undefined;
230
+ }
231
+ else {
232
+ responseChars = chars;
233
+ roundTripMs = Date.now() - startedAt;
234
+ }
235
+ }
236
+ catch {
237
+ roundTripMs = undefined;
238
+ }
239
+ finally {
240
+ clearTimeout(timer);
241
+ }
242
+ const httpReachable = roundTripMs !== undefined && statusCode > 0;
243
+ checks.push({
244
+ id: "round-trip-ceiling",
245
+ status: httpReachable && roundTripMs !== undefined && roundTripMs <= timeoutMs
246
+ ? "PASS"
247
+ : "FAIL",
248
+ message: httpReachable && roundTripMs !== undefined
249
+ ? `probe round-trip completed in ${roundTripMs}ms (ceiling ${timeoutMs}ms)`
250
+ : "public ingress did not respond within the round-trip ceiling",
251
+ });
252
+ const sizeOk = responseChars < 100_000;
253
+ checks.push({
254
+ id: "request-response-size",
255
+ status: !httpReachable ? "FAIL" : sizeOk ? "PASS" : "FAIL",
256
+ message: !httpReachable
257
+ ? "could not measure the probe response size"
258
+ : sizeOk
259
+ ? `probe response is ${responseChars} chars (< 100,000)`
260
+ : `probe response is ${responseChars} chars (>= 100,000)`,
261
+ });
262
+ // File relay is a frozen hard contract; a missing real proof is a FAIL, never SKIP.
263
+ if (options?.verifyFileRelay === undefined) {
264
+ checks.push({
265
+ id: "file-relay-reachable",
266
+ status: "FAIL",
267
+ message: "file relay proof is not injected; relay reachability is unverified",
268
+ });
269
+ }
270
+ else {
271
+ const proof = await options.verifyFileRelay();
272
+ checks.push({
273
+ id: "file-relay-reachable",
274
+ status: proof.verified ? "PASS" : "FAIL",
275
+ message: proof.message,
276
+ });
277
+ }
278
+ // Real 429/5xx semantics are a frozen hard contract; missing proof is a FAIL.
279
+ if (options?.verifyErrorSemantics === undefined) {
280
+ checks.push({
281
+ id: "real-status-429",
282
+ status: "FAIL",
283
+ message: "429 proof is not injected; rate-limit semantics are unverified",
284
+ }, {
285
+ id: "real-status-5xx",
286
+ status: "FAIL",
287
+ message: "5xx proof is not injected; server-error semantics are unverified",
288
+ });
289
+ }
290
+ else {
291
+ const proof = await options.verifyErrorSemantics();
292
+ checks.push({
293
+ id: "real-status-429",
294
+ status: proof.rateLimit429Verified ? "PASS" : "FAIL",
295
+ message: proof.rateLimit429Verified
296
+ ? "429 rate-limit semantics verified"
297
+ : "429 rate-limit semantics unverified",
298
+ }, {
299
+ id: "real-status-5xx",
300
+ status: proof.server5xxVerified ? "PASS" : "FAIL",
301
+ message: proof.server5xxVerified
302
+ ? "5xx server-error semantics verified"
303
+ : "5xx server-error semantics unverified",
304
+ });
305
+ }
306
+ const reachable = httpReachable;
307
+ const ok = checks.every((check) => check.status === "PASS");
308
+ return { reachable, ok, checks };
309
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@tomflow/proflow-dev-tunnel",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "exports": {
9
+ ".": "./dist/src/index.js",
10
+ "./deployment/adapter": "./dist/deployment/adapter.js",
11
+ "./deployment/descriptor": "./dist/deployment/descriptor.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "proflow.module.json",
16
+ "conformance.json",
17
+ "README.md",
18
+ "self-install.mjs"
19
+ ],
20
+ "dependencies": {
21
+ "@tomflow/proflow-module-contract": "^0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@tomflow/proflow-deployment-conformance": "^0.1.0"
25
+ },
26
+ "description": "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.",
27
+ "keywords": [
28
+ "proflow",
29
+ "proflow-module",
30
+ "deployment-governance"
31
+ ],
32
+ "proflow": {
33
+ "module": true,
34
+ "installClass": "core",
35
+ "installRequires": [
36
+ "@tomflow/proflow-module-contract"
37
+ ],
38
+ "descriptor": "./dist/deployment/descriptor.js",
39
+ "manifest": "./proflow.module.json"
40
+ },
41
+ "bin": {
42
+ "proflow-dev-tunnel": "./self-install.mjs"
43
+ },
44
+ "scripts": {
45
+ "test": "node --test tests/**/*.test.ts",
46
+ "typecheck": "tsc --noEmit"
47
+ }
48
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "contract": "module",
3
+ "contractVersion": "1.0.0",
4
+ "moduleRef": "dev-tunnel",
5
+ "packageName": "@tomflow/proflow-dev-tunnel",
6
+ "moduleVersion": "0.1.0",
7
+ "kind": "external-resource",
8
+ "templateVersion": "1.0.0",
9
+ "platformCompatibility": ">=1.0.0 <2.0.0",
10
+ "installClass": "core",
11
+ "identity": {
12
+ "domain": "deployment-governance",
13
+ "summary": "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process."
14
+ },
15
+ "provides": [],
16
+ "requires": [],
17
+ "requirements": [
18
+ {
19
+ "kind": "executable",
20
+ "command": "devtunnel"
21
+ },
22
+ {
23
+ "kind": "human",
24
+ "action": "Complete Microsoft Dev Tunnel login when required"
25
+ }
26
+ ],
27
+ "configSlots": [
28
+ {
29
+ "key": "publicBaseUrl",
30
+ "type": "url",
31
+ "required": true,
32
+ "description": "Public HTTPS URL supplied by the dev-tunnel resource"
33
+ },
34
+ {
35
+ "key": "tunnelId",
36
+ "type": "string",
37
+ "required": false,
38
+ "description": "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop"
39
+ }
40
+ ],
41
+ "lifecycle": {
42
+ "supported": [
43
+ "describe",
44
+ "preflight",
45
+ "status",
46
+ "verify",
47
+ "doctor",
48
+ "start",
49
+ "stop",
50
+ "restart",
51
+ "uninstall"
52
+ ]
53
+ },
54
+ "verification": {
55
+ "checks": [
56
+ {
57
+ "id": "tunnel-status",
58
+ "description": "dev-tunnel process reports a real running or stopped state",
59
+ "lifecycle": "status"
60
+ },
61
+ {
62
+ "id": "tunnel-public-ingress",
63
+ "description": "public HTTPS ingress passes the frozen TLS/port/size/latency contract",
64
+ "lifecycle": "verify"
65
+ },
66
+ {
67
+ "id": "tunnel-file-relay",
68
+ "description": "file relay behind the public ingress is reachable",
69
+ "lifecycle": "verify"
70
+ },
71
+ {
72
+ "id": "tunnel-diagnostics",
73
+ "description": "dev-tunnel configuration has actionable diagnostics",
74
+ "lifecycle": "doctor"
75
+ }
76
+ ]
77
+ },
78
+ "effects": [
79
+ {
80
+ "kind": "process",
81
+ "description": "Manage the dev-tunnel public ingress process",
82
+ "retention": "remove"
83
+ },
84
+ {
85
+ "kind": "network",
86
+ "description": "Probes the dev-tunnel public HTTPS ingress",
87
+ "retention": "preserve"
88
+ },
89
+ {
90
+ "kind": "external-resource",
91
+ "description": "Exposes the local platform via a public HTTPS tunnel",
92
+ "retention": "preserve"
93
+ }
94
+ ],
95
+ "documentation": [
96
+ {
97
+ "id": "overview",
98
+ "path": "./README.md",
99
+ "description": "Dev Tunnel module overview"
100
+ }
101
+ ]
102
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ const [command, ...rest] = process.argv.slice(2);
5
+ const usage = "Usage: npx @tomflow/proflow-dev-tunnel install\n";
6
+ if (command === "--help" || command === "-h") {
7
+ process.stdout.write(usage);
8
+ process.exit(0);
9
+ }
10
+ if (command !== "install" || rest.length > 0) {
11
+ process.stderr.write(usage);
12
+ process.exit(2);
13
+ }
14
+ const executable = process.platform === "win32" ? "npx.cmd" : "npx";
15
+ const result = spawnSync(
16
+ executable,
17
+ [
18
+ "--yes",
19
+ "@tomflow/proflow-platform-cli",
20
+ "install",
21
+ "@tomflow/proflow-dev-tunnel",
22
+ ],
23
+ { cwd: process.cwd(), env: process.env, stdio: "inherit" },
24
+ );
25
+ if (result.error) throw result.error;
26
+ process.exit(result.status ?? 1);