@ornncompute/cli 0.2.24 → 1.0.25
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 +298 -426
- package/package.json +12 -6
- package/src/admin-capacity-reservations.mjs +280 -0
- package/src/admin-command-handlers.mjs +57 -0
- package/src/admin-enrollment.mjs +680 -0
- package/src/admin-mutations.mjs +187 -0
- package/src/admin-node-diagnostics.mjs +412 -0
- package/src/admin-node-lifecycle.mjs +908 -0
- package/src/admin-shell.mjs +322 -0
- package/src/api-client.mjs +81 -28
- package/src/atomic-output.mjs +112 -0
- package/src/auth-store.mjs +182 -11
- package/src/capability-catalog.mjs +7 -0
- package/src/catalog-shell.mjs +1350 -0
- package/src/cli.mjs +123 -10266
- package/src/connect-mode-handlers.mjs +170 -0
- package/src/device-auth.mjs +178 -61
- package/src/evidence-commands.mjs +1340 -0
- package/src/external-bucket-commands.mjs +1374 -0
- package/src/interaction-runtime.mjs +310 -0
- package/src/kubernetes-commands.mjs +2229 -0
- package/src/launch-mode-handlers.mjs +119 -0
- package/src/machine-reference.mjs +277 -0
- package/src/mount-commands.mjs +1146 -0
- package/src/session-commands.mjs +565 -0
- package/src/slurm-commands.mjs +263 -0
- package/src/startup.mjs +34 -11
- package/src/storage-commands.mjs +1144 -0
- package/src/update.mjs +172 -24
- package/src/vm-commands.mjs +4273 -0
- package/src/vpn-secret-output.mjs +150 -0
- package/src/wait-runtime.mjs +231 -0
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
- package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
- package/vendor/capabilities/capabilities/admin.js +89 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
- package/vendor/capabilities/capabilities/cli.js +156 -0
- package/vendor/capabilities/capabilities/clusters.js +150 -1
- package/vendor/capabilities/capabilities/compute.js +522 -0
- package/vendor/capabilities/capabilities/evidence.js +145 -0
- package/vendor/capabilities/capabilities/external-buckets.js +191 -0
- package/vendor/capabilities/capabilities/fleet.js +21 -5
- package/vendor/capabilities/capabilities/identity.js +114 -3
- package/vendor/capabilities/capabilities/listings.js +14 -8
- package/vendor/capabilities/capabilities/mounts.js +176 -0
- package/vendor/capabilities/capabilities/networks.js +1257 -0
- package/vendor/capabilities/capabilities/observability.js +38 -8
- package/vendor/capabilities/capabilities/tokens.js +20 -8
- package/vendor/capabilities/capabilities/volumes.js +178 -0
- package/vendor/capabilities/capability.js +19 -13
- package/vendor/capabilities/cli-catalog.js +65 -0
- package/vendor/capabilities/cli-migrations.js +288 -0
- package/vendor/capabilities/cli.js +7 -0
- package/vendor/capabilities/software-catalog.js +36 -0
- package/vendor/capabilities/spec-catalog.js +2 -2
- package/src/catalog-dispatch.mjs +0 -605
- package/src/color.mjs +0 -71
- package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
- package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
- package/vendor/capabilities/capabilities/identity.d.ts +0 -3
- package/vendor/capabilities/capabilities/listings.d.ts +0 -51
- package/vendor/capabilities/capabilities/observability.d.ts +0 -5
- package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
- package/vendor/capabilities/capabilities/staff-access.js +0 -278
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
- package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
- package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
- package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
- package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
- package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
- package/vendor/capabilities/capabilities/staff-shared.js +0 -98
- package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
- package/vendor/capabilities/capabilities/users.d.ts +0 -2
- package/vendor/capabilities/capability.d.ts +0 -93
- package/vendor/capabilities/catalog.d.ts +0 -3
- package/vendor/capabilities/catalog.js +0 -26
- package/vendor/capabilities/index.d.ts +0 -16
- package/vendor/capabilities/index.js +0 -16
- package/vendor/capabilities/names.d.ts +0 -6
- package/vendor/capabilities/role.d.ts +0 -6
- package/vendor/capabilities/role.js +0 -17
- package/vendor/capabilities/spec-catalog.d.ts +0 -78
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continue the shared `connect` journey after ticket 10 has proved private
|
|
3
|
+
* network and VPN readiness. Access-mode modules add ordered handlers with:
|
|
4
|
+
*
|
|
5
|
+
* { mode, matches(state), connect(state) }
|
|
6
|
+
*
|
|
7
|
+
* `connect` returns interaction-runtime result options (`data`, `outcome`, and
|
|
8
|
+
* optional `presentation`), not a rendered envelope. A handler must match from
|
|
9
|
+
* authoritative server facts; target spelling is never a mode selector.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function resolverErrorPriority(error) {
|
|
13
|
+
if (Number(error?.exitCode) === 2) return 2;
|
|
14
|
+
const status = Number(error?.status);
|
|
15
|
+
const code = typeof error?.code === "string" ? error.code.toLowerCase() : "";
|
|
16
|
+
if (status === 404 || /(?:not_found|not_supported|unsupported|mode_unavailable)$/.test(code)) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
async function validateConnectModeInput(state, handler, resolved) {
|
|
22
|
+
const input = state.input ?? {};
|
|
23
|
+
const mode = resolved?.mode ?? handler.mode;
|
|
24
|
+
if (mode === "vm" || mode === "bare-metal" || mode === "slurm") {
|
|
25
|
+
if (input.output || input.overwrite) {
|
|
26
|
+
state.interaction.usage(
|
|
27
|
+
`${mode === "slurm" ? "Slurm" : mode === "vm" ? "VM" : "Bare Metal"} connect uses native SSH and does not write a kubeconfig or other output file.`,
|
|
28
|
+
{
|
|
29
|
+
code: mode === "slurm" ? "slurm_output_unsupported" : "direct_machine_output_unsupported",
|
|
30
|
+
field: input.output ? "output" : "overwrite",
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (mode !== "kubernetes") return;
|
|
37
|
+
if (input.identityFile || input.user) {
|
|
38
|
+
state.interaction.usage(
|
|
39
|
+
"Kubernetes connect writes a kubeconfig and does not accept SSH identity or user options.",
|
|
40
|
+
{
|
|
41
|
+
code: "kubernetes_ssh_options_unsupported",
|
|
42
|
+
field: input.identityFile ? "identityFile" : "user",
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (input.print === true) {
|
|
47
|
+
state.interaction.usage(
|
|
48
|
+
"Kubernetes connect writes only to an explicit --output path; ordinary output never contains credential bytes.",
|
|
49
|
+
{
|
|
50
|
+
code: "kubeconfig_output_required",
|
|
51
|
+
field: "output",
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
!input.output &&
|
|
57
|
+
input.guided === true &&
|
|
58
|
+
state.interaction?.eligibleHuman === true &&
|
|
59
|
+
state.interaction?.machine !== true
|
|
60
|
+
) {
|
|
61
|
+
input.output = await state.interaction.ask({ label: "Kubeconfig output path" });
|
|
62
|
+
}
|
|
63
|
+
if (!input.output) {
|
|
64
|
+
state.interaction.usage("Kubernetes connect requires --output <path>.", {
|
|
65
|
+
code: "kubeconfig_output_required",
|
|
66
|
+
field: "output",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function resolveConnectTargetWithHandlers(state, handlers = []) {
|
|
72
|
+
for (const handler of handlers) {
|
|
73
|
+
if (
|
|
74
|
+
!handler ||
|
|
75
|
+
typeof handler.matches !== "function" ||
|
|
76
|
+
typeof handler.connect !== "function"
|
|
77
|
+
) {
|
|
78
|
+
throw new TypeError("Connect mode handlers require matches(state) and connect(state).");
|
|
79
|
+
}
|
|
80
|
+
if (handler.resolve !== undefined && typeof handler.resolve !== "function") {
|
|
81
|
+
throw new TypeError("A connect mode handler resolve member must be a function.");
|
|
82
|
+
}
|
|
83
|
+
if (handler.candidates !== undefined && typeof handler.candidates !== "function") {
|
|
84
|
+
throw new TypeError("A connect mode handler candidates member must be a function.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
!state.reference &&
|
|
90
|
+
state.interaction?.eligibleHuman === true &&
|
|
91
|
+
state.interaction?.machine !== true
|
|
92
|
+
) {
|
|
93
|
+
const candidates = [];
|
|
94
|
+
for (const handler of handlers) {
|
|
95
|
+
if (typeof handler.candidates !== "function") continue;
|
|
96
|
+
const available = await handler.candidates(state);
|
|
97
|
+
if (!Array.isArray(available)) {
|
|
98
|
+
throw new TypeError("A connect mode handler candidates member must return an array.");
|
|
99
|
+
}
|
|
100
|
+
for (const candidate of available) {
|
|
101
|
+
if (
|
|
102
|
+
!candidate ||
|
|
103
|
+
typeof candidate.label !== "string" ||
|
|
104
|
+
typeof candidate.reference !== "string"
|
|
105
|
+
) {
|
|
106
|
+
throw new TypeError("Connect candidates require label and reference strings.");
|
|
107
|
+
}
|
|
108
|
+
candidates.push({ ...candidate, handler });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (candidates.length > 0) {
|
|
112
|
+
let selected = candidates[0];
|
|
113
|
+
if (candidates.length > 1) {
|
|
114
|
+
const value = await state.interaction.ask({
|
|
115
|
+
label: "Compute target",
|
|
116
|
+
choices: candidates.map((candidate, index) => ({
|
|
117
|
+
label: candidate.label,
|
|
118
|
+
value: String(index),
|
|
119
|
+
})),
|
|
120
|
+
});
|
|
121
|
+
selected = candidates[Number(value)];
|
|
122
|
+
if (!selected) {
|
|
123
|
+
state.interaction.usage("Choose one exact compute target.", {
|
|
124
|
+
code: "connect_target_required",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const resolved = await selected.handler.resolve({
|
|
129
|
+
...state,
|
|
130
|
+
reference: selected.reference,
|
|
131
|
+
});
|
|
132
|
+
if (resolved) await validateConnectModeInput(state, selected.handler, resolved);
|
|
133
|
+
return resolved;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let strongestError = null;
|
|
138
|
+
let strongestPriority = 0;
|
|
139
|
+
for (const handler of handlers) {
|
|
140
|
+
if (handler.resolve === undefined) continue;
|
|
141
|
+
try {
|
|
142
|
+
const resolved = await handler.resolve(state);
|
|
143
|
+
if (resolved) {
|
|
144
|
+
await validateConnectModeInput(state, handler, resolved);
|
|
145
|
+
return resolved;
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const priority = resolverErrorPriority(error);
|
|
149
|
+
if (priority > strongestPriority) {
|
|
150
|
+
strongestError = error;
|
|
151
|
+
strongestPriority = priority;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (strongestError) throw strongestError;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
export async function continueConnectWithHandlers(state, handlers = []) {
|
|
159
|
+
for (const handler of handlers) {
|
|
160
|
+
if (
|
|
161
|
+
!handler ||
|
|
162
|
+
typeof handler.matches !== "function" ||
|
|
163
|
+
typeof handler.connect !== "function"
|
|
164
|
+
) {
|
|
165
|
+
throw new TypeError("Connect mode handlers require matches(state) and connect(state).");
|
|
166
|
+
}
|
|
167
|
+
if (await handler.matches(state)) return handler.connect(state);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
package/src/device-auth.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
import { configuredAuthBaseUrl } from "./auth-store.mjs";
|
|
4
|
+
import { readUntilDeadline, waitForDelay } from "./wait-runtime.mjs";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_AUTH_BASE_URL = "http://localhost:3000";
|
|
6
7
|
const DEFAULT_POLL_INTERVAL_SECONDS = 2;
|
|
@@ -27,18 +28,61 @@ export function resolveAuthBaseUrl({ explicit, env = process.env } = {}) {
|
|
|
27
28
|
return value.replace(/\/+$/, "");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
function browserUrl(value, expectedBaseUrl) {
|
|
32
|
+
if (
|
|
33
|
+
typeof value !== "string" ||
|
|
34
|
+
value !== value.trim() ||
|
|
35
|
+
/[\u0000-\u001f\u007f]|%(?:0[0-9a-f]|1[0-9a-f]|7f)/i.test(value)
|
|
36
|
+
) {
|
|
37
|
+
throw new DeviceAuthError("Auth server returned an unsafe browser URL.");
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(value);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new DeviceAuthError("Auth server returned an invalid browser URL.");
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
!["http:", "https:"].includes(parsed.protocol) ||
|
|
47
|
+
parsed.username ||
|
|
48
|
+
parsed.password ||
|
|
49
|
+
(expectedBaseUrl && parsed.origin !== new URL(expectedBaseUrl).origin)
|
|
50
|
+
) {
|
|
51
|
+
throw new DeviceAuthError("Auth server returned an unexpected browser URL.");
|
|
52
|
+
}
|
|
53
|
+
return parsed.href;
|
|
54
|
+
}
|
|
34
55
|
|
|
56
|
+
export async function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
|
|
35
57
|
try {
|
|
58
|
+
const target = browserUrl(url);
|
|
59
|
+
const command =
|
|
60
|
+
platform === "darwin" ? "open" : platform === "win32" ? "powershell.exe" : "xdg-open";
|
|
61
|
+
// The script is constant. The URL is environment data, never cmd.exe or
|
|
62
|
+
// PowerShell source, even when its query contains shell metacharacters.
|
|
63
|
+
const script = "Start-Process -FilePath $env:ORNN_BROWSER_URL";
|
|
64
|
+
const args =
|
|
65
|
+
platform === "win32"
|
|
66
|
+
? [
|
|
67
|
+
"-NoProfile",
|
|
68
|
+
"-NonInteractive",
|
|
69
|
+
"-EncodedCommand",
|
|
70
|
+
Buffer.from(script, "utf16le").toString("base64"),
|
|
71
|
+
]
|
|
72
|
+
: [target];
|
|
36
73
|
const child = spawnImpl(command, args, {
|
|
37
74
|
detached: true,
|
|
38
75
|
stdio: "ignore",
|
|
76
|
+
shell: false,
|
|
77
|
+
...(platform === "win32" ? { env: { ...process.env, ORNN_BROWSER_URL: target } } : {}),
|
|
78
|
+
});
|
|
79
|
+
return await new Promise((resolve) => {
|
|
80
|
+
child.once("error", () => resolve(false));
|
|
81
|
+
child.once("spawn", () => {
|
|
82
|
+
child.unref?.();
|
|
83
|
+
resolve(true);
|
|
84
|
+
});
|
|
39
85
|
});
|
|
40
|
-
child.unref?.();
|
|
41
|
-
return true;
|
|
42
86
|
} catch {
|
|
43
87
|
return false;
|
|
44
88
|
}
|
|
@@ -50,9 +94,14 @@ export async function startDeviceFlow({ apiBaseUrl, authBaseUrl, fetchImpl = fet
|
|
|
50
94
|
|
|
51
95
|
const deviceCode = pickString(data, "deviceCode", "device_code");
|
|
52
96
|
const userCode = pickString(data, "userCode", "user_code");
|
|
53
|
-
const verificationUri =
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
const verificationUri = [
|
|
98
|
+
"verificationUriComplete",
|
|
99
|
+
"verification_uri_complete",
|
|
100
|
+
"verificationUri",
|
|
101
|
+
"verification_uri",
|
|
102
|
+
]
|
|
103
|
+
.map((key) => data?.[key])
|
|
104
|
+
.find((value) => typeof value === "string" && value.length > 0);
|
|
56
105
|
|
|
57
106
|
if (!deviceCode || !userCode || !verificationUri) {
|
|
58
107
|
throw new DeviceAuthError("Auth server returned an invalid device login response.", {
|
|
@@ -65,15 +114,24 @@ export async function startDeviceFlow({ apiBaseUrl, authBaseUrl, fetchImpl = fet
|
|
|
65
114
|
expiresIn: pickNumber(data, "expiresIn", "expires_in") ?? DEFAULT_TIMEOUT_SECONDS,
|
|
66
115
|
interval: pickNumber(data, "interval") ?? DEFAULT_POLL_INTERVAL_SECONDS,
|
|
67
116
|
userCode,
|
|
68
|
-
verificationUri,
|
|
117
|
+
verificationUri: browserUrl(verificationUri, authBaseUrl || apiBaseUrl),
|
|
69
118
|
};
|
|
70
119
|
}
|
|
71
120
|
|
|
72
|
-
export async function pollDeviceFlow({
|
|
121
|
+
export async function pollDeviceFlow({
|
|
122
|
+
apiBaseUrl,
|
|
123
|
+
authBaseUrl,
|
|
124
|
+
deviceCode,
|
|
125
|
+
fetchImpl = fetch,
|
|
126
|
+
signal,
|
|
127
|
+
} = {}) {
|
|
73
128
|
const requestBaseUrl = apiBaseUrl || authBaseUrl;
|
|
74
|
-
return postJson(
|
|
75
|
-
|
|
76
|
-
|
|
129
|
+
return postJson(
|
|
130
|
+
fetchImpl,
|
|
131
|
+
`${requestBaseUrl}/v1/cli/device/poll`,
|
|
132
|
+
{ device_code: deviceCode },
|
|
133
|
+
{ signal }
|
|
134
|
+
);
|
|
77
135
|
}
|
|
78
136
|
|
|
79
137
|
export async function waitForDeviceApproval({
|
|
@@ -83,60 +141,124 @@ export async function waitForDeviceApproval({
|
|
|
83
141
|
expiresIn,
|
|
84
142
|
fetchImpl = fetch,
|
|
85
143
|
interval,
|
|
144
|
+
now = () => Date.now(),
|
|
86
145
|
onPending,
|
|
146
|
+
signal: suppliedSignal,
|
|
147
|
+
sleepImpl,
|
|
87
148
|
timeoutSeconds,
|
|
88
149
|
} = {}) {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
150
|
+
const suppliedInterval = Number(interval);
|
|
151
|
+
const suppliedTimeout = Number(timeoutSeconds ?? expiresIn);
|
|
152
|
+
const pollIntervalMs = Math.max(
|
|
153
|
+
1,
|
|
154
|
+
(Number.isFinite(suppliedInterval) && suppliedInterval > 0
|
|
155
|
+
? suppliedInterval
|
|
156
|
+
: DEFAULT_POLL_INTERVAL_SECONDS) * 1_000
|
|
157
|
+
);
|
|
158
|
+
const timeoutMs = Math.max(
|
|
159
|
+
1,
|
|
160
|
+
(Number.isFinite(suppliedTimeout) && suppliedTimeout > 0
|
|
161
|
+
? suppliedTimeout
|
|
162
|
+
: DEFAULT_TIMEOUT_SECONDS) * 1_000
|
|
163
|
+
);
|
|
164
|
+
const startedAt = now();
|
|
165
|
+
let scheduledElapsedMs = 0;
|
|
166
|
+
const localController = suppliedSignal ? null : new AbortController();
|
|
167
|
+
const signal = suppliedSignal ?? localController.signal;
|
|
168
|
+
const onSigint = localController ? () => localController.abort() : null;
|
|
169
|
+
if (onSigint) process.once("SIGINT", onSigint);
|
|
170
|
+
|
|
171
|
+
const remainingTime = () =>
|
|
172
|
+
timeoutMs - Math.max(scheduledElapsedMs, Math.max(0, now() - startedAt));
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
for (;;) {
|
|
176
|
+
const remainingBeforeSleep = remainingTime();
|
|
177
|
+
if (remainingBeforeSleep <= 0) {
|
|
178
|
+
throw new DeviceAuthError("Timed out waiting for browser login approval.");
|
|
179
|
+
}
|
|
180
|
+
const delay = Math.min(pollIntervalMs, remainingBeforeSleep);
|
|
181
|
+
const waited = await waitForDelay(delay, sleepImpl, signal);
|
|
182
|
+
if (waited === "interrupted" || signal.aborted) {
|
|
183
|
+
throw new DeviceAuthError("Browser login wait was interrupted.", {
|
|
184
|
+
detail: { code: "interrupted" },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
scheduledElapsedMs += delay;
|
|
188
|
+
const remaining = remainingTime();
|
|
189
|
+
if (remaining <= 0) {
|
|
190
|
+
throw new DeviceAuthError("Timed out waiting for browser login approval.");
|
|
191
|
+
}
|
|
192
|
+
const polled = await readUntilDeadline(
|
|
193
|
+
({ signal: requestSignal }) =>
|
|
194
|
+
pollDeviceFlow({
|
|
195
|
+
apiBaseUrl,
|
|
196
|
+
authBaseUrl,
|
|
197
|
+
deviceCode,
|
|
198
|
+
fetchImpl,
|
|
199
|
+
signal: requestSignal,
|
|
200
|
+
}),
|
|
201
|
+
{ signal, timeoutMs: remaining }
|
|
202
|
+
);
|
|
203
|
+
if (polled.kind === "interrupted") {
|
|
204
|
+
throw new DeviceAuthError("Browser login wait was interrupted.", {
|
|
205
|
+
detail: { code: "interrupted" },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (polled.kind === "timeout") {
|
|
209
|
+
throw new DeviceAuthError("Timed out waiting for browser login approval.");
|
|
210
|
+
}
|
|
211
|
+
const data = polled.value;
|
|
212
|
+
const status = pickString(data, "status") || "pending";
|
|
213
|
+
|
|
214
|
+
if (status === "pending") {
|
|
215
|
+
onPending?.();
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (status === "approved") {
|
|
220
|
+
const token = pickString(
|
|
221
|
+
data,
|
|
222
|
+
"accessToken",
|
|
223
|
+
"access_token",
|
|
224
|
+
"sessionToken",
|
|
225
|
+
"session_token"
|
|
226
|
+
);
|
|
227
|
+
const user = data.user && typeof data.user === "object" ? data.user : null;
|
|
103
228
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
229
|
+
if (!token || !user?.email) {
|
|
230
|
+
throw new DeviceAuthError("Auth server approved login without a usable session.");
|
|
231
|
+
}
|
|
107
232
|
|
|
108
|
-
|
|
109
|
-
|
|
233
|
+
return {
|
|
234
|
+
accessToken: token,
|
|
235
|
+
apiBaseUrl: apiBaseUrl || authBaseUrl,
|
|
236
|
+
authBaseUrl,
|
|
237
|
+
tokenType: pickString(data, "tokenType", "token_type") || "bearer",
|
|
238
|
+
user: {
|
|
239
|
+
email: String(user.email),
|
|
240
|
+
id: user.id ? String(user.id) : null,
|
|
241
|
+
name: user.name ? String(user.name) : null,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
110
244
|
}
|
|
111
245
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
authBaseUrl,
|
|
116
|
-
tokenType: pickString(data, "tokenType", "token_type") || "bearer",
|
|
117
|
-
user: {
|
|
118
|
-
email: String(user.email),
|
|
119
|
-
id: user.id ? String(user.id) : null,
|
|
120
|
-
name: user.name ? String(user.name) : null,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
246
|
+
if (status === "denied") {
|
|
247
|
+
throw new DeviceAuthError("Device login was denied in the browser.");
|
|
248
|
+
}
|
|
124
249
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
250
|
+
if (status === "expired") {
|
|
251
|
+
throw new DeviceAuthError("Device login expired. Run `ornn login` again.");
|
|
252
|
+
}
|
|
128
253
|
|
|
129
|
-
|
|
130
|
-
throw new DeviceAuthError("Device login expired. Run `ornn login` again.");
|
|
254
|
+
throw new DeviceAuthError(`Auth server returned unexpected status: ${status}`);
|
|
131
255
|
}
|
|
132
|
-
|
|
133
|
-
|
|
256
|
+
} finally {
|
|
257
|
+
if (onSigint) process.removeListener("SIGINT", onSigint);
|
|
134
258
|
}
|
|
135
|
-
|
|
136
|
-
throw new DeviceAuthError("Timed out waiting for browser login approval.");
|
|
137
259
|
}
|
|
138
260
|
|
|
139
|
-
async function postJson(fetchImpl, url, body) {
|
|
261
|
+
async function postJson(fetchImpl, url, body, { signal } = {}) {
|
|
140
262
|
const response = await fetchImpl(url, {
|
|
141
263
|
body: JSON.stringify(body),
|
|
142
264
|
headers: {
|
|
@@ -144,6 +266,7 @@ async function postJson(fetchImpl, url, body) {
|
|
|
144
266
|
"Content-Type": "application/json",
|
|
145
267
|
},
|
|
146
268
|
method: "POST",
|
|
269
|
+
...(signal ? { signal } : {}),
|
|
147
270
|
});
|
|
148
271
|
const text = await response.text();
|
|
149
272
|
const data = text ? parseJson(text, url) : {};
|
|
@@ -186,9 +309,3 @@ function pickNumber(value, ...keys) {
|
|
|
186
309
|
}
|
|
187
310
|
return null;
|
|
188
311
|
}
|
|
189
|
-
|
|
190
|
-
function sleep(ms) {
|
|
191
|
-
return new Promise((resolve) => {
|
|
192
|
-
setTimeout(resolve, ms);
|
|
193
|
-
});
|
|
194
|
-
}
|