@tomflow/proflow-dev-tunnel 0.1.5 → 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.
@@ -1,42 +1,29 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- 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";
4
5
  import { descriptor } from "./descriptor.js";
5
- const success = (data) => ({
6
+ const base = {
6
7
  contract: "deployment.result.v1",
7
8
  ok: true,
8
9
  status: "SUCCEEDED",
9
10
  moduleRef: descriptor.moduleRef,
10
11
  moduleVersion: descriptor.moduleVersion,
11
- ...(data === undefined ? {} : { data }),
12
- });
13
- const actionRequired = (action, description) => ({
14
- contract: "deployment.result.v1",
15
- ok: false,
16
- status: "ACTION_REQUIRED",
17
- moduleRef: descriptor.moduleRef,
18
- moduleVersion: descriptor.moduleVersion,
19
- actionRequired: { action, description },
20
- });
21
- export async function readDevTunnelVerificationEvidence(file, publicBaseUrl) {
22
- if (!file)
23
- 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) {
24
18
  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")
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:")
40
27
  return undefined;
41
28
  return raw;
42
29
  }
@@ -44,264 +31,307 @@ export async function readDevTunnelVerificationEvidence(file, publicBaseUrl) {
44
31
  return undefined;
45
32
  }
46
33
  }
47
- export function createBehaviorAdapter(input) {
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");
48
48
  return {
49
- describe: () => ({
50
- result: success({
51
- publicApi: ["status", "verify", "doctor", "start", "stop", "restart"],
52
- }),
53
- observedEffects: [],
54
- }),
55
- preflight: () => ({
56
- result: input
57
- ? success()
58
- : actionRequired("configure-tunnel", "Bind a dev-tunnel resource with login status and a public HTTPS ingress"),
59
- observedEffects: [],
60
- }),
61
- status: async () => {
62
- if (!input) {
63
- return {
64
- result: actionRequired("configure-tunnel", "No dev-tunnel resource is bound"),
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
+ },
65
93
  observedEffects: [],
66
94
  };
67
- }
68
- const observation = await input.runtime.status();
95
+ }
96
+ catch (error) {
69
97
  return {
70
98
  result: {
71
- ...success(),
72
- checks: [
73
- {
74
- id: "tunnel-status",
75
- status: observation.state === "RUNNING"
76
- ? "PASS"
77
- : "WARN",
78
- message: `dev-tunnel state is ${observation.state}`,
79
- },
80
- ],
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
+ },
81
109
  },
82
110
  observedEffects: [],
83
111
  };
84
- },
85
- verify: async () => {
86
- if (!input) {
87
- return {
88
- result: actionRequired("configure-tunnel", "A live dev-tunnel resource is required for public ingress verification"),
89
- observedEffects: [],
90
- };
91
- }
92
- if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
93
- return {
94
- result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before verifying public ingress"),
95
- observedEffects: [],
96
- };
97
- }
98
- const publicBaseUrl = input.runtime.publicBaseUrl();
99
- if (publicBaseUrl === undefined) {
100
- return {
101
- result: actionRequired("configure-tunnel", "publicBaseUrl must be configured before public ingress verification"),
102
- observedEffects: [],
103
- };
104
- }
105
- const verification = await verifyPublicIngress(publicBaseUrl, {
106
- ...(input.verifyErrorSemantics === undefined
107
- ? {}
108
- : { verifyErrorSemantics: input.verifyErrorSemantics }),
109
- ...(input.verifyFileRelay === undefined
110
- ? {}
111
- : { verifyFileRelay: input.verifyFileRelay }),
112
- });
112
+ }
113
+ },
114
+ status: async (context) => {
115
+ const state = await readState(context);
116
+ if (!state) {
113
117
  return {
114
- result: verification.ok
115
- ? { ...success(), checks: verification.checks }
116
- : {
117
- ...actionRequired("repair-tunnel-ingress", failureReason(verification)),
118
- checks: verification.checks,
118
+ result: {
119
+ ...base,
120
+ data: {
121
+ setupStatus: "ACTION_REQUIRED",
122
+ runtimeStatus: "STOPPED",
119
123
  },
120
- observedEffects: verification.reachable
121
- ? ["Probes the dev-tunnel public HTTPS ingress"]
122
- : [],
124
+ },
125
+ observedEffects: [],
123
126
  };
124
- },
125
- doctor: async () => {
126
- if (!input) {
127
- return {
128
- result: actionRequired("configure-tunnel", "dev-tunnel login and public ingress configuration are required for diagnostics"),
129
- observedEffects: [],
130
- };
131
- }
132
- const login = await input.runtime.loginStatus();
133
- const status = await input.runtime.status();
134
- const publicBaseUrl = input.runtime.publicBaseUrl();
135
- const checks = [
136
- {
137
- id: "tunnel-login",
138
- status: login === "LOGGED_IN" ? "PASS" : "FAIL",
139
- 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,
140
151
  },
141
- {
142
- id: "tunnel-state",
143
- status: status.state === "RUNNING"
144
- ? "PASS"
145
- : status.state === "STOPPED"
146
- ? "WARN"
147
- : "FAIL",
148
- 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
+ },
149
173
  },
150
- {
151
- id: "tunnel-public-url",
152
- status: publicBaseUrl === undefined ? "FAIL" : "PASS",
153
- message: publicBaseUrl === undefined
154
- ? "publicBaseUrl is not configured"
155
- : `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
+ },
156
187
  },
157
- ];
158
- const healthy = login === "LOGGED_IN" &&
159
- status.state === "RUNNING" &&
160
- 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) {
161
207
  return {
162
- result: healthy
163
- ? { ...success(), checks }
164
- : {
165
- ...actionRequired("repair-tunnel", "dev-tunnel resource is not healthy"),
166
- 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",
167
217
  },
218
+ },
168
219
  observedEffects: [],
169
220
  };
170
- },
171
- start: async () => {
172
- if (!input) {
173
- return {
174
- result: actionRequired("configure-tunnel", "Cannot start without a bound dev-tunnel resource"),
175
- observedEffects: [],
176
- };
177
- }
178
- if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
179
- return {
180
- result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before starting the tunnel"),
181
- observedEffects: [],
182
- };
183
- }
184
- try {
185
- const observation = await input.runtime.start();
186
- return {
187
- result: success(observation),
188
- observedEffects: ["Manage the dev-tunnel public ingress process"],
189
- };
190
- }
191
- catch (error) {
192
- return {
193
- result: actionRequired("start-tunnel", error instanceof Error
194
- ? error.message
195
- : "failed to start the dev-tunnel process"),
196
- observedEffects: [],
197
- };
198
- }
199
- },
200
- stop: async () => {
201
- if (!input) {
202
- return {
203
- result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to stop"),
204
- observedEffects: [],
205
- };
206
- }
207
- const stopped = await input.runtime.stop();
208
- if (stopped.state === "STOPPED") {
209
- return {
210
- result: success(),
211
- observedEffects: ["Manage the dev-tunnel public ingress process"],
212
- };
213
- }
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)
214
230
  return {
215
- 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
+ },
216
241
  observedEffects: [],
217
242
  };
218
- },
219
- uninstall: async () => {
220
- if (!input) {
221
- return {
222
- // No bound tunnel means there is no managed external resource to
223
- // stop. Whole-instance uninstall must remain idempotent.
224
- result: success(),
225
- observedEffects: [],
226
- };
227
- }
228
- const stopped = await input.runtime.stop();
229
- if (stopped.state === "STOPPED") {
230
- return {
231
- result: success(),
232
- observedEffects: ["Manage the dev-tunnel public ingress process"],
233
- };
234
- }
243
+ const rt = runtime(context, state);
244
+ if ((await rt.loginStatus()) !== "LOGGED_IN")
235
245
  return {
236
- 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
+ },
237
256
  observedEffects: [],
238
257
  };
239
- },
240
- restart: async () => {
241
- if (!input) {
242
- return {
243
- result: actionRequired("configure-tunnel", "No bound dev-tunnel resource to restart"),
244
- observedEffects: [],
245
- };
246
- }
247
- if ((await input.runtime.loginStatus()) !== "LOGGED_IN") {
248
- return {
249
- result: actionRequired("complete-tunnel-login", "Complete the interactive dev-tunnel login before restarting the tunnel"),
250
- observedEffects: [],
251
- };
252
- }
253
- const stopped = await input.runtime.stop();
254
- if (stopped.state !== "STOPPED") {
255
- return {
256
- 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
+ },
257
276
  observedEffects: [],
258
277
  };
259
- }
260
- try {
261
- const observation = await input.runtime.start();
262
- return {
263
- result: success(observation),
264
- observedEffects: ["Manage the dev-tunnel public ingress process"],
265
- };
266
- }
267
- catch (error) {
268
- return {
269
- result: actionRequired("start-tunnel", error instanceof Error
270
- ? error.message
271
- : "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
+ },
272
316
  observedEffects: [],
273
317
  };
274
- }
275
- },
276
- };
277
- }
278
- function failureReason(verification) {
279
- const failed = verification.checks.find((check) => check.status === "FAIL");
280
- return failed?.message ?? "public ingress verification did not pass";
281
- }
282
- export const behaviorAdapter = createBehaviorAdapter();
283
- export async function createProductionBinding(input) {
284
- const publicBaseUrl = input.config.publicBaseUrl;
285
- if (!publicBaseUrl)
286
- return undefined;
287
- const { createDevTunnelRuntime } = await import("../src/resource-adapter.js");
288
- const readEvidence = () => readDevTunnelVerificationEvidence(input.config.verificationEvidenceFile, publicBaseUrl);
289
- return {
290
- behaviorAdapter: createBehaviorAdapter({
291
- runtime: createDevTunnelRuntime({
292
- publicBaseUrl,
293
- ...(input.config.tunnelId ? { tunnelId: input.config.tunnelId } : {}),
294
- processStateFile: join(input.workspaceRoot, ".proflow", "runtime", "external-resources", "dev-tunnel", "process.json"),
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
- },
305
- }),
306
- };
307
- }
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
+ };