apiblaze 0.19.16 → 0.19.18

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 +5 -0
  2. package/dist/index.js +127 -63
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,6 +33,10 @@ npx apiblaze apichat --openapi https://apiblaze.com/pokeapi_openapi.yaml
33
33
  # Make an API in one line — no account needed (prints a claim URL)
34
34
  npx apiblaze create --target https://api.example.com
35
35
 
36
+ # Or build it straight from an OpenAPI spec — a URL or a local file.
37
+ # The routes, the API version and one environment per `servers` entry all come from the spec.
38
+ npx apiblaze create --openapi https://petstore3.swagger.io/api/v3/openapi.json
39
+
36
40
  # Configure how your backend is accessed through the proxy
37
41
  npx apiblaze config
38
42
 
@@ -114,6 +118,7 @@ Every chat turn shows its cost.
114
118
  | Command | What it does |
115
119
  |---|---|
116
120
  | `apiblaze create --target <url>` | Make an API from a backend (no account needed) |
121
+ | `apiblaze create --openapi <file\|url>` | Make an API from an OpenAPI spec — a local file or a spec URL (`--openapispec` is the same flag) |
117
122
  | `apiblaze sidecar` | Route a Next.js app's external `fetch()` calls through APIblaze (one command) |
118
123
  | `apiblaze dev [port]` | Put your localhost behind a public URL |
119
124
  | `apiblaze login` / `logout` | Sign in / out (logout asks producer or consumer) |
package/dist/index.js CHANGED
@@ -137,14 +137,13 @@ async function createProxyAnonymous(body) {
137
137
  }
138
138
  return res.json();
139
139
  }
140
- async function apiFetch(path8, options = {}) {
141
- const token = getAccessToken();
140
+ async function apiFetch(path8, options = {}, auth) {
142
141
  const url = `${DASHBOARD_BASE}${path8}`;
143
142
  const res = await fetch(url, {
144
143
  ...options,
145
144
  headers: {
146
145
  "Content-Type": "application/json",
147
- Authorization: `Bearer ${token}`,
146
+ ...auth?.apiKey ? { "X-API-Key": auth.apiKey } : { Authorization: `Bearer ${getAccessToken()}` },
148
147
  ...options.headers ?? {}
149
148
  }
150
149
  });
@@ -187,8 +186,8 @@ async function getTeams() {
187
186
  return { teamId: teamId ?? "", name: t.name ?? teamId ?? "" };
188
187
  }).filter((t) => t.teamId);
189
188
  }
190
- async function getLocalhostTargets(teamId) {
191
- return apiFetch(`/api/cli/localhost-targets?team_id=${encodeURIComponent(teamId)}`);
189
+ async function getLocalhostTargets(teamId, auth) {
190
+ return apiFetch(`/api/cli/localhost-targets?team_id=${encodeURIComponent(teamId)}`, {}, auth);
192
191
  }
193
192
  async function getProjects(teamId) {
194
193
  return apiFetch(`/api/cli/projects?team_id=${encodeURIComponent(teamId)}`);
@@ -220,17 +219,17 @@ async function sidecarInitAnonymous() {
220
219
  if (!res.ok) throw new ApiError(res.status, data?.error ?? "init_failed", data);
221
220
  return data;
222
221
  }
223
- async function putDevTunnel(payload) {
222
+ async function putDevTunnel(payload, auth) {
224
223
  return apiFetch("/api/cli/dev-tunnel", {
225
224
  method: "PUT",
226
225
  body: JSON.stringify(payload)
227
- });
226
+ }, auth);
228
227
  }
229
- async function deleteDevTunnel(restore) {
228
+ async function deleteDevTunnel(restore, auth) {
230
229
  return apiFetch("/api/cli/dev-tunnel", {
231
230
  method: "DELETE",
232
231
  body: JSON.stringify({ restore })
233
- });
232
+ }, auth);
234
233
  }
235
234
  var DASHBOARD_BASE, PUBLIC_API_BASE;
236
235
  var init_api = __esm({
@@ -933,7 +932,7 @@ var import_commander = require("commander");
933
932
  var import_chalk44 = __toESM(require("chalk"));
934
933
 
935
934
  // package.json
936
- var version = "0.19.16";
935
+ var version = "0.19.18";
937
936
 
938
937
  // src/index.ts
939
938
  init_types();
@@ -1539,31 +1538,61 @@ function startTunnelClient(opts) {
1539
1538
  }
1540
1539
 
1541
1540
  // src/commands/dev.ts
1542
- async function ensureLoggedIn(interactive) {
1543
- const existing = loadCredentials();
1544
- if (existing && Date.now() < existing.expiresAt) return existing;
1545
- console.log(import_chalk4.default.yellow(existing ? "\nYour APIblaze session has expired." : "\nYou're not logged in to APIblaze."));
1546
- if (!interactive) {
1547
- console.error(import_chalk4.default.red("Run `apiblaze login` first."));
1548
- process.exit(1);
1541
+ init_anon_cred();
1542
+ function resolveDevAuth() {
1543
+ const creds = loadCredentials();
1544
+ if (creds && Date.now() < creds.expiresAt) return { mode: "session", creds };
1545
+ const anon = loadAnonCred();
1546
+ if (anon?.cp_key) return { mode: "key", apiKey: anon.cp_key, teamId: anon.team_id };
1547
+ return { mode: "none" };
1548
+ }
1549
+ async function offerAutoCreateAnon(existingKey, port) {
1550
+ if (!process.stdin.isTTY) {
1551
+ console.log(import_chalk4.default.yellow("No projects found with an internal target."));
1552
+ console.log("Run `apiblaze create --target http://localhost:" + port + "` first, or run `apiblaze dev` in an interactive terminal to create one automatically.");
1553
+ return null;
1549
1554
  }
1550
- const { doLogin } = await import_inquirer.default.prompt([{
1555
+ const { create } = await import_inquirer.default.prompt([{
1551
1556
  type: "confirm",
1552
- name: "doLogin",
1553
- message: "Log in now?",
1557
+ name: "create",
1558
+ message: `No project points at this machine. Create a quick dev proxy \u2192 ${import_chalk4.default.bold(`http://localhost:${port}`)} and tunnel it?`,
1554
1559
  default: true
1555
1560
  }]);
1556
- if (!doLogin) {
1557
- console.log(import_chalk4.default.dim("Run `apiblaze login` when you're ready."));
1558
- process.exit(0);
1561
+ if (!create) return null;
1562
+ const spinner = (0, import_ora2.default)("Creating dev proxy (anonymous workspace)...").start();
1563
+ let result = null;
1564
+ let lastErr = null;
1565
+ for (let i = 0; i < 4 && !result?.project_id; i++) {
1566
+ const body = { name: randomProxyName(), target_url: `http://localhost:${port}`, auth_type: "none" };
1567
+ try {
1568
+ result = existingKey ? await cpFetch(existingKey, "/projects", { method: "POST", body: JSON.stringify(body) }) : await createProxyAnonymous(body);
1569
+ } catch (err) {
1570
+ lastErr = err;
1571
+ }
1559
1572
  }
1560
- await runLogin();
1561
- const creds = loadCredentials();
1562
- if (!creds) {
1563
- console.error(import_chalk4.default.red("Login did not complete. Run `apiblaze login` and try again."));
1564
- process.exit(1);
1573
+ if (!result?.project_id) {
1574
+ spinner.fail("Failed to create the dev proxy.");
1575
+ throw lastErr instanceof Error ? lastErr : new Error("create failed");
1565
1576
  }
1566
- return creds;
1577
+ spinner.succeed(import_chalk4.default.green(`Created dev proxy "${result.project_id}".`));
1578
+ let apiKey = existingKey ?? "";
1579
+ if (!existingKey && result.cp_key && result.team_id) {
1580
+ saveAnonCred(result.cp_key, result.team_id, result.claim_code);
1581
+ apiKey = result.cp_key;
1582
+ console.log(import_chalk4.default.dim(" Anonymous workspace created \u2014 claim it into a free account anytime: npx apiblaze claim"));
1583
+ }
1584
+ if (!apiKey) {
1585
+ console.log(import_chalk4.default.red(" The anonymous create returned no workspace key \u2014 run `apiblaze create` once, then retry."));
1586
+ return null;
1587
+ }
1588
+ const anonTeamId = result.team_id ?? loadAnonCred()?.team_id ?? "";
1589
+ const targets = await getLocalhostTargets(anonTeamId, { apiKey }).catch(() => []);
1590
+ const created = targets.find((t) => t.projectId === result.project_id);
1591
+ if (!created) {
1592
+ console.log(import_chalk4.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
1593
+ return null;
1594
+ }
1595
+ return { target: created, apiKey, teamId: anonTeamId };
1567
1596
  }
1568
1597
  async function offerAutoCreate(teamId, port) {
1569
1598
  if (!process.stdin.isTTY) {
@@ -1627,9 +1656,10 @@ function isInternalTarget(url) {
1627
1656
  return false;
1628
1657
  }
1629
1658
  }
1630
- function printTunnelEndpoints(restore, targets) {
1659
+ function printTunnelEndpoints(restore, targets, anon) {
1631
1660
  if (restore.length === 0) return;
1632
1661
  console.log(import_chalk4.default.bold("\nYour proxy is live at:"));
1662
+ const domain2 = anon ? "tryabz.run" : "abz.run";
1633
1663
  for (const r of restore) {
1634
1664
  const label3 = targets.find((t) => t.projectId === r.projectId)?.projectName ?? r.projectId;
1635
1665
  console.log(`
@@ -1637,7 +1667,7 @@ function printTunnelEndpoints(restore, targets) {
1637
1667
  const internalEnvs = Object.keys(r.environments ?? {}).filter((e) => isInternalTarget(r.environments[e]?.target));
1638
1668
  const envs = internalEnvs.includes("dev") ? ["dev"] : internalEnvs.length ? internalEnvs : ["dev"];
1639
1669
  for (const env of envs) {
1640
- console.log(` ${import_chalk4.default.dim("API: ")} ${import_chalk4.default.cyan(`https://${r.projectId}.abz.run/${r.apiVersion}/${env}/`)}`);
1670
+ console.log(` ${import_chalk4.default.dim("API: ")} ${import_chalk4.default.cyan(`https://${r.projectId}.${domain2}/${r.apiVersion}/${env}/`)}`);
1641
1671
  }
1642
1672
  if (r.tenant) {
1643
1673
  console.log(` ${import_chalk4.default.dim("Portal:")} ${import_chalk4.default.cyan(`https://${r.tenant}.portal.apiblaze.com/${r.apiVersion}`)}`);
@@ -1659,24 +1689,34 @@ async function probeLocalServer(port) {
1659
1689
  }
1660
1690
  }
1661
1691
  async function runDev(options) {
1662
- const creds = await ensureLoggedIn(!!process.stdin.isTTY);
1663
- const linked = await resolveLinkedTeam({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
1664
- if (!linked) {
1665
- console.error(import_chalk4.default.red("No team available. Run `apiblaze login` to set up your team."));
1666
- process.exit(1);
1667
- }
1668
- const teamId = linked.teamId;
1669
- if (linked.teamId !== creds.teamId || linked.teamName !== creds.teamName) {
1670
- saveCredentials({ ...creds, teamId: linked.teamId, teamName: linked.teamName });
1671
- }
1672
- if (linked.teamName) {
1673
- console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(linked.teamName)}`);
1692
+ let auth = resolveDevAuth();
1693
+ let keyAuth = auth.mode === "key" ? { apiKey: auth.apiKey } : void 0;
1694
+ let teamId = "";
1695
+ if (auth.mode === "session") {
1696
+ const { creds } = auth;
1697
+ const linked = await resolveLinkedTeam({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
1698
+ if (!linked) {
1699
+ console.error(import_chalk4.default.red("No team available. Run `apiblaze login` to set up your team."));
1700
+ process.exit(1);
1701
+ }
1702
+ teamId = linked.teamId;
1703
+ if (linked.teamId !== creds.teamId || linked.teamName !== creds.teamName) {
1704
+ saveCredentials({ ...creds, teamId: linked.teamId, teamName: linked.teamName });
1705
+ }
1706
+ if (linked.teamName) {
1707
+ console.log(`${import_chalk4.default.cyan("\u2192")} Team: ${import_chalk4.default.bold(linked.teamName)}`);
1708
+ }
1709
+ } else if (auth.mode === "key") {
1710
+ teamId = auth.teamId;
1711
+ console.log(`${import_chalk4.default.cyan("\u2192")} Anonymous workspace ${import_chalk4.default.dim(`(${teamId})`)} \u2014 claim it anytime: ${import_chalk4.default.bold("npx apiblaze claim")}`);
1712
+ } else {
1713
+ console.log(import_chalk4.default.dim("\u2192 No account needed \u2014 this run uses an anonymous workspace (log in anytime with `apiblaze login`)."));
1674
1714
  }
1675
- let targets;
1676
- {
1715
+ let targets = [];
1716
+ if (auth.mode !== "none") {
1677
1717
  const spinner = (0, import_ora2.default)("Fetching your localhost projects...").start();
1678
1718
  try {
1679
- targets = await getLocalhostTargets(teamId);
1719
+ targets = await getLocalhostTargets(teamId, keyAuth);
1680
1720
  spinner.stop();
1681
1721
  } catch (err) {
1682
1722
  spinner.fail("Failed to fetch projects.");
@@ -1700,7 +1740,18 @@ async function runDev(options) {
1700
1740
  }
1701
1741
  selectedTargets = [match];
1702
1742
  } else if (targets.length === 0) {
1703
- const created = await offerAutoCreate(teamId, options.port);
1743
+ let created = null;
1744
+ if (auth.mode === "session") {
1745
+ created = await offerAutoCreate(teamId, options.port);
1746
+ } else {
1747
+ const made = await offerAutoCreateAnon(auth.mode === "key" ? auth.apiKey : null, options.port);
1748
+ if (made) {
1749
+ created = made.target;
1750
+ keyAuth = { apiKey: made.apiKey };
1751
+ teamId = made.teamId;
1752
+ auth = { mode: "key", apiKey: made.apiKey, teamId: made.teamId };
1753
+ }
1754
+ }
1704
1755
  if (!created) {
1705
1756
  console.log("Set a project's upstream target to localhost or a private IP, then try again.");
1706
1757
  process.exit(0);
@@ -1763,7 +1814,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
1763
1814
  try {
1764
1815
  const result = await putDevTunnel({
1765
1816
  targets: selectedTargets.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId }))
1766
- });
1817
+ }, keyAuth);
1767
1818
  restore = result.restore ?? [];
1768
1819
  connect = result.connect;
1769
1820
  spinner.succeed("Tunnel registered.");
@@ -1772,7 +1823,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
1772
1823
  throw err;
1773
1824
  }
1774
1825
  }
1775
- printTunnelEndpoints(restore, selectedTargets);
1826
+ printTunnelEndpoints(restore, selectedTargets, auth.mode === "key");
1776
1827
  const clients = connect.projects.map(
1777
1828
  (projectId) => startTunnelClient({
1778
1829
  connectUrl: connect.url,
@@ -1809,7 +1860,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
1809
1860
  console.log(import_chalk4.default.gray("\n\nShutting down..."));
1810
1861
  for (const client of clients) client.close();
1811
1862
  captureStream?.end();
1812
- await deleteDevTunnel(restore).catch(() => {
1863
+ await deleteDevTunnel(restore, keyAuth).catch(() => {
1813
1864
  });
1814
1865
  console.log(import_chalk4.default.green("Tunnel stopped."));
1815
1866
  process.exit(0);
@@ -1897,6 +1948,27 @@ function isHttpUrl(s) {
1897
1948
  return false;
1898
1949
  }
1899
1950
  }
1951
+ async function loadOpenapiSource(ref) {
1952
+ let text;
1953
+ if (isHttpUrl(ref)) {
1954
+ const res = await fetch(ref.trim(), {
1955
+ headers: { accept: "application/json, application/yaml, text/yaml, */*" }
1956
+ }).catch((err) => fail(`Could not fetch the OpenAPI spec at ${ref} \u2014 ${err.message}`));
1957
+ if (!res.ok) fail(`Could not fetch the OpenAPI spec at ${ref} (HTTP ${res.status}).`);
1958
+ text = await res.text();
1959
+ } else {
1960
+ try {
1961
+ text = import_fs2.default.readFileSync(ref, "utf8");
1962
+ } catch {
1963
+ fail(`Cannot read the OpenAPI spec "${ref}" \u2014 it is not a readable file, and not an http(s) URL.`);
1964
+ }
1965
+ }
1966
+ if (!text.trim()) fail(`The OpenAPI spec is empty: ${ref}`);
1967
+ if (!/["']?(openapi|swagger|paths)["']?\s*:/i.test(text)) {
1968
+ fail(`That does not look like an OpenAPI/Swagger document (no openapi/swagger/paths): ${ref}`);
1969
+ }
1970
+ return text;
1971
+ }
1900
1972
  function stripTenantFromPortal(devPortal) {
1901
1973
  try {
1902
1974
  const u = new URL(devPortal);
@@ -2005,12 +2077,7 @@ async function runCreate(opts = {}) {
2005
2077
  let openapiContent = null;
2006
2078
  if (opts.openapi !== void 0) {
2007
2079
  if (opts.target !== void 0) fail("Provide only one of --target or --openapi.");
2008
- try {
2009
- openapiContent = import_fs2.default.readFileSync(opts.openapi, "utf8");
2010
- } catch {
2011
- fail(`Cannot read --openapi file: ${opts.openapi}`);
2012
- }
2013
- if (!openapiContent || !openapiContent.trim()) fail(`--openapi file is empty: ${opts.openapi}`);
2080
+ openapiContent = await loadOpenapiSource(opts.openapi);
2014
2081
  }
2015
2082
  let targetUrl = "";
2016
2083
  if (openapiContent) {
@@ -2135,11 +2202,7 @@ async function runAnonymousCreate(opts) {
2135
2202
  fail("Proxy name must be at least 3 characters (letters and digits only).");
2136
2203
  }
2137
2204
  if (opts.openapi !== void 0) {
2138
- try {
2139
- body.openapi = import_fs2.default.readFileSync(opts.openapi, "utf8");
2140
- } catch {
2141
- fail(`Cannot read --openapi file: ${opts.openapi}`);
2142
- }
2205
+ body.openapi = await loadOpenapiSource(opts.openapi);
2143
2206
  }
2144
2207
  if (opts.target && !isHttpUrl(opts.target)) fail("--target must be a valid http(s) URL.");
2145
2208
  let target = opts.target?.trim() || (typeof body.target === "string" ? body.target : "") || (typeof body.target_url === "string" ? body.target_url : "");
@@ -7416,9 +7479,9 @@ program.command("login").description("Authenticate with APIblaze").option("--tea
7416
7479
  process.exit(1);
7417
7480
  }
7418
7481
  });
7419
- program.command("create").description("Create a new API proxy (no login needed \u2014 without auth it creates an anonymous proxy and prints a claim URL)").option("--name <name>", "Proxy name (becomes <name>.abz.run)").option("--target <url>", "Target URL to forward requests to").option("--openapi <file>", "Create FROM an OpenAPI file instead of --target: routes, API version and environments (one per `servers` entry) all come from the spec").option("--team <id|name>", "Team to create under (defaults to your active team)").option("--auth <type>", "Auth type: api_key | none | oauth", "api_key").option("--identified", "Require every call to identify its end user (X-End-User-Id or a login token); unattributed calls are rejected").option("--iam", "Turn IAM on for the proxy's tenant so users & groups apply to identified calls").option("--apiversion <version>", "API version to create (e.g. 2.0.0). Creating a new version of a proxy you own adds a version to the existing project.").option("--tenant <slug>", "Tenant to attach the proxy to (created if new). Omitted \u2192 your team's most-recently-used tenant.").option("--product <slug>", "Product tag to group this project under in the portal (team-scoped; anonymous create). Defaults to your team's existing/placeholder tag.").option("--display-name <name>", "Human-friendly display name").option("--subdomain <slug>", "Explicit subdomain (defaults to --name)").option("--config <file>", "JSON file with the full request body (anonymous create): requests_auth, login providers + client/server token types, scopes, callback URLs, etc. See apiblaze_anonymous.yaml. Flags override its fields.").option("-y, --yes", "Skip the confirmation prompt").option("--new-session", "Start a fresh anonymous session (do not group with prior anonymous creates)").option("--json", "Output machine-readable JSON (non-interactive)").action(async (opts) => {
7482
+ program.command("create").description("Create a new API proxy (no login needed \u2014 without auth it creates an anonymous proxy and prints a claim URL)").option("--name <name>", "Proxy name (becomes <name>.abz.run)").option("--target <url>", "Target URL to forward requests to").option("--openapi <file|url>", "Create FROM an OpenAPI spec instead of --target \u2014 a local file or a URL (e.g. https://pokeapi.co/openapi.yaml). Routes, API version and environments (one per `servers` entry) all come from the spec").option("--openapispec <file|url>", "Alias for --openapi").option("--team <id|name>", "Team to create under (defaults to your active team)").option("--auth <type>", "Auth type: api_key | none | oauth", "api_key").option("--identified", "Require every call to identify its end user (X-End-User-Id or a login token); unattributed calls are rejected").option("--iam", "Turn IAM on for the proxy's tenant so users & groups apply to identified calls").option("--apiversion <version>", "API version to create (e.g. 2.0.0). Creating a new version of a proxy you own adds a version to the existing project.").option("--tenant <slug>", "Tenant to attach the proxy to (created if new). Omitted \u2192 your team's most-recently-used tenant.").option("--product <slug>", "Product tag to group this project under in the portal (team-scoped; anonymous create). Defaults to your team's existing/placeholder tag.").option("--display-name <name>", "Human-friendly display name").option("--subdomain <slug>", "Explicit subdomain (defaults to --name)").option("--config <file>", "JSON file with the full request body (anonymous create): requests_auth, login providers + client/server token types, scopes, callback URLs, etc. See apiblaze_anonymous.yaml. Flags override its fields.").option("-y, --yes", "Skip the confirmation prompt").option("--new-session", "Start a fresh anonymous session (do not group with prior anonymous creates)").option("--json", "Output machine-readable JSON (non-interactive)").action(async (opts) => {
7420
7483
  try {
7421
- await runCreate(opts);
7484
+ await runCreate({ ...opts, openapi: opts.openapi ?? opts.openapispec });
7422
7485
  } catch (err) {
7423
7486
  await printError(err);
7424
7487
  process.exit(1);
@@ -7588,6 +7651,7 @@ Examples:
7588
7651
  $ npx apiblaze apichat --openapi https://pokeapi.co/openapi.yaml # chat with any API
7589
7652
  $ npx apiblaze agent # just chat
7590
7653
  $ npx apiblaze create --target https://api.example.com # one-line API
7654
+ $ npx apiblaze create --openapi https://pokeapi.co/openapi.yaml # build it from a spec URL
7591
7655
  $ npx apiblaze dev 3000 # localhost \u2192 public URL
7592
7656
  $ npx apiblaze throttle myapi --rate 50 --verbose # configure + show the API call
7593
7657
  $ npx apiblaze consumer login # act as a consumer of your API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.19.16",
3
+ "version": "0.19.18",
4
4
  "description": "APIblaze CLI — Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
5
5
  "keywords": [
6
6
  "apiblaze",