apiblaze 0.21.18 → 0.21.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +214 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ var init_types = __esm({
53
53
  // src/lib/auth.ts
54
54
  var auth_exports = {};
55
55
  __export(auth_exports, {
56
+ LocalAuthError: () => LocalAuthError,
56
57
  clearCredentials: () => clearCredentials,
57
58
  getAccessToken: () => getAccessToken,
58
59
  getApiblazeDir: () => getApiblazeDir,
@@ -85,17 +86,17 @@ function getAccessToken() {
85
86
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
86
87
  const creds = loadCredentials();
87
88
  if (!creds) {
88
- throw new Error("Not authenticated. Run `apiblaze login` first.");
89
+ throw new LocalAuthError("not_authenticated", "Not authenticated. Run `apiblaze login` first.");
89
90
  }
90
91
  if (Date.now() >= creds.expiresAt) {
91
- throw new Error("Session expired. Run `apiblaze login` to re-authenticate.");
92
+ throw new LocalAuthError("session_expired", "Session expired. Run `apiblaze login` to re-authenticate.");
92
93
  }
93
94
  return creds.accessToken;
94
95
  }
95
96
  function getApiblazeDir() {
96
97
  return APIBLAZE_DIR;
97
98
  }
98
- var fs2, os2, path2, APIBLAZE_DIR, CREDENTIALS_PATH;
99
+ var fs2, os2, path2, APIBLAZE_DIR, CREDENTIALS_PATH, LocalAuthError;
99
100
  var init_auth = __esm({
100
101
  "src/lib/auth.ts"() {
101
102
  "use strict";
@@ -104,6 +105,13 @@ var init_auth = __esm({
104
105
  path2 = __toESM(require("path"));
105
106
  APIBLAZE_DIR = path2.join(os2.homedir(), ".apiblaze");
106
107
  CREDENTIALS_PATH = path2.join(APIBLAZE_DIR, "credentials.json");
108
+ LocalAuthError = class extends Error {
109
+ constructor(code, message) {
110
+ super(message);
111
+ this.code = code;
112
+ this.name = "LocalAuthError";
113
+ }
114
+ };
107
115
  }
108
116
  });
109
117
 
@@ -1352,7 +1360,7 @@ var import_commander = require("commander");
1352
1360
  var import_chalk68 = __toESM(require("chalk"));
1353
1361
 
1354
1362
  // package.json
1355
- var version = "0.21.18";
1363
+ var version = "0.21.19";
1356
1364
 
1357
1365
  // src/lib/version-guard.ts
1358
1366
  var import_fs = __toESM(require("fs"));
@@ -1384,33 +1392,77 @@ function cliRefusal(policy, headerVersion) {
1384
1392
 
1385
1393
  // src/lib/version-guard.ts
1386
1394
  var DASHBOARD_BASE = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
1387
- var CACHE_FILE = import_path.default.join(import_os.default.homedir(), ".apiblaze", "cli-policy.json");
1395
+ function cacheFile() {
1396
+ return import_path.default.join(import_os.default.homedir(), ".apiblaze", "cli-policy.json");
1397
+ }
1388
1398
  var CACHE_MS = 10 * 6e4;
1389
1399
  var UPGRADE_EXIT_CODE = 3;
1390
- var OWN_SUFFIXES = ["apiblaze.com", "abz.run", "tryabz.run", "mcpblaze.com"];
1391
- function isApiblazeHost(url) {
1392
- let host;
1400
+ var CONTROL_PLANE_HOSTS = /* @__PURE__ */ new Set([
1401
+ "dashboard.apiblaze.com",
1402
+ "auth.apiblaze.com",
1403
+ "apikeys.apiblaze.com",
1404
+ "api.apiblaze.com",
1405
+ "mcp.apiblaze.com",
1406
+ "mcp.mcpblaze.com",
1407
+ "tunnel.apiblaze.com"
1408
+ // the tunnel worker (dashboard TUNNEL_CONNECT_URL → lib/tunnel-client.ts)
1409
+ ]);
1410
+ var CONTROL_PLANE_ENV = ["APIBLAZE_DASHBOARD_BASE", "APIBLAZE_AUTH_BASE", "APIBLAZE_APIKEYS_BASE", "APIBLAZE_API_BASE"];
1411
+ function isControlPlaneHost(url) {
1412
+ let parsed;
1393
1413
  try {
1394
- host = new URL(url).hostname.toLowerCase();
1414
+ parsed = new URL(url);
1395
1415
  } catch {
1396
1416
  return false;
1397
1417
  }
1398
- if (OWN_SUFFIXES.some((s) => host === s || host.endsWith(`.${s}`))) return true;
1399
- for (const v of [process.env.APIBLAZE_DASHBOARD_BASE, process.env.APIBLAZE_AUTH_BASE, process.env.APIBLAZE_APIKEYS_BASE]) {
1418
+ if (CONTROL_PLANE_HOSTS.has(parsed.hostname.toLowerCase())) return true;
1419
+ for (const name of CONTROL_PLANE_ENV) {
1420
+ const v = process.env[name];
1400
1421
  try {
1401
- if (v && new URL(v).hostname.toLowerCase() === host) return true;
1422
+ if (v && new URL(v).host.toLowerCase() === parsed.host.toLowerCase()) return true;
1402
1423
  } catch {
1403
1424
  }
1404
1425
  }
1405
1426
  return false;
1406
1427
  }
1407
- function exitForUpgrade(version2, min, message) {
1428
+ var CliUpgradeRequired = class extends Error {
1429
+ constructor(version2, minVersion, serverMessage) {
1430
+ super(`This APIblaze CLI (v${version2}) is no longer supported \u2014 the minimum is v${minVersion}.`);
1431
+ this.version = version2;
1432
+ this.minVersion = minVersion;
1433
+ this.serverMessage = serverMessage;
1434
+ this.code = "cli_upgrade_required";
1435
+ this.name = "CliUpgradeRequired";
1436
+ }
1437
+ };
1438
+ function printUpgradeMessage(err) {
1408
1439
  console.error("");
1409
- console.error(import_chalk.default.red(` This APIblaze CLI (v${version2}) is no longer supported \u2014 the minimum is v${min}.`));
1410
- if (message) console.error(import_chalk.default.yellow(` ${message}`));
1440
+ console.error(import_chalk.default.red(` ${err.message}`));
1441
+ if (err.serverMessage) console.error(import_chalk.default.yellow(` ${err.serverMessage}`));
1411
1442
  console.error(import_chalk.default.cyan(" Upgrade: npm i -g apiblaze@latest (or run: npx apiblaze@latest \u2026)"));
1443
+ console.error(import_chalk.default.dim(" Operators: `apiblaze op \u2026` is never blocked; APIBLAZE_SKIP_VERSION_CHECK=1 skips this local check."));
1412
1444
  console.error("");
1413
- process.exit(UPGRADE_EXIT_CODE);
1445
+ }
1446
+ var shutdownHook = null;
1447
+ var handling = null;
1448
+ function setUpgradeShutdown(hook) {
1449
+ shutdownHook = hook;
1450
+ }
1451
+ function handleUpgradeRequired(err) {
1452
+ if (handling) return handling;
1453
+ handling = (async () => {
1454
+ printUpgradeMessage(err);
1455
+ const hook = shutdownHook;
1456
+ shutdownHook = null;
1457
+ if (hook) {
1458
+ try {
1459
+ await hook(UPGRADE_EXIT_CODE);
1460
+ } catch {
1461
+ }
1462
+ }
1463
+ process.exit(UPGRADE_EXIT_CODE);
1464
+ })();
1465
+ return handling;
1414
1466
  }
1415
1467
  var installed = false;
1416
1468
  function installFetchGuard(version2) {
@@ -1419,7 +1471,7 @@ function installFetchGuard(version2) {
1419
1471
  const base2 = globalThis.fetch.bind(globalThis);
1420
1472
  globalThis.fetch = (async (input, init) => {
1421
1473
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1422
- if (!isApiblazeHost(url)) return base2(input, init);
1474
+ if (!isControlPlaneHost(url)) return base2(input, init);
1423
1475
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
1424
1476
  headers.set(CLI_VERSION_HEADER, version2);
1425
1477
  if (!headers.has("user-agent")) headers.set("user-agent", `apiblaze-cli/${version2}`);
@@ -1430,49 +1482,75 @@ function installFetchGuard(version2) {
1430
1482
  body = await res.clone().json();
1431
1483
  } catch {
1432
1484
  }
1433
- if (body?.error === "cli_upgrade_required") exitForUpgrade(version2, body.min_version ?? "?", body.message ?? null);
1485
+ if (body?.error === "cli_upgrade_required") {
1486
+ const err = new CliUpgradeRequired(
1487
+ version2,
1488
+ typeof body.min_version === "string" ? body.min_version : "?",
1489
+ typeof body.message === "string" ? body.message : null
1490
+ );
1491
+ if (shutdownHook) void handleUpgradeRequired(err);
1492
+ throw err;
1493
+ }
1434
1494
  }
1435
1495
  return res;
1436
1496
  });
1437
1497
  }
1498
+ function normalizePolicy(raw) {
1499
+ const r = raw && typeof raw === "object" ? raw : {};
1500
+ return {
1501
+ ...DEFAULT_CLI_POLICY,
1502
+ min_version: isVersion(r.min_version) ? r.min_version : DEFAULT_CLI_POLICY.min_version,
1503
+ reject_unversioned: r.reject_unversioned === true,
1504
+ message: typeof r.message === "string" && r.message.trim() ? r.message : null,
1505
+ ...typeof r.updated_at === "string" ? { updated_at: r.updated_at } : {},
1506
+ ...typeof r.updated_by === "string" ? { updated_by: r.updated_by } : {}
1507
+ };
1508
+ }
1438
1509
  async function fetchPolicy() {
1439
1510
  try {
1440
1511
  const res = await fetch(`${DASHBOARD_BASE}/api/cli/policy`, { signal: AbortSignal.timeout(1500) });
1441
1512
  if (!res.ok) return null;
1442
- return { ...DEFAULT_CLI_POLICY, ...await res.json() };
1513
+ return normalizePolicy(await res.json());
1443
1514
  } catch {
1444
1515
  return null;
1445
1516
  }
1446
1517
  }
1447
1518
  function readCache() {
1448
1519
  try {
1449
- return JSON.parse(import_fs.default.readFileSync(CACHE_FILE, "utf8"));
1520
+ const raw = JSON.parse(import_fs.default.readFileSync(cacheFile(), "utf8"));
1521
+ if (!raw || typeof raw !== "object" || typeof raw.at !== "number" || !Number.isFinite(raw.at)) return null;
1522
+ return { at: raw.at, policy: raw.policy === null ? null : normalizePolicy(raw.policy) };
1450
1523
  } catch {
1451
1524
  return null;
1452
1525
  }
1453
1526
  }
1454
1527
  function writeCache(policy) {
1455
1528
  try {
1456
- import_fs.default.mkdirSync(import_path.default.dirname(CACHE_FILE), { recursive: true });
1457
- import_fs.default.writeFileSync(CACHE_FILE, JSON.stringify({ at: Date.now(), policy }));
1529
+ import_fs.default.mkdirSync(import_path.default.dirname(cacheFile()), { recursive: true });
1530
+ import_fs.default.writeFileSync(cacheFile(), JSON.stringify({ at: Date.now(), policy }));
1458
1531
  } catch {
1459
1532
  }
1460
1533
  }
1461
1534
  async function checkCliPolicy(version2, opts = {}) {
1462
1535
  if (process.env.APIBLAZE_SKIP_VERSION_CHECK === "1") return;
1463
1536
  const cached = opts.fresh ? null : readCache();
1464
- let policy = cached && Date.now() - cached.at < CACHE_MS ? cached.policy : null;
1465
- if (!policy) {
1537
+ let policy;
1538
+ if (cached && Date.now() - cached.at >= 0 && Date.now() - cached.at < CACHE_MS) {
1539
+ policy = cached.policy;
1540
+ } else {
1466
1541
  policy = await fetchPolicy();
1467
- if (policy) writeCache(policy);
1542
+ writeCache(policy);
1468
1543
  }
1469
- if (policy && cliRefusal(policy, version2)) exitForUpgrade(version2, policy.min_version, policy.message);
1544
+ if (policy && cliRefusal(policy, version2)) throw new CliUpgradeRequired(version2, policy.min_version, policy.message);
1470
1545
  }
1471
1546
  function watchCliPolicy(version2) {
1472
1547
  const t = setInterval(() => {
1473
- void checkCliPolicy(version2, { fresh: true });
1548
+ checkCliPolicy(version2, { fresh: true }).catch((err) => {
1549
+ if (err instanceof CliUpgradeRequired) void handleUpgradeRequired(err);
1550
+ });
1474
1551
  }, 10 * 6e4);
1475
1552
  t.unref?.();
1553
+ return () => clearInterval(t);
1476
1554
  }
1477
1555
 
1478
1556
  // src/index.ts
@@ -2109,6 +2187,20 @@ var PING_INTERVAL_MS = 3e4;
2109
2187
  var SILENCE_LIMIT_MS = 75e3;
2110
2188
  var MAX_RECONNECT_DELAY_MS = 15e3;
2111
2189
  var TOKEN_REFRESH_AFTER_MS = 24e4;
2190
+ var MAX_REFRESH_RETRY_DELAY_MS = 6e4;
2191
+ var DEFINITIVE_CODES = /* @__PURE__ */ new Set(["key_rejected", "team_not_covered", "token_expired", "invalid_token", "project_not_found"]);
2192
+ var REFUSAL_MIN_REPEATS = 2;
2193
+ var REFUSAL_MIN_SPAN_MS = 6e4;
2194
+ function definitiveRefusal(err, projectId) {
2195
+ const body = err?.body;
2196
+ const code = typeof body?.error === "string" ? body.error : null;
2197
+ if (!code || !DEFINITIVE_CODES.has(code)) return null;
2198
+ if (code === "project_not_found") {
2199
+ const named = body?.project === projectId || Array.isArray(body?.projects) && body.projects.includes(projectId);
2200
+ return named ? code : null;
2201
+ }
2202
+ return code;
2203
+ }
2112
2204
  var OFFLINE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EHOSTUNREACH"]);
2113
2205
  var STRIP_HEADERS = /* @__PURE__ */ new Set([
2114
2206
  "host",
@@ -2142,11 +2234,14 @@ function startTunnelClient(opts) {
2142
2234
  let token = opts.token;
2143
2235
  let tokenAt = Date.now();
2144
2236
  let tokenRefused = false;
2237
+ let refreshFailures = 0;
2238
+ let refusal = null;
2145
2239
  let lastFrameAt = Date.now();
2146
2240
  let capturing = false;
2147
2241
  function connect() {
2148
2242
  const url = `${connectUrl}?project=${encodeURIComponent(opts.projectId)}&token=${encodeURIComponent(token)}`;
2149
- const socket = new import_ws.default(url, { headers: { [CLI_VERSION_HEADER]: version, "user-agent": `apiblaze-cli/${version}` } });
2243
+ const headers = isControlPlaneHost(connectUrl) ? { [CLI_VERSION_HEADER]: version, "user-agent": `apiblaze-cli/${version}` } : {};
2244
+ const socket = new import_ws.default(url, { headers });
2150
2245
  ws = socket;
2151
2246
  socket.on("open", () => {
2152
2247
  reconnects = 0;
@@ -2185,14 +2280,18 @@ function startTunnelClient(opts) {
2185
2280
  socket.on("error", () => {
2186
2281
  });
2187
2282
  }
2188
- function scheduleReconnect() {
2189
- const delay = Math.min(1e3 * 2 ** reconnects, MAX_RECONNECT_DELAY_MS);
2283
+ function scheduleReconnect(delayMs) {
2284
+ const delay = delayMs ?? Math.min(1e3 * 2 ** reconnects, MAX_RECONNECT_DELAY_MS);
2190
2285
  reconnects++;
2191
- opts.onStatus?.(`disconnected \u2014 reconnecting in ${Math.round(delay / 1e3)}s`);
2286
+ if (delayMs === void 0) opts.onStatus?.(`disconnected \u2014 reconnecting in ${Math.round(delay / 1e3)}s`);
2192
2287
  setTimeout(() => {
2193
2288
  void reconnect();
2194
2289
  }, delay);
2195
2290
  }
2291
+ function stop(reason, code) {
2292
+ closed = true;
2293
+ opts.onFatal?.(reason, code);
2294
+ }
2196
2295
  async function reconnect() {
2197
2296
  if (closed) return;
2198
2297
  if (opts.refresh && (tokenRefused || Date.now() - tokenAt > TOKEN_REFRESH_AFTER_MS)) {
@@ -2202,15 +2301,30 @@ function startTunnelClient(opts) {
2202
2301
  token = fresh.token;
2203
2302
  tokenAt = Date.now();
2204
2303
  tokenRefused = false;
2304
+ refreshFailures = 0;
2305
+ refusal = null;
2205
2306
  } catch (err) {
2206
- const status2 = typeof err?.status === "number" ? err.status : 0;
2207
- if (status2 === 401 || status2 === 403) {
2208
- closed = true;
2209
- opts.onFatal?.(err?.message ?? `HTTP ${status2}`);
2307
+ if (closed) return;
2308
+ if (err?.name === "LocalAuthError") {
2309
+ stop(err.message, typeof err.code === "string" ? err.code : "session_expired");
2210
2310
  return;
2211
2311
  }
2212
- opts.onStatus?.(`could not refresh the tunnel token (${err?.message ?? err}) \u2014 retrying`);
2213
- scheduleReconnect();
2312
+ const code = definitiveRefusal(err, opts.projectId);
2313
+ if (code) {
2314
+ const now = Date.now();
2315
+ refusal = refusal && refusal.code === code ? { ...refusal, count: refusal.count + 1 } : { code, firstAt: now, count: 1 };
2316
+ if (refusal.count >= REFUSAL_MIN_REPEATS && now - refusal.firstAt >= REFUSAL_MIN_SPAN_MS) {
2317
+ stop(err?.message ?? code, code);
2318
+ return;
2319
+ }
2320
+ } else {
2321
+ refusal = null;
2322
+ }
2323
+ const delay = Math.min(2e3 * 2 ** refreshFailures, MAX_REFRESH_RETRY_DELAY_MS);
2324
+ refreshFailures++;
2325
+ tokenRefused = true;
2326
+ opts.onStatus?.(`could not refresh the tunnel token (${err?.message ?? err}) \u2014 retrying in ${Math.round(delay / 1e3)}s`);
2327
+ scheduleReconnect(delay);
2214
2328
  return;
2215
2329
  }
2216
2330
  }
@@ -2340,6 +2454,7 @@ function startTunnelClient(opts) {
2340
2454
  }
2341
2455
 
2342
2456
  // src/commands/dev.ts
2457
+ init_types();
2343
2458
  init_anon_cred();
2344
2459
 
2345
2460
  // src/commands/create.ts
@@ -6690,6 +6805,20 @@ async function runDev(options) {
6690
6805
  }
6691
6806
  let restore = [];
6692
6807
  let connect;
6808
+ let clients = [];
6809
+ let isCleaningUp = false;
6810
+ async function cleanup(exitCode = 0) {
6811
+ if (isCleaningUp) return;
6812
+ isCleaningUp = true;
6813
+ setUpgradeShutdown(null);
6814
+ console.log(import_chalk20.default.gray("\n\nShutting down..."));
6815
+ for (const client of clients) client.close();
6816
+ if (captureStream) await new Promise((resolve4) => captureStream.end(() => resolve4()));
6817
+ await deleteDevTunnel(restore, keyAuth).catch(() => {
6818
+ });
6819
+ console.log(exitCode === 0 ? import_chalk20.default.green("Tunnel stopped.") : import_chalk20.default.yellow("Tunnel stopped."));
6820
+ process.exit(exitCode);
6821
+ }
6693
6822
  const registerTargets = { targets: selected.map((t) => ({ projectId: t.projectId, tenantId: t.tenantId })) };
6694
6823
  {
6695
6824
  const spinner = (0, import_ora7.default)("Registering the tunnel...").start();
@@ -6697,6 +6826,7 @@ async function runDev(options) {
6697
6826
  const result = await putDevTunnel(registerTargets, keyAuth);
6698
6827
  restore = (result.restore ?? []).map((r) => unmarkRestore(r, options.port));
6699
6828
  connect = result.connect;
6829
+ setUpgradeShutdown((code) => cleanup(code));
6700
6830
  spinner.stop();
6701
6831
  } catch (err) {
6702
6832
  spinner.fail("Failed to register the tunnel.");
@@ -6710,15 +6840,22 @@ async function runDev(options) {
6710
6840
  else held.push(line);
6711
6841
  };
6712
6842
  const fatal = /* @__PURE__ */ new Set();
6713
- const clients = connect.projects.map(
6843
+ let lastFatalCode;
6844
+ clients = connect.projects.map(
6714
6845
  (projectId) => startTunnelClient({
6715
6846
  connectUrl: connect.url,
6716
6847
  token: connect.token,
6717
6848
  // Re-register for a fresh token. PUT is idempotent on an already-tunnelled
6718
6849
  // project; its restore snapshot would be the marker, so it is IGNORED —
6719
- // teardown keeps replaying the one taken at start.
6850
+ // teardown keeps replaying the one taken at start. Projects whose client
6851
+ // already stopped are left out; a project the dashboard reports missing
6852
+ // is a per-project refusal (stops that client only, once it repeats).
6720
6853
  refresh: async () => {
6721
- const again = await putDevTunnel(registerTargets, keyAuth);
6854
+ const still = { targets: registerTargets.targets.filter((t) => !fatal.has(t.projectId)) };
6855
+ const again = await putDevTunnel(still.targets.length ? still : registerTargets, keyAuth);
6856
+ if (again.not_found?.includes(projectId) || !again.connect.projects.includes(projectId)) {
6857
+ throw new ApiError(404, `project ${projectId} not found`, { error: "project_not_found", project: projectId });
6858
+ }
6722
6859
  return { url: again.connect.url, token: again.connect.token };
6723
6860
  },
6724
6861
  projectId,
@@ -6733,12 +6870,16 @@ async function runDev(options) {
6733
6870
  \u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
6734
6871
  `)),
6735
6872
  onRecord: recordSink,
6736
- onFatal: (reason) => {
6737
- say(import_chalk20.default.red(`[${projectId}] tunnel stopped: the dashboard refused to renew it (${reason}).`));
6873
+ onFatal: (reason, code) => {
6874
+ const local = code === "session_expired" || code === "not_authenticated";
6875
+ say(import_chalk20.default.red(local ? `[${projectId}] tunnel stopped: ${reason}` : code === "project_not_found" ? `[${projectId}] tunnel stopped: the project no longer exists (or is no longer visible to this credential).` : `[${projectId}] tunnel stopped: the dashboard refused to renew it (${reason}).`));
6738
6876
  fatal.add(projectId);
6877
+ if (code !== "project_not_found") lastFatalCode = code;
6739
6878
  if (fatal.size === connect.projects.length) {
6740
- say(import_chalk20.default.yellow(auth.mode === "key" ? ` This anonymous workspace's key no longer covers these projects \u2014 most likely the workspace was claimed into an account.
6741
- Run \`npx ${B.cli} login\`, then \`npx ${B.cli} dev --port ${options.port} --auto\` to reattach.` : ` Your login no longer covers these projects. Run \`npx ${B.cli} login\`, then \`npx ${B.cli} dev --port ${options.port} --auto\` to reattach.`));
6879
+ if (lastFatalCode) {
6880
+ say(import_chalk20.default.yellow(auth.mode === "key" ? ` This anonymous workspace's key no longer covers these projects \u2014 most likely the workspace was claimed into an account.
6881
+ Run \`npx ${B.cli} login\`, then \`npx ${B.cli} dev --port ${options.port} --auto\` to reattach.` : ` Your login no longer covers these projects (or it expired). Run \`npx ${B.cli} login\`, then \`npx ${B.cli} dev --port ${options.port} --auto\` to reattach.`));
6882
+ }
6742
6883
  void cleanup(1);
6743
6884
  }
6744
6885
  }
@@ -6755,18 +6896,6 @@ async function runDev(options) {
6755
6896
  console.log();
6756
6897
  live = true;
6757
6898
  for (const line of held) console.log(line);
6758
- let isCleaningUp = false;
6759
- async function cleanup(exitCode = 0) {
6760
- if (isCleaningUp) return;
6761
- isCleaningUp = true;
6762
- console.log(import_chalk20.default.gray("\n\nShutting down..."));
6763
- for (const client of clients) client.close();
6764
- captureStream?.end();
6765
- await deleteDevTunnel(restore, keyAuth).catch(() => {
6766
- });
6767
- console.log(exitCode === 0 ? import_chalk20.default.green("Tunnel stopped.") : import_chalk20.default.yellow("Tunnel stopped."));
6768
- process.exit(exitCode);
6769
- }
6770
6899
  process.on("SIGINT", () => void cleanup());
6771
6900
  process.on("SIGTERM", () => void cleanup());
6772
6901
  process.on("SIGHUP", () => void cleanup());
@@ -15303,7 +15432,7 @@ function isOperatorLogin() {
15303
15432
  return !!email && OPERATOR_EMAILS.has(email);
15304
15433
  }
15305
15434
  var OP_COMMANDS = [
15306
- { cmd: "cli-policy", blurb: "Minimum CLI version the platform accepts (--min x.y.z, --reject-unversioned)" },
15435
+ { cmd: "cli-policy", blurb: 'Minimum CLI version the platform accepts (--min x.y.z --reason "\u2026" [--force], --reject-unversioned)' },
15307
15436
  { cmd: "op latency", blurb: "which FEATURE is slow, and what inside it", extra: "--hours N --team T --project P" },
15308
15437
  { cmd: "op latency slow", blurb: "slowest requests, each with a request id you can look up", extra: "--min-ms N" },
15309
15438
  { cmd: "op latency llm", blurb: "chat timing by provider/model, tokens, credit-reserve cost" },
@@ -15450,16 +15579,20 @@ async function runOp(sub, opts = {}, view, arg) {
15450
15579
  const wantsChange = typeof opts.min === "string" || !!opts.rejectUnversioned || !!opts.allowUnversioned || typeof opts.message === "string";
15451
15580
  let out = cur;
15452
15581
  if (wantsChange) {
15582
+ const reason = typeof opts.reason === "string" ? opts.reason.trim() : "";
15583
+ if (!reason) throw new Error('cli-policy: a change needs --reason "<why>" (recorded with the policy).');
15453
15584
  out = await opCall({ method: "PUT", path: "/operator/cli/policy", summary: "set CLI version policy", body: {
15454
15585
  min_version: typeof opts.min === "string" ? opts.min : cur.min_version,
15455
15586
  reject_unversioned: opts.rejectUnversioned ? true : opts.allowUnversioned ? false : cur.reject_unversioned,
15456
- message: typeof opts.message === "string" ? opts.message : cur.message
15587
+ message: typeof opts.message === "string" ? opts.message : cur.message,
15588
+ reason,
15589
+ ...opts.force ? { force: true } : {}
15457
15590
  } });
15458
15591
  }
15459
15592
  if (opts.json) return void console.log(JSON.stringify(out, null, 2));
15460
15593
  console.log("");
15461
15594
  console.log(` Minimum CLI version: ${import_chalk67.default.bold(out.min_version)}${out.min_version === "0.0.0" ? import_chalk67.default.dim(" (no minimum)") : ""}`);
15462
- console.log(` Refuse unversioned: ${out.reject_unversioned ? import_chalk67.default.yellow("yes \u2014 CLIs released before 2026-09-27 are refused") : "no"}`);
15595
+ console.log(` Refuse unversioned: ${out.reject_unversioned ? import_chalk67.default.yellow("yes \u2014 CLIs released before 2026-09-27 get a 426 (cheap refusals; they keep retrying)") : "no"}`);
15463
15596
  console.log(` Message: ${out.message ?? import_chalk67.default.dim("(default)")}`);
15464
15597
  if (out.updated_at) console.log(import_chalk67.default.dim(` Updated ${out.updated_at}${out.updated_by ? ` by ${out.updated_by}` : ""}`));
15465
15598
  console.log(import_chalk67.default.dim(" Takes effect within ~1 minute on the CLI lane; running `dev` sessions re-check every 10 minutes."));
@@ -15971,7 +16104,7 @@ apikeys.command("list").description("List control-plane API keys in your team").
15971
16104
  apikeys.command("mint").description("Create a control-plane API key (secret shown once) \u2014 or, with --tenant, a consumer key in that tenant: a backend key (default), a client key (--client) or a person-bound key (--for)").option("--desc <text>", "Description").option("--expires-days <n>", "Expiry in days (default 90 server-side)").option("--tenant <slug>", "Mint a CONSUMER key in this tenant instead of a control-plane key").option("--backend", `Backend key (default). ${HOLDER_HELP.backend}`).option("--client", `Client key. ${HOLDER_HELP.client} Needs --tenant.`).option("--for <email|id>", "Issue this key to a named person \u2014 calls made with it count as that person, and ownership rules trust it. The Users & Groups widget's admin lane accepts only such a key, one per admin. Needs --tenant.").option("--admin", "With --for: a key that acts as that tenant admin on the IAM API (users & groups) \u2014 adds manage-access; env defaults to prod").option("--env <name>", "Environment the consumer key is valid for (with --tenant; default dev, prod with --admin)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts, cmd) => runKeyMint({ ...cmd.parent?.opts(), ...opts })));
15972
16105
  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 })));
15973
16106
  program.addCommand(apikeys);
15974
- var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "latency | mark | wallet | money | incident | residue | sweep | test | perf | cli-policy (omit for the menu)").argument("[view]", "for `latency`: slow | llm | compare | grades | distance \xB7 for `wallet`: list | inspect | freeze \u2026 \xB7 for `money`: exposure | journal | reconcile \xB7 for `incident`: status | allow | deny | extend \xB7 for `mark`: the change name, quoted").argument("[arg]", "for `wallet inspect` / `money journal`: the wallet id (or a team id)").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("--min <version>", "for `cli-policy`: minimum CLI version (x.y.z; 0.0.0 = none)").option("--reject-unversioned", "for `cli-policy`: also refuse CLIs that send no version (released before 2026-09-27)").option("--allow-unversioned", "for `cli-policy`: stop refusing unversioned CLIs").option("--message <text>", "for `cli-policy`: message shown with the upgrade instruction").option("--days <n>", "For `latency grades`: how many days back (default 7)").option("--threshold-ms <n>", "For `latency distance`: network ms above which distance counts as a problem (default 50)").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 `test --perf --url`: the fixture's API key").option("--since <date>", "for `money journal` / `money reconcile`: start of the window (ISO date)").option("--kind <k>", "for `money journal`: one journal kind only").option("--limit <n>", "for `money journal` / `money exposure`: row cap").action(action((sub, view, arg, opts) => runOp(sub, opts, view, arg))).addHelpText("after", () => `
16107
+ var op = new import_commander.Command("op").description("Operator menu (platform operators only)").argument("[action]", "latency | mark | wallet | money | incident | residue | sweep | test | perf | cli-policy (omit for the menu)").argument("[view]", "for `latency`: slow | llm | compare | grades | distance \xB7 for `wallet`: list | inspect | freeze \u2026 \xB7 for `money`: exposure | journal | reconcile \xB7 for `incident`: status | allow | deny | extend \xB7 for `mark`: the change name, quoted").argument("[arg]", "for `wallet inspect` / `money journal`: the wallet id (or a team id)").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("--min <version>", "for `cli-policy`: minimum CLI version (x.y.z; 0.0.0 = none)").option("--reject-unversioned", "for `cli-policy`: also refuse CLIs that send no version (released before 2026-09-27)").option("--allow-unversioned", "for `cli-policy`: stop refusing unversioned CLIs").option("--message <text>", "for `cli-policy`: message shown with the upgrade instruction").option("--reason <text>", "for `cli-policy`: why the policy changes (required for a change; recorded with it)").option("--force", "for `cli-policy`: allow a minimum above the latest published npm version (locks out every CLI until it ships)").option("--days <n>", "For `latency grades`: how many days back (default 7)").option("--threshold-ms <n>", "For `latency distance`: network ms above which distance counts as a problem (default 50)").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 `test --perf --url`: the fixture's API key").option("--since <date>", "for `money journal` / `money reconcile`: start of the window (ISO date)").option("--kind <k>", "for `money journal`: one journal kind only").option("--limit <n>", "for `money journal` / `money exposure`: row cap").action(action((sub, view, arg, opts) => runOp(sub, opts, view, arg))).addHelpText("after", () => `
15975
16108
  ${renderOpCommands()}
15976
16109
  `);
15977
16110
  program.addCommand(op, { hidden: true });
@@ -16070,6 +16203,10 @@ async function recoverStaleTeam() {
16070
16203
  }
16071
16204
  }
16072
16205
  async function printError(err) {
16206
+ if (err instanceof CliUpgradeRequired) {
16207
+ await handleUpgradeRequired(err);
16208
+ return;
16209
+ }
16073
16210
  if (err instanceof ApiError) {
16074
16211
  const data = err.body;
16075
16212
  const extra = [data?.body?.reason, data?.body?.details, data?.details, data?.body?.error].find((x) => typeof x === "string" && x && x !== err.message);
@@ -16103,8 +16240,23 @@ does not have it, your npx cache is serving an older copy \u2014 re-run as:
16103
16240
  }
16104
16241
  hintStaleCacheOnUnknownOption(program);
16105
16242
  installFetchGuard(version);
16106
- var skipPolicy = process.argv.some((a) => a === "--version" || a === "-V" || a === "--help" || a === "-h");
16243
+ var firstCommand = process.argv.slice(2).find((a) => !a.startsWith("-"));
16244
+ var skipPolicy = firstCommand === "op" || process.argv.some((a) => a === "--version" || a === "-V" || a === "--help" || a === "-h");
16245
+ process.on("unhandledRejection", (reason) => {
16246
+ if (reason instanceof CliUpgradeRequired) {
16247
+ void handleUpgradeRequired(reason);
16248
+ return;
16249
+ }
16250
+ console.error(reason);
16251
+ process.exit(1);
16252
+ });
16107
16253
  (async () => {
16108
- if (!skipPolicy) await checkCliPolicy(version);
16254
+ if (!skipPolicy) {
16255
+ try {
16256
+ await checkCliPolicy(version);
16257
+ } catch (err) {
16258
+ if (err instanceof CliUpgradeRequired) return void await handleUpgradeRequired(err);
16259
+ }
16260
+ }
16109
16261
  program.parse(process.argv);
16110
16262
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.21.18",
3
+ "version": "0.21.19",
4
4
  "description": "APIblaze CLI — Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
5
5
  "keywords": [
6
6
  "apiblaze",