apiblaze 0.21.5 → 0.21.6
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 +754 -226
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -40,9 +40,9 @@ var init_types = __esm({
|
|
|
40
40
|
"src/types.ts"() {
|
|
41
41
|
"use strict";
|
|
42
42
|
ApiError = class extends Error {
|
|
43
|
-
constructor(
|
|
43
|
+
constructor(status2, message, body) {
|
|
44
44
|
super(message);
|
|
45
|
-
this.status =
|
|
45
|
+
this.status = status2;
|
|
46
46
|
this.body = body;
|
|
47
47
|
this.name = "ApiError";
|
|
48
48
|
}
|
|
@@ -81,6 +81,8 @@ function loadCredentials() {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
function getAccessToken() {
|
|
84
|
+
const fromEnv = process.env.APIBLAZE_TOKEN;
|
|
85
|
+
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
84
86
|
const creds = loadCredentials();
|
|
85
87
|
if (!creds) {
|
|
86
88
|
throw new Error("Not authenticated. Run `apiblaze login` first.");
|
|
@@ -545,19 +547,19 @@ var init_trace = __esm({
|
|
|
545
547
|
});
|
|
546
548
|
|
|
547
549
|
// src/lib/admin.ts
|
|
548
|
-
async function admin(
|
|
550
|
+
async function admin(call2) {
|
|
549
551
|
const token = getAccessToken();
|
|
550
552
|
const res = await fetch(`${DASHBOARD_BASE3}/api/cli/admin`, {
|
|
551
553
|
method: "POST",
|
|
552
554
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
553
|
-
body: JSON.stringify({ path:
|
|
555
|
+
body: JSON.stringify({ path: call2.path, method: call2.method, body: call2.body })
|
|
554
556
|
});
|
|
555
557
|
let data = null;
|
|
556
558
|
try {
|
|
557
559
|
data = await res.json();
|
|
558
560
|
} catch {
|
|
559
561
|
}
|
|
560
|
-
recordCall({ method:
|
|
562
|
+
recordCall({ method: call2.method, path: call2.path, body: call2.body, status: res.status, summary: call2.summary });
|
|
561
563
|
maybePrintBilling(data);
|
|
562
564
|
if (!res.ok) {
|
|
563
565
|
const msg = data?.details ?? data?.error ?? res.statusText;
|
|
@@ -650,13 +652,13 @@ async function resolveActingTeam(caller, optTeam) {
|
|
|
650
652
|
}
|
|
651
653
|
return resolveTeam(optTeam);
|
|
652
654
|
}
|
|
653
|
-
async function producer(caller,
|
|
654
|
-
if (caller.mode === "session") return admin(
|
|
655
|
-
const out = await cpFetch(caller.cpKey,
|
|
656
|
-
method:
|
|
657
|
-
...
|
|
655
|
+
async function producer(caller, call2) {
|
|
656
|
+
if (caller.mode === "session") return admin(call2);
|
|
657
|
+
const out = await cpFetch(caller.cpKey, call2.path, {
|
|
658
|
+
method: call2.method,
|
|
659
|
+
...call2.body !== void 0 ? { body: JSON.stringify(call2.body) } : {}
|
|
658
660
|
});
|
|
659
|
-
recordCall({ method:
|
|
661
|
+
recordCall({ method: call2.method, path: call2.path, body: call2.body, status: 200, summary: call2.summary });
|
|
660
662
|
return out;
|
|
661
663
|
}
|
|
662
664
|
async function resolveProjectVia(caller, teamId, nameOrId, version2) {
|
|
@@ -1332,10 +1334,10 @@ var init_tenant_pick = __esm({
|
|
|
1332
1334
|
// src/index.ts
|
|
1333
1335
|
var import_fs5 = __toESM(require("fs"));
|
|
1334
1336
|
var import_commander = require("commander");
|
|
1335
|
-
var
|
|
1337
|
+
var import_chalk62 = __toESM(require("chalk"));
|
|
1336
1338
|
|
|
1337
1339
|
// package.json
|
|
1338
|
-
var version = "0.21.
|
|
1340
|
+
var version = "0.21.6";
|
|
1339
1341
|
|
|
1340
1342
|
// src/index.ts
|
|
1341
1343
|
init_types();
|
|
@@ -1523,11 +1525,11 @@ function colorMethod(method) {
|
|
|
1523
1525
|
const colorFn = METHOD_COLORS[method.toUpperCase()] ?? import_chalk3.default.white;
|
|
1524
1526
|
return colorFn(method.padEnd(7));
|
|
1525
1527
|
}
|
|
1526
|
-
function colorStatus(
|
|
1527
|
-
const s = String(
|
|
1528
|
-
if (
|
|
1529
|
-
if (
|
|
1530
|
-
if (
|
|
1528
|
+
function colorStatus(status2) {
|
|
1529
|
+
const s = String(status2);
|
|
1530
|
+
if (status2 >= 500) return import_chalk3.default.red(s);
|
|
1531
|
+
if (status2 >= 400) return import_chalk3.default.yellow(s);
|
|
1532
|
+
if (status2 >= 300) return import_chalk3.default.cyan(s);
|
|
1531
1533
|
return import_chalk3.default.green(s);
|
|
1532
1534
|
}
|
|
1533
1535
|
function colorLatency(latency) {
|
|
@@ -2041,8 +2043,8 @@ function startTunnelClient(opts) {
|
|
|
2041
2043
|
inflight.delete(id);
|
|
2042
2044
|
void forward(socket, id, f);
|
|
2043
2045
|
}
|
|
2044
|
-
function sendResponse(socket, id,
|
|
2045
|
-
send(socket, { id, type: "res", status, headers, bodyLen: body.length });
|
|
2046
|
+
function sendResponse(socket, id, status2, headers, body) {
|
|
2047
|
+
send(socket, { id, type: "res", status: status2, headers, bodyLen: body.length });
|
|
2046
2048
|
if (body.length === 0) {
|
|
2047
2049
|
send(socket, { id, type: "chunk", seq: 0, data: "", final: true });
|
|
2048
2050
|
return;
|
|
@@ -2052,7 +2054,7 @@ function startTunnelClient(opts) {
|
|
|
2052
2054
|
send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= body.length });
|
|
2053
2055
|
}
|
|
2054
2056
|
}
|
|
2055
|
-
function record(f, reqBody,
|
|
2057
|
+
function record(f, reqBody, status2, captured, resHeaders, resBody) {
|
|
2056
2058
|
if (!opts.onRecord) return;
|
|
2057
2059
|
const req = encodeBody(reqBody);
|
|
2058
2060
|
const res = resBody ? encodeBody(resBody) : void 0;
|
|
@@ -2065,7 +2067,7 @@ function startTunnelClient(opts) {
|
|
|
2065
2067
|
requestHeaders: f.headers,
|
|
2066
2068
|
requestBody: reqBody.length ? req.value : void 0,
|
|
2067
2069
|
requestBodyEncoding: reqBody.length ? req.encoding : void 0,
|
|
2068
|
-
status,
|
|
2070
|
+
status: status2,
|
|
2069
2071
|
responseHeaders: resHeaders,
|
|
2070
2072
|
responseBody: res?.value,
|
|
2071
2073
|
responseBodyEncoding: res?.encoding,
|
|
@@ -2078,7 +2080,7 @@ function startTunnelClient(opts) {
|
|
|
2078
2080
|
if (body.length) init.body = body;
|
|
2079
2081
|
try {
|
|
2080
2082
|
const resp = await fetch(target + f.path, init);
|
|
2081
|
-
const
|
|
2083
|
+
const status2 = resp.status;
|
|
2082
2084
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
2083
2085
|
const headers = {};
|
|
2084
2086
|
resp.headers.forEach((value, key) => {
|
|
@@ -2088,9 +2090,9 @@ function startTunnelClient(opts) {
|
|
|
2088
2090
|
capturing = false;
|
|
2089
2091
|
opts.onResume?.();
|
|
2090
2092
|
}
|
|
2091
|
-
sendResponse(socket, id,
|
|
2092
|
-
opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
|
|
2093
|
-
record(f, body,
|
|
2093
|
+
sendResponse(socket, id, status2, headers, buf);
|
|
2094
|
+
opts.onEntry({ method: f.method, path: f.path, status: status2, latency: Date.now() - f.start });
|
|
2095
|
+
record(f, body, status2, false, headers, buf);
|
|
2094
2096
|
} catch (err) {
|
|
2095
2097
|
const code = err?.cause?.code;
|
|
2096
2098
|
if (OFFLINE_CODES.has(code)) {
|
|
@@ -2731,101 +2733,101 @@ function refreshInstall(cli, spec2) {
|
|
|
2731
2733
|
const r = cli === "claude" ? installIntoClaude(spec2) : installIntoCodex(spec2);
|
|
2732
2734
|
return r.ok;
|
|
2733
2735
|
}
|
|
2734
|
-
async function installAndDemo(cli, spec2, getQuestion,
|
|
2736
|
+
async function installAndDemo(cli, spec2, getQuestion, log4 = console.log) {
|
|
2735
2737
|
if (cli.kind === "claude") {
|
|
2736
|
-
|
|
2738
|
+
log4(`
|
|
2737
2739
|
${import_chalk10.default.dim("$")} ${renderCommand(["claude", ...claudeInstallArgs(spec2, true)])}`);
|
|
2738
2740
|
const r = installIntoClaude(spec2);
|
|
2739
2741
|
if (!r.ok) {
|
|
2740
|
-
|
|
2742
|
+
log4(import_chalk10.default.red(` ${r.conflict ? "" : "Install failed: "}${r.error}`));
|
|
2741
2743
|
return false;
|
|
2742
2744
|
}
|
|
2743
|
-
|
|
2744
|
-
if (spec2.apiKey)
|
|
2745
|
+
log4(` ${import_chalk10.default.green("\u2714")} MCP ${import_chalk10.default.bold(spec2.name)} added to Claude CLI (local scope \u2014 this directory).`);
|
|
2746
|
+
if (spec2.apiKey) log4(import_chalk10.default.dim(" The API key is stored in Claude's local MCP config for this directory."));
|
|
2745
2747
|
} else {
|
|
2746
2748
|
const r = installIntoCodex(spec2);
|
|
2747
2749
|
if (!r.ok) {
|
|
2748
|
-
|
|
2750
|
+
log4(import_chalk10.default.red(` ${r.conflict ? "" : `Could not write ${r.path}: `}${r.error}`));
|
|
2749
2751
|
return false;
|
|
2750
2752
|
}
|
|
2751
|
-
|
|
2753
|
+
log4(` ${import_chalk10.default.green("\u2714")} MCP ${import_chalk10.default.bold(spec2.name)} added to ${r.path}.`);
|
|
2752
2754
|
}
|
|
2753
2755
|
if (!spec2.apiKey) {
|
|
2754
2756
|
if (!supportsMcpLogin(cli.kind)) {
|
|
2755
|
-
|
|
2757
|
+
log4(`
|
|
2756
2758
|
${import_chalk10.default.dim("This proxy authenticates by login.")}`);
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2759
|
+
log4(import_chalk10.default.yellow(` Your ${cli.label}${cli.version ? ` (${cli.version})` : ""} can't start that sign-in from the shell.`));
|
|
2760
|
+
log4(import_chalk10.default.dim(` ${manualAuthHint(cli.kind, spec2.name)}`));
|
|
2761
|
+
log4(import_chalk10.default.dim(" (Upgrading the CLI lets apichat do this for you.)"));
|
|
2760
2762
|
} else {
|
|
2761
2763
|
const loginArgv = [cli.kind, "mcp", "login", spec2.name];
|
|
2762
|
-
|
|
2764
|
+
log4(`
|
|
2763
2765
|
${import_chalk10.default.dim("This proxy authenticates by login \u2014 signing you in:")}`);
|
|
2764
|
-
|
|
2766
|
+
log4(` ${import_chalk10.default.dim("$")} ${renderCommand(loginArgv)}`);
|
|
2765
2767
|
const r = run(loginArgv[0], loginArgv.slice(1), { interactive: true });
|
|
2766
2768
|
if (r.status !== 0) {
|
|
2767
|
-
|
|
2769
|
+
log4(import_chalk10.default.yellow(` Sign-in didn't complete (exit ${r.status ?? "?"}). Run \`${renderCommand(loginArgv)}\` yourself to finish it.`));
|
|
2768
2770
|
} else {
|
|
2769
|
-
|
|
2771
|
+
log4(` ${import_chalk10.default.green("\u2714")} Signed in.`);
|
|
2770
2772
|
}
|
|
2771
2773
|
}
|
|
2772
2774
|
}
|
|
2773
2775
|
if (cli.kind === "claude") {
|
|
2774
|
-
const
|
|
2775
|
-
if (
|
|
2776
|
-
|
|
2777
|
-
${cli.label} has the MCP configured, but reports: ${
|
|
2778
|
-
if (
|
|
2779
|
-
|
|
2776
|
+
const status2 = waitForClaudeConnection(spec2.name);
|
|
2777
|
+
if (status2 !== "connected") {
|
|
2778
|
+
log4(import_chalk10.default.yellow(`
|
|
2779
|
+
${cli.label} has the MCP configured, but reports: ${status2 === "needs-auth" ? "needs authentication" : status2 === "failed" ? "failed to connect" : "unknown status"}.`));
|
|
2780
|
+
if (status2 === "needs-auth") {
|
|
2781
|
+
log4(import_chalk10.default.dim(supportsMcpLogin("claude") ? ` Finish the sign-in with: claude mcp login ${spec2.name}` : ` ${manualAuthHint("claude", spec2.name)}`));
|
|
2780
2782
|
} else {
|
|
2781
|
-
|
|
2783
|
+
log4(import_chalk10.default.dim(` Check it with: claude mcp get ${spec2.name} (or /mcp inside Claude Code)`));
|
|
2782
2784
|
}
|
|
2783
|
-
|
|
2785
|
+
log4(import_chalk10.default.dim(" Chatting here instead so you still get an answer."));
|
|
2784
2786
|
return false;
|
|
2785
2787
|
}
|
|
2786
|
-
|
|
2788
|
+
log4(` ${import_chalk10.default.green("\u2714")} ${cli.label} reports the server connected.`);
|
|
2787
2789
|
}
|
|
2788
2790
|
const oneShot = cli.kind === "claude" ? claudeOneShot : codexOneShot;
|
|
2789
2791
|
const abilities = `What are the tool abilities of the MCP server "${spec2.name}"? List them briefly.`;
|
|
2790
|
-
|
|
2792
|
+
log4(`
|
|
2791
2793
|
${import_chalk10.default.dim("Checking what the API can do\u2026")}`);
|
|
2792
|
-
|
|
2794
|
+
log4(` ${import_chalk10.default.dim("$")} ${renderCommand(cli.kind === "claude" ? ["claude", "-p", abilities, "--allowedTools", `mcp__${spec2.name}__*`] : ["codex", "exec", "--skip-git-repo-check", abilities])}
|
|
2793
2795
|
`);
|
|
2794
2796
|
const check = oneShot(spec2, abilities);
|
|
2795
2797
|
if (check.status !== 0) {
|
|
2796
|
-
|
|
2798
|
+
log4(import_chalk10.default.yellow(`
|
|
2797
2799
|
${cli.label} exited with ${check.status ?? "no status"} \u2014 the MCP is installed, but the demo call failed.`));
|
|
2798
|
-
|
|
2800
|
+
log4(import_chalk10.default.yellow(` Open ${cli.label} and try it there${cli.kind === "claude" ? " (use /mcp to inspect the connection)" : ""}.`));
|
|
2799
2801
|
return true;
|
|
2800
2802
|
}
|
|
2801
2803
|
const question = await getQuestion();
|
|
2802
2804
|
if (question) {
|
|
2803
2805
|
const handoff = !!process.stdin.isTTY;
|
|
2804
2806
|
const ask = handoff ? cli.kind === "claude" ? claudeInteractive : codexInteractive : oneShot;
|
|
2805
|
-
|
|
2807
|
+
log4(`
|
|
2806
2808
|
${import_chalk10.default.dim(handoff ? `Opening ${cli.label} with your question \u2014 you'll stay in that session:` : `Your question, through ${cli.label}:`)}`);
|
|
2807
2809
|
const directed = directedPrompt(spec2, question);
|
|
2808
2810
|
const shown = cli.kind === "claude" ? handoff ? ["claude", directed, "--allowedTools", `mcp__${spec2.name}__*`, "--append-system-prompt", "\u2026"] : ["claude", "-p", directed, "--allowedTools", `mcp__${spec2.name}__*`] : handoff ? ["codex", directed] : ["codex", "exec", "--skip-git-repo-check", directed];
|
|
2809
|
-
|
|
2811
|
+
log4(` ${import_chalk10.default.dim("$")} ${renderCommand(shown)}
|
|
2810
2812
|
`);
|
|
2811
2813
|
const ans = ask(spec2, directedPrompt(spec2, question));
|
|
2812
|
-
if (ans.status !== 0)
|
|
2814
|
+
if (ans.status !== 0) log4(import_chalk10.default.yellow(`
|
|
2813
2815
|
${cli.label} exited with ${ans.status ?? "no status"} \u2014 the MCP stays installed.`));
|
|
2814
2816
|
}
|
|
2815
2817
|
const instructionFile = writeAgentInstruction(cli.kind, spec2);
|
|
2816
2818
|
const off = `npx apiblaze apichat ${spec2.name} --remove-mcp ${cli.kind}`;
|
|
2817
2819
|
const mdName = cli.kind === "claude" ? "CLAUDE.md" : "AGENTS.md";
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2820
|
+
log4("");
|
|
2821
|
+
log4(import_chalk10.default.bgRed.white.bold(` ${cli.label} will launch with your MCP to ${spec2.projectLabel} enabled from now on. `));
|
|
2822
|
+
log4(import_chalk10.default.red.bold(` Every ${cli.label} session in this directory can call your API's tools.`));
|
|
2821
2823
|
if (instructionFile) {
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
+
log4(import_chalk10.default.red(` We added a "use this MCP" block to your ${import_chalk10.default.bold(mdName)} \u2014 that is what makes it stick:`));
|
|
2825
|
+
log4(import_chalk10.default.red(` ${instructionFile}`));
|
|
2824
2826
|
} else {
|
|
2825
|
-
|
|
2827
|
+
log4(import_chalk10.default.red(` (Could not write ${mdName} here, so this applies only to sessions apichat starts.)`));
|
|
2826
2828
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
+
log4(import_chalk10.default.red(` Turn it off (removes the MCP server AND the ${mdName} block):`));
|
|
2830
|
+
log4(import_chalk10.default.red(` ${import_chalk10.default.bold(off)}`));
|
|
2829
2831
|
return true;
|
|
2830
2832
|
}
|
|
2831
2833
|
|
|
@@ -3301,13 +3303,13 @@ async function provision(spec2, target, opts) {
|
|
|
3301
3303
|
spinner.succeed(`Proxy provisioned${name !== base2 ? ` as "${name}"` : ""}.`);
|
|
3302
3304
|
break;
|
|
3303
3305
|
} catch (err) {
|
|
3304
|
-
const
|
|
3306
|
+
const status2 = err instanceof ApiError ? err.status : void 0;
|
|
3305
3307
|
const collision = err instanceof ApiError && (err.status === 409 || /exist|taken|available/i.test(err.message));
|
|
3306
3308
|
if (collision && attempt < 3) {
|
|
3307
3309
|
name = `${base2}${salt()}`;
|
|
3308
3310
|
continue;
|
|
3309
3311
|
}
|
|
3310
|
-
if (
|
|
3312
|
+
if (status2 === 401) {
|
|
3311
3313
|
if (!loggedIn && attempt < 3) {
|
|
3312
3314
|
clearAnonCred();
|
|
3313
3315
|
continue;
|
|
@@ -3682,9 +3684,9 @@ async function replTurn(p, messages, userText) {
|
|
|
3682
3684
|
const id = String(ev.toolCallId ?? "");
|
|
3683
3685
|
const meta = toolMeta.get(id);
|
|
3684
3686
|
const ok = ev.type === "tool-output-available";
|
|
3685
|
-
const
|
|
3687
|
+
const ms2 = meta ? Date.now() - meta.startedAt : void 0;
|
|
3686
3688
|
const mark = ok ? import_chalk12.default.green("\u2713") : import_chalk12.default.red("\u2717");
|
|
3687
|
-
console.log(` ${mark} ${import_chalk12.default.cyan(meta?.name ?? "tool")} ${import_chalk12.default.dim(`(${ok ? "ok" : "error"}${
|
|
3689
|
+
console.log(` ${mark} ${import_chalk12.default.cyan(meta?.name ?? "tool")} ${import_chalk12.default.dim(`(${ok ? "ok" : "error"}${ms2 != null ? `, ${ms2}ms` : ""})`)}`);
|
|
3688
3690
|
const detail = ok ? String(ev.output ?? "") : String(ev.errorText ?? "Tool call failed.");
|
|
3689
3691
|
if (meta) {
|
|
3690
3692
|
Object.assign(parts[meta.partIdx], ok ? { state: "output-available", output: detail } : { state: "output-error", errorText: detail });
|
|
@@ -5084,10 +5086,10 @@ async function chooseSecure(args) {
|
|
|
5084
5086
|
return null;
|
|
5085
5087
|
}
|
|
5086
5088
|
const spinner = quiet ? null : (0, import_ora6.default)("Reading your OpenAPI spec\u2026").start();
|
|
5087
|
-
const { status, data } = await proposeFromSpec(openapi).catch((err) => ({ status: 0, data: { error: err.message } }));
|
|
5088
|
-
if (
|
|
5089
|
+
const { status: status2, data } = await proposeFromSpec(openapi).catch((err) => ({ status: 0, data: { error: err.message } }));
|
|
5090
|
+
if (status2 !== 200 || !data || !Array.isArray(data.families)) {
|
|
5089
5091
|
spinner?.stop();
|
|
5090
|
-
const why = data?.details ?? data?.error ?? (
|
|
5092
|
+
const why = data?.details ?? data?.error ?? (status2 ? `HTTP ${status2}` : "network error");
|
|
5091
5093
|
say(import_chalk16.default.yellow(` \u26A0 Could not work out what to protect (${why}) \u2014 creating without rules. Later: npx ${B.cli} rule ${name}`));
|
|
5092
5094
|
return null;
|
|
5093
5095
|
}
|
|
@@ -6227,7 +6229,7 @@ async function runDev(options) {
|
|
|
6227
6229
|
projectId,
|
|
6228
6230
|
localPort: options.port,
|
|
6229
6231
|
onEntry: (entry) => say(formatLogLine(entry)),
|
|
6230
|
-
onStatus: (
|
|
6232
|
+
onStatus: (status2) => say(import_chalk17.default.gray(`[${projectId}] ${status2}`)),
|
|
6231
6233
|
onCapture: (req, note) => say(formatCapturedRequest(req, note)),
|
|
6232
6234
|
onCaptureStart: () => say(import_chalk17.default.magenta(`
|
|
6233
6235
|
\u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
|
|
@@ -7080,14 +7082,14 @@ async function runAgentChatRepl(opts) {
|
|
|
7080
7082
|
async function turn(content) {
|
|
7081
7083
|
messages.push({ role: "user", content });
|
|
7082
7084
|
process.stdout.write(import_chalk26.default.dim(" \u2026thinking\n"));
|
|
7083
|
-
const { status, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
|
|
7084
|
-
if (
|
|
7085
|
+
const { status: status2, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
|
|
7086
|
+
if (status2 === 402) {
|
|
7085
7087
|
console.log(import_chalk26.default.red(" Insufficient credits \u2014 top up to keep using the agents.\n"));
|
|
7086
7088
|
messages.pop();
|
|
7087
7089
|
return;
|
|
7088
7090
|
}
|
|
7089
|
-
if (
|
|
7090
|
-
console.log(import_chalk26.default.red(` Error (${
|
|
7091
|
+
if (status2 >= 400 || !data) {
|
|
7092
|
+
console.log(import_chalk26.default.red(` Error (${status2}): ${(data && data.error) ?? "request failed"}
|
|
7091
7093
|
`));
|
|
7092
7094
|
messages.pop();
|
|
7093
7095
|
return;
|
|
@@ -7188,17 +7190,17 @@ function maybeWarnNoTraffic(total, projectArg) {
|
|
|
7188
7190
|
`));
|
|
7189
7191
|
}
|
|
7190
7192
|
async function applyProposal(opts) {
|
|
7191
|
-
const
|
|
7193
|
+
const log4 = opts.log ?? ((s) => console.log(s));
|
|
7192
7194
|
const { projectId, apiVersion, tenant: tenant2, proposal, enable } = opts;
|
|
7193
7195
|
{
|
|
7194
7196
|
const m = await agentCall(`/projects/${projectId}/${apiVersion}/policies/model?tenantId=${encodeURIComponent(tenant2)}`, "POST", proposal.model);
|
|
7195
7197
|
if (m.status === 404) {
|
|
7196
|
-
|
|
7198
|
+
log4(import_chalk27.default.red(` Project ${projectId} ${apiVersion} not found on the authorization service \u2014 check \`apiblaze projects\`.
|
|
7197
7199
|
`));
|
|
7198
7200
|
return { ok: 0, fail: 0, enabled: false, aborted: true };
|
|
7199
7201
|
}
|
|
7200
7202
|
if (m.status >= 400) {
|
|
7201
|
-
|
|
7203
|
+
log4(import_chalk27.default.red(` Model save failed (${m.status}): ${m.data?.error ?? ""}
|
|
7202
7204
|
`));
|
|
7203
7205
|
return { ok: 0, fail: 0, enabled: false, aborted: true };
|
|
7204
7206
|
}
|
|
@@ -7230,17 +7232,17 @@ async function applyProposal(opts) {
|
|
|
7230
7232
|
}
|
|
7231
7233
|
}
|
|
7232
7234
|
if (fail6 > 0) {
|
|
7233
|
-
|
|
7234
|
-
for (const f of failed)
|
|
7235
|
+
log4(import_chalk27.default.red(` ${fail6} of ${proposal.routes.length} route rule(s) could not be saved:`));
|
|
7236
|
+
for (const f of failed) log4(import_chalk27.default.red(` ${f}`));
|
|
7235
7237
|
if (enable) {
|
|
7236
|
-
|
|
7238
|
+
log4(import_chalk27.default.red(" Enforcement NOT turned on \u2014 a route without a saved rule is left open by the gateway. Fix the rule(s) above and re-run with --enforce.\n"));
|
|
7237
7239
|
}
|
|
7238
7240
|
return { ok, fail: fail6, enabled: false, aborted: true, failed };
|
|
7239
7241
|
}
|
|
7240
7242
|
if (enable) {
|
|
7241
7243
|
const e = await agentCall(`/projects/${projectId}/${apiVersion}`, "PATCH", { authorization: { enforce_authorization: true }, tenant: tenant2 });
|
|
7242
7244
|
if (e.status >= 400) {
|
|
7243
|
-
|
|
7245
|
+
log4(import_chalk27.default.red(` Enforced ${ok} route(s) but turning on the project-level switch failed (${e.status}). Toggle "Enforce Authorization" on in the dashboard.
|
|
7244
7246
|
`));
|
|
7245
7247
|
return { ok, fail: fail6, enabled: false, failed };
|
|
7246
7248
|
}
|
|
@@ -7330,13 +7332,13 @@ async function runRule(projectArg, ruleText, opts) {
|
|
|
7330
7332
|
role: "user",
|
|
7331
7333
|
content: `Author this authorization rule and generate the COMPLETE proposal (OpenFGA model + per-route rules) NOW for every affected route. Do not ask clarifying questions; make reasonable assumptions and state them briefly. Rule: "${rule}"`
|
|
7332
7334
|
}];
|
|
7333
|
-
const { status, data } = await agentCall(
|
|
7335
|
+
const { status: status2, data } = await agentCall(
|
|
7334
7336
|
`/projects/${projectId}/${apiVersion}/authz/chat`,
|
|
7335
7337
|
"POST",
|
|
7336
7338
|
{ messages, included_sample_ids: sampleIds, existing_model: null, existing_routes: [] }
|
|
7337
7339
|
);
|
|
7338
|
-
if (
|
|
7339
|
-
spinner.fail(`Authorization agent error (${
|
|
7340
|
+
if (status2 >= 400) {
|
|
7341
|
+
spinner.fail(`Authorization agent error (${status2}): ${data?.error ?? ""}`);
|
|
7340
7342
|
return;
|
|
7341
7343
|
}
|
|
7342
7344
|
const proposal = data?.proposal;
|
|
@@ -7759,16 +7761,16 @@ async function runExport(projectArg, versionArg, opts) {
|
|
|
7759
7761
|
});
|
|
7760
7762
|
if (!job?.job_id) throw new Error("Failed to queue export.");
|
|
7761
7763
|
const statusPath = `/projects/${projectId}/${version2}/export/jobs/${job.job_id}`;
|
|
7762
|
-
let
|
|
7764
|
+
let status2 = "queued";
|
|
7763
7765
|
const started = Date.now();
|
|
7764
7766
|
process.stdout.write(import_chalk35.default.dim("Building bundle"));
|
|
7765
|
-
while (
|
|
7767
|
+
while (status2 !== "ready" && status2 !== "failed") {
|
|
7766
7768
|
if (Date.now() - started > 10 * 6e4) throw new Error("Export timed out after 10 minutes.");
|
|
7767
7769
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
7768
7770
|
process.stdout.write(import_chalk35.default.dim("."));
|
|
7769
7771
|
const st = await admin({ path: statusPath, method: "GET", summary: "poll export" });
|
|
7770
|
-
|
|
7771
|
-
if (
|
|
7772
|
+
status2 = st?.status ?? "queued";
|
|
7773
|
+
if (status2 === "failed") {
|
|
7772
7774
|
process.stdout.write("\n");
|
|
7773
7775
|
throw new Error(`Export failed: ${st?.error ?? "unknown error"}`);
|
|
7774
7776
|
}
|
|
@@ -7854,38 +7856,38 @@ async function publicFetch(path11) {
|
|
|
7854
7856
|
}
|
|
7855
7857
|
return { status: res.status, data, json };
|
|
7856
7858
|
}
|
|
7857
|
-
function registryDown(
|
|
7859
|
+
function registryDown(status2) {
|
|
7858
7860
|
return new Error(
|
|
7859
|
-
`The recipe registry at ${RECIPES_BASE}/api/recipes did not answer with JSON (HTTP ${
|
|
7861
|
+
`The recipe registry at ${RECIPES_BASE}/api/recipes did not answer with JSON (HTTP ${status2}).
|
|
7860
7862
|
Set APIBLAZE_RECIPES_BASE if you are pointing at a different deployment.`
|
|
7861
7863
|
);
|
|
7862
7864
|
}
|
|
7863
7865
|
async function searchRecipes(query, limit) {
|
|
7864
7866
|
const q = `?q=${encodeURIComponent(query ?? "")}${limit ? `&limit=${limit}` : ""}`;
|
|
7865
|
-
const { status, data, json } = await publicFetch(`/search${q}`);
|
|
7866
|
-
if (!json) throw registryDown(
|
|
7867
|
-
if (
|
|
7867
|
+
const { status: status2, data, json } = await publicFetch(`/search${q}`);
|
|
7868
|
+
if (!json) throw registryDown(status2);
|
|
7869
|
+
if (status2 >= 400) throw new ApiError(status2, data?.error ?? "Search failed.");
|
|
7868
7870
|
return data?.recipes ?? [];
|
|
7869
7871
|
}
|
|
7870
7872
|
async function getRecipeHead(ref) {
|
|
7871
|
-
const { status, data, json } = await publicFetch(`/${ref.handle}/${ref.slug}`);
|
|
7872
|
-
if (!json) throw registryDown(
|
|
7873
|
-
if (
|
|
7874
|
-
if (
|
|
7873
|
+
const { status: status2, data, json } = await publicFetch(`/${ref.handle}/${ref.slug}`);
|
|
7874
|
+
if (!json) throw registryDown(status2);
|
|
7875
|
+
if (status2 === 404) return null;
|
|
7876
|
+
if (status2 >= 400) throw new ApiError(status2, data?.error ?? "Lookup failed.");
|
|
7875
7877
|
return data;
|
|
7876
7878
|
}
|
|
7877
7879
|
async function fetchRecipeFile(ref, caller) {
|
|
7878
7880
|
const head = ref.revision !== void 0 ? null : await getRecipeHead(ref);
|
|
7879
7881
|
const revision = ref.revision ?? head?.revision;
|
|
7880
7882
|
if (revision !== void 0) {
|
|
7881
|
-
const { status, data, json } = await publicFetch(
|
|
7883
|
+
const { status: status2, data, json } = await publicFetch(
|
|
7882
7884
|
`/${ref.handle}/${ref.slug}/${revision}`
|
|
7883
7885
|
);
|
|
7884
|
-
if (!json) throw registryDown(
|
|
7885
|
-
if (
|
|
7886
|
+
if (!json) throw registryDown(status2);
|
|
7887
|
+
if (status2 === 410) {
|
|
7886
7888
|
throw new Error(`${ref.name}@${revision} was withdrawn by its publisher. Try a different revision.`);
|
|
7887
7889
|
}
|
|
7888
|
-
if (
|
|
7890
|
+
if (status2 < 400 && data) return data;
|
|
7889
7891
|
}
|
|
7890
7892
|
if (!caller) {
|
|
7891
7893
|
throw new Error(
|
|
@@ -7901,8 +7903,8 @@ async function fetchRecipeFile(ref, caller) {
|
|
|
7901
7903
|
summary: `Read private recipe ${ref.name}${revision !== void 0 ? `@${revision}` : ""}`
|
|
7902
7904
|
});
|
|
7903
7905
|
} catch (err) {
|
|
7904
|
-
const
|
|
7905
|
-
if (
|
|
7906
|
+
const status2 = err instanceof ApiError ? err.status : 0;
|
|
7907
|
+
if (status2 === 404 || status2 === 400) {
|
|
7906
7908
|
throw new Error(
|
|
7907
7909
|
`${ref.name} does not exist, or it is private and your team does not own it.
|
|
7908
7910
|
(admin-api exposes no authorized read for a private recipe file yet \u2014 only GET /recipes/public/* is registered, so private install cannot resolve.)`
|
|
@@ -9124,10 +9126,10 @@ init_resolve();
|
|
|
9124
9126
|
// src/lib/log-view.ts
|
|
9125
9127
|
var import_chalk39 = __toESM(require("chalk"));
|
|
9126
9128
|
var readline2 = __toESM(require("readline"));
|
|
9127
|
-
function statusColor(
|
|
9128
|
-
if (
|
|
9129
|
-
if (
|
|
9130
|
-
if (
|
|
9129
|
+
function statusColor(status2) {
|
|
9130
|
+
if (status2 < 300) return import_chalk39.default.green;
|
|
9131
|
+
if (status2 < 400) return import_chalk39.default.blue;
|
|
9132
|
+
if (status2 < 500) return import_chalk39.default.yellow;
|
|
9131
9133
|
return import_chalk39.default.red;
|
|
9132
9134
|
}
|
|
9133
9135
|
function methodColor(method) {
|
|
@@ -9157,10 +9159,10 @@ function formatRow(r) {
|
|
|
9157
9159
|
const time = import_chalk39.default.dim(clock(r.ts));
|
|
9158
9160
|
const method = methodColor(r.method)(String(r.method || "\u2014").padEnd(6));
|
|
9159
9161
|
const route = routeCell(String(r.route || "\u2014"));
|
|
9160
|
-
const
|
|
9161
|
-
const
|
|
9162
|
+
const status2 = statusColor(r.status)(String(r.status ?? "\u2014").padStart(3));
|
|
9163
|
+
const ms2 = import_chalk39.default.dim(`${r.duration_ms}ms`.padStart(8));
|
|
9162
9164
|
const step = r.error_step ? import_chalk39.default.dim(` ${r.error_step}`) : "";
|
|
9163
|
-
return `${time} ${method} ${route} ${
|
|
9165
|
+
return `${time} ${method} ${route} ${status2} ${ms2}${step}`;
|
|
9164
9166
|
}
|
|
9165
9167
|
function printRow(r, json) {
|
|
9166
9168
|
console.log(json ? JSON.stringify(r) : formatRow(r));
|
|
@@ -13026,10 +13028,33 @@ async function runOriginsRemove(origin, opts) {
|
|
|
13026
13028
|
}
|
|
13027
13029
|
|
|
13028
13030
|
// src/commands/op.ts
|
|
13029
|
-
var
|
|
13031
|
+
var import_chalk61 = __toESM(require("chalk"));
|
|
13032
|
+
init_auth();
|
|
13033
|
+
|
|
13034
|
+
// src/lib/op-call.ts
|
|
13030
13035
|
init_auth();
|
|
13031
13036
|
init_trace();
|
|
13032
13037
|
init_types();
|
|
13038
|
+
var DASHBOARD_BASE8 = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
|
|
13039
|
+
async function opCall(call2) {
|
|
13040
|
+
const token = getAccessToken();
|
|
13041
|
+
const res = await fetch(`${DASHBOARD_BASE8}/api/cli/op`, {
|
|
13042
|
+
method: "POST",
|
|
13043
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
13044
|
+
body: JSON.stringify({ path: call2.path, method: call2.method, body: call2.body })
|
|
13045
|
+
});
|
|
13046
|
+
let data = null;
|
|
13047
|
+
try {
|
|
13048
|
+
data = await res.json();
|
|
13049
|
+
} catch {
|
|
13050
|
+
}
|
|
13051
|
+
recordCall({ method: call2.method, path: call2.path, body: call2.body, status: res.status, summary: call2.summary });
|
|
13052
|
+
if (!res.ok) {
|
|
13053
|
+
const msg = data?.details ?? data?.error ?? res.statusText;
|
|
13054
|
+
throw new ApiError(res.status, typeof msg === "string" ? msg : JSON.stringify(msg), data);
|
|
13055
|
+
}
|
|
13056
|
+
return data;
|
|
13057
|
+
}
|
|
13033
13058
|
|
|
13034
13059
|
// src/commands/op-billing.ts
|
|
13035
13060
|
var import_chalk57 = __toESM(require("chalk"));
|
|
@@ -13043,7 +13068,7 @@ var SANDBOX = {
|
|
|
13043
13068
|
};
|
|
13044
13069
|
var dpHost = `${SANDBOX.project}.abz.run`;
|
|
13045
13070
|
var ZONE_COUNT = 3;
|
|
13046
|
-
var sleep = (
|
|
13071
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
13047
13072
|
async function readQuiet(read, tries = 8) {
|
|
13048
13073
|
let prev = await read();
|
|
13049
13074
|
for (let i = 0; i < tries; i++) {
|
|
@@ -13261,32 +13286,502 @@ function printChecks(checks) {
|
|
|
13261
13286
|
console.log("");
|
|
13262
13287
|
}
|
|
13263
13288
|
|
|
13264
|
-
// src/commands/op.ts
|
|
13265
|
-
var
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
}
|
|
13271
|
-
async function opCall(call) {
|
|
13289
|
+
// src/commands/op-test.ts
|
|
13290
|
+
var import_chalk58 = __toESM(require("chalk"));
|
|
13291
|
+
init_auth();
|
|
13292
|
+
init_types();
|
|
13293
|
+
var DASHBOARD_BASE9 = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
|
|
13294
|
+
async function runnerCall(body, timeoutS) {
|
|
13272
13295
|
const token = getAccessToken();
|
|
13273
|
-
const res = await fetch(`${
|
|
13296
|
+
const res = await fetch(`${DASHBOARD_BASE9}/api/cli/op/test`, {
|
|
13274
13297
|
method: "POST",
|
|
13275
13298
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
13276
|
-
body: JSON.stringify(
|
|
13299
|
+
body: JSON.stringify(body),
|
|
13300
|
+
signal: AbortSignal.timeout(timeoutS * 1e3)
|
|
13277
13301
|
});
|
|
13278
|
-
|
|
13279
|
-
try {
|
|
13280
|
-
data = await res.json();
|
|
13281
|
-
} catch {
|
|
13282
|
-
}
|
|
13283
|
-
recordCall({ method: call.method, path: call.path, body: call.body, status: res.status, summary: call.summary });
|
|
13302
|
+
const data = await res.json().catch(() => ({}));
|
|
13284
13303
|
if (!res.ok) {
|
|
13285
|
-
const msg = data
|
|
13304
|
+
const msg = data.error ?? res.statusText;
|
|
13286
13305
|
throw new ApiError(res.status, typeof msg === "string" ? msg : JSON.stringify(msg), data);
|
|
13287
13306
|
}
|
|
13288
13307
|
return data;
|
|
13289
13308
|
}
|
|
13309
|
+
var call = runnerCall;
|
|
13310
|
+
var matches = (id, group2, o) => {
|
|
13311
|
+
if (o.only) {
|
|
13312
|
+
const wanted = o.only.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13313
|
+
if (!wanted.some((w) => id === w || id.startsWith(w))) return false;
|
|
13314
|
+
}
|
|
13315
|
+
if (o.group && group2 && !group2.toLowerCase().includes(o.group.toLowerCase())) return false;
|
|
13316
|
+
if (o.group && !group2) return false;
|
|
13317
|
+
return true;
|
|
13318
|
+
};
|
|
13319
|
+
var ms = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}s` : `${n}ms`;
|
|
13320
|
+
var log = (s) => process.stderr.write(s + "\n");
|
|
13321
|
+
async function runOpTest(o = {}) {
|
|
13322
|
+
const timeoutS = o.timeout && o.timeout > 0 ? o.timeout : 150;
|
|
13323
|
+
const listing = await call({ action: "list" }, 60);
|
|
13324
|
+
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 }));
|
|
13326
|
+
const total = specs.length + guts.length;
|
|
13327
|
+
if (total === 0) {
|
|
13328
|
+
log(import_chalk58.default.yellow("nothing matched"));
|
|
13329
|
+
process.exitCode = 2;
|
|
13330
|
+
return;
|
|
13331
|
+
}
|
|
13332
|
+
log(import_chalk58.default.bold(`apiblaze op test \u2014 ${specs.length} spec(s) + ${guts.length} gut check(s) on sandbox ${listing.sandbox_team}`));
|
|
13333
|
+
const results = [];
|
|
13334
|
+
let failed = 0;
|
|
13335
|
+
const started = Date.now();
|
|
13336
|
+
const record = (r) => {
|
|
13337
|
+
results.push(r);
|
|
13338
|
+
if (!r.ok) failed += 1;
|
|
13339
|
+
const mark = r.ok ? import_chalk58.default.green("PASS") : import_chalk58.default.red("FAIL");
|
|
13340
|
+
log(`[${mark}] ${r.id} \u2014 ${r.label} (${ms(r.duration_ms)})`);
|
|
13341
|
+
if (!r.ok) {
|
|
13342
|
+
if (r.kind === "spec") for (const c of r.cases.filter((c2) => !c2.ok)) log(import_chalk58.default.red(` \u2717 ${c.name}: ${c.detail}`));
|
|
13343
|
+
else if (r.error) log(import_chalk58.default.red(` \u2717 ${r.error}`));
|
|
13344
|
+
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
|
+
}
|
|
13346
|
+
};
|
|
13347
|
+
for (const s of specs) {
|
|
13348
|
+
let r;
|
|
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) {
|
|
13359
|
+
let r;
|
|
13360
|
+
try {
|
|
13361
|
+
r = await call({ action: "gut", id: g.id }, timeoutS);
|
|
13362
|
+
} catch (e) {
|
|
13363
|
+
r = { kind: "gut", id: g.id, label: g.label, ok: false, results: [], error: e instanceof Error ? e.message : String(e), duration_ms: 0 };
|
|
13364
|
+
}
|
|
13365
|
+
record(r);
|
|
13366
|
+
if (!r.ok && o.failFast) break;
|
|
13367
|
+
}
|
|
13368
|
+
}
|
|
13369
|
+
const summary = { ran: results.length, passed: results.length - failed, failed, duration_ms: Date.now() - started, sandbox_team: listing.sandbox_team };
|
|
13370
|
+
log("");
|
|
13371
|
+
log(failed ? import_chalk58.default.red(`${summary.passed}/${summary.ran} passed, ${failed} failed (${ms(summary.duration_ms)})`) : import_chalk58.default.green(`${summary.passed}/${summary.ran} passed (${ms(summary.duration_ms)})`));
|
|
13372
|
+
if (o.json) process.stdout.write(JSON.stringify({ ...summary, results }, null, 2) + "\n");
|
|
13373
|
+
if (failed) process.exitCode = 1;
|
|
13374
|
+
}
|
|
13375
|
+
|
|
13376
|
+
// src/commands/op-consumer.ts
|
|
13377
|
+
var import_chalk59 = __toESM(require("chalk"));
|
|
13378
|
+
var SANDBOX_TENANT = "portalteststenant";
|
|
13379
|
+
var SANDBOX_PROXY = "portaltestproxypreapproved@2.7.0";
|
|
13380
|
+
function proxyUrlFor(spec2, path11 = "/get") {
|
|
13381
|
+
const [name, version2 = "1.0.0"] = spec2.split("@");
|
|
13382
|
+
return `https://${name}.abz.run/${version2}/prod${path11.startsWith("/") ? path11 : `/${path11}`}`;
|
|
13383
|
+
}
|
|
13384
|
+
function isPortalAudience(aud) {
|
|
13385
|
+
return /^https:\/\/[a-z0-9-]+\.portal\.apiblaze\.com\b/i.test(aud);
|
|
13386
|
+
}
|
|
13387
|
+
var log2 = (s) => process.stderr.write(s + "\n");
|
|
13388
|
+
function probeUrlFor(audience, path11 = "/") {
|
|
13389
|
+
const base2 = audience.replace(/\/+$/, "");
|
|
13390
|
+
return `${base2}/prod${path11.startsWith("/") ? path11 : `/${path11}`}`;
|
|
13391
|
+
}
|
|
13392
|
+
function tamper(token) {
|
|
13393
|
+
const parts = token.split(".");
|
|
13394
|
+
if (parts.length !== 3) return token + "x";
|
|
13395
|
+
const sig = parts[2];
|
|
13396
|
+
const i = Math.floor(sig.length / 2);
|
|
13397
|
+
const c = sig[i] === "A" ? "B" : "A";
|
|
13398
|
+
parts[2] = sig.slice(0, i) + c + sig.slice(i + 1);
|
|
13399
|
+
return parts.join(".");
|
|
13400
|
+
}
|
|
13401
|
+
function audienceOf(token) {
|
|
13402
|
+
try {
|
|
13403
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString());
|
|
13404
|
+
return typeof payload.aud === "string" ? payload.aud : Array.isArray(payload.aud) ? String(payload.aud[0]) : null;
|
|
13405
|
+
} catch {
|
|
13406
|
+
return null;
|
|
13407
|
+
}
|
|
13408
|
+
}
|
|
13409
|
+
async function status(url, headers) {
|
|
13410
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(15e3), redirect: "manual" });
|
|
13411
|
+
const body = (await res.text().catch(() => "")).slice(0, 300);
|
|
13412
|
+
return { status: res.status, body };
|
|
13413
|
+
}
|
|
13414
|
+
async function runOpConsumer(o = {}) {
|
|
13415
|
+
const t0 = Date.now();
|
|
13416
|
+
const creds = loadConsumer();
|
|
13417
|
+
const done = (r) => {
|
|
13418
|
+
const out = { kind: "consumer", id: "consumer-door", label: "Consumer door: the stored consumer token opens its tenant's OAuth-door proxy", duration_ms: Date.now() - t0, ...r };
|
|
13419
|
+
const mark = out.skipped ? import_chalk59.default.yellow("SKIP") : out.ok ? import_chalk59.default.green("PASS") : import_chalk59.default.red("FAIL");
|
|
13420
|
+
log2(`[${mark}] ${out.id} \u2014 ${out.label}${out.tenant ? ` (tenant ${out.tenant})` : ""}${out.url ? ` \u2192 ${out.url}` : ""}`);
|
|
13421
|
+
for (const c of out.cases) if (!c.ok || out.skipped) log2((c.ok ? import_chalk59.default.dim : import_chalk59.default.red)(` ${c.ok ? "ok " : "\u2717"} ${c.name}: ${c.detail}`));
|
|
13422
|
+
if (o.json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
13423
|
+
if (!out.ok && !out.skipped) process.exitCode = 1;
|
|
13424
|
+
if (out.skipped) process.exitCode = 2;
|
|
13425
|
+
return out;
|
|
13426
|
+
};
|
|
13427
|
+
if (!creds) {
|
|
13428
|
+
return done({ ok: false, skipped: true, cases: [{ name: "consumer session", ok: false, detail: "none stored \u2014 run `apiblaze consumer login --team <team> --tenant <tenant>` first" }] });
|
|
13429
|
+
}
|
|
13430
|
+
if (Date.now() >= creds.expiresAt) {
|
|
13431
|
+
return done({ ok: false, skipped: true, tenant: creds.tenant, cases: [{ name: "consumer session", ok: false, detail: `expired ${new Date(creds.expiresAt).toISOString()} \u2014 run \`apiblaze consumer login --tenant ${creds.tenant}\`` }] });
|
|
13432
|
+
}
|
|
13433
|
+
const audience = audienceOf(creds.accessToken);
|
|
13434
|
+
if (!audience) {
|
|
13435
|
+
return done({ ok: false, tenant: creds.tenant, cases: [{ name: "token audience", ok: false, detail: "the stored access token carries no aud claim" }] });
|
|
13436
|
+
}
|
|
13437
|
+
let url = null;
|
|
13438
|
+
if (o.url) url = o.url;
|
|
13439
|
+
else if (o.proxy) url = proxyUrlFor(o.proxy, o.path ?? "/get");
|
|
13440
|
+
else if (creds.tenant === SANDBOX_TENANT) url = proxyUrlFor(SANDBOX_PROXY, o.path ?? "/get");
|
|
13441
|
+
else if (!isPortalAudience(audience)) url = probeUrlFor(audience, o.path ?? "/");
|
|
13442
|
+
if (!url) {
|
|
13443
|
+
return done({ ok: false, skipped: true, tenant: creds.tenant, audience, cases: [{ name: "target", ok: false, detail: `the session's audience is the tenant portal (${audience}); pass --proxy <name@version> (an OAuth-door proxy of tenant ${creds.tenant}) or --url` }] });
|
|
13444
|
+
}
|
|
13445
|
+
const cases = [];
|
|
13446
|
+
try {
|
|
13447
|
+
const noAuth = await status(url, {});
|
|
13448
|
+
cases.push({ name: "no token \u2192 401", ok: noAuth.status === 401, detail: `${noAuth.status} ${noAuth.body}` });
|
|
13449
|
+
const withAuth = await status(url, { Authorization: `Bearer ${creds.accessToken}` });
|
|
13450
|
+
const opened = withAuth.status !== 401 && withAuth.status !== 403;
|
|
13451
|
+
cases.push({ name: "valid token \u2192 door opens (not 401/403)", ok: opened, detail: `${withAuth.status} ${withAuth.body}` });
|
|
13452
|
+
const bad = await status(url, { Authorization: `Bearer ${tamper(creds.accessToken)}` });
|
|
13453
|
+
cases.push({ name: "tampered signature \u2192 401", ok: bad.status === 401, detail: `${bad.status} ${bad.body}` });
|
|
13454
|
+
} catch (e) {
|
|
13455
|
+
cases.push({ name: "probe", ok: false, detail: e instanceof Error ? e.message : String(e) });
|
|
13456
|
+
}
|
|
13457
|
+
return done({ ok: cases.every((c) => c.ok), tenant: creds.tenant, audience, url, cases });
|
|
13458
|
+
}
|
|
13459
|
+
|
|
13460
|
+
// src/commands/op-perf.ts
|
|
13461
|
+
var import_chalk60 = __toESM(require("chalk"));
|
|
13462
|
+
var RPS_DEFAULT = 100;
|
|
13463
|
+
var RPS_CAP = 300;
|
|
13464
|
+
var SECONDS_DEFAULT = 60;
|
|
13465
|
+
var SECONDS_CAP = 300;
|
|
13466
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
13467
|
+
var PROGRESS_EVERY_MS = 1e4;
|
|
13468
|
+
var GATEWAY_WAIT_MS = 9e4;
|
|
13469
|
+
var GATEWAY_POLL_MS = 1e4;
|
|
13470
|
+
var GATEWAY_MIN_SHARE = 0.8;
|
|
13471
|
+
var DEFAULT_THRESHOLDS = {
|
|
13472
|
+
p50_ms: 250,
|
|
13473
|
+
p95_ms: 800,
|
|
13474
|
+
error_rate_pct: 0.5,
|
|
13475
|
+
gateway_p50_ms: 60,
|
|
13476
|
+
gateway_p95_ms: 250
|
|
13477
|
+
};
|
|
13478
|
+
function percentile(values, p) {
|
|
13479
|
+
if (values.length === 0) return 0;
|
|
13480
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
13481
|
+
const rank = Math.ceil(p * sorted.length) - 1;
|
|
13482
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank))];
|
|
13483
|
+
}
|
|
13484
|
+
function clampRate(rps, seconds) {
|
|
13485
|
+
const r = Number.isFinite(rps) && rps > 0 ? Math.min(RPS_CAP, rps) : RPS_DEFAULT;
|
|
13486
|
+
const s = Number.isFinite(seconds) && seconds > 0 ? Math.min(SECONDS_CAP, seconds) : SECONDS_DEFAULT;
|
|
13487
|
+
return { rps: r, seconds: s };
|
|
13488
|
+
}
|
|
13489
|
+
function thresholdsFrom(o) {
|
|
13490
|
+
const pick2 = (v, d) => Number.isFinite(v) && v >= 0 ? v : d;
|
|
13491
|
+
return {
|
|
13492
|
+
p50_ms: pick2(o.maxP50, DEFAULT_THRESHOLDS.p50_ms),
|
|
13493
|
+
p95_ms: pick2(o.maxP95, DEFAULT_THRESHOLDS.p95_ms),
|
|
13494
|
+
error_rate_pct: pick2(o.maxErrorRate, DEFAULT_THRESHOLDS.error_rate_pct),
|
|
13495
|
+
gateway_p50_ms: pick2(o.maxGatewayP50, DEFAULT_THRESHOLDS.gateway_p50_ms),
|
|
13496
|
+
gateway_p95_ms: pick2(o.maxGatewayP95, DEFAULT_THRESHOLDS.gateway_p95_ms)
|
|
13497
|
+
};
|
|
13498
|
+
}
|
|
13499
|
+
function perfOptionsFrom(opts) {
|
|
13500
|
+
const num = (v) => {
|
|
13501
|
+
if (v === void 0 || v === null || v === "") return void 0;
|
|
13502
|
+
const n = Number(v);
|
|
13503
|
+
return Number.isFinite(n) ? n : void 0;
|
|
13504
|
+
};
|
|
13505
|
+
const str = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
13506
|
+
return {
|
|
13507
|
+
json: !!opts.json,
|
|
13508
|
+
rps: num(opts.rps),
|
|
13509
|
+
seconds: num(opts.seconds),
|
|
13510
|
+
url: str(opts.url),
|
|
13511
|
+
key: str(opts.key),
|
|
13512
|
+
maxP50: num(opts.maxP50),
|
|
13513
|
+
maxP95: num(opts.maxP95),
|
|
13514
|
+
maxErrorRate: num(opts.maxErrorRate),
|
|
13515
|
+
maxGatewayP50: num(opts.maxGatewayP50),
|
|
13516
|
+
maxGatewayP95: num(opts.maxGatewayP95)
|
|
13517
|
+
};
|
|
13518
|
+
}
|
|
13519
|
+
function summarize(samples, elapsedMs) {
|
|
13520
|
+
let ok = 0, non2xx = 0, timeouts = 0, network = 0;
|
|
13521
|
+
for (const s of samples) {
|
|
13522
|
+
if (s.error === "timeout") timeouts += 1;
|
|
13523
|
+
else if (s.error === "network") network += 1;
|
|
13524
|
+
else if (s.status >= 200 && s.status < 300) ok += 1;
|
|
13525
|
+
else non2xx += 1;
|
|
13526
|
+
}
|
|
13527
|
+
const sent = samples.length;
|
|
13528
|
+
const errors = non2xx + timeouts + network;
|
|
13529
|
+
const ms2 = samples.map((s) => s.ms);
|
|
13530
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
13531
|
+
return {
|
|
13532
|
+
sent,
|
|
13533
|
+
ok,
|
|
13534
|
+
errors,
|
|
13535
|
+
non2xx,
|
|
13536
|
+
timeouts,
|
|
13537
|
+
network,
|
|
13538
|
+
error_rate_pct: sent ? Math.round(errors / sent * 1e4) / 100 : 0,
|
|
13539
|
+
p50: r1(percentile(ms2, 0.5)),
|
|
13540
|
+
p95: r1(percentile(ms2, 0.95)),
|
|
13541
|
+
p99: r1(percentile(ms2, 0.99)),
|
|
13542
|
+
rps_achieved: elapsedMs > 0 ? r1(sent / (elapsedMs / 1e3)) : 0
|
|
13543
|
+
};
|
|
13544
|
+
}
|
|
13545
|
+
function evaluate(client, gateway, t) {
|
|
13546
|
+
const out = [];
|
|
13547
|
+
if (client.p50 > t.p50_ms) out.push({ metric: "client_p50", value: client.p50, limit: t.p50_ms });
|
|
13548
|
+
if (client.p95 > t.p95_ms) out.push({ metric: "client_p95", value: client.p95, limit: t.p95_ms });
|
|
13549
|
+
if (client.error_rate_pct > t.error_rate_pct) out.push({ metric: "error_rate", value: client.error_rate_pct, limit: t.error_rate_pct });
|
|
13550
|
+
if (gateway) {
|
|
13551
|
+
if (gateway.p50 > t.gateway_p50_ms) out.push({ metric: "gateway_p50", value: gateway.p50, limit: t.gateway_p50_ms });
|
|
13552
|
+
if (gateway.p95 > t.gateway_p95_ms) out.push({ metric: "gateway_p95", value: gateway.p95, limit: t.gateway_p95_ms });
|
|
13553
|
+
}
|
|
13554
|
+
return out;
|
|
13555
|
+
}
|
|
13556
|
+
function schedule(opts) {
|
|
13557
|
+
return new Promise((resolve3) => {
|
|
13558
|
+
const start = Date.now();
|
|
13559
|
+
let sent = 0;
|
|
13560
|
+
let timer;
|
|
13561
|
+
const finish = () => {
|
|
13562
|
+
if (timer) clearTimeout(timer);
|
|
13563
|
+
opts.signal?.removeEventListener("abort", finish);
|
|
13564
|
+
resolve3({ sent, elapsedMs: Date.now() - start });
|
|
13565
|
+
};
|
|
13566
|
+
opts.signal?.addEventListener("abort", finish, { once: true });
|
|
13567
|
+
const tick = () => {
|
|
13568
|
+
if (opts.signal?.aborted) return;
|
|
13569
|
+
const due = Math.min(opts.total, Math.floor((Date.now() - start) / opts.intervalMs) + 1);
|
|
13570
|
+
while (sent < due) {
|
|
13571
|
+
opts.fire(sent);
|
|
13572
|
+
sent += 1;
|
|
13573
|
+
}
|
|
13574
|
+
if (sent >= opts.total) return finish();
|
|
13575
|
+
const next = start + sent * opts.intervalMs;
|
|
13576
|
+
timer = setTimeout(tick, Math.max(0, next - Date.now()));
|
|
13577
|
+
};
|
|
13578
|
+
if (opts.total <= 0 || opts.signal?.aborted) return finish();
|
|
13579
|
+
tick();
|
|
13580
|
+
});
|
|
13581
|
+
}
|
|
13582
|
+
function proxyFromUrl(url) {
|
|
13583
|
+
return new URL(url).hostname.split(".")[0];
|
|
13584
|
+
}
|
|
13585
|
+
var log3 = (s) => process.stderr.write(s + "\n");
|
|
13586
|
+
var fmtMs = (n) => `${Math.round(n)}ms`;
|
|
13587
|
+
var sleep2 = (ms2, signal) => new Promise((resolve3) => {
|
|
13588
|
+
const t = setTimeout(done, ms2);
|
|
13589
|
+
function done() {
|
|
13590
|
+
clearTimeout(t);
|
|
13591
|
+
signal?.removeEventListener("abort", done);
|
|
13592
|
+
resolve3();
|
|
13593
|
+
}
|
|
13594
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
13595
|
+
});
|
|
13596
|
+
async function fireRequest(url, key, samples) {
|
|
13597
|
+
const t0 = performance.now();
|
|
13598
|
+
try {
|
|
13599
|
+
const res = await fetch(url, {
|
|
13600
|
+
method: "GET",
|
|
13601
|
+
headers: { "X-API-Key": key, "User-Agent": "apiblaze-cli op-perf" },
|
|
13602
|
+
keepalive: true,
|
|
13603
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
13604
|
+
});
|
|
13605
|
+
await res.arrayBuffer();
|
|
13606
|
+
samples.push({ status: res.status, ms: performance.now() - t0 });
|
|
13607
|
+
} catch (e) {
|
|
13608
|
+
const name = e?.name;
|
|
13609
|
+
const timeout = name === "TimeoutError" || name === "AbortError";
|
|
13610
|
+
samples.push({ status: 0, ms: performance.now() - t0, error: timeout ? "timeout" : "network" });
|
|
13611
|
+
}
|
|
13612
|
+
}
|
|
13613
|
+
async function readGateway(proxy, sent, signal) {
|
|
13614
|
+
const q = `?hours=1&project=${encodeURIComponent(proxy)}`;
|
|
13615
|
+
const deadline = Date.now() + GATEWAY_WAIT_MS;
|
|
13616
|
+
const need = Math.ceil(sent * GATEWAY_MIN_SHARE);
|
|
13617
|
+
let lastSeen = 0;
|
|
13618
|
+
let lastErr = "";
|
|
13619
|
+
for (; ; ) {
|
|
13620
|
+
if (signal.aborted) return null;
|
|
13621
|
+
try {
|
|
13622
|
+
const s = await opCall({ method: "GET", path: `/operator/latency/summary${q}`, summary: "perf gate: gateway share" });
|
|
13623
|
+
lastSeen = Number(s?.requests ?? 0);
|
|
13624
|
+
if (lastSeen >= need) {
|
|
13625
|
+
const ov = s?.apiblaze_overhead_ms ?? {};
|
|
13626
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
13627
|
+
const g = { p50: Number(ov.p50 ?? 0), p95: Number(ov.p95 ?? 0), p99: num(ov.p99), requests: lastSeen };
|
|
13628
|
+
const kv = num(s?.kv_reads_per_request ?? s?.kv_per_req);
|
|
13629
|
+
const redis = num(s?.redis_commands_per_request ?? s?.redis_per_req);
|
|
13630
|
+
if (kv !== void 0) g.kv_per_req = kv;
|
|
13631
|
+
if (redis !== void 0) g.redis_per_req = redis;
|
|
13632
|
+
try {
|
|
13633
|
+
const blame = await opCall({ method: "GET", path: `/operator/latency${q}`, summary: "perf gate: blame table" });
|
|
13634
|
+
const rows = blame?.blame ?? [];
|
|
13635
|
+
if (rows.length) g.blame = rows.slice(0, 3).map((r) => ({ feature: String(r.feature), dep: String(r.dep), share: Number(r.share ?? 0), p95_ms: Number(r.p95_ms ?? 0) }));
|
|
13636
|
+
} catch {
|
|
13637
|
+
}
|
|
13638
|
+
return g;
|
|
13639
|
+
}
|
|
13640
|
+
} catch (e) {
|
|
13641
|
+
lastErr = e instanceof Error ? e.message : String(e);
|
|
13642
|
+
}
|
|
13643
|
+
const left = deadline - Date.now();
|
|
13644
|
+
if (left <= 0) {
|
|
13645
|
+
log3(import_chalk60.default.yellow(` gateway: not available \u2014 Analytics Engine had ${lastSeen}/${sent} rows for ${proxy} after ${GATEWAY_WAIT_MS / 1e3}s${lastErr ? ` (last error: ${lastErr})` : ""}`));
|
|
13646
|
+
return null;
|
|
13647
|
+
}
|
|
13648
|
+
log3(import_chalk60.default.dim(` waiting for the latency ledger\u2026 ${lastSeen}/${sent} rows for ${proxy}${lastErr ? ` (${lastErr})` : ""}`));
|
|
13649
|
+
await sleep2(Math.min(GATEWAY_POLL_MS, left), signal);
|
|
13650
|
+
}
|
|
13651
|
+
}
|
|
13652
|
+
async function runOpPerf(o = {}) {
|
|
13653
|
+
const { rps, seconds } = clampRate(o.rps, o.seconds);
|
|
13654
|
+
const thresholds = thresholdsFrom(o);
|
|
13655
|
+
const ctrl = new AbortController();
|
|
13656
|
+
const onSigint = () => {
|
|
13657
|
+
log3(import_chalk60.default.yellow("\ninterrupted \u2014 stopping the load and tearing down the fixture"));
|
|
13658
|
+
ctrl.abort();
|
|
13659
|
+
};
|
|
13660
|
+
process.once("SIGINT", onSigint);
|
|
13661
|
+
let fixture;
|
|
13662
|
+
if (o.url) {
|
|
13663
|
+
if (!o.key) throw new Error("--url needs --key (the fixture proxy's API key)");
|
|
13664
|
+
fixture = { url: o.url, key: o.key, proxy: proxyFromUrl(o.url), provisioned: false };
|
|
13665
|
+
} else {
|
|
13666
|
+
log3(import_chalk60.default.dim("provisioning the perf fixture through the runner\u2026"));
|
|
13667
|
+
const f = await runnerCall({ action: "perf-fixture" }, 120);
|
|
13668
|
+
if (!f?.url || !f?.key) throw new Error(`perf-fixture returned no url/key: ${JSON.stringify(f).slice(0, 200)}`);
|
|
13669
|
+
fixture = { url: f.url, key: f.key, proxy: f.proxy || proxyFromUrl(f.url), team: f.team, provisioned: true };
|
|
13670
|
+
}
|
|
13671
|
+
try {
|
|
13672
|
+
const total = Math.round(rps * seconds);
|
|
13673
|
+
const intervalMs = 1e3 / rps;
|
|
13674
|
+
log3(import_chalk60.default.bold(`apiblaze op test --perf \u2014 ${rps} rps \xD7 ${seconds}s (${total} requests) against ${fixture.url}${fixture.team ? ` (sandbox ${fixture.team})` : ""}`));
|
|
13675
|
+
const samples = [];
|
|
13676
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
13677
|
+
let fired = 0;
|
|
13678
|
+
const t0 = Date.now();
|
|
13679
|
+
const progress = setInterval(() => {
|
|
13680
|
+
const m = summarize(samples, Date.now() - t0);
|
|
13681
|
+
const secs = (Date.now() - t0) / 1e3;
|
|
13682
|
+
log3(import_chalk60.default.dim(` ${Math.round(secs)}s sent ${fired} ok ${m.ok} err ${m.errors} p50 ${fmtMs(m.p50)} p95 ${fmtMs(m.p95)} (${(fired / secs).toFixed(1)} rps)`));
|
|
13683
|
+
}, PROGRESS_EVERY_MS);
|
|
13684
|
+
const { sent, elapsedMs } = await schedule({
|
|
13685
|
+
total,
|
|
13686
|
+
intervalMs,
|
|
13687
|
+
signal: ctrl.signal,
|
|
13688
|
+
fire: () => {
|
|
13689
|
+
fired += 1;
|
|
13690
|
+
const p = fireRequest(fixture.url, fixture.key, samples);
|
|
13691
|
+
inflight.add(p);
|
|
13692
|
+
p.finally(() => inflight.delete(p));
|
|
13693
|
+
}
|
|
13694
|
+
});
|
|
13695
|
+
clearInterval(progress);
|
|
13696
|
+
if (inflight.size) log3(import_chalk60.default.dim(` load sent; waiting for ${inflight.size} in-flight response(s) (\u2264 ${REQUEST_TIMEOUT_MS / 1e3}s)\u2026`));
|
|
13697
|
+
await Promise.all([...inflight]);
|
|
13698
|
+
const client = summarize(samples, elapsedMs);
|
|
13699
|
+
log3("");
|
|
13700
|
+
log3(` ${import_chalk60.default.bold("client ")} p50 ${fmtMs(client.p50).padStart(7)} p95 ${fmtMs(client.p95).padStart(7)} p99 ${fmtMs(client.p99).padStart(7)} errors ${client.error_rate_pct.toFixed(2)}% (${client.errors}/${client.sent}: ${client.non2xx} non-2xx, ${client.timeouts} timeouts, ${client.network} network) achieved ${client.rps_achieved} rps of ${rps}`);
|
|
13701
|
+
const gateway = ctrl.signal.aborted ? null : await readGateway(fixture.proxy, sent, ctrl.signal);
|
|
13702
|
+
if (gateway) {
|
|
13703
|
+
const extras = [
|
|
13704
|
+
gateway.kv_per_req !== void 0 ? `kv/req ${gateway.kv_per_req}` : "",
|
|
13705
|
+
gateway.redis_per_req !== void 0 ? `redis/req ${gateway.redis_per_req}` : ""
|
|
13706
|
+
].filter(Boolean).join(" ");
|
|
13707
|
+
log3(` ${import_chalk60.default.bold("gateway")} p50 ${fmtMs(gateway.p50).padStart(7)} p95 ${fmtMs(gateway.p95).padStart(7)}${gateway.p99 !== void 0 ? ` p99 ${fmtMs(gateway.p99).padStart(7)}` : ""} ${import_chalk60.default.dim(`(${gateway.requests} ledger rows${extras ? `, ${extras}` : ""})`)}`);
|
|
13708
|
+
for (const b of gateway.blame ?? []) log3(import_chalk60.default.dim(` ${(b.share * 100).toFixed(1).padStart(5)}% p95 ${fmtMs(b.p95_ms).padStart(6)} ${b.feature} \u2192 ${b.dep}`));
|
|
13709
|
+
} else {
|
|
13710
|
+
log3(` ${import_chalk60.default.bold("gateway")} ${import_chalk60.default.yellow("not available")} ${import_chalk60.default.dim("(gateway thresholds not evaluated)")}`);
|
|
13711
|
+
}
|
|
13712
|
+
const breaches = evaluate(client, gateway, thresholds);
|
|
13713
|
+
const aborted = ctrl.signal.aborted;
|
|
13714
|
+
const ok = breaches.length === 0 && !aborted;
|
|
13715
|
+
log3("");
|
|
13716
|
+
const row = (metric, value, limit, unit) => {
|
|
13717
|
+
const hit = breaches.find((b) => b.metric === metric);
|
|
13718
|
+
log3(`[${hit ? import_chalk60.default.red("FAIL") : import_chalk60.default.green("PASS")}] ${metric.padEnd(11)} ${String(value)}${unit} ${hit ? ">" : "\u2264"} ${limit}${unit}`);
|
|
13719
|
+
};
|
|
13720
|
+
row("client_p50", client.p50, thresholds.p50_ms, "ms");
|
|
13721
|
+
row("client_p95", client.p95, thresholds.p95_ms, "ms");
|
|
13722
|
+
row("error_rate", client.error_rate_pct, thresholds.error_rate_pct, "%");
|
|
13723
|
+
if (gateway) {
|
|
13724
|
+
row("gateway_p50", gateway.p50, thresholds.gateway_p50_ms, "ms");
|
|
13725
|
+
row("gateway_p95", gateway.p95, thresholds.gateway_p95_ms, "ms");
|
|
13726
|
+
} else {
|
|
13727
|
+
log3(`[${import_chalk60.default.yellow("SKIP")}] gateway_p50 / gateway_p95 \u2014 ledger rows not available`);
|
|
13728
|
+
}
|
|
13729
|
+
log3("");
|
|
13730
|
+
if (aborted) log3(import_chalk60.default.red(`FAIL \u2014 interrupted after ${sent}/${total} requests`));
|
|
13731
|
+
else if (ok) log3(import_chalk60.default.green(`PASS \u2014 ${client.sent} requests at ${client.rps_achieved} rps, no threshold breached`));
|
|
13732
|
+
else log3(import_chalk60.default.red(`FAIL \u2014 ${breaches.length} threshold(s) breached: ${breaches.map((b) => `${b.metric} ${b.value} > ${b.limit}`).join(", ")}`));
|
|
13733
|
+
if (o.json) {
|
|
13734
|
+
const doc = {
|
|
13735
|
+
kind: "perf",
|
|
13736
|
+
fixture: { url: fixture.url, proxy: fixture.proxy, team: fixture.team ?? null, provisioned: fixture.provisioned },
|
|
13737
|
+
rps_target: rps,
|
|
13738
|
+
seconds,
|
|
13739
|
+
rps_achieved: client.rps_achieved,
|
|
13740
|
+
sent: client.sent,
|
|
13741
|
+
// 2xx count. The plan's shape spells both this and the verdict `ok`; a
|
|
13742
|
+
// fixing loop reads the verdict, so that keeps the name (last field).
|
|
13743
|
+
succeeded: client.ok,
|
|
13744
|
+
errors: client.errors,
|
|
13745
|
+
error_rate_pct: client.error_rate_pct,
|
|
13746
|
+
error_detail: { non2xx: client.non2xx, timeouts: client.timeouts, network: client.network },
|
|
13747
|
+
client: { p50: client.p50, p95: client.p95, p99: client.p99 },
|
|
13748
|
+
gateway: gateway ? {
|
|
13749
|
+
p50: gateway.p50,
|
|
13750
|
+
p95: gateway.p95,
|
|
13751
|
+
...gateway.p99 !== void 0 ? { p99: gateway.p99 } : {},
|
|
13752
|
+
...gateway.kv_per_req !== void 0 ? { kv_per_req: gateway.kv_per_req } : {},
|
|
13753
|
+
...gateway.redis_per_req !== void 0 ? { redis_per_req: gateway.redis_per_req } : {},
|
|
13754
|
+
requests: gateway.requests,
|
|
13755
|
+
...gateway.blame ? { blame: gateway.blame } : {}
|
|
13756
|
+
} : null,
|
|
13757
|
+
thresholds,
|
|
13758
|
+
breaches,
|
|
13759
|
+
...aborted ? { interrupted: true } : {},
|
|
13760
|
+
ok
|
|
13761
|
+
};
|
|
13762
|
+
process.stdout.write(JSON.stringify(doc, null, 2) + "\n");
|
|
13763
|
+
}
|
|
13764
|
+
if (!ok) process.exitCode = 1;
|
|
13765
|
+
} finally {
|
|
13766
|
+
process.off("SIGINT", onSigint);
|
|
13767
|
+
if (fixture.provisioned) {
|
|
13768
|
+
try {
|
|
13769
|
+
await runnerCall({ action: "perf-teardown", proxy: fixture.proxy }, 120);
|
|
13770
|
+
log3(import_chalk60.default.dim(`fixture ${fixture.proxy} torn down`));
|
|
13771
|
+
} catch (e) {
|
|
13772
|
+
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
|
+
process.exitCode = 1;
|
|
13774
|
+
}
|
|
13775
|
+
}
|
|
13776
|
+
}
|
|
13777
|
+
}
|
|
13778
|
+
|
|
13779
|
+
// src/commands/op.ts
|
|
13780
|
+
var OPERATOR_EMAILS = /* @__PURE__ */ new Set(["julienpmjacquet@gmail.com", "chkev@umich.edu"]);
|
|
13781
|
+
function isOperatorLogin() {
|
|
13782
|
+
const email = loadCredentials()?.email?.toLowerCase();
|
|
13783
|
+
return !!email && OPERATOR_EMAILS.has(email);
|
|
13784
|
+
}
|
|
13290
13785
|
var OP_COMMANDS = [
|
|
13291
13786
|
{ cmd: "op latency", blurb: "which FEATURE is slow, and what inside it", extra: "--hours N --team T --project P" },
|
|
13292
13787
|
{ cmd: "op latency slow", blurb: "slowest requests, each with a request id you can look up", extra: "--min-ms N" },
|
|
@@ -13298,102 +13793,135 @@ var OP_COMMANDS = [
|
|
|
13298
13793
|
{ cmd: "op billing", blurb: "billing conformance: every door metered, 1 request = 1 charge", extra: "--key K --count N" },
|
|
13299
13794
|
{ cmd: "op billing doors", blurb: "every live route + workers.dev URL, and whether it passes the meter (free, read-only)" },
|
|
13300
13795
|
{ cmd: "op billing meter", blurb: "send known traffic, assert the ledger moved by exactly that much", extra: "spends a few tenths of a cent" },
|
|
13796
|
+
{ cmd: "op test", blurb: "run the operator test suite against production (sandbox team only): contract specs + gut checks", extra: "--json --only <ids> --group <g> --skip-guts --guts-only --fail-fast" },
|
|
13797
|
+
{ cmd: "op test --perf", blurb: "performance gate: fixed-rate load on a sandbox proxy, client + gateway latency vs thresholds (alias: op perf)", extra: "--rps 100 --seconds 60 --json --max-p50 --max-p95 --max-error-rate --max-gateway-p50 --max-gateway-p95 --url --key" },
|
|
13798
|
+
{ cmd: "op test --consumer", blurb: "the data plane as a consumer: the stored `consumer login` token opens exactly its own proxy (401 without, 401 tampered)", extra: "--path /get --json" },
|
|
13301
13799
|
{ cmd: "op residue", blurb: "external-store residue report (Upstash + Neon/OpenFGA) \u2014 dry run, deletes nothing" },
|
|
13302
13800
|
{ cmd: "op sweep", blurb: "delete the orphans the residue report found", extra: "asks first; -y to skip" }
|
|
13303
13801
|
];
|
|
13304
13802
|
function renderOpCommands() {
|
|
13305
13803
|
const width = Math.max(...OP_COMMANDS.map((c) => c.cmd.length)) + 10;
|
|
13306
13804
|
const lines = OP_COMMANDS.map((c) => {
|
|
13307
|
-
const left = ` ${
|
|
13805
|
+
const left = ` ${import_chalk61.default.cyan(`apiblaze ${c.cmd}`)}`;
|
|
13308
13806
|
const pad = " ".repeat(Math.max(1, width - c.cmd.length));
|
|
13309
|
-
return `${left}${pad}${c.blurb}${c.extra ? " " +
|
|
13807
|
+
return `${left}${pad}${c.blurb}${c.extra ? " " + import_chalk61.default.dim(`(${c.extra})`) : ""}`;
|
|
13310
13808
|
});
|
|
13311
13809
|
return [
|
|
13312
|
-
|
|
13810
|
+
import_chalk61.default.bold("Operator commands"),
|
|
13313
13811
|
...lines,
|
|
13314
13812
|
"",
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13813
|
+
import_chalk61.default.dim(" Operators only. The gate is server-side (dashboard /api/cli/op checks the"),
|
|
13814
|
+
import_chalk61.default.dim(" signed-in email, admin-api re-checks with operatorGate) \u2014 a patched CLI just"),
|
|
13815
|
+
import_chalk61.default.dim(" gets 403s. Every op call is read-only except `op sweep`."),
|
|
13318
13816
|
"",
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13817
|
+
import_chalk61.default.dim(" Not a CLI command: to prune all non-CP data run scripts/nuke-but-cp.sh --apply --sweep"),
|
|
13818
|
+
import_chalk61.default.dim(" in the repo. Operator dashboards (dlq, thresholds, throttling, pricing, billing,"),
|
|
13819
|
+
import_chalk61.default.dim(" agent-spend, teams, tests, leak-detection, lifecycle) live at /operator/* in the app.")
|
|
13322
13820
|
].join("\n");
|
|
13323
13821
|
}
|
|
13324
13822
|
function printResidue(report, applied) {
|
|
13325
13823
|
const up = report?.upstash ?? {};
|
|
13326
13824
|
const fga = report?.fga ?? {};
|
|
13327
13825
|
const ghosts = report?.ghosts ?? {};
|
|
13328
|
-
console.log(
|
|
13329
|
-
console.log(
|
|
13826
|
+
console.log(import_chalk61.default.bold(applied ? "\nExternal-residue sweep" : "\nExternal residue (dry-run \u2014 nothing deleted)"));
|
|
13827
|
+
console.log(import_chalk61.default.bold("\n Upstash"));
|
|
13330
13828
|
const orphans = up.orphans ?? [];
|
|
13331
|
-
if (orphans.length === 0) console.log(
|
|
13332
|
-
for (const o of orphans) console.log(` ${
|
|
13333
|
-
console.log(
|
|
13829
|
+
if (orphans.length === 0) console.log(import_chalk61.default.green(" no orphaned keys"));
|
|
13830
|
+
for (const o of orphans) console.log(` ${import_chalk61.default.yellow(o.key)} ${import_chalk61.default.dim(`\u2014 ${o.reason}`)}`);
|
|
13831
|
+
console.log(import_chalk61.default.dim(` kept (live principals): ${up.kept ?? 0} \xB7 anon wallets (untouched): ${up.anon_wallets ?? 0}`));
|
|
13334
13832
|
if (up.anon_wallet_detail) {
|
|
13335
13833
|
const d = up.anon_wallet_detail;
|
|
13336
|
-
console.log(
|
|
13834
|
+
console.log(import_chalk61.default.dim(` anon wallets: ${d.count} ($${(d.total_cents / 100).toFixed(2)}), ${d.no_ttl} with NO TTL${d.no_ttl ? " \u26A0" : " (all self-expire)"}`));
|
|
13337
13835
|
}
|
|
13338
13836
|
if (up.keyspace_census) {
|
|
13339
13837
|
const census = Object.entries(up.keyspace_census).map(([k, v]) => `${k}=${v}`).join(" \xB7 ");
|
|
13340
|
-
console.log(
|
|
13838
|
+
console.log(import_chalk61.default.dim(` keyspace: ${census}`));
|
|
13341
13839
|
}
|
|
13342
|
-
if (up.unknown?.length) console.log(
|
|
13343
|
-
if (applied) console.log(` ${
|
|
13344
|
-
for (const e of up.errors ?? []) console.log(
|
|
13345
|
-
console.log(
|
|
13840
|
+
if (up.unknown?.length) console.log(import_chalk61.default.dim(` unknown (never deleted): ${up.unknown.join(", ")}`));
|
|
13841
|
+
if (applied) console.log(` ${import_chalk61.default.bold(String(up.deleted ?? 0))} key(s) deleted`);
|
|
13842
|
+
for (const e of up.errors ?? []) console.log(import_chalk61.default.red(` error: ${e}`));
|
|
13843
|
+
console.log(import_chalk61.default.bold("\n OpenFGA / Neon \u2014 orphan stores"));
|
|
13346
13844
|
if (applied) {
|
|
13347
13845
|
const swept = fga?.swept ?? [];
|
|
13348
|
-
if (swept.length === 0) console.log(
|
|
13846
|
+
if (swept.length === 0) console.log(import_chalk61.default.green(" no orphaned stores"));
|
|
13349
13847
|
for (const s of swept) {
|
|
13350
13848
|
console.log(
|
|
13351
|
-
` ${
|
|
13849
|
+
` ${import_chalk61.default.yellow(s.store_id)} ${import_chalk61.default.dim(`\u2014 store ${s.openfga_deleted ? "deleted" : "DEFERRED"}, ${s.neon_deleted} Neon tuple(s) purged`)}`
|
|
13352
13850
|
);
|
|
13353
13851
|
}
|
|
13354
|
-
if (fga?.remaining) console.log(
|
|
13852
|
+
if (fga?.remaining) console.log(import_chalk61.default.yellow(` ${fga.remaining} more orphan store(s) \u2014 re-run to drain`));
|
|
13355
13853
|
const st = fga?.side_tables;
|
|
13356
|
-
if (st) console.log(
|
|
13854
|
+
if (st) console.log(import_chalk61.default.dim(` Neon side-tables purged: ${st.soft_deleted_stores} store records, ${st.orphan_models} models, ${st.orphan_changelog} changelog rows${st.error ? ` (${st.error})` : ""}`));
|
|
13357
13855
|
} else {
|
|
13358
13856
|
const fgaOrphans = fga?.orphans ?? [];
|
|
13359
|
-
if (fgaOrphans.length === 0) console.log(
|
|
13857
|
+
if (fgaOrphans.length === 0) console.log(import_chalk61.default.green(" no orphaned stores"));
|
|
13360
13858
|
for (const s of fgaOrphans) {
|
|
13361
13859
|
const src = s.in_openfga ? "live in OpenFGA" : "Neon tuples only";
|
|
13362
|
-
console.log(` ${
|
|
13860
|
+
console.log(` ${import_chalk61.default.yellow(s.store_id)} ${import_chalk61.default.dim(`\u2014 ${src}${s.name ? ` (${s.name})` : ""}, ${s.neon_tuples} Neon tuple(s)`)}`);
|
|
13363
13861
|
}
|
|
13364
|
-
console.log(
|
|
13862
|
+
console.log(import_chalk61.default.dim(` kept stores: ${(fga?.kept_store_ids ?? []).length}`));
|
|
13365
13863
|
const st = fga?.side_tables;
|
|
13366
|
-
if (st) console.log(
|
|
13864
|
+
if (st) console.log(import_chalk61.default.dim(` Neon side-table residue: ${st.soft_deleted_stores} soft-deleted store records, ${st.orphan_models} orphan models, ${st.orphan_changelog} orphan changelog rows`));
|
|
13367
13865
|
}
|
|
13368
|
-
for (const e of fga?.errors ?? []) console.log(
|
|
13369
|
-
console.log(
|
|
13866
|
+
for (const e of fga?.errors ?? []) console.log(import_chalk61.default.red(` error: ${e}`));
|
|
13867
|
+
console.log(import_chalk61.default.bold("\n OpenFGA \u2014 ghost tuples in surviving stores"));
|
|
13370
13868
|
if (applied) {
|
|
13371
|
-
if ((ghosts?.ghost_count ?? 0) === 0) console.log(
|
|
13372
|
-
else console.log(` ${
|
|
13869
|
+
if ((ghosts?.ghost_count ?? 0) === 0) console.log(import_chalk61.default.green(" no ghost tuples"));
|
|
13870
|
+
else console.log(` ${import_chalk61.default.bold(String(ghosts.deleted ?? 0))} ghost tuple(s) deleted ${import_chalk61.default.dim(`(of ${ghosts.ghost_count} found, ${ghosts.scanned_tuples} scanned across ${ghosts.live_stores} live stores)`)}`);
|
|
13373
13871
|
} else {
|
|
13374
13872
|
const n = ghosts?.ghost_count ?? 0;
|
|
13375
|
-
if (n === 0) console.log(
|
|
13873
|
+
if (n === 0) console.log(import_chalk61.default.green(` no ghost tuples ${import_chalk61.default.dim(`(${ghosts.scanned_tuples ?? 0} scanned across ${ghosts.live_stores ?? 0} live stores)`)}`));
|
|
13376
13874
|
else {
|
|
13377
|
-
console.log(
|
|
13875
|
+
console.log(import_chalk61.default.yellow(` ${n} ghost tuple(s) referencing entities absent from D1:`));
|
|
13378
13876
|
for (const g of (ghosts.ghosts ?? []).slice(0, 20)) {
|
|
13379
|
-
console.log(
|
|
13877
|
+
console.log(import_chalk61.default.dim(` ${g.object_type}:${g.object_id} ${g.relation} ${g._user}`));
|
|
13380
13878
|
}
|
|
13381
|
-
if (n > 20) console.log(
|
|
13879
|
+
if (n > 20) console.log(import_chalk61.default.dim(` \u2026 and ${n - 20} more`));
|
|
13382
13880
|
}
|
|
13383
13881
|
}
|
|
13384
|
-
for (const e of ghosts?.errors ?? []) console.log(
|
|
13882
|
+
for (const e of ghosts?.errors ?? []) console.log(import_chalk61.default.red(` error: ${e}`));
|
|
13385
13883
|
console.log();
|
|
13386
13884
|
}
|
|
13387
13885
|
async function runOp(sub, opts = {}, view) {
|
|
13388
13886
|
if (!loadCredentials()) {
|
|
13389
|
-
console.log(
|
|
13887
|
+
console.log(import_chalk61.default.dim("Not logged in. Run `apiblaze login`."));
|
|
13390
13888
|
return;
|
|
13391
13889
|
}
|
|
13392
13890
|
if (!isOperatorLogin()) {
|
|
13393
|
-
console.log(
|
|
13891
|
+
console.log(import_chalk61.default.dim("`apiblaze op` is only available to platform operators."));
|
|
13394
13892
|
return;
|
|
13395
13893
|
}
|
|
13396
13894
|
switch (sub) {
|
|
13895
|
+
// `op test --perf` and `op perf` are the same thing: the load gate (plan row
|
|
13896
|
+
// 6) instead of the suite. `--perf` is perf ONLY — the suite is not run first.
|
|
13897
|
+
case "perf": {
|
|
13898
|
+
await runOpPerf(perfOptionsFrom(opts));
|
|
13899
|
+
return;
|
|
13900
|
+
}
|
|
13901
|
+
case "consumer": {
|
|
13902
|
+
await runOpConsumer({ json: !!opts.json, path: typeof opts.path === "string" ? opts.path : void 0, proxy: typeof opts.proxy === "string" ? opts.proxy : void 0, url: typeof opts.url === "string" ? opts.url : void 0 });
|
|
13903
|
+
return;
|
|
13904
|
+
}
|
|
13905
|
+
case "test": {
|
|
13906
|
+
if (opts.consumer) {
|
|
13907
|
+
await runOpConsumer({ json: !!opts.json, path: typeof opts.path === "string" ? opts.path : void 0, proxy: typeof opts.proxy === "string" ? opts.proxy : void 0, url: typeof opts.url === "string" ? opts.url : void 0 });
|
|
13908
|
+
return;
|
|
13909
|
+
}
|
|
13910
|
+
if (opts.perf) {
|
|
13911
|
+
await runOpPerf(perfOptionsFrom(opts));
|
|
13912
|
+
return;
|
|
13913
|
+
}
|
|
13914
|
+
await runOpTest({
|
|
13915
|
+
json: !!opts.json,
|
|
13916
|
+
only: typeof opts.only === "string" ? opts.only : void 0,
|
|
13917
|
+
group: typeof opts.group === "string" ? opts.group : void 0,
|
|
13918
|
+
skipGuts: !!opts.skipGuts,
|
|
13919
|
+
gutsOnly: !!opts.gutsOnly,
|
|
13920
|
+
failFast: !!opts.failFast,
|
|
13921
|
+
timeout: typeof opts.timeout === "string" ? Number(opts.timeout) : void 0
|
|
13922
|
+
});
|
|
13923
|
+
return;
|
|
13924
|
+
}
|
|
13397
13925
|
case void 0:
|
|
13398
13926
|
case "menu": {
|
|
13399
13927
|
console.log("");
|
|
@@ -13416,17 +13944,17 @@ async function runOp(sub, opts = {}, view) {
|
|
|
13416
13944
|
const nSide = (st.soft_deleted_stores ?? 0) + (st.orphan_models ?? 0) + (st.orphan_changelog ?? 0);
|
|
13417
13945
|
printResidue(report, false);
|
|
13418
13946
|
if (nUp + nFga + nGhost + nSide === 0) {
|
|
13419
|
-
console.log(
|
|
13947
|
+
console.log(import_chalk61.default.green("Nothing to sweep."));
|
|
13420
13948
|
return;
|
|
13421
13949
|
}
|
|
13422
13950
|
if (!opts.yes) {
|
|
13423
13951
|
const readline3 = await import("readline/promises");
|
|
13424
13952
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
13425
13953
|
const answer = await rl.question(
|
|
13426
|
-
|
|
13954
|
+
import_chalk61.default.red(`Delete ${nUp} Upstash key(s) + ${nFga} OpenFGA store(s) + ${nGhost} ghost tuple(s) + ${nSide} Neon side-table row(s)? Type 'sweep' to confirm: `)
|
|
13427
13955
|
);
|
|
13428
13956
|
rl.close();
|
|
13429
|
-
if (answer.trim() !== "sweep") return void console.log(
|
|
13957
|
+
if (answer.trim() !== "sweep") return void console.log(import_chalk61.default.dim("Aborted."));
|
|
13430
13958
|
}
|
|
13431
13959
|
const result = await opCall({ method: "POST", path: "/operator/external-residue/sweep", summary: "external residue sweep" });
|
|
13432
13960
|
if (opts.json) return void console.log(JSON.stringify(result, null, 2));
|
|
@@ -13436,25 +13964,25 @@ async function runOp(sub, opts = {}, view) {
|
|
|
13436
13964
|
case "mark": {
|
|
13437
13965
|
const label3 = (view ?? "").trim();
|
|
13438
13966
|
if (!label3) {
|
|
13439
|
-
console.log(
|
|
13967
|
+
console.log(import_chalk61.default.red("Give the change a name:") + import_chalk61.default.cyan(' apiblaze op mark "cached tenant count"'));
|
|
13440
13968
|
return;
|
|
13441
13969
|
}
|
|
13442
13970
|
const res = await opCall({ method: "POST", path: "/operator/latency/mark", body: { label: label3 }, summary: "record change marker" });
|
|
13443
13971
|
const ts = new Date(res?.marker?.ts ?? Date.now()).toISOString();
|
|
13444
|
-
console.log(
|
|
13445
|
-
Marked: `) +
|
|
13446
|
-
console.log(
|
|
13447
|
-
console.log(
|
|
13972
|
+
console.log(import_chalk61.default.green(`
|
|
13973
|
+
Marked: `) + import_chalk61.default.bold(label3));
|
|
13974
|
+
console.log(import_chalk61.default.dim(` ${ts}`));
|
|
13975
|
+
console.log(import_chalk61.default.dim(` Once traffic has run on both sides, compare with: `) + import_chalk61.default.cyan("apiblaze op latency compare") + "\n");
|
|
13448
13976
|
return;
|
|
13449
13977
|
}
|
|
13450
13978
|
case "credits": {
|
|
13451
13979
|
const data = await opCall({ method: "GET", path: "/operator/credits", summary: "list credit wallets" });
|
|
13452
13980
|
if (opts.json) return void console.log(JSON.stringify(data, null, 2));
|
|
13453
13981
|
const accounts = data?.accounts ?? [];
|
|
13454
|
-
if (accounts.length === 0) return void console.log(
|
|
13982
|
+
if (accounts.length === 0) return void console.log(import_chalk61.default.dim("No credit wallets."));
|
|
13455
13983
|
for (const a of accounts) {
|
|
13456
13984
|
const bal = typeof a.balance_cents === "number" ? `$${(a.balance_cents / 100).toFixed(2)}` : "?";
|
|
13457
|
-
console.log(` ${
|
|
13985
|
+
console.log(` ${import_chalk61.default.bold(bal.padStart(9))} ${a.walletId}${a.owner_email ? import_chalk61.default.dim(` \u2014 ${a.owner_email}`) : a.anon ? import_chalk61.default.dim(" \u2014 anon") : ""}`);
|
|
13458
13986
|
}
|
|
13459
13987
|
return;
|
|
13460
13988
|
}
|
|
@@ -13465,7 +13993,7 @@ async function runOp(sub, opts = {}, view) {
|
|
|
13465
13993
|
case "billing": {
|
|
13466
13994
|
const which = (view ?? "").trim().toLowerCase();
|
|
13467
13995
|
if (which && which !== "doors" && which !== "meter") {
|
|
13468
|
-
return void console.log(
|
|
13996
|
+
return void console.log(import_chalk61.default.red(`Unknown: apiblaze op billing ${which}. Use 'doors', 'meter', or neither for both.`));
|
|
13469
13997
|
}
|
|
13470
13998
|
const checks = [];
|
|
13471
13999
|
let doorsData = null;
|
|
@@ -13534,29 +14062,29 @@ async function runOp(sub, opts = {}, view) {
|
|
|
13534
14062
|
const data = await opCall({ method: "GET", path: `/operator/latency/grades${q}`, summary: "latency grades" });
|
|
13535
14063
|
if (opts.json) return void console.log(JSON.stringify(data, null, 2));
|
|
13536
14064
|
const t = data.thresholds_ms;
|
|
13537
|
-
console.log(
|
|
13538
|
-
console.log(
|
|
14065
|
+
console.log(import_chalk61.default.bold("\nHow good was apiblaze itself?") + import_chalk61.default.dim(" (our overhead only \u2014 a slow customer API never counts against us)"));
|
|
14066
|
+
console.log(import_chalk61.default.dim(` excellent <${t.excellent.replace("<", "")}ms \xB7 okay ${t.okay}ms \xB7 bad ${t.bad}ms \xB7 terrible ${t.terrible.replace(">=", "")}ms+
|
|
13539
14067
|
`));
|
|
13540
|
-
console.log(
|
|
14068
|
+
console.log(import_chalk61.default.dim(" date reqs excellent okay bad terrible"));
|
|
13541
14069
|
for (const d of data.days ?? []) {
|
|
13542
14070
|
const p = d.pct;
|
|
13543
|
-
const cell = (v, colour) => v > 0 ? colour(`${String(v).padStart(5)}%`) :
|
|
14071
|
+
const cell = (v, colour) => v > 0 ? colour(`${String(v).padStart(5)}%`) : import_chalk61.default.dim(`${String(v).padStart(5)}%`);
|
|
13544
14072
|
console.log(
|
|
13545
|
-
` ${d.date} ${String(d.total).padStart(5)} ${cell(p.excellent,
|
|
14073
|
+
` ${d.date} ${String(d.total).padStart(5)} ${cell(p.excellent, import_chalk61.default.green)} ${cell(p.okay, import_chalk61.default.cyan)} ${cell(p.bad, import_chalk61.default.yellow)} ${cell(p.terrible, import_chalk61.default.red)}`
|
|
13546
14074
|
);
|
|
13547
14075
|
}
|
|
13548
14076
|
const cul = data.culprits ?? [];
|
|
13549
14077
|
if (cul.length) {
|
|
13550
|
-
console.log(
|
|
13551
|
-
console.log(
|
|
14078
|
+
console.log(import_chalk61.default.bold("\n Who caused the bad and terrible ones\n"));
|
|
14079
|
+
console.log(import_chalk61.default.dim(" bad terrible feature \u2192 dependency"));
|
|
13552
14080
|
for (const r of cul.slice(0, 12)) {
|
|
13553
14081
|
if (!r.bad && !r.terrible) continue;
|
|
13554
14082
|
console.log(
|
|
13555
|
-
` ${String(r.bad).padStart(6)} ${
|
|
14083
|
+
` ${String(r.bad).padStart(6)} ${import_chalk61.default.red(String(r.terrible).padStart(8))} ${import_chalk61.default.yellow(r.feature)} ${import_chalk61.default.dim("\u2192")} ${import_chalk61.default.cyan(r.dep)}`
|
|
13556
14084
|
);
|
|
13557
14085
|
}
|
|
13558
14086
|
}
|
|
13559
|
-
if (data.caveat) console.log(
|
|
14087
|
+
if (data.caveat) console.log(import_chalk61.default.dim(`
|
|
13560
14088
|
\u26A0 ${data.caveat}
|
|
13561
14089
|
`));
|
|
13562
14090
|
return;
|
|
@@ -13565,24 +14093,24 @@ async function runOp(sub, opts = {}, view) {
|
|
|
13565
14093
|
const data = await opCall({ method: "GET", path: `/operator/latency/compare${q}`, summary: "latency before/after" });
|
|
13566
14094
|
if (opts.json) return void console.log(JSON.stringify(data, null, 2));
|
|
13567
14095
|
const b = data.before, a = data.after, d = data.delta;
|
|
13568
|
-
console.log(
|
|
13569
|
-
Before vs after: `) +
|
|
13570
|
-
console.log(
|
|
14096
|
+
console.log(import_chalk61.default.bold(`
|
|
14097
|
+
Before vs after: `) + import_chalk61.default.cyan(data.marker.label));
|
|
14098
|
+
console.log(import_chalk61.default.dim(` marked ${new Date(data.marker.ts).toISOString()} \xB7 ${data.window_hours}h either side
|
|
13571
14099
|
`));
|
|
13572
14100
|
const row = (name, before, after, delta) => {
|
|
13573
14101
|
const arrow = delta === 0 ? "=" : delta < 0 ? "\u2193" : "\u2191";
|
|
13574
14102
|
const txt = `${String(before).padStart(6)}ms \u2192${String(after).padStart(7)}ms ${arrow}${Math.abs(delta)}ms`;
|
|
13575
|
-
console.log(` ${name.padEnd(22)}${data.trustworthy ? delta <= 0 ?
|
|
14103
|
+
console.log(` ${name.padEnd(22)}${data.trustworthy ? delta <= 0 ? import_chalk61.default.green(txt) : import_chalk61.default.red(txt) : import_chalk61.default.dim(txt)}`);
|
|
13576
14104
|
};
|
|
13577
|
-
console.log(
|
|
14105
|
+
console.log(import_chalk61.default.dim(" metric before after change"));
|
|
13578
14106
|
row("total p50", b.total_p50, a.total_p50, d.total_p50);
|
|
13579
14107
|
row("total p95", b.total_p95, a.total_p95, d.total_p95);
|
|
13580
14108
|
row("apiblaze overhead p50", b.gw_p50, a.gw_p50, d.gw_p50);
|
|
13581
14109
|
row("apiblaze overhead p95", b.gw_p95, a.gw_p95, d.gw_p95);
|
|
13582
|
-
console.log(
|
|
14110
|
+
console.log(import_chalk61.default.dim(`
|
|
13583
14111
|
requests: ${b.requests} before \xB7 ${a.requests} after`));
|
|
13584
14112
|
for (const w of data.warnings ?? []) {
|
|
13585
|
-
console.log((data.trustworthy ?
|
|
14113
|
+
console.log((data.trustworthy ? import_chalk61.default.dim : import_chalk61.default.yellow)(` ${data.trustworthy ? "\xB7" : "\u26A0"} ${w}`));
|
|
13586
14114
|
}
|
|
13587
14115
|
console.log("");
|
|
13588
14116
|
return;
|
|
@@ -13591,19 +14119,19 @@ Before vs after: `) + import_chalk58.default.cyan(data.marker.label));
|
|
|
13591
14119
|
const data = await opCall({ method: "GET", path: `/operator/latency/slow${q}`, summary: "slowest requests" });
|
|
13592
14120
|
if (opts.json) return void console.log(JSON.stringify(data, null, 2));
|
|
13593
14121
|
const rows2 = data?.rows ?? [];
|
|
13594
|
-
if (!rows2.length) return void console.log(
|
|
13595
|
-
console.log(
|
|
14122
|
+
if (!rows2.length) return void console.log(import_chalk61.default.dim("No requests over the threshold in that window."));
|
|
14123
|
+
console.log(import_chalk61.default.bold(`
|
|
13596
14124
|
Slowest requests \u2014 last ${data.window_hours}h, over ${data.min_ms}ms
|
|
13597
14125
|
`));
|
|
13598
|
-
console.log(
|
|
14126
|
+
console.log(import_chalk61.default.dim(" total ours theirs blame request id"));
|
|
13599
14127
|
for (const r of rows2.slice(0, 30)) {
|
|
13600
14128
|
console.log(
|
|
13601
|
-
` ${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)} ${
|
|
14129
|
+
` ${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_chalk61.default.yellow(`${r.slow_gw_feature || "-"}\u2192${r.slow_dep || "-"}`.padEnd(20))} ${import_chalk61.default.dim(r.request_id || "")}`
|
|
13602
14130
|
);
|
|
13603
14131
|
}
|
|
13604
|
-
console.log(
|
|
14132
|
+
console.log(import_chalk61.default.dim(`
|
|
13605
14133
|
The last column is the request id (Cloudflare calls it a "cf-ray"). Look one up with`));
|
|
13606
|
-
console.log(
|
|
14134
|
+
console.log(import_chalk61.default.dim(` \`apiblaze logs\` for that request's exact per-feature breakdown \u2014 unsampled, unlike the table above.
|
|
13607
14135
|
`));
|
|
13608
14136
|
return;
|
|
13609
14137
|
}
|
|
@@ -13611,18 +14139,18 @@ Slowest requests \u2014 last ${data.window_hours}h, over ${data.min_ms}ms
|
|
|
13611
14139
|
const data = await opCall({ method: "GET", path: `/operator/latency/llm${q}`, summary: "llm latency" });
|
|
13612
14140
|
if (opts.json) return void console.log(JSON.stringify(data, null, 2));
|
|
13613
14141
|
const rows2 = data?.rows ?? [];
|
|
13614
|
-
if (!rows2.length) return void console.log(
|
|
13615
|
-
console.log(
|
|
14142
|
+
if (!rows2.length) return void console.log(import_chalk61.default.dim("No LLM traffic in that window."));
|
|
14143
|
+
console.log(import_chalk61.default.bold(`
|
|
13616
14144
|
LLM timing \u2014 last ${data.window_hours}h
|
|
13617
14145
|
`));
|
|
13618
|
-
console.log(
|
|
14146
|
+
console.log(import_chalk61.default.dim(" turns turn p95 ttfc p95 gen p95 reserve p95 in/out tokens p95 model"));
|
|
13619
14147
|
for (const r of rows2) {
|
|
13620
14148
|
const n = r.turns ?? r.requests ?? 0;
|
|
13621
14149
|
console.log(
|
|
13622
14150
|
` ${String(Math.round(n)).padStart(8)} ${String(Math.round(r.turn_p95_ms ?? 0)).padStart(6)}ms ${String(Math.round(r.ttfc_p95_ms ?? 0)).padStart(6)}ms ${String(Math.round(r.gen_p95_ms ?? 0)).padStart(6)}ms ${String(Math.round(r.reserve_p95_ms ?? 0)).padStart(9)}ms ${String(Math.round(r.input_tokens_p95 ?? 0)).padStart(6)}/${String(Math.round(r.output_tokens_p95 ?? 0)).padEnd(6)} ${r.model || "-"}`
|
|
13623
14151
|
);
|
|
13624
14152
|
}
|
|
13625
|
-
console.log(
|
|
14153
|
+
console.log(import_chalk61.default.dim(`
|
|
13626
14154
|
${data.note}
|
|
13627
14155
|
`));
|
|
13628
14156
|
return;
|
|
@@ -13633,26 +14161,26 @@ LLM timing \u2014 last ${data.window_hours}h
|
|
|
13633
14161
|
]);
|
|
13634
14162
|
if (opts.json) return void console.log(JSON.stringify({ blame, summary }, null, 2));
|
|
13635
14163
|
const ov = summary?.apiblaze_overhead_ms ?? {};
|
|
13636
|
-
console.log(
|
|
14164
|
+
console.log(import_chalk61.default.bold(`
|
|
13637
14165
|
Latency \u2014 last ${summary?.window_hours ?? "?"}h, ${Number(summary?.requests ?? 0).toLocaleString()} requests
|
|
13638
14166
|
`));
|
|
13639
|
-
console.log(` ${
|
|
13640
|
-
console.log(` ${
|
|
13641
|
-
console.log(
|
|
14167
|
+
console.log(` ${import_chalk61.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_chalk61.default.dim("\u2190 ours")}`);
|
|
14168
|
+
console.log(` ${import_chalk61.default.bold("upstream ttfb ")} ${" ".repeat(24)}p95 ${String(summary?.upstream_ttfb_p95_ms ?? 0).padStart(6)}ms ${import_chalk61.default.dim("\u2190 theirs")}`);
|
|
14169
|
+
console.log(import_chalk61.default.dim(` (per-request percentiles \u2014 never subtract one from the other)
|
|
13642
14170
|
`));
|
|
13643
14171
|
const rows = blame?.blame ?? [];
|
|
13644
|
-
if (!rows.length) return void console.log(
|
|
13645
|
-
console.log(
|
|
13646
|
-
console.log(
|
|
14172
|
+
if (!rows.length) return void console.log(import_chalk61.default.dim("No latency rows in that window."));
|
|
14173
|
+
console.log(import_chalk61.default.bold(" Which feature ate the time, and what inside it\n"));
|
|
14174
|
+
console.log(import_chalk61.default.dim(" share p95 feature \u2192 dependency"));
|
|
13647
14175
|
for (const r of rows.slice(0, 15)) {
|
|
13648
14176
|
const share = `${(r.share * 100).toFixed(1)}%`;
|
|
13649
|
-
console.log(` ${share.padStart(6)} ${String(r.p95_ms).padStart(6)}ms ${
|
|
14177
|
+
console.log(` ${share.padStart(6)} ${String(r.p95_ms).padStart(6)}ms ${import_chalk61.default.yellow(r.feature)} ${import_chalk61.default.dim("\u2192")} ${import_chalk61.default.cyan(r.dep)}`);
|
|
13650
14178
|
}
|
|
13651
14179
|
console.log("");
|
|
13652
14180
|
return;
|
|
13653
14181
|
}
|
|
13654
14182
|
default:
|
|
13655
|
-
console.log(
|
|
14183
|
+
console.log(import_chalk61.default.red(`Unknown op subcommand '${sub}'. Run \`apiblaze op\` for the menu.`));
|
|
13656
14184
|
}
|
|
13657
14185
|
}
|
|
13658
14186
|
|
|
@@ -13722,7 +14250,7 @@ program.command("dev").description(`Put the code running on this machine behind
|
|
|
13722
14250
|
try {
|
|
13723
14251
|
const resolved = parseInt(port ?? opts.port, 10);
|
|
13724
14252
|
if (Number.isNaN(resolved)) {
|
|
13725
|
-
console.error(
|
|
14253
|
+
console.error(import_chalk62.default.red(`Invalid port: ${port ?? opts.port}`));
|
|
13726
14254
|
process.exit(1);
|
|
13727
14255
|
}
|
|
13728
14256
|
await runDev({ ...opts, port: resolved });
|
|
@@ -13826,7 +14354,7 @@ apikeys.command("list").description("List control-plane API keys in your team").
|
|
|
13826
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 })));
|
|
13827
14355
|
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 })));
|
|
13828
14356
|
program.addCommand(apikeys, { hidden: true });
|
|
13829
|
-
var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "latency | billing | mark | credits | residue | sweep (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("--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)").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", () => `
|
|
14357
|
+
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", () => `
|
|
13830
14358
|
${renderOpCommands()}
|
|
13831
14359
|
`);
|
|
13832
14360
|
program.addCommand(op, { hidden: true });
|
|
@@ -13865,7 +14393,7 @@ function groupedCommandHelp() {
|
|
|
13865
14393
|
const sub = byName.get(e.parent)?.commands.find((s) => s.name() === e.sub);
|
|
13866
14394
|
return sub ? ` ${helpLabel(e).padEnd(width)}${sub.description()}` : "";
|
|
13867
14395
|
}).filter(Boolean).join("\n");
|
|
13868
|
-
return `${
|
|
14396
|
+
return `${import_chalk62.default.bold(g.title)}
|
|
13869
14397
|
${rows}`;
|
|
13870
14398
|
}).join("\n\n");
|
|
13871
14399
|
}
|
|
@@ -13876,7 +14404,7 @@ program.configureHelp({
|
|
|
13876
14404
|
}
|
|
13877
14405
|
});
|
|
13878
14406
|
program.addHelpText("after", () => `
|
|
13879
|
-
${
|
|
14407
|
+
${import_chalk62.default.bold("Don't like reading docs?")} Run ${import_chalk62.default.bgCyan.black.bold(` npx ${B.cli} docs `)} ${import_chalk62.default.cyan(`\u2014 ask an AI chatbot about ${B.product}, and let it act on your account.`)}
|
|
13880
14408
|
|
|
13881
14409
|
${groupedCommandHelp()}
|
|
13882
14410
|
|
|
@@ -13912,14 +14440,14 @@ async function recoverStaleTeam() {
|
|
|
13912
14440
|
const { resolveLinkedTeam: resolveLinkedTeam2 } = await Promise.resolve().then(() => (init_team(), team_exports));
|
|
13913
14441
|
const linked = await resolveLinkedTeam2({ preferredId: creds.teamId, interactive: !!process.stdin.isTTY });
|
|
13914
14442
|
if (!linked) {
|
|
13915
|
-
console.error(
|
|
14443
|
+
console.error(import_chalk62.default.yellow("Your account has no teams anymore (deleted?). Run `apiblaze login` or `apiblaze create` to get a workspace."));
|
|
13916
14444
|
return;
|
|
13917
14445
|
}
|
|
13918
14446
|
if (linked.teamId === creds.teamId) return;
|
|
13919
14447
|
const next = { ...creds, teamId: linked.teamId, teamName: linked.teamName };
|
|
13920
14448
|
delete next.activeTenant;
|
|
13921
14449
|
saveCredentials(next);
|
|
13922
|
-
console.error(
|
|
14450
|
+
console.error(import_chalk62.default.yellow(`Your previous team no longer exists \u2014 relinked to ${import_chalk62.default.bold(linked.teamName ?? linked.teamId)}. Re-run your command.`));
|
|
13923
14451
|
} catch {
|
|
13924
14452
|
}
|
|
13925
14453
|
}
|
|
@@ -13927,16 +14455,16 @@ async function printError(err) {
|
|
|
13927
14455
|
if (err instanceof ApiError) {
|
|
13928
14456
|
const data = err.body;
|
|
13929
14457
|
const extra = [data?.body?.reason, data?.body?.details, data?.details, data?.body?.error].find((x) => typeof x === "string" && x && x !== err.message);
|
|
13930
|
-
console.error(
|
|
14458
|
+
console.error(import_chalk62.default.red(`
|
|
13931
14459
|
API error (${err.status}): ${err.message}${extra ? ` \u2014 ${extra}` : ""}`));
|
|
13932
14460
|
if (err.status === 403 || err.status === 404) {
|
|
13933
14461
|
await recoverStaleTeam();
|
|
13934
14462
|
}
|
|
13935
14463
|
} else if (err instanceof Error) {
|
|
13936
|
-
console.error(
|
|
14464
|
+
console.error(import_chalk62.default.red(`
|
|
13937
14465
|
Error: ${err.message}`));
|
|
13938
14466
|
} else {
|
|
13939
|
-
console.error(
|
|
14467
|
+
console.error(import_chalk62.default.red("\nUnknown error"));
|
|
13940
14468
|
}
|
|
13941
14469
|
}
|
|
13942
14470
|
program.parse(process.argv);
|