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.
- package/dist/index.js +214 -62
- 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
|
|
89
|
+
throw new LocalAuthError("not_authenticated", "Not authenticated. Run `apiblaze login` first.");
|
|
89
90
|
}
|
|
90
91
|
if (Date.now() >= creds.expiresAt) {
|
|
91
|
-
throw new
|
|
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.
|
|
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
|
-
|
|
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
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
1414
|
+
parsed = new URL(url);
|
|
1395
1415
|
} catch {
|
|
1396
1416
|
return false;
|
|
1397
1417
|
}
|
|
1398
|
-
if (
|
|
1399
|
-
for (const
|
|
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).
|
|
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
|
-
|
|
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(`
|
|
1410
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
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")
|
|
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
|
|
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
|
-
|
|
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(
|
|
1457
|
-
import_fs.default.writeFileSync(
|
|
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
|
|
1465
|
-
if (
|
|
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
|
-
|
|
1542
|
+
writeCache(policy);
|
|
1468
1543
|
}
|
|
1469
|
-
if (policy && cliRefusal(policy, version2))
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2207
|
-
if (
|
|
2208
|
-
|
|
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
|
-
|
|
2213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6741
|
-
|
|
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:
|
|
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
|
|
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
|
|
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)
|
|
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
|
})();
|