apiblaze 0.21.6 → 0.21.8
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/README.md +6 -2
- package/dist/index.js +147 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,9 +96,13 @@ The skill teaches Claude to ask "where do your users sign in today?" before crea
|
|
|
96
96
|
npx apiblaze create --target https://api.example.com
|
|
97
97
|
|
|
98
98
|
# 2. YOUR OWN OAuth app on a hosted login page — the agent's users sign in exactly where
|
|
99
|
-
# your users already do. provider is one of: auth0 · google · github · microsoft · facebook.
|
|
99
|
+
# your users already do. provider is one of: oidc · auth0 · google · github · microsoft · facebook.
|
|
100
|
+
# clientSecret is optional for every provider except github and facebook (public client, PKCE is always on).
|
|
100
101
|
npx apiblaze create --target https://api.example.com \
|
|
101
|
-
--oauth «'{"provider":"auth0","clientId":"AbC123…","clientSecret":"s3cr3t…"}'»
|
|
102
|
+
--oauth «'{"provider":"auth0","issuer":"https://acme.auth0.com/","clientId":"AbC123…","clientSecret":"s3cr3t…"}'»
|
|
103
|
+
# Any OpenID Connect IdP (Okta, Entra, Keycloak, Cognito, …) is `oidc` + its issuer URL:
|
|
104
|
+
npx apiblaze create --target https://api.example.com \
|
|
105
|
+
--oauth «'{"provider":"oidc","issuer":"https://acme.okta.com/oauth2/default","clientId":"0oa…"}'»
|
|
102
106
|
|
|
103
107
|
# 3. YOUR OWN JWT issuer — no login page at all; the proxy verifies the tokens you already
|
|
104
108
|
# issue. All three fields are required; jwks is that issuer's JWKS URL.
|
package/dist/index.js
CHANGED
|
@@ -1149,7 +1149,7 @@ async function addApiblazeHostedLogin(teamId, tenant2, opts = {}) {
|
|
|
1149
1149
|
message: "Which login provider?",
|
|
1150
1150
|
pageSize: 10,
|
|
1151
1151
|
choices: [
|
|
1152
|
-
...
|
|
1152
|
+
...PROVIDERS2.map((p) => ({ name: p.label, value: p.id })),
|
|
1153
1153
|
...opts.allowSkip ? [new inquirer3.Separator(), { name: import_chalk44.default.dim("Skip for now \u2014 set up a login method later"), value: null }] : []
|
|
1154
1154
|
]
|
|
1155
1155
|
}]);
|
|
@@ -1157,7 +1157,7 @@ async function addApiblazeHostedLogin(teamId, tenant2, opts = {}) {
|
|
|
1157
1157
|
console.log(import_chalk44.default.dim(" No login method yet \u2014 add one later under Login methods."));
|
|
1158
1158
|
return false;
|
|
1159
1159
|
}
|
|
1160
|
-
const opt =
|
|
1160
|
+
const opt = PROVIDERS2.find((p) => p.id === provider);
|
|
1161
1161
|
let providerBody;
|
|
1162
1162
|
if (!opt.own) {
|
|
1163
1163
|
providerBody = { type: "github", managed: true };
|
|
@@ -1215,14 +1215,14 @@ async function createTenantInteractive(teamId) {
|
|
|
1215
1215
|
await addApiblazeHostedLogin(teamId, tenant2, { allowSkip: true });
|
|
1216
1216
|
return tenant2;
|
|
1217
1217
|
}
|
|
1218
|
-
var import_chalk44, import_ora17,
|
|
1218
|
+
var import_chalk44, import_ora17, PROVIDERS2, DEFAULT_SCOPES;
|
|
1219
1219
|
var init_tenant_create = __esm({
|
|
1220
1220
|
"src/lib/tenant-create.ts"() {
|
|
1221
1221
|
"use strict";
|
|
1222
1222
|
import_chalk44 = __toESM(require("chalk"));
|
|
1223
1223
|
import_ora17 = __toESM(require("ora"));
|
|
1224
1224
|
init_admin();
|
|
1225
|
-
|
|
1225
|
+
PROVIDERS2 = [
|
|
1226
1226
|
{ id: "apiblaze", label: "APIblaze (via GitHub) \u2014 zero setup, recommended", own: false },
|
|
1227
1227
|
{ id: "google", label: "Google", own: true },
|
|
1228
1228
|
{ id: "github", label: "GitHub", own: true },
|
|
@@ -1337,7 +1337,7 @@ var import_commander = require("commander");
|
|
|
1337
1337
|
var import_chalk62 = __toESM(require("chalk"));
|
|
1338
1338
|
|
|
1339
1339
|
// package.json
|
|
1340
|
-
var version = "0.21.
|
|
1340
|
+
var version = "0.21.8";
|
|
1341
1341
|
|
|
1342
1342
|
// src/index.ts
|
|
1343
1343
|
init_types();
|
|
@@ -4675,6 +4675,44 @@ init_api();
|
|
|
4675
4675
|
var import_chalk13 = __toESM(require("chalk"));
|
|
4676
4676
|
var import_ora3 = __toESM(require("ora"));
|
|
4677
4677
|
init_caller();
|
|
4678
|
+
var HOLDER_HELP = {
|
|
4679
|
+
backend: "Lives on your server; says who is calling with X-End-User-Id.",
|
|
4680
|
+
client: "Safe to ship in a browser, app or widget; cannot name a user; calls are anonymous unless the user signs in."
|
|
4681
|
+
};
|
|
4682
|
+
function holderPermissions(kind) {
|
|
4683
|
+
return kind === "backend" ? ["call", "assert-end-user"] : ["call"];
|
|
4684
|
+
}
|
|
4685
|
+
function resolveHolder(opts) {
|
|
4686
|
+
const who = (opts.for ?? "").trim();
|
|
4687
|
+
if (opts.backend && opts.client) throw new Error("--backend and --client are mutually exclusive: a key is either kept on your server or shipped to users.");
|
|
4688
|
+
if (who && (opts.backend || opts.client)) throw new Error(`--for issues the key to ${who} (a person-bound key): drop --backend / --client.`);
|
|
4689
|
+
if (who) return "person";
|
|
4690
|
+
return opts.client ? "client" : "backend";
|
|
4691
|
+
}
|
|
4692
|
+
function holderKindOf(record) {
|
|
4693
|
+
const cm = record.creation_method;
|
|
4694
|
+
if (cm === "self" || cm === "on_behalf") return "person";
|
|
4695
|
+
let perms = record.permissions ?? record.permissions_json;
|
|
4696
|
+
if (typeof perms === "string") {
|
|
4697
|
+
try {
|
|
4698
|
+
perms = JSON.parse(perms);
|
|
4699
|
+
} catch {
|
|
4700
|
+
perms = void 0;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
if (!Array.isArray(perms)) return cm == null ? void 0 : "client";
|
|
4704
|
+
return perms.includes("assert-end-user") ? "backend" : "client";
|
|
4705
|
+
}
|
|
4706
|
+
function holderLine(kind, person) {
|
|
4707
|
+
switch (kind) {
|
|
4708
|
+
case "backend":
|
|
4709
|
+
return "backend key \u2014 send X-API-Key and say who is calling with X-End-User-Id";
|
|
4710
|
+
case "client":
|
|
4711
|
+
return "client key \u2014 safe to ship to users; it cannot name a user (calls are anonymous unless they sign in)";
|
|
4712
|
+
case "person":
|
|
4713
|
+
return `issued to: ${person ?? "?"} \u2014 calls made with this key count as this person; an X-End-User-Id naming anyone else is refused`;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4678
4716
|
async function runApikeysMenu(opts) {
|
|
4679
4717
|
await runKeyList(opts);
|
|
4680
4718
|
if (opts.json) return;
|
|
@@ -4706,11 +4744,14 @@ async function runKeyList(opts) {
|
|
|
4706
4744
|
return;
|
|
4707
4745
|
}
|
|
4708
4746
|
for (const k of keys) {
|
|
4709
|
-
|
|
4747
|
+
const kind = holderKindOf(k);
|
|
4748
|
+
const kindCol = kind ? `${import_chalk13.default.cyan(kind.padEnd(7))} ` : "";
|
|
4749
|
+
console.log(` ${import_chalk13.default.bold(k.key_id ?? k.id)} ${kindCol}${import_chalk13.default.dim(k.description ?? "")} ${import_chalk13.default.dim(k.expires_at ?? "no expiry")}`);
|
|
4710
4750
|
}
|
|
4711
4751
|
}
|
|
4712
4752
|
async function runKeyMint(opts) {
|
|
4713
|
-
|
|
4753
|
+
resolveHolder(opts);
|
|
4754
|
+
if (opts.tenant || opts.for || opts.client) return runConsumerKeyMint(opts);
|
|
4714
4755
|
const caller = requireCaller();
|
|
4715
4756
|
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
4716
4757
|
const body = { role: "consumer-admin" };
|
|
@@ -4739,14 +4780,22 @@ async function runKeyMint(opts) {
|
|
|
4739
4780
|
}
|
|
4740
4781
|
async function mintConsumerKey(o) {
|
|
4741
4782
|
const tenant2 = (o.tenant ?? "").trim();
|
|
4742
|
-
if (!tenant2) throw new Error("--for needs a tenant: add --tenant <slug> (the tenant whose person this key is for).");
|
|
4743
4783
|
const who = (o.for ?? "").trim();
|
|
4784
|
+
if (!tenant2) {
|
|
4785
|
+
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).`);
|
|
4786
|
+
}
|
|
4744
4787
|
const environment = (o.env ?? "").trim() || "dev";
|
|
4788
|
+
const holder = who ? "person" : o.holder ?? "backend";
|
|
4789
|
+
const permissions = holder === "person" ? void 0 : holderPermissions(holder);
|
|
4745
4790
|
const caller = requireCaller();
|
|
4746
4791
|
const { teamId } = await resolveActingTeam(caller, o.team);
|
|
4747
4792
|
const body = { tenant: tenant2, environment };
|
|
4748
4793
|
if (o.desc) body.description = o.desc;
|
|
4749
4794
|
if (o.expiresDays) body.expires_in_seconds = Number(o.expiresDays) * 24 * 60 * 60;
|
|
4795
|
+
if (permissions) {
|
|
4796
|
+
body.holder = holder;
|
|
4797
|
+
body.permissions = permissions;
|
|
4798
|
+
}
|
|
4750
4799
|
const headers = { "Content-Type": "application/json" };
|
|
4751
4800
|
if (who) {
|
|
4752
4801
|
headers["X-End-User-Id"] = who;
|
|
@@ -4767,16 +4816,35 @@ async function mintConsumerKey(o) {
|
|
|
4767
4816
|
});
|
|
4768
4817
|
const key = out?.key ?? out?.fullKey;
|
|
4769
4818
|
if (typeof key !== "string" || !key) throw new Error("Mint failed: the apikeys plane returned no key.");
|
|
4770
|
-
return {
|
|
4819
|
+
return {
|
|
4820
|
+
key,
|
|
4821
|
+
keyId: out?.metadata?.key_id,
|
|
4822
|
+
expiresAt: out?.metadata?.expires_at,
|
|
4823
|
+
tenant: tenant2,
|
|
4824
|
+
environment,
|
|
4825
|
+
holder,
|
|
4826
|
+
...permissions ? { permissions } : {},
|
|
4827
|
+
...who ? { person: who } : {},
|
|
4828
|
+
raw: out
|
|
4829
|
+
};
|
|
4771
4830
|
}
|
|
4772
4831
|
async function runConsumerKeyMint(opts) {
|
|
4832
|
+
const kind = resolveHolder(opts);
|
|
4773
4833
|
const who = (opts.for ?? "").trim();
|
|
4774
4834
|
const tenant2 = (opts.tenant ?? "").trim();
|
|
4775
|
-
const spinner = (0, import_ora3.default)(who ? `Minting a key for ${who} in ${tenant2}...` : `Minting a key in ${tenant2}...`).start();
|
|
4835
|
+
const spinner = (0, import_ora3.default)(who ? `Minting a key for ${who} in ${tenant2}...` : `Minting a ${kind} key in ${tenant2}...`).start();
|
|
4776
4836
|
let out;
|
|
4777
4837
|
try {
|
|
4778
|
-
out = await mintConsumerKey({
|
|
4779
|
-
|
|
4838
|
+
out = await mintConsumerKey({
|
|
4839
|
+
tenant: tenant2,
|
|
4840
|
+
for: who || void 0,
|
|
4841
|
+
env: opts.env,
|
|
4842
|
+
team: opts.team,
|
|
4843
|
+
desc: opts.desc,
|
|
4844
|
+
expiresDays: opts.expiresDays,
|
|
4845
|
+
...kind === "person" ? {} : { holder: kind }
|
|
4846
|
+
});
|
|
4847
|
+
spinner.succeed(who ? `Key minted for ${who}.` : `${kind === "client" ? "Client" : "Backend"} key minted.`);
|
|
4780
4848
|
} catch (err) {
|
|
4781
4849
|
spinner.fail("Mint failed.");
|
|
4782
4850
|
throw err;
|
|
@@ -4789,10 +4857,11 @@ async function runConsumerKeyMint(opts) {
|
|
|
4789
4857
|
console.log(` ${import_chalk13.default.bold("key")}: ${import_chalk13.default.green(out.key)} ${import_chalk13.default.dim("(shown once \u2014 store it now)")}`);
|
|
4790
4858
|
console.log(` ${import_chalk13.default.dim("tenant:")} ${out.tenant} ${import_chalk13.default.dim("env:")} ${out.environment}`);
|
|
4791
4859
|
if (out.expiresAt) console.log(` ${import_chalk13.default.dim("expires:")} ${out.expiresAt}`);
|
|
4792
|
-
if (out.person) {
|
|
4860
|
+
if (out.holder === "person") {
|
|
4793
4861
|
console.log(` ${import_chalk13.default.dim("issued to:")} ${out.person} ${import_chalk13.default.dim("\u2014 calls made with this key count as this person; an X-End-User-Id naming anyone else is refused")}`);
|
|
4794
4862
|
} else {
|
|
4795
|
-
console.log(import_chalk13.default.
|
|
4863
|
+
console.log(` ${import_chalk13.default.cyan(holderLine(out.holder))}`);
|
|
4864
|
+
console.log(import_chalk13.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>.)"));
|
|
4796
4865
|
}
|
|
4797
4866
|
}
|
|
4798
4867
|
async function runKeyRevoke(keyId, opts) {
|
|
@@ -4961,12 +5030,18 @@ var MANAGED_GITHUB = {
|
|
|
4961
5030
|
};
|
|
4962
5031
|
var API_KEY = { auth: "api_key", doors: ["api_key"], bodyPatch: {}, summary: "callers send the API key (X-API-Key) \u2014 no sign-in" };
|
|
4963
5032
|
var OPEN = { auth: "none", doors: [], bodyPatch: {}, summary: "open \u2014 no credential required" };
|
|
5033
|
+
var PROVIDERS = ["github", "google", "microsoft", "facebook", "auth0", "oidc"];
|
|
5034
|
+
var ISSUER_PROVIDERS = /* @__PURE__ */ new Set(["oidc", "auth0"]);
|
|
5035
|
+
var SECRET_REQUIRED_TYPES = /* @__PURE__ */ new Set(["github", "facebook"]);
|
|
4964
5036
|
var OAUTH_SHAPES = `Shapes:
|
|
4965
5037
|
--oauth ${B.product}-hosted GitHub sign-in
|
|
4966
5038
|
--oauth '{"iss":"https://login.acme.com/","aud":"acme-api","jwks":"https://login.acme.com/.well-known/jwks.json"}'
|
|
4967
5039
|
your own hosted login: the proxy trusts JWTs from that issuer
|
|
4968
5040
|
--oauth '{"provider":"google","clientId":"\u2026","clientSecret":"\u2026"}'
|
|
4969
|
-
${B.product}-hosted login page with YOUR OAuth app (github \xB7 google \xB7 microsoft \xB7 facebook \xB7 auth0)
|
|
5041
|
+
${B.product}-hosted login page with YOUR OAuth app (github \xB7 google \xB7 microsoft \xB7 facebook \xB7 auth0 \xB7 oidc)
|
|
5042
|
+
--oauth '{"provider":"oidc","issuer":"https://acme.okta.com/oauth2/default","clientId":"\u2026"}'
|
|
5043
|
+
any OpenID Connect IdP (Okta \xB7 Entra \xB7 Keycloak \xB7 Cognito \xB7 \u2026) by issuer; clientSecret is optional
|
|
5044
|
+
for every provider except github and facebook (public client, PKCE); "scopes":[\u2026] overrides the default`;
|
|
4970
5045
|
function parseOauthFlag(raw) {
|
|
4971
5046
|
if (raw === void 0 || raw === false) return null;
|
|
4972
5047
|
if (raw === true || typeof raw === "string" && raw.trim() === "") return MANAGED_GITHUB;
|
|
@@ -4999,21 +5074,34 @@ function parseOauthFlag(raw) {
|
|
|
4999
5074
|
}
|
|
5000
5075
|
if (typeof parsed.provider === "string") {
|
|
5001
5076
|
const provider = parsed.provider.trim().toLowerCase();
|
|
5002
|
-
|
|
5003
|
-
if (!PROVIDERS2.includes(provider)) fail4(`--oauth: unknown provider "${provider}". One of: ${PROVIDERS2.join(" \xB7 ")}.`);
|
|
5077
|
+
if (!PROVIDERS.includes(provider)) fail4(`--oauth: unknown provider "${provider}". One of: ${PROVIDERS.join(" \xB7 ")}.`, OAUTH_SHAPES);
|
|
5004
5078
|
const clientId = typeof parsed.clientId === "string" ? parsed.clientId.trim() : typeof parsed.client_id === "string" ? parsed.client_id.trim() : "";
|
|
5005
5079
|
const clientSecret = typeof parsed.clientSecret === "string" ? parsed.clientSecret.trim() : typeof parsed.client_secret === "string" ? parsed.client_secret.trim() : "";
|
|
5006
|
-
if (!clientId
|
|
5080
|
+
if (!clientId) fail4("--oauth provider shape needs clientId (from your OAuth app).", OAUTH_SHAPES);
|
|
5081
|
+
if (SECRET_REQUIRED_TYPES.has(provider) && !clientSecret) fail4(`--oauth provider "${provider}" needs clientSecret too (its OAuth apps cannot be public clients).`, OAUTH_SHAPES);
|
|
5082
|
+
const issuer = typeof parsed.issuer === "string" ? parsed.issuer.trim() : "";
|
|
5083
|
+
if (ISSUER_PROVIDERS.has(provider) && !issuer) fail4(`--oauth provider "${provider}" needs "issuer" (your IdP's issuer URL, e.g. https://acme.okta.com/oauth2/default).`, OAUTH_SHAPES);
|
|
5084
|
+
if (issuer && !isHttpUrl3(issuer)) fail4("--oauth: issuer must be an http(s) URL.", OAUTH_SHAPES);
|
|
5085
|
+
if (parsed.scopes !== void 0 && (!Array.isArray(parsed.scopes) || !parsed.scopes.every((s) => typeof s === "string" && s.trim()))) {
|
|
5086
|
+
fail4('--oauth: scopes must be an array of non-empty strings, e.g. ["openid","email","profile"].', OAUTH_SHAPES);
|
|
5087
|
+
}
|
|
5088
|
+
const scopes = Array.isArray(parsed.scopes) ? parsed.scopes.map((s) => s.trim()) : void 0;
|
|
5007
5089
|
return {
|
|
5008
5090
|
auth: "oauth",
|
|
5009
5091
|
doors: ["oauth"],
|
|
5010
5092
|
bodyPatch: {
|
|
5011
5093
|
login: {
|
|
5012
|
-
providers: [{
|
|
5094
|
+
providers: [{
|
|
5095
|
+
type: provider,
|
|
5096
|
+
client_id: clientId,
|
|
5097
|
+
...clientSecret ? { client_secret: clientSecret } : {},
|
|
5098
|
+
...issuer ? { issuer } : {},
|
|
5099
|
+
...scopes && scopes.length ? { scopes } : {}
|
|
5100
|
+
}]
|
|
5013
5101
|
}
|
|
5014
5102
|
},
|
|
5015
|
-
summary: `callers sign in with your own ${provider} app (${B.product}-hosted page)`,
|
|
5016
|
-
provider: cap(provider),
|
|
5103
|
+
summary: provider === "oidc" ? `callers sign in through your OpenID Connect provider (${issuer}, ${B.product}-hosted page)` : `callers sign in with your own ${provider} app (${B.product}-hosted page)`,
|
|
5104
|
+
provider: provider === "oidc" ? "OpenID Connect" : cap(provider),
|
|
5017
5105
|
ownApp: true
|
|
5018
5106
|
};
|
|
5019
5107
|
}
|
|
@@ -5372,7 +5460,7 @@ async function printDevOutcome(o, port) {
|
|
|
5372
5460
|
if (!o.adminEmail) console.log(` ${import_chalk16.default.dim("\u2013")} ${import_chalk16.default.dim(`no admin yet \u2014 npx ${B.cli} admins add you@example.com --tenant ${o.tenant ?? "<tenant>"}`)}`);
|
|
5373
5461
|
if (o.auth.doors.includes("api_key") && o.apiKey) {
|
|
5374
5462
|
console.log();
|
|
5375
|
-
console.log(` ${import_chalk16.default.dim("API key (for your frontend-server, shown once):")} ${import_chalk16.default.bold.green(o.apiKey)} ${import_chalk16.default.dim("\u2192 send it as X-API-Key and say who is calling with X-End-User-Id")}`);
|
|
5463
|
+
console.log(` ${import_chalk16.default.dim("API key (backend key, for your frontend-server, shown once):")} ${import_chalk16.default.bold.green(o.apiKey)} ${import_chalk16.default.dim("\u2192 send it as X-API-Key and say who is calling with X-End-User-Id")}`);
|
|
5376
5464
|
}
|
|
5377
5465
|
const okLines = agentPrompts(locked, o.openapi, null).filter((l) => l.ok);
|
|
5378
5466
|
const tryHint = okLines[1]?.say ?? okLines[0]?.say;
|
|
@@ -5415,7 +5503,7 @@ async function printOutcome(o) {
|
|
|
5415
5503
|
}
|
|
5416
5504
|
if (o.auth.doors.includes("api_key") && o.apiKey) {
|
|
5417
5505
|
console.log();
|
|
5418
|
-
console.log(` ${import_chalk16.default.dim("API key (for your backend and widget
|
|
5506
|
+
console.log(` ${import_chalk16.default.dim("API key (backend key \u2014 for your backend and widget; shown once, save it now):")}`);
|
|
5419
5507
|
console.log(` ${import_chalk16.default.bold.green(o.apiKey)}`);
|
|
5420
5508
|
console.log(import_chalk16.default.dim(" Send it as X-API-Key and say who is calling with X-End-User-Id."));
|
|
5421
5509
|
const otherEnvs = Object.keys(o.keys).filter((e) => o.keys[e] !== o.apiKey);
|
|
@@ -5425,6 +5513,7 @@ async function printOutcome(o) {
|
|
|
5425
5513
|
printTryIt(buildTryItCurl(url, o.auth.auth, o.apiKey, o.tenant, o.auth.ownIssuer), [o.tenant, "$TOKEN", o.apiKey, "X-End-User-Id: you"]);
|
|
5426
5514
|
if (o.auth.auth === "api_key" && o.apiKey) {
|
|
5427
5515
|
console.log(import_chalk16.default.dim(" X-End-User-Id = who your server is acting for; the rules check that person (any handle works for a test)."));
|
|
5516
|
+
console.log(import_chalk16.default.dim(` Keys you ship to users (browser, app, widget) must not name anyone: npx ${B.cli} apikeys mint --client --tenant ${o.tenant ?? "<tenant>"}`));
|
|
5428
5517
|
}
|
|
5429
5518
|
if (mcp !== shown) {
|
|
5430
5519
|
console.log();
|
|
@@ -13307,6 +13396,18 @@ async function runnerCall(body, timeoutS) {
|
|
|
13307
13396
|
return data;
|
|
13308
13397
|
}
|
|
13309
13398
|
var call = runnerCall;
|
|
13399
|
+
var TEARDOWN_GUT_ID = "sandbox-key-scrub";
|
|
13400
|
+
async function scrubSandboxKeys(timeoutS = 150) {
|
|
13401
|
+
try {
|
|
13402
|
+
const r = await runnerCall({ action: "gut", id: TEARDOWN_GUT_ID }, timeoutS);
|
|
13403
|
+
const first = r.results?.[0]?.detail ?? r.error ?? "";
|
|
13404
|
+
log(import_chalk58.default.dim(`teardown: ${first || (r.ok ? "sandbox fixture keys scrubbed" : "scrub reported a problem")}`));
|
|
13405
|
+
return r.ok;
|
|
13406
|
+
} catch (e) {
|
|
13407
|
+
log(import_chalk58.default.dim(`teardown: sandbox key scrub skipped (${e instanceof Error ? e.message : String(e)}) \u2014 leftovers expire within 14 days; scripts/purge-test-keys.mjs reaps them`));
|
|
13408
|
+
return false;
|
|
13409
|
+
}
|
|
13410
|
+
}
|
|
13310
13411
|
var matches = (id, group2, o) => {
|
|
13311
13412
|
if (o.only) {
|
|
13312
13413
|
const wanted = o.only.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -13322,7 +13423,7 @@ async function runOpTest(o = {}) {
|
|
|
13322
13423
|
const timeoutS = o.timeout && o.timeout > 0 ? o.timeout : 150;
|
|
13323
13424
|
const listing = await call({ action: "list" }, 60);
|
|
13324
13425
|
const specs = o.gutsOnly ? [] : listing.specs.filter((s) => matches(s.id, s.group, o));
|
|
13325
|
-
const guts = o.skipGuts ? [] : listing.guts.filter((g) => matches(g.id, void 0, { ...o, group: void 0 }));
|
|
13426
|
+
const guts = o.skipGuts ? [] : listing.guts.filter((g) => g.id !== TEARDOWN_GUT_ID && matches(g.id, void 0, { ...o, group: void 0 }));
|
|
13326
13427
|
const total = specs.length + guts.length;
|
|
13327
13428
|
if (total === 0) {
|
|
13328
13429
|
log(import_chalk58.default.yellow("nothing matched"));
|
|
@@ -13344,27 +13445,31 @@ async function runOpTest(o = {}) {
|
|
|
13344
13445
|
else for (const x of r.results.filter((x2) => x2 && x2.ok === false)) log(import_chalk58.default.red(` \u2717 ${x.name ?? ""}: ${x.detail ?? JSON.stringify(x)}`));
|
|
13345
13446
|
}
|
|
13346
13447
|
};
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
try {
|
|
13350
|
-
r = await call({ action: "spec", id: s.id }, timeoutS);
|
|
13351
|
-
} catch (e) {
|
|
13352
|
-
r = { kind: "spec", id: s.id, label: s.label, group: s.group, ok: false, cases: [{ name: "runner", ok: false, detail: e instanceof Error ? e.message : String(e) }], duration_ms: 0 };
|
|
13353
|
-
}
|
|
13354
|
-
record(r);
|
|
13355
|
-
if (!r.ok && o.failFast) break;
|
|
13356
|
-
}
|
|
13357
|
-
if (!(o.failFast && failed)) {
|
|
13358
|
-
for (const g of guts) {
|
|
13448
|
+
try {
|
|
13449
|
+
for (const s of specs) {
|
|
13359
13450
|
let r;
|
|
13360
13451
|
try {
|
|
13361
|
-
r = await call({ action: "
|
|
13452
|
+
r = await call({ action: "spec", id: s.id }, timeoutS);
|
|
13362
13453
|
} catch (e) {
|
|
13363
|
-
r = { kind: "
|
|
13454
|
+
r = { kind: "spec", id: s.id, label: s.label, group: s.group, ok: false, cases: [{ name: "runner", ok: false, detail: e instanceof Error ? e.message : String(e) }], duration_ms: 0 };
|
|
13364
13455
|
}
|
|
13365
13456
|
record(r);
|
|
13366
13457
|
if (!r.ok && o.failFast) break;
|
|
13367
13458
|
}
|
|
13459
|
+
if (!(o.failFast && failed)) {
|
|
13460
|
+
for (const g of guts) {
|
|
13461
|
+
let r;
|
|
13462
|
+
try {
|
|
13463
|
+
r = await call({ action: "gut", id: g.id }, timeoutS);
|
|
13464
|
+
} catch (e) {
|
|
13465
|
+
r = { kind: "gut", id: g.id, label: g.label, ok: false, results: [], error: e instanceof Error ? e.message : String(e), duration_ms: 0 };
|
|
13466
|
+
}
|
|
13467
|
+
record(r);
|
|
13468
|
+
if (!r.ok && o.failFast) break;
|
|
13469
|
+
}
|
|
13470
|
+
}
|
|
13471
|
+
} finally {
|
|
13472
|
+
await scrubSandboxKeys(timeoutS);
|
|
13368
13473
|
}
|
|
13369
13474
|
const summary = { ran: results.length, passed: results.length - failed, failed, duration_ms: Date.now() - started, sandbox_team: listing.sandbox_team };
|
|
13370
13475
|
log("");
|
|
@@ -13772,6 +13877,7 @@ async function runOpPerf(o = {}) {
|
|
|
13772
13877
|
log3(import_chalk60.default.red(`teardown of ${fixture.proxy} failed: ${e instanceof Error ? e.message : String(e)} \u2014 run \`apiblaze op residue\` / clean the sandbox`));
|
|
13773
13878
|
process.exitCode = 1;
|
|
13774
13879
|
}
|
|
13880
|
+
await scrubSandboxKeys(120);
|
|
13775
13881
|
}
|
|
13776
13882
|
}
|
|
13777
13883
|
}
|
|
@@ -14214,7 +14320,7 @@ program.command("login").description(`Authenticate with ${B.product}`).option("-
|
|
|
14214
14320
|
process.exit(1);
|
|
14215
14321
|
}
|
|
14216
14322
|
});
|
|
14217
|
-
program.command("create").description(`Create a new ${B.thing} \u2014 both doors open by default (an API key for your backend and widget, GitHub sign-in for people and agents) and resources locked to their creator (no login needed: logged out it creates an anonymous ${B.thingShort} you can claim)`).option("--name <name>", B.cli === "mcpblaze" ? "MCP server name (becomes <name>-<tenant>.mcpblaze.com)" : "Proxy name (becomes <name>.abz.run)").option("--target <url|file>", "What to proxy \u2014 pass ANY of: a target server base URL (https://httpbin.org), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml). Spec inputs are detected automatically; routes, API version and environments then come from the spec").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--team <id|name>", "Team to create under (defaults to your active team)").option("--auth <type>", "Open ONE door only: api_key | oauth (sign in with GitHub, APIblaze-hosted) | none. Omitted = both doors.").option("--apikey", "API-key door only (no sign-in): the bootstrap keys are minted and printed; callers send X-API-Key, agents connect with a key.").option("--oauth [config]", `Sign-in door only (no API key). Bare = APIblaze-hosted GitHub sign-in. '{"iss","aud","jwks"}' = trust YOUR hosted login's JWTs. '{"provider","clientId","clientSecret"}' = APIblaze-hosted login page with YOUR OAuth app (github \xB7 google \xB7 microsoft \xB7 facebook \xB7 auth0).`).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 ${B.thingShort}'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 ${B.thingShort} you own adds a version to the existing project.`).option("--tenant <slug>", `Tenant to attach the ${B.thingShort} 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("--auto", `Zero prompts, start to finish: generates the ${B.thingShort} name when --name is omitted (from the target host, salted), takes every confirmation as yes, auto-picks the tenant. Implies --yes.`).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) => {
|
|
14323
|
+
program.command("create").description(`Create a new ${B.thing} \u2014 both doors open by default (an API key for your backend and widget, GitHub sign-in for people and agents) and resources locked to their creator (no login needed: logged out it creates an anonymous ${B.thingShort} you can claim)`).option("--name <name>", B.cli === "mcpblaze" ? "MCP server name (becomes <name>-<tenant>.mcpblaze.com)" : "Proxy name (becomes <name>.abz.run)").option("--target <url|file>", "What to proxy \u2014 pass ANY of: a target server base URL (https://httpbin.org), a local OpenAPI file (./openapi.yaml), or a remote OpenAPI URL (https://acme.com/openapi.yaml). Spec inputs are detected automatically; routes, API version and environments then come from the spec").addOption(new import_commander.Option("--openapi <file|url>", "Deprecated alias \u2014 --target now detects spec files/URLs itself").hideHelp()).addOption(new import_commander.Option("--openapispec <file|url>", "Deprecated alias for --openapi").hideHelp()).option("--team <id|name>", "Team to create under (defaults to your active team)").option("--auth <type>", "Open ONE door only: api_key | oauth (sign in with GitHub, APIblaze-hosted) | none. Omitted = both doors.").option("--apikey", "API-key door only (no sign-in): the bootstrap keys are minted and printed; callers send X-API-Key, agents connect with a key.").option("--oauth [config]", `Sign-in door only (no API key). Bare = APIblaze-hosted GitHub sign-in. '{"iss","aud","jwks"}' = trust YOUR hosted login's JWTs. '{"provider","clientId","clientSecret"?}' = APIblaze-hosted login page with YOUR OAuth app (github \xB7 google \xB7 microsoft \xB7 facebook \xB7 auth0 \xB7 oidc); the secret is optional except for github and facebook. '{"provider":"oidc","issuer":"https://acme.okta.com/oauth2/default","clientId":"\u2026"}' = any OpenID Connect IdP by issuer (Okta \xB7 Entra \xB7 Keycloak \xB7 Cognito \xB7 \u2026).`).option("--identified", "Require every call to identify its end user (X-End-User-Id from a backend key, or a login token \u2014 a client key alone cannot); unattributed calls are rejected").option("--iam", `Turn IAM on for the ${B.thingShort}'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 ${B.thingShort} you own adds a version to the existing project.`).option("--tenant <slug>", `Tenant to attach the ${B.thingShort} 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("--auto", `Zero prompts, start to finish: generates the ${B.thingShort} name when --name is omitted (from the target host, salted), takes every confirmation as yes, auto-picks the tenant. Implies --yes.`).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) => {
|
|
14218
14324
|
try {
|
|
14219
14325
|
await runCreate({ ...opts, openapi: opts.openapi ?? opts.openapispec });
|
|
14220
14326
|
} catch (err) {
|
|
@@ -14348,10 +14454,10 @@ admins.command("add").description("Add an email as a tenant admin (e.g. yourself
|
|
|
14348
14454
|
admins.command("remove").description("Remove a tenant admin").argument("<email>", "Email to remove").option("--tenant <slug>", "Tenant slug").option("--team <id|name>", "Team").option("--json", "JSON").action(action((email, opts, cmd) => runAdminsRemove(email, { ...cmd.parent?.opts(), ...opts })));
|
|
14349
14455
|
program.addCommand(admins);
|
|
14350
14456
|
program.command("iam").description("Turn users & groups on/off for a proxy's tenant (identified calls get their user's groups applied)").argument("<project>", "Project name or id").argument("<state>", "on | off").option("--tenant <name>", "Tenant to toggle (defaults to the project's tenant)").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, state, opts) => runIamToggle(project, state, opts)));
|
|
14351
|
-
program.command("identified").description("Require calls to identify their end user (X-End-User-Id or a login token) \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)));
|
|
14457
|
+
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)));
|
|
14352
14458
|
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)));
|
|
14353
14459
|
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 })));
|
|
14354
|
-
apikeys.command("mint").description("Create a control-plane API key (secret shown once) \u2014 or, with --tenant, a consumer key in that tenant").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("--for <email|id>", "Issue this key to a named person \u2014 calls made with it count as that person, and ownership rules trust it. 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 })));
|
|
14460
|
+
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. 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 })));
|
|
14355
14461
|
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 })));
|
|
14356
14462
|
program.addCommand(apikeys, { hidden: true });
|
|
14357
14463
|
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", () => `
|