@gravity-rail/cli 0.13.1 → 0.15.0
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 +25 -14
- package/dist/{account-verify-phone-N7EJS4J3.js → account-verify-phone-Q2NIRSX4.js} +4 -4
- package/dist/{chunk-WZVMWN3S.js → chunk-3T7A5PSF.js} +3 -4
- package/dist/chunk-3T7A5PSF.js.map +1 -0
- package/dist/{chunk-LE5IR5MC.js → chunk-EITDLX75.js} +226 -96
- package/dist/chunk-EITDLX75.js.map +1 -0
- package/dist/{chunk-XSFAJ5ZO.js → chunk-PODEUTUQ.js} +35 -1
- package/dist/chunk-PODEUTUQ.js.map +1 -0
- package/dist/{cli-client-context-C5l8lg7h.d.ts → cli-client-context-D81-ym1X.d.ts} +27 -0
- package/dist/cli-utils.d.ts +2 -2
- package/dist/cli-utils.js +1 -1
- package/dist/{errors-YHGVIDPO.js → errors-CFRLSAGA.js} +2 -2
- package/dist/gr.js +1300 -18648
- package/dist/gr.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +620 -43
- package/dist/lib.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-LE5IR5MC.js.map +0 -1
- package/dist/chunk-WZVMWN3S.js.map +0 -1
- package/dist/chunk-XSFAJ5ZO.js.map +0 -1
- /package/dist/{account-verify-phone-N7EJS4J3.js.map → account-verify-phone-Q2NIRSX4.js.map} +0 -0
- /package/dist/{errors-YHGVIDPO.js.map → errors-CFRLSAGA.js.map} +0 -0
package/README.md
CHANGED
|
@@ -59,15 +59,15 @@ profiles; re-run import after re-seed. Worktree API ports get isolated credentia
|
|
|
59
59
|
|
|
60
60
|
### Credential store (security)
|
|
61
61
|
|
|
62
|
-
| Item | Detail
|
|
63
|
-
| --------------- |
|
|
64
|
-
| Location | `~/.gr-cli/credentials-<env>.json` (e.g. `credentials-prod.json`)
|
|
65
|
-
| Directory mode | `0700` on `~/.gr-cli`
|
|
66
|
-
| File mode | `0600` on credential files
|
|
62
|
+
| Item | Detail |
|
|
63
|
+
| --------------- | ------------------------------------------------------------------------------------------- |
|
|
64
|
+
| Location | `~/.gr-cli/credentials-<env>.json` (e.g. `credentials-prod.json`) |
|
|
65
|
+
| Directory mode | `0700` on `~/.gr-cli` |
|
|
66
|
+
| File mode | `0600` on credential files |
|
|
67
67
|
| Contents | OAuth client id/secret + tokens, and/or token-only dev profiles (+ optional workspace UUID) |
|
|
68
|
-
| Not stored | `GRAVITY_RAIL_API_KEY` (process environment only; always overrides profiles)
|
|
69
|
-
| Name validation | Profiles allow `name` or `workspace/member`; env names are allowlisted
|
|
70
|
-
| Logging | CLI code never writes token or secret values to stdout/stderr
|
|
68
|
+
| Not stored | `GRAVITY_RAIL_API_KEY` (process environment only; always overrides profiles) |
|
|
69
|
+
| Name validation | Profiles allow `name` or `workspace/member`; env names are allowlisted |
|
|
70
|
+
| Logging | CLI code never writes token or secret values to stdout/stderr |
|
|
71
71
|
|
|
72
72
|
**Security review notes:** tokens never leave the local filesystem except on HTTPS calls to the Gravity Rail API. There is no cloud sync of CLI credentials. Rotate by `gr auth logout --name <profile>` (or deleting the store file) and logging in again. Multi-user machines should rely on OS home-directory isolation plus the `0600`/`0700` modes. Do not commit `~/.gr-cli` into repositories or bake it into container images.
|
|
73
73
|
|
|
@@ -88,6 +88,16 @@ gr tasks list -w $WORKSPACE
|
|
|
88
88
|
|
|
89
89
|
Useful for scripts, CI/CD, and automation. Create scoped keys from workspace settings or via [`@gravity-rail/sdk`](https://www.npmjs.com/package/@gravity-rail/sdk). API keys are **not** written into the profile store.
|
|
90
90
|
|
|
91
|
+
### Organizations that require SSO
|
|
92
|
+
|
|
93
|
+
Workspaces in an SSO-enforced organization need an org-scoped session in addition to your account login. When you run a `gr` command against such a workspace after `gr login`, the CLI opens your browser once to complete the organization's SSO, captures the resulting session over a local `127.0.0.1` callback, and caches it under your profile — subsequent commands reuse it silently until it expires. No token is ever placed in a URL.
|
|
94
|
+
|
|
95
|
+
For **non-interactive** use (CI, scripts, code-interpreter sandboxes, or any machine with no browser), org SSO cannot run; use an **API key** instead — API keys are exempt from the org SSO gate:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
export GRAVITY_RAIL_API_KEY=your-api-key # set GRAVITY_RAIL_NO_BROWSER=1 to force the API-key hint
|
|
99
|
+
```
|
|
100
|
+
|
|
91
101
|
## Command Pattern
|
|
92
102
|
|
|
93
103
|
```
|
|
@@ -190,12 +200,12 @@ cat member.json | gr members update -w $WORKSPACE --id 42 --allow-write
|
|
|
190
200
|
|
|
191
201
|
## Exit Codes
|
|
192
202
|
|
|
193
|
-
| Code | Meaning
|
|
194
|
-
| ---- |
|
|
195
|
-
| `0` | Success
|
|
196
|
-
| `1` | General error
|
|
197
|
-
| `2` | Authentication required (run `gr login` or set `GRAVITY_RAIL_API_KEY`)
|
|
198
|
-
| `3` | A human-in-the-loop (HITL) interrupt requires out-of-band action before the operation can continue
|
|
203
|
+
| Code | Meaning |
|
|
204
|
+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
205
|
+
| `0` | Success |
|
|
206
|
+
| `1` | General error |
|
|
207
|
+
| `2` | Authentication required (run `gr login` or set `GRAVITY_RAIL_API_KEY`) |
|
|
208
|
+
| `3` | A human-in-the-loop (HITL) interrupt requires out-of-band action before the operation can continue |
|
|
199
209
|
| `4` | No assistant reply — a `--message` turn made tool calls but returned no final summary, or an inference error occurred with no final message (side effects may have been applied) |
|
|
200
210
|
|
|
201
211
|
When a concierge command run with `--message` (non-interactive mode) hits a HITL
|
|
@@ -268,6 +278,7 @@ not forwarded, as it may contain sensitive data. Use `resume.chatUuid` /
|
|
|
268
278
|
| `slack-apps` | Slack app management — installations, threads, accounts |
|
|
269
279
|
| `fhir-connections` | FHIR healthcare connections — patients, practitioners, encounters |
|
|
270
280
|
| `patientsync` | PatientSync PSTN-transfer connection — `configure` / `status` / `delete` (singleton per workspace) |
|
|
281
|
+
| `documo-fax` | Documo (mFax) inbound fax — `install` / `configure` / `setup` / `verify` / `status` / `faxes` |
|
|
271
282
|
| `app-connections` | Platform app integrations |
|
|
272
283
|
| `monday` | Monday.com — boards, columns, webhooks |
|
|
273
284
|
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
confirmAccountPhoneVerify,
|
|
3
3
|
requireAuth,
|
|
4
4
|
startAccountPhoneVerify
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-EITDLX75.js";
|
|
6
|
+
import "./chunk-PODEUTUQ.js";
|
|
7
|
+
import "./chunk-3T7A5PSF.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/account-verify-phone.ts
|
|
10
10
|
import * as readline from "readline/promises";
|
|
@@ -38,4 +38,4 @@ async function promptLine(question) {
|
|
|
38
38
|
export {
|
|
39
39
|
accountVerifyPhoneCommand
|
|
40
40
|
};
|
|
41
|
-
//# sourceMappingURL=account-verify-phone-
|
|
41
|
+
//# sourceMappingURL=account-verify-phone-Q2NIRSX4.js.map
|
|
@@ -39,9 +39,8 @@ Hint: Run "gr login" to authenticate, or set GRAVITY_RAIL_API_KEY for non-intera
|
|
|
39
39
|
process.stderr.write(
|
|
40
40
|
`
|
|
41
41
|
Hint: This is a write/admin operation and the sandbox token lacks the required scope.
|
|
42
|
-
|
|
43
|
-
exec(code="<your gr command>", language="bash", scopes=["
|
|
44
|
-
Common scopes: datatypes:write, workflows:write, members:write, automations:admin, agents:write
|
|
42
|
+
If the error above names a required scope, re-run the exec tool with it, e.g.:
|
|
43
|
+
exec(code="<your gr command>", language="bash", scopes=["<required scope>"])
|
|
45
44
|
`
|
|
46
45
|
);
|
|
47
46
|
}
|
|
@@ -57,4 +56,4 @@ export {
|
|
|
57
56
|
AuthRequiredError,
|
|
58
57
|
handleError
|
|
59
58
|
};
|
|
60
|
-
//# sourceMappingURL=chunk-
|
|
59
|
+
//# sourceMappingURL=chunk-3T7A5PSF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/errors.ts"],"sourcesContent":["export class CLIError extends Error {\n constructor(\n message: string,\n public exitCode: number = 1\n ) {\n super(message);\n this.name = 'CLIError';\n }\n}\n\nexport class AuthRequiredError extends CLIError {\n constructor(message = 'Not authenticated. Run `gr login` or set GRAVITY_RAIL_API_KEY.') {\n super(message, 2);\n this.name = 'AuthRequiredError';\n }\n}\n\nexport function handleError(err: unknown): never {\n if (err instanceof CLIError) {\n process.stderr.write(`Error: ${err.message}\\n`);\n process.exit(err.exitCode);\n }\n\n if (err instanceof Error) {\n process.stderr.write(`Error: ${err.message}\\n`);\n if ('errors' in err && Array.isArray((err as any).errors)) {\n for (const e of (err as any).errors) {\n process.stderr.write(` - ${e}\\n`);\n }\n }\n if ('statusCode' in err && (err as any).statusCode === 401) {\n process.stderr.write(\n `\\nHint: Run \"gr login\" to authenticate, or set GRAVITY_RAIL_API_KEY for non-interactive use.\\n`\n );\n }\n // In the manager sandbox a 403 almost always means the service token lacks the\n // write/admin scope for this command. The token's scopes are granted by the exec\n // tool's `scopes=[...]` (HITL-approved), so tell the agent how to re-request them.\n //\n // Deliberately NO hard-coded scope list: it drifts from the Scope enum and then\n // actively misleads the agent (prod manager chat 287 — the omitted scope was\n // records:write). Scope-only denials name the scope in `err.message`\n // (\"... Required scopes: records:write\"), which is printed above, and the exec\n // tool derives its retry hint from that same text.\n if (\n 'statusCode' in err &&\n (err as any).statusCode === 403 &&\n process.env.GRAVITY_RAIL_SANDBOX === '1'\n ) {\n process.stderr.write(\n `\\nHint: This is a write/admin operation and the sandbox token lacks the required scope.\\n` +\n `If the error above names a required scope, re-run the exec tool with it, e.g.:\\n` +\n ` exec(code=\"<your gr command>\", language=\"bash\", scopes=[\"<required scope>\"])\\n`\n );\n }\n process.exit(1);\n }\n\n process.stderr.write(`Error: ${String(err)}\\n`);\n process.exit(1);\n}\n"],"mappings":";AAAO,IAAM,WAAN,cAAuB,MAAM;AAAA,EACjC,YACG,SACO,WAAmB,GAC3B;AACC,UAAM,OAAO;AAFN;AAGP,SAAK,OAAO;AAAA,EACf;AAAA,EAJU;AAKb;AAEO,IAAM,oBAAN,cAAgC,SAAS;AAAA,EAC7C,YAAY,UAAU,kEAAkE;AACrF,UAAM,SAAS,CAAC;AAChB,SAAK,OAAO;AAAA,EACf;AACH;AAEO,SAAS,YAAY,KAAqB;AAC9C,MAAI,eAAe,UAAU;AAC1B,YAAQ,OAAO,MAAM,UAAU,IAAI,OAAO;AAAA,CAAI;AAC9C,YAAQ,KAAK,IAAI,QAAQ;AAAA,EAC5B;AAEA,MAAI,eAAe,OAAO;AACvB,YAAQ,OAAO,MAAM,UAAU,IAAI,OAAO;AAAA,CAAI;AAC9C,QAAI,YAAY,OAAO,MAAM,QAAS,IAAY,MAAM,GAAG;AACxD,iBAAW,KAAM,IAAY,QAAQ;AAClC,gBAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,CAAI;AAAA,MACpC;AAAA,IACH;AACA,QAAI,gBAAgB,OAAQ,IAAY,eAAe,KAAK;AACzD,cAAQ,OAAO;AAAA,QACZ;AAAA;AAAA;AAAA,MACH;AAAA,IACH;AAUA,QACG,gBAAgB,OACf,IAAY,eAAe,OAC5B,QAAQ,IAAI,yBAAyB,KACtC;AACC,cAAQ,OAAO;AAAA,QACZ;AAAA;AAAA;AAAA;AAAA;AAAA,MAGH;AAAA,IACH;AACA,YAAQ,KAAK,CAAC;AAAA,EACjB;AAEA,UAAQ,OAAO,MAAM,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AAC9C,UAAQ,KAAK,CAAC;AACjB;","names":[]}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
getEnvironmentConfig,
|
|
4
4
|
printTable,
|
|
5
5
|
refreshAccessToken
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-PODEUTUQ.js";
|
|
7
7
|
import {
|
|
8
8
|
AuthRequiredError,
|
|
9
9
|
CLIError
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-3T7A5PSF.js";
|
|
11
11
|
|
|
12
12
|
// src/lib/pagination.ts
|
|
13
13
|
function isListResult(data) {
|
|
@@ -223,6 +223,10 @@ var COL = {
|
|
|
223
223
|
getValue: (item) => String(item.enabled ?? "")
|
|
224
224
|
})
|
|
225
225
|
};
|
|
226
|
+
function hint(opts, lines) {
|
|
227
|
+
if ((opts.outputFormat ?? "table") !== "table") return;
|
|
228
|
+
process.stdout.write("\n" + lines.join("\n") + "\n");
|
|
229
|
+
}
|
|
226
230
|
function stderr(msg) {
|
|
227
231
|
process.stderr.write(msg + "\n");
|
|
228
232
|
}
|
|
@@ -232,8 +236,13 @@ import { GravityRailClient } from "@gravity-rail/sdk";
|
|
|
232
236
|
import { homedir } from "os";
|
|
233
237
|
|
|
234
238
|
// src/lib/org-auth.ts
|
|
239
|
+
import { createServer } from "http";
|
|
235
240
|
import { execFile } from "child_process";
|
|
241
|
+
import { randomBytes } from "crypto";
|
|
236
242
|
var ORG_AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
243
|
+
var ORG_SESSION_CACHE_TTL_MS = 6.5 * 24 * 60 * 60 * 1e3;
|
|
244
|
+
var CLI_ORG_AUTH_BRIDGE_PATH = "/cli/org-auth";
|
|
245
|
+
var ORG_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
237
246
|
function extractOrgSessionFromSetCookies(setCookieValues, preferredCookieName) {
|
|
238
247
|
let fallback;
|
|
239
248
|
for (const value of setCookieValues) {
|
|
@@ -276,19 +285,18 @@ async function tryDirectJoin(apiUrl, accessToken, orgAuthInfo) {
|
|
|
276
285
|
}
|
|
277
286
|
return orgSession;
|
|
278
287
|
}
|
|
279
|
-
function isAllowedAuthUrl(url, apiUrl) {
|
|
288
|
+
function isAllowedAuthUrl(url, apiUrl, frontendUrl) {
|
|
280
289
|
try {
|
|
281
290
|
const parsed = new URL(url);
|
|
282
|
-
const api = new URL(apiUrl);
|
|
283
291
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return
|
|
292
|
+
const allowedHosts = [apiUrl, frontendUrl].filter((u) => Boolean(u)).map((u) => {
|
|
293
|
+
try {
|
|
294
|
+
return new URL(u).hostname;
|
|
295
|
+
} catch {
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
}).filter((h) => Boolean(h));
|
|
299
|
+
return allowedHosts.includes(parsed.hostname);
|
|
292
300
|
} catch {
|
|
293
301
|
return false;
|
|
294
302
|
}
|
|
@@ -309,84 +317,175 @@ function openBrowser(url) {
|
|
|
309
317
|
execFile(command, args, (_err) => {
|
|
310
318
|
});
|
|
311
319
|
}
|
|
312
|
-
function
|
|
320
|
+
function isHeadless() {
|
|
321
|
+
if (process.env.GRAVITY_RAIL_NO_BROWSER) return true;
|
|
322
|
+
if (process.platform === "linux") {
|
|
323
|
+
return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
function startOrgSessionCapture(expectedState, expectedOrgUuid, allowedOrigin) {
|
|
328
|
+
let resolveSession;
|
|
329
|
+
let rejectSession;
|
|
330
|
+
const session = new Promise((resolve, reject) => {
|
|
331
|
+
resolveSession = resolve;
|
|
332
|
+
rejectSession = reject;
|
|
333
|
+
});
|
|
334
|
+
const applyCors = (req, res) => {
|
|
335
|
+
if (req.headers.origin && req.headers.origin === allowedOrigin) {
|
|
336
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
337
|
+
res.setHeader("Vary", "Origin");
|
|
338
|
+
}
|
|
339
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
340
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
341
|
+
};
|
|
342
|
+
const server = createServer((req, res) => {
|
|
343
|
+
applyCors(req, res);
|
|
344
|
+
if (req.method === "OPTIONS") {
|
|
345
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
346
|
+
res.writeHead(204);
|
|
347
|
+
res.end();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (req.method !== "POST") {
|
|
351
|
+
res.writeHead(405);
|
|
352
|
+
res.end();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let body = "";
|
|
356
|
+
let tooLarge = false;
|
|
357
|
+
req.on("data", (chunk) => {
|
|
358
|
+
body += chunk;
|
|
359
|
+
if (body.length > 16 * 1024) {
|
|
360
|
+
tooLarge = true;
|
|
361
|
+
req.destroy();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
req.on("end", () => {
|
|
365
|
+
if (tooLarge) return;
|
|
366
|
+
let capturedSession;
|
|
367
|
+
try {
|
|
368
|
+
const payload = JSON.parse(body);
|
|
369
|
+
if (typeof payload.org_session === "string" && payload.org_session.length > 0 && payload.state === expectedState && payload.org_uuid === expectedOrgUuid) {
|
|
370
|
+
capturedSession = payload.org_session;
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
if (capturedSession) {
|
|
375
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
376
|
+
res.end('{"ok":true}');
|
|
377
|
+
resolveSession(capturedSession);
|
|
378
|
+
} else {
|
|
379
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
380
|
+
res.end('{"ok":false}');
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
let resolveReady;
|
|
385
|
+
let rejectReady;
|
|
386
|
+
const ready = new Promise((resolve, reject) => {
|
|
387
|
+
resolveReady = resolve;
|
|
388
|
+
rejectReady = reject;
|
|
389
|
+
});
|
|
390
|
+
server.on("error", (err) => {
|
|
391
|
+
rejectReady(err);
|
|
392
|
+
rejectSession(err);
|
|
393
|
+
});
|
|
394
|
+
server.listen(0, "127.0.0.1", () => {
|
|
395
|
+
resolveReady(server.address().port);
|
|
396
|
+
});
|
|
397
|
+
const timeout = setTimeout(() => {
|
|
398
|
+
rejectSession(
|
|
399
|
+
new Error(
|
|
400
|
+
"Organization authentication timed out.\nComplete sign-in in the browser, then re-run the command."
|
|
401
|
+
)
|
|
402
|
+
);
|
|
403
|
+
}, ORG_AUTH_TIMEOUT_MS);
|
|
404
|
+
timeout.unref();
|
|
405
|
+
let closed = false;
|
|
406
|
+
const close = () => {
|
|
407
|
+
if (closed) return;
|
|
408
|
+
closed = true;
|
|
409
|
+
clearTimeout(timeout);
|
|
410
|
+
server.close();
|
|
411
|
+
};
|
|
412
|
+
return { ready, session, close };
|
|
413
|
+
}
|
|
414
|
+
function buildStepUpUrl(authUrl, orgUuid, port, state) {
|
|
415
|
+
const url = new URL(authUrl);
|
|
416
|
+
const bridgeQuery = new URLSearchParams({ org: orgUuid, port: String(port), state });
|
|
417
|
+
url.searchParams.set("return_to", `${CLI_ORG_AUTH_BRIDGE_PATH}?${bridgeQuery.toString()}`);
|
|
418
|
+
return url.toString();
|
|
419
|
+
}
|
|
420
|
+
function newStateNonce() {
|
|
421
|
+
return randomBytes(16).toString("hex");
|
|
422
|
+
}
|
|
423
|
+
async function stepUpViaBrowser(orgAuthInfo) {
|
|
424
|
+
const state = newStateNonce();
|
|
425
|
+
const allowedOrigin = new URL(orgAuthInfo.authUrl).origin;
|
|
426
|
+
const capture = startOrgSessionCapture(state, orgAuthInfo.orgUuid, allowedOrigin);
|
|
427
|
+
try {
|
|
428
|
+
const port = await capture.ready;
|
|
429
|
+
const stepUpUrl = buildStepUpUrl(orgAuthInfo.authUrl, orgAuthInfo.orgUuid, port, state);
|
|
430
|
+
stderr(`
|
|
431
|
+
This workspace requires organization SSO authentication.`);
|
|
432
|
+
stderr(`Opening browser to complete sign-in...`);
|
|
433
|
+
stderr(` ${stepUpUrl}
|
|
434
|
+
`);
|
|
435
|
+
stderr(`If the browser does not open, visit the URL above manually.`);
|
|
436
|
+
openBrowser(stepUpUrl);
|
|
437
|
+
return await capture.session;
|
|
438
|
+
} finally {
|
|
439
|
+
capture.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function registerOrgAuthHandler(apiClient, apiUrl, accessToken, orgAuthCtx) {
|
|
313
443
|
apiClient.setOrgAuthHandler(async (orgAuthInfo) => {
|
|
444
|
+
const orgUuid = orgAuthInfo.orgUuid;
|
|
445
|
+
if (!ORG_UUID_RE.test(orgUuid)) {
|
|
446
|
+
throw new Error("Invalid organization id in auth challenge");
|
|
447
|
+
}
|
|
448
|
+
const cm = orgAuthCtx?.credentialsManager;
|
|
449
|
+
const adopt = (session2) => {
|
|
450
|
+
if (cm && orgAuthCtx) {
|
|
451
|
+
const expiresAt = new Date(Date.now() + ORG_SESSION_CACHE_TTL_MS).toISOString();
|
|
452
|
+
cm.setOrgSession(orgAuthCtx.env, orgAuthCtx.profile, orgUuid, session2, expiresAt);
|
|
453
|
+
}
|
|
454
|
+
apiClient.setOrgSession(session2, orgUuid);
|
|
455
|
+
};
|
|
456
|
+
if (cm && orgAuthCtx) {
|
|
457
|
+
const cached = cm.getOrgSession(orgAuthCtx.env, orgAuthCtx.profile, orgUuid);
|
|
458
|
+
if (cached) {
|
|
459
|
+
apiClient.setOrgSession(cached, orgUuid);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
314
463
|
try {
|
|
315
464
|
const orgSession = await tryDirectJoin(apiUrl, accessToken, orgAuthInfo);
|
|
316
465
|
if (orgSession !== null) {
|
|
317
|
-
|
|
466
|
+
adopt(orgSession);
|
|
318
467
|
return;
|
|
319
468
|
}
|
|
320
469
|
} catch (err) {
|
|
321
470
|
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
-
stderr(`Warning: org direct-join failed (${msg}); trying browser
|
|
471
|
+
stderr(`Warning: org direct-join failed (${msg}); trying browser sign-in`);
|
|
323
472
|
}
|
|
324
|
-
if (!
|
|
473
|
+
if (!orgAuthCtx?.interactive || isHeadless()) {
|
|
325
474
|
throw new Error(
|
|
326
|
-
|
|
475
|
+
"This workspace\u2019s organization requires SSO sign-in, which needs a browser.\nRun an interactive `gr` command on a machine with a browser to sign in once (the session is then cached), or use an API key for non-interactive use:\n export GRAVITY_RAIL_API_KEY=<key> (see: gr api-keys)"
|
|
327
476
|
);
|
|
328
477
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
stderr(` ${orgAuthInfo.authUrl}
|
|
333
|
-
`);
|
|
334
|
-
stderr(`If the browser does not open, visit the URL above manually.`);
|
|
335
|
-
stderr(
|
|
336
|
-
`After signing in, re-run the command \u2014 the CLI cannot receive the SSO session directly.`
|
|
337
|
-
);
|
|
338
|
-
openBrowser(orgAuthInfo.authUrl);
|
|
339
|
-
await new Promise((_, reject) => {
|
|
340
|
-
const t = setTimeout(
|
|
341
|
-
() => reject(
|
|
342
|
-
new Error(
|
|
343
|
-
"Organization authentication timed out.\nPlease complete authentication in the browser, then re-run the command."
|
|
344
|
-
)
|
|
345
|
-
),
|
|
346
|
-
ORG_AUTH_TIMEOUT_MS
|
|
478
|
+
if (!isAllowedAuthUrl(orgAuthInfo.authUrl, apiUrl, orgAuthCtx.frontendUrl)) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`Refusing to open organization auth URL: host does not match the configured API/frontend host. Re-run with the expected --api-url, or contact support.`
|
|
347
481
|
);
|
|
348
|
-
t.unref?.();
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// src/lib/pinned-fetch.ts
|
|
354
|
-
import { Agent, fetch as undiciFetch } from "undici";
|
|
355
|
-
function createPinnedFetch(apiUrl, apiIp) {
|
|
356
|
-
if (!apiIp) return fetch;
|
|
357
|
-
let parsed;
|
|
358
|
-
try {
|
|
359
|
-
parsed = new URL(apiUrl);
|
|
360
|
-
} catch {
|
|
361
|
-
return fetch;
|
|
362
|
-
}
|
|
363
|
-
const hostname = parsed.hostname;
|
|
364
|
-
const dispatcher = new Agent({
|
|
365
|
-
connect: {
|
|
366
|
-
servername: hostname
|
|
367
482
|
}
|
|
483
|
+
const session = await stepUpViaBrowser(orgAuthInfo);
|
|
484
|
+
adopt(session);
|
|
368
485
|
});
|
|
369
|
-
return async (input, init) => {
|
|
370
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
371
|
-
let reqUrl;
|
|
372
|
-
try {
|
|
373
|
-
reqUrl = new URL(url, apiUrl);
|
|
374
|
-
} catch {
|
|
375
|
-
return fetch(input, init);
|
|
376
|
-
}
|
|
377
|
-
const headers = new Headers(init?.headers);
|
|
378
|
-
if (reqUrl.hostname === hostname) {
|
|
379
|
-
headers.set("Host", hostname);
|
|
380
|
-
reqUrl = new URL(reqUrl.href);
|
|
381
|
-
reqUrl.hostname = apiIp;
|
|
382
|
-
}
|
|
383
|
-
return undiciFetch(reqUrl, {
|
|
384
|
-
...init,
|
|
385
|
-
headers,
|
|
386
|
-
dispatcher
|
|
387
|
-
});
|
|
388
|
-
};
|
|
389
486
|
}
|
|
487
|
+
|
|
488
|
+
// src/lib/sandbox-fetch.ts
|
|
390
489
|
function withAuditHeaders(fetchFn, headers) {
|
|
391
490
|
const { chatId, sessionId, toolCallId } = headers;
|
|
392
491
|
return async (input, init) => {
|
|
@@ -404,7 +503,7 @@ function withAuditHeaders(fetchFn, headers) {
|
|
|
404
503
|
return fetchFn(input, { ...init, headers: reqHeaders });
|
|
405
504
|
};
|
|
406
505
|
}
|
|
407
|
-
function
|
|
506
|
+
function createSandboxFetchFromEnv() {
|
|
408
507
|
const env = typeof process !== "undefined" ? process.env : {};
|
|
409
508
|
const chatIdRaw = env.GRAVITY_RAIL_CHAT_ID;
|
|
410
509
|
const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
|
|
@@ -414,30 +513,42 @@ function createBrokerFetchFromEnv(_brokerUrl) {
|
|
|
414
513
|
toolCallId: env.GRAVITY_RAIL_TOOL_CALL_ID
|
|
415
514
|
});
|
|
416
515
|
}
|
|
417
|
-
function createSandboxFetchFromEnv(apiUrl) {
|
|
418
|
-
const env = typeof process !== "undefined" ? process.env : {};
|
|
419
|
-
const apiIp = env.GRAVITY_RAIL_API_IP;
|
|
420
|
-
let fetchFn = createPinnedFetch(apiUrl, apiIp);
|
|
421
|
-
const chatIdRaw = env.GRAVITY_RAIL_CHAT_ID;
|
|
422
|
-
const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
|
|
423
|
-
return withAuditHeaders(fetchFn, {
|
|
424
|
-
chatId: chatId !== void 0 && Number.isFinite(chatId) ? chatId : void 0,
|
|
425
|
-
sessionId: env.GRAVITY_RAIL_SESSION_ID,
|
|
426
|
-
toolCallId: env.GRAVITY_RAIL_TOOL_CALL_ID
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
516
|
|
|
430
517
|
// src/lib/context.ts
|
|
431
|
-
function makeApiClient(accessToken, apiUrl, fetchFn) {
|
|
432
|
-
const resolvedFetch = fetchFn ?? createSandboxFetchFromEnv(
|
|
518
|
+
function makeApiClient(accessToken, apiUrl, fetchFn, orgAuthCtx) {
|
|
519
|
+
const resolvedFetch = fetchFn ?? createSandboxFetchFromEnv();
|
|
433
520
|
const apiClient = new GravityRailClient(accessToken, apiUrl, resolvedFetch);
|
|
434
|
-
registerOrgAuthHandler(apiClient, apiUrl, accessToken ?? "");
|
|
521
|
+
registerOrgAuthHandler(apiClient, apiUrl, accessToken ?? "", orgAuthCtx);
|
|
435
522
|
return apiClient;
|
|
436
523
|
}
|
|
437
|
-
function
|
|
524
|
+
function interactiveOrgAuth(ctx) {
|
|
525
|
+
return {
|
|
526
|
+
credentialsManager: ctx.credentialsManager,
|
|
527
|
+
env: ctx.env,
|
|
528
|
+
profile: ctx.profile,
|
|
529
|
+
frontendUrl: ctx.envConfig.frontendUrl,
|
|
530
|
+
interactive: true
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function isLoopbackBrokerUrl(rawUrl) {
|
|
534
|
+
try {
|
|
535
|
+
const { hostname, protocol } = new URL(rawUrl);
|
|
536
|
+
if (protocol !== "http:" && protocol !== "https:") return false;
|
|
537
|
+
const host = hostname.replace(/^\[|\]$/g, "");
|
|
538
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function sandboxBrokerUrl() {
|
|
438
544
|
const brokerUrl = process.env.GRAVITY_RAIL_BROKER_URL;
|
|
545
|
+
if (!brokerUrl || !isLoopbackBrokerUrl(brokerUrl)) return null;
|
|
546
|
+
return brokerUrl;
|
|
547
|
+
}
|
|
548
|
+
function tryBrokerContext(ctx) {
|
|
549
|
+
const brokerUrl = sandboxBrokerUrl();
|
|
439
550
|
if (!brokerUrl) return null;
|
|
440
|
-
const apiClient = makeApiClient(void 0, brokerUrl,
|
|
551
|
+
const apiClient = makeApiClient(void 0, brokerUrl, createSandboxFetchFromEnv());
|
|
441
552
|
return { ...ctx, accessToken: "", apiClient };
|
|
442
553
|
}
|
|
443
554
|
async function tryTokenRefresh(ctx, creds) {
|
|
@@ -450,7 +561,12 @@ async function tryTokenRefresh(ctx, creds) {
|
|
|
450
561
|
ctx.profile
|
|
451
562
|
);
|
|
452
563
|
if (!refreshed?.accessToken) return null;
|
|
453
|
-
const apiClient = makeApiClient(
|
|
564
|
+
const apiClient = makeApiClient(
|
|
565
|
+
refreshed.accessToken,
|
|
566
|
+
ctx.envConfig.apiUrl,
|
|
567
|
+
void 0,
|
|
568
|
+
interactiveOrgAuth(ctx)
|
|
569
|
+
);
|
|
454
570
|
return {
|
|
455
571
|
...ctx,
|
|
456
572
|
accessToken: refreshed.accessToken,
|
|
@@ -488,7 +604,12 @@ async function createAuthenticatedContext(opts) {
|
|
|
488
604
|
const buffer = 5 * 60 * 1e3;
|
|
489
605
|
const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
|
|
490
606
|
if (notExpired) {
|
|
491
|
-
const apiClient = makeApiClient(
|
|
607
|
+
const apiClient = makeApiClient(
|
|
608
|
+
creds.accessToken,
|
|
609
|
+
ctx.envConfig.apiUrl,
|
|
610
|
+
void 0,
|
|
611
|
+
interactiveOrgAuth(ctx)
|
|
612
|
+
);
|
|
492
613
|
return {
|
|
493
614
|
...ctx,
|
|
494
615
|
accessToken: creds.accessToken,
|
|
@@ -534,6 +655,8 @@ function createPublicContext(opts) {
|
|
|
534
655
|
}
|
|
535
656
|
async function createPublicAuthenticatedContext(opts) {
|
|
536
657
|
const ctx = createPublicContext(opts);
|
|
658
|
+
const brokerCtx = tryBrokerContext(ctx);
|
|
659
|
+
if (brokerCtx) return brokerCtx;
|
|
537
660
|
const apiKey = process.env.GRAVITY_RAIL_API_KEY;
|
|
538
661
|
if (apiKey) {
|
|
539
662
|
const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
|
|
@@ -544,7 +667,12 @@ async function createPublicAuthenticatedContext(opts) {
|
|
|
544
667
|
const buffer = 5 * 60 * 1e3;
|
|
545
668
|
const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
|
|
546
669
|
if (notExpired) {
|
|
547
|
-
const apiClient = makeApiClient(
|
|
670
|
+
const apiClient = makeApiClient(
|
|
671
|
+
creds.accessToken,
|
|
672
|
+
ctx.envConfig.apiUrl,
|
|
673
|
+
void 0,
|
|
674
|
+
interactiveOrgAuth(ctx)
|
|
675
|
+
);
|
|
548
676
|
return {
|
|
549
677
|
...ctx,
|
|
550
678
|
accessToken: creds.accessToken,
|
|
@@ -692,8 +820,10 @@ export {
|
|
|
692
820
|
outputData,
|
|
693
821
|
outputSingle,
|
|
694
822
|
COL,
|
|
823
|
+
hint,
|
|
695
824
|
stderr,
|
|
696
825
|
createSandboxFetchFromEnv,
|
|
826
|
+
sandboxBrokerUrl,
|
|
697
827
|
createContext,
|
|
698
828
|
createAuthenticatedContext,
|
|
699
829
|
requireAuth,
|
|
@@ -708,4 +838,4 @@ export {
|
|
|
708
838
|
confirmAccountPhoneVerify,
|
|
709
839
|
getConciergeChatMessages
|
|
710
840
|
};
|
|
711
|
-
//# sourceMappingURL=chunk-
|
|
841
|
+
//# sourceMappingURL=chunk-EITDLX75.js.map
|