apiblaze 0.21.13 → 0.21.14

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 +75 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -978,7 +978,7 @@ async function runAdminsAdd(email, opts) {
978
978
  else if (!opts.quiet) {
979
979
  console.log(import_chalk15.default.dim(" Reload the Users & Groups widget \u2014 access flips from \u201Cpending\u201D to ready."));
980
980
  console.log(import_chalk15.default.dim(` The widget's admin lane takes a personal key, one per admin \u2014 mint it with:`));
981
- console.log(import_chalk15.default.dim(` npx apiblaze apikeys mint --for ${email} --tenant ${tenant2}`));
981
+ console.log(import_chalk15.default.dim(` npx apiblaze apikeys mint --for ${email} --tenant ${tenant2} --admin`));
982
982
  console.log(import_chalk15.default.dim(" (a backend/tenant key with X-End-User-Id is refused there: widget_key_must_be_personal)."));
983
983
  }
984
984
  return out ?? { ok: true };
@@ -1342,7 +1342,7 @@ var import_commander = require("commander");
1342
1342
  var import_chalk64 = __toESM(require("chalk"));
1343
1343
 
1344
1344
  // package.json
1345
- var version = "0.21.13";
1345
+ var version = "0.21.14";
1346
1346
 
1347
1347
  // src/index.ts
1348
1348
  init_types();
@@ -4847,17 +4847,18 @@ async function mintConsumerKey(o) {
4847
4847
  if (!tenant2) {
4848
4848
  throw new Error(who ? "--for needs a tenant: add --tenant <slug> (the tenant whose person this key is for)." : `a ${o.holder ?? "backend"} key is a consumer key: add --tenant <slug> (the tenant it calls).`);
4849
4849
  }
4850
- const environment = (o.env ?? "").trim() || "dev";
4850
+ if (o.admin && !who) throw new Error("--admin needs --for <admin email>: the admin lane only opens for a key issued to that admin.");
4851
+ const environment = (o.env ?? "").trim() || (o.admin ? "prod" : "dev");
4851
4852
  const holder = who ? "person" : o.holder ?? "backend";
4852
- const permissions = holder === "person" ? void 0 : holderPermissions(holder);
4853
+ const permissions = holder === "person" ? o.admin ? ["call", "manage-access"] : void 0 : holderPermissions(holder);
4853
4854
  const caller = requireCaller();
4854
4855
  const { teamId } = await resolveActingTeam(caller, o.team);
4855
4856
  const body = { tenant: tenant2, environment };
4856
4857
  if (o.desc) body.description = o.desc;
4857
4858
  if (o.expiresDays) body.expires_in_seconds = Number(o.expiresDays) * 24 * 60 * 60;
4858
4859
  if (permissions) {
4859
- body.holder = holder;
4860
4860
  body.permissions = permissions;
4861
+ if (holder !== "person") body.holder = holder;
4861
4862
  }
4862
4863
  const headers = { "Content-Type": "application/json" };
4863
4864
  if (who) {
@@ -4925,6 +4926,7 @@ async function runConsumerKeyMint(opts) {
4925
4926
  team: opts.team,
4926
4927
  desc: opts.desc,
4927
4928
  expiresDays: opts.expiresDays,
4929
+ admin: opts.admin,
4928
4930
  ...kind === "person" ? {} : { holder: kind }
4929
4931
  });
4930
4932
  spinner.succeed(who ? `Key minted for ${who}.` : `${kind === "client" ? "Client" : "Backend"} key minted.`);
@@ -4942,6 +4944,7 @@ async function runConsumerKeyMint(opts) {
4942
4944
  if (out.expiresAt) console.log(` ${import_chalk14.default.dim("expires:")} ${out.expiresAt}`);
4943
4945
  if (out.holder === "person") {
4944
4946
  console.log(` ${import_chalk14.default.dim("issued to:")} ${out.person} ${import_chalk14.default.dim("\u2014 calls made with this key count as this person; an X-End-User-Id naming anyone else is refused")}`);
4947
+ if (opts.admin) console.log(` ${import_chalk14.default.dim("admin key:")} ${import_chalk14.default.dim("manage-access \u2014 put it in APIBLAZE_CP_KEY for the Users & Groups widget, one key per admin")}`);
4945
4948
  } else {
4946
4949
  console.log(` ${import_chalk14.default.cyan(holderLine(out.holder))}`);
4947
4950
  console.log(import_chalk14.default.dim(out.holder === "backend" ? " (For a key you ship to users, re-mint with --client; to bind one to a person, --for <email>.)" : " (For your server, re-mint with --backend; to bind one to a person, --for <email>.)"));
@@ -10341,6 +10344,7 @@ function describe(r) {
10341
10344
  return `set ${a.destination} = ${secret ? "(secret)" : a.value}`;
10342
10345
  }
10343
10346
  if (a.type === "remove") return `remove ${a.field}`;
10347
+ if (a.type === "map") return `map ${a.source} \u2192 ${a.destination}${a.lookup?.mapping_id ? " (through a mapping table)" : ""}`;
10344
10348
  return a.type;
10345
10349
  }
10346
10350
  async function runTransformList(project, opts) {
@@ -10405,6 +10409,66 @@ async function runTransformRemove(project, nameOrId, opts) {
10405
10409
  if (opts.json) console.log(JSON.stringify({ ok: true, removed: rule.id }));
10406
10410
  else console.log(`${import_chalk44.default.green("\u2713")} Removed transform ${import_chalk44.default.bold(rule.name)} from ${t.proj.projectName}.`);
10407
10411
  }
10412
+ function tablesBase(t) {
10413
+ return t.base.replace(/\/transforms$/, "/mappings");
10414
+ }
10415
+ async function listTables(t) {
10416
+ const out = await producer(t.caller, {
10417
+ method: "GET",
10418
+ path: tablesBase(t),
10419
+ summary: `List mapping tables for ${t.proj.projectName}`
10420
+ });
10421
+ return out?.tables ?? [];
10422
+ }
10423
+ function parseEntries(pairs) {
10424
+ return pairs.map((p) => {
10425
+ const i = p.indexOf("=");
10426
+ if (i <= 0) throw new Error(`"${p}" is not a from=to pair (e.g. FR=EUR).`);
10427
+ return { from: p.slice(0, i), to: p.slice(i + 1) };
10428
+ });
10429
+ }
10430
+ var FIELD_REF = /^(header|query|param|bodyvar|jwt):.+/;
10431
+ async function runTransformMapping(project, table, pairs, opts) {
10432
+ if (!pairs.length) throw new Error("Give at least one from=to pair, e.g. FR=EUR US=USD.");
10433
+ const entries2 = parseEntries(pairs);
10434
+ const t = await target(project, opts);
10435
+ const existing = (await listTables(t)).find((m) => m.name === table);
10436
+ const body = { name: table, entries: entries2, ...opts.secret ? { encrypt_values: true } : {} };
10437
+ const summary = `${existing ? "Replace" : "Create"} mapping table "${table}" (${entries2.length} entr${entries2.length === 1 ? "y" : "ies"})`;
10438
+ const out = await producer(t.caller, existing ? { method: "PUT", path: `${tablesBase(t)}/${encodeURIComponent(existing.id)}`, body, summary } : { method: "POST", path: tablesBase(t), body, summary });
10439
+ if (opts.json) {
10440
+ console.log(JSON.stringify(out ?? { ok: true }));
10441
+ return;
10442
+ }
10443
+ console.log(`${import_chalk44.default.green("\u2714")} ${summary} \u2014 ${t.proj.projectName} v${t.proj.apiVersion}.`);
10444
+ for (const e of entries2) console.log(import_chalk44.default.dim(` ${e.from} \u2192 ${opts.secret ? "(secret)" : e.to}`));
10445
+ }
10446
+ async function runTransformMap(project, source, destination, opts) {
10447
+ if (!FIELD_REF.test(source)) throw new Error(`Source "${source}" must look like header:X-Country, query:country or bodyvar:order.country.`);
10448
+ if (!/^(header|query|bodyvar):.+/.test(destination)) throw new Error(`Destination "${destination}" must look like header:X-Currency, query:currency or bodyvar:order.currency.`);
10449
+ if (!opts.table) throw new Error("--table <name> is required: the mapping table to look the value up in (create it with `transform mapping`).");
10450
+ const onNoMatch = opts.onNoMatch ?? "skip";
10451
+ if (!["skip", "fail", "passthrough"].includes(onNoMatch)) throw new Error("--on-no-match is skip, fail or passthrough.");
10452
+ const t = await target(project, opts);
10453
+ const table = (await listTables(t)).find((m) => m.name === opts.table);
10454
+ if (!table) throw new Error(`No mapping table "${opts.table}" on ${t.proj.projectName}. Create it: npx apiblaze transform mapping ${project} ${opts.table} FR=EUR \u2026`);
10455
+ const name = `map ${source} to ${destination}`;
10456
+ const action2 = {
10457
+ type: "map",
10458
+ source,
10459
+ destination,
10460
+ lookup: { mapping_id: table.id, on_no_match: onNoMatch }
10461
+ };
10462
+ const existing = (await listRules(t)).find((r) => r.name === name && r.phase === "request");
10463
+ const summary = `${existing ? "Update" : "Add"} transform: ${source} \u2192 ${destination} through "${table.name}"`;
10464
+ const out = await producer(t.caller, existing ? { method: "PUT", path: `${t.base}/${encodeURIComponent(existing.id)}`, body: { action: action2, enabled: true }, summary } : { method: "POST", path: t.base, body: { name, action: action2, phase: "request", enabled: true }, summary });
10465
+ if (opts.json) {
10466
+ console.log(JSON.stringify(out ?? { ok: true }));
10467
+ return;
10468
+ }
10469
+ console.log(`${import_chalk44.default.green("\u2714")} ${summary} \u2014 ${t.proj.projectName} v${t.proj.apiVersion}.`);
10470
+ console.log(import_chalk44.default.dim(` A value not in the table: ${onNoMatch === "skip" ? "nothing is written" : onNoMatch === "fail" ? "the call is refused" : "it is copied as-is"}.`));
10471
+ }
10408
10472
 
10409
10473
  // src/commands/config-browse.ts
10410
10474
  var import_chalk53 = __toESM(require("chalk"));
@@ -14771,6 +14835,11 @@ transform.command("set-header").description("Send a header with a fixed value to
14771
14835
  Example \u2014 lock your backend to the proxy:
14772
14836
  export TARGET_SERVER_SECRET=$(openssl rand -hex 24) # also give it to your backend
14773
14837
  apiblaze transform set-header shop x-target-api-key --value-env TARGET_SERVER_SECRET --secret`).action(action((project, header, opts) => runTransformSetHeader(project, header, opts)));
14838
+ transform.command("mapping").description("Create or replace a lookup table for map transforms, e.g. country code \u2192 currency").argument("<project>", "Project name or id").argument("<table>", "Table name, e.g. currencies").argument("<pairs...>", "from=to pairs, e.g. FR=EUR US=USD GB=GBP").option("--secret", "Store the values encrypted and never show them again").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, table, pairs, opts) => runTransformMapping(project, table, pairs, opts)));
14839
+ transform.command("map").description("Rewrite a value through a mapping table on its way to the backend, e.g. header:X-Country \u2192 header:X-Currency").argument("<project>", "Project name or id").argument("<source>", "Where to read: header:X-Country \xB7 query:country \xB7 bodyvar:order.country").argument("<destination>", "Where to write: header:X-Currency \xB7 query:currency \xB7 bodyvar:order.currency").requiredOption("--table <name>", "The mapping table to look the value up in").option("--on-no-match <mode>", "skip (default) \xB7 fail \xB7 passthrough").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").addHelpText("after", `
14840
+ Example \u2014 the backend wants a currency, callers send a country:
14841
+ apiblaze transform mapping shop currencies FR=EUR US=USD GB=GBP
14842
+ apiblaze transform map shop header:X-Country header:X-Currency --table currencies`).action(action((project, source, destination, opts) => runTransformMap(project, source, destination, opts)));
14774
14843
  transform.command("remove").description("Remove a transform by name, id, or header name").argument("<project>", "Project name or id").argument("<rule>", "Rule name, id, or the header a set-header rule sets").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, rule, opts) => runTransformRemove(project, rule, opts)));
14775
14844
  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)));
14776
14845
  var domain = program.command("domain").description("Use your own domain for your API, MCP server, developer portal or login pages (billed monthly)");
@@ -14814,7 +14883,7 @@ program.command("iam").description("Turn users & groups on/off for a proxy's ten
14814
14883
  program.command("identified").description("Require calls to identify their end user (X-End-User-Id from a backend key, or a login token \u2014 a client key alone cannot) \u2014 or allow unattributed calls again").argument("<project>", "Project name or id").argument("<mode>", "require | allow-anon").option("--team <id|name>", "Team (defaults to active team, or your anonymous workspace)").option("--apiversion <version>", "API version (defaults to the first)").option("--json", "Output machine-readable JSON").action(action((project, mode, opts) => runIdentifiedToggle(project, mode, opts)));
14815
14884
  var apikeys = new import_commander.Command("apikeys").description("API keys \u2014 for your team (control plane), or with `mint --tenant` for your API's callers: a backend key, a client key (--client) or a key bound to one person (--for)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runApikeysMenu(opts)));
14816
14885
  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, cmd) => runKeyList({ ...cmd.parent?.opts(), ...opts })));
14817
- apikeys.command("mint").description("Create a control-plane API key (secret shown once) \u2014 or, with --tenant, a consumer key in that tenant: a backend key (default), a client key (--client) or a person-bound key (--for)").option("--desc <text>", "Description").option("--expires-days <n>", "Expiry in days (default 90 server-side)").option("--tenant <slug>", "Mint a CONSUMER key in this tenant instead of a control-plane key").option("--backend", `Backend key (default). ${HOLDER_HELP.backend}`).option("--client", `Client key. ${HOLDER_HELP.client} Needs --tenant.`).option("--for <email|id>", "Issue this key to a named person \u2014 calls made with it count as that person, and ownership rules trust it. The Users & Groups widget's admin lane accepts only such a key, one per admin. Needs --tenant.").option("--env <name>", "Environment the consumer key is valid for (with --tenant; default dev)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts, cmd) => runKeyMint({ ...cmd.parent?.opts(), ...opts })));
14886
+ apikeys.command("mint").description("Create a control-plane API key (secret shown once) \u2014 or, with --tenant, a consumer key in that tenant: a backend key (default), a client key (--client) or a person-bound key (--for)").option("--desc <text>", "Description").option("--expires-days <n>", "Expiry in days (default 90 server-side)").option("--tenant <slug>", "Mint a CONSUMER key in this tenant instead of a control-plane key").option("--backend", `Backend key (default). ${HOLDER_HELP.backend}`).option("--client", `Client key. ${HOLDER_HELP.client} Needs --tenant.`).option("--for <email|id>", "Issue this key to a named person \u2014 calls made with it count as that person, and ownership rules trust it. The Users & Groups widget's admin lane accepts only such a key, one per admin. Needs --tenant.").option("--admin", "With --for: the key a tenant admin uses in the Users & Groups widget (adds manage-access; env defaults to prod)").option("--env <name>", "Environment the consumer key is valid for (with --tenant; default dev, prod with --admin)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts, cmd) => runKeyMint({ ...cmd.parent?.opts(), ...opts })));
14818
14887
  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 })));
14819
14888
  program.addCommand(apikeys);
14820
14889
  var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "latency | billing | mark | credits | residue | sweep | test | perf (omit for the menu)").argument("[view]", "for `latency`: slow | llm | compare | grades \xB7 for `billing`: doors | meter \xB7 for `mark`: the change name, quoted").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("--only <ids>", "for `test`: comma-separated spec/gut ids or prefixes").option("--group <name>", "for `test`: only specs whose group matches").option("--skip-guts", "for `test`: contract specs only").option("--guts-only", "for `test`: server-side gut checks only").option("--fail-fast", "for `test`: stop at the first failure").option("--timeout <s>", "for `test`: seconds to wait per check (default 150)").option("--perf", "for `test`: run the performance gate instead of the suite (fixed-rate load on a sandbox proxy)").option("--consumer", "for `test`: probe the data plane with the stored `consumer login` token instead of the suite").option("--path <p>", "for `test --consumer`: request path on the proxy (default /get)").option("--proxy <name@version>", "for `test --consumer`: the OAuth-door proxy to knock on (default: the sandbox proxy for a sandbox-tenant session)").option("--rps <n>", "for `test --perf`: requests per second (default 100, max 300)").option("--seconds <n>", "for `test --perf`: how long to send for (default 60, max 300)").option("--url <url>", "for `test --perf`: load this fixture URL instead of provisioning one (with --key; nothing is torn down)").option("--max-p50 <ms>", "for `test --perf`: client p50 threshold (default 250)").option("--max-p95 <ms>", "for `test --perf`: client p95 threshold (default 800)").option("--max-error-rate <pct>", "for `test --perf`: error-rate threshold in percent (default 0.5)").option("--max-gateway-p50 <ms>", "for `test --perf`: apiblaze-overhead p50 threshold (default 60)").option("--max-gateway-p95 <ms>", "for `test --perf`: apiblaze-overhead p95 threshold (default 250)").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)").option("--days <n>", "For `latency grades`: how many days back (default 7)").option("--excellent <ms>", "Grade threshold: under this is excellent (default 250)").option("--okay <ms>", "Grade threshold: under this is okay (default 500)").option("--bad <ms>", "Grade threshold: under this is bad, at/over is terrible (default 1000)").option("--key <key>", "For `billing meter`: a data-plane API key for the sandbox proxy (without it the served-request checks are SKIPPED) \xB7 for `test --perf --url`: the fixture's API key").option("--count <n>", "For `billing meter`: requests per check (default 3, max 10)").option("--mcp-host <host>", "For `billing meter`: an MCP host to check tools/call is charged once").action(action((sub, view, opts) => runOp(sub, opts, view))).addHelpText("after", () => `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.21.13",
3
+ "version": "0.21.14",
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",