@mycadet/cli 0.12.6-dev.202610091338.ee7f3e3 → 0.12.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/package.json +4 -3
- package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +66 -74
- package/src/__tests__/pair.test.ts +135 -121
- package/src/__tests__/uninstall.test.ts +171 -0
- package/src/commands/client.ts +76 -34
- package/src/commands/ps.ts +1 -0
- package/src/commands/uninstall.ts +228 -0
- package/src/index.ts +2 -0
- package/src/lib/assistant-config.ts +9 -13
- package/src/lib/backup-ops.ts +3 -4
- package/src/lib/client-identity.ts +8 -10
- package/src/lib/guardian-token.ts +72 -18
- package/src/lib/health-check.ts +24 -15
- package/src/lib/local-hatch-lock.ts +19 -27
- package/src/lib/local-runtime-client.ts +25 -14
- package/src/lib/ngrok.ts +48 -17
- package/src/lib/platform-client.ts +141 -72
- package/src/lib/retire-archive.ts +4 -4
- package/src/lib/retire-local.ts +2 -25
- package/src/lib/teleport-backup.ts +22 -13
- package/src/lib/terminal-client.ts +19 -12
- package/src/lib/tunnel-edge.ts +11 -13
- package/src/lib/upgrade-lifecycle.ts +16 -4
- package/src/lib/workos-pkce.ts +51 -25
- package/src/lib/json.ts +0 -6
|
@@ -16,6 +16,15 @@ import { getLockfilePlatformBaseUrl } from "./assistant-config.js";
|
|
|
16
16
|
import { getConfigDir, getConfigDirs } from "./environments/paths.js";
|
|
17
17
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
18
18
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
const PlatformErrorDetailSchema = z.object({
|
|
22
|
+
detail: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const InProgressResponseSchema = z.object({
|
|
26
|
+
in_progress: z.boolean().optional(),
|
|
27
|
+
});
|
|
19
28
|
|
|
20
29
|
function getPlatformTokenPath(): string {
|
|
21
30
|
return join(getConfigDir(getCurrentEnvironment()), "platform-token");
|
|
@@ -200,6 +209,95 @@ export interface HatchedAssistant {
|
|
|
200
209
|
status: string;
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
const HatchedAssistantSchema = z.object({
|
|
213
|
+
id: z.string(),
|
|
214
|
+
name: z.string(),
|
|
215
|
+
status: z.string(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const HatchedListResponseSchema = z.object({
|
|
219
|
+
results: z.array(HatchedAssistantSchema).optional(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const EnsureRegistrationResponseSchema = z.object({
|
|
223
|
+
assistant: z.object({ id: z.string(), name: z.string() }),
|
|
224
|
+
registration: z.object({
|
|
225
|
+
client_installation_id: z.string(),
|
|
226
|
+
runtime_assistant_id: z.string(),
|
|
227
|
+
client_platform: z.string(),
|
|
228
|
+
}),
|
|
229
|
+
assistant_api_key: z.string().nullable(),
|
|
230
|
+
webhook_secret: z.string(),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const ReprovisionApiKeyResponseSchema = z.object({
|
|
234
|
+
provisioning: z.object({ assistant_api_key: z.string() }),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const SecretReadResponseSchema = z
|
|
238
|
+
.object({
|
|
239
|
+
found: z.boolean(),
|
|
240
|
+
value: z.string().optional(),
|
|
241
|
+
unreachable: z.boolean().optional(),
|
|
242
|
+
})
|
|
243
|
+
.catch({ found: false });
|
|
244
|
+
|
|
245
|
+
const ReleaseChannelResponseSchema = z.object({
|
|
246
|
+
current_release_version: z.string().nullable().optional(),
|
|
247
|
+
release_channel: z.string().optional(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const OrganizationListResponseSchema = z.object({
|
|
251
|
+
results: z.array(z.object({ id: z.string(), name: z.string() })).optional(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const AllauthSessionResponseSchema = z.object({
|
|
255
|
+
status: z.number(),
|
|
256
|
+
data: z.object({
|
|
257
|
+
user: z.object({ id: z.string(), email: z.string(), display: z.string() }),
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const VersionCheckResponseSchema = z
|
|
262
|
+
.object({
|
|
263
|
+
detail: z.string().optional(),
|
|
264
|
+
version: z.string().nullable().optional(),
|
|
265
|
+
})
|
|
266
|
+
.catch({});
|
|
267
|
+
|
|
268
|
+
const PassthroughBodySchema = z.record(z.string(), z.unknown()).catch({});
|
|
269
|
+
|
|
270
|
+
const BundleUploadUrlSchema = z.object({
|
|
271
|
+
url: z.string(),
|
|
272
|
+
bundle_key: z.string(),
|
|
273
|
+
expires_at: z.string(),
|
|
274
|
+
max_content_length: z.number().optional(),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const BundleImportErrorSchema = z
|
|
278
|
+
.object({
|
|
279
|
+
detail: z.string().optional(),
|
|
280
|
+
reason: z.string().optional(),
|
|
281
|
+
bundle_compat: z
|
|
282
|
+
.object({
|
|
283
|
+
min_runtime_version: z.string(),
|
|
284
|
+
max_runtime_version: z.string().nullable(),
|
|
285
|
+
})
|
|
286
|
+
.optional()
|
|
287
|
+
.catch(undefined),
|
|
288
|
+
target_runtime_version: z.string().optional(),
|
|
289
|
+
})
|
|
290
|
+
.catch({});
|
|
291
|
+
|
|
292
|
+
const RawUnifiedJobStatusSchema = z.object({
|
|
293
|
+
job_id: z.string(),
|
|
294
|
+
type: z.enum(["export", "import"]),
|
|
295
|
+
status: z.enum(["processing", "complete", "failed"]),
|
|
296
|
+
bundle_key: z.string().optional(),
|
|
297
|
+
result: z.unknown().optional(),
|
|
298
|
+
error: z.string().optional(),
|
|
299
|
+
});
|
|
300
|
+
|
|
203
301
|
export interface HatchAssistantResult {
|
|
204
302
|
assistant: HatchedAssistant;
|
|
205
303
|
/** true when the platform returned an existing assistant (HTTP 200) */
|
|
@@ -276,7 +374,7 @@ export async function ensureSelfHostedLocalRegistration(
|
|
|
276
374
|
);
|
|
277
375
|
}
|
|
278
376
|
|
|
279
|
-
return (await response.json())
|
|
377
|
+
return EnsureRegistrationResponseSchema.parse(await response.json());
|
|
280
378
|
}
|
|
281
379
|
|
|
282
380
|
// ---------------------------------------------------------------------------
|
|
@@ -342,7 +440,7 @@ export async function reprovisionAssistantApiKey(
|
|
|
342
440
|
);
|
|
343
441
|
}
|
|
344
442
|
|
|
345
|
-
return (await response.json())
|
|
443
|
+
return ReprovisionApiKeyResponseSchema.parse(await response.json());
|
|
346
444
|
}
|
|
347
445
|
|
|
348
446
|
// ---------------------------------------------------------------------------
|
|
@@ -442,11 +540,7 @@ export async function readGatewayCredential(
|
|
|
442
540
|
return { value: null, unreachable: response.status >= 500 };
|
|
443
541
|
}
|
|
444
542
|
|
|
445
|
-
const json = (await response.json())
|
|
446
|
-
found: boolean;
|
|
447
|
-
value?: string;
|
|
448
|
-
unreachable?: boolean;
|
|
449
|
-
};
|
|
543
|
+
const json = SecretReadResponseSchema.parse(await response.json());
|
|
450
544
|
// The daemon's /v1/secrets/read returns `unreachable: true` when the
|
|
451
545
|
// credential backend (CES) can't be reached. Respect that signal.
|
|
452
546
|
if (json.unreachable) {
|
|
@@ -553,14 +647,14 @@ export async function hatchAssistant(
|
|
|
553
647
|
});
|
|
554
648
|
|
|
555
649
|
if (response.ok) {
|
|
556
|
-
const assistant = (await response.json())
|
|
650
|
+
const assistant = HatchedAssistantSchema.parse(await response.json());
|
|
557
651
|
return { assistant, reusedExisting: response.status === 200 };
|
|
558
652
|
}
|
|
559
653
|
|
|
560
654
|
if (response.status === 401 || response.status === 403) {
|
|
561
|
-
const detail =
|
|
562
|
-
|
|
563
|
-
|
|
655
|
+
const detail = PlatformErrorDetailSchema
|
|
656
|
+
.catch({})
|
|
657
|
+
.parse(await response.json().catch(() => ({})));
|
|
564
658
|
throw new Error(
|
|
565
659
|
detail.detail ??
|
|
566
660
|
"Invalid or expired token. Run `cadet login` to re-authenticate.",
|
|
@@ -571,9 +665,9 @@ export async function hatchAssistant(
|
|
|
571
665
|
throw new Error("Insufficient balance to hatch a new assistant.");
|
|
572
666
|
}
|
|
573
667
|
|
|
574
|
-
const errorBody =
|
|
575
|
-
|
|
576
|
-
|
|
668
|
+
const errorBody = PlatformErrorDetailSchema
|
|
669
|
+
.catch({})
|
|
670
|
+
.parse(await response.json().catch(() => ({})));
|
|
577
671
|
throw new Error(
|
|
578
672
|
errorBody.detail ??
|
|
579
673
|
`Platform API error: ${response.status} ${response.statusText}`,
|
|
@@ -614,11 +708,9 @@ export async function checkExistingPlatformAssistant(
|
|
|
614
708
|
return null;
|
|
615
709
|
}
|
|
616
710
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const active = body.results?.find((a) => a.status === "active");
|
|
621
|
-
return active ?? null;
|
|
711
|
+
const parsed = HatchedListResponseSchema.safeParse(await response.json());
|
|
712
|
+
const results = parsed.success ? parsed.data.results : undefined;
|
|
713
|
+
return results?.find((a) => a.status === "active") ?? null;
|
|
622
714
|
} finally {
|
|
623
715
|
clearTimeout(timeoutId);
|
|
624
716
|
}
|
|
@@ -649,11 +741,10 @@ export async function fetchPlatformAssistants(
|
|
|
649
741
|
|
|
650
742
|
if (!response.ok) return [];
|
|
651
743
|
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
};
|
|
744
|
+
const parsed = HatchedListResponseSchema.safeParse(await response.json());
|
|
745
|
+
const results = parsed.success ? (parsed.data.results ?? []) : [];
|
|
655
746
|
|
|
656
|
-
return
|
|
747
|
+
return results.filter((a) => a.status === "active");
|
|
657
748
|
} finally {
|
|
658
749
|
clearTimeout(timeoutId);
|
|
659
750
|
}
|
|
@@ -685,10 +776,10 @@ export async function fetchAssistantDetail(
|
|
|
685
776
|
|
|
686
777
|
if (!response.ok) return null;
|
|
687
778
|
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
};
|
|
779
|
+
const parsed = ReleaseChannelResponseSchema.safeParse(
|
|
780
|
+
await response.json(),
|
|
781
|
+
);
|
|
782
|
+
const body = parsed.success ? parsed.data : {};
|
|
692
783
|
return {
|
|
693
784
|
currentReleaseVersion: body.current_release_version ?? null,
|
|
694
785
|
releaseChannel: body.release_channel === "preview" ? "preview" : "stable",
|
|
@@ -720,8 +811,10 @@ export async function fetchUpgradeInProgress(
|
|
|
720
811
|
|
|
721
812
|
if (!response.ok) return null;
|
|
722
813
|
|
|
723
|
-
const
|
|
724
|
-
return
|
|
814
|
+
const parsed = InProgressResponseSchema.safeParse(await response.json());
|
|
815
|
+
return parsed.success && parsed.data.in_progress !== undefined
|
|
816
|
+
? parsed.data.in_progress
|
|
817
|
+
: null;
|
|
725
818
|
} catch {
|
|
726
819
|
return null;
|
|
727
820
|
}
|
|
@@ -733,10 +826,6 @@ export interface PlatformUser {
|
|
|
733
826
|
display: string;
|
|
734
827
|
}
|
|
735
828
|
|
|
736
|
-
interface OrganizationListResponse {
|
|
737
|
-
results: { id: string; name: string }[];
|
|
738
|
-
}
|
|
739
|
-
|
|
740
829
|
export async function fetchOrganizationId(
|
|
741
830
|
token: string,
|
|
742
831
|
platformUrl?: string,
|
|
@@ -762,8 +851,10 @@ export async function fetchOrganizationId(
|
|
|
762
851
|
);
|
|
763
852
|
}
|
|
764
853
|
|
|
765
|
-
const
|
|
766
|
-
|
|
854
|
+
const parsed = OrganizationListResponseSchema.safeParse(
|
|
855
|
+
await response.json(),
|
|
856
|
+
);
|
|
857
|
+
const orgId = parsed.success ? parsed.data.results?.[0]?.id : undefined;
|
|
767
858
|
if (!orgId) {
|
|
768
859
|
throw new Error("No organization found for this account.");
|
|
769
860
|
}
|
|
@@ -773,17 +864,6 @@ export async function fetchOrganizationId(
|
|
|
773
864
|
}
|
|
774
865
|
}
|
|
775
866
|
|
|
776
|
-
interface AllauthSessionResponse {
|
|
777
|
-
status: number;
|
|
778
|
-
data: {
|
|
779
|
-
user: {
|
|
780
|
-
id: string;
|
|
781
|
-
email: string;
|
|
782
|
-
display: string;
|
|
783
|
-
};
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
867
|
export async function fetchCurrentUser(
|
|
788
868
|
token: string,
|
|
789
869
|
platformUrl?: string,
|
|
@@ -816,7 +896,7 @@ export async function fetchCurrentUser(
|
|
|
816
896
|
);
|
|
817
897
|
}
|
|
818
898
|
|
|
819
|
-
const body = (await response.json())
|
|
899
|
+
const body = AllauthSessionResponseSchema.parse(await response.json());
|
|
820
900
|
return body.data.user;
|
|
821
901
|
} finally {
|
|
822
902
|
clearTimeout(timeoutId);
|
|
@@ -843,10 +923,9 @@ export async function rollbackPlatformAssistant(
|
|
|
843
923
|
},
|
|
844
924
|
);
|
|
845
925
|
|
|
846
|
-
const body =
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
};
|
|
926
|
+
const body = VersionCheckResponseSchema.parse(
|
|
927
|
+
await response.json().catch(() => ({})),
|
|
928
|
+
);
|
|
850
929
|
|
|
851
930
|
if (response.status === 200) {
|
|
852
931
|
return { detail: body.detail ?? "", version: body.version ?? null };
|
|
@@ -907,10 +986,9 @@ export async function platformImportPreflightFromGcs(
|
|
|
907
986
|
},
|
|
908
987
|
);
|
|
909
988
|
|
|
910
|
-
const body =
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
>;
|
|
989
|
+
const body = PassthroughBodySchema.parse(
|
|
990
|
+
await response.json().catch(() => ({})),
|
|
991
|
+
);
|
|
914
992
|
return { statusCode: response.status, body };
|
|
915
993
|
}
|
|
916
994
|
|
|
@@ -934,10 +1012,9 @@ export async function platformImportBundleFromGcs(
|
|
|
934
1012
|
throw new Error("Bundle too large to import");
|
|
935
1013
|
}
|
|
936
1014
|
|
|
937
|
-
const body =
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
>;
|
|
1015
|
+
const body = PassthroughBodySchema.parse(
|
|
1016
|
+
await response.json().catch(() => ({})),
|
|
1017
|
+
);
|
|
941
1018
|
return { statusCode: response.status, body };
|
|
942
1019
|
}
|
|
943
1020
|
|
|
@@ -1128,12 +1205,7 @@ export async function platformRequestSignedUrl(
|
|
|
1128
1205
|
}
|
|
1129
1206
|
|
|
1130
1207
|
if (response.status === 201 || response.status === 200) {
|
|
1131
|
-
const json = (await response.json())
|
|
1132
|
-
url: string;
|
|
1133
|
-
bundle_key: string;
|
|
1134
|
-
expires_at: string;
|
|
1135
|
-
max_content_length?: number;
|
|
1136
|
-
};
|
|
1208
|
+
const json = BundleUploadUrlSchema.parse(await response.json());
|
|
1137
1209
|
return {
|
|
1138
1210
|
url: json.url,
|
|
1139
1211
|
bundleKey: json.bundle_key,
|
|
@@ -1145,12 +1217,9 @@ export async function platformRequestSignedUrl(
|
|
|
1145
1217
|
// Non-success body. Read once and reuse for both the 422 version-mismatch
|
|
1146
1218
|
// branch and the generic-error fallthrough — `response.json()` consumes
|
|
1147
1219
|
// the body, so a second read would always return undefined.
|
|
1148
|
-
const errorBody =
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
bundle_compat?: BundleCompatibility;
|
|
1152
|
-
target_runtime_version?: string;
|
|
1153
|
-
};
|
|
1220
|
+
const errorBody = BundleImportErrorSchema.parse(
|
|
1221
|
+
await response.json().catch(() => ({})),
|
|
1222
|
+
);
|
|
1154
1223
|
|
|
1155
1224
|
if (
|
|
1156
1225
|
response.status === 422 &&
|
|
@@ -1197,6 +1266,6 @@ export async function platformPollJobStatus(
|
|
|
1197
1266
|
);
|
|
1198
1267
|
}
|
|
1199
1268
|
|
|
1200
|
-
const raw = (await response.json())
|
|
1269
|
+
const raw = RawUnifiedJobStatusSchema.parse(await response.json());
|
|
1201
1270
|
return parseUnifiedJobStatus(raw);
|
|
1202
1271
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mkdirSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join, posix, win32 } from "path";
|
|
4
3
|
|
|
4
|
+
import { getStateDir } from "./environments/paths.js";
|
|
5
|
+
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
6
|
+
|
|
5
7
|
function isPathInside(
|
|
6
8
|
parent: string,
|
|
7
9
|
candidate: string,
|
|
@@ -17,9 +19,7 @@ function isPathInside(
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function getRetiredDir(): string {
|
|
20
|
-
const
|
|
21
|
-
process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
22
|
-
const dir = join(xdgData, "cadet", "retired");
|
|
22
|
+
const dir = join(getStateDir(getCurrentEnvironment()), "retired");
|
|
23
23
|
mkdirSync(dir, { recursive: true });
|
|
24
24
|
return dir;
|
|
25
25
|
}
|
package/src/lib/retire-local.ts
CHANGED
|
@@ -128,34 +128,11 @@ export async function retireLocal(
|
|
|
128
128
|
|
|
129
129
|
// Stop gateway via PID file — use a longer timeout because the gateway has a
|
|
130
130
|
// drain window (5s) before it exits.
|
|
131
|
-
|
|
132
|
-
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
131
|
+
await stopProcessByPidFile(join(cadetDir, "gateway.pid"), "gateway", undefined, 7000);
|
|
133
132
|
|
|
134
133
|
// Stop the CES sibling — it is stopped by its PID file, a no-op when the
|
|
135
134
|
// PID file is absent (e.g. the sibling was never started or already exited).
|
|
136
|
-
|
|
137
|
-
const cesStopped = await stopProcessByPidFile(
|
|
138
|
-
cesPidFile,
|
|
139
|
-
"credential-executor",
|
|
140
|
-
);
|
|
141
|
-
if (cesStopped) {
|
|
142
|
-
reporter.log("credential-executor stopped.");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Stop Qdrant. The daemon's graceful shutdown tries to stop it via
|
|
146
|
-
// qdrantManager.stop(), but a daemon that never completes that shutdown
|
|
147
|
-
// (crash, OOM kill, or the SIGKILL ceiling above) leaves Qdrant running as
|
|
148
|
-
// an orphan. Check both the current PID file location and the legacy one.
|
|
149
|
-
const qdrantPidFile = join(
|
|
150
|
-
cadetDir,
|
|
151
|
-
"workspace",
|
|
152
|
-
"data",
|
|
153
|
-
"qdrant",
|
|
154
|
-
"qdrant.pid",
|
|
155
|
-
);
|
|
156
|
-
const qdrantLegacyPidFile = join(cadetDir, "qdrant.pid");
|
|
157
|
-
await stopProcessByPidFile(qdrantPidFile, "qdrant", undefined, 5000);
|
|
158
|
-
await stopProcessByPidFile(qdrantLegacyPidFile, "qdrant", undefined, 5000);
|
|
135
|
+
await stopProcessByPidFile(join(cadetDir, "ces.pid"), "credential-executor");
|
|
159
136
|
|
|
160
137
|
// Stop the nginx ingress if one is fronting this gateway — it would
|
|
161
138
|
// otherwise be orphaned when the instance directory is archived.
|
|
@@ -23,8 +23,23 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import type { AssistantEntry } from "./assistant-config.js";
|
|
25
25
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
26
|
-
import { isJsonObject } from "./json";
|
|
27
26
|
import { authHeaders, invalidateOrgIdCache } from "./platform-client.js";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
|
|
29
|
+
const ManagedBackupEntrySchema = z.object({
|
|
30
|
+
snapshot_name: z.string().optional(),
|
|
31
|
+
created_at: z.string().optional(),
|
|
32
|
+
ready_to_use: z.boolean().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const ManagedBackupsResponseSchema = z.object({
|
|
36
|
+
backups: z.array(ManagedBackupEntrySchema).optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const CreatedBackupResponseSchema = z.object({
|
|
40
|
+
snapshot_name: z.string().optional(),
|
|
41
|
+
ready_to_use: z.boolean().optional(),
|
|
42
|
+
});
|
|
28
43
|
|
|
29
44
|
type PlatformEntry = Pick<AssistantEntry, "runtimeUrl" | "assistantId">;
|
|
30
45
|
|
|
@@ -73,8 +88,8 @@ async function fetchPlatformBackups(
|
|
|
73
88
|
`Cadet Cloud backup list failed ${await errorSuffix(response)}`,
|
|
74
89
|
);
|
|
75
90
|
}
|
|
76
|
-
const
|
|
77
|
-
return
|
|
91
|
+
const parsed = ManagedBackupsResponseSchema.safeParse(await response.json());
|
|
92
|
+
return parsed.success ? (parsed.data.backups ?? []) : [];
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
/**
|
|
@@ -115,20 +130,14 @@ export async function createPlatformBackup(
|
|
|
115
130
|
);
|
|
116
131
|
}
|
|
117
132
|
const body: unknown = await response.json().catch(() => null);
|
|
118
|
-
const created =
|
|
119
|
-
|
|
120
|
-
snapshot_name:
|
|
121
|
-
typeof body.snapshot_name === "string" ? body.snapshot_name : undefined,
|
|
122
|
-
ready_to_use: body.ready_to_use === true ? true : undefined,
|
|
123
|
-
}
|
|
124
|
-
: null;
|
|
125
|
-
const snapshotName = created?.snapshot_name;
|
|
126
|
-
if (!snapshotName) {
|
|
133
|
+
const created = CreatedBackupResponseSchema.safeParse(body);
|
|
134
|
+
if (!created.success || !created.data.snapshot_name) {
|
|
127
135
|
throw new Error("Cadet Cloud backup create returned no snapshot name");
|
|
128
136
|
}
|
|
129
|
-
if (created
|
|
137
|
+
if (created.data.ready_to_use === true) {
|
|
130
138
|
return;
|
|
131
139
|
}
|
|
140
|
+
const snapshotName = created.data.snapshot_name;
|
|
132
141
|
|
|
133
142
|
const deadline = Date.now() + timeoutMs;
|
|
134
143
|
while (Date.now() < deadline) {
|
|
@@ -6,9 +6,19 @@
|
|
|
6
6
|
* the web UI's xterm.js terminal uses.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { authHeaders, getPlatformUrl } from "./platform-client.js";
|
|
10
9
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
11
|
-
import {
|
|
10
|
+
import { authHeaders, getPlatformUrl } from "./platform-client.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
const TerminalSessionResponseSchema = z.object({ session_id: z.string() });
|
|
14
|
+
|
|
15
|
+
const TerminalOutputEventSchema = z.object({
|
|
16
|
+
seq: z.number(),
|
|
17
|
+
/** Base64-encoded PTY output bytes. */
|
|
18
|
+
data: z.string(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type TerminalOutputEvent = z.infer<typeof TerminalOutputEventSchema>;
|
|
12
22
|
|
|
13
23
|
// ---------------------------------------------------------------------------
|
|
14
24
|
// Create / Close
|
|
@@ -41,11 +51,11 @@ export async function createTerminalSession(
|
|
|
41
51
|
`Failed to create terminal session (${response.status}): ${detail || response.statusText}`,
|
|
42
52
|
);
|
|
43
53
|
}
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
54
|
+
const parsed = TerminalSessionResponseSchema.safeParse(await response.json());
|
|
55
|
+
if (!parsed.success) {
|
|
46
56
|
throw new Error("Terminal session response did not include a session_id.");
|
|
47
57
|
}
|
|
48
|
-
return
|
|
58
|
+
return parsed.data;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
export async function closeTerminalSession(
|
|
@@ -125,12 +135,6 @@ export async function resizeTerminalSession(
|
|
|
125
135
|
// SSE event stream
|
|
126
136
|
// ---------------------------------------------------------------------------
|
|
127
137
|
|
|
128
|
-
export interface TerminalOutputEvent {
|
|
129
|
-
seq: number;
|
|
130
|
-
/** Base64-encoded PTY output bytes. */
|
|
131
|
-
data: string;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
138
|
/**
|
|
135
139
|
* Subscribe to the terminal output SSE stream. Yields parsed events as they
|
|
136
140
|
* arrive. The generator completes when the stream ends or is aborted.
|
|
@@ -175,7 +179,10 @@ export async function* subscribeTerminalEvents(
|
|
|
175
179
|
const trimmed = line.trimEnd();
|
|
176
180
|
if (trimmed.startsWith("data: ")) {
|
|
177
181
|
try {
|
|
178
|
-
|
|
182
|
+
const frame = TerminalOutputEventSchema.safeParse(
|
|
183
|
+
JSON.parse(trimmed.slice(6)),
|
|
184
|
+
);
|
|
185
|
+
if (frame.success) yield frame.data;
|
|
179
186
|
} catch {
|
|
180
187
|
// Skip malformed SSE frames
|
|
181
188
|
}
|
package/src/lib/tunnel-edge.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
loadRawConfig,
|
|
15
15
|
parseGatewayPortFromEntryUrls,
|
|
16
16
|
} from "./ingress-config.js";
|
|
17
|
-
import {
|
|
17
|
+
import { IngressConfigSchema } from "./ngrok.js";
|
|
18
18
|
import {
|
|
19
19
|
ensureTunnelEdge,
|
|
20
20
|
formatEdgeMode,
|
|
@@ -34,12 +34,11 @@ export const DOCKER_GATEWAY_READY_TIMEOUT_MS = 5 * 60_000;
|
|
|
34
34
|
* enabled with a saved public URL.
|
|
35
35
|
*/
|
|
36
36
|
function wantsWebIngress(config: Record<string, unknown>): boolean {
|
|
37
|
-
const
|
|
37
|
+
const parsed = IngressConfigSchema.safeParse(config.ingress);
|
|
38
38
|
return (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ingress.publicBaseUrl.trim() !== ""
|
|
39
|
+
parsed.success &&
|
|
40
|
+
parsed.data.enabled === true &&
|
|
41
|
+
(parsed.data.publicBaseUrl?.trim() ?? "") !== ""
|
|
43
42
|
);
|
|
44
43
|
}
|
|
45
44
|
|
|
@@ -189,17 +188,16 @@ function ownsSharedIngress(
|
|
|
189
188
|
) {
|
|
190
189
|
return false;
|
|
191
190
|
}
|
|
192
|
-
let publicBaseUrl:
|
|
191
|
+
let publicBaseUrl: string | undefined;
|
|
193
192
|
try {
|
|
194
|
-
|
|
195
|
-
loadRawConfig(workspaceDir).ingress
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)?.publicBaseUrl;
|
|
193
|
+
const parsed = IngressConfigSchema.safeParse(
|
|
194
|
+
loadRawConfig(workspaceDir).ingress,
|
|
195
|
+
);
|
|
196
|
+
publicBaseUrl = parsed.success ? parsed.data.publicBaseUrl : undefined;
|
|
199
197
|
} catch {
|
|
200
198
|
return true;
|
|
201
199
|
}
|
|
202
|
-
if (
|
|
200
|
+
if (!publicBaseUrl || !publicBaseUrl.trim()) {
|
|
203
201
|
return true;
|
|
204
202
|
}
|
|
205
203
|
if (entry.ingressUrl === publicBaseUrl) {
|
|
@@ -25,6 +25,16 @@ import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
|
25
25
|
import { loadGuardianToken } from "./guardian-token.js";
|
|
26
26
|
import { classifyReadyzResponse } from "./http-client.js";
|
|
27
27
|
import { fetchReleases, resolveImageRefs } from "./platform-releases.js";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
|
|
30
|
+
const GatewayHealthSchema = z.object({
|
|
31
|
+
version: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const MigrationGateBodySchema = z.object({
|
|
35
|
+
status: z.string().optional(),
|
|
36
|
+
reason: z.string().optional(),
|
|
37
|
+
});
|
|
28
38
|
import {
|
|
29
39
|
getBuilderManagedEnvKeys,
|
|
30
40
|
type DockerStatefulSetSpec,
|
|
@@ -289,8 +299,8 @@ export async function fetchCurrentVersion(
|
|
|
289
299
|
signal: AbortSignal.timeout(5000),
|
|
290
300
|
});
|
|
291
301
|
if (resp.ok) {
|
|
292
|
-
const
|
|
293
|
-
return
|
|
302
|
+
const parsed = GatewayHealthSchema.safeParse(await resp.json());
|
|
303
|
+
return parsed.success ? parsed.data.version : undefined;
|
|
294
304
|
}
|
|
295
305
|
} catch {
|
|
296
306
|
// Best-effort
|
|
@@ -532,9 +542,11 @@ export async function rollbackMigrations(
|
|
|
532
542
|
// The daemon's running-migrations 503 also says "starting" but
|
|
533
543
|
// carries a `reason` field — don't retry against that one (the
|
|
534
544
|
// rollback stays blocked until migrations leave the running state).
|
|
535
|
-
const parsed = JSON.parse(text)
|
|
545
|
+
const parsed = MigrationGateBodySchema.safeParse(JSON.parse(text));
|
|
536
546
|
gatewayStarting =
|
|
537
|
-
parsed.
|
|
547
|
+
parsed.success &&
|
|
548
|
+
parsed.data.status === "starting" &&
|
|
549
|
+
parsed.data.reason === undefined;
|
|
538
550
|
} catch {
|
|
539
551
|
// Non-JSON error body — not the starting gate.
|
|
540
552
|
}
|