@gravity-rail/cli 0.13.1 → 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 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-LE5IR5MC.js";
6
- import "./chunk-XSFAJ5ZO.js";
7
- import "./chunk-WZVMWN3S.js";
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-N7EJS4J3.js.map
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
- Re-run the exec tool with the needed write scope, e.g.:
43
- exec(code="<your gr command>", language="bash", scopes=["datatypes:write"])
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-WZVMWN3S.js.map
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-XSFAJ5ZO.js";
6
+ } from "./chunk-PODEUTUQ.js";
7
7
  import {
8
8
  AuthRequiredError,
9
9
  CLIError
10
- } from "./chunk-WZVMWN3S.js";
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
- if (parsed.hostname === api.hostname) return true;
285
- const lastTwo = (host) => {
286
- const parts = host.split(".").filter(Boolean);
287
- return parts.length >= 2 ? parts.slice(-2).join(".") : host;
288
- };
289
- const isIp = /^[0-9.]+$|^\[.*\]$/.test(parsed.hostname);
290
- if (isIp) return false;
291
- return lastTwo(parsed.hostname) === lastTwo(api.hostname);
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 registerOrgAuthHandler(apiClient, apiUrl, accessToken) {
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
- apiClient.setOrgSession(orgSession, orgAuthInfo.orgUuid);
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 auth`);
467
+ stderr(`Warning: org direct-join failed (${msg}); trying browser sign-in`);
323
468
  }
324
- if (!isAllowedAuthUrl(orgAuthInfo.authUrl, apiUrl)) {
469
+ if (!orgAuthCtx?.interactive || isHeadless()) {
325
470
  throw new Error(
326
- `Refusing to open organization auth URL: scheme/host does not match the configured API host. Re-run with the expected --api-url, or contact support.`
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
- stderr(`
330
- This workspace requires organization SSO authentication.`);
331
- stderr(`Opening browser to complete authentication...`);
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,7 +499,7 @@ function withAuditHeaders(fetchFn, headers) {
404
499
  return fetchFn(input, { ...init, headers: reqHeaders });
405
500
  };
406
501
  }
407
- function createBrokerFetchFromEnv(_brokerUrl) {
502
+ function createSandboxFetchFromEnv() {
408
503
  const env = typeof process !== "undefined" ? process.env : {};
409
504
  const chatIdRaw = env.GRAVITY_RAIL_CHAT_ID;
410
505
  const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
@@ -414,30 +509,42 @@ function createBrokerFetchFromEnv(_brokerUrl) {
414
509
  toolCallId: env.GRAVITY_RAIL_TOOL_CALL_ID
415
510
  });
416
511
  }
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
512
 
430
513
  // src/lib/context.ts
431
- function makeApiClient(accessToken, apiUrl, fetchFn) {
432
- const resolvedFetch = fetchFn ?? createSandboxFetchFromEnv(apiUrl);
514
+ function makeApiClient(accessToken, apiUrl, fetchFn, orgAuthCtx) {
515
+ const resolvedFetch = fetchFn ?? createSandboxFetchFromEnv();
433
516
  const apiClient = new GravityRailClient(accessToken, apiUrl, resolvedFetch);
434
- registerOrgAuthHandler(apiClient, apiUrl, accessToken ?? "");
517
+ registerOrgAuthHandler(apiClient, apiUrl, accessToken ?? "", orgAuthCtx);
435
518
  return apiClient;
436
519
  }
437
- function tryBrokerContext(ctx) {
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() {
438
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();
439
546
  if (!brokerUrl) return null;
440
- const apiClient = makeApiClient(void 0, brokerUrl, createBrokerFetchFromEnv(brokerUrl));
547
+ const apiClient = makeApiClient(void 0, brokerUrl, createSandboxFetchFromEnv());
441
548
  return { ...ctx, accessToken: "", apiClient };
442
549
  }
443
550
  async function tryTokenRefresh(ctx, creds) {
@@ -450,7 +557,12 @@ async function tryTokenRefresh(ctx, creds) {
450
557
  ctx.profile
451
558
  );
452
559
  if (!refreshed?.accessToken) return null;
453
- const apiClient = makeApiClient(refreshed.accessToken, ctx.envConfig.apiUrl);
560
+ const apiClient = makeApiClient(
561
+ refreshed.accessToken,
562
+ ctx.envConfig.apiUrl,
563
+ void 0,
564
+ interactiveOrgAuth(ctx)
565
+ );
454
566
  return {
455
567
  ...ctx,
456
568
  accessToken: refreshed.accessToken,
@@ -488,7 +600,12 @@ async function createAuthenticatedContext(opts) {
488
600
  const buffer = 5 * 60 * 1e3;
489
601
  const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
490
602
  if (notExpired) {
491
- const apiClient = makeApiClient(creds.accessToken, ctx.envConfig.apiUrl);
603
+ const apiClient = makeApiClient(
604
+ creds.accessToken,
605
+ ctx.envConfig.apiUrl,
606
+ void 0,
607
+ interactiveOrgAuth(ctx)
608
+ );
492
609
  return {
493
610
  ...ctx,
494
611
  accessToken: creds.accessToken,
@@ -534,6 +651,8 @@ function createPublicContext(opts) {
534
651
  }
535
652
  async function createPublicAuthenticatedContext(opts) {
536
653
  const ctx = createPublicContext(opts);
654
+ const brokerCtx = tryBrokerContext(ctx);
655
+ if (brokerCtx) return brokerCtx;
537
656
  const apiKey = process.env.GRAVITY_RAIL_API_KEY;
538
657
  if (apiKey) {
539
658
  const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
@@ -544,7 +663,12 @@ async function createPublicAuthenticatedContext(opts) {
544
663
  const buffer = 5 * 60 * 1e3;
545
664
  const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
546
665
  if (notExpired) {
547
- const apiClient = makeApiClient(creds.accessToken, ctx.envConfig.apiUrl);
666
+ const apiClient = makeApiClient(
667
+ creds.accessToken,
668
+ ctx.envConfig.apiUrl,
669
+ void 0,
670
+ interactiveOrgAuth(ctx)
671
+ );
548
672
  return {
549
673
  ...ctx,
550
674
  accessToken: creds.accessToken,
@@ -694,6 +818,7 @@ export {
694
818
  COL,
695
819
  stderr,
696
820
  createSandboxFetchFromEnv,
821
+ sandboxBrokerUrl,
697
822
  createContext,
698
823
  createAuthenticatedContext,
699
824
  requireAuth,
@@ -708,4 +833,4 @@ export {
708
833
  confirmAccountPhoneVerify,
709
834
  getConciergeChatMessages
710
835
  };
711
- //# sourceMappingURL=chunk-LE5IR5MC.js.map
836
+ //# sourceMappingURL=chunk-M4XMXS5D.js.map