harnery 0.44.0 → 0.45.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.
Files changed (87) hide show
  1. package/dist/commander.js +1 -0
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +7 -4
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +11 -17
  7. package/dist/commands/tunnel.d.ts +15 -0
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +453 -153
  10. package/dist/commands/turn-rollback.d.ts +5 -0
  11. package/dist/commands/turn-rollback.d.ts.map +1 -0
  12. package/dist/commands/turn-rollback.js +76 -0
  13. package/dist/commands/workflow.d.ts.map +1 -1
  14. package/dist/commands/workflow.js +44 -0
  15. package/dist/core/agents/coord-client.d.ts.map +1 -1
  16. package/dist/core/agents/coord-client.js +18 -0
  17. package/dist/core/agents/state/proc-start.d.ts +5 -6
  18. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  19. package/dist/core/agents/state/proc-start.js +13 -9
  20. package/dist/core/agents/state/process-handle.d.ts +26 -0
  21. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  22. package/dist/core/agents/state/process-handle.js +121 -0
  23. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  24. package/dist/core/artifacts/delivery-card.js +2 -1
  25. package/dist/core/resources/service.d.ts +2 -16
  26. package/dist/core/resources/service.d.ts.map +1 -1
  27. package/dist/core/resources/service.js +1 -78
  28. package/dist/core/semantic/service.d.ts.map +1 -1
  29. package/dist/core/semantic/service.js +2 -9
  30. package/dist/core/servers/index.d.ts +12 -11
  31. package/dist/core/servers/index.d.ts.map +1 -1
  32. package/dist/core/servers/index.js +194 -81
  33. package/dist/core/servers/tunnels.d.ts +5 -3
  34. package/dist/core/servers/tunnels.d.ts.map +1 -1
  35. package/dist/core/servers/tunnels.js +45 -2
  36. package/dist/core/supervisor/service.d.ts +2 -4
  37. package/dist/core/supervisor/service.d.ts.map +1 -1
  38. package/dist/core/turn-rollback/content.d.ts +53 -0
  39. package/dist/core/turn-rollback/content.d.ts.map +1 -0
  40. package/dist/core/turn-rollback/content.js +396 -0
  41. package/dist/core/turn-rollback/index.d.ts +72 -0
  42. package/dist/core/turn-rollback/index.d.ts.map +1 -0
  43. package/dist/core/turn-rollback/index.js +443 -0
  44. package/dist/core/workflow/index.d.ts +4 -2
  45. package/dist/core/workflow/index.d.ts.map +1 -1
  46. package/dist/core/workflow/index.js +2 -1
  47. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  48. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  49. package/dist/core/workflow/recovery-audit.js +116 -0
  50. package/dist/core/workflow/run-state.d.ts +19 -2
  51. package/dist/core/workflow/run-state.d.ts.map +1 -1
  52. package/dist/core/workflow/run-state.js +113 -8
  53. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  54. package/dist/core/workflow/workspaces/local-git.js +70 -14
  55. package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
  56. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  57. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  58. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  59. package/dist/core/workflow/workspaces/recovery.js +24 -0
  60. package/dist/lib/tunnel/state.d.ts +13 -6
  61. package/dist/lib/tunnel/state.d.ts.map +1 -1
  62. package/dist/lib/tunnel/state.js +58 -12
  63. package/package.json +6 -1
  64. package/src/commander.ts +10 -0
  65. package/src/commands/files.ts +13 -5
  66. package/src/commands/servers.ts +12 -23
  67. package/src/commands/tunnel.ts +533 -166
  68. package/src/commands/turn-rollback.ts +111 -0
  69. package/src/commands/workflow.ts +59 -0
  70. package/src/core/agents/coord-client.ts +20 -0
  71. package/src/core/agents/state/proc-start.ts +14 -11
  72. package/src/core/agents/state/process-handle.ts +165 -0
  73. package/src/core/artifacts/delivery-card.ts +7 -1
  74. package/src/core/resources/service.ts +3 -88
  75. package/src/core/semantic/service.ts +2 -8
  76. package/src/core/servers/index.ts +234 -85
  77. package/src/core/servers/tunnels.ts +55 -3
  78. package/src/core/supervisor/service.ts +2 -3
  79. package/src/core/turn-rollback/content.ts +499 -0
  80. package/src/core/turn-rollback/index.ts +580 -0
  81. package/src/core/workflow/index.ts +12 -1
  82. package/src/core/workflow/recovery-audit.ts +181 -0
  83. package/src/core/workflow/run-state.ts +137 -8
  84. package/src/core/workflow/workspaces/local-git.ts +86 -15
  85. package/src/core/workflow/workspaces/reclaim.ts +4 -0
  86. package/src/core/workflow/workspaces/recovery.ts +23 -0
  87. package/src/lib/tunnel/state.ts +70 -15
@@ -2,6 +2,7 @@
2
2
  // <cwd>/.cache/tunnel/; gitignored, so the allowlist is per-machine.
3
3
 
4
4
  import { spawnSync } from "node:child_process";
5
+ import { randomUUID } from "node:crypto";
5
6
  import {
6
7
  accessSync,
7
8
  chmodSync,
@@ -10,6 +11,7 @@ import {
10
11
  mkdirSync,
11
12
  readdirSync,
12
13
  readFileSync,
14
+ renameSync,
13
15
  statSync,
14
16
  unlinkSync,
15
17
  writeFileSync,
@@ -104,10 +106,12 @@ export interface TunnelState {
104
106
  provider: TunnelProvider;
105
107
  url: string;
106
108
  gate_pid: number;
109
+ gate_start_token?: string;
110
+ cloudflared_start_token?: string;
111
+ registration_id?: string;
112
+ provider_state?: "pending" | "ready";
107
113
  /** Present for Cloudflare quick tunnels; absent for Tailscale Serve/Funnel. */
108
114
  cloudflared_pid?: number;
109
- /** Optional provider-side process when a provider owns one. */
110
- provider_pid?: number;
111
115
  started_at: string;
112
116
  target: string;
113
117
  vhost: string;
@@ -168,7 +172,15 @@ export function readState(
168
172
  }
169
173
 
170
174
  export function writeState(state: TunnelState, root: string = process.cwd()): void {
171
- writeFileSync(cachePath(stateFile(state.name), root), JSON.stringify(state, null, 2));
175
+ const path = cachePath(stateFile(state.name), root);
176
+ const temporary = `${path}.${randomUUID()}.tmp`;
177
+ try {
178
+ writeFileSync(temporary, JSON.stringify(state, null, 2), { flag: "wx", mode: 0o600 });
179
+ renameSync(temporary, path);
180
+ } catch (error) {
181
+ if (existsSync(temporary)) unlinkSync(temporary);
182
+ throw error;
183
+ }
172
184
  }
173
185
 
174
186
  export function clearState(name: string = DEFAULT_INSTANCE, root: string = process.cwd()): void {
@@ -202,17 +214,60 @@ export function isProcessAlive(pid: number): boolean {
202
214
  }
203
215
  }
204
216
 
205
- export type ProcessAliveCheck = (pid: number) => boolean;
217
+ export type TunnelReadiness = "ready" | "dead" | "unknown";
218
+ export type TunnelProcessObservation = "same" | "absent" | "different" | "unknown";
219
+
220
+ export interface TunnelReadinessVerifier {
221
+ observeProcess(pid: number, originalToken: string): TunnelProcessObservation;
222
+ withRegistration(state: TunnelState, inspect: () => TunnelReadiness): TunnelReadiness;
223
+ }
206
224
 
207
- /** True when both the local gate and its provider-side process are live. */
208
- export function isTunnelStateLive(
225
+ /** Classify saved original witnesses under the verifier's registration custody. */
226
+ export function tunnelReadiness(
209
227
  state: TunnelState,
210
- processAlive: ProcessAliveCheck = isProcessAlive,
211
- ): boolean {
212
- if (!processAlive(state.gate_pid)) return false;
213
- if (state.provider === "tailscale") return true;
214
- const providerPid = state.cloudflared_pid ?? state.provider_pid;
215
- return typeof providerPid === "number" && processAlive(providerPid);
228
+ verifier: TunnelReadinessVerifier,
229
+ ): TunnelReadiness {
230
+ if (
231
+ !state ||
232
+ typeof state !== "object" ||
233
+ Object.hasOwn(state, "provider_pid") ||
234
+ Object.hasOwn(state, "provider_start_token") ||
235
+ typeof state.registration_id !== "string" ||
236
+ !state.registration_id ||
237
+ !Number.isSafeInteger(state.gate_pid) ||
238
+ state.gate_pid < 1 ||
239
+ typeof state.gate_start_token !== "string" ||
240
+ !state.gate_start_token
241
+ )
242
+ return "unknown";
243
+ const targets = [{ pid: state.gate_pid, token: state.gate_start_token }];
244
+ if (state.provider === "tailscale") {
245
+ if (
246
+ state.provider_state !== "ready" ||
247
+ state.cloudflared_pid !== undefined ||
248
+ state.cloudflared_start_token !== undefined
249
+ )
250
+ return "unknown";
251
+ } else if (state.provider === "cloudflare") {
252
+ if (
253
+ !Number.isSafeInteger(state.cloudflared_pid) ||
254
+ state.cloudflared_pid! < 1 ||
255
+ typeof state.cloudflared_start_token !== "string" ||
256
+ !state.cloudflared_start_token
257
+ )
258
+ return "unknown";
259
+ targets.push({ pid: state.cloudflared_pid!, token: state.cloudflared_start_token });
260
+ } else return "unknown";
261
+ try {
262
+ return verifier.withRegistration(state, () => {
263
+ const observed = targets.map(({ pid, token }) => verifier.observeProcess(pid, token));
264
+ if (observed.every((value) => value === "absent")) return "dead";
265
+ if (observed.every((value) => value === "same")) return "ready";
266
+ return "unknown";
267
+ });
268
+ } catch {
269
+ return "unknown";
270
+ }
216
271
  }
217
272
 
218
273
  export function tunnelTargetPort(state: TunnelState): number | null {
@@ -242,15 +297,15 @@ export function tunnelTargetPort(state: TunnelState): number | null {
242
297
  export function findLiveTunnelForOrigin(
243
298
  port: number,
244
299
  vhost: string,
245
- root: string = process.cwd(),
246
- processAlive: ProcessAliveCheck = isProcessAlive,
300
+ root: string,
301
+ verifier: TunnelReadinessVerifier,
247
302
  ): TunnelState | null {
248
303
  return (
249
304
  listStates(root).find(
250
305
  (state) =>
251
306
  tunnelTargetPort(state) === port &&
252
307
  state.vhost === vhost &&
253
- isTunnelStateLive(state, processAlive),
308
+ tunnelReadiness(state, verifier) === "ready",
254
309
  ) ?? null
255
310
  );
256
311
  }