@gravity-rail/cli 0.13.0 → 0.14.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-7YQXKU3X.js → account-verify-phone-UA5OUOOR.js} +4 -4
- package/dist/{chunk-WZVMWN3S.js → chunk-3T7A5PSF.js} +3 -4
- package/dist/chunk-3T7A5PSF.js.map +1 -0
- package/dist/{chunk-GRLUFHMI.js → chunk-M4XMXS5D.js} +229 -86
- package/dist/chunk-M4XMXS5D.js.map +1 -0
- package/dist/{chunk-SIV2WU2I.js → chunk-PODEUTUQ.js} +158 -21
- 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 +30 -3
- package/dist/cli-utils.js +3 -1
- package/dist/{errors-YHGVIDPO.js → errors-CFRLSAGA.js} +2 -2
- package/dist/gr.js +1306 -18591
- package/dist/gr.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +617 -51
- package/dist/lib.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-GRLUFHMI.js.map +0 -1
- package/dist/chunk-SIV2WU2I.js.map +0 -1
- package/dist/chunk-WZVMWN3S.js.map +0 -1
- /package/dist/{account-verify-phone-7YQXKU3X.js.map → account-verify-phone-UA5OUOOR.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-M4XMXS5D.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-UA5OUOOR.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) {
|
|
@@ -232,8 +232,13 @@ import { GravityRailClient } from "@gravity-rail/sdk";
|
|
|
232
232
|
import { homedir } from "os";
|
|
233
233
|
|
|
234
234
|
// src/lib/org-auth.ts
|
|
235
|
+
import { createServer } from "http";
|
|
235
236
|
import { execFile } from "child_process";
|
|
237
|
+
import { randomBytes } from "crypto";
|
|
236
238
|
var ORG_AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
239
|
+
var ORG_SESSION_CACHE_TTL_MS = 6.5 * 24 * 60 * 60 * 1e3;
|
|
240
|
+
var CLI_ORG_AUTH_BRIDGE_PATH = "/cli/org-auth";
|
|
241
|
+
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
242
|
function extractOrgSessionFromSetCookies(setCookieValues, preferredCookieName) {
|
|
238
243
|
let fallback;
|
|
239
244
|
for (const value of setCookieValues) {
|
|
@@ -276,19 +281,18 @@ async function tryDirectJoin(apiUrl, accessToken, orgAuthInfo) {
|
|
|
276
281
|
}
|
|
277
282
|
return orgSession;
|
|
278
283
|
}
|
|
279
|
-
function isAllowedAuthUrl(url, apiUrl) {
|
|
284
|
+
function isAllowedAuthUrl(url, apiUrl, frontendUrl) {
|
|
280
285
|
try {
|
|
281
286
|
const parsed = new URL(url);
|
|
282
|
-
const api = new URL(apiUrl);
|
|
283
287
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return
|
|
288
|
+
const allowedHosts = [apiUrl, frontendUrl].filter((u) => Boolean(u)).map((u) => {
|
|
289
|
+
try {
|
|
290
|
+
return new URL(u).hostname;
|
|
291
|
+
} catch {
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
}).filter((h) => Boolean(h));
|
|
295
|
+
return allowedHosts.includes(parsed.hostname);
|
|
292
296
|
} catch {
|
|
293
297
|
return false;
|
|
294
298
|
}
|
|
@@ -309,84 +313,175 @@ function openBrowser(url) {
|
|
|
309
313
|
execFile(command, args, (_err) => {
|
|
310
314
|
});
|
|
311
315
|
}
|
|
312
|
-
function
|
|
316
|
+
function isHeadless() {
|
|
317
|
+
if (process.env.GRAVITY_RAIL_NO_BROWSER) return true;
|
|
318
|
+
if (process.platform === "linux") {
|
|
319
|
+
return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
function startOrgSessionCapture(expectedState, expectedOrgUuid, allowedOrigin) {
|
|
324
|
+
let resolveSession;
|
|
325
|
+
let rejectSession;
|
|
326
|
+
const session = new Promise((resolve, reject) => {
|
|
327
|
+
resolveSession = resolve;
|
|
328
|
+
rejectSession = reject;
|
|
329
|
+
});
|
|
330
|
+
const applyCors = (req, res) => {
|
|
331
|
+
if (req.headers.origin && req.headers.origin === allowedOrigin) {
|
|
332
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
333
|
+
res.setHeader("Vary", "Origin");
|
|
334
|
+
}
|
|
335
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
336
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
337
|
+
};
|
|
338
|
+
const server = createServer((req, res) => {
|
|
339
|
+
applyCors(req, res);
|
|
340
|
+
if (req.method === "OPTIONS") {
|
|
341
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
342
|
+
res.writeHead(204);
|
|
343
|
+
res.end();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (req.method !== "POST") {
|
|
347
|
+
res.writeHead(405);
|
|
348
|
+
res.end();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
let body = "";
|
|
352
|
+
let tooLarge = false;
|
|
353
|
+
req.on("data", (chunk) => {
|
|
354
|
+
body += chunk;
|
|
355
|
+
if (body.length > 16 * 1024) {
|
|
356
|
+
tooLarge = true;
|
|
357
|
+
req.destroy();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
req.on("end", () => {
|
|
361
|
+
if (tooLarge) return;
|
|
362
|
+
let capturedSession;
|
|
363
|
+
try {
|
|
364
|
+
const payload = JSON.parse(body);
|
|
365
|
+
if (typeof payload.org_session === "string" && payload.org_session.length > 0 && payload.state === expectedState && payload.org_uuid === expectedOrgUuid) {
|
|
366
|
+
capturedSession = payload.org_session;
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
if (capturedSession) {
|
|
371
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
372
|
+
res.end('{"ok":true}');
|
|
373
|
+
resolveSession(capturedSession);
|
|
374
|
+
} else {
|
|
375
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
376
|
+
res.end('{"ok":false}');
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
let resolveReady;
|
|
381
|
+
let rejectReady;
|
|
382
|
+
const ready = new Promise((resolve, reject) => {
|
|
383
|
+
resolveReady = resolve;
|
|
384
|
+
rejectReady = reject;
|
|
385
|
+
});
|
|
386
|
+
server.on("error", (err) => {
|
|
387
|
+
rejectReady(err);
|
|
388
|
+
rejectSession(err);
|
|
389
|
+
});
|
|
390
|
+
server.listen(0, "127.0.0.1", () => {
|
|
391
|
+
resolveReady(server.address().port);
|
|
392
|
+
});
|
|
393
|
+
const timeout = setTimeout(() => {
|
|
394
|
+
rejectSession(
|
|
395
|
+
new Error(
|
|
396
|
+
"Organization authentication timed out.\nComplete sign-in in the browser, then re-run the command."
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
}, ORG_AUTH_TIMEOUT_MS);
|
|
400
|
+
timeout.unref();
|
|
401
|
+
let closed = false;
|
|
402
|
+
const close = () => {
|
|
403
|
+
if (closed) return;
|
|
404
|
+
closed = true;
|
|
405
|
+
clearTimeout(timeout);
|
|
406
|
+
server.close();
|
|
407
|
+
};
|
|
408
|
+
return { ready, session, close };
|
|
409
|
+
}
|
|
410
|
+
function buildStepUpUrl(authUrl, orgUuid, port, state) {
|
|
411
|
+
const url = new URL(authUrl);
|
|
412
|
+
const bridgeQuery = new URLSearchParams({ org: orgUuid, port: String(port), state });
|
|
413
|
+
url.searchParams.set("return_to", `${CLI_ORG_AUTH_BRIDGE_PATH}?${bridgeQuery.toString()}`);
|
|
414
|
+
return url.toString();
|
|
415
|
+
}
|
|
416
|
+
function newStateNonce() {
|
|
417
|
+
return randomBytes(16).toString("hex");
|
|
418
|
+
}
|
|
419
|
+
async function stepUpViaBrowser(orgAuthInfo) {
|
|
420
|
+
const state = newStateNonce();
|
|
421
|
+
const allowedOrigin = new URL(orgAuthInfo.authUrl).origin;
|
|
422
|
+
const capture = startOrgSessionCapture(state, orgAuthInfo.orgUuid, allowedOrigin);
|
|
423
|
+
try {
|
|
424
|
+
const port = await capture.ready;
|
|
425
|
+
const stepUpUrl = buildStepUpUrl(orgAuthInfo.authUrl, orgAuthInfo.orgUuid, port, state);
|
|
426
|
+
stderr(`
|
|
427
|
+
This workspace requires organization SSO authentication.`);
|
|
428
|
+
stderr(`Opening browser to complete sign-in...`);
|
|
429
|
+
stderr(` ${stepUpUrl}
|
|
430
|
+
`);
|
|
431
|
+
stderr(`If the browser does not open, visit the URL above manually.`);
|
|
432
|
+
openBrowser(stepUpUrl);
|
|
433
|
+
return await capture.session;
|
|
434
|
+
} finally {
|
|
435
|
+
capture.close();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function registerOrgAuthHandler(apiClient, apiUrl, accessToken, orgAuthCtx) {
|
|
313
439
|
apiClient.setOrgAuthHandler(async (orgAuthInfo) => {
|
|
440
|
+
const orgUuid = orgAuthInfo.orgUuid;
|
|
441
|
+
if (!ORG_UUID_RE.test(orgUuid)) {
|
|
442
|
+
throw new Error("Invalid organization id in auth challenge");
|
|
443
|
+
}
|
|
444
|
+
const cm = orgAuthCtx?.credentialsManager;
|
|
445
|
+
const adopt = (session2) => {
|
|
446
|
+
if (cm && orgAuthCtx) {
|
|
447
|
+
const expiresAt = new Date(Date.now() + ORG_SESSION_CACHE_TTL_MS).toISOString();
|
|
448
|
+
cm.setOrgSession(orgAuthCtx.env, orgAuthCtx.profile, orgUuid, session2, expiresAt);
|
|
449
|
+
}
|
|
450
|
+
apiClient.setOrgSession(session2, orgUuid);
|
|
451
|
+
};
|
|
452
|
+
if (cm && orgAuthCtx) {
|
|
453
|
+
const cached = cm.getOrgSession(orgAuthCtx.env, orgAuthCtx.profile, orgUuid);
|
|
454
|
+
if (cached) {
|
|
455
|
+
apiClient.setOrgSession(cached, orgUuid);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
314
459
|
try {
|
|
315
460
|
const orgSession = await tryDirectJoin(apiUrl, accessToken, orgAuthInfo);
|
|
316
461
|
if (orgSession !== null) {
|
|
317
|
-
|
|
462
|
+
adopt(orgSession);
|
|
318
463
|
return;
|
|
319
464
|
}
|
|
320
465
|
} catch (err) {
|
|
321
466
|
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
-
stderr(`Warning: org direct-join failed (${msg}); trying browser
|
|
467
|
+
stderr(`Warning: org direct-join failed (${msg}); trying browser sign-in`);
|
|
323
468
|
}
|
|
324
|
-
if (!
|
|
469
|
+
if (!orgAuthCtx?.interactive || isHeadless()) {
|
|
325
470
|
throw new Error(
|
|
326
|
-
|
|
471
|
+
"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
472
|
);
|
|
328
473
|
}
|
|
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
|
|
474
|
+
if (!isAllowedAuthUrl(orgAuthInfo.authUrl, apiUrl, orgAuthCtx.frontendUrl)) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`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
477
|
);
|
|
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
478
|
}
|
|
479
|
+
const session = await stepUpViaBrowser(orgAuthInfo);
|
|
480
|
+
adopt(session);
|
|
368
481
|
});
|
|
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
482
|
}
|
|
483
|
+
|
|
484
|
+
// src/lib/sandbox-fetch.ts
|
|
390
485
|
function withAuditHeaders(fetchFn, headers) {
|
|
391
486
|
const { chatId, sessionId, toolCallId } = headers;
|
|
392
487
|
return async (input, init) => {
|
|
@@ -404,13 +499,11 @@ function withAuditHeaders(fetchFn, headers) {
|
|
|
404
499
|
return fetchFn(input, { ...init, headers: reqHeaders });
|
|
405
500
|
};
|
|
406
501
|
}
|
|
407
|
-
function createSandboxFetchFromEnv(
|
|
502
|
+
function createSandboxFetchFromEnv() {
|
|
408
503
|
const env = typeof process !== "undefined" ? process.env : {};
|
|
409
|
-
const apiIp = env.GRAVITY_RAIL_API_IP;
|
|
410
|
-
let fetchFn = createPinnedFetch(apiUrl, apiIp);
|
|
411
504
|
const chatIdRaw = env.GRAVITY_RAIL_CHAT_ID;
|
|
412
505
|
const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
|
|
413
|
-
return withAuditHeaders(
|
|
506
|
+
return withAuditHeaders(fetch, {
|
|
414
507
|
chatId: chatId !== void 0 && Number.isFinite(chatId) ? chatId : void 0,
|
|
415
508
|
sessionId: env.GRAVITY_RAIL_SESSION_ID,
|
|
416
509
|
toolCallId: env.GRAVITY_RAIL_TOOL_CALL_ID
|
|
@@ -418,12 +511,42 @@ function createSandboxFetchFromEnv(apiUrl) {
|
|
|
418
511
|
}
|
|
419
512
|
|
|
420
513
|
// src/lib/context.ts
|
|
421
|
-
function makeApiClient(accessToken, apiUrl) {
|
|
422
|
-
const
|
|
423
|
-
const apiClient = new GravityRailClient(accessToken, apiUrl,
|
|
424
|
-
registerOrgAuthHandler(apiClient, apiUrl, accessToken);
|
|
514
|
+
function makeApiClient(accessToken, apiUrl, fetchFn, orgAuthCtx) {
|
|
515
|
+
const resolvedFetch = fetchFn ?? createSandboxFetchFromEnv();
|
|
516
|
+
const apiClient = new GravityRailClient(accessToken, apiUrl, resolvedFetch);
|
|
517
|
+
registerOrgAuthHandler(apiClient, apiUrl, accessToken ?? "", orgAuthCtx);
|
|
425
518
|
return apiClient;
|
|
426
519
|
}
|
|
520
|
+
function interactiveOrgAuth(ctx) {
|
|
521
|
+
return {
|
|
522
|
+
credentialsManager: ctx.credentialsManager,
|
|
523
|
+
env: ctx.env,
|
|
524
|
+
profile: ctx.profile,
|
|
525
|
+
frontendUrl: ctx.envConfig.frontendUrl,
|
|
526
|
+
interactive: true
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function isLoopbackBrokerUrl(rawUrl) {
|
|
530
|
+
try {
|
|
531
|
+
const { hostname, protocol } = new URL(rawUrl);
|
|
532
|
+
if (protocol !== "http:" && protocol !== "https:") return false;
|
|
533
|
+
const host = hostname.replace(/^\[|\]$/g, "");
|
|
534
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
535
|
+
} catch {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function sandboxBrokerUrl() {
|
|
540
|
+
const brokerUrl = process.env.GRAVITY_RAIL_BROKER_URL;
|
|
541
|
+
if (!brokerUrl || !isLoopbackBrokerUrl(brokerUrl)) return null;
|
|
542
|
+
return brokerUrl;
|
|
543
|
+
}
|
|
544
|
+
function tryBrokerContext(ctx) {
|
|
545
|
+
const brokerUrl = sandboxBrokerUrl();
|
|
546
|
+
if (!brokerUrl) return null;
|
|
547
|
+
const apiClient = makeApiClient(void 0, brokerUrl, createSandboxFetchFromEnv());
|
|
548
|
+
return { ...ctx, accessToken: "", apiClient };
|
|
549
|
+
}
|
|
427
550
|
async function tryTokenRefresh(ctx, creds) {
|
|
428
551
|
if (!creds.refreshToken) return null;
|
|
429
552
|
const refreshed = await refreshAccessToken(
|
|
@@ -434,7 +557,12 @@ async function tryTokenRefresh(ctx, creds) {
|
|
|
434
557
|
ctx.profile
|
|
435
558
|
);
|
|
436
559
|
if (!refreshed?.accessToken) return null;
|
|
437
|
-
const apiClient = makeApiClient(
|
|
560
|
+
const apiClient = makeApiClient(
|
|
561
|
+
refreshed.accessToken,
|
|
562
|
+
ctx.envConfig.apiUrl,
|
|
563
|
+
void 0,
|
|
564
|
+
interactiveOrgAuth(ctx)
|
|
565
|
+
);
|
|
438
566
|
return {
|
|
439
567
|
...ctx,
|
|
440
568
|
accessToken: refreshed.accessToken,
|
|
@@ -460,6 +588,8 @@ function createContext(opts) {
|
|
|
460
588
|
}
|
|
461
589
|
async function createAuthenticatedContext(opts) {
|
|
462
590
|
const ctx = createContext(opts);
|
|
591
|
+
const brokerCtx = tryBrokerContext(ctx);
|
|
592
|
+
if (brokerCtx) return brokerCtx;
|
|
463
593
|
const apiKey = process.env.GRAVITY_RAIL_API_KEY;
|
|
464
594
|
if (apiKey) {
|
|
465
595
|
const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
|
|
@@ -470,7 +600,12 @@ async function createAuthenticatedContext(opts) {
|
|
|
470
600
|
const buffer = 5 * 60 * 1e3;
|
|
471
601
|
const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
|
|
472
602
|
if (notExpired) {
|
|
473
|
-
const apiClient = makeApiClient(
|
|
603
|
+
const apiClient = makeApiClient(
|
|
604
|
+
creds.accessToken,
|
|
605
|
+
ctx.envConfig.apiUrl,
|
|
606
|
+
void 0,
|
|
607
|
+
interactiveOrgAuth(ctx)
|
|
608
|
+
);
|
|
474
609
|
return {
|
|
475
610
|
...ctx,
|
|
476
611
|
accessToken: creds.accessToken,
|
|
@@ -516,6 +651,8 @@ function createPublicContext(opts) {
|
|
|
516
651
|
}
|
|
517
652
|
async function createPublicAuthenticatedContext(opts) {
|
|
518
653
|
const ctx = createPublicContext(opts);
|
|
654
|
+
const brokerCtx = tryBrokerContext(ctx);
|
|
655
|
+
if (brokerCtx) return brokerCtx;
|
|
519
656
|
const apiKey = process.env.GRAVITY_RAIL_API_KEY;
|
|
520
657
|
if (apiKey) {
|
|
521
658
|
const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
|
|
@@ -526,7 +663,12 @@ async function createPublicAuthenticatedContext(opts) {
|
|
|
526
663
|
const buffer = 5 * 60 * 1e3;
|
|
527
664
|
const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
|
|
528
665
|
if (notExpired) {
|
|
529
|
-
const apiClient = makeApiClient(
|
|
666
|
+
const apiClient = makeApiClient(
|
|
667
|
+
creds.accessToken,
|
|
668
|
+
ctx.envConfig.apiUrl,
|
|
669
|
+
void 0,
|
|
670
|
+
interactiveOrgAuth(ctx)
|
|
671
|
+
);
|
|
530
672
|
return {
|
|
531
673
|
...ctx,
|
|
532
674
|
accessToken: creds.accessToken,
|
|
@@ -676,6 +818,7 @@ export {
|
|
|
676
818
|
COL,
|
|
677
819
|
stderr,
|
|
678
820
|
createSandboxFetchFromEnv,
|
|
821
|
+
sandboxBrokerUrl,
|
|
679
822
|
createContext,
|
|
680
823
|
createAuthenticatedContext,
|
|
681
824
|
requireAuth,
|
|
@@ -690,4 +833,4 @@ export {
|
|
|
690
833
|
confirmAccountPhoneVerify,
|
|
691
834
|
getConciergeChatMessages
|
|
692
835
|
};
|
|
693
|
-
//# sourceMappingURL=chunk-
|
|
836
|
+
//# sourceMappingURL=chunk-M4XMXS5D.js.map
|