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.
- package/dist/index.js +75 -6
- 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.
|
|
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
|
-
|
|
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", () => `
|