@granular-software/sdk 0.3.2 → 0.3.3

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +236 -29
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -4,10 +4,12 @@
4
4
  var fs = require('fs');
5
5
  var path = require('path');
6
6
  var readline = require('readline');
7
+ var http = require('http');
8
+ var child_process = require('child_process');
9
+ var crypto = require('crypto');
7
10
  var process6 = require('process');
8
11
  var os = require('os');
9
12
  var tty = require('tty');
10
- var child_process = require('child_process');
11
13
 
12
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
15
 
@@ -32,6 +34,7 @@ function _interopNamespace(e) {
32
34
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
33
35
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
34
36
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
37
+ var http__namespace = /*#__PURE__*/_interopNamespace(http);
35
38
  var process6__default = /*#__PURE__*/_interopDefault(process6);
36
39
  var os__default = /*#__PURE__*/_interopDefault(os);
37
40
  var tty__default = /*#__PURE__*/_interopDefault(tty);
@@ -5455,6 +5458,12 @@ function loadApiUrl() {
5455
5458
  if (process.env.GRANULAR_API_URL) return process.env.GRANULAR_API_URL;
5456
5459
  return "https://cf-api-gateway.arthur6084.workers.dev/granular";
5457
5460
  }
5461
+ function loadAuthUrl() {
5462
+ if (process.env.GRANULAR_AUTH_URL) {
5463
+ return process.env.GRANULAR_AUTH_URL;
5464
+ }
5465
+ return "https://app.granular.software";
5466
+ }
5458
5467
  function saveApiKey(apiKey) {
5459
5468
  const envLocalPath = getEnvLocalPath();
5460
5469
  let content = "";
@@ -5722,6 +5731,161 @@ var ApiClient = class {
5722
5731
  });
5723
5732
  }
5724
5733
  };
5734
+ var CALLBACK_HOST = "127.0.0.1";
5735
+ var CALLBACK_PATH = "/callback";
5736
+ var DEFAULT_TIMEOUT_MS = 3 * 60 * 1e3;
5737
+ function normalizeAuthBaseUrl(input) {
5738
+ const url = new URL(input);
5739
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
5740
+ throw new Error("Auth URL must start with http:// or https://");
5741
+ }
5742
+ return url;
5743
+ }
5744
+ function renderHtmlPage(title, message) {
5745
+ return `<!doctype html>
5746
+ <html lang="en">
5747
+ <head>
5748
+ <meta charset="utf-8" />
5749
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5750
+ <title>${title}</title>
5751
+ <style>
5752
+ :root { color-scheme: light dark; }
5753
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; line-height: 1.5; }
5754
+ h1 { margin-bottom: 0.5rem; }
5755
+ p { margin-top: 0; color: #666; }
5756
+ </style>
5757
+ </head>
5758
+ <body>
5759
+ <h1>${title}</h1>
5760
+ <p>${message}</p>
5761
+ </body>
5762
+ </html>`;
5763
+ }
5764
+ function describeAuthError(code) {
5765
+ switch (code) {
5766
+ case "unauthorized":
5767
+ return "You must sign in before authorizing the CLI.";
5768
+ case "no_organization":
5769
+ return "Your account is not associated with an organization.";
5770
+ case "key_creation_failed":
5771
+ return "Failed to create an API key for CLI login.";
5772
+ case "missing_key_value":
5773
+ return "The auth server did not return an API key value.";
5774
+ case "auth_failed":
5775
+ return "Authentication failed on the server.";
5776
+ default:
5777
+ return code;
5778
+ }
5779
+ }
5780
+ function openExternalUrl(url) {
5781
+ const platform = process.platform;
5782
+ return new Promise((resolve) => {
5783
+ let child;
5784
+ if (platform === "darwin") {
5785
+ child = child_process.spawn("open", [url], { stdio: "ignore" });
5786
+ } else if (platform === "win32") {
5787
+ child = child_process.spawn("cmd", ["/c", "start", "", url], {
5788
+ stdio: "ignore",
5789
+ windowsHide: true
5790
+ });
5791
+ } else {
5792
+ child = child_process.spawn("xdg-open", [url], { stdio: "ignore" });
5793
+ }
5794
+ child.once("error", () => resolve(false));
5795
+ child.once("spawn", () => resolve(true));
5796
+ });
5797
+ }
5798
+ function closeServer(server) {
5799
+ return new Promise((resolve) => {
5800
+ server.close(() => resolve());
5801
+ });
5802
+ }
5803
+ async function loginWithBrowser(options) {
5804
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5805
+ const authBaseUrl = normalizeAuthBaseUrl(options.authBaseUrl);
5806
+ const state = crypto.randomBytes(24).toString("hex");
5807
+ const server = http__namespace.createServer();
5808
+ await new Promise((resolve, reject) => {
5809
+ server.once("error", reject);
5810
+ server.listen(0, CALLBACK_HOST, () => resolve());
5811
+ });
5812
+ const address = server.address();
5813
+ if (!address || typeof address === "string") {
5814
+ await closeServer(server);
5815
+ throw new Error("Could not start local callback server.");
5816
+ }
5817
+ const callbackUrl = `http://${CALLBACK_HOST}:${address.port}${CALLBACK_PATH}`;
5818
+ const authUrl = new URL("/api/cli/auth/start", authBaseUrl);
5819
+ authUrl.searchParams.set("callback", callbackUrl);
5820
+ authUrl.searchParams.set("state", state);
5821
+ const browserOpened = await openExternalUrl(authUrl.toString());
5822
+ options.onAuthUrl?.(authUrl.toString(), browserOpened);
5823
+ let settled = false;
5824
+ try {
5825
+ const apiKey = await new Promise((resolve, reject) => {
5826
+ const timeout = setTimeout(() => {
5827
+ if (settled) return;
5828
+ settled = true;
5829
+ reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for browser authentication.`));
5830
+ }, timeoutMs);
5831
+ server.on("request", (req, res) => {
5832
+ const url = new URL(req.url || "/", `http://${CALLBACK_HOST}`);
5833
+ if (url.pathname !== CALLBACK_PATH) {
5834
+ res.statusCode = 404;
5835
+ res.end("Not found");
5836
+ return;
5837
+ }
5838
+ const returnedState = url.searchParams.get("state");
5839
+ const apiKey2 = url.searchParams.get("api_key");
5840
+ const error2 = url.searchParams.get("error");
5841
+ if (!returnedState || returnedState !== state) {
5842
+ res.statusCode = 400;
5843
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
5844
+ res.end(renderHtmlPage("Granular login failed", "State mismatch. Please close this window and retry."));
5845
+ if (!settled) {
5846
+ settled = true;
5847
+ clearTimeout(timeout);
5848
+ reject(new Error("Browser auth state mismatch."));
5849
+ }
5850
+ return;
5851
+ }
5852
+ if (error2) {
5853
+ res.statusCode = 400;
5854
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
5855
+ res.end(renderHtmlPage("Granular login failed", "Authentication was canceled or failed. You can close this window."));
5856
+ if (!settled) {
5857
+ settled = true;
5858
+ clearTimeout(timeout);
5859
+ reject(new Error(describeAuthError(error2)));
5860
+ }
5861
+ return;
5862
+ }
5863
+ if (!apiKey2) {
5864
+ res.statusCode = 400;
5865
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
5866
+ res.end(renderHtmlPage("Granular login failed", "No API key was returned. Please retry."));
5867
+ if (!settled) {
5868
+ settled = true;
5869
+ clearTimeout(timeout);
5870
+ reject(new Error("No API key returned from browser auth."));
5871
+ }
5872
+ return;
5873
+ }
5874
+ res.statusCode = 200;
5875
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
5876
+ res.end(renderHtmlPage("Granular login complete", "Authentication succeeded. You can close this window."));
5877
+ if (!settled) {
5878
+ settled = true;
5879
+ clearTimeout(timeout);
5880
+ resolve(apiKey2);
5881
+ }
5882
+ });
5883
+ });
5884
+ return apiKey;
5885
+ } finally {
5886
+ await closeServer(server);
5887
+ }
5888
+ }
5725
5889
 
5726
5890
  // ../../node_modules/.bun/chalk@5.4.1/node_modules/chalk/source/vendor/ansi-styles/index.js
5727
5891
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -7311,15 +7475,6 @@ function prompt(question, defaultValue) {
7311
7475
  });
7312
7476
  });
7313
7477
  }
7314
- function promptSecret(question) {
7315
- const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7316
- return new Promise((resolve) => {
7317
- rl.question(` ${question}: `, (answer) => {
7318
- rl.close();
7319
- resolve(answer.trim());
7320
- });
7321
- });
7322
- }
7323
7478
  function confirm(question, defaultYes = true) {
7324
7479
  const hint2 = defaultYes ? "Y/n" : "y/N";
7325
7480
  return prompt(`${question} [${hint2}]`).then((answer) => {
@@ -7339,13 +7494,25 @@ async function initCommand(projectName, options) {
7339
7494
  }
7340
7495
  let apiKey = loadApiKey();
7341
7496
  if (!apiKey) {
7342
- info("No API key found. Let's set one up.");
7343
- console.log();
7344
- dim("Get your API key at https://granular.dev/dashboard/api-keys");
7345
- console.log();
7346
- apiKey = await promptSecret("Enter your API key");
7347
- if (!apiKey) {
7348
- error("API key is required.");
7497
+ info("No API key found. Starting browser login...");
7498
+ const waiting = spinner("Opening browser and waiting for authentication...");
7499
+ try {
7500
+ apiKey = await loginWithBrowser({
7501
+ authBaseUrl: loadAuthUrl(),
7502
+ onAuthUrl: (authUrl, opened) => {
7503
+ if (!opened) {
7504
+ waiting.stop();
7505
+ warn("Could not open your browser automatically.");
7506
+ dim(`Open this URL to continue:
7507
+ ${authUrl}`);
7508
+ waiting.start();
7509
+ }
7510
+ }
7511
+ });
7512
+ waiting.succeed(" Browser authentication completed.");
7513
+ } catch (err) {
7514
+ waiting.fail(` Browser authentication failed: ${err.message}`);
7515
+ dim("Run `granular login --manual` to paste an API key directly.");
7349
7516
  process.exit(1);
7350
7517
  }
7351
7518
  } else {
@@ -7439,7 +7606,7 @@ async function initCommand(projectName, options) {
7439
7606
  hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
7440
7607
  console.log();
7441
7608
  }
7442
- function promptSecret2(question) {
7609
+ function promptSecret(question) {
7443
7610
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7444
7611
  return new Promise((resolve) => {
7445
7612
  rl.question(` ${question}: `, (answer) => {
@@ -7448,26 +7615,59 @@ function promptSecret2(question) {
7448
7615
  });
7449
7616
  });
7450
7617
  }
7451
- async function loginCommand() {
7618
+ function maskApiKey(apiKey) {
7619
+ if (apiKey.length < 10) return `${apiKey}...`;
7620
+ return `${apiKey.substring(0, 10)}...`;
7621
+ }
7622
+ async function loginCommand(options = {}) {
7452
7623
  printHeader();
7453
7624
  const existing = loadApiKey();
7454
7625
  if (existing) {
7455
7626
  info("An API key is already configured.");
7456
- dim(`Current: ${existing.substring(0, 10)}...`);
7627
+ dim(`Current: ${maskApiKey(existing)}`);
7457
7628
  console.log();
7458
7629
  }
7459
- dim("Get your API key at https://granular.dev/dashboard/api-keys");
7460
- console.log();
7461
- const apiKey = await promptSecret2("Enter your API key");
7630
+ const apiUrl = loadApiUrl();
7631
+ let apiKey = options.apiKey?.trim();
7632
+ if (!apiKey) {
7633
+ if (options.manual) {
7634
+ dim("Get your API key at https://app.granular.software/w/default/api-keys");
7635
+ console.log();
7636
+ apiKey = await promptSecret("Enter your API key");
7637
+ } else {
7638
+ const timeoutMs = options.timeout && options.timeout > 0 ? options.timeout * 1e3 : void 0;
7639
+ const waiting = spinner("Opening browser and waiting for authentication...");
7640
+ try {
7641
+ apiKey = await loginWithBrowser({
7642
+ authBaseUrl: loadAuthUrl(),
7643
+ timeoutMs,
7644
+ onAuthUrl: (authUrl, opened) => {
7645
+ if (!opened) {
7646
+ waiting.stop();
7647
+ warn("Could not open your browser automatically.");
7648
+ dim(`Open this URL to continue:
7649
+ ${authUrl}`);
7650
+ waiting.start();
7651
+ }
7652
+ }
7653
+ });
7654
+ waiting.succeed(" Browser authentication completed.");
7655
+ } catch (err) {
7656
+ waiting.fail(` Browser authentication failed: ${err.message}`);
7657
+ dim("Retry with `granular login --manual` to paste an API key directly.");
7658
+ process.exit(1);
7659
+ }
7660
+ }
7661
+ }
7462
7662
  if (!apiKey) {
7463
7663
  error("API key is required.");
7464
7664
  process.exit(1);
7465
7665
  }
7466
- if (!apiKey.startsWith("sk_")) {
7467
- warn('API key should start with "sk_". Continuing anyway...');
7666
+ if (!apiKey.startsWith("sk_") && !apiKey.startsWith("gn_sk_")) {
7667
+ warn("API key does not look like a recognized Granular/WorkOS key. Continuing anyway...");
7468
7668
  }
7469
7669
  const spinner2 = spinner("Validating...");
7470
- const api = new ApiClient(apiKey, loadApiUrl());
7670
+ const api = new ApiClient(apiKey, apiUrl);
7471
7671
  const valid = await api.validateKey();
7472
7672
  if (!valid) {
7473
7673
  spinner2.fail(" Invalid API key.");
@@ -7475,7 +7675,7 @@ async function loginCommand() {
7475
7675
  }
7476
7676
  saveApiKey(apiKey);
7477
7677
  spinner2.succeed(" Authenticated successfully.");
7478
- dim("API key saved to .env.local");
7678
+ dim("API key saved to .env.local (used by CLI and SDK examples)");
7479
7679
  console.log();
7480
7680
  }
7481
7681
 
@@ -8217,9 +8417,16 @@ program2.command("init [project-name]").description("Initialize a new Granular p
8217
8417
  process.exit(1);
8218
8418
  }
8219
8419
  });
8220
- program2.command("login").description("Authenticate with your Granular API key").action(async () => {
8420
+ program2.command("login").description("Authenticate with Granular (browser by default)").option("--manual", "Prompt for API key instead of browser login").option("--api-key <key>", "Use a provided API key directly").option("--timeout <seconds>", "Browser auth timeout in seconds", (value) => {
8421
+ const n = Number.parseInt(value, 10);
8422
+ return Number.isFinite(n) ? n : 180;
8423
+ }).action(async (opts) => {
8221
8424
  try {
8222
- await loginCommand();
8425
+ await loginCommand({
8426
+ manual: opts.manual,
8427
+ apiKey: opts.apiKey,
8428
+ timeout: opts.timeout
8429
+ });
8223
8430
  } catch (err) {
8224
8431
  error(err.message);
8225
8432
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granular-software/sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "TypeScript SDK and CLI for Granular - define, build, and deploy AI sandboxes",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",