@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64
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/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
- 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
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +6 -4
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +1 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/ensure-binary.test.ts +182 -0
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +15 -20
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +149 -85
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
- package/src/__tests__/ps-table.test.ts +79 -0
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -211
- package/src/__tests__/tunnel.test.ts +490 -270
- package/src/__tests__/uninstall.test.ts +150 -9
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -78
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +12 -12
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +27 -24
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +232 -86
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +140 -22
- package/src/commands/retire.ts +48 -150
- package/src/commands/rollback.ts +20 -26
- package/src/commands/setup.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +110 -129
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +156 -175
- package/src/commands/uninstall.ts +137 -54
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +30 -54
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +166 -50
- package/src/index.ts +18 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +2 -2
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
- package/src/lib/cloud-login.ts +23 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/color.ts +4 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +300 -111
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +29 -3
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +10 -12
- package/src/lib/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +37 -5
- package/src/lib/hatch-local.ts +32 -29
- package/src/lib/hatch-next-steps.ts +5 -9
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +2 -2
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +46 -34
- package/src/lib/nginx-ingress.ts +40 -15
- package/src/lib/pair.ts +1 -1
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/sync-cloud-assistants.ts +19 -19
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
- package/src/lib/upgrade-lifecycle.ts +8 -21
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- 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
|
|
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
|
|
31
|
-
|
|
32
|
-
"
|
|
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
|
-
}
|
|
50
|
-
ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
|
|
51
|
-
>);
|
|
51
|
+
});
|
|
52
52
|
const loadGuardianTokenSpy = spyOn(
|
|
53
53
|
guardianToken,
|
|
54
54
|
"loadGuardianToken",
|
|
55
|
-
).mockReturnValue(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
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
|
-
})
|
|
95
|
+
});
|
|
92
96
|
fetchCalls = [];
|
|
93
97
|
uploadUrlStatus = 201;
|
|
94
|
-
globalThis.fetch =
|
|
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
|
-
)
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
243
|
-
}
|
|
242
|
+
cloudAssistantId: undefined,
|
|
243
|
+
});
|
|
244
244
|
const errors: string[] = [];
|
|
245
245
|
const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
|
|
246
246
|
errors.push(String(msg));
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
import { ensureBinaryInstalled, readLineText } from "../lib/ensure-binary.js";
|
|
5
|
+
|
|
6
|
+
/** A real Writable capturing everything written, as a test output stream. */
|
|
7
|
+
function makeOutput(): { writable: Writable; text(): string } {
|
|
8
|
+
const captured: string[] = [];
|
|
9
|
+
const writable = new Writable({
|
|
10
|
+
write(chunk, _encoding, callback) {
|
|
11
|
+
captured.push(chunk.toString());
|
|
12
|
+
callback();
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
return { writable, text: () => captured.join("") };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("ensureBinaryInstalled", () => {
|
|
19
|
+
test("returns the existing probe result without prompting", async () => {
|
|
20
|
+
const output = makeOutput();
|
|
21
|
+
const version = await ensureBinaryInstalled({
|
|
22
|
+
binary: "nginx",
|
|
23
|
+
purpose: "the tunnel edge",
|
|
24
|
+
packages: { brew: ["brew", "install", "nginx"] },
|
|
25
|
+
manualHint: "install nginx",
|
|
26
|
+
probe: () => "nginx version 1.2",
|
|
27
|
+
confirmInstall: () => {
|
|
28
|
+
throw new Error("must not prompt for a present binary");
|
|
29
|
+
},
|
|
30
|
+
streams: { output: output.writable },
|
|
31
|
+
});
|
|
32
|
+
expect(version).toBe("nginx version 1.2");
|
|
33
|
+
expect(output.text()).toBe("");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("offers to install and runs the first available package manager", async () => {
|
|
37
|
+
const output = makeOutput();
|
|
38
|
+
const installed: string[][] = [];
|
|
39
|
+
let probeCalls = 0;
|
|
40
|
+
const version = await ensureBinaryInstalled({
|
|
41
|
+
binary: "nginx",
|
|
42
|
+
purpose: "the tunnel edge",
|
|
43
|
+
packages: {
|
|
44
|
+
brew: ["brew", "install", "nginx"],
|
|
45
|
+
"apt-get": ["sudo", "apt-get", "install", "-y", "nginx"],
|
|
46
|
+
},
|
|
47
|
+
manualHint: "install nginx yourself",
|
|
48
|
+
probe: () => (++probeCalls > 1 ? "nginx version 1.2" : null),
|
|
49
|
+
pmAvailable: (pm) => pm === "apt-get",
|
|
50
|
+
runInstall: (argv) => {
|
|
51
|
+
installed.push(argv);
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
confirmInstall: async () => true,
|
|
55
|
+
streams: { output: output.writable },
|
|
56
|
+
});
|
|
57
|
+
expect(version).toBe("nginx version 1.2");
|
|
58
|
+
expect(installed).toEqual([["sudo", "apt-get", "install", "-y", "nginx"]]);
|
|
59
|
+
expect(output.text()).toContain("isn't installed");
|
|
60
|
+
expect(output.text()).not.toContain("apt-get install");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("the default confirm takes Enter as yes", async () => {
|
|
64
|
+
const output = makeOutput();
|
|
65
|
+
const input = Object.assign(new Readable({ read() {} }), {
|
|
66
|
+
isTTY: true,
|
|
67
|
+
});
|
|
68
|
+
const installed: string[][] = [];
|
|
69
|
+
let probeCalls = 0;
|
|
70
|
+
const pending = ensureBinaryInstalled({
|
|
71
|
+
binary: "nginx",
|
|
72
|
+
purpose: "the tunnel edge",
|
|
73
|
+
packages: { brew: ["brew", "install", "nginx"] },
|
|
74
|
+
manualHint: "install nginx yourself",
|
|
75
|
+
probe: () => (++probeCalls > 1 ? "nginx version 1.2" : null),
|
|
76
|
+
pmAvailable: () => true,
|
|
77
|
+
runInstall: (argv) => {
|
|
78
|
+
installed.push(argv);
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
streams: { input, output: output.writable },
|
|
82
|
+
});
|
|
83
|
+
input.emit("data", "\n");
|
|
84
|
+
expect(await pending).toBe("nginx version 1.2");
|
|
85
|
+
expect(installed).toEqual([["brew", "install", "nginx"]]);
|
|
86
|
+
expect(output.text()).toContain("Install nginx now?");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a declined offer prints the manual hint and installs nothing", async () => {
|
|
90
|
+
const output = makeOutput();
|
|
91
|
+
let ran = false;
|
|
92
|
+
const version = await ensureBinaryInstalled({
|
|
93
|
+
binary: "ngrok",
|
|
94
|
+
purpose: "the ngrok tunnel",
|
|
95
|
+
packages: { brew: ["brew", "install", "ngrok/ngrok/ngrok"] },
|
|
96
|
+
manualHint: "get ngrok from ngrok.com",
|
|
97
|
+
probe: () => null,
|
|
98
|
+
runInstall: () => {
|
|
99
|
+
ran = true;
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
confirmInstall: async () => false,
|
|
103
|
+
streams: { output: output.writable },
|
|
104
|
+
});
|
|
105
|
+
expect(version).toBe(null);
|
|
106
|
+
expect(ran).toBe(false);
|
|
107
|
+
expect(output.text()).toContain("get ngrok from ngrok.com");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a failed install falls back to the manual hint", async () => {
|
|
111
|
+
const output = makeOutput();
|
|
112
|
+
const version = await ensureBinaryInstalled({
|
|
113
|
+
binary: "nginx",
|
|
114
|
+
purpose: "the tunnel edge",
|
|
115
|
+
packages: { brew: ["brew", "install", "nginx"] },
|
|
116
|
+
manualHint: "install nginx yourself",
|
|
117
|
+
probe: () => null,
|
|
118
|
+
runInstall: () => false,
|
|
119
|
+
confirmInstall: async () => true,
|
|
120
|
+
streams: { output: output.writable },
|
|
121
|
+
});
|
|
122
|
+
expect(version).toBe(null);
|
|
123
|
+
expect(output.text()).toContain("install nginx yourself");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("no matching package manager prints the hint", async () => {
|
|
127
|
+
const output = makeOutput();
|
|
128
|
+
let ran = false;
|
|
129
|
+
const version = await ensureBinaryInstalled({
|
|
130
|
+
binary: "cloudflared",
|
|
131
|
+
purpose: "the cloudflare tunnel",
|
|
132
|
+
packages: { brew: ["brew", "install", "cloudflared"] },
|
|
133
|
+
manualHint: "cloudflared install docs",
|
|
134
|
+
probe: () => null,
|
|
135
|
+
pmAvailable: () => false,
|
|
136
|
+
runInstall: () => {
|
|
137
|
+
ran = true;
|
|
138
|
+
return true;
|
|
139
|
+
},
|
|
140
|
+
confirmInstall: async () => true,
|
|
141
|
+
streams: { output: output.writable },
|
|
142
|
+
});
|
|
143
|
+
expect(version).toBe(null);
|
|
144
|
+
expect(ran).toBe(false);
|
|
145
|
+
expect(output.text()).toContain("cloudflared install docs");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a non-interactive terminal declines instead of hanging", async () => {
|
|
149
|
+
const version = await ensureBinaryInstalled({
|
|
150
|
+
binary: "nginx",
|
|
151
|
+
purpose: "the tunnel edge",
|
|
152
|
+
packages: { brew: ["brew", "install", "nginx"] },
|
|
153
|
+
manualHint: "install nginx yourself",
|
|
154
|
+
probe: () => null,
|
|
155
|
+
confirmInstall: async () => {
|
|
156
|
+
throw new Error("Install confirm requires an interactive terminal.");
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
expect(version).toBe(null);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("readLineText", () => {
|
|
164
|
+
test("resolves the first line and ignores the rest", async () => {
|
|
165
|
+
const output = makeOutput();
|
|
166
|
+
const input = new Readable({ read() {} });
|
|
167
|
+
const pending = readLineText("token: ", {
|
|
168
|
+
input,
|
|
169
|
+
output: output.writable,
|
|
170
|
+
});
|
|
171
|
+
input.emit("data", "abc123\nsecond line\n");
|
|
172
|
+
expect(await pending).toBe("abc123");
|
|
173
|
+
expect(output.text()).toBe("token: ");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("resolves an empty line on an immediate newline", async () => {
|
|
177
|
+
const input = new Readable({ read() {} });
|
|
178
|
+
const pending = readLineText("[Y/n] ", { input });
|
|
179
|
+
input.emit("data", "\n");
|
|
180
|
+
expect(await pending).toBe("");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -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 = (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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 = (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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 = (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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 = (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 = (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 = (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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 = (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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 = (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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 = (
|
|
455
|
-
|
|
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 = (
|
|
469
|
-
|
|
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 = (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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 = (
|
|
507
|
-
|
|
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",
|