@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 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-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-N7EJS4J3.js.map
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
- 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) {
@@ -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
- 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);
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 registerOrgAuthHandler(apiClient, apiUrl, accessToken) {
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
- apiClient.setOrgSession(orgSession, orgAuthInfo.orgUuid);
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 auth`);
471
+ stderr(`Warning: org direct-join failed (${msg}); trying browser sign-in`);
323
472
  }
324
- if (!isAllowedAuthUrl(orgAuthInfo.authUrl, apiUrl)) {
473
+ if (!orgAuthCtx?.interactive || isHeadless()) {
325
474
  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.`
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
- 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
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 createBrokerFetchFromEnv(_brokerUrl) {
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(apiUrl);
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 tryBrokerContext(ctx) {
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, createBrokerFetchFromEnv(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(refreshed.accessToken, ctx.envConfig.apiUrl);
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(creds.accessToken, ctx.envConfig.apiUrl);
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(creds.accessToken, ctx.envConfig.apiUrl);
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-LE5IR5MC.js.map
841
+ //# sourceMappingURL=chunk-EITDLX75.js.map