@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 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-GRLUFHMI.js";
6
- import "./chunk-SIV2WU2I.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-7YQXKU3X.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-SIV2WU2I.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,13 +499,11 @@ function withAuditHeaders(fetchFn, headers) {
404
499
  return fetchFn(input, { ...init, headers: reqHeaders });
405
500
  };
406
501
  }
407
- function createSandboxFetchFromEnv(apiUrl) {
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(fetchFn, {
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 fetchFn = createSandboxFetchFromEnv(apiUrl);
423
- const apiClient = new GravityRailClient(accessToken, apiUrl, fetchFn);
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(refreshed.accessToken, ctx.envConfig.apiUrl);
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(creds.accessToken, ctx.envConfig.apiUrl);
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(creds.accessToken, ctx.envConfig.apiUrl);
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-GRLUFHMI.js.map
836
+ //# sourceMappingURL=chunk-M4XMXS5D.js.map