@tomflow/proflow-dev-tunnel 0.1.6 → 0.1.7

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/DOCS.md ADDED
@@ -0,0 +1,27 @@
1
+ # @tomflow/proflow-dev-tunnel — Module Docs
2
+
3
+ Module ref: `dev-tunnel`
4
+ Domain: `deployment-governance`
5
+ Kind: `external-resource`
6
+
7
+ Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.
8
+
9
+ ## Standard management surface
10
+
11
+ `install / uninstall / status / setup / docs / start / stop`
12
+
13
+ ## Provides
14
+
15
+ - None.
16
+
17
+ ## Requires
18
+
19
+ - None.
20
+
21
+ ## Public setup inputs
22
+
23
+ - None. Deterministic/private values and cross-Module shared facts are not public configuration.
24
+
25
+ ## Ownership boundary
26
+
27
+ Module business APIs and any module-specific extra commands remain package-owned. Platform does not proxy or interpret them. Current human/external preparation is documented in `SETUP.md`.
package/SETUP.md ADDED
@@ -0,0 +1,7 @@
1
+ # @tomflow/proflow-dev-tunnel — Module Setup
2
+
3
+ Authenticate with Microsoft Dev Tunnels when requested, create or select the required tunnel, and allow the Module to establish and verify the public HTTPS ingress. The resulting tunnel id/public URL/evidence are Module-owned state or shared facts, not values to copy into Platform config.
4
+
5
+ ## Completion
6
+
7
+ Setup is complete only when the Module's own `status` reports `setupStatus=READY`. Re-running setup must re-observe reality; it must not resume from a blind historical step index.
@@ -1,141 +1,137 @@
1
- import { type ModuleOperationResult } from "@tomflow/proflow-module-contract";
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>;
12
- export declare function createBehaviorAdapter(input?: {
13
- runtime: DevTunnelRuntime;
14
- verifyErrorSemantics?: () => Promise<ErrorSemanticsProof>;
15
- verifyFileRelay?: () => Promise<FileRelayProof>;
16
- }, config?: Record<string, string>): {
17
- describe: () => {
18
- result: ModuleOperationResult;
19
- observedEffects: never[];
20
- };
21
- preflight: () => {
22
- result: ModuleOperationResult;
23
- observedEffects: never[];
24
- };
25
- status: () => Promise<{
26
- result: ModuleOperationResult;
1
+ import { type ModuleCommandContext } from "@tomflow/proflow-module-contract";
2
+ export declare const behaviorAdapter: {
3
+ readonly install: (context: ModuleCommandContext) => Promise<{
4
+ result: {
5
+ readonly contract: "deployment.result.v1";
6
+ readonly ok: true;
7
+ readonly status: "SUCCEEDED";
8
+ readonly moduleRef: "dev-tunnel";
9
+ readonly moduleVersion: "0.1.7";
10
+ };
27
11
  observedEffects: never[];
28
12
  }>;
29
- verify: () => Promise<{
30
- result: ModuleOperationResult;
31
- observedEffects: never[];
13
+ readonly uninstall: (context: ModuleCommandContext) => Promise<{
14
+ result: {
15
+ readonly contract: "deployment.result.v1";
16
+ readonly ok: true;
17
+ readonly status: "SUCCEEDED";
18
+ readonly moduleRef: "dev-tunnel";
19
+ readonly moduleVersion: "0.1.7";
20
+ };
21
+ observedEffects: string[];
32
22
  } | {
33
23
  result: {
34
24
  contract: "deployment.result.v1";
35
- ok: boolean;
36
- status: "ACTION_REQUIRED" | "BLOCKED" | "FAILED" | "SUCCEEDED";
37
- moduleRef: string;
38
- moduleVersion: string;
39
- actionRequired?: {
40
- action: string;
41
- description: string;
42
- } | undefined;
43
- error?: {
44
- 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";
25
+ moduleRef: "dev-tunnel";
26
+ moduleVersion: "0.1.7";
27
+ ok: false;
28
+ status: "FAILED";
29
+ error: {
30
+ code: "UNINSTALL_FAILED";
45
31
  message: string;
46
32
  retryable: boolean;
47
- } | undefined;
48
- resourceVersion?: string | undefined;
49
- data?: unknown;
50
- checks: import("../src/resource-adapter.ts").IngressCheck[];
33
+ };
51
34
  };
52
- observedEffects: string[];
53
- }>;
54
- doctor: () => Promise<{
55
- result: ModuleOperationResult;
56
35
  observedEffects: never[];
57
36
  }>;
58
- start: () => Promise<{
59
- result: ModuleOperationResult;
60
- observedEffects: string[];
61
- }>;
62
- stop: () => Promise<{
63
- result: ModuleOperationResult;
64
- observedEffects: string[];
65
- }>;
66
- uninstall: () => Promise<{
67
- result: ModuleOperationResult;
68
- observedEffects: string[];
69
- }>;
70
- restart: () => Promise<{
71
- result: ModuleOperationResult;
72
- observedEffects: string[];
73
- }>;
74
- };
75
- export declare const behaviorAdapter: {
76
- describe: () => {
77
- result: ModuleOperationResult;
37
+ readonly status: (context: ModuleCommandContext) => Promise<{
38
+ result: {
39
+ contract: "deployment.result.v1";
40
+ ok: true;
41
+ status: "SUCCEEDED";
42
+ moduleRef: "dev-tunnel";
43
+ moduleVersion: "0.1.7";
44
+ data: {
45
+ setupStatus: "ACTION_REQUIRED" | "READY";
46
+ runtimeStatus: "FAILED" | "RUNNING" | "STOPPED";
47
+ };
48
+ };
78
49
  observedEffects: never[];
79
- };
80
- preflight: () => {
81
- result: ModuleOperationResult;
50
+ }>;
51
+ readonly setup: (context: ModuleCommandContext) => Promise<{
52
+ result: {
53
+ contract: "deployment.result.v1";
54
+ moduleRef: "dev-tunnel";
55
+ moduleVersion: "0.1.7";
56
+ ok: false;
57
+ status: "ACTION_REQUIRED";
58
+ actionRequired: {
59
+ action: string;
60
+ description: string;
61
+ };
62
+ };
82
63
  observedEffects: never[];
83
- };
84
- status: () => Promise<{
85
- result: ModuleOperationResult;
64
+ } | {
65
+ result: {
66
+ readonly contract: "deployment.result.v1";
67
+ readonly ok: true;
68
+ readonly status: "SUCCEEDED";
69
+ readonly moduleRef: "dev-tunnel";
70
+ readonly moduleVersion: "0.1.7";
71
+ };
86
72
  observedEffects: never[];
87
73
  }>;
88
- verify: () => Promise<{
89
- result: ModuleOperationResult;
74
+ readonly docs: (_context: ModuleCommandContext) => Promise<{
75
+ result: {
76
+ contract: "deployment.result.v1";
77
+ ok: true;
78
+ status: "SUCCEEDED";
79
+ moduleRef: "dev-tunnel";
80
+ moduleVersion: "0.1.7";
81
+ data: {
82
+ docs: string;
83
+ setup: string;
84
+ };
85
+ };
90
86
  observedEffects: never[];
91
- } | {
87
+ }>;
88
+ readonly start: (context: ModuleCommandContext) => Promise<{
92
89
  result: {
93
90
  contract: "deployment.result.v1";
94
- ok: boolean;
95
- status: "ACTION_REQUIRED" | "BLOCKED" | "FAILED" | "SUCCEEDED";
96
- moduleRef: string;
97
- moduleVersion: string;
98
- actionRequired?: {
99
- action: string;
100
- description: string;
101
- } | undefined;
102
- error?: {
103
- 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";
91
+ moduleRef: "dev-tunnel";
92
+ moduleVersion: "0.1.7";
93
+ ok: false;
94
+ status: "FAILED";
95
+ error: {
96
+ code: "START_FAILED";
104
97
  message: string;
105
98
  retryable: boolean;
106
- } | undefined;
107
- resourceVersion?: string | undefined;
108
- data?: unknown;
109
- checks: import("../src/resource-adapter.ts").IngressCheck[];
99
+ };
110
100
  };
111
- observedEffects: string[];
112
- }>;
113
- doctor: () => Promise<{
114
- result: ModuleOperationResult;
115
101
  observedEffects: never[];
116
- }>;
117
- start: () => Promise<{
118
- result: ModuleOperationResult;
119
- observedEffects: string[];
120
- }>;
121
- stop: () => Promise<{
122
- result: ModuleOperationResult;
123
- observedEffects: string[];
124
- }>;
125
- uninstall: () => Promise<{
126
- result: ModuleOperationResult;
102
+ } | {
103
+ result: {
104
+ contract: "deployment.result.v1";
105
+ ok: true;
106
+ status: "SUCCEEDED";
107
+ moduleRef: "dev-tunnel";
108
+ moduleVersion: "0.1.7";
109
+ data: import("../src/resource-adapter.ts").DevTunnelObservation;
110
+ };
127
111
  observedEffects: string[];
128
112
  }>;
129
- restart: () => Promise<{
130
- result: ModuleOperationResult;
113
+ readonly stop: (context: ModuleCommandContext) => Promise<{
114
+ result: {
115
+ readonly contract: "deployment.result.v1";
116
+ readonly ok: true;
117
+ readonly status: "SUCCEEDED";
118
+ readonly moduleRef: "dev-tunnel";
119
+ readonly moduleVersion: "0.1.7";
120
+ };
131
121
  observedEffects: string[];
122
+ } | {
123
+ result: {
124
+ contract: "deployment.result.v1";
125
+ moduleRef: "dev-tunnel";
126
+ moduleVersion: "0.1.7";
127
+ ok: false;
128
+ status: "FAILED";
129
+ error: {
130
+ code: "STOP_FAILED";
131
+ message: string;
132
+ retryable: boolean;
133
+ };
134
+ };
135
+ observedEffects: never[];
132
136
  }>;
133
137
  };
134
- export declare function createProductionBinding(input: {
135
- moduleRef: string;
136
- config: Record<string, string>;
137
- workspaceRoot: string;
138
- }): Promise<{
139
- behaviorAdapter: Record<string, unknown>;
140
- } | undefined>;
141
- export {};
@@ -1,43 +1,29 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { observeDeclaredModuleStatus, } from "@tomflow/proflow-module-contract";
4
- import { verifyPublicIngress } from "../src/resource-adapter.js";
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
4
+ import { createDevTunnelRuntime } from "../src/resource-adapter.js";
5
5
  import { descriptor } from "./descriptor.js";
6
- const success = (data) => ({
6
+ const base = {
7
7
  contract: "deployment.result.v1",
8
8
  ok: true,
9
9
  status: "SUCCEEDED",
10
10
  moduleRef: descriptor.moduleRef,
11
11
  moduleVersion: descriptor.moduleVersion,
12
- ...(data === undefined ? {} : { data }),
13
- });
14
- const actionRequired = (action, description) => ({
15
- contract: "deployment.result.v1",
16
- ok: false,
17
- status: "ACTION_REQUIRED",
18
- moduleRef: descriptor.moduleRef,
19
- moduleVersion: descriptor.moduleVersion,
20
- actionRequired: { action, description },
21
- });
22
- export async function readDevTunnelVerificationEvidence(file, publicBaseUrl) {
23
- if (!file)
24
- return undefined;
12
+ };
13
+ const processEffect = "Manage the dev-tunnel public ingress process";
14
+ const stateDir = (context) => join(resolve(context.workspaceRoot), ".proflow", "runtime", "external-resources", "dev-tunnel");
15
+ const stateFile = (context) => join(stateDir(context), "setup.json");
16
+ const processFile = (context) => join(stateDir(context), "process.json");
17
+ async function readState(context) {
25
18
  try {
26
- const raw = JSON.parse(await readFile(file, "utf8"));
27
- if (raw.contract !== "proflow.dev-tunnel-verification.v1" ||
28
- raw.moduleVersion !== descriptor.moduleVersion ||
29
- raw.publicBaseUrl !== publicBaseUrl ||
30
- typeof raw.observedAt !== "string" ||
31
- Number.isNaN(Date.parse(raw.observedAt)) ||
32
- typeof raw.fileRelay !== "object" ||
33
- raw.fileRelay === null ||
34
- typeof raw.fileRelay.verified !== "boolean" ||
35
- typeof raw.fileRelay.message !== "string" ||
36
- typeof raw.errorSemantics !== "object" ||
37
- raw.errorSemantics === null ||
38
- typeof raw.errorSemantics.rateLimit429Verified !== "boolean" ||
39
- typeof raw.errorSemantics.server5xxVerified !== "boolean" ||
40
- typeof raw.errorSemantics.message !== "string")
19
+ const raw = JSON.parse(await readFile(stateFile(context), "utf8"));
20
+ if (raw.contract !== "proflow.dev-tunnel-setup.v1" ||
21
+ typeof raw.tunnelId !== "string" ||
22
+ !raw.tunnelId ||
23
+ typeof raw.publicBaseUrl !== "string")
24
+ return undefined;
25
+ const url = new URL(raw.publicBaseUrl);
26
+ if (url.protocol !== "https:")
41
27
  return undefined;
42
28
  return raw;
43
29
  }
@@ -45,254 +31,307 @@ export async function readDevTunnelVerificationEvidence(file, publicBaseUrl) {
45
31
  return undefined;
46
32
  }
47
33
  }
48
- export function createBehaviorAdapter(input, config) {
34
+ async function writeState(context, state) {
35
+ await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
36
+ const tmp = `${stateFile(context)}.${process.pid}.tmp`;
37
+ await writeFile(tmp, `${JSON.stringify(state, null, 2)}
38
+ `, { encoding: "utf8", mode: 0o600 });
39
+ await rename(tmp, stateFile(context));
40
+ }
41
+ function setupInput(context) {
42
+ if (typeof context.input !== "object" ||
43
+ context.input === null ||
44
+ Array.isArray(context.input))
45
+ return {};
46
+ const tunnelId = Reflect.get(context.input, "tunnelId");
47
+ const publicBaseUrl = Reflect.get(context.input, "publicBaseUrl");
49
48
  return {
50
- describe: () => ({
51
- result: success({
52
- publicApi: ["status", "verify", "doctor", "start", "stop", "restart"],
53
- }),
54
- observedEffects: [],
55
- }),
56
- preflight: () => ({
57
- result: input
58
- ? success()
59
- : actionRequired("configure-tunnel", "Bind a dev-tunnel resource with login status and a public HTTPS ingress"),
60
- observedEffects: [],
61
- }),
62
- status: async () => {
63
- const observation = input ? await input.runtime.status() : undefined;
64
- const runtimeStatus = observation?.state === "RUNNING"
65
- ? "RUNNING"
66
- : observation?.state === "STOPPED"
67
- ? "STOPPED"
68
- : input
69
- ? "FAILED"
70
- : "UNKNOWN";
49
+ ...(typeof tunnelId === "string" && tunnelId ? { tunnelId } : {}),
50
+ ...(typeof publicBaseUrl === "string" && publicBaseUrl
51
+ ? { publicBaseUrl }
52
+ : {}),
53
+ };
54
+ }
55
+ function runtime(context, state) {
56
+ return createDevTunnelRuntime({
57
+ ...(state
58
+ ? { tunnelId: state.tunnelId, publicBaseUrl: state.publicBaseUrl }
59
+ : {}),
60
+ processStateFile: processFile(context),
61
+ });
62
+ }
63
+ export const behaviorAdapter = {
64
+ install: async (context) => {
65
+ await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
66
+ const state = await readState(context);
67
+ if (state)
68
+ await writeModuleSharedFacts(context, descriptor.moduleRef, {
69
+ tunnelId: state.tunnelId,
70
+ publicBaseUrl: state.publicBaseUrl,
71
+ });
72
+ return { result: base, observedEffects: [] };
73
+ },
74
+ uninstall: async (context) => {
75
+ const state = await readState(context);
76
+ if (!state)
77
+ return { result: base, observedEffects: [] };
78
+ try {
79
+ const stopped = await runtime(context, state).stop();
80
+ return stopped.state === "STOPPED"
81
+ ? { result: base, observedEffects: [processEffect] }
82
+ : {
83
+ result: {
84
+ ...base,
85
+ ok: false,
86
+ status: "FAILED",
87
+ error: {
88
+ code: "UNINSTALL_FAILED",
89
+ message: "dev-tunnel stop state is UNKNOWN",
90
+ retryable: true,
91
+ },
92
+ },
93
+ observedEffects: [],
94
+ };
95
+ }
96
+ catch (error) {
71
97
  return {
72
- result: success(observeDeclaredModuleStatus(descriptor, config, runtimeStatus)),
98
+ result: {
99
+ ...base,
100
+ ok: false,
101
+ status: "FAILED",
102
+ error: {
103
+ code: "UNINSTALL_FAILED",
104
+ message: error instanceof Error
105
+ ? error.message
106
+ : "failed to stop dev-tunnel",
107
+ retryable: true,
108
+ },
109
+ },
73
110
  observedEffects: [],
74
111
  };
75
- },
76
- verify: async () => {
77
- if (!input) {
78
- return {
79
- result: actionRequired("configure-tunnel", "A live dev-tunnel resource is required for public ingress verification"),
80
- observedEffects: [],
81
- };
82
- }
83
- if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
84
- return {
85
- result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before verifying public ingress"),
86
- observedEffects: [],
87
- };
88
- }
89
- const publicBaseUrl = input.runtime.publicBaseUrl();
90
- if (publicBaseUrl === undefined) {
91
- return {
92
- result: actionRequired("configure-tunnel", "publicBaseUrl must be configured before public ingress verification"),
93
- observedEffects: [],
94
- };
95
- }
96
- const verification = await verifyPublicIngress(publicBaseUrl, {
97
- ...(input.verifyErrorSemantics === undefined
98
- ? {}
99
- : { verifyErrorSemantics: input.verifyErrorSemantics }),
100
- ...(input.verifyFileRelay === undefined
101
- ? {}
102
- : { verifyFileRelay: input.verifyFileRelay }),
103
- });
112
+ }
113
+ },
114
+ status: async (context) => {
115
+ const state = await readState(context);
116
+ if (!state) {
104
117
  return {
105
- result: verification.ok
106
- ? { ...success(), checks: verification.checks }
107
- : {
108
- ...actionRequired("repair-tunnel-ingress", failureReason(verification)),
109
- checks: verification.checks,
118
+ result: {
119
+ ...base,
120
+ data: {
121
+ setupStatus: "ACTION_REQUIRED",
122
+ runtimeStatus: "STOPPED",
110
123
  },
111
- observedEffects: verification.reachable
112
- ? ["Probes the dev-tunnel public HTTPS ingress"]
113
- : [],
124
+ },
125
+ observedEffects: [],
114
126
  };
115
- },
116
- doctor: async () => {
117
- if (!input) {
118
- return {
119
- result: actionRequired("configure-tunnel", "dev-tunnel login and public ingress configuration are required for diagnostics"),
120
- observedEffects: [],
121
- };
122
- }
123
- const login = await input.runtime.loginStatus();
124
- const status = await input.runtime.status();
125
- const publicBaseUrl = input.runtime.publicBaseUrl();
126
- const checks = [
127
- {
128
- id: "tunnel-login",
129
- status: login === "LOGGED_IN" ? "PASS" : "FAIL",
130
- message: `login status is ${login}`,
127
+ }
128
+ await writeModuleSharedFacts(context, descriptor.moduleRef, {
129
+ tunnelId: state.tunnelId,
130
+ publicBaseUrl: state.publicBaseUrl,
131
+ });
132
+ const rt = runtime(context, state);
133
+ const login = await rt.loginStatus();
134
+ const observed = await rt.status();
135
+ const configured = state !== undefined && login === "LOGGED_IN";
136
+ const runtimeStatus = observed.state === "RUNNING"
137
+ ? "RUNNING"
138
+ : observed.state === "STOPPED"
139
+ ? "STOPPED"
140
+ : configured
141
+ ? "FAILED"
142
+ : "STOPPED";
143
+ return {
144
+ result: {
145
+ ...base,
146
+ data: {
147
+ setupStatus: configured
148
+ ? "READY"
149
+ : "ACTION_REQUIRED",
150
+ runtimeStatus,
131
151
  },
132
- {
133
- id: "tunnel-state",
134
- status: status.state === "RUNNING"
135
- ? "PASS"
136
- : status.state === "STOPPED"
137
- ? "WARN"
138
- : "FAIL",
139
- message: `tunnel state is ${status.state}`,
152
+ },
153
+ observedEffects: [],
154
+ };
155
+ },
156
+ setup: async (context) => {
157
+ await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
158
+ const previous = await readState(context);
159
+ const supplied = setupInput(context);
160
+ const candidate = { ...(previous ?? {}), ...supplied };
161
+ const rt = runtime(context, previous);
162
+ const login = await rt.loginStatus();
163
+ if (login !== "LOGGED_IN")
164
+ return {
165
+ result: {
166
+ ...base,
167
+ ok: false,
168
+ status: "ACTION_REQUIRED",
169
+ actionRequired: {
170
+ action: "complete-tunnel-login",
171
+ description: "Complete Microsoft Dev Tunnel login, then rerun setup.",
172
+ },
140
173
  },
141
- {
142
- id: "tunnel-public-url",
143
- status: publicBaseUrl === undefined ? "FAIL" : "PASS",
144
- message: publicBaseUrl === undefined
145
- ? "publicBaseUrl is not configured"
146
- : `publicBaseUrl is ${publicBaseUrl}`,
174
+ observedEffects: [],
175
+ };
176
+ if (typeof candidate.tunnelId !== "string" ||
177
+ typeof candidate.publicBaseUrl !== "string")
178
+ return {
179
+ result: {
180
+ ...base,
181
+ ok: false,
182
+ status: "ACTION_REQUIRED",
183
+ actionRequired: {
184
+ action: "select-or-create-tunnel",
185
+ description: "Create or select the persistent Dev Tunnel and provide tunnelId plus publicBaseUrl to this setup step.",
186
+ },
147
187
  },
148
- ];
149
- const healthy = login === "LOGGED_IN" &&
150
- status.state === "RUNNING" &&
151
- publicBaseUrl !== undefined;
188
+ observedEffects: [],
189
+ };
190
+ try {
191
+ const url = new URL(candidate.publicBaseUrl);
192
+ if (url.protocol !== "https:")
193
+ throw new TypeError("publicBaseUrl must be HTTPS");
194
+ const state = {
195
+ contract: "proflow.dev-tunnel-setup.v1",
196
+ tunnelId: candidate.tunnelId,
197
+ publicBaseUrl: url.href,
198
+ };
199
+ await writeState(context, state);
200
+ await writeModuleSharedFacts(context, descriptor.moduleRef, {
201
+ tunnelId: state.tunnelId,
202
+ publicBaseUrl: state.publicBaseUrl,
203
+ });
204
+ return { result: base, observedEffects: [] };
205
+ }
206
+ catch (error) {
152
207
  return {
153
- result: healthy
154
- ? { ...success(), checks }
155
- : {
156
- ...actionRequired("repair-tunnel", "dev-tunnel resource is not healthy"),
157
- checks,
208
+ result: {
209
+ ...base,
210
+ ok: false,
211
+ status: "ACTION_REQUIRED",
212
+ actionRequired: {
213
+ action: "correct-tunnel-facts",
214
+ description: error instanceof Error
215
+ ? error.message
216
+ : "Tunnel setup facts are invalid",
158
217
  },
218
+ },
159
219
  observedEffects: [],
160
220
  };
161
- },
162
- start: async () => {
163
- if (!input) {
164
- return {
165
- result: actionRequired("configure-tunnel", "Cannot start without a bound dev-tunnel resource"),
166
- observedEffects: [],
167
- };
168
- }
169
- if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
170
- return {
171
- result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before starting the tunnel"),
172
- observedEffects: [],
173
- };
174
- }
175
- try {
176
- const observation = await input.runtime.start();
177
- return {
178
- result: success(observation),
179
- observedEffects: ["Manage the dev-tunnel public ingress process"],
180
- };
181
- }
182
- catch (error) {
183
- return {
184
- result: actionRequired("start-tunnel", error instanceof Error
185
- ? error.message
186
- : "failed to start the dev-tunnel process"),
187
- observedEffects: [],
188
- };
189
- }
190
- },
191
- stop: async () => {
192
- if (!input) {
193
- return {
194
- result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to stop"),
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
- }
221
+ }
222
+ },
223
+ docs: async (_context) => ({
224
+ result: { ...base, data: { docs: "DOCS.md", setup: "SETUP.md" } },
225
+ observedEffects: [],
226
+ }),
227
+ start: async (context) => {
228
+ const state = await readState(context);
229
+ if (!state)
205
230
  return {
206
- result: actionRequired("complete-tunnel-stop", "dev-tunnel stop state is UNKNOWN; cannot confirm the tunnel stopped"),
231
+ result: {
232
+ ...base,
233
+ ok: false,
234
+ status: "FAILED",
235
+ error: {
236
+ code: "START_FAILED",
237
+ message: "dev-tunnel setup is not READY",
238
+ retryable: true,
239
+ },
240
+ },
207
241
  observedEffects: [],
208
242
  };
209
- },
210
- uninstall: async () => {
211
- if (!input) {
212
- return {
213
- // No bound tunnel means there is no managed external resource to
214
- // stop. Whole-instance uninstall must remain idempotent.
215
- result: success(),
216
- observedEffects: [],
217
- };
218
- }
219
- const stopped = await input.runtime.stop();
220
- if (stopped.state === "STOPPED") {
221
- return {
222
- result: success(),
223
- observedEffects: ["Manage the dev-tunnel public ingress process"],
224
- };
225
- }
243
+ const rt = runtime(context, state);
244
+ if ((await rt.loginStatus()) !== "LOGGED_IN")
226
245
  return {
227
- result: actionRequired("complete-tunnel-stop", "dev-tunnel stop state is UNKNOWN; package removal cannot continue"),
246
+ result: {
247
+ ...base,
248
+ ok: false,
249
+ status: "FAILED",
250
+ error: {
251
+ code: "START_FAILED",
252
+ message: "Microsoft Dev Tunnel login is not ready",
253
+ retryable: true,
254
+ },
255
+ },
228
256
  observedEffects: [],
229
257
  };
230
- },
231
- restart: async () => {
232
- if (!input) {
233
- return {
234
- result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to restart"),
235
- observedEffects: [],
236
- };
237
- }
238
- if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
239
- return {
240
- result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before restarting the tunnel"),
241
- observedEffects: [],
242
- };
243
- }
244
- const stopped = await input.runtime.stop();
245
- if (stopped.state !== "STOPPED") {
246
- return {
247
- result: actionRequired("complete-tunnel-stop", "Cannot restart: dev-tunnel stop state is UNKNOWN"),
258
+ try {
259
+ const observed = await rt.start();
260
+ return observed.state === "RUNNING"
261
+ ? {
262
+ result: { ...base, data: observed },
263
+ observedEffects: [processEffect],
264
+ }
265
+ : {
266
+ result: {
267
+ ...base,
268
+ ok: false,
269
+ status: "FAILED",
270
+ error: {
271
+ code: "START_FAILED",
272
+ message: "dev-tunnel did not reach RUNNING",
273
+ retryable: true,
274
+ },
275
+ },
248
276
  observedEffects: [],
249
277
  };
250
- }
251
- try {
252
- const observation = await input.runtime.start();
253
- return {
254
- result: success(observation),
255
- observedEffects: ["Manage the dev-tunnel public ingress process"],
256
- };
257
- }
258
- catch (error) {
259
- return {
260
- result: actionRequired("start-tunnel", error instanceof Error
261
- ? error.message
262
- : "failed to start the dev-tunnel process"),
278
+ }
279
+ catch (error) {
280
+ return {
281
+ result: {
282
+ ...base,
283
+ ok: false,
284
+ status: "FAILED",
285
+ error: {
286
+ code: "START_FAILED",
287
+ message: error instanceof Error
288
+ ? error.message
289
+ : "failed to start dev-tunnel",
290
+ retryable: true,
291
+ },
292
+ },
293
+ observedEffects: [],
294
+ };
295
+ }
296
+ },
297
+ stop: async (context) => {
298
+ const state = await readState(context);
299
+ if (!state)
300
+ return { result: base, observedEffects: [] };
301
+ try {
302
+ const observed = await runtime(context, state).stop();
303
+ return observed.state === "STOPPED"
304
+ ? { result: base, observedEffects: [processEffect] }
305
+ : {
306
+ result: {
307
+ ...base,
308
+ ok: false,
309
+ status: "FAILED",
310
+ error: {
311
+ code: "STOP_FAILED",
312
+ message: "dev-tunnel stop state is UNKNOWN",
313
+ retryable: true,
314
+ },
315
+ },
263
316
  observedEffects: [],
264
317
  };
265
- }
266
- },
267
- };
268
- }
269
- function failureReason(verification) {
270
- const failed = verification.checks.find((check) => check.status === "FAIL");
271
- return failed?.message ?? "public ingress verification did not pass";
272
- }
273
- export const behaviorAdapter = createBehaviorAdapter();
274
- export async function createProductionBinding(input) {
275
- const publicBaseUrl = input.config.publicBaseUrl;
276
- if (!publicBaseUrl)
277
- return undefined;
278
- const { createDevTunnelRuntime } = await import("../src/resource-adapter.js");
279
- const readEvidence = () => readDevTunnelVerificationEvidence(input.config.verificationEvidenceFile, publicBaseUrl);
280
- return {
281
- behaviorAdapter: createBehaviorAdapter({
282
- runtime: createDevTunnelRuntime({
283
- publicBaseUrl,
284
- ...(input.config.tunnelId ? { tunnelId: input.config.tunnelId } : {}),
285
- processStateFile: join(input.workspaceRoot, ".proflow", "runtime", "external-resources", "dev-tunnel", "process.json"),
286
- }),
287
- verifyFileRelay: async () => (await readEvidence())?.fileRelay ?? {
288
- verified: false,
289
- message: "dev-tunnel verification evidence is missing, stale, or invalid",
290
- },
291
- verifyErrorSemantics: async () => (await readEvidence())?.errorSemantics ?? {
292
- rateLimit429Verified: false,
293
- server5xxVerified: false,
294
- message: "dev-tunnel verification evidence is missing, stale, or invalid",
295
- },
296
- }, input.config),
297
- };
298
- }
318
+ }
319
+ catch (error) {
320
+ return {
321
+ result: {
322
+ ...base,
323
+ ok: false,
324
+ status: "FAILED",
325
+ error: {
326
+ code: "STOP_FAILED",
327
+ message: error instanceof Error
328
+ ? error.message
329
+ : "failed to stop dev-tunnel",
330
+ retryable: true,
331
+ },
332
+ },
333
+ observedEffects: [],
334
+ };
335
+ }
336
+ },
337
+ };
@@ -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";
6
+ readonly moduleVersion: "0.1.7";
7
7
  readonly kind: "external-resource";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -11,7 +11,10 @@ export declare const descriptor: {
11
11
  readonly domain: "deployment-governance";
12
12
  readonly summary: "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.";
13
13
  };
14
- readonly provides: readonly [];
14
+ readonly provides: readonly [{
15
+ readonly contractRef: "public-ingress";
16
+ readonly version: "1.0.0";
17
+ }];
15
18
  readonly requires: readonly [];
16
19
  readonly requirements: readonly [{
17
20
  readonly kind: "executable";
@@ -20,44 +23,7 @@ export declare const descriptor: {
20
23
  readonly kind: "human";
21
24
  readonly action: "Complete Microsoft Dev Tunnel login when required";
22
25
  }];
23
- readonly configSlots: readonly [{
24
- readonly key: "publicBaseUrl";
25
- readonly type: "url";
26
- readonly required: true;
27
- readonly description: "Public HTTPS URL supplied by the dev-tunnel resource";
28
- }, {
29
- readonly key: "tunnelId";
30
- readonly type: "string";
31
- readonly required: false;
32
- readonly description: "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop";
33
- }, {
34
- readonly key: "verificationEvidenceFile";
35
- readonly type: "path";
36
- readonly required: false;
37
- readonly description: "JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress";
38
- }];
39
- readonly lifecycle: {
40
- readonly supported: readonly ["describe", "preflight", "status", "verify", "doctor", "start", "stop", "restart", "uninstall"];
41
- };
42
- readonly verification: {
43
- readonly checks: readonly [{
44
- readonly id: "tunnel-status";
45
- readonly description: "dev-tunnel process reports a real running or stopped state";
46
- readonly lifecycle: "status";
47
- }, {
48
- readonly id: "tunnel-public-ingress";
49
- readonly description: "public HTTPS ingress passes the frozen TLS/port/size/latency contract";
50
- readonly lifecycle: "verify";
51
- }, {
52
- readonly id: "tunnel-file-relay";
53
- readonly description: "file relay behind the public ingress is reachable";
54
- readonly lifecycle: "verify";
55
- }, {
56
- readonly id: "tunnel-diagnostics";
57
- readonly description: "dev-tunnel configuration has actionable diagnostics";
58
- readonly lifecycle: "doctor";
59
- }];
60
- };
26
+ readonly configSlots: readonly [];
61
27
  readonly effects: readonly [{
62
28
  readonly kind: "process";
63
29
  readonly description: "Manage the dev-tunnel public ingress process";
@@ -71,13 +37,8 @@ export declare const descriptor: {
71
37
  readonly description: "Exposes the local platform via a public HTTPS tunnel";
72
38
  readonly retention: "preserve";
73
39
  }];
74
- readonly documentation: readonly [{
75
- readonly id: "overview";
76
- readonly path: "./README.md";
77
- readonly description: "Dev Tunnel module overview";
78
- }, {
79
- readonly id: "configuration";
80
- readonly path: "./CONFIGURATION.md";
81
- readonly description: "Module configuration fields, sources and materialization instructions";
82
- }];
40
+ readonly documentation: {
41
+ readonly docs: "DOCS.md";
42
+ readonly setup: "SETUP.md";
43
+ };
83
44
  };
@@ -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",
6
+ moduleVersion: "0.1.7",
7
7
  kind: "external-resource",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -11,7 +11,12 @@ export const descriptor = {
11
11
  domain: "deployment-governance",
12
12
  summary: "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.",
13
13
  },
14
- provides: [],
14
+ provides: [
15
+ {
16
+ contractRef: "public-ingress",
17
+ version: "1.0.0",
18
+ },
19
+ ],
15
20
  requires: [],
16
21
  requirements: [
17
22
  {
@@ -23,63 +28,7 @@ export const descriptor = {
23
28
  action: "Complete Microsoft Dev Tunnel login when required",
24
29
  },
25
30
  ],
26
- configSlots: [
27
- {
28
- key: "publicBaseUrl",
29
- type: "url",
30
- required: true,
31
- description: "Public HTTPS URL supplied by the dev-tunnel resource",
32
- },
33
- {
34
- key: "tunnelId",
35
- type: "string",
36
- required: false,
37
- description: "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop",
38
- },
39
- {
40
- key: "verificationEvidenceFile",
41
- type: "path",
42
- required: false,
43
- description: "JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress",
44
- },
45
- ],
46
- lifecycle: {
47
- supported: [
48
- "describe",
49
- "preflight",
50
- "status",
51
- "verify",
52
- "doctor",
53
- "start",
54
- "stop",
55
- "restart",
56
- "uninstall",
57
- ],
58
- },
59
- verification: {
60
- checks: [
61
- {
62
- id: "tunnel-status",
63
- description: "dev-tunnel process reports a real running or stopped state",
64
- lifecycle: "status",
65
- },
66
- {
67
- id: "tunnel-public-ingress",
68
- description: "public HTTPS ingress passes the frozen TLS/port/size/latency contract",
69
- lifecycle: "verify",
70
- },
71
- {
72
- id: "tunnel-file-relay",
73
- description: "file relay behind the public ingress is reachable",
74
- lifecycle: "verify",
75
- },
76
- {
77
- id: "tunnel-diagnostics",
78
- description: "dev-tunnel configuration has actionable diagnostics",
79
- lifecycle: "doctor",
80
- },
81
- ],
82
- },
31
+ configSlots: [],
83
32
  effects: [
84
33
  {
85
34
  kind: "process",
@@ -97,16 +46,8 @@ export const descriptor = {
97
46
  retention: "preserve",
98
47
  },
99
48
  ],
100
- documentation: [
101
- {
102
- id: "overview",
103
- path: "./README.md",
104
- description: "Dev Tunnel module overview",
105
- },
106
- {
107
- id: "configuration",
108
- path: "./CONFIGURATION.md",
109
- description: "Module configuration fields, sources and materialization instructions",
110
- },
111
- ],
49
+ documentation: {
50
+ docs: "DOCS.md",
51
+ setup: "SETUP.md",
52
+ },
112
53
  };
@@ -1,2 +1,2 @@
1
- export { behaviorAdapter, createBehaviorAdapter, } from "../deployment/adapter.ts";
1
+ export { behaviorAdapter } from "../deployment/adapter.ts";
2
2
  export { descriptor } from "../deployment/descriptor.ts";
package/dist/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { behaviorAdapter, createBehaviorAdapter, } from "../deployment/adapter.js";
1
+ export { behaviorAdapter } from "../deployment/adapter.js";
2
2
  export { descriptor } from "../deployment/descriptor.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-dev-tunnel",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,13 +15,14 @@
15
15
  "proflow.module.json",
16
16
  "conformance.json",
17
17
  "README.md",
18
- "CONFIGURATION.md"
18
+ "DOCS.md",
19
+ "SETUP.md"
19
20
  ],
20
21
  "dependencies": {
21
- "@tomflow/proflow-module-contract": "^0.1.2"
22
+ "@tomflow/proflow-module-contract": "^0.1.4"
22
23
  },
23
24
  "devDependencies": {
24
- "@tomflow/proflow-deployment-conformance": "^0.1.3"
25
+ "@tomflow/proflow-deployment-conformance": "^0.1.4"
25
26
  },
26
27
  "description": "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process.",
27
28
  "keywords": [
@@ -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",
6
+ "moduleVersion": "0.1.7",
7
7
  "kind": "external-resource",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",
@@ -11,7 +11,12 @@
11
11
  "domain": "deployment-governance",
12
12
  "summary": "Governs the Microsoft Dev Tunnel public HTTPS ingress resource and its managed local host process."
13
13
  },
14
- "provides": [],
14
+ "provides": [
15
+ {
16
+ "contractRef": "public-ingress",
17
+ "version": "1.0.0"
18
+ }
19
+ ],
15
20
  "requires": [],
16
21
  "requirements": [
17
22
  {
@@ -23,63 +28,7 @@
23
28
  "action": "Complete Microsoft Dev Tunnel login when required"
24
29
  }
25
30
  ],
26
- "configSlots": [
27
- {
28
- "key": "publicBaseUrl",
29
- "type": "url",
30
- "required": true,
31
- "description": "Public HTTPS URL supplied by the dev-tunnel resource"
32
- },
33
- {
34
- "key": "tunnelId",
35
- "type": "string",
36
- "required": false,
37
- "description": "Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop"
38
- },
39
- {
40
- "key": "verificationEvidenceFile",
41
- "type": "path",
42
- "required": false,
43
- "description": "JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress"
44
- }
45
- ],
46
- "lifecycle": {
47
- "supported": [
48
- "describe",
49
- "preflight",
50
- "status",
51
- "verify",
52
- "doctor",
53
- "start",
54
- "stop",
55
- "restart",
56
- "uninstall"
57
- ]
58
- },
59
- "verification": {
60
- "checks": [
61
- {
62
- "id": "tunnel-status",
63
- "description": "dev-tunnel process reports a real running or stopped state",
64
- "lifecycle": "status"
65
- },
66
- {
67
- "id": "tunnel-public-ingress",
68
- "description": "public HTTPS ingress passes the frozen TLS/port/size/latency contract",
69
- "lifecycle": "verify"
70
- },
71
- {
72
- "id": "tunnel-file-relay",
73
- "description": "file relay behind the public ingress is reachable",
74
- "lifecycle": "verify"
75
- },
76
- {
77
- "id": "tunnel-diagnostics",
78
- "description": "dev-tunnel configuration has actionable diagnostics",
79
- "lifecycle": "doctor"
80
- }
81
- ]
82
- },
31
+ "configSlots": [],
83
32
  "effects": [
84
33
  {
85
34
  "kind": "process",
@@ -97,16 +46,8 @@
97
46
  "retention": "preserve"
98
47
  }
99
48
  ],
100
- "documentation": [
101
- {
102
- "id": "overview",
103
- "path": "./README.md",
104
- "description": "Dev Tunnel module overview"
105
- },
106
- {
107
- "id": "configuration",
108
- "path": "./CONFIGURATION.md",
109
- "description": "Module configuration fields, sources and materialization instructions"
110
- }
111
- ]
49
+ "documentation": {
50
+ "docs": "DOCS.md",
51
+ "setup": "SETUP.md"
52
+ }
112
53
  }
package/CONFIGURATION.md DELETED
@@ -1,25 +0,0 @@
1
- # dev-tunnel configuration
2
-
3
- Canonical public config: `.proflow/config/dev-tunnel.json`
4
-
5
- This file is Workspace-owned data. `platform uninstall` does not delete it. Values are JSON strings. Raw secrets must not be stored here; `secretRef` fields contain opaque references only.
6
-
7
- ## Fields
8
-
9
- | Key | Type | Required | Sensitive | Source | Meaning |
10
- |---|---|---:|---:|---|---|
11
- | `publicBaseUrl` | `url` | yes | no | User or owning external/runtime Module supplies the reachable endpoint URL. | Public HTTPS URL supplied by the dev-tunnel resource |
12
- | `tunnelId` | `string` | no | no | User or owning Module supplies this value according to the field description. | Persistent Microsoft Dev Tunnel identifier used when this adapter owns lifecycle start/stop |
13
- | `verificationEvidenceFile` | `path` | no | no | User or owning Module supplies an absolute/local path that exists on this Workspace. | JSON evidence for real file-relay and 429/5xx verification behind the configured public ingress |
14
-
15
- ## Materialize
16
-
17
- Create or update the canonical JSON object at:
18
-
19
- `.proflow/config/dev-tunnel.json`
20
-
21
- Use only declared keys and string values. Do not create a second Platform configuration database.
22
-
23
- ## Base completion check
24
-
25
- Run `platform modules`. This Module should no longer report required keys in `missingConfig`. `platform start` performs the authoritative Module-owned validation before starting anything.
@@ -1,19 +0,0 @@
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
- };
@@ -1,2 +0,0 @@
1
- import { descriptor } from "./descriptor.js";
2
- export const verification = descriptor.verification;