@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.
- package/dist/cli/index.js +236 -29
- 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.
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
7627
|
+
dim(`Current: ${maskApiKey(existing)}`);
|
|
7457
7628
|
console.log();
|
|
7458
7629
|
}
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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);
|