apiblaze 0.4.0 → 0.4.2

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 (3) hide show
  1. package/README.md +72 -71
  2. package/dist/index.js +464 -142
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,44 +1,38 @@
1
1
  # apiblaze
2
2
 
3
- CLI for [APIblaze](https://apiblaze.com) — instantly tunnel localhost to your APIblaze projects during development.
3
+ CLI for [APIblaze](https://apiblaze.com) — turn any backend into a managed API: add a key, auth, rate limits, a spec, and your own domain, in seconds.
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
7
  ```bash
8
- npm install -g apiblaze
8
+ npx apiblaze --help # run without installing
9
+ npm install -g apiblaze # or install globally
9
10
  ```
10
11
 
11
- Or run without installing:
12
+ Requires Node.js 18+.
13
+
14
+ ## Quick start
15
+
16
+ The easiest way is to just chat with it:
12
17
 
13
18
  ```bash
14
- npx apiblaze --help
19
+ npx apiblaze agent
15
20
  ```
16
21
 
17
- ## Requirements
22
+ > Talk in plain English — "make an API for httpbin.org", "rate-limit it to 50/sec",
23
+ > "give me a key". It does the work and shows what each step costs.
18
24
 
19
- - Node.js 18+
20
-
21
- ## Quick start
25
+ Prefer commands? A few one-liners:
22
26
 
23
27
  ```bash
24
- # Create a proxy (no account needed)
28
+ # Make an API in one line — no account needed (prints a claim URL)
25
29
  npx apiblaze create --target https://api.example.com
26
30
 
27
- # Optional: sign in if you want it under your team
28
- npx apiblaze login
29
-
30
- # Create a proxy under your team
31
- npx apiblaze create --name myapi --target https://api.example.com --auth api_key
32
-
33
- # Start a dev tunnel (defaults to port 3000)
34
- npx apiblaze dev
35
-
36
- # Or specify a port
31
+ # Run your localhost through a public URL
37
32
  npx apiblaze dev 3000
38
33
 
39
- # Stream full request/response traffic to a file (JSON lines)
40
- npx apiblaze dev 3000 --capture-file traffic.jsonl
41
-
34
+ # Sign in to manage APIs under your team
35
+ npx apiblaze login
42
36
  ```
43
37
 
44
38
  ## Help
@@ -59,64 +53,71 @@ apiblaze throttle myapi --rate 50 --verbose
59
53
 
60
54
  ## Commands
61
55
 
62
- ### Account & basics
56
+ ### Chat
63
57
 
64
- | Command | Description |
58
+ | Command | What it does |
65
59
  |---|---|
66
- | `apiblaze login` | Authenticate with your APIblaze account |
67
- | `apiblaze logout` | Sign out and remove stored credentials |
68
- | `apiblaze whoami` | Show the signed-in identity and active team |
69
- | `apiblaze team [team]` | Switch the active team |
70
- | `apiblaze projects` | List your team's proxies |
71
- | `apiblaze create [options]` | Create a new API proxy (anonymous if not logged in) |
72
- | `apiblaze claim [code]` | Claim an anonymously-created proxy into your team |
73
- | `apiblaze dev [port]` | Start a dev tunnel for your localhost projects |
74
-
75
- ### Manage a proxy
76
-
77
- | Command | Description |
78
- |---|---|
79
- | `apiblaze delete <project> [version]` | Delete a proxy (full cascade) — shows impact, then confirms |
80
- | `apiblaze target <project> --url <u> [--env <e>]` | Set the target URL (per-environment with `--env`, else project-level) |
81
- | `apiblaze throttle <project> [--rate n] [--quota n] [--period daily\|weekly\|monthly]` | Set rate limits + quota |
82
- | `apiblaze rename <project> --display-name <name>` | Change the display name |
83
- | `apiblaze spec get <project>` | Print the current OpenAPI document |
84
- | `apiblaze spec set <project> --file <path>` | Replace the OpenAPI spec from a local file |
60
+ | `apiblaze agent` | Chat about anything — create, configure, and inspect your APIs |
61
+ | `apiblaze agent openapi <project>` | Chat to build your API spec from real traffic |
62
+ | `apiblaze agent authz <project>` | Chat to design and turn on access rules |
63
+ | `apiblaze agent mcp <project>` | Chat to build an MCP server for your API |
64
+
65
+ Every chat turn shows its cost.
85
66
 
86
- ### Custom domains
67
+ ### Setup
87
68
 
88
- | Command | Description |
69
+ | Command | What it does |
89
70
  |---|---|
90
- | `apiblaze domain add <project> --domain <host>` | Register a custom hostname (prints DNS records; does not poll) |
91
- | `apiblaze domain list <project>` | List custom domains |
92
- | `apiblaze domain status <project> --id <domainId>` | Check a domain's validation status |
93
- | `apiblaze domain rm <project> --id <domainId>` | Remove a custom domain |
94
- | `apiblaze domain set-base <project> [--env <e>]` | Point the bare `{project}` hostname at a (version, env) |
71
+ | `apiblaze create --target <url>` | Make an API from a backend (no account needed) |
72
+ | `apiblaze dev [port]` | Put your localhost behind a public URL |
73
+ | `apiblaze login` / `logout` | Sign in / out (logout asks producer or consumer) |
74
+ | `apiblaze whoami` | Who am I both Producer and Consumer |
75
+ | `apiblaze team [name]` | Switch team |
76
+ | `apiblaze claim [code]` | Claim an API you made before signing in |
95
77
 
96
- ### Tenants & keys
78
+ ### Producer your APIs
97
79
 
98
- | Command | Description |
80
+ | Command | What it does |
99
81
  |---|---|
100
- | `apiblaze tenant list` | List tenants in your team |
101
- | `apiblaze tenant create --name <display> [--slug <s>]` | Create a tenant |
102
- | `apiblaze tenant attach <project> --tenant <slug>` | Attach a tenant to a proxy |
103
- | `apiblaze tenant cors --tenant <slug> --origins <a,b>` | Set the CORS allow-list for a tenant |
104
- | `apiblaze tenant delete <slug>` | Delete a tenant (full cascade) |
105
- | `apiblaze key list` | List control-plane developer keys |
106
- | `apiblaze key mint [--desc <text>] [--expires-days <n>]` | Mint a consumer-admin key (secret shown once) |
107
- | `apiblaze key revoke <keyId>` | Revoke a developer key |
108
-
109
- ### Design assistants (chat)
110
-
111
- | Command | Description |
82
+ | `apiblaze projects` | List your APIs |
83
+ | `apiblaze delete <project>` | Delete it and everything under it (asks first) |
84
+ | `apiblaze domain add <project> --domain <host>` | Add your own domain (shows the DNS records to set) |
85
+ | `apiblaze domain status / list / rm <project>` | Check / list / remove custom domains |
86
+ | `apiblaze domain set-base <project> [--env <e>]` | Pick which version/env your main URL serves |
87
+ | `apiblaze tenant create --name <name>` | Create a tenant (a separate group of your API's users) |
88
+ | `apiblaze tenant attach <project> --tenant <slug>` | Give a proxy its own set of users |
89
+ | `apiblaze tenant cors --tenant <slug> --origins <a,b>` | Set which websites can call it |
90
+ | `apiblaze tenant list / delete <slug>` | List / delete tenants |
91
+
92
+ ### Producer — change a proxy's config
93
+
94
+ | Command | What it does |
112
95
  |---|---|
113
- | `apiblaze agent` | Chat with a producer assistant that can create/manage your proxies, tenants, keys, domains, and specs. Billed per turn (cost shown). |
114
- | `apiblaze authz <project>` | Design API authorization interactively, then publish + enable it |
115
- | `apiblaze openapi <project>` | Design your OpenAPI spec from captured traffic, then publish it |
116
- | `apiblaze mcp <project>` | Design an MCP server from the spec + traffic, then publish it |
96
+ | `apiblaze target <project> --url <url> [--env <e>]` | Change where it forwards requests |
97
+ | `apiblaze throttle <project> [--rate n] [--quota n] [--period daily\|weekly\|monthly]` | Set rate limits and quotas |
98
+ | `apiblaze rename <project> --display-name <name>` | Rename it |
99
+ | `apiblaze spec get <project>` | Print its OpenAPI spec |
100
+ | `apiblaze spec set <project> --file <path>` | Replace its OpenAPI spec from a file |
117
101
 
118
- > Most management commands accept `--team <id\|name>` (defaults to your active team),
119
- > `--apiversion <version>`, and `--json` (machine-readable output for scripts).
102
+ > Every command in these two sections accepts `--verbose` (see the equivalent API
103
+ > calls), plus `--team`, `--apiversion`, and `--json`.
104
+
105
+ ### Consumer — use an API
106
+
107
+ Log in to one of your tenants' portals **as a consumer** — handy for testing auth
108
+ and keys end-to-end. This is a separate identity from your producer login; `whoami`
109
+ shows both, and `logout` asks which to drop.
110
+
111
+ | Command | What it does |
112
+ |---|---|
113
+ | `apiblaze consumer login` | Pick a tenant and log in to its portal (device flow in your browser) |
114
+ | `apiblaze consumer tokens` | Show your consumer access / refresh / id tokens |
115
+ | `apiblaze consumer apikeys` | List your API keys (reveals expiring ones in clear), then offer to create one |
116
+
117
+ > A standalone consumer (no producer account) can log in with
118
+ > `apiblaze consumer login --tenant <slug> --client <appClientId>`.
119
+ >
120
+ > Advanced: `apiblaze apikeys` manages producer control-plane keys for scripting.
120
121
 
121
122
  ## How it works
122
123
 
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __copyProps = (to, from, except, desc) => {
10
10
  if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key2 of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key2) && key2 !== except)
13
- __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
14
  }
15
15
  return to;
16
16
  };
@@ -25,10 +25,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/index.ts
27
27
  var import_commander = require("commander");
28
- var import_chalk25 = __toESM(require("chalk"));
28
+ var import_chalk26 = __toESM(require("chalk"));
29
29
 
30
30
  // package.json
31
- var version = "0.4.0";
31
+ var version = "0.4.2";
32
32
 
33
33
  // src/types.ts
34
34
  var ApiError = class extends Error {
@@ -105,9 +105,9 @@ async function createProxyAnonymous(body) {
105
105
  }
106
106
  return res.json();
107
107
  }
108
- async function apiFetch(path2, options = {}) {
108
+ async function apiFetch(path3, options = {}) {
109
109
  const token = getAccessToken();
110
- const url = `${DASHBOARD_BASE}${path2}`;
110
+ const url = `${DASHBOARD_BASE}${path3}`;
111
111
  const res = await fetch(url, {
112
112
  ...options,
113
113
  headers: {
@@ -132,12 +132,12 @@ async function apiFetch(path2, options = {}) {
132
132
  }
133
133
  return res.json();
134
134
  }
135
- async function agentCall(path2, method, body) {
135
+ async function agentCall(path3, method, body) {
136
136
  const token = getAccessToken();
137
137
  const res = await fetch(`${DASHBOARD_BASE}/api/cli/agents`, {
138
138
  method: "POST",
139
139
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
140
- body: JSON.stringify({ path: path2, method, body })
140
+ body: JSON.stringify({ path: path3, method, body })
141
141
  });
142
142
  let data = null;
143
143
  try {
@@ -420,11 +420,11 @@ function decodeJwt(token) {
420
420
  return null;
421
421
  }
422
422
  }
423
- function maskPath(path2) {
424
- const q = path2.indexOf("?");
425
- if (q < 0) return path2;
426
- const base = path2.slice(0, q);
427
- const query = path2.slice(q + 1);
423
+ function maskPath(path3) {
424
+ const q = path3.indexOf("?");
425
+ if (q < 0) return path3;
426
+ const base = path3.slice(0, q);
427
+ const query = path3.slice(q + 1);
428
428
  const masked = query.split("&").map((pair) => {
429
429
  const eq = pair.indexOf("=");
430
430
  if (eq < 0) return pair;
@@ -586,8 +586,8 @@ var STRIP_HEADERS = /* @__PURE__ */ new Set([
586
586
  ]);
587
587
  function stripHeaders(headers) {
588
588
  const out = {};
589
- for (const key2 of Object.keys(headers)) {
590
- if (!STRIP_HEADERS.has(key2.toLowerCase())) out[key2] = headers[key2];
589
+ for (const key of Object.keys(headers)) {
590
+ if (!STRIP_HEADERS.has(key.toLowerCase())) out[key] = headers[key];
591
591
  }
592
592
  return out;
593
593
  }
@@ -702,8 +702,8 @@ function startTunnelClient(opts) {
702
702
  const status = resp.status;
703
703
  const buf = Buffer.from(await resp.arrayBuffer());
704
704
  const headers = {};
705
- resp.headers.forEach((value, key2) => {
706
- if (!STRIP_HEADERS.has(key2.toLowerCase())) headers[key2] = value;
705
+ resp.headers.forEach((value, key) => {
706
+ if (!STRIP_HEADERS.has(key.toLowerCase())) headers[key] = value;
707
707
  });
708
708
  if (capturing) {
709
709
  capturing = false;
@@ -822,9 +822,9 @@ async function offerAutoCreate(teamId, port) {
822
822
  throw err;
823
823
  }
824
824
  if (auth === "api_key") {
825
- const key2 = result.api_keys?.dev ?? Object.values(result.api_keys ?? {})[0];
826
- if (key2) {
827
- console.log(` ${import_chalk4.default.dim("API key (dev):")} ${import_chalk4.default.bold.green(key2)}`);
825
+ const key = result.api_keys?.dev ?? Object.values(result.api_keys ?? {})[0];
826
+ if (key) {
827
+ console.log(` ${import_chalk4.default.dim("API key (dev):")} ${import_chalk4.default.bold.green(key)}`);
828
828
  console.log(import_chalk4.default.dim(" Send it as the X-API-Key header. It may not be shown again."));
829
829
  }
830
830
  }
@@ -1452,51 +1452,204 @@ async function runClaim(claimCodeArg, opts) {
1452
1452
 
1453
1453
  // src/commands/logout.ts
1454
1454
  var import_chalk8 = __toESM(require("chalk"));
1455
- async function runLogout() {
1455
+
1456
+ // src/lib/consumer-auth.ts
1457
+ var fs4 = __toESM(require("fs"));
1458
+ var os2 = __toESM(require("os"));
1459
+ var path2 = __toESM(require("path"));
1460
+ var crypto = __toESM(require("crypto"));
1461
+ var import_child_process = require("child_process");
1462
+ var AUTH_BASE = process.env.APIBLAZE_AUTH_BASE || "https://auth.apiblaze.com";
1463
+ var APIBLAZE_DIR2 = path2.join(os2.homedir(), ".apiblaze");
1464
+ var CONSUMER_PATH = path2.join(APIBLAZE_DIR2, "consumer.json");
1465
+ var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1466
+ function loadConsumer() {
1467
+ try {
1468
+ return JSON.parse(fs4.readFileSync(CONSUMER_PATH, "utf-8"));
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ }
1473
+ function saveConsumer(creds) {
1474
+ fs4.mkdirSync(APIBLAZE_DIR2, { recursive: true });
1475
+ fs4.writeFileSync(CONSUMER_PATH, JSON.stringify(creds, null, 2), "utf-8");
1476
+ fs4.chmodSync(CONSUMER_PATH, 384);
1477
+ }
1478
+ function clearConsumer() {
1479
+ try {
1480
+ fs4.unlinkSync(CONSUMER_PATH);
1481
+ return true;
1482
+ } catch {
1483
+ return false;
1484
+ }
1485
+ }
1486
+ function b64url(buf) {
1487
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1488
+ }
1489
+ function pkce() {
1490
+ const verifier = b64url(crypto.randomBytes(32));
1491
+ const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
1492
+ return { verifier, challenge };
1493
+ }
1494
+ function decodeJwt2(token) {
1495
+ const parts = token.split(".");
1496
+ if (parts.length < 2) return null;
1497
+ try {
1498
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString());
1499
+ } catch {
1500
+ return null;
1501
+ }
1502
+ }
1503
+ function openBrowser2(url) {
1504
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1505
+ try {
1506
+ (0, import_child_process.spawn)(cmd, [url], { stdio: "ignore", detached: true }).unref();
1507
+ } catch {
1508
+ }
1509
+ }
1510
+ async function deviceLogin(clientId, scope, onPrompt) {
1511
+ const { verifier, challenge } = pkce();
1512
+ const startRes = await fetch(`${AUTH_BASE}/device_authorization`, {
1513
+ method: "POST",
1514
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1515
+ body: new URLSearchParams({ client_id: clientId, scope, code_challenge: challenge, code_challenge_method: "S256" })
1516
+ });
1517
+ const start = await startRes.json().catch(() => ({}));
1518
+ if (!startRes.ok) {
1519
+ throw new Error(`device_authorization failed (${startRes.status}): ${start.error_description ?? start.error ?? startRes.statusText}`);
1520
+ }
1521
+ const deviceCode = String(start.device_code);
1522
+ const userCode = String(start.user_code);
1523
+ const verificationUri = String(start.verification_uri_complete ?? start.verification_uri);
1524
+ const interval = Math.max(2, Number(start.interval) || 5);
1525
+ const expiresIn = Number(start.expires_in) || 900;
1526
+ onPrompt({ verificationUri, userCode });
1527
+ openBrowser2(verificationUri);
1528
+ const deadline = Date.now() + expiresIn * 1e3;
1529
+ while (true) {
1530
+ if (Date.now() > deadline) throw new Error("Login timed out. Run the command again.");
1531
+ await new Promise((r) => setTimeout(r, interval * 1e3));
1532
+ const tokRes = await fetch(`${AUTH_BASE}/token`, {
1533
+ method: "POST",
1534
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1535
+ body: new URLSearchParams({ grant_type: DEVICE_GRANT, device_code: deviceCode, code_verifier: verifier })
1536
+ });
1537
+ const tok = await tokRes.json().catch(() => ({}));
1538
+ if (tokRes.ok && tok.access_token) {
1539
+ return {
1540
+ accessToken: String(tok.access_token),
1541
+ refreshToken: tok.refresh_token ? String(tok.refresh_token) : void 0,
1542
+ idToken: tok.id_token ? String(tok.id_token) : void 0,
1543
+ expiresIn: tok.expires_in ? Number(tok.expires_in) : void 0,
1544
+ scope: tok.scope ? String(tok.scope) : void 0
1545
+ };
1546
+ }
1547
+ if (tok.error === "authorization_pending" || tok.error === "slow_down") continue;
1548
+ throw new Error(`Login failed: ${tok.error_description ?? tok.error ?? tokRes.statusText}`);
1549
+ }
1550
+ }
1551
+ async function refreshConsumer(creds) {
1552
+ if (!creds.refreshToken) return null;
1553
+ const res = await fetch(`${AUTH_BASE}/refresh`, {
1554
+ method: "POST",
1555
+ headers: { "Content-Type": "application/json" },
1556
+ body: JSON.stringify({ refresh_token: creds.refreshToken })
1557
+ });
1558
+ const data = await res.json().catch(() => ({}));
1559
+ if (!res.ok || !data.access_token) return null;
1560
+ const updated = {
1561
+ ...creds,
1562
+ accessToken: String(data.access_token),
1563
+ refreshToken: data.refresh_token ? String(data.refresh_token) : creds.refreshToken,
1564
+ idToken: data.id_token ? String(data.id_token) : creds.idToken,
1565
+ expiresAt: Date.now() + (Number(data.expires_in) || 3600) * 1e3,
1566
+ obtainedAt: Date.now()
1567
+ };
1568
+ saveConsumer(updated);
1569
+ return updated;
1570
+ }
1571
+ async function validConsumerToken(creds) {
1572
+ if (Date.now() < creds.expiresAt - 3e4) return creds;
1573
+ return refreshConsumer(creds);
1574
+ }
1575
+
1576
+ // src/commands/logout.ts
1577
+ async function runLogout(opts = {}) {
1456
1578
  const creds = loadCredentials();
1457
- const removed = clearCredentials();
1458
- if (!removed) {
1579
+ const consumer2 = loadConsumer();
1580
+ if (!creds && !consumer2) {
1459
1581
  console.log(import_chalk8.default.yellow("You were not logged in."));
1460
1582
  return;
1461
1583
  }
1462
- const who = creds?.githubHandle ?? creds?.email;
1463
- console.log(import_chalk8.default.green(`\u2714 Logged out${who ? ` (${who})` : ""}.`));
1584
+ let target;
1585
+ if (opts.all) target = "all";
1586
+ else if (opts.producer) target = "producer";
1587
+ else if (opts.consumer) target = "consumer";
1588
+ else if (creds && !consumer2) target = "producer";
1589
+ else if (consumer2 && !creds) target = "consumer";
1590
+ else {
1591
+ const { default: inquirer2 } = await import("inquirer");
1592
+ const { chosen } = await inquirer2.prompt([
1593
+ {
1594
+ type: "list",
1595
+ name: "chosen",
1596
+ message: "Log out of which identity?",
1597
+ choices: [
1598
+ { name: `API Producer (${creds?.githubHandle ?? creds?.email ?? "producer"})`, value: "producer" },
1599
+ { name: `API Consumer (${consumer2?.email ?? consumer2?.tenant})`, value: "consumer" },
1600
+ { name: "Both", value: "all" }
1601
+ ]
1602
+ }
1603
+ ]);
1604
+ target = chosen;
1605
+ }
1606
+ if (target === "producer" || target === "all") {
1607
+ clearCredentials();
1608
+ console.log(import_chalk8.default.green(`\u2714 Logged out of API Producer${creds?.githubHandle ? ` (${creds.githubHandle})` : ""}.`));
1609
+ }
1610
+ if (target === "consumer" || target === "all") {
1611
+ clearConsumer();
1612
+ console.log(import_chalk8.default.green(`\u2714 Logged out of API Consumer${consumer2?.tenant ? ` (${consumer2.tenant})` : ""}.`));
1613
+ }
1464
1614
  }
1465
1615
 
1466
1616
  // src/commands/whoami.ts
1467
1617
  var import_chalk9 = __toESM(require("chalk"));
1468
1618
  async function runWhoami(opts = {}) {
1469
1619
  const creds = loadCredentials();
1470
- if (!creds) {
1471
- if (opts.json) {
1472
- console.log(JSON.stringify({ loggedIn: false }, null, 2));
1473
- } else {
1474
- console.log(import_chalk9.default.yellow("Not logged in. Run `apiblaze login` first."));
1475
- }
1476
- return;
1477
- }
1478
- const expired = Date.now() >= creds.expiresAt;
1620
+ const consumer2 = loadConsumer();
1479
1621
  if (opts.json) {
1480
1622
  console.log(JSON.stringify({
1481
- loggedIn: true,
1482
- expired,
1483
- apiblazeUserId: creds.apiblazeUserId ?? null,
1484
- githubHandle: creds.githubHandle ?? null,
1485
- email: creds.email ?? null,
1486
- teamId: creds.teamId ?? null,
1487
- teamName: creds.teamName ?? null
1623
+ producer: creds ? {
1624
+ loggedIn: true,
1625
+ expired: Date.now() >= creds.expiresAt,
1626
+ apiblazeUserId: creds.apiblazeUserId ?? null,
1627
+ githubHandle: creds.githubHandle ?? null,
1628
+ email: creds.email ?? null,
1629
+ teamId: creds.teamId ?? null,
1630
+ teamName: creds.teamName ?? null
1631
+ } : { loggedIn: false },
1632
+ consumer: consumer2 ? { loggedIn: true, expired: Date.now() >= consumer2.expiresAt, tenant: consumer2.tenant, email: consumer2.email ?? null } : { loggedIn: false }
1488
1633
  }, null, 2));
1489
1634
  return;
1490
1635
  }
1491
- const who = creds.githubHandle ?? creds.email ?? creds.apiblazeUserId ?? "unknown";
1492
- console.log(`${import_chalk9.default.cyan("Signed in as")} ${import_chalk9.default.bold(who)}`);
1493
- if (creds.email && creds.email !== who) console.log(` Email: ${creds.email}`);
1494
- if (creds.apiblazeUserId) console.log(` User ID: ${creds.apiblazeUserId}`);
1495
- if (creds.teamName || creds.teamId) {
1496
- console.log(` Team: ${import_chalk9.default.bold(creds.teamName ?? creds.teamId)}${creds.teamName && creds.teamId ? import_chalk9.default.gray(` (${creds.teamId})`) : ""}`);
1497
- }
1498
- if (expired) {
1499
- console.log(import_chalk9.default.yellow("\n\u26A0 Session expired. Run `apiblaze login` to re-authenticate."));
1636
+ console.log(import_chalk9.default.bold("API Producer"));
1637
+ if (!creds) {
1638
+ console.log(import_chalk9.default.dim(" Not logged in. Run `apiblaze login`."));
1639
+ } else {
1640
+ const who = creds.githubHandle ?? creds.email ?? creds.apiblazeUserId ?? "unknown";
1641
+ console.log(` ${import_chalk9.default.cyan("Signed in as")} ${import_chalk9.default.bold(who)}`);
1642
+ if (creds.email && creds.email !== who) console.log(` Email: ${creds.email}`);
1643
+ if (creds.teamName || creds.teamId) console.log(` Team: ${import_chalk9.default.bold(creds.teamName ?? creds.teamId)}`);
1644
+ if (Date.now() >= creds.expiresAt) console.log(import_chalk9.default.yellow(" \u26A0 Session expired \u2014 run `apiblaze login`."));
1645
+ }
1646
+ console.log(import_chalk9.default.bold("\nAPI Consumer"));
1647
+ if (!consumer2) {
1648
+ console.log(import_chalk9.default.dim(" Not logged in. Run `apiblaze consumer login`."));
1649
+ } else {
1650
+ console.log(` ${import_chalk9.default.cyan("Signed in as")} ${import_chalk9.default.bold(consumer2.email ?? consumer2.tenant)}`);
1651
+ console.log(` Tenant: ${import_chalk9.default.bold(consumer2.tenant)}`);
1652
+ if (Date.now() >= consumer2.expiresAt) console.log(import_chalk9.default.yellow(" \u26A0 Token expired \u2014 it will refresh on next use, or re-run `apiblaze consumer login`."));
1500
1653
  }
1501
1654
  }
1502
1655
 
@@ -2283,6 +2436,19 @@ async function runTenantCors(opts) {
2283
2436
  // src/commands/key.ts
2284
2437
  var import_chalk22 = __toESM(require("chalk"));
2285
2438
  var import_ora9 = __toESM(require("ora"));
2439
+ async function runApikeysMenu(opts) {
2440
+ await runKeyList(opts);
2441
+ if (opts.json) return;
2442
+ const { default: inquirer2 } = await import("inquirer");
2443
+ const { make } = await inquirer2.prompt([
2444
+ { type: "confirm", name: "make", message: "Generate a new control-plane API key?", default: false }
2445
+ ]);
2446
+ if (!make) return;
2447
+ const { desc } = await inquirer2.prompt([
2448
+ { type: "input", name: "desc", message: "Description (optional):" }
2449
+ ]);
2450
+ await runKeyMint({ ...opts, desc: desc || void 0 });
2451
+ }
2286
2452
  async function runKeyList(opts) {
2287
2453
  const { teamId, teamName } = await resolveTeam(opts.team);
2288
2454
  const out = await admin({
@@ -2346,7 +2512,7 @@ async function runKeyRevoke(keyId, opts) {
2346
2512
  }
2347
2513
 
2348
2514
  // src/commands/spec.ts
2349
- var fs4 = __toESM(require("fs"));
2515
+ var fs5 = __toESM(require("fs"));
2350
2516
  var import_chalk23 = __toESM(require("chalk"));
2351
2517
  var import_ora10 = __toESM(require("ora"));
2352
2518
  async function runSpecGet(project, opts) {
@@ -2366,7 +2532,7 @@ async function runSpecSet(project, opts) {
2366
2532
  }
2367
2533
  let specContent;
2368
2534
  try {
2369
- specContent = fs4.readFileSync(opts.file, "utf-8");
2535
+ specContent = fs5.readFileSync(opts.file, "utf-8");
2370
2536
  } catch {
2371
2537
  console.error(import_chalk23.default.red(`Cannot read file: ${opts.file}`));
2372
2538
  process.exit(1);
@@ -2398,6 +2564,12 @@ async function proj(teamId, name, version2) {
2398
2564
  return resolveProject(teamId, name, version2);
2399
2565
  }
2400
2566
  var TOOLS = [
2567
+ {
2568
+ name: "create_proxy",
2569
+ description: "Create a new API proxy (project) from a backend URL. auth is one of: api_key, none, oauth.",
2570
+ params: { name: "proxy name (becomes its subdomain)", target: "backend URL to forward to", auth: "api_key | none | oauth (default api_key)" },
2571
+ run: async (a, { teamId }) => createProxy({ name: a.name, target_url: a.target ?? a.target_url ?? a.url, auth_type: a.auth ?? a.auth_type ?? "api_key", team_id: teamId })
2572
+ },
2401
2573
  {
2402
2574
  name: "list_projects",
2403
2575
  description: "List the proxies (projects) in the active team.",
@@ -2604,6 +2776,175 @@ async function runAgent(opts) {
2604
2776
  console.log(import_chalk24.default.dim("\nBye."));
2605
2777
  }
2606
2778
 
2779
+ // src/commands/consumer.ts
2780
+ var import_chalk25 = __toESM(require("chalk"));
2781
+ var import_ora12 = __toESM(require("ora"));
2782
+ var APIKEYS_VER = "1.0.0";
2783
+ var DEFAULT_SCOPE = "openid email profile offline_access";
2784
+ function apikeysBase(tenant2) {
2785
+ const tmpl = process.env.APIBLAZE_APIKEYS_BASE_TMPL;
2786
+ return tmpl ? tmpl.replace("{tenant}", tenant2) : `https://${tenant2}.apikeys.apiblaze.com`;
2787
+ }
2788
+ async function consumerFetch(creds, suffix, init) {
2789
+ const fresh = await validConsumerToken(creds) ?? creds;
2790
+ const res = await fetch(`${apikeysBase(fresh.tenant)}/${APIKEYS_VER}${suffix}`, {
2791
+ ...init,
2792
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${fresh.accessToken}`, ...init?.headers ?? {} }
2793
+ });
2794
+ let data = null;
2795
+ try {
2796
+ data = await res.json();
2797
+ } catch {
2798
+ }
2799
+ return { status: res.status, data, creds: fresh };
2800
+ }
2801
+ function requireConsumer() {
2802
+ const c = loadConsumer();
2803
+ if (!c) {
2804
+ console.error(import_chalk25.default.red("Not logged in as a consumer. Run `apiblaze consumer login` first."));
2805
+ process.exit(1);
2806
+ }
2807
+ return c;
2808
+ }
2809
+ async function runConsumerLogin(opts) {
2810
+ const { default: inquirer2 } = await import("inquirer");
2811
+ let tenant2 = opts.tenant;
2812
+ let clientId = opts.client;
2813
+ if (clientId) {
2814
+ if (!tenant2) {
2815
+ console.error(import_chalk25.default.red("When using --client, also pass --tenant <slug> (it sets which portal/keys host to use)."));
2816
+ process.exit(1);
2817
+ }
2818
+ } else {
2819
+ requireAuth();
2820
+ const { teamId, teamName } = await resolveTeam(opts.team);
2821
+ const spinner = (0, import_ora12.default)("Loading your tenants...").start();
2822
+ const tdata = await admin({ method: "GET", path: `/teams/${encodeURIComponent(teamId)}/tenants?detail=1`, summary: `List tenants for ${teamName ?? teamId}` });
2823
+ spinner.stop();
2824
+ const tenants = (tdata?.tenants ?? []).map(
2825
+ (t) => typeof t === "string" ? { tenant_name: t } : t
2826
+ );
2827
+ if (!tenants.length) {
2828
+ console.error(import_chalk25.default.red("This team has no tenants. Create one with `apiblaze tenant create`."));
2829
+ process.exit(1);
2830
+ }
2831
+ if (!tenant2) {
2832
+ if (tenants.length === 1) tenant2 = tenants[0].tenant_name;
2833
+ else {
2834
+ const { chosen } = await inquirer2.prompt([{ type: "list", name: "chosen", message: "Which tenant portal?", choices: tenants.map((t) => ({ name: t.display_name ? `${t.display_name} (${t.tenant_name})` : t.tenant_name, value: t.tenant_name })) }]);
2835
+ tenant2 = chosen;
2836
+ }
2837
+ }
2838
+ const s2 = (0, import_ora12.default)("Finding the login app...").start();
2839
+ const clients = await admin({ method: "GET", path: `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/app-clients`, summary: `List app clients for ${tenant2}` }).catch(() => []);
2840
+ s2.stop();
2841
+ const usable = (Array.isArray(clients) ? clients : []).filter((c) => c && (c.client_id || c.clientId));
2842
+ const pick2 = usable.find((c) => c.is_default || c.default) ?? usable.find((c) => c.verified !== false) ?? usable[0];
2843
+ if (!pick2) {
2844
+ console.error(import_chalk25.default.red(`Tenant "${tenant2}" has no login app configured. Set one up in the dashboard (or \`apiblaze create\` with auth).`));
2845
+ process.exit(1);
2846
+ }
2847
+ clientId = pick2.client_id ?? pick2.clientId;
2848
+ }
2849
+ console.log(`${import_chalk25.default.cyan("\u2192")} Logging in to ${import_chalk25.default.bold(tenant2)} as a consumer...`);
2850
+ const result = await deviceLogin(clientId, DEFAULT_SCOPE, ({ verificationUri, userCode }) => {
2851
+ console.log(`
2852
+ Open: ${import_chalk25.default.underline(verificationUri)}`);
2853
+ console.log(` Code: ${import_chalk25.default.bold(userCode)}
2854
+ `);
2855
+ console.log(import_chalk25.default.dim(" (opening your browser\u2026 waiting for you to finish)"));
2856
+ });
2857
+ const claims = result.idToken && decodeJwt2(result.idToken) || (decodeJwt2(result.accessToken) ?? {});
2858
+ const creds = {
2859
+ tenant: tenant2,
2860
+ clientId,
2861
+ accessToken: result.accessToken,
2862
+ refreshToken: result.refreshToken,
2863
+ idToken: result.idToken,
2864
+ expiresAt: Date.now() + (result.expiresIn ?? 3600) * 1e3,
2865
+ scope: result.scope,
2866
+ email: typeof claims.email === "string" ? claims.email : void 0,
2867
+ obtainedAt: Date.now()
2868
+ };
2869
+ saveConsumer(creds);
2870
+ console.log(import_chalk25.default.green(`\u2714 Logged in as consumer${creds.email ? ` ${creds.email}` : ""} on ${tenant2}.`));
2871
+ }
2872
+ async function runConsumerTokens(opts) {
2873
+ const creds = requireConsumer();
2874
+ const fresh = await validConsumerToken(creds) ?? creds;
2875
+ const exp = (t) => {
2876
+ const p = t ? decodeJwt2(t) : null;
2877
+ return p && typeof p.exp === "number" ? new Date(p.exp * 1e3).toISOString() : void 0;
2878
+ };
2879
+ if (opts.json) {
2880
+ console.log(JSON.stringify({ tenant: fresh.tenant, access_token: fresh.accessToken, refresh_token: fresh.refreshToken, id_token: fresh.idToken, expires_at: new Date(fresh.expiresAt).toISOString() }, null, 2));
2881
+ return;
2882
+ }
2883
+ console.log(`${import_chalk25.default.cyan("Consumer")} ${import_chalk25.default.bold(fresh.email ?? fresh.tenant)} on ${import_chalk25.default.bold(fresh.tenant)}
2884
+ `);
2885
+ console.log(`${import_chalk25.default.bold("access_token")} ${import_chalk25.default.dim("exp " + (exp(fresh.accessToken) ?? "?"))}
2886
+ ${fresh.accessToken}
2887
+ `);
2888
+ if (fresh.idToken) console.log(`${import_chalk25.default.bold("id_token")} ${import_chalk25.default.dim("exp " + (exp(fresh.idToken) ?? "?"))}
2889
+ ${fresh.idToken}
2890
+ `);
2891
+ if (fresh.refreshToken) console.log(`${import_chalk25.default.bold("refresh_token")}
2892
+ ${fresh.refreshToken}
2893
+ `);
2894
+ console.log(import_chalk25.default.dim("These are your own tokens \u2014 keep them secret."));
2895
+ }
2896
+ async function runConsumerApikeys(opts) {
2897
+ const creds = requireConsumer();
2898
+ const { default: inquirer2 } = await import("inquirer");
2899
+ const spinner = (0, import_ora12.default)("Loading your API keys...").start();
2900
+ const list = await consumerFetch(creds, "/apikeys");
2901
+ const revealed = await consumerFetch(list.creds, "/apikeys/reveal").catch(() => ({ status: 0, data: null, creds: list.creds }));
2902
+ spinner.stop();
2903
+ if (list.status >= 400) {
2904
+ console.error(import_chalk25.default.red(`Failed to list keys (${list.status}): ${list.data?.error ?? ""}`));
2905
+ if (list.status === 401) console.error(import_chalk25.default.dim("Your consumer session may have expired \u2014 run `apiblaze consumer login` again."));
2906
+ process.exit(1);
2907
+ }
2908
+ const keys = list.data?.keys ?? [];
2909
+ const revealMap = revealed.data?.keys ?? {};
2910
+ if (opts.json) {
2911
+ console.log(JSON.stringify({ keys, revealed: revealMap }, null, 2));
2912
+ } else if (!keys.length) {
2913
+ console.log(import_chalk25.default.yellow("No API keys yet."));
2914
+ } else {
2915
+ for (const k of keys) {
2916
+ const clear = revealMap[k.environment]?.key;
2917
+ const shown = clear ? import_chalk25.default.green(clear) : import_chalk25.default.dim(`${k.key_prefix ?? ""}\u2026${k.key_suffix ?? ""}`);
2918
+ const exp = k.expires_at ? import_chalk25.default.dim(`exp ${k.expires_at}`) : import_chalk25.default.dim("no expiry");
2919
+ console.log(` ${import_chalk25.default.bold(k.environment ?? "")} ${shown} ${exp} ${import_chalk25.default.dim(k.description ?? "")}`);
2920
+ }
2921
+ if (Object.keys(revealMap).length === 0 && keys.some((k) => !k.expires_at)) {
2922
+ console.log(import_chalk25.default.dim("\n(Only expiring keys can be shown in clear; non-expiring keys show a prefix only.)"));
2923
+ }
2924
+ }
2925
+ if (opts.json) return;
2926
+ const { make } = await inquirer2.prompt([{ type: "confirm", name: "make", message: "Create a new API key?", default: false }]);
2927
+ if (!make) return;
2928
+ const answers = await inquirer2.prompt([
2929
+ { type: "input", name: "environment", message: "Environment:", default: "production" },
2930
+ { type: "input", name: "description", message: "Description (optional):" },
2931
+ { type: "input", name: "expiresDays", message: "Expires in how many days? (blank = no expiry):" }
2932
+ ]);
2933
+ const body = { environment: answers.environment };
2934
+ if (answers.description) body.description = answers.description;
2935
+ if (answers.expiresDays) body.expires_in_seconds = Number(answers.expiresDays) * 86400;
2936
+ const s2 = (0, import_ora12.default)("Creating key...").start();
2937
+ const created = await consumerFetch(list.creds, "/apikeys", { method: "POST", body: JSON.stringify(body) });
2938
+ if (created.status >= 400) {
2939
+ s2.fail(`Create failed (${created.status}): ${created.data?.error ?? ""}`);
2940
+ return;
2941
+ }
2942
+ s2.succeed("Key created.");
2943
+ const key = created.data?.key ?? created.data?.fullKey;
2944
+ if (key) console.log(` ${import_chalk25.default.green(key)} ${import_chalk25.default.dim("(shown once \u2014 store it now)")}`);
2945
+ else console.log(import_chalk25.default.dim(" Key created; run `apiblaze consumer apikeys` to reveal it if it expires."));
2946
+ }
2947
+
2607
2948
  // src/index.ts
2608
2949
  var program = new import_commander.Command();
2609
2950
  program.name("apiblaze").description("APIblaze CLI \u2014 create & manage API proxies and run dev tunnels").version(version).option("-v, --verbose", "Print the exact series of API calls each command makes (curl-equivalent you could run yourself)");
@@ -2637,15 +2978,29 @@ program.command("create").description("Create a new API proxy (no login needed \
2637
2978
  process.exit(1);
2638
2979
  }
2639
2980
  });
2640
- program.command("logout").description("Sign out \u2014 remove stored credentials from this machine").action(async () => {
2981
+ var agent = program.command("agent").description("Chat with an assistant that builds and runs your APIs (billed per turn)").option("--team <id|name>", "Team to work in (defaults to your active team)").action(action((opts) => runAgent(opts)));
2982
+ agent.command("authz").description("Chat to design and turn on access rules for an API").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").action(action((project, apiVersion) => runAuthz(project, apiVersion)));
2983
+ agent.command("openapi").description("Chat to build your API spec from real traffic").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").action(action((project, apiVersion) => runOpenapi(project, apiVersion)));
2984
+ agent.command("mcp").description("Chat to build an MCP server for an API").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").option("--environment <env>", "Environment to publish (default: prod)").action(action((project, apiVersion, opts) => runMcp(project, apiVersion, opts)));
2985
+ program.command("dev").description("Put your localhost behind a public URL (dev tunnel)").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").option("-o, --capture-file <path>", "Stream full request/response traffic to a file (JSON lines)").action(async (port, opts) => {
2641
2986
  try {
2642
- await runLogout();
2987
+ const resolved = parseInt(port ?? opts.port, 10);
2988
+ if (Number.isNaN(resolved)) {
2989
+ console.error(import_chalk26.default.red(`Invalid port: ${port ?? opts.port}`));
2990
+ process.exit(1);
2991
+ }
2992
+ await runDev({ port: resolved, captureFile: opts.captureFile });
2643
2993
  } catch (err) {
2644
2994
  printError(err);
2645
2995
  process.exit(1);
2646
2996
  }
2647
2997
  });
2648
- program.command("whoami").description("Show the signed-in identity and active team").option("--json", "Output machine-readable JSON").action(async (opts) => {
2998
+ var consumer = program.command("consumer").description("Act as a consumer of your API \u2014 log in, get tokens, manage API keys");
2999
+ consumer.command("login").description("Log in to a tenant's portal as a consumer (device flow)").option("--team <id|name>", "Team whose tenants to choose from (producer mode)").option("--tenant <slug>", "Tenant to log in to (skips the picker)").option("--client <clientId>", "App client id \u2014 for standalone login without a producer session").action(action((opts) => runConsumerLogin(opts)));
3000
+ consumer.command("tokens").description("Show the logged-in consumer access / refresh / id tokens").option("--json", "Output machine-readable JSON").action(action((opts) => runConsumerTokens(opts)));
3001
+ consumer.command("apikeys").description("List your consumer API keys (reveals expiring ones), then offer to create one").option("--json", "Output machine-readable JSON").action(action((opts) => runConsumerApikeys(opts)));
3002
+ program.command("logout").description("Sign out (asks whether to drop the Producer or Consumer login)").option("--producer", "Log out of the API Producer identity").option("--consumer", "Log out of the API Consumer identity").option("--all", "Log out of both").action(action((opts) => runLogout(opts)));
3003
+ program.command("whoami").description("Show who you are \u2014 both API Producer and API Consumer").option("--json", "Output machine-readable JSON").action(async (opts) => {
2649
3004
  try {
2650
3005
  await runWhoami(opts);
2651
3006
  } catch (err) {
@@ -2677,111 +3032,78 @@ program.command("projects").description("List the projects in your team").action
2677
3032
  process.exit(1);
2678
3033
  }
2679
3034
  });
2680
- program.command("authz").description("Design API authorization interactively (chat), then publish + enable it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
2681
- try {
2682
- await runAuthz(project, apiVersion);
2683
- } catch (err) {
2684
- printError(err);
2685
- process.exit(1);
2686
- }
2687
- });
2688
- program.command("openapi").description("Design your OpenAPI spec from captured traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
2689
- try {
2690
- await runOpenapi(project, apiVersion);
2691
- } catch (err) {
2692
- printError(err);
2693
- process.exit(1);
2694
- }
2695
- });
2696
- program.command("mcp").description("Design an MCP server from the spec + traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").option("--environment <env>", "Environment to publish (default: prod)").action(async (project, apiVersion, opts) => {
2697
- try {
2698
- await runMcp(project, apiVersion, opts);
2699
- } catch (err) {
2700
- printError(err);
2701
- process.exit(1);
2702
- }
2703
- });
2704
- program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").option("-o, --capture-file <path>", "Stream full request/response traffic to a file (JSON lines)").action(async (port, opts) => {
2705
- try {
2706
- const resolved = parseInt(port ?? opts.port, 10);
2707
- if (Number.isNaN(resolved)) {
2708
- console.error(import_chalk25.default.red(`Invalid port: ${port ?? opts.port}`));
2709
- process.exit(1);
2710
- }
2711
- await runDev({ port: resolved, captureFile: opts.captureFile });
2712
- } catch (err) {
2713
- printError(err);
2714
- process.exit(1);
2715
- }
2716
- });
2717
- program.command("delete").description("Delete a proxy (full cascade) \u2014 shows impact, then confirms").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[version]", "API version (defaults to the first match)").option("--team <id|name>", "Team the project is in (defaults to active team)").option("-y, --yes", "Skip the confirmation prompt").option("--json", "Output machine-readable JSON").action(action((project, version2, opts) => runDelete(project, version2, opts)));
2718
- program.command("target").description("Set a proxy's target URL (per-environment with --env, else project-level)").argument("<project>", "Project name or id").requiredOption("--url <url>", "Target URL to forward to").option("--env <env>", "Environment to scope the target to (e.g. prod, dev)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version (defaults to the first match)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTargetSet(project, opts)));
2719
- program.command("throttle").description("Set per-proxy throttling (rate limits + quota)").argument("<project>", "Project name or id").option("--rate <n>", "User rate limit (requests/sec)").option("--end-user-rate <n>", "Per-end-user rate limit (requests/sec)").option("--quota <n>", "Proxy quota (requests/period)").option("--period <p>", "Quota period: daily | weekly | monthly").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runThrottleSet(project, opts)));
3035
+ program.command("delete").description("Delete a proxy and everything under it (asks first)").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[version]", "API version (defaults to the first match)").option("--team <id|name>", "Team the project is in (defaults to active team)").option("-y, --yes", "Skip the confirmation prompt").option("--json", "Output machine-readable JSON").action(action((project, version2, opts) => runDelete(project, version2, opts)));
3036
+ program.command("target").description("Change where a proxy forwards requests").argument("<project>", "Project name or id").requiredOption("--url <url>", "Target URL to forward to").option("--env <env>", "Environment to scope the target to (e.g. prod, dev)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version (defaults to the first match)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTargetSet(project, opts)));
3037
+ program.command("throttle").description("Set rate limits and quotas for a proxy").argument("<project>", "Project name or id").option("--rate <n>", "User rate limit (requests/sec)").option("--end-user-rate <n>", "Per-end-user rate limit (requests/sec)").option("--quota <n>", "Proxy quota (requests/period)").option("--period <p>", "Quota period: daily | weekly | monthly").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runThrottleSet(project, opts)));
2720
3038
  program.command("rename").description("Change a proxy's display name").argument("<project>", "Project name or id").requiredOption("--display-name <name>", "New human-friendly display name").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runRename(project, opts)));
2721
- var domain = program.command("domain").description("Manage custom domains for a proxy");
2722
- domain.command("add").description("Register a custom hostname (prints DNS records; does not poll)").argument("<project>", "Project name or id").requiredOption("--domain <host>", "Custom hostname to add").option("--tenant <slug>", "Tenant to scope the domain to").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runDomainAdd(project, opts)));
3039
+ var domain = program.command("domain").description("Use your own domain for a proxy");
3040
+ domain.command("add").description("Add your own domain (shows the DNS records to set)").argument("<project>", "Project name or id").requiredOption("--domain <host>", "Custom hostname to add").option("--tenant <slug>", "Tenant to scope the domain to").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runDomainAdd(project, opts)));
2723
3041
  domain.command("list").description("List custom domains for a proxy").argument("<project>", "Project name or id").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runDomainList(project, opts)));
2724
3042
  domain.command("status").description("Check a custom domain's validation status").argument("<project>", "Project name or id").requiredOption("--id <domainId>", "Domain id (see `domain list`)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runDomainStatus(project, opts)));
2725
3043
  domain.command("rm").description("Remove a custom domain").argument("<project>", "Project name or id").requiredOption("--id <domainId>", "Domain id (see `domain list`)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").action(action((project, opts) => runDomainRemove(project, opts)));
2726
- domain.command("set-base").description("Point the bare {project} hostname at a (version, env)").argument("<project>", "Project name or id").option("--env <env>", "Environment (default: prod)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").action(action((project, opts) => runDomainSetBase(project, opts)));
2727
- var tenant = program.command("tenant").description("Manage tenants (consumer scopes) for your team/proxies");
3044
+ domain.command("set-base").description("Choose which version/environment your main URL serves").argument("<project>", "Project name or id").option("--env <env>", "Environment (default: prod)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").action(action((project, opts) => runDomainSetBase(project, opts)));
3045
+ var tenant = program.command("tenant").description("Manage tenants \u2014 separate groups of your API's users");
2728
3046
  tenant.command("list").description("List tenants in your team").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantList(opts)));
2729
3047
  tenant.command("create").description("Create a tenant in your team").requiredOption("--name <display>", "Display name").option("--slug <tenant_name>", "Explicit tenant slug (generated if omitted)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantCreate(opts)));
2730
3048
  tenant.command("attach").description("Attach a tenant to a proxy").argument("<project>", "Project name or id").requiredOption("--tenant <slug>", "Tenant slug to attach").option("--auth-config <id>", "Auth config id to bind").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTenantAttach(project, opts)));
2731
3049
  tenant.command("delete").description("Delete a tenant (full cascade)").argument("<slug>", "Tenant slug to delete").option("--team <id|name>", "Team (defaults to active team)").option("-y, --yes", "Skip the confirmation prompt").action(action((slug, opts) => runTenantDelete(slug, opts)));
2732
3050
  tenant.command("cors").description("Set the CORS allow-list for a tenant").requiredOption("--tenant <slug>", "Tenant slug").option("--origins <list>", 'Comma-separated origins (or "*"); empty clears').option("--team <id|name>", "Team (defaults to active team)").action(action((opts) => runTenantCors(opts)));
2733
- var key = program.command("key").description("Manage control-plane developer keys (consumer-admin) for curl-based management");
2734
- key.command("list").description("List developer keys in your team").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runKeyList(opts)));
2735
- key.command("mint").description("Mint a consumer-admin developer key (secret shown once)").option("--desc <text>", "Description").option("--expires-days <n>", "Expiry in days (default 90 server-side)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runKeyMint(opts)));
2736
- key.command("revoke").description("Revoke a developer key").argument("<keyId>", "Key id (see `key list`)").option("--team <id|name>", "Team (defaults to active team)").action(action((keyId, opts) => runKeyRevoke(keyId, opts)));
2737
- var spec = program.command("spec").description("Inspect / regenerate a proxy OpenAPI spec (author it conversationally with `apiblaze openapi`)");
3051
+ var apikeys = new import_commander.Command("apikeys").description("Producer control-plane API keys (list, then offer to create one)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runApikeysMenu(opts)));
3052
+ apikeys.command("list").description("List control-plane API keys in your team").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runKeyList(opts)));
3053
+ apikeys.command("mint").description("Create a control-plane API key (secret shown once)").option("--desc <text>", "Description").option("--expires-days <n>", "Expiry in days (default 90 server-side)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runKeyMint(opts)));
3054
+ apikeys.command("revoke").description("Revoke a control-plane API key").argument("<keyId>", "Key id (see `apikeys list`)").option("--team <id|name>", "Team (defaults to active team)").action(action((keyId, opts) => runKeyRevoke(keyId, opts)));
3055
+ program.addCommand(apikeys, { hidden: true });
3056
+ var spec = program.command("spec").description("View or update a proxy's OpenAPI spec (or build one by chatting: apiblaze agent openapi)");
2738
3057
  spec.command("get").description("Print the current OpenAPI document").argument("<project>", "Project name or id").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Compact JSON output").action(action((project, opts) => runSpecGet(project, opts)));
2739
3058
  spec.command("set").description("Replace the stored OpenAPI spec from a local file").argument("<project>", "Project name or id").requiredOption("--file <path>", "OpenAPI JSON or YAML file to upload").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runSpecSet(project, opts)));
2740
- program.command("agent").description("Chat with a producer assistant that can create/manage your proxies, tenants, keys, domains, and specs (billed per turn)").option("--team <id|name>", "Team to operate in (defaults to active team)").action(action((opts) => runAgent(opts)));
2741
- program.addHelpText(
2742
- "after",
2743
- `
2744
- Examples:
2745
- # No account needed \u2014 create an anonymous proxy, get a claim URL:
2746
- $ npx apiblaze create --target https://api.example.com
2747
-
2748
- # Non-interactive (CI / scripts):
2749
- $ npx apiblaze create --target https://api.example.com --name myapi --json
2750
-
2751
- # Sign in, then create under your team:
2752
- $ npx apiblaze login
2753
- $ npx apiblaze create --name myapi --target https://api.example.com --auth api_key
2754
-
2755
- # Dev tunnel \u2014 auto-creates a proxy if none point here, and captures
2756
- # traffic (full headers + body, secrets masked) until your server is up:
2757
- $ npx apiblaze dev 3000
2758
- $ npx apiblaze dev 3000 --capture-file traffic.jsonl
3059
+ var HELP_GROUPS = [
3060
+ { title: "Chat", commands: ["agent"] },
3061
+ { title: "Setup", commands: ["login", "create", "dev", "claim", "team", "whoami", "logout"] },
3062
+ { title: "Producer \xB7 your APIs", commands: ["projects", "tenant", "domain", "delete"] },
3063
+ { title: "Producer \xB7 change a proxy", commands: ["target", "throttle", "rename", "spec"] },
3064
+ { title: "Consumer \xB7 use an API", commands: ["consumer"] }
3065
+ ];
3066
+ function groupedCommandHelp() {
3067
+ const byName = new Map(program.commands.map((c) => [c.name(), c]));
3068
+ const width = Math.max(...HELP_GROUPS.flatMap((g) => g.commands).map((n) => n.length)) + 2;
3069
+ return HELP_GROUPS.map((g) => {
3070
+ const rows = g.commands.map((n) => {
3071
+ const c = byName.get(n);
3072
+ return c ? ` ${n.padEnd(width)}${c.description()}` : "";
3073
+ }).filter(Boolean).join("\n");
3074
+ return `${import_chalk26.default.bold(g.title)}
3075
+ ${rows}`;
3076
+ }).join("\n\n");
3077
+ }
3078
+ program.configureHelp({
3079
+ visibleCommands(cmd) {
3080
+ const all = import_commander.Help.prototype.visibleCommands.call(this, cmd);
3081
+ return cmd === program ? [] : all;
3082
+ }
3083
+ });
3084
+ program.addHelpText("after", () => `
3085
+ ${groupedCommandHelp()}
2759
3086
 
2760
- # Manage your proxies (require login; --verbose prints the equivalent API calls):
2761
- $ npx apiblaze projects
2762
- $ npx apiblaze target myapi --url https://api.example.com --env prod
2763
- $ npx apiblaze throttle myapi --rate 50 --quota 100000 --period daily
2764
- $ npx apiblaze rename myapi --display-name "My API"
2765
- $ npx apiblaze domain add myapi --domain api.mysite.com
2766
- $ npx apiblaze tenant create --name "Acme" && npx apiblaze tenant attach myapi --tenant acme
2767
- $ npx apiblaze key mint --desc "ci key"
2768
- $ npx apiblaze spec set myapi --file ./openapi.json
2769
- $ npx apiblaze delete myapi --verbose
2770
- $ npx apiblaze whoami | team | logout
3087
+ Tips:
3088
+ \u2022 Add --verbose to any command to see the equivalent API calls.
3089
+ \u2022 Run \`apiblaze <command> --help\` (e.g. \`apiblaze consumer --help\`) for sub-commands.
2771
3090
 
2772
- # Or just chat (billed per turn):
2773
- $ npx apiblaze agent
2774
- `
2775
- );
3091
+ Examples:
3092
+ $ npx apiblaze agent # just chat
3093
+ $ npx apiblaze create --target https://api.example.com # one-line API
3094
+ $ npx apiblaze dev 3000 # localhost \u2192 public URL
3095
+ $ npx apiblaze throttle myapi --rate 50 --verbose # configure + show the API call
3096
+ $ npx apiblaze consumer login # act as a consumer of your API
3097
+ `);
2776
3098
  function printError(err) {
2777
3099
  if (err instanceof ApiError) {
2778
- console.error(import_chalk25.default.red(`
3100
+ console.error(import_chalk26.default.red(`
2779
3101
  API error (${err.status}): ${err.message}`));
2780
3102
  } else if (err instanceof Error) {
2781
- console.error(import_chalk25.default.red(`
3103
+ console.error(import_chalk26.default.red(`
2782
3104
  Error: ${err.message}`));
2783
3105
  } else {
2784
- console.error(import_chalk25.default.red("\nUnknown error"));
3106
+ console.error(import_chalk26.default.red("\nUnknown error"));
2785
3107
  }
2786
3108
  }
2787
3109
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Dev tunnel CLI for APIblaze — route localhost projects through your APIblaze endpoints",
5
5
  "keywords": [
6
6
  "apiblaze",