apiblaze 0.20.2 → 0.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +158 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -932,7 +932,7 @@ var import_commander = require("commander");
932
932
  var import_chalk51 = __toESM(require("chalk"));
933
933
 
934
934
  // package.json
935
- var version = "0.20.2";
935
+ var version = "0.20.4";
936
936
 
937
937
  // src/index.ts
938
938
  init_types();
@@ -3666,7 +3666,7 @@ async function runRecipeInstall(nameArg, opts) {
3666
3666
  const assumeYes = !!opts.yes;
3667
3667
  const interactive = !!process.stdin.isTTY && !assumeYes;
3668
3668
  const caller = requireCallerForInstall();
3669
- const recipe = await fetchRecipeFile(ref, caller);
3669
+ let recipe = await fetchRecipeFile(ref, caller);
3670
3670
  const pinned = `${recipe.name}@${recipe.revision}`;
3671
3671
  const picked = await chooseTeam(caller, opts.team, interactive);
3672
3672
  const teamId = picked.teamId;
@@ -3677,6 +3677,7 @@ async function runRecipeInstall(nameArg, opts) {
3677
3677
  if (recipe.summary) console.log(` ${recipe.summary}`);
3678
3678
  console.log();
3679
3679
  let project;
3680
+ let reuseTenant = false;
3680
3681
  let apiVersion;
3681
3682
  let tenant2;
3682
3683
  let reinstall = false;
@@ -3691,15 +3692,19 @@ async function runRecipeInstall(nameArg, opts) {
3691
3692
  } else {
3692
3693
  project = await chooseProxyName(ref.slug, opts.as, teamId, interactive);
3693
3694
  apiVersion = recipe.api_version || "1.0.0";
3694
- tenant2 = `${project}users`;
3695
- await assertTenantFree(tenant2, teamId);
3696
- printSummary(recipe, project, tenant2);
3695
+ const chosenTenant = await chooseTenant(caller, project, teamId, opts.tenant, interactive);
3696
+ tenant2 = chosenTenant.tenant;
3697
+ reuseTenant = chosenTenant.existing;
3698
+ printSummary(recipe, project, tenant2, reuseTenant);
3697
3699
  if (!assumeYes && !await confirmYesNo(" continue?", interactive)) {
3698
3700
  console.log(import_chalk26.default.yellow(" Cancelled \u2014 nothing was created."));
3699
3701
  return;
3700
3702
  }
3701
3703
  }
3702
3704
  const ctx = { project, tenant: tenant2 };
3705
+ if (reuseTenant) {
3706
+ recipe = { ...recipe, auth: { app_client: null, provider: null, key_types: [] }, questions: [] };
3707
+ }
3703
3708
  const declined = assumeYes ? declineAll(recipe.requires_confirmation ?? []) : await askConsent(recipe.requires_confirmation ?? [], interactive);
3704
3709
  const answers = await askQuestions(recipe.questions ?? [], ctx, interactive);
3705
3710
  const started = Date.now();
@@ -4102,7 +4107,7 @@ async function assertTenantFree(tenant2, teamId) {
4102
4107
  );
4103
4108
  }
4104
4109
  }
4105
- function printSummary(recipe, project, tenant2) {
4110
+ function printSummary(recipe, project, tenant2, reuseTenant = false) {
4106
4111
  const hosts = upstreamHosts(recipe);
4107
4112
  const routes = countRoutes(recipe.openapi);
4108
4113
  const keyTypes = recipe.auth?.key_types?.length ?? 0;
@@ -4110,7 +4115,11 @@ function printSummary(recipe, project, tenant2) {
4110
4115
  console.log();
4111
4116
  if (hosts.length) console.log(` ${import_chalk26.default.dim("sends requests to")} ${hosts.join(", ")}`);
4112
4117
  console.log(` ${import_chalk26.default.dim("creates ")} ${describeCreates(keyTypes, routes, recipe.settings.mcp_enabled === true)}`);
4113
- console.log(` ${import_chalk26.default.dim("asks you for ")} ${questions} value${questions === 1 ? "" : "s"}`);
4118
+ console.log(` ${import_chalk26.default.dim("asks you for ")} ${reuseTenant ? "nothing" : `${questions} value${questions === 1 ? "" : "s"}`}`);
4119
+ if (reuseTenant) {
4120
+ console.log(` ${import_chalk26.default.dim("users live in ")} ${tenant2} ${import_chalk26.default.dim("(existing \u2014 its login is used as it is)")}`);
4121
+ console.log(import_chalk26.default.dim(` ${" ".repeat(17)} the recipe's own login setup is not applied`));
4122
+ }
4114
4123
  if (recipe.settings.publish_openapi === true) {
4115
4124
  console.log(` ${import_chalk26.default.dim("makes public ")} this API's spec, at ${project}.abz.run/${recipe.api_version}/openapi.json`);
4116
4125
  }
@@ -4300,6 +4309,55 @@ async function chooseTeam(caller, optTeam, interactive) {
4300
4309
  const match = teams.find((t) => t.teamId === picked);
4301
4310
  return { teamId: picked, teamName: match?.name, chosen: true };
4302
4311
  }
4312
+ async function chooseTenant(caller, project, teamId, optTenant, interactive) {
4313
+ const fresh = `${project}users`;
4314
+ const owned = await teamTenants(caller, teamId);
4315
+ const isOwned = (t2) => owned.some((o) => o === t2);
4316
+ if (optTenant) {
4317
+ const t2 = normalizeName2(optTenant);
4318
+ if (isOwned(t2)) return { tenant: t2, existing: true };
4319
+ await assertTenantFree(t2, teamId);
4320
+ return { tenant: t2, existing: false };
4321
+ }
4322
+ if (!interactive || !owned.length) {
4323
+ await assertTenantFree(fresh, teamId);
4324
+ return { tenant: fresh, existing: false };
4325
+ }
4326
+ const { default: inquirer3 } = await import("inquirer");
4327
+ const { picked } = await inquirer3.prompt([{
4328
+ type: "list",
4329
+ name: "picked",
4330
+ message: " where should this proxy's users live?",
4331
+ default: "__new__",
4332
+ choices: [
4333
+ { name: `create ${fresh} ${import_chalk26.default.dim("(a fresh tenant, set up by the recipe)")}`, value: "__new__" },
4334
+ new inquirer3.Separator(import_chalk26.default.dim(" \u2014 or reuse one you already have \u2014")),
4335
+ ...owned.map((t2) => ({
4336
+ name: `${t2} ${import_chalk26.default.dim("(existing \u2014 its own login is kept)")}`,
4337
+ value: t2
4338
+ }))
4339
+ ]
4340
+ }]);
4341
+ if (picked !== "__new__") return { tenant: picked, existing: true };
4342
+ const { name } = await inquirer3.prompt([{
4343
+ type: "input",
4344
+ name: "name",
4345
+ message: " new tenant name",
4346
+ default: fresh,
4347
+ filter: (v) => normalizeName2(v)
4348
+ }]);
4349
+ const t = normalizeName2(String(name || fresh));
4350
+ await assertTenantFree(t, teamId);
4351
+ return { tenant: t, existing: false };
4352
+ }
4353
+ async function teamTenants(caller, teamId) {
4354
+ const out = await producer(caller, {
4355
+ method: "GET",
4356
+ path: `/teams/${encodeURIComponent(teamId)}/tenants`,
4357
+ summary: "List the team's tenants"
4358
+ }).catch(() => ({ tenants: [] }));
4359
+ return (out?.tenants ?? []).map((t) => typeof t === "string" ? t : t.disabled ? "" : t.tenant_name ?? t.name ?? "").filter(Boolean);
4360
+ }
4303
4361
 
4304
4362
  // src/commands/recipe-publish.ts
4305
4363
  var import_chalk27 = __toESM(require("chalk"));
@@ -9830,7 +9888,7 @@ function printResidue(report, applied) {
9830
9888
  for (const e of ghosts?.errors ?? []) console.log(import_chalk50.default.red(` error: ${e}`));
9831
9889
  console.log();
9832
9890
  }
9833
- async function runOp(sub, opts = {}) {
9891
+ async function runOp(sub, opts = {}, view) {
9834
9892
  if (!loadCredentials()) {
9835
9893
  console.log(import_chalk50.default.dim("Not logged in. Run `apiblaze login`."));
9836
9894
  return;
@@ -9846,6 +9904,9 @@ async function runOp(sub, opts = {}) {
9846
9904
  console.log(` ${import_chalk50.default.cyan("apiblaze op residue")} external-store residue report (Upstash + Neon/OpenFGA, dry-run)`);
9847
9905
  console.log(` ${import_chalk50.default.cyan("apiblaze op sweep")} delete the orphans the report shows (asks first; ${import_chalk50.default.dim("-y to skip")})`);
9848
9906
  console.log(` ${import_chalk50.default.cyan("apiblaze op credits")} list credit wallets`);
9907
+ console.log(` ${import_chalk50.default.cyan("apiblaze op latency")} which FEATURE is slow, and what inside it ${import_chalk50.default.dim("(--hours N --team T --project P)")}`);
9908
+ console.log(` ${import_chalk50.default.cyan("apiblaze op latency slow")} the slowest requests, with a cf-ray to drill into`);
9909
+ console.log(` ${import_chalk50.default.cyan("apiblaze op latency llm")} chat timing by provider/model, tokens, credit-reserve cost`);
9849
9910
  console.log(import_chalk50.default.dim(` (to prune all non-CP data: run scripts/nuke-but-cp.sh --apply --sweep in the repo)
9850
9911
  `));
9851
9912
  return;
@@ -9893,6 +9954,93 @@ async function runOp(sub, opts = {}) {
9893
9954
  }
9894
9955
  return;
9895
9956
  }
9957
+ // ── LATENCY (specs/latency/ §7.4) ─────────────────────────────────────
9958
+ // The blame table first: level 1 (which feature) and level 2 (what inside
9959
+ // it) in one row. `upstream` is excluded from the blame column on purpose —
9960
+ // it is the biggest feature on almost every request, so blaming it would
9961
+ // report "upstream, 94%" forever and tell an operator nothing. The
9962
+ // summary's "us or them" split is where upstream shows up.
9963
+ case "latency": {
9964
+ const qs = new URLSearchParams();
9965
+ const FILTERS = [
9966
+ ["hours", "hours"],
9967
+ ["team", "team"],
9968
+ ["project", "project"],
9969
+ ["apiVersion", "api_version"],
9970
+ ["tenant", "tenant"],
9971
+ ["route", "route"],
9972
+ ["environment", "environment"],
9973
+ ["statusClass", "status_class"],
9974
+ ["minMs", "min_ms"]
9975
+ ];
9976
+ for (const [optKey, queryKey] of FILTERS) {
9977
+ const v = opts[optKey];
9978
+ if (v !== void 0 && v !== null && v !== "") qs.set(queryKey, String(v));
9979
+ }
9980
+ const q = qs.toString() ? `?${qs}` : "";
9981
+ const which = view;
9982
+ if (which === "slow") {
9983
+ const data = await opCall({ method: "GET", path: `/operator/latency/slow${q}`, summary: "slowest requests" });
9984
+ if (opts.json) return void console.log(JSON.stringify(data, null, 2));
9985
+ const rows2 = data?.rows ?? [];
9986
+ if (!rows2.length) return void console.log(import_chalk50.default.dim("No requests over the threshold in that window."));
9987
+ console.log(import_chalk50.default.bold(`
9988
+ Slowest requests \u2014 last ${data.window_hours}h, over ${data.min_ms}ms
9989
+ `));
9990
+ console.log(import_chalk50.default.dim(" total ours theirs blame request (cf-ray)"));
9991
+ for (const r of rows2.slice(0, 30)) {
9992
+ console.log(
9993
+ ` ${String(Math.round(r.duration_ms)).padStart(6)} ${String(Math.round(r.gateway_ms)).padStart(5)} ${String(Math.round(r.upstream_ttfb_ms)).padStart(6)} ${import_chalk50.default.yellow(`${r.slow_gw_feature || "-"}\u2192${r.slow_dep || "-"}`.padEnd(20))} ${import_chalk50.default.dim(r.request_id || "")}`
9994
+ );
9995
+ }
9996
+ console.log(import_chalk50.default.dim(`
9997
+ Take a cf-ray to \`apiblaze logs\` for the exact per-feature breakdown \u2014 that row is unsampled.
9998
+ `));
9999
+ return;
10000
+ }
10001
+ if (which === "llm") {
10002
+ const data = await opCall({ method: "GET", path: `/operator/latency/llm${q}`, summary: "llm latency" });
10003
+ if (opts.json) return void console.log(JSON.stringify(data, null, 2));
10004
+ const rows2 = data?.rows ?? [];
10005
+ if (!rows2.length) return void console.log(import_chalk50.default.dim("No LLM traffic in that window."));
10006
+ console.log(import_chalk50.default.bold(`
10007
+ LLM timing \u2014 last ${data.window_hours}h
10008
+ `));
10009
+ console.log(import_chalk50.default.dim(" requests gen p95 reserve p95 in/out tokens p95 model"));
10010
+ for (const r of rows2) {
10011
+ console.log(
10012
+ ` ${String(Math.round(r.requests)).padStart(8)} ${String(Math.round(r.gen_p95_ms)).padStart(6)}ms ${String(Math.round(r.reserve_p95_ms)).padStart(9)}ms ${String(Math.round(r.input_tokens_p95)).padStart(6)}/${String(Math.round(r.output_tokens_p95)).padEnd(6)} ${r.model || "-"}`
10013
+ );
10014
+ }
10015
+ console.log(import_chalk50.default.dim(`
10016
+ ${data.note}
10017
+ `));
10018
+ return;
10019
+ }
10020
+ const [blame, summary] = await Promise.all([
10021
+ opCall({ method: "GET", path: `/operator/latency${q}`, summary: "latency blame table" }),
10022
+ opCall({ method: "GET", path: `/operator/latency/summary${q}`, summary: "latency summary" })
10023
+ ]);
10024
+ if (opts.json) return void console.log(JSON.stringify({ blame, summary }, null, 2));
10025
+ const ov = summary?.apiblaze_overhead_ms ?? {};
10026
+ console.log(import_chalk50.default.bold(`
10027
+ Latency \u2014 last ${summary?.window_hours ?? "?"}h, ${Number(summary?.requests ?? 0).toLocaleString()} requests
10028
+ `));
10029
+ console.log(` ${import_chalk50.default.bold("apiblaze overhead")} p50 ${String(ov.p50 ?? 0).padStart(5)}ms p95 ${String(ov.p95 ?? 0).padStart(6)}ms p99 ${String(ov.p99 ?? 0).padStart(6)}ms ${import_chalk50.default.dim("\u2190 ours")}`);
10030
+ console.log(` ${import_chalk50.default.bold("upstream ttfb ")} ${" ".repeat(24)}p95 ${String(summary?.upstream_ttfb_p95_ms ?? 0).padStart(6)}ms ${import_chalk50.default.dim("\u2190 theirs")}`);
10031
+ console.log(import_chalk50.default.dim(` (per-request percentiles \u2014 never subtract one from the other)
10032
+ `));
10033
+ const rows = blame?.blame ?? [];
10034
+ if (!rows.length) return void console.log(import_chalk50.default.dim("No latency rows in that window."));
10035
+ console.log(import_chalk50.default.bold(" Which feature ate the time, and what inside it\n"));
10036
+ console.log(import_chalk50.default.dim(" share p95 feature \u2192 dependency"));
10037
+ for (const r of rows.slice(0, 15)) {
10038
+ const share = `${(r.share * 100).toFixed(1)}%`;
10039
+ console.log(` ${share.padStart(6)} ${String(r.p95_ms).padStart(6)}ms ${import_chalk50.default.yellow(r.feature)} ${import_chalk50.default.dim("\u2192")} ${import_chalk50.default.cyan(r.dep)}`);
10040
+ }
10041
+ console.log("");
10042
+ return;
10043
+ }
9896
10044
  default:
9897
10045
  console.log(import_chalk50.default.red(`Unknown op subcommand '${sub}'. Run \`apiblaze op\` for the menu.`));
9898
10046
  }
@@ -10008,7 +10156,7 @@ program.command("projects").description("List the projects in your team").action
10008
10156
  });
10009
10157
  program.command("search").description("Find a published recipe \u2014 a whole working proxy someone else set up").argument("[query]", "Words to match against a recipe name or summary").option("--limit <n>", "Maximum results (default 25)").option("--json", "Output machine-readable JSON").action(action((query, opts) => runRecipeSearch(query, opts)));
10010
10158
  program.command("show").description("Read a recipe before you install it (upstreams, questions, transforms, spec)").argument("<recipe>", "Recipe name, e.g. @julien/gmail or @julien/gmail@2").option("--settings", "Print the settings section").option("--auth", "Print the auth section (login provider, key types)").option("--transforms", "Print the transform rules and mapping tables").option("--openapi", "Print the OpenAPI document").option("--json", "Output machine-readable JSON").action(action((recipe, opts) => runRecipeShow(recipe, opts)));
10011
- program.command("install").description("Create a proxy from a recipe, with your own credentials").argument("<recipe>", "Recipe name, e.g. @julien/gmail or @julien/gmail@2 to pin a revision").option("--as <name>", "Proxy name (skips the name prompt)").option("--into <proxy>", "Reinstall over an existing proxy \u2014 replaces its settings, transforms and spec").option("--team <id|name>", "Team the proxy is created in (you are asked when you belong to several)").option("--apiversion <v>", "API version of the proxy named by --into").option("-y, --yes", "Skip the routine prompts (installs WITHOUT anything needing consent)").action(action((recipe, opts) => runRecipeInstall(recipe, opts)));
10159
+ program.command("install").description("Create a proxy from a recipe, with your own credentials").argument("<recipe>", "Recipe name, e.g. @julien/gmail or @julien/gmail@2 to pin a revision").option("--as <name>", "Proxy name (skips the name prompt)").option("--into <proxy>", "Reinstall over an existing proxy \u2014 replaces its settings, transforms and spec").option("--team <id|name>", "Team the proxy is created in (you are asked when you belong to several)").option("--apiversion <v>", "API version of the proxy named by --into").option("--tenant <slug>", "Where this proxy's users live. One of yours is reused as it is; a new name is created").option("-y, --yes", "Skip the routine prompts (installs WITHOUT anything needing consent)").action(action((recipe, opts) => runRecipeInstall(recipe, opts)));
10012
10160
  program.command("publish").description("Publish one of your proxies as a recipe others can install").argument("<project>", "Project name or id (see `apiblaze projects`)").option("--as <name>", "Recipe name. Defaults to your GitHub handle + the proxy name; pass `pokedex` for just the name, or `@acme/pokedex` to set both").option("--private", "Only your team can see or install it (default is public)").option("--tenant <slug>", "Which tenant's auth setup the recipe carries (asked when a proxy has several)").option("--display-name <text>", "Human-readable title").option("--summary <text>", "One-line description shown in search").option("--license <id>", "Licence identifier, e.g. MIT").option("--team <id|name>", "Team the project is in (defaults to active team)").option("--apiversion <v>", "API version to publish (defaults to the first match)").option("-y, --yes", "Skip routine confirmations (never accepts scanner findings)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runRecipePublish(project, opts)));
10013
10161
  program.command("withdraw").description("Permanently delete one published recipe revision").argument("<recipe>", "Recipe revision, e.g. @julien/gmail@3").option("--team <id|name>", "Team that published it (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((recipe, opts) => runRecipeWithdraw(recipe, opts)));
10014
10162
  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)));
@@ -10056,7 +10204,7 @@ apikeys.command("list").description("List control-plane API keys in your team").
10056
10204
  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, cmd) => runKeyMint({ ...cmd.parent?.opts(), ...opts })));
10057
10205
  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, cmd) => runKeyRevoke(keyId, { ...cmd.parent?.opts(), ...opts })));
10058
10206
  program.addCommand(apikeys, { hidden: true });
10059
- var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "residue | sweep | credits (omit for the menu)").option("-y, --yes", "Skip the sweep confirmation prompt").option("--json", "Output machine-readable JSON").action(action((sub, opts) => runOp(sub, opts)));
10207
+ var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "residue | sweep | credits | latency (omit for the menu)").argument("[view]", "for `latency`: slow | llm (omit for the blame table)").option("-y, --yes", "Skip the sweep confirmation prompt").option("--json", "Output machine-readable JSON").option("--hours <n>", "Look-back window in hours (default 1)").option("--team <id>", "Filter to one team").option("--project <name>", "Filter to one proxy").option("--api-version <v>", "Filter to one API version").option("--tenant <name>", "Filter to one tenant").option("--route <path>", "Filter to one route").option("--environment <env>", "Filter to one environment").option("--status-class <c>", "Filter to a status class (2xx/4xx/5xx)").option("--min-ms <n>", "For `latency slow`: minimum duration (default 1000)").action(action((sub, view, opts) => runOp(sub, opts, view)));
10060
10208
  program.addCommand(op, { hidden: true });
10061
10209
  var spec = program.command("spec").description("View or update a proxy's OpenAPI spec (or build one by chatting: apiblaze agent openapi)");
10062
10210
  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)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
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",