@mycadet/cli 0.12.7 → 0.12.8

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 (165) hide show
  1. package/README.md +2 -2
  2. package/knip.json +1 -1
  3. package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
  4. package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
  5. package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
  6. package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
  7. package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
  8. package/node_modules/@mycadet/assistant-api/events/{show-platform-login.ts → show-cloud-login.ts} +3 -5
  9. package/node_modules/@mycadet/assistant-api/index.ts +11 -11
  10. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
  11. package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  12. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  13. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  14. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  15. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  16. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  17. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  18. package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
  19. package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
  20. package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  21. package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  22. package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  23. package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  24. package/node_modules/@mycadet/environments/src/types.ts +6 -6
  25. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  26. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  27. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  28. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  29. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
  30. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
  31. package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  32. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  33. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  34. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  35. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  36. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  37. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  38. package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
  39. package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
  40. package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
  41. package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
  42. package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
  43. package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
  44. package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
  45. package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
  46. package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
  47. package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
  48. package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
  49. package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
  50. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  51. package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
  52. package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  53. package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  54. package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  55. package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  56. package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  57. package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  58. package/package.json +3 -2
  59. package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
  60. package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
  61. package/src/__tests__/assistant-client-refresh.test.ts +2 -2
  62. package/src/__tests__/backup.test.ts +86 -91
  63. package/src/__tests__/client-token.test.ts +1 -1
  64. package/src/__tests__/client-tui-refresh.test.ts +1 -1
  65. package/src/__tests__/client-web-config.test.ts +2 -2
  66. package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
  67. package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +5 -5
  68. package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
  69. package/src/__tests__/connect-import.test.ts +55 -39
  70. package/src/__tests__/debug.test.ts +38 -38
  71. package/src/__tests__/guardian-token.test.ts +149 -56
  72. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  73. package/src/__tests__/helpers/guardian-token.ts +22 -0
  74. package/src/__tests__/helpers/http.ts +10 -0
  75. package/src/__tests__/nginx-ingress.test.ts +18 -18
  76. package/src/__tests__/orphan-detection.test.ts +1 -1
  77. package/src/__tests__/pair.test.ts +1 -8
  78. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
  79. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  80. package/src/__tests__/retire.test.ts +58 -199
  81. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  82. package/src/__tests__/sleep.test.ts +19 -19
  83. package/src/__tests__/statefulset.test.ts +8 -8
  84. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
  85. package/src/__tests__/teleport-backup.test.ts +3 -3
  86. package/src/__tests__/teleport.test.ts +180 -196
  87. package/src/__tests__/tunnel.test.ts +255 -227
  88. package/src/__tests__/uninstall.test.ts +13 -4
  89. package/src/__tests__/upgrade-local.test.ts +3 -3
  90. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  91. package/src/__tests__/wake.test.ts +258 -41
  92. package/src/__tests__/workos-pkce.test.ts +2 -2
  93. package/src/commands/backup.ts +22 -22
  94. package/src/commands/client.ts +76 -76
  95. package/src/commands/debug.ts +20 -20
  96. package/src/commands/exec.ts +5 -5
  97. package/src/commands/hatch.ts +22 -22
  98. package/src/commands/login.ts +22 -22
  99. package/src/commands/logs.ts +1 -1
  100. package/src/commands/ps.ts +188 -79
  101. package/src/commands/restart.ts +30 -0
  102. package/src/commands/restore.ts +138 -20
  103. package/src/commands/retire.ts +46 -148
  104. package/src/commands/rollback.ts +18 -24
  105. package/src/commands/setup.ts +2 -2
  106. package/src/commands/teleport.ts +93 -93
  107. package/src/commands/terminal.ts +157 -17
  108. package/src/commands/tunnel.ts +80 -122
  109. package/src/commands/uninstall.ts +55 -7
  110. package/src/commands/unpair.ts +3 -3
  111. package/src/commands/upgrade.ts +22 -46
  112. package/src/commands/wake.ts +162 -46
  113. package/src/index.ts +3 -9
  114. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  115. package/src/lib/__tests__/job-polling.test.ts +1 -1
  116. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  117. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  118. package/src/lib/assistant-api-key-resolution.ts +5 -5
  119. package/src/lib/assistant-client.ts +8 -8
  120. package/src/lib/assistant-config.ts +58 -12
  121. package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
  122. package/src/lib/cloud-login.ts +24 -24
  123. package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
  124. package/src/lib/cloud-values.ts +3 -0
  125. package/src/lib/config-utils.ts +1 -1
  126. package/src/lib/detached-process.ts +1 -1
  127. package/src/lib/docker.ts +111 -10
  128. package/src/lib/environments/__tests__/paths.test.ts +2 -2
  129. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  130. package/src/lib/environments/resolve.ts +8 -8
  131. package/src/lib/guardian-token.ts +36 -4
  132. package/src/lib/hatch-local.ts +19 -23
  133. package/src/lib/health-check.ts +6 -6
  134. package/src/lib/ingress-config.ts +8 -35
  135. package/src/lib/job-polling.ts +1 -1
  136. package/src/lib/live-voice/connection.ts +12 -12
  137. package/src/lib/live-voice/tunnel.test.ts +11 -11
  138. package/src/lib/live-voice/tunnel.ts +15 -15
  139. package/src/lib/local-runtime-client.ts +20 -20
  140. package/src/lib/local.ts +9 -30
  141. package/src/lib/nginx-ingress.ts +41 -14
  142. package/src/lib/process.ts +1 -1
  143. package/src/lib/provider-secrets.ts +3 -3
  144. package/src/lib/retire-apple-container.ts +1 -1
  145. package/src/lib/runtime-url.ts +8 -8
  146. package/src/lib/statefulset.ts +2 -2
  147. package/src/lib/sync-cloud-assistants.ts +18 -18
  148. package/src/lib/teleport-backup.ts +4 -4
  149. package/src/lib/terminal-client.ts +17 -17
  150. package/src/lib/terminal-session.ts +24 -24
  151. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  152. package/src/lib/tunnels/cadet.ts +29 -0
  153. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
  154. package/src/lib/tunnels/gateway-notify.ts +61 -0
  155. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
  156. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
  157. package/src/lib/upgrade-lifecycle.ts +3 -17
  158. package/src/lib/upgrade-preflight.ts +2 -2
  159. package/src/lib/workos-pkce.ts +5 -5
  160. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  161. package/src/commands/recover.ts +0 -131
  162. package/src/commands/roadmap.ts +0 -459
  163. package/src/commands/ssh.ts +0 -144
  164. package/src/commands/workflows.ts +0 -305
  165. package/src/lib/managed-cloud.ts +0 -3
@@ -19,7 +19,9 @@ process.env.CADET_LOCKFILE_DIR = testDir;
19
19
  import * as assistantConfig from "../lib/assistant-config.js";
20
20
  import * as guardianToken from "../lib/guardian-token.js";
21
21
  import * as localRuntimeClient from "../lib/local-runtime-client.js";
22
- import * as platformClient from "../lib/platform-client.js";
22
+ import * as cloudClient from "../lib/cloud-client.js";
23
+ import { withPreconnect } from "./helpers/http.js";
24
+ import { makeGuardianTokenData } from "./helpers/guardian-token.js";
23
25
 
24
26
  const resolveTargetMock = spyOn(
25
27
  assistantConfig,
@@ -27,9 +29,9 @@ const resolveTargetMock = spyOn(
27
29
  ).mockImplementation(() => {
28
30
  throw new Error("process.exit:1");
29
31
  });
30
- const readPlatformTokenMock = spyOn(
31
- platformClient,
32
- "readPlatformToken",
32
+ const readCloudTokenMock = spyOn(
33
+ cloudClient,
34
+ "readCloudToken",
33
35
  ).mockReturnValue("platform-token");
34
36
  const exportMock = spyOn(
35
37
  localRuntimeClient,
@@ -46,23 +48,25 @@ const pollMock = spyOn(
46
48
  jobId: "job-1",
47
49
  type: "export",
48
50
  status: "complete",
49
- } as unknown as Awaited<
50
- ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
51
- >);
51
+ });
52
52
  const loadGuardianTokenSpy = spyOn(
53
53
  guardianToken,
54
54
  "loadGuardianToken",
55
- ).mockReturnValue({
56
- accessToken: "local-token",
57
- accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
58
- } as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
55
+ ).mockReturnValue(
56
+ makeGuardianTokenData({
57
+ accessToken: "local-token",
58
+ accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
59
+ }),
60
+ );
59
61
  const leaseGuardianTokenSpy = spyOn(
60
62
  guardianToken,
61
63
  "leaseGuardianToken",
62
- ).mockResolvedValue({
63
- accessToken: "leased-token",
64
- accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
65
- } as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
64
+ ).mockResolvedValue(
65
+ makeGuardianTokenData({
66
+ accessToken: "leased-token",
67
+ accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
68
+ }),
69
+ );
66
70
 
67
71
  const { debug } = await import("../commands/debug.js");
68
72
 
@@ -72,10 +76,10 @@ const LOCAL_ENTRY = {
72
76
  runtimeUrl: "http://127.0.0.1:7821",
73
77
  cloud: "local",
74
78
  hatchedAt: new Date().toISOString(),
75
- platformAssistantId: "11111111-2222-3333-4444-555555555555",
76
- platformBaseUrl: "https://cloud.mycadet.ai",
77
- platformOrganizationId: "org-1",
78
- } as unknown as assistantConfig.AssistantEntry;
79
+ cloudAssistantId: "11111111-2222-3333-4444-555555555555",
80
+ cloudBaseUrl: "https://cloud.mycadet.ai",
81
+ cloudOrganizationId: "org-1",
82
+ } satisfies assistantConfig.AssistantEntry;
79
83
 
80
84
  let originalFetch: typeof globalThis.fetch;
81
85
  let originalExit: typeof process.exit;
@@ -86,12 +90,12 @@ beforeEach(() => {
86
90
  process.argv = ["bun", "cadet", "debug"];
87
91
  originalFetch = globalThis.fetch;
88
92
  originalExit = process.exit;
89
- process.exit = mock((code?: number) => {
93
+ process.exit = mock((code?: number | string | undefined): never => {
90
94
  throw new Error(`process.exit:${code}`);
91
- }) as unknown as typeof process.exit;
95
+ });
92
96
  fetchCalls = [];
93
97
  uploadUrlStatus = 201;
94
- globalThis.fetch = mock(
98
+ globalThis.fetch = withPreconnect(
95
99
  async (input: RequestInfo | URL, init?: RequestInit) => {
96
100
  fetchCalls.push({ url: String(input), init });
97
101
  return new Response(
@@ -102,14 +106,14 @@ beforeEach(() => {
102
106
  },
103
107
  );
104
108
  },
105
- ) as unknown as typeof globalThis.fetch;
109
+ );
106
110
  resolveTargetMock.mockReset();
107
111
  resolveTargetMock.mockReturnValue(LOCAL_ENTRY);
108
112
  identityMock.mockReset();
109
113
  identityMock.mockResolvedValue({ version: "0.12.3" });
110
114
  leaseGuardianTokenSpy.mockClear();
111
- readPlatformTokenMock.mockReset();
112
- readPlatformTokenMock.mockReturnValue("platform-token");
115
+ readCloudTokenMock.mockReset();
116
+ readCloudTokenMock.mockReturnValue("platform-token");
113
117
  exportMock.mockClear();
114
118
  pollMock.mockClear();
115
119
  });
@@ -123,7 +127,7 @@ afterAll(() => {
123
127
  resolveTargetMock.mockRestore();
124
128
  identityMock.mockRestore();
125
129
  leaseGuardianTokenSpy.mockRestore();
126
- readPlatformTokenMock.mockRestore();
130
+ readCloudTokenMock.mockRestore();
127
131
  exportMock.mockRestore();
128
132
  pollMock.mockRestore();
129
133
  loadGuardianTokenSpy.mockRestore();
@@ -131,7 +135,7 @@ afterAll(() => {
131
135
  });
132
136
 
133
137
  describe("cadet debug", () => {
134
- test("mints the URL on the platform, exports with the debug profile, and waits", async () => {
138
+ test("mints the URL on the cloud, exports with the debug profile, and waits", async () => {
135
139
  await debug();
136
140
 
137
141
  expect(fetchCalls).toHaveLength(1);
@@ -140,9 +144,7 @@ describe("cadet debug", () => {
140
144
  );
141
145
  expect(fetchCalls[0].init?.method).toBe("POST");
142
146
  expect(
143
- (fetchCalls[0].init?.headers as Record<string, string>)[
144
- "Cadet-Organization-Id"
145
- ],
147
+ new Headers(fetchCalls[0].init?.headers).get("Cadet-Organization-Id"),
146
148
  ).toBe("org-1");
147
149
 
148
150
  expect(exportMock).toHaveBeenCalledTimes(1);
@@ -155,7 +157,7 @@ describe("cadet debug", () => {
155
157
  profile: "debug",
156
158
  });
157
159
  expect(pollMock).toHaveBeenCalled();
158
- // The daemon was checked for the debug profile before the platform call.
160
+ // The daemon was checked for the debug profile before the cloud call.
159
161
  expect(identityMock).toHaveBeenCalledTimes(1);
160
162
  });
161
163
 
@@ -196,9 +198,7 @@ describe("cadet debug", () => {
196
198
  jobId: "job-1",
197
199
  type: "export",
198
200
  status: "complete",
199
- } as unknown as Awaited<
200
- ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
201
- >;
201
+ };
202
202
  });
203
203
 
204
204
  await debug();
@@ -207,7 +207,7 @@ describe("cadet debug", () => {
207
207
  expect(polls).toBe(2);
208
208
  });
209
209
 
210
- test("explains what to turn on when the platform refuses", async () => {
210
+ test("explains what to turn on when the cloud refuses", async () => {
211
211
  uploadUrlStatus = 403;
212
212
  const errors: string[] = [];
213
213
  const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
@@ -226,7 +226,7 @@ describe("cadet debug", () => {
226
226
  resolveTargetMock.mockReturnValue({
227
227
  ...LOCAL_ENTRY,
228
228
  cloud: "cadet",
229
- } as unknown as assistantConfig.AssistantEntry);
229
+ });
230
230
  const errorSpy = spyOn(console, "error").mockImplementation(() => {});
231
231
  try {
232
232
  await expect(debug()).rejects.toThrow("process.exit:1");
@@ -239,8 +239,8 @@ describe("cadet debug", () => {
239
239
  test("asks for a login when the assistant is not registered", async () => {
240
240
  resolveTargetMock.mockReturnValue({
241
241
  ...LOCAL_ENTRY,
242
- platformAssistantId: undefined,
243
- } as unknown as assistantConfig.AssistantEntry);
242
+ cloudAssistantId: undefined,
243
+ });
244
244
  const errors: string[] = [];
245
245
  const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
246
246
  errors.push(String(msg));
@@ -16,6 +16,8 @@ import { guardianTokenPath, resolveConfigDir } from "@mycadet/local-mode";
16
16
  import {
17
17
  getOrCreatePersistedDeviceId,
18
18
  guardianTokenDueForRenewal,
19
+ GuardianLeasePermanentError,
20
+ leaseGuardianToken,
19
21
  loadGuardianToken,
20
22
  refreshGuardianToken,
21
23
  refreshGuardianTokenResult,
@@ -25,6 +27,7 @@ import {
25
27
  } from "../lib/guardian-token.js";
26
28
  import { getConfigDir } from "../lib/environments/paths.js";
27
29
  import { getCurrentEnvironment } from "../lib/environments/resolve.js";
30
+ import { withPreconnect } from "./helpers/http.js";
28
31
 
29
32
  function makeTokenData(suffix: string): GuardianTokenData {
30
33
  const now = new Date().toISOString();
@@ -286,19 +289,21 @@ describe("refreshGuardianToken", () => {
286
289
  test("refreshes, persists the rotated token, sends an abort signal, releases the lock", async () => {
287
290
  seed(future());
288
291
  let sawSignal = false;
289
- globalThis.fetch = (async (_url: unknown, init?: RequestInit) => {
290
- sawSignal = init?.signal instanceof AbortSignal;
291
- return new Response(
292
- JSON.stringify({
293
- accessToken: "new-acc",
294
- refreshToken: "new-ref",
295
- accessTokenExpiresAt: future(),
296
- refreshTokenExpiresAt: future(),
297
- refreshAfter: "",
298
- }),
299
- { status: 200, headers: { "content-type": "application/json" } },
300
- );
301
- }) as typeof fetch;
292
+ globalThis.fetch = withPreconnect(
293
+ async (_url: unknown, init?: RequestInit) => {
294
+ sawSignal = init?.signal instanceof AbortSignal;
295
+ return new Response(
296
+ JSON.stringify({
297
+ accessToken: "new-acc",
298
+ refreshToken: "new-ref",
299
+ accessTokenExpiresAt: future(),
300
+ refreshTokenExpiresAt: future(),
301
+ refreshAfter: "",
302
+ }),
303
+ { status: 200, headers: { "content-type": "application/json" } },
304
+ );
305
+ },
306
+ );
302
307
 
303
308
  const result = await refreshGuardianToken("https://gw.example.com", "px");
304
309
 
@@ -311,13 +316,81 @@ describe("refreshGuardianToken", () => {
311
316
  expect(existsSync(lockPath("px"))).toBe(false); // lock released
312
317
  });
313
318
 
319
+ test("leases against the gateway's real init shape — refreshAfter arrives as a number", async () => {
320
+ // The gateway's GuardianBootstrapResult serializes refreshAfter as a
321
+ // NUMBER of ms (mintRefreshToken), and the strict lease check rejects a
322
+ // response missing any field. A string-only schema read that number as
323
+ // malformed, so every real hatch failed its lease while the ISO-string
324
+ // test doubles here stayed green.
325
+ globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
326
+ const url = String(input);
327
+ if (url.endsWith("/readyz")) {
328
+ return new Response("", { status: 404 }); // no starting gate — proceed
329
+ }
330
+ return new Response(
331
+ JSON.stringify({
332
+ guardianPrincipalId: "principal-lease",
333
+ accessToken: "access-lease",
334
+ accessTokenExpiresAt: Date.now() + 60 * 60 * 1000,
335
+ refreshToken: "refresh-lease",
336
+ refreshTokenExpiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
337
+ refreshAfter: Date.now() + 48 * 60 * 60 * 1000,
338
+ isNew: true,
339
+ mintedOverPriorEvidence: false,
340
+ }),
341
+ { status: 200, headers: { "content-type": "application/json" } },
342
+ );
343
+ });
344
+
345
+ const leased = await leaseGuardianToken("https://gw.example.com", "px");
346
+
347
+ expect(leased.accessToken).toBe("access-lease");
348
+ expect(leased.refreshAfter).toBeDefined();
349
+ const stored = loadGuardianToken("px");
350
+ expect(stored?.accessToken).toBe("access-lease");
351
+ expect(stored?.refreshAfter).toBe(leased.refreshAfter);
352
+ });
353
+
354
+ test("a malformed 200 names the unusable fields and is not retried", async () => {
355
+ globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
356
+ const url = String(input);
357
+ if (url.endsWith("/readyz")) {
358
+ return new Response("", { status: 404 });
359
+ }
360
+ return new Response(
361
+ JSON.stringify({
362
+ guardianPrincipalId: "principal-lease",
363
+ accessToken: 42,
364
+ refreshToken: "refresh-lease",
365
+ }),
366
+ { status: 200, headers: { "content-type": "application/json" } },
367
+ );
368
+ });
369
+
370
+ const err = await leaseGuardianToken("https://gw.example.com", "px").then(
371
+ () => null,
372
+ (e: unknown) => e,
373
+ );
374
+
375
+ if (!(err instanceof GuardianLeasePermanentError)) {
376
+ throw new Error(
377
+ `expected GuardianLeasePermanentError, got ${String(err)}`,
378
+ );
379
+ }
380
+ expect(err.message).toContain("(200)");
381
+ expect(err.message).toContain("accessToken arrived as number");
382
+ expect(err.message).toContain("refreshTokenExpiresAt arrived as absent");
383
+ });
384
+
314
385
  test("returns null without calling the gateway when the refresh token is expired", async () => {
315
386
  seed(new Date(Date.now() - 1000).toISOString());
316
387
  let called = false;
317
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
318
- called = true;
319
- return new Response("", { status: 200 });
320
- }) as typeof fetch;
388
+ globalThis.fetch = withPreconnect(
389
+ async (_url: unknown, _init?: RequestInit) => {
390
+ called = true;
391
+ return new Response("", { status: 200 });
392
+ },
393
+ );
321
394
 
322
395
  expect(
323
396
  await refreshGuardianToken("https://gw.example.com", "px"),
@@ -327,10 +400,12 @@ describe("refreshGuardianToken", () => {
327
400
 
328
401
  test("returns null when there is no stored token", async () => {
329
402
  let called = false;
330
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
331
- called = true;
332
- return new Response("", { status: 200 });
333
- }) as typeof fetch;
403
+ globalThis.fetch = withPreconnect(
404
+ async (_url: unknown, _init?: RequestInit) => {
405
+ called = true;
406
+ return new Response("", { status: 200 });
407
+ },
408
+ );
334
409
 
335
410
  expect(
336
411
  await refreshGuardianToken("https://gw.example.com", "missing"),
@@ -348,11 +423,13 @@ describe("refreshGuardianToken", () => {
348
423
  const old = new Date(Date.now() - 60_000);
349
424
  utimesSync(lp, old, old);
350
425
 
351
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) =>
352
- new Response(JSON.stringify({ accessToken: "new-acc" }), {
353
- status: 200,
354
- headers: { "content-type": "application/json" },
355
- })) as typeof fetch;
426
+ globalThis.fetch = withPreconnect(
427
+ async (_url: unknown, _init?: RequestInit) =>
428
+ new Response(JSON.stringify({ accessToken: "new-acc" }), {
429
+ status: 200,
430
+ headers: { "content-type": "application/json" },
431
+ }),
432
+ );
356
433
 
357
434
  const result = await refreshGuardianToken("https://gw.example.com", "px");
358
435
  expect(result?.accessToken).toBe("new-acc");
@@ -371,13 +448,15 @@ describe("refreshGuardianToken", () => {
371
448
  ]) {
372
449
  seed(future());
373
450
  let called = false;
374
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
375
- called = true;
376
- return new Response(JSON.stringify({ accessToken: "new-acc" }), {
377
- status: 200,
378
- headers: { "content-type": "application/json" },
379
- });
380
- }) as typeof fetch;
451
+ globalThis.fetch = withPreconnect(
452
+ async (_url: unknown, _init?: RequestInit) => {
453
+ called = true;
454
+ return new Response(JSON.stringify({ accessToken: "new-acc" }), {
455
+ status: 200,
456
+ headers: { "content-type": "application/json" },
457
+ });
458
+ },
459
+ );
381
460
 
382
461
  expect(await refreshGuardianToken(url, "px")).not.toBeNull();
383
462
  expect(called).toBe(true); // loopback http is allowed
@@ -387,10 +466,12 @@ describe("refreshGuardianToken", () => {
387
466
  test("refuses a non-loopback plaintext http URL: no fetch, returns null, warns", async () => {
388
467
  seed(future());
389
468
  let called = false;
390
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
391
- called = true;
392
- return new Response("", { status: 200 });
393
- }) as typeof fetch;
469
+ globalThis.fetch = withPreconnect(
470
+ async (_url: unknown, _init?: RequestInit) => {
471
+ called = true;
472
+ return new Response("", { status: 200 });
473
+ },
474
+ );
394
475
 
395
476
  const origWarn = console.warn;
396
477
  let warned = false;
@@ -411,10 +492,12 @@ describe("refreshGuardianToken", () => {
411
492
  test("refuses a malformed gateway URL: no fetch, returns null", async () => {
412
493
  seed(future());
413
494
  let called = false;
414
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
415
- called = true;
416
- return new Response("", { status: 200 });
417
- }) as typeof fetch;
495
+ globalThis.fetch = withPreconnect(
496
+ async (_url: unknown, _init?: RequestInit) => {
497
+ called = true;
498
+ return new Response("", { status: 200 });
499
+ },
500
+ );
418
501
 
419
502
  const origWarn = console.warn;
420
503
  console.warn = () => {};
@@ -428,11 +511,13 @@ describe("refreshGuardianToken", () => {
428
511
 
429
512
  test("an unreachable gateway is a 503, not a 401", async () => {
430
513
  seed(future());
431
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
432
- throw Object.assign(new TypeError("fetch failed"), {
433
- cause: { code: "ECONNREFUSED" },
434
- });
435
- }) as unknown as typeof fetch;
514
+ globalThis.fetch = withPreconnect(
515
+ async (_url: unknown, _init?: RequestInit) => {
516
+ throw Object.assign(new TypeError("fetch failed"), {
517
+ cause: { code: "ECONNREFUSED" },
518
+ });
519
+ },
520
+ );
436
521
 
437
522
  const result = await refreshGuardianTokenResult(
438
523
  "http://127.0.0.1:59999",
@@ -451,8 +536,10 @@ describe("refreshGuardianToken", () => {
451
536
 
452
537
  test("an HTTP 401 from the gateway is a spent credential", async () => {
453
538
  seed(future());
454
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) =>
455
- new Response("", { status: 401 })) as typeof fetch;
539
+ globalThis.fetch = withPreconnect(
540
+ async (_url: unknown, _init?: RequestInit) =>
541
+ new Response("", { status: 401 }),
542
+ );
456
543
 
457
544
  expect(
458
545
  await refreshGuardianTokenResult("https://gw.example.com", "px"),
@@ -465,8 +552,10 @@ describe("refreshGuardianToken", () => {
465
552
 
466
553
  test("an HTTP 403 from the refresh route is a spent credential, not a loopback refusal", async () => {
467
554
  seed(future());
468
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) =>
469
- new Response("", { status: 403 })) as typeof fetch;
555
+ globalThis.fetch = withPreconnect(
556
+ async (_url: unknown, _init?: RequestInit) =>
557
+ new Response("", { status: 403 }),
558
+ );
470
559
 
471
560
  expect(
472
561
  await refreshGuardianTokenResult("https://gw.example.com", "px"),
@@ -480,10 +569,12 @@ describe("refreshGuardianToken", () => {
480
569
  test("an insecure gateway URL is a local 403, not a spent credential", async () => {
481
570
  seed(future());
482
571
  let called = false;
483
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) => {
484
- called = true;
485
- return new Response("", { status: 200 });
486
- }) as typeof fetch;
572
+ globalThis.fetch = withPreconnect(
573
+ async (_url: unknown, _init?: RequestInit) => {
574
+ called = true;
575
+ return new Response("", { status: 200 });
576
+ },
577
+ );
487
578
 
488
579
  const origWarn = console.warn;
489
580
  console.warn = () => {};
@@ -503,8 +594,10 @@ describe("refreshGuardianToken", () => {
503
594
 
504
595
  test("an HTTP 503 from a still-starting gateway is retryable", async () => {
505
596
  seed(future());
506
- globalThis.fetch = (async (_url: unknown, _init?: RequestInit) =>
507
- new Response("", { status: 503 })) as typeof fetch;
597
+ globalThis.fetch = withPreconnect(
598
+ async (_url: unknown, _init?: RequestInit) =>
599
+ new Response("", { status: 503 }),
600
+ );
508
601
 
509
602
  const result = await refreshGuardianTokenResult(
510
603
  "https://gw.example.com",
@@ -292,7 +292,7 @@ describe("hatch provider secrets", () => {
292
292
  test("HATCH_PROVIDER_CHOICES only lists labeled providers", () => {
293
293
  for (const provider of HATCH_PROVIDER_CHOICES) {
294
294
  expect(formatProviderName(provider)).toBeTruthy();
295
- // Cadet Cloud is the managed-connection exception: no env var, no
295
+ // Cadet Cloud is the cloud-connection exception: no env var, no
296
296
  // key collection — everything else must have a supported setup flow.
297
297
  if (provider !== "cadet") {
298
298
  expect(isSupportedLlmProvider(provider)).toBe(true);
@@ -313,7 +313,7 @@ describe("hatch provider secrets", () => {
313
313
  ]);
314
314
  });
315
315
 
316
- test("Cadet Cloud configures silently through the managed connection", async () => {
316
+ test("Cadet Cloud configures silently through the cloud connection", async () => {
317
317
  const { calls, fetchImpl } = makeFetch([]);
318
318
  const logs: string[] = [];
319
319
 
@@ -0,0 +1,22 @@
1
+ import type { GuardianTokenData } from "../../lib/guardian-token.js";
2
+
3
+ /**
4
+ * A complete `GuardianTokenData` fixture with per-test overrides — the type's
5
+ * nine fields are all required, so partial fakes need this to stand in.
6
+ */
7
+ export function makeGuardianTokenData(
8
+ over: Partial<GuardianTokenData> = {},
9
+ ): GuardianTokenData {
10
+ return {
11
+ guardianPrincipalId: "principal-1",
12
+ accessToken: "local-token",
13
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
14
+ refreshToken: "refresh-token",
15
+ refreshTokenExpiresAt: new Date(Date.now() + 3_600_000).toISOString(),
16
+ refreshAfter: new Date(Date.now() + 30_000).toISOString(),
17
+ isNew: false,
18
+ deviceId: "device-1",
19
+ leasedAt: new Date().toISOString(),
20
+ ...over,
21
+ };
22
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * bun-types' global fetch carries a `preconnect` member; these mocks only need
3
+ * the call shape, so hand the assignment a member-complete callable. The SUT
4
+ * never calls `preconnect`.
5
+ */
6
+ export function withPreconnect(
7
+ impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
8
+ ): typeof globalThis.fetch {
9
+ return Object.assign(impl, { preconnect: () => {} });
10
+ }
@@ -158,9 +158,9 @@ describe("buildIngressNginxConfig", () => {
158
158
  remoteWebIngress: {
159
159
  webDistDir: "/tmp/cadet web/dist",
160
160
  config: {
161
- mode: "remote-gateway",
161
+ remote: "docker",
162
162
  apiBaseUrl: "/v1",
163
- platformDisabled: true,
163
+ cloudDisabled: true,
164
164
  },
165
165
  },
166
166
  });
@@ -330,7 +330,7 @@ describe("buildIngressNginxConfig", () => {
330
330
  expect(remoteConf).toContain("default_type application/json;");
331
331
  expect(remoteConf).toContain('add_header Cache-Control "no-store";');
332
332
  expect(remoteConf).toContain(
333
- 'return 200 "{\\"mode\\":\\"remote-gateway\\",\\"apiBaseUrl\\":\\"/v1\\",\\"platformDisabled\\":true,\\"disablePlatform\\":true}";',
333
+ 'return 200 "{\\"remote\\":\\"docker\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true}";',
334
334
  );
335
335
  expect(remoteConf).not.toContain("assistantId");
336
336
  });
@@ -347,7 +347,7 @@ describe("buildIngressNginxConfig", () => {
347
347
  },
348
348
  });
349
349
  expect(named).toContain(
350
- 'return 200 "{\\"mode\\":\\"remote-gateway\\",\\"apiBaseUrl\\":\\"/v1\\",\\"platformDisabled\\":true,\\"disablePlatform\\":true,\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
350
+ 'return 200 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true,\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
351
351
  );
352
352
  });
353
353
 
@@ -447,19 +447,19 @@ describe("buildRemoteWebIndexHtml", () => {
447
447
  const html =
448
448
  '<html><head><script>window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}</script></head><body></body></html>';
449
449
  const result = buildRemoteWebIndexHtml(html, {
450
- mode: "remote-gateway",
450
+ remote: "docker",
451
451
  apiBaseUrl: "/v1",
452
- disablePlatform: true,
452
+ disableCloud: true,
453
453
  });
454
454
 
455
455
  expect(result).toContain(
456
456
  'window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}',
457
457
  );
458
458
  expect(result).toContain(
459
- 'window.__CADET_CONFIG__={"mode":"remote-gateway","apiBaseUrl":"/v1","disablePlatform":true}',
459
+ 'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1","disableCloud":true}',
460
460
  );
461
461
  expect(result.indexOf('"webUrl"')).toBeLessThan(
462
- result.indexOf('"remote-gateway"'),
462
+ result.indexOf('"docker"'),
463
463
  );
464
464
  });
465
465
 
@@ -484,7 +484,7 @@ describe("buildRemoteWebIndexHtml", () => {
484
484
  "</head><body></body></html>",
485
485
  ].join("\n");
486
486
 
487
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
487
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
488
488
 
489
489
  expect(result.match(/rel="modulepreload"/g)).toHaveLength(4);
490
490
  expect(result).toContain('href="/assistant/assets/a-1.js"');
@@ -506,7 +506,7 @@ describe("buildRemoteWebIndexHtml", () => {
506
506
  "</head><body></body></html>",
507
507
  ].join("\n");
508
508
 
509
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
509
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
510
510
 
511
511
  expect(result).not.toContain("modulepreload");
512
512
  expect(result).not.toContain("/assistant/assets/chunk-0.js");
@@ -522,7 +522,7 @@ describe("buildRemoteWebIndexHtml", () => {
522
522
  );
523
523
  const html = `<html><head>${preloads.join("\n")}</head><body></body></html>`;
524
524
 
525
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
525
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
526
526
 
527
527
  expect(result.match(/rel="modulepreload"/g)).toHaveLength(
528
528
  MAX_PRESERVED_MODULE_PRELOADS,
@@ -862,10 +862,10 @@ function spaConfigHash(
862
862
  JSON.stringify({
863
863
  template: EDGE_TEMPLATE_VERSION,
864
864
  config: {
865
- mode: "remote-gateway",
865
+ remote: "custom",
866
866
  apiBaseUrl: "/v1",
867
- platformDisabled: true,
868
- disablePlatform: true,
867
+ cloudDisabled: true,
868
+ disableCloud: true,
869
869
  ...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
870
870
  ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
871
871
  hubUrl: PRODUCTION_HUB_URL,
@@ -988,7 +988,7 @@ describe("startRemoteWebIngress", () => {
988
988
  join(ws, "data", "ingress", "assistant-index.html"),
989
989
  "utf-8",
990
990
  );
991
- expect(indexHtml).toContain("remote-gateway");
991
+ expect(indexHtml).toContain('"remote":"custom"');
992
992
  expect(indexHtml).toContain(`"hubUrl":"${PRODUCTION_HUB_URL}"`);
993
993
  // No assistant name was provided, so the served config omits the label.
994
994
  expect(indexHtml).not.toContain("assistantName");
@@ -1352,10 +1352,10 @@ describe("startRemoteWebIngress", () => {
1352
1352
  JSON.stringify({
1353
1353
  template: 1,
1354
1354
  config: {
1355
- mode: "remote-gateway",
1355
+ remote: "custom",
1356
1356
  apiBaseUrl: "/v1",
1357
- platformDisabled: true,
1358
- disablePlatform: true,
1357
+ cloudDisabled: true,
1358
+ disableCloud: true,
1359
1359
  hubUrl: PRODUCTION_HUB_URL,
1360
1360
  },
1361
1361
  }),
@@ -141,7 +141,7 @@ describe("loadAllAssistantsAcrossEnvs", () => {
141
141
  function makeEnv(name: string, lockfileDir: string): EnvironmentDefinition {
142
142
  return {
143
143
  name,
144
- platformUrl: "https://example.invalid",
144
+ cloudUrl: "https://example.invalid",
145
145
  webUrl: "https://example.invalid",
146
146
  lockfileDirOverride: lockfileDir,
147
147
  };