@ornncompute/cli 0.1.4 → 0.1.6
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 +163 -25
- package/package.json +1 -1
- package/src/api-client.mjs +148 -12
- package/src/auth-store.mjs +21 -0
- package/src/cli.mjs +2946 -223
- package/src/device-auth.mjs +4 -0
- package/src/update.mjs +1 -5
package/README.md
CHANGED
|
@@ -17,9 +17,10 @@ curl -fsSL https://compute.ornn.com/cli/install | sh
|
|
|
17
17
|
|
|
18
18
|
The installer requires Node.js 20 or newer and npm. It installs `@ornncompute/cli`
|
|
19
19
|
globally, exposes the `ornn` binary, adds the npm global bin directory to the
|
|
20
|
-
user's shell profile when needed, and
|
|
21
|
-
`
|
|
22
|
-
|
|
20
|
+
user's shell profile when needed, and records the Ornn host that served it as
|
|
21
|
+
`authBaseUrl` in `~/.config/ornn/config.json`, so `ornn login` reaches the right
|
|
22
|
+
host from the shell that ran the installer. The
|
|
23
|
+
installer prints a short Ornn Compute welcome banner and next steps; set
|
|
23
24
|
`ORNN_INSTALL_BANNER=0` to skip it or `ORNN_INSTALL_ANIMATION=0` to keep it
|
|
24
25
|
static.
|
|
25
26
|
|
|
@@ -35,28 +36,31 @@ ornn listings list [--gpu-type <type>] [--facility <name>] [--operator <name>] [
|
|
|
35
36
|
ornn listings show <listing-id> [--open] [--json]
|
|
36
37
|
ornn buy <listing-id> [--no-open] [--json]
|
|
37
38
|
ornn exchange create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
|
|
38
|
-
ornn exchange list [--json]
|
|
39
|
+
ornn exchange list [--limit <1-500>] [--cursor <last-id>] [--json]
|
|
39
40
|
ornn exchange show <bid-id> [--open] [--json]
|
|
40
41
|
ornn exchange update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
|
|
41
42
|
ornn exchange withdraw <bid-id>
|
|
42
|
-
ornn gpus list [--status <status>] [--json]
|
|
43
|
+
ornn gpus list [--status <status>] [--limit <1-500>] [--cursor <last-id>] [--json]
|
|
43
44
|
ornn gpus show <reservation-id> [--open] [--json]
|
|
44
45
|
ornn gpus checkout <reservation-id> [--no-open] [--json]
|
|
45
46
|
ornn nodes list [--json]
|
|
46
47
|
ornn nodes show <node-id> [--json]
|
|
47
48
|
ornn nodes launch <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--json]
|
|
49
|
+
ornn nodes switch <reservation-id> --network public|private --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--wait] [--json]
|
|
48
50
|
ornn nodes wait <node-or-reservation-id> [--timeout <seconds>] [--json]
|
|
49
|
-
ornn nodes
|
|
50
|
-
ornn nodes
|
|
51
|
-
ornn nodes teardown <node-id> [--json]
|
|
52
|
-
ornn nodes revoke <node-id> [--json]
|
|
51
|
+
ornn nodes reboot <node-id> [--json]
|
|
52
|
+
ornn nodes hard-reset <node-id> [--json]
|
|
53
53
|
ornn nodes ssh-command <node-or-reservation-id> [--json]
|
|
54
54
|
ornn nodes keys attach <node-id> --key <path|id|label> [--json]
|
|
55
55
|
ornn nodes keys list <node-id> [--json]
|
|
56
56
|
ornn node health <node-id> [--json]
|
|
57
57
|
ornn node diagnose <node-id> [--json]
|
|
58
|
-
ornn node
|
|
59
|
-
ornn
|
|
58
|
+
ornn node deenroll <node-id> [--reason <text>] [--keep-record] [--json]
|
|
59
|
+
ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--ssh-user ubuntu|admin|ornn] [--json]
|
|
60
|
+
ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--json]
|
|
61
|
+
ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--source-user <user>]... [--preserve-user <user>]... [--json]
|
|
62
|
+
ornn fleet enroll <fleet-id> --identity-file <path> [--confirm-takeover <ip[,ip...]>] [--json]
|
|
63
|
+
ornn fleet deploy <fleet-id> --tenant <contact-email> --user <email-or-id> [--commerce-reservation <id>] [--network public|private] [--json]
|
|
60
64
|
ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
|
|
61
65
|
ornn metrics nodes [--json]
|
|
62
66
|
ornn metrics node <node-id> [--json]
|
|
@@ -75,6 +79,16 @@ ornn clusters ssh <reservation-id> [--print] [--identity-file <path>] [--user <n
|
|
|
75
79
|
ornn clusters add-node <reservation-id> --node <node-id> [--json]
|
|
76
80
|
ornn clusters remove-node <reservation-id> --node <node-id> [--json]
|
|
77
81
|
ornn clusters teardown <reservation-id> [--type kubernetes|slurm] [--json]
|
|
82
|
+
ornn slurm launch <reservation-id> [--network public|private] [--node <node-id>] [--node-count <n>] [--wait] [--wait-timeout <seconds>] [--json]
|
|
83
|
+
ornn slurm teardown <reservation-id> [--json]
|
|
84
|
+
ornn slurm status <reservation-id> [--json]
|
|
85
|
+
ornn slurm credentials <reservation-id> [--json]
|
|
86
|
+
ornn slurm ssh <reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
|
|
87
|
+
ornn kubernetes launch <reservation-id> [--network public|private] [--node <node-id>] [--node-count <n>] [--wait] [--wait-timeout <seconds>] [--json]
|
|
88
|
+
ornn kubernetes teardown <reservation-id> [--json]
|
|
89
|
+
ornn kubernetes status <reservation-id> [--json]
|
|
90
|
+
ornn kubernetes credentials <reservation-id> [--json]
|
|
91
|
+
ornn kubernetes kubeconfig <reservation-id> [--output <path>] [--json]
|
|
78
92
|
ornn networks list [--json]
|
|
79
93
|
ornn networks show <network-id> [--json]
|
|
80
94
|
ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]
|
|
@@ -91,6 +105,8 @@ ornn storage volumes clear <drive-id> [--json]
|
|
|
91
105
|
ornn storage volumes delete <drive-id> [--json]
|
|
92
106
|
ornn storage deploy <drive-id> --reservation <reservation-id> [--mount-path <path>] [--read-only|--read-write] [--json]
|
|
93
107
|
ornn storage deploy status --reservation <reservation-id> [--json]
|
|
108
|
+
ornn storage unmount --reservation <reservation-id> [--json]
|
|
109
|
+
ornn storage undeploy --reservation <reservation-id> [--json]
|
|
94
110
|
ornn storage buckets list [--json]
|
|
95
111
|
ornn storage buckets show <drive-id> [--json]
|
|
96
112
|
ornn storage buckets connect gcs|s3|r2 --bucket <bucket>|--url <url> [--name <name>] [--prefix <prefix>] [--region <region>] [--account-id <id>|--endpoint-url <url>] [--access-key-id <id> --secret-access-key-file <path>] [--read-only|--read-write] [--verify] [--json]
|
|
@@ -102,6 +118,7 @@ ornn keys add [<public-key-file>] [--public-key <key>] [--label <label>] [--json
|
|
|
102
118
|
ornn keys delete <key-id> [--json]
|
|
103
119
|
ornn access show <reservation-id> [--json]
|
|
104
120
|
ornn access activate <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--no-open] [--json]
|
|
121
|
+
ornn access switch <reservation-id> --network public|private --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--wait] [--json]
|
|
105
122
|
ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
|
|
106
123
|
ornn access keys list <reservation-id> [--json]
|
|
107
124
|
ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
|
|
@@ -120,33 +137,141 @@ ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
|
|
|
120
137
|
ornn logout
|
|
121
138
|
```
|
|
122
139
|
|
|
140
|
+
`ornn access keys add` is safe before or after launch. When the reservation has
|
|
141
|
+
active VM or Bare Metal machines, adding the key automatically queues the
|
|
142
|
+
current user's complete reservation key set on those machines. Use
|
|
143
|
+
`ornn access keys push` to explicitly re-sync keys that are already attached.
|
|
144
|
+
|
|
145
|
+
`ornn storage deploy status` prints reconciled object and byte progress once the
|
|
146
|
+
managed transfer has reported progress. AWS S3 and external GCS sources use the
|
|
147
|
+
managed transfer path. Cloudflare R2 support is coming soon.
|
|
148
|
+
|
|
123
149
|
UI aliases are first-class in the CLI: `listings` maps to the older
|
|
124
150
|
`availability` command, `exchange` maps to `bid`, and `gpus` maps to
|
|
125
151
|
`reservations`. The older command names remain available for scripts.
|
|
152
|
+
Bid and reservation list commands return a bounded page (500 rows by default);
|
|
153
|
+
use `--limit` and `--cursor` to traverse older history. Pass the final row ID
|
|
154
|
+
from one page as the next page's cursor. `show` and `checkout`
|
|
155
|
+
use exact tenant-owned item lookups, so they are not limited to the current page.
|
|
126
156
|
|
|
127
157
|
## Operator commands
|
|
128
158
|
|
|
129
159
|
The `ornn node` commands are for Ornn operators and reviewers, not tenants. They
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
160
|
+
wrap internal-reviewer-gated compute endpoints. Prefer `ornn login` as an
|
|
161
|
+
internal-allowed staff user (`ORNN_AUTH_BASE_URL`); the CLI calls
|
|
162
|
+
`/api/cli/operator/*` and the web server attaches the review secret — it never
|
|
163
|
+
downloads to the laptop. Optionally set `ORNN_INTERNAL_REVIEW_SECRET` plus
|
|
164
|
+
`ORNN_COMPUTE_BASE_URL` for direct compute access (CI / break-glass):
|
|
133
165
|
|
|
134
166
|
- `ornn node health <node-id>` shows launch readiness (ready/blocked plus
|
|
135
167
|
reasons), passive fault reasons, and the last validation status.
|
|
136
|
-
- `ornn node diagnose <node-id>` queues
|
|
137
|
-
- `ornn node
|
|
138
|
-
|
|
168
|
+
- `ornn node diagnose <node-id>` queues a manual node validation test.
|
|
169
|
+
- `ornn node list [--operator <id-or-slug>] [--facility <id>]` lists GPU nodes,
|
|
170
|
+
optionally filtered by operator and/or facility. A UUID operator is matched by
|
|
171
|
+
id; anything else is treated as the operator slug.
|
|
172
|
+
- `ornn node reboot <node-id>` queues an OS reboot of the host backing the node
|
|
173
|
+
(storage and SSH keys preserved).
|
|
174
|
+
- `ornn node hard-reset <node-id>` wipes tenant storage/users and reboots, then
|
|
175
|
+
re-pushes the tenant's keys on reconnect. The tenant keeps ownership of the
|
|
176
|
+
host.
|
|
177
|
+
- `ornn node off-grid <node-id> [--reason <text>]` queues the agentless handoff
|
|
178
|
+
that removes Ornn agent management from the still-powered-on host.
|
|
179
|
+
- `ornn node on-grid <node-id>` re-enrolls an off-grid host (SSH in with the
|
|
180
|
+
stored admin key and re-run the installer) to restore Ornn management.
|
|
181
|
+
- `ornn node terminate <node-id> [--reason <text>] [--force]` best-effort cleans
|
|
182
|
+
the host, removes the Ornn agent, and hard-deletes the node record from the
|
|
183
|
+
platform. It does not return the host to a tenant.
|
|
184
|
+
- `ornn node admin-key <node-id>` prints the Ornn admin SSH keypair for the node
|
|
185
|
+
(including the private key) so an operator can SSH into the host. Treat the
|
|
186
|
+
output as a secret.
|
|
187
|
+
- `ornn node deenroll <node-id>` removes a node from the environment: it first
|
|
188
|
+
gracefully terminates the enrollment (revokes the node claim, retires any
|
|
189
|
+
listing, and queues the node-agent self-uninstall packet), then dereferences
|
|
190
|
+
(hard-deletes) the `gpu_nodes` record so nothing lingers. Pass `--keep-record`
|
|
191
|
+
to stop after termination and leave the row in a `deenrolled` state, or
|
|
192
|
+
`--reason <text>` to annotate the audit log. The command is idempotent: an
|
|
193
|
+
already-removed node reports success. Terminate is forced by default so a
|
|
194
|
+
dead/unreachable node does not block on workload drain.
|
|
195
|
+
|
|
196
|
+
The operator resource commands share the same reviewer auth:
|
|
197
|
+
|
|
198
|
+
- `ornn operators list` lists enrolled operators.
|
|
199
|
+
- `ornn facilities list [--operator <id-or-slug>]` lists facilities, optionally
|
|
200
|
+
filtered to one operator.
|
|
201
|
+
- `ornn tokens list` lists enrollment tokens.
|
|
202
|
+
- `ornn tokens create --operator <id> [--facility <id>]
|
|
203
|
+
[--expires-in <seconds>] [--mode bare-metal|vm] [--ip <addr>]` mints an
|
|
204
|
+
enrollment token and prints a ready-to-run `curl … | sudo bash` install
|
|
205
|
+
command.
|
|
206
|
+
- `ornn tokens revoke <token-id>` revokes an enrollment token.
|
|
207
|
+
|
|
208
|
+
Fleet handoff is an ordered, Compute-backed workflow:
|
|
209
|
+
|
|
210
|
+
- Before first contact, independently verify each host key and add it to the
|
|
211
|
+
operator's SSH `known_hosts`; fleet SSH uses strict host-key checking and will
|
|
212
|
+
not trust a key first presented during cleanup.
|
|
213
|
+
- `fleet clean ... --dry-run` tries `ubuntu`, `admin`, then `ornn`, records each
|
|
214
|
+
host's hardware identity and exact deletion plan, and prints one fleet plan
|
|
215
|
+
hash. Repeat `--source-user` for Linux accounts owned by the departing tenant
|
|
216
|
+
(including an explicitly reviewed orphaned `/home/<user>` directory) and
|
|
217
|
+
`--preserve-user` for reviewed host accounts. Package/service-owned
|
|
218
|
+
accounts are protected automatically; any other account stops planning for
|
|
219
|
+
explicit review. Re-running with `--confirm-clean <plan-hash>` and the same
|
|
220
|
+
account flags removes the approved tenant
|
|
221
|
+
users, keys, containers/images/volumes/networks/build cache, workload paths,
|
|
222
|
+
mounts, services, and tenant-scoped logs. Apply refuses plans with running
|
|
223
|
+
containers, locks the management/root passwords, enforces key-only SSH, trims
|
|
224
|
+
supported filesystems, preserves existing root and management keys and host
|
|
225
|
+
logs, and stores per-node proof in Compute. A failed
|
|
226
|
+
cleanup can be dry-run again with the same fleet ID; only failed members are
|
|
227
|
+
replanned. Residual GPU
|
|
228
|
+
consumers or tenant/platform listeners fail verification instead of being
|
|
229
|
+
silently accepted.
|
|
230
|
+
- `fleet enroll <fleet-id>` refuses nodes without successful cleanup proof,
|
|
231
|
+
issues and revokes one cleanup-bound token per host, installs the agent,
|
|
232
|
+
assigns the IB island, verifies the same management account still has
|
|
233
|
+
key-based SSH and passwordless sudo, and records the resulting GPU node IDs
|
|
234
|
+
in Compute. Generic enrollment tokens retain the legacy enrollment sanitizer.
|
|
235
|
+
- `fleet deploy <fleet-id> --tenant <contact-email> --user <email-or-id>
|
|
236
|
+
[--commerce-reservation <id>]` validates the exact tenant/user and, when
|
|
237
|
+
supplied, the Commerce reservation. It succeeds only after every selected
|
|
238
|
+
user's SSH key is installed on every running node and passwordless sudo is
|
|
239
|
+
reported ready.
|
|
240
|
+
|
|
241
|
+
Compute Postgres is authoritative; the mode-0600 files under
|
|
242
|
+
`~/.config/ornn/fleets/` are only a secret-free cache. Cleanup proves “no
|
|
243
|
+
observable residue under the verification scope,” not certified physical-media
|
|
244
|
+
erasure. Root is never the preserved management account. `--confirm-takeover`
|
|
245
|
+
names only fleet IPs whose existing node claims may be replaced. The IB-island
|
|
246
|
+
is a locality label, not cluster creation. Test scheduling remains a separate
|
|
247
|
+
control-plane policy.
|
|
248
|
+
|
|
249
|
+
Reservation lifecycle verbs also run under the operator surface:
|
|
250
|
+
|
|
251
|
+
- `ornn reservations withdraw <reservation-id>` withdraws a reservation's
|
|
252
|
+
acceptance, returning the node(s) to tenant inventory.
|
|
253
|
+
- `ornn reservations transfer <reservation-id> --target-tenant <id>
|
|
254
|
+
[--target-user <id>] [--node <id>] [--strategy reject|park] [--confirm]`
|
|
255
|
+
transfers a reservation to another tenant.
|
|
256
|
+
- `ornn reservations deploy <node-id> --target-tenant <id> [--target-user <id>]
|
|
257
|
+
[--network public|private]` deploys a node into a tenant's inventory.
|
|
139
258
|
|
|
140
|
-
|
|
259
|
+
The tenant `ornn reservations list|show|checkout` verbs still use the tenant CLI
|
|
260
|
+
login and `/api/cli` proxy; `withdraw|transfer|deploy` use the operator path
|
|
261
|
+
above (staff session proxy or optional review secret).
|
|
141
262
|
|
|
142
|
-
|
|
143
|
-
`http://localhost:8000`). The review secret travels as a request header, so
|
|
144
|
-
in any non-local deployment this should be an `https://` origin.
|
|
145
|
-
- `ORNN_INTERNAL_REVIEW_SECRET` — the reviewer secret, sent as the
|
|
146
|
-
`X-Internal-Review-Secret` header.
|
|
263
|
+
Staff happy path:
|
|
147
264
|
|
|
148
|
-
|
|
149
|
-
|
|
265
|
+
- `ORNN_AUTH_BASE_URL` — web/auth origin
|
|
266
|
+
- `ornn login` as a user allowed by `INTERNAL_ALLOWED_*` on that environment
|
|
267
|
+
|
|
268
|
+
Optional direct compute override:
|
|
269
|
+
|
|
270
|
+
- `ORNN_COMPUTE_BASE_URL` — compute origin (for example `http://localhost:8000`)
|
|
271
|
+
- `ORNN_INTERNAL_REVIEW_SECRET` — reviewer secret (`X-Internal-Review-Secret`)
|
|
272
|
+
|
|
273
|
+
Without a staff login session or the review-secret pair, operator commands fail
|
|
274
|
+
with a clear error before making a network request.
|
|
150
275
|
|
|
151
276
|
Commands print concise human-readable output by default. Use `--json` on
|
|
152
277
|
read/show/list commands and transaction handoffs when you need structured
|
|
@@ -161,6 +286,19 @@ the terminal keeps polling until the login is approved or expires.
|
|
|
161
286
|
Run `ornn --help` for the full command list and `ornn api` for the
|
|
162
287
|
allowlisted compute API escape hatch.
|
|
163
288
|
|
|
289
|
+
WireGuard client configs use the current tenant-scoped `/nodes` routes:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
ornn api get /nodes/reservations/<reservation-id>/vpn/peers --raw
|
|
293
|
+
ornn api post /nodes/reservations/<reservation-id>/vpn/peers \
|
|
294
|
+
--data '{"public_key":"<wireguard-public-key>","label":"Laptop"}' --raw
|
|
295
|
+
ornn api delete /nodes/reservations/<reservation-id>/vpn/peers/<peer-id> --raw
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The legacy `/standalone-vms/*` prefix is not exposed through the CLI proxy.
|
|
299
|
+
Deleting a peer is the supported way to replace its key; switching a node
|
|
300
|
+
between Public and Private VPN preserves the peer.
|
|
301
|
+
|
|
164
302
|
## Updating
|
|
165
303
|
|
|
166
304
|
The CLI checks the npm registry for a newer version at most once per day and prints a one-line notice on stderr when one exists. Run `ornn update` to install it (the command detects npm, pnpm, yarn, or bun installs). The check is skipped in CI and non-interactive shells.
|
package/package.json
CHANGED
package/src/api-client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearAuthSession, loadAuthSession } from "./auth-store.mjs";
|
|
1
|
+
import { clearAuthSession, configuredAuthBaseUrl, loadAuthSession } from "./auth-store.mjs";
|
|
2
2
|
import { resolveAuthBaseUrl } from "./device-auth.mjs";
|
|
3
3
|
|
|
4
4
|
export class CliApiError extends Error {
|
|
@@ -11,13 +11,22 @@ export class CliApiError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function resolveApiBaseUrl({ env = process.env, explicit, session } = {}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
env.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
// Prefer the first configured source; do not fall through when it strips empty.
|
|
15
|
+
const candidates = [
|
|
16
|
+
[explicit, "API base URL"],
|
|
17
|
+
[env.ORNN_API_BASE_URL, "ORNN_API_BASE_URL"],
|
|
18
|
+
[env.ORNN_AUTH_BASE_URL, "ORNN_AUTH_BASE_URL"],
|
|
19
|
+
[session?.authBaseUrl, "session authBaseUrl"],
|
|
20
|
+
];
|
|
21
|
+
for (const [value, sourceName] of candidates) {
|
|
22
|
+
const trimmed = value?.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
return requireValidBaseUrl(trimmed, sourceName);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return requireValidBaseUrl(resolveAuthBaseUrl({ env }), "API base URL");
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export async function loadRequiredSession({ env = process.env } = {}) {
|
|
@@ -101,32 +110,111 @@ export function resolveComputeBaseUrl({ env = process.env } = {}) {
|
|
|
101
110
|
"Set ORNN_COMPUTE_BASE_URL to the internal compute service origin to run operator commands.",
|
|
102
111
|
);
|
|
103
112
|
}
|
|
104
|
-
return base
|
|
113
|
+
return requireValidBaseUrl(base, "ORNN_COMPUTE_BASE_URL");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Origin used in `curl …/enrollment/install` one-liners.
|
|
118
|
+
*
|
|
119
|
+
* Prefer an explicit compute/installer URL. When staff use the web operator
|
|
120
|
+
* proxy (no local review secret), derive compute from the auth/web origin:
|
|
121
|
+
* - `https://<slug>.fabric.ornn.com` → `https://<slug>-api.fabric.ornn.com`
|
|
122
|
+
* - `https://compute.ornn.com` → `https://api-compute.ornn.com`
|
|
123
|
+
* - `http://localhost:3000` → `http://localhost:8000`
|
|
124
|
+
*/
|
|
125
|
+
export function resolveInstallerBaseUrl({ env = process.env, session } = {}) {
|
|
126
|
+
const explicit =
|
|
127
|
+
env.ORNN_INSTALLER_URL?.trim() ||
|
|
128
|
+
env.ORNN_COMPUTE_BASE_URL?.trim() ||
|
|
129
|
+
env.NEXT_PUBLIC_INSTALLER_URL?.trim();
|
|
130
|
+
if (explicit) {
|
|
131
|
+
return requireValidBaseUrl(explicit, "installer base URL");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The session records where the caller actually logged in, so it outranks the
|
|
135
|
+
// installed default.
|
|
136
|
+
const authCandidates = [
|
|
137
|
+
env.ORNN_AUTH_BASE_URL,
|
|
138
|
+
env.ORNN_API_BASE_URL,
|
|
139
|
+
session?.authBaseUrl,
|
|
140
|
+
configuredAuthBaseUrl(env),
|
|
141
|
+
];
|
|
142
|
+
for (const candidate of authCandidates) {
|
|
143
|
+
const trimmed = candidate?.trim();
|
|
144
|
+
if (!trimmed) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const authOrigin = requireValidBaseUrl(trimmed, "ORNN_AUTH_BASE_URL");
|
|
148
|
+
return deriveInstallerOriginFromAuth(authOrigin);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function deriveInstallerOriginFromAuth(authOrigin) {
|
|
154
|
+
const url = new URL(authOrigin);
|
|
155
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
156
|
+
url.port = "8000";
|
|
157
|
+
url.protocol = "http:";
|
|
158
|
+
return url.origin;
|
|
159
|
+
}
|
|
160
|
+
const fabricMatch = url.hostname.match(/^([a-z0-9-]+)\.fabric\.ornn\.com$/i);
|
|
161
|
+
if (fabricMatch && !fabricMatch[1].endsWith("-api")) {
|
|
162
|
+
url.hostname = `${fabricMatch[1]}-api.fabric.ornn.com`;
|
|
163
|
+
return url.origin;
|
|
164
|
+
}
|
|
165
|
+
if (url.hostname === "compute.ornn.com") {
|
|
166
|
+
url.hostname = "api-compute.ornn.com";
|
|
167
|
+
return url.origin;
|
|
168
|
+
}
|
|
169
|
+
return url.origin;
|
|
105
170
|
}
|
|
106
171
|
|
|
107
172
|
export function resolveInternalReviewSecret({ env = process.env } = {}) {
|
|
108
173
|
const secret = env.ORNN_INTERNAL_REVIEW_SECRET?.trim();
|
|
109
174
|
if (!secret) {
|
|
110
175
|
throw new CliApiError(
|
|
111
|
-
"Set ORNN_INTERNAL_REVIEW_SECRET to authenticate operator commands.",
|
|
176
|
+
"Set ORNN_INTERNAL_REVIEW_SECRET to authenticate operator commands, or run `ornn login` as Ornn staff.",
|
|
112
177
|
);
|
|
113
178
|
}
|
|
114
179
|
return secret;
|
|
115
180
|
}
|
|
116
181
|
|
|
117
|
-
|
|
182
|
+
function operatorProxyEndpoint(endpoint) {
|
|
183
|
+
return `/api/cli/operator${normalizeComputePath(endpoint)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function operatorRequestViaStaffSession({
|
|
187
|
+
body,
|
|
188
|
+
endpoint,
|
|
189
|
+
env = process.env,
|
|
190
|
+
fetchImpl = fetch,
|
|
191
|
+
method = "GET",
|
|
192
|
+
} = {}) {
|
|
193
|
+
// Staff path: CLI bearer → web /api/cli/operator → compute with server-side review secret.
|
|
194
|
+
return cliRequest({
|
|
195
|
+
body,
|
|
196
|
+
endpoint: operatorProxyEndpoint(endpoint),
|
|
197
|
+
env,
|
|
198
|
+
fetchImpl,
|
|
199
|
+
method,
|
|
200
|
+
authRequired: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function operatorRequestViaReviewSecret({
|
|
118
205
|
body,
|
|
119
206
|
endpoint,
|
|
120
207
|
env = process.env,
|
|
121
208
|
fetchImpl = fetch,
|
|
122
209
|
method = "GET",
|
|
123
210
|
} = {}) {
|
|
124
|
-
//
|
|
211
|
+
// Break-glass / CI: hit compute directly with the review secret.
|
|
125
212
|
const baseUrl = resolveComputeBaseUrl({ env });
|
|
126
213
|
const secret = resolveInternalReviewSecret({ env });
|
|
127
214
|
const url = new URL(endpoint, `${baseUrl}/`);
|
|
128
215
|
const headers = {
|
|
129
216
|
Accept: "application/json",
|
|
217
|
+
"X-Internal-Actor": env.ORNN_INTERNAL_ACTOR?.trim() || "break-glass-cli",
|
|
130
218
|
"X-Internal-Review-Secret": secret,
|
|
131
219
|
};
|
|
132
220
|
|
|
@@ -157,6 +245,32 @@ export async function operatorRequest({
|
|
|
157
245
|
return data;
|
|
158
246
|
}
|
|
159
247
|
|
|
248
|
+
export async function operatorRequest({
|
|
249
|
+
body,
|
|
250
|
+
endpoint,
|
|
251
|
+
env = process.env,
|
|
252
|
+
fetchImpl = fetch,
|
|
253
|
+
method = "GET",
|
|
254
|
+
} = {}) {
|
|
255
|
+
// Prefer an explicit review secret (CI / break-glass). Otherwise use a staff
|
|
256
|
+
// `ornn login` session through the web operator proxy — the secret never
|
|
257
|
+
// leaves the server.
|
|
258
|
+
if (env.ORNN_INTERNAL_REVIEW_SECRET?.trim()) {
|
|
259
|
+
return operatorRequestViaReviewSecret({ body, endpoint, env, fetchImpl, method });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const session = await loadAuthSession({ env });
|
|
263
|
+
if (session?.accessToken) {
|
|
264
|
+
return operatorRequestViaStaffSession({ body, endpoint, env, fetchImpl, method });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
throw new CliApiError(
|
|
268
|
+
"Operator commands need Ornn staff auth: run `ornn login` as an internal user " +
|
|
269
|
+
"(ORNN_AUTH_BASE_URL), or set ORNN_INTERNAL_REVIEW_SECRET (+ ORNN_COMPUTE_BASE_URL) for direct compute access.",
|
|
270
|
+
{ status: 401 },
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
160
274
|
export function computeEndpoint(path) {
|
|
161
275
|
const normalized = normalizeComputePath(path);
|
|
162
276
|
return `/api/cli/compute${normalized}`;
|
|
@@ -173,6 +287,28 @@ export function normalizeComputePath(path) {
|
|
|
173
287
|
return trimmed;
|
|
174
288
|
}
|
|
175
289
|
|
|
290
|
+
function requireValidBaseUrl(raw, sourceName) {
|
|
291
|
+
const stripped = raw.replace(/\/+$/, "");
|
|
292
|
+
if (!stripped) {
|
|
293
|
+
throw new CliApiError(
|
|
294
|
+
`${sourceName} must be a valid origin (got empty value after removing trailing slashes).`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
let parsed;
|
|
298
|
+
try {
|
|
299
|
+
parsed = new URL(stripped);
|
|
300
|
+
} catch {
|
|
301
|
+
throw new CliApiError(`${sourceName} must be a valid origin (got ${JSON.stringify(raw)}).`);
|
|
302
|
+
}
|
|
303
|
+
// Path/search/hash are dropped when composing absolute endpoints against this base.
|
|
304
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
305
|
+
throw new CliApiError(
|
|
306
|
+
`${sourceName} must be a valid origin with no path (got ${JSON.stringify(raw)}).`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return stripped;
|
|
310
|
+
}
|
|
311
|
+
|
|
176
312
|
function parseJson(text, url, response) {
|
|
177
313
|
try {
|
|
178
314
|
return JSON.parse(text);
|
package/src/auth-store.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
|
|
5
6
|
const APP_DIR = "ornn";
|
|
6
7
|
const AUTH_FILE = "auth.json";
|
|
8
|
+
const CONFIG_FILE = "config.json";
|
|
9
|
+
const FLEETS_DIR = "fleets";
|
|
7
10
|
|
|
8
11
|
export function getAuthConfigDir(env = process.env) {
|
|
9
12
|
if (env.ORNN_CONFIG_HOME?.trim()) {
|
|
@@ -21,6 +24,24 @@ export function getAuthConfigPath(env = process.env) {
|
|
|
21
24
|
return join(getAuthConfigDir(env), AUTH_FILE);
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
export function getCliConfigPath(env = process.env) {
|
|
28
|
+
return join(getAuthConfigDir(env), CONFIG_FILE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getFleetConfigDir(env = process.env) {
|
|
32
|
+
return join(getAuthConfigDir(env), FLEETS_DIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read synchronously so callers resolving a base URL stay synchronous.
|
|
36
|
+
export function configuredAuthBaseUrl(env = process.env) {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(readFileSync(getCliConfigPath(env), "utf8"))?.authBaseUrl;
|
|
39
|
+
return typeof value === "string" ? value.trim() : "";
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
export async function loadAuthSession({ env = process.env } = {}) {
|
|
25
46
|
const path = getAuthConfigPath(env);
|
|
26
47
|
|