apiblaze 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.js +717 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,9 @@ npx apiblaze dev
|
|
|
36
36
|
# Or specify a port
|
|
37
37
|
npx apiblaze dev 3000
|
|
38
38
|
|
|
39
|
+
# Stream full request/response traffic to a file (JSON lines)
|
|
40
|
+
npx apiblaze dev 3000 --capture-file traffic.jsonl
|
|
41
|
+
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
## Help
|
|
@@ -70,6 +73,17 @@ apiblaze help dev
|
|
|
70
73
|
|
|
71
74
|
On Ctrl+C the tunnel is cleanly deregistered.
|
|
72
75
|
|
|
76
|
+
### Zero-setup conveniences
|
|
77
|
+
|
|
78
|
+
- **No project yet?** If none of your projects point at this machine, `apiblaze dev`
|
|
79
|
+
offers to spin up a throwaway dev proxy (random name like `braveotter42`) pointed at
|
|
80
|
+
`http://localhost:<port>` and tunnels it immediately — pick `none` or `api_key` auth.
|
|
81
|
+
- **Server not running yet?** If nothing is listening on the local port, requests aren't
|
|
82
|
+
dropped: each one is printed in full (headers + body, with API keys/JWTs masked and JWTs
|
|
83
|
+
decoded) and answered with a friendly synthetic `200`. The moment your dev server comes
|
|
84
|
+
up, the next request forwards to it automatically. Add `--capture-file <path>` to also
|
|
85
|
+
stream every request/response to a JSON-lines file.
|
|
86
|
+
|
|
73
87
|
## License
|
|
74
88
|
|
|
75
89
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -93,6 +93,7 @@ var init_auth = __esm({
|
|
|
93
93
|
// src/lib/api.ts
|
|
94
94
|
var api_exports = {};
|
|
95
95
|
__export(api_exports, {
|
|
96
|
+
agentCall: () => agentCall,
|
|
96
97
|
checkProxyName: () => checkProxyName,
|
|
97
98
|
claimProxy: () => claimProxy,
|
|
98
99
|
createProxy: () => createProxy,
|
|
@@ -147,6 +148,20 @@ async function apiFetch(path2, options = {}) {
|
|
|
147
148
|
}
|
|
148
149
|
return res.json();
|
|
149
150
|
}
|
|
151
|
+
async function agentCall(path2, method, body) {
|
|
152
|
+
const token = getAccessToken();
|
|
153
|
+
const res = await fetch(`${DASHBOARD_BASE}/api/cli/agents`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
156
|
+
body: JSON.stringify({ path: path2, method, body })
|
|
157
|
+
});
|
|
158
|
+
let data = null;
|
|
159
|
+
try {
|
|
160
|
+
data = await res.json();
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
return { status: res.status, data };
|
|
164
|
+
}
|
|
150
165
|
async function getTeams() {
|
|
151
166
|
const res = await apiFetch("/api/cli/teams");
|
|
152
167
|
const raw = Array.isArray(res) ? res : res?.teams ?? res?.data ?? [];
|
|
@@ -203,10 +218,10 @@ var init_api = __esm({
|
|
|
203
218
|
|
|
204
219
|
// src/index.ts
|
|
205
220
|
var import_commander = require("commander");
|
|
206
|
-
var
|
|
221
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
207
222
|
|
|
208
223
|
// package.json
|
|
209
|
-
var version = "0.
|
|
224
|
+
var version = "0.3.1";
|
|
210
225
|
|
|
211
226
|
// src/index.ts
|
|
212
227
|
init_types();
|
|
@@ -327,6 +342,7 @@ ${import_chalk.default.cyan("\u2192")} Team: ${import_chalk.default.bold(teamNam
|
|
|
327
342
|
}
|
|
328
343
|
|
|
329
344
|
// src/commands/dev.ts
|
|
345
|
+
var import_fs = __toESM(require("fs"));
|
|
330
346
|
var import_chalk3 = __toESM(require("chalk"));
|
|
331
347
|
var import_ora2 = __toESM(require("ora"));
|
|
332
348
|
var import_inquirer = __toESM(require("inquirer"));
|
|
@@ -361,10 +377,217 @@ function colorLatency(latency) {
|
|
|
361
377
|
if (latency < 500) return import_chalk2.default.yellow(s);
|
|
362
378
|
return import_chalk2.default.red(s);
|
|
363
379
|
}
|
|
380
|
+
function timestamp() {
|
|
381
|
+
return (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
|
|
382
|
+
}
|
|
364
383
|
function formatLogLine(entry) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
384
|
+
return `${import_chalk2.default.gray(`[${timestamp()}]`)} ${colorMethod(entry.method)} ${import_chalk2.default.white(entry.path)} ${import_chalk2.default.gray("\u2192")} ${colorStatus(entry.status)} ${import_chalk2.default.gray(`(${colorLatency(entry.latency)})`)}`;
|
|
385
|
+
}
|
|
386
|
+
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
387
|
+
"authorization",
|
|
388
|
+
"proxy-authorization",
|
|
389
|
+
"cookie",
|
|
390
|
+
"set-cookie",
|
|
391
|
+
"x-api-key",
|
|
392
|
+
"api-key",
|
|
393
|
+
"apikey",
|
|
394
|
+
"x-auth-token",
|
|
395
|
+
"x-access-token",
|
|
396
|
+
"x-refresh-token",
|
|
397
|
+
"x-amz-security-token",
|
|
398
|
+
"x-csrf-token"
|
|
399
|
+
]);
|
|
400
|
+
var SENSITIVE_QUERY = /* @__PURE__ */ new Set([
|
|
401
|
+
"api_key",
|
|
402
|
+
"apikey",
|
|
403
|
+
"key",
|
|
404
|
+
"token",
|
|
405
|
+
"access_token",
|
|
406
|
+
"refresh_token",
|
|
407
|
+
"id_token",
|
|
408
|
+
"secret",
|
|
409
|
+
"client_secret",
|
|
410
|
+
"password",
|
|
411
|
+
"sig",
|
|
412
|
+
"signature"
|
|
413
|
+
]);
|
|
414
|
+
function maskSecret(value) {
|
|
415
|
+
const v = value.trim();
|
|
416
|
+
if (v.length <= 8) return "\u2022\u2022\u2022\u2022";
|
|
417
|
+
const head = v.slice(0, 4);
|
|
418
|
+
const tail = v.slice(-4);
|
|
419
|
+
return `${head}\u2026${tail} ${import_chalk2.default.gray(`(${v.length} chars, masked)`)}`;
|
|
420
|
+
}
|
|
421
|
+
function base64UrlDecode(seg) {
|
|
422
|
+
try {
|
|
423
|
+
const pad = seg.length % 4 === 0 ? "" : "=".repeat(4 - seg.length % 4);
|
|
424
|
+
const b64 = seg.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
425
|
+
return Buffer.from(b64, "base64").toString("utf8");
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function decodeJwt(token) {
|
|
431
|
+
const parts = token.trim().split(".");
|
|
432
|
+
if (parts.length !== 3) return null;
|
|
433
|
+
const headerRaw = base64UrlDecode(parts[0]);
|
|
434
|
+
const payloadRaw = base64UrlDecode(parts[1]);
|
|
435
|
+
if (!headerRaw || !payloadRaw) return null;
|
|
436
|
+
try {
|
|
437
|
+
const header = JSON.parse(headerRaw);
|
|
438
|
+
const payload = JSON.parse(payloadRaw);
|
|
439
|
+
if (!header || typeof header !== "object" || !("alg" in header)) return null;
|
|
440
|
+
return { header, payload };
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function maskPath(path2) {
|
|
446
|
+
const q = path2.indexOf("?");
|
|
447
|
+
if (q < 0) return path2;
|
|
448
|
+
const base = path2.slice(0, q);
|
|
449
|
+
const query = path2.slice(q + 1);
|
|
450
|
+
const masked = query.split("&").map((pair) => {
|
|
451
|
+
const eq = pair.indexOf("=");
|
|
452
|
+
if (eq < 0) return pair;
|
|
453
|
+
const name = pair.slice(0, eq);
|
|
454
|
+
const val = pair.slice(eq + 1);
|
|
455
|
+
if (val && SENSITIVE_QUERY.has(decodeURIComponent(name).toLowerCase())) {
|
|
456
|
+
return `${name}=${maskSecret(decodeURIComponent(val))}`;
|
|
457
|
+
}
|
|
458
|
+
return pair;
|
|
459
|
+
}).join("&");
|
|
460
|
+
return `${base}?${masked}`;
|
|
461
|
+
}
|
|
462
|
+
function formatHeaderLines(name, value) {
|
|
463
|
+
const lower = name.toLowerCase();
|
|
464
|
+
if (lower === "authorization" || lower === "proxy-authorization") {
|
|
465
|
+
const m = /^Bearer\s+(.+)$/i.exec(value.trim());
|
|
466
|
+
if (m) {
|
|
467
|
+
const token = m[1];
|
|
468
|
+
const jwt = decodeJwt(token);
|
|
469
|
+
if (jwt) {
|
|
470
|
+
return [
|
|
471
|
+
` ${import_chalk2.default.dim(name)}: Bearer ${maskSecret(token)} ${import_chalk2.default.cyan("(JWT)")}`,
|
|
472
|
+
` ${import_chalk2.default.gray("\u251C header: ")} ${import_chalk2.default.gray(JSON.stringify(jwt.header))}`,
|
|
473
|
+
` ${import_chalk2.default.gray("\u2514 payload:")} ${import_chalk2.default.gray(JSON.stringify(jwt.payload))}`
|
|
474
|
+
];
|
|
475
|
+
}
|
|
476
|
+
return [` ${import_chalk2.default.dim(name)}: Bearer ${maskSecret(token)}`];
|
|
477
|
+
}
|
|
478
|
+
return [` ${import_chalk2.default.dim(name)}: ${maskSecret(value)}`];
|
|
479
|
+
}
|
|
480
|
+
if (SENSITIVE_HEADERS.has(lower)) {
|
|
481
|
+
return [` ${import_chalk2.default.dim(name)}: ${maskSecret(value)}`];
|
|
482
|
+
}
|
|
483
|
+
return [` ${import_chalk2.default.dim(name)}: ${value}`];
|
|
484
|
+
}
|
|
485
|
+
function formatBody(body, contentType) {
|
|
486
|
+
if (body.length === 0) return import_chalk2.default.gray(" (empty)");
|
|
487
|
+
if (body.includes(0)) return import_chalk2.default.gray(` <binary, ${body.length} bytes>`);
|
|
488
|
+
let text = body.toString("utf8");
|
|
489
|
+
const ct = (contentType ?? "").toLowerCase();
|
|
490
|
+
if (ct.includes("json") || /^[[{]/.test(text.trim())) {
|
|
491
|
+
try {
|
|
492
|
+
text = JSON.stringify(JSON.parse(text), null, 2);
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const MAX = 4e3;
|
|
497
|
+
const truncated = text.length > MAX;
|
|
498
|
+
const shown = truncated ? text.slice(0, MAX) : text;
|
|
499
|
+
const indented = shown.split("\n").map((l) => ` ${l}`).join("\n");
|
|
500
|
+
return indented + (truncated ? import_chalk2.default.gray(`
|
|
501
|
+
\u2026 (${text.length - MAX} more bytes \u2014 see --capture-file for full)`) : "");
|
|
502
|
+
}
|
|
503
|
+
function formatCapturedRequest(req, note) {
|
|
504
|
+
const headerLines = Object.keys(req.headers).flatMap((name) => formatHeaderLines(name, req.headers[name]));
|
|
505
|
+
const contentType = Object.keys(req.headers).find((k) => k.toLowerCase() === "content-type");
|
|
506
|
+
return [
|
|
507
|
+
`${import_chalk2.default.gray(`[${timestamp()}]`)} ${import_chalk2.default.magenta("\u26B2 CAPTURED")} ${colorMethod(req.method)} ${import_chalk2.default.white(maskPath(req.path))} ${import_chalk2.default.gray(`\u2014 ${note}`)}`,
|
|
508
|
+
import_chalk2.default.bold(" Headers:"),
|
|
509
|
+
...headerLines,
|
|
510
|
+
import_chalk2.default.bold(" Body:"),
|
|
511
|
+
formatBody(req.body, contentType ? req.headers[contentType] : void 0),
|
|
512
|
+
""
|
|
513
|
+
].join("\n");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/lib/random-name.ts
|
|
517
|
+
var ADJECTIVES = [
|
|
518
|
+
"amber",
|
|
519
|
+
"azure",
|
|
520
|
+
"brave",
|
|
521
|
+
"bright",
|
|
522
|
+
"calm",
|
|
523
|
+
"clever",
|
|
524
|
+
"cosmic",
|
|
525
|
+
"crisp",
|
|
526
|
+
"eager",
|
|
527
|
+
"fancy",
|
|
528
|
+
"gentle",
|
|
529
|
+
"happy",
|
|
530
|
+
"jolly",
|
|
531
|
+
"keen",
|
|
532
|
+
"lively",
|
|
533
|
+
"lucky",
|
|
534
|
+
"mellow",
|
|
535
|
+
"merry",
|
|
536
|
+
"nimble",
|
|
537
|
+
"noble",
|
|
538
|
+
"plucky",
|
|
539
|
+
"quiet",
|
|
540
|
+
"rapid",
|
|
541
|
+
"shiny",
|
|
542
|
+
"smooth",
|
|
543
|
+
"snappy",
|
|
544
|
+
"sunny",
|
|
545
|
+
"swift",
|
|
546
|
+
"tidy",
|
|
547
|
+
"vivid",
|
|
548
|
+
"witty",
|
|
549
|
+
"zesty"
|
|
550
|
+
];
|
|
551
|
+
var NOUNS = [
|
|
552
|
+
"badger",
|
|
553
|
+
"beacon",
|
|
554
|
+
"cedar",
|
|
555
|
+
"comet",
|
|
556
|
+
"dolphin",
|
|
557
|
+
"ember",
|
|
558
|
+
"falcon",
|
|
559
|
+
"finch",
|
|
560
|
+
"harbor",
|
|
561
|
+
"heron",
|
|
562
|
+
"koala",
|
|
563
|
+
"lark",
|
|
564
|
+
"lemur",
|
|
565
|
+
"maple",
|
|
566
|
+
"meadow",
|
|
567
|
+
"otter",
|
|
568
|
+
"panda",
|
|
569
|
+
"pebble",
|
|
570
|
+
"puffin",
|
|
571
|
+
"quail",
|
|
572
|
+
"raccoon",
|
|
573
|
+
"river",
|
|
574
|
+
"robin",
|
|
575
|
+
"sparrow",
|
|
576
|
+
"spruce",
|
|
577
|
+
"tiger",
|
|
578
|
+
"turtle",
|
|
579
|
+
"walrus",
|
|
580
|
+
"willow",
|
|
581
|
+
"wombat",
|
|
582
|
+
"yak",
|
|
583
|
+
"zebra"
|
|
584
|
+
];
|
|
585
|
+
function pick(list) {
|
|
586
|
+
return list[Math.floor(Math.random() * list.length)];
|
|
587
|
+
}
|
|
588
|
+
function randomProxyName() {
|
|
589
|
+
const digits = String(Math.floor(Math.random() * 100)).padStart(2, "0");
|
|
590
|
+
return `${pick(ADJECTIVES)}${pick(NOUNS)}${digits}`;
|
|
368
591
|
}
|
|
369
592
|
|
|
370
593
|
// src/lib/tunnel-client.ts
|
|
@@ -372,6 +595,7 @@ var import_ws = __toESM(require("ws"));
|
|
|
372
595
|
var CHUNK_BYTES = 512 * 1024;
|
|
373
596
|
var PING_INTERVAL_MS = 6e4;
|
|
374
597
|
var MAX_RECONNECT_DELAY_MS = 15e3;
|
|
598
|
+
var OFFLINE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EHOSTUNREACH"]);
|
|
375
599
|
var STRIP_HEADERS = /* @__PURE__ */ new Set([
|
|
376
600
|
"host",
|
|
377
601
|
"content-length",
|
|
@@ -389,6 +613,10 @@ function stripHeaders(headers) {
|
|
|
389
613
|
}
|
|
390
614
|
return out;
|
|
391
615
|
}
|
|
616
|
+
function encodeBody(buf) {
|
|
617
|
+
if (buf.includes(0)) return { value: buf.toString("base64"), encoding: "base64" };
|
|
618
|
+
return { value: buf.toString("utf8"), encoding: "utf8" };
|
|
619
|
+
}
|
|
392
620
|
function startTunnelClient(opts) {
|
|
393
621
|
const target = `http://127.0.0.1:${opts.localPort}`;
|
|
394
622
|
const inflight = /* @__PURE__ */ new Map();
|
|
@@ -396,6 +624,7 @@ function startTunnelClient(opts) {
|
|
|
396
624
|
let closed = false;
|
|
397
625
|
let reconnects = 0;
|
|
398
626
|
let pingTimer;
|
|
627
|
+
let capturing = false;
|
|
399
628
|
function connect() {
|
|
400
629
|
const url = `${opts.connectUrl}?project=${encodeURIComponent(opts.projectId)}&token=${encodeURIComponent(opts.token)}`;
|
|
401
630
|
const socket = new import_ws.default(url);
|
|
@@ -455,30 +684,80 @@ function startTunnelClient(opts) {
|
|
|
455
684
|
inflight.delete(id);
|
|
456
685
|
void forward(socket, id, f);
|
|
457
686
|
}
|
|
687
|
+
function sendResponse(socket, id, status, headers, body) {
|
|
688
|
+
send(socket, { id, type: "res", status, headers, bodyLen: body.length });
|
|
689
|
+
if (body.length === 0) {
|
|
690
|
+
send(socket, { id, type: "chunk", seq: 0, data: "", final: true });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
for (let off = 0, seq = 0; off < body.length; off += CHUNK_BYTES) {
|
|
694
|
+
const slice = body.subarray(off, Math.min(off + CHUNK_BYTES, body.length));
|
|
695
|
+
send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= body.length });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function record(f, reqBody, status, captured, resHeaders, resBody) {
|
|
699
|
+
if (!opts.onRecord) return;
|
|
700
|
+
const req = encodeBody(reqBody);
|
|
701
|
+
const res = resBody ? encodeBody(resBody) : void 0;
|
|
702
|
+
opts.onRecord({
|
|
703
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
704
|
+
projectId: opts.projectId,
|
|
705
|
+
captured,
|
|
706
|
+
method: f.method,
|
|
707
|
+
path: f.path,
|
|
708
|
+
requestHeaders: f.headers,
|
|
709
|
+
requestBody: reqBody.length ? req.value : void 0,
|
|
710
|
+
requestBodyEncoding: reqBody.length ? req.encoding : void 0,
|
|
711
|
+
status,
|
|
712
|
+
responseHeaders: resHeaders,
|
|
713
|
+
responseBody: res?.value,
|
|
714
|
+
responseBodyEncoding: res?.encoding,
|
|
715
|
+
latencyMs: Date.now() - f.start
|
|
716
|
+
});
|
|
717
|
+
}
|
|
458
718
|
async function forward(socket, id, f) {
|
|
459
719
|
const body = Buffer.concat(f.chunks);
|
|
460
720
|
const init = { method: f.method, headers: stripHeaders(f.headers) };
|
|
461
721
|
if (body.length) init.body = body;
|
|
462
|
-
let status = 502;
|
|
463
722
|
try {
|
|
464
723
|
const resp = await fetch(target + f.path, init);
|
|
465
|
-
status = resp.status;
|
|
724
|
+
const status = resp.status;
|
|
466
725
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
467
726
|
const headers = {};
|
|
468
727
|
resp.headers.forEach((value, key) => {
|
|
469
728
|
if (!STRIP_HEADERS.has(key.toLowerCase())) headers[key] = value;
|
|
470
729
|
});
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
send(socket, { id, type: "chunk", seq: seq++, data: slice.toString("base64"), final: off + CHUNK_BYTES >= buf.length });
|
|
730
|
+
if (capturing) {
|
|
731
|
+
capturing = false;
|
|
732
|
+
opts.onResume?.();
|
|
475
733
|
}
|
|
734
|
+
sendResponse(socket, id, status, headers, buf);
|
|
735
|
+
opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
|
|
736
|
+
record(f, body, status, false, headers, buf);
|
|
476
737
|
} catch (err) {
|
|
477
738
|
const code = err?.cause?.code;
|
|
478
|
-
|
|
739
|
+
if (OFFLINE_CODES.has(code)) {
|
|
740
|
+
if (!capturing) {
|
|
741
|
+
capturing = true;
|
|
742
|
+
opts.onCaptureStart?.();
|
|
743
|
+
}
|
|
744
|
+
const note = `no local server on port ${opts.localPort}`;
|
|
745
|
+
const payload = Buffer.from(JSON.stringify({
|
|
746
|
+
apiblaze_dev: "captured",
|
|
747
|
+
message: `No local server on port ${opts.localPort} \u2014 request captured by \`apiblaze dev\`. Start your server and resend to forward it.`,
|
|
748
|
+
request: { method: f.method, path: f.path }
|
|
749
|
+
}, null, 2));
|
|
750
|
+
const headers = { "content-type": "application/json", "x-apiblaze-dev": "captured" };
|
|
751
|
+
sendResponse(socket, id, 200, headers, payload);
|
|
752
|
+
opts.onCapture?.({ method: f.method, path: f.path, headers: f.headers, body }, note);
|
|
753
|
+
record(f, body, 200, true, headers, payload);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const message = err?.cause?.message || err?.message || String(err);
|
|
479
757
|
send(socket, { id, type: "err", message });
|
|
758
|
+
opts.onEntry({ method: f.method, path: f.path, status: 502, latency: Date.now() - f.start });
|
|
759
|
+
record(f, body, 502, false);
|
|
480
760
|
}
|
|
481
|
-
opts.onEntry({ method: f.method, path: f.path, status, latency: Date.now() - f.start });
|
|
482
761
|
}
|
|
483
762
|
function send(socket, frame) {
|
|
484
763
|
try {
|
|
@@ -500,6 +779,76 @@ function startTunnelClient(opts) {
|
|
|
500
779
|
}
|
|
501
780
|
|
|
502
781
|
// src/commands/dev.ts
|
|
782
|
+
async function offerAutoCreate(teamId, port) {
|
|
783
|
+
if (!process.stdin.isTTY) {
|
|
784
|
+
console.log(import_chalk3.default.yellow("No projects found with an internal target."));
|
|
785
|
+
console.log("Point a project at localhost in the dashboard, or run `apiblaze dev` in an interactive terminal to create one automatically.");
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
const { create } = await import_inquirer.default.prompt([{
|
|
789
|
+
type: "confirm",
|
|
790
|
+
name: "create",
|
|
791
|
+
message: `No project points at this machine. Create a quick dev proxy \u2192 ${import_chalk3.default.bold(`http://localhost:${port}`)} and tunnel it?`,
|
|
792
|
+
default: true
|
|
793
|
+
}]);
|
|
794
|
+
if (!create) return null;
|
|
795
|
+
const { auth } = await import_inquirer.default.prompt([{
|
|
796
|
+
type: "list",
|
|
797
|
+
name: "auth",
|
|
798
|
+
message: "How should callers authenticate to the new proxy?",
|
|
799
|
+
choices: [
|
|
800
|
+
{ name: "none \u2014 open to anyone with the URL (simplest)", value: "none" },
|
|
801
|
+
{ name: "api_key \u2014 callers must send an X-API-Key header", value: "api_key" }
|
|
802
|
+
],
|
|
803
|
+
default: "none"
|
|
804
|
+
}]);
|
|
805
|
+
let name = randomProxyName();
|
|
806
|
+
for (let i = 0; i < 8; i++) {
|
|
807
|
+
const check = await checkProxyName(name, teamId).catch(() => null);
|
|
808
|
+
if (!check || check.canUseProjectName && check.canUseApiVersion) break;
|
|
809
|
+
name = randomProxyName();
|
|
810
|
+
}
|
|
811
|
+
const spinner = (0, import_ora2.default)(`Creating dev proxy "${name}"...`).start();
|
|
812
|
+
let result;
|
|
813
|
+
try {
|
|
814
|
+
result = await createProxy({ name, target_url: `http://localhost:${port}`, auth_type: auth, team_id: teamId });
|
|
815
|
+
spinner.succeed(import_chalk3.default.green(`Created dev proxy "${name}".`));
|
|
816
|
+
} catch (err) {
|
|
817
|
+
spinner.fail("Failed to create the dev proxy.");
|
|
818
|
+
throw err;
|
|
819
|
+
}
|
|
820
|
+
const version2 = result.api_version || "1.0.0";
|
|
821
|
+
const endpoint = `https://${name}.apiblaze.com/${version2}/dev`;
|
|
822
|
+
console.log(` ${import_chalk3.default.dim("Endpoint:")} ${import_chalk3.default.bold(endpoint)}`);
|
|
823
|
+
if (auth === "api_key") {
|
|
824
|
+
const key = result.api_keys?.dev ?? Object.values(result.api_keys ?? {})[0];
|
|
825
|
+
if (key) {
|
|
826
|
+
console.log(` ${import_chalk3.default.dim("API key (dev):")} ${import_chalk3.default.bold.green(key)}`);
|
|
827
|
+
console.log(import_chalk3.default.dim(" Send it as the X-API-Key header. It may not be shown again."));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const targets = await getLocalhostTargets(teamId).catch(() => []);
|
|
831
|
+
const created = targets.find((t) => t.projectId === result.project_id);
|
|
832
|
+
if (!created) {
|
|
833
|
+
console.log(import_chalk3.default.yellow(" Proxy created, but it did not appear as a localhost target \u2014 try `apiblaze dev` again."));
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
return created;
|
|
837
|
+
}
|
|
838
|
+
async function probeLocalServer(port) {
|
|
839
|
+
const controller = new AbortController();
|
|
840
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
841
|
+
try {
|
|
842
|
+
await fetch(`http://127.0.0.1:${port}/`, { method: "HEAD", signal: controller.signal });
|
|
843
|
+
return true;
|
|
844
|
+
} catch (err) {
|
|
845
|
+
if (err?.name === "AbortError") return true;
|
|
846
|
+
const code = err?.cause?.code;
|
|
847
|
+
return !(code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH");
|
|
848
|
+
} finally {
|
|
849
|
+
clearTimeout(timer);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
503
852
|
async function runDev(options) {
|
|
504
853
|
const creds = loadCredentials();
|
|
505
854
|
if (!creds) {
|
|
@@ -540,13 +889,15 @@ async function runDev(options) {
|
|
|
540
889
|
throw err;
|
|
541
890
|
}
|
|
542
891
|
}
|
|
543
|
-
if (targets.length === 0) {
|
|
544
|
-
console.log(import_chalk3.default.yellow("No projects found with an internal target."));
|
|
545
|
-
console.log("Set a project's upstream target to localhost or a private IP in your APIblaze dashboard, then try again.");
|
|
546
|
-
process.exit(0);
|
|
547
|
-
}
|
|
548
892
|
let selectedTargets;
|
|
549
|
-
if (targets.length ===
|
|
893
|
+
if (targets.length === 0) {
|
|
894
|
+
const created = await offerAutoCreate(teamId, options.port);
|
|
895
|
+
if (!created) {
|
|
896
|
+
console.log("Set a project's upstream target to localhost or a private IP, then try again.");
|
|
897
|
+
process.exit(0);
|
|
898
|
+
}
|
|
899
|
+
selectedTargets = [created];
|
|
900
|
+
} else if (targets.length === 1) {
|
|
550
901
|
const { confirmed } = await import_inquirer.default.prompt([{
|
|
551
902
|
type: "confirm",
|
|
552
903
|
name: "confirmed",
|
|
@@ -580,6 +931,14 @@ async function runDev(options) {
|
|
|
580
931
|
Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
|
|
581
932
|
`)
|
|
582
933
|
);
|
|
934
|
+
let recordSink;
|
|
935
|
+
let captureStream;
|
|
936
|
+
if (options.captureFile) {
|
|
937
|
+
captureStream = import_fs.default.createWriteStream(options.captureFile, { flags: "a" });
|
|
938
|
+
recordSink = (r) => captureStream.write(JSON.stringify(r) + "\n");
|
|
939
|
+
console.log(import_chalk3.default.gray(`Streaming full traffic to ${options.captureFile}
|
|
940
|
+
`));
|
|
941
|
+
}
|
|
583
942
|
let restore = [];
|
|
584
943
|
let connect;
|
|
585
944
|
{
|
|
@@ -603,11 +962,27 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
|
|
|
603
962
|
projectId,
|
|
604
963
|
localPort: options.port,
|
|
605
964
|
onEntry: (entry) => console.log(formatLogLine(entry)),
|
|
606
|
-
onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`))
|
|
965
|
+
onStatus: (status) => console.log(import_chalk3.default.gray(`[${projectId}] ${status}`)),
|
|
966
|
+
onCapture: (req, note) => console.log(formatCapturedRequest(req, note)),
|
|
967
|
+
onCaptureStart: () => console.log(
|
|
968
|
+
import_chalk3.default.magenta(`
|
|
969
|
+
\u26B2 No local server on port ${options.port} yet \u2014 capturing requests below. Start your server and they'll forward automatically.
|
|
970
|
+
`)
|
|
971
|
+
),
|
|
972
|
+
onResume: () => console.log(
|
|
973
|
+
import_chalk3.default.green(`
|
|
974
|
+
\u2713 Local server detected on port ${options.port} \u2014 forwarding resumed.
|
|
975
|
+
`)
|
|
976
|
+
),
|
|
977
|
+
onRecord: recordSink
|
|
607
978
|
})
|
|
608
979
|
);
|
|
980
|
+
const localUp = await probeLocalServer(options.port);
|
|
609
981
|
console.log("\n" + import_chalk3.default.gray("\u2500".repeat(60)));
|
|
610
982
|
console.log(import_chalk3.default.bold("Live traffic") + import_chalk3.default.gray(" (Ctrl+C to stop)"));
|
|
983
|
+
console.log(
|
|
984
|
+
localUp ? import_chalk3.default.green(`\u2713 Local server detected on port ${options.port} \u2014 forwarding live.`) : import_chalk3.default.magenta(`\u26B2 Nothing listening on port ${options.port} yet \u2014 requests will be captured until your server starts.`)
|
|
985
|
+
);
|
|
611
986
|
console.log(import_chalk3.default.gray("\u2500".repeat(60)) + "\n");
|
|
612
987
|
let isCleaningUp = false;
|
|
613
988
|
async function cleanup() {
|
|
@@ -615,6 +990,7 @@ Tunneling ${selectedTargets.length} project(s) to localhost:${options.port}
|
|
|
615
990
|
isCleaningUp = true;
|
|
616
991
|
console.log(import_chalk3.default.gray("\n\nShutting down..."));
|
|
617
992
|
for (const client of clients) client.close();
|
|
993
|
+
captureStream?.end();
|
|
618
994
|
await deleteDevTunnel(restore).catch(() => {
|
|
619
995
|
});
|
|
620
996
|
console.log(import_chalk3.default.green("Tunnel stopped."));
|
|
@@ -687,7 +1063,7 @@ ${projects.length} project${projects.length === 1 ? "" : "s"}`));
|
|
|
687
1063
|
}
|
|
688
1064
|
|
|
689
1065
|
// src/commands/create.ts
|
|
690
|
-
var
|
|
1066
|
+
var import_fs2 = __toESM(require("fs"));
|
|
691
1067
|
var import_chalk5 = __toESM(require("chalk"));
|
|
692
1068
|
var import_ora4 = __toESM(require("ora"));
|
|
693
1069
|
init_auth();
|
|
@@ -922,7 +1298,7 @@ async function runAnonymousCreate(opts) {
|
|
|
922
1298
|
if (opts.config) {
|
|
923
1299
|
let raw = "";
|
|
924
1300
|
try {
|
|
925
|
-
raw =
|
|
1301
|
+
raw = import_fs2.default.readFileSync(opts.config, "utf8");
|
|
926
1302
|
} catch {
|
|
927
1303
|
fail(`Cannot read --config file: ${opts.config}`);
|
|
928
1304
|
}
|
|
@@ -1200,6 +1576,290 @@ async function runTeam(arg) {
|
|
|
1200
1576
|
\u2714 Active team: ${import_chalk9.default.bold(chosen.name)}`));
|
|
1201
1577
|
}
|
|
1202
1578
|
|
|
1579
|
+
// src/commands/authz.ts
|
|
1580
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
1581
|
+
init_auth();
|
|
1582
|
+
init_api();
|
|
1583
|
+
|
|
1584
|
+
// src/lib/agent-chat.ts
|
|
1585
|
+
var import_readline = __toESM(require("readline"));
|
|
1586
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
1587
|
+
init_api();
|
|
1588
|
+
async function runAgentChatRepl(opts) {
|
|
1589
|
+
const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
|
|
1590
|
+
console.log("\n" + import_chalk10.default.cyan.bold(title));
|
|
1591
|
+
if (subtitle) console.log(import_chalk10.default.dim(subtitle));
|
|
1592
|
+
const messages = [];
|
|
1593
|
+
const ctx = { proposal: null, lastData: null, log: (s) => console.log(s) };
|
|
1594
|
+
const cmdByName = new Map(commands.map((c) => [c.name, c]));
|
|
1595
|
+
const footer = () => {
|
|
1596
|
+
const parts = [
|
|
1597
|
+
import_chalk10.default.dim("type to chat/refine"),
|
|
1598
|
+
...commands.map((c) => import_chalk10.default.dim(`/${c.name} ${c.describe}`)),
|
|
1599
|
+
import_chalk10.default.dim("/show"),
|
|
1600
|
+
import_chalk10.default.dim("/drop discard & exit")
|
|
1601
|
+
];
|
|
1602
|
+
return " " + import_chalk10.default.dim("[ ") + parts.join(import_chalk10.default.dim(" \xB7 ")) + import_chalk10.default.dim(" ]");
|
|
1603
|
+
};
|
|
1604
|
+
async function turn(content) {
|
|
1605
|
+
messages.push({ role: "user", content });
|
|
1606
|
+
process.stdout.write(import_chalk10.default.dim(" \u2026thinking\n"));
|
|
1607
|
+
const { status, data } = await agentCall(endpoint, "POST", { messages, ...buildBody(messages) });
|
|
1608
|
+
if (status === 402) {
|
|
1609
|
+
console.log(import_chalk10.default.red(" Insufficient credits \u2014 top up to keep using the agents.\n"));
|
|
1610
|
+
messages.pop();
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (status >= 400 || !data) {
|
|
1614
|
+
console.log(import_chalk10.default.red(` Error (${status}): ${(data && data.error) ?? "request failed"}
|
|
1615
|
+
`));
|
|
1616
|
+
messages.pop();
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
ctx.lastData = data;
|
|
1620
|
+
const reply = data.reply ?? data.message ?? "(no reply)";
|
|
1621
|
+
messages.push({ role: "assistant", content: reply });
|
|
1622
|
+
console.log("\n" + import_chalk10.default.cyan("agent \u203A ") + reply + "\n");
|
|
1623
|
+
if (data.proposal) {
|
|
1624
|
+
ctx.proposal = data.proposal;
|
|
1625
|
+
const summary = summarizeProposal?.(data);
|
|
1626
|
+
if (summary) console.log(import_chalk10.default.yellow(" " + summary));
|
|
1627
|
+
const ready = commands.filter((c) => c.needsProposal).map((c) => `/${c.name}`).join(" or ");
|
|
1628
|
+
if (ready) console.log(import_chalk10.default.dim(` Ready \u2014 ${ready} to make it official, or keep refining.`));
|
|
1629
|
+
console.log();
|
|
1630
|
+
}
|
|
1631
|
+
const llm = data.llm;
|
|
1632
|
+
if (llm) {
|
|
1633
|
+
const credits = data.credits_remaining;
|
|
1634
|
+
const left = typeof credits === "number" ? ` \xB7 $${(credits / 100).toFixed(2)} credit left` : "";
|
|
1635
|
+
console.log(import_chalk10.default.dim(` ~$${(llm.cost_estimate ?? 0).toFixed(4)} \xB7 ${llm.model ?? "?"}${left}`));
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
1639
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
1640
|
+
await turn(seedPrompt);
|
|
1641
|
+
for (; ; ) {
|
|
1642
|
+
console.log(footer());
|
|
1643
|
+
const line = (await ask(import_chalk10.default.green("you \u203A "))).trim();
|
|
1644
|
+
if (!line) continue;
|
|
1645
|
+
if (line === "/drop" || line === "/exit" || line === "/quit") {
|
|
1646
|
+
console.log(import_chalk10.default.dim(" Dropped \u2014 nothing applied.\n"));
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
if (line === "/show") {
|
|
1650
|
+
if (!ctx.proposal) console.log(import_chalk10.default.dim(" No proposal yet \u2014 keep chatting until the agent proposes one.\n"));
|
|
1651
|
+
else console.log("\n" + JSON.stringify(ctx.proposal, null, 2) + "\n");
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (line.startsWith("/")) {
|
|
1655
|
+
const cmd = cmdByName.get(line.slice(1));
|
|
1656
|
+
if (!cmd) {
|
|
1657
|
+
console.log(import_chalk10.default.red(` Unknown command "${line}". Try one of: ${commands.map((c) => "/" + c.name).join(", ")}, /show, /drop
|
|
1658
|
+
`));
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
if (cmd.needsProposal && !ctx.proposal) {
|
|
1662
|
+
console.log(import_chalk10.default.yellow(" No proposal yet \u2014 keep chatting until the agent generates one, then try again.\n"));
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
await cmd.run(ctx);
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
await turn(line);
|
|
1669
|
+
}
|
|
1670
|
+
rl.close();
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/commands/authz.ts
|
|
1674
|
+
async function runAuthz(projectArg, apiVersionArg) {
|
|
1675
|
+
const creds = loadCredentials();
|
|
1676
|
+
if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
|
|
1677
|
+
const teamId = creds.teamId ?? "";
|
|
1678
|
+
const projects = await getProjects(teamId);
|
|
1679
|
+
const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
|
|
1680
|
+
if (!match) {
|
|
1681
|
+
throw new Error(`Project "${projectArg}" not found in your active team (${creds.teamName ?? teamId}). Run \`apiblaze projects\`.`);
|
|
1682
|
+
}
|
|
1683
|
+
const projectId = match.projectId;
|
|
1684
|
+
const apiVersion = apiVersionArg || match.apiVersion;
|
|
1685
|
+
const tenant = match.tenant || projectId;
|
|
1686
|
+
async function apply(ctx, enable) {
|
|
1687
|
+
const proposal = ctx.proposal;
|
|
1688
|
+
if (!enable) {
|
|
1689
|
+
const m = await agentCall(`/projects/${projectId}/${apiVersion}/policies/model?tenantId=${encodeURIComponent(tenant)}`, "POST", proposal.model);
|
|
1690
|
+
if (m.status === 404) {
|
|
1691
|
+
ctx.log(import_chalk11.default.red(" Authorization store not provisioned yet. Open the dashboard Authorization tab for this project once (it auto-provisions the store), then re-run.\n"));
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (m.status >= 400) {
|
|
1695
|
+
ctx.log(import_chalk11.default.red(` Model save failed (${m.status}): ${m.data?.error ?? ""}
|
|
1696
|
+
`));
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
let ok = 0;
|
|
1701
|
+
let fail4 = 0;
|
|
1702
|
+
for (const r of proposal.routes) {
|
|
1703
|
+
const resource = "/" + String(r.resource || "").replace(/^\/+/, "");
|
|
1704
|
+
const policiesBody = {
|
|
1705
|
+
rule_mode: r.rule_mode === "list" ? "list" : "check-write",
|
|
1706
|
+
on_request_read: Array.isArray(r.on_request_read) ? r.on_request_read : [],
|
|
1707
|
+
post_response_write: Array.isArray(r.post_response_write) ? r.post_response_write : [],
|
|
1708
|
+
list_objects_read: r.list_objects_read ?? null,
|
|
1709
|
+
authentication_config: { require_authentication: true },
|
|
1710
|
+
authorization_enabled: enable
|
|
1711
|
+
};
|
|
1712
|
+
const encoded = resource.replace(/\{/g, "%7B").replace(/\}/g, "%7D");
|
|
1713
|
+
const putPath = `/projects/${projectId}/${apiVersion}/policies/route/${r.method.toUpperCase()}${encoded}`;
|
|
1714
|
+
let res = await agentCall(putPath, "PUT", { ...policiesBody, resource });
|
|
1715
|
+
if (res.status === 404) {
|
|
1716
|
+
res = await agentCall(`/projects/${projectId}/${apiVersion}/policies/route`, "POST", { method: r.method.toUpperCase(), resource, ...policiesBody });
|
|
1717
|
+
}
|
|
1718
|
+
if (res.status >= 200 && res.status < 300) ok++;
|
|
1719
|
+
else fail4++;
|
|
1720
|
+
}
|
|
1721
|
+
if (enable) {
|
|
1722
|
+
const e = await agentCall(`/${projectId}/${apiVersion}/config`, "PATCH", { authorization: { enforce_authorization: true }, tenant });
|
|
1723
|
+
if (e.status >= 400) {
|
|
1724
|
+
ctx.log(import_chalk11.default.red(` Enforced ${ok} route(s) but turning on the project-level switch failed (${e.status}). Toggle "Enforce Authorization" on in the dashboard.
|
|
1725
|
+
`));
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
ctx.log(import_chalk11.default.green(` \u2713 Enforcement ON \u2014 ${ok} route(s) + project switch on${fail4 ? `, ${fail4} failed` : ""}.
|
|
1729
|
+
`));
|
|
1730
|
+
} else {
|
|
1731
|
+
ctx.log(import_chalk11.default.green(` \u2713 Published (shadow): model + ${ok} route(s)${fail4 ? `, ${fail4} failed` : ""}. Review, then /enable.
|
|
1732
|
+
`));
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
await runAgentChatRepl({
|
|
1736
|
+
title: `Authorization assistant \u2014 ${projectId} ${apiVersion}`,
|
|
1737
|
+
subtitle: `tenant ${tenant} \xB7 discuss what authorization fits this API, then make it official.`,
|
|
1738
|
+
endpoint: `/projects/${projectId}/${apiVersion}/authz/chat`,
|
|
1739
|
+
buildBody: () => ({ included_sample_ids: [], existing_model: null, existing_routes: [] }),
|
|
1740
|
+
seedPrompt: "Analyze this API and tell me what authorization is feasible. If it is read-only, say so plainly. Then propose options \u2014 do not generate rules yet.",
|
|
1741
|
+
summarizeProposal: (data) => {
|
|
1742
|
+
const proposal = data.proposal;
|
|
1743
|
+
const sim = data.simulation;
|
|
1744
|
+
const cov = sim ? `, ${sim.routes_covered ?? 0} covered by rules` : "";
|
|
1745
|
+
const warn = sim?.blocked_without_backfill ? ` \u26A0 ${sim.blocked_without_backfill} currently-OK request(s) would be denied until tuples exist.` : "";
|
|
1746
|
+
return `Proposal ready: ${proposal.routes.length} route(s)${cov}.${warn}`;
|
|
1747
|
+
},
|
|
1748
|
+
commands: [
|
|
1749
|
+
{ name: "publish", describe: "save in shadow mode", needsProposal: true, run: (ctx) => apply(ctx, false) },
|
|
1750
|
+
{ name: "enable", describe: "turn on enforcement", needsProposal: true, run: (ctx) => apply(ctx, true) }
|
|
1751
|
+
]
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/commands/openapi.ts
|
|
1756
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
1757
|
+
init_auth();
|
|
1758
|
+
init_api();
|
|
1759
|
+
async function runOpenapi(projectArg, apiVersionArg) {
|
|
1760
|
+
const creds = loadCredentials();
|
|
1761
|
+
if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
|
|
1762
|
+
const projects = await getProjects(creds.teamId ?? "");
|
|
1763
|
+
const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
|
|
1764
|
+
if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
|
|
1765
|
+
const projectId = match.projectId;
|
|
1766
|
+
const apiVersion = apiVersionArg || match.apiVersion;
|
|
1767
|
+
console.log(import_chalk12.default.dim("Gathering captured traffic samples\u2026"));
|
|
1768
|
+
const routesRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes`, "GET");
|
|
1769
|
+
if (routesRes.status >= 400) {
|
|
1770
|
+
console.log(import_chalk12.default.red(`Could not list traffic (${routesRes.status}): ${routesRes.data?.error ?? ""}`));
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const routes = routesRes.data?.routes ?? (Array.isArray(routesRes.data) ? routesRes.data : []);
|
|
1774
|
+
const hashes = routes.map((r) => r.route_hash).filter((h) => !!h);
|
|
1775
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1776
|
+
for (const h of hashes) {
|
|
1777
|
+
const s = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes/${encodeURIComponent(h)}/samples`, "GET");
|
|
1778
|
+
const arr = s.data?.samples ?? [];
|
|
1779
|
+
for (const x of arr) if (x.sample_id) ids.add(x.sample_id);
|
|
1780
|
+
}
|
|
1781
|
+
const sampleIds = [...ids];
|
|
1782
|
+
if (sampleIds.length === 0) {
|
|
1783
|
+
console.log(import_chalk12.default.yellow("No captured traffic samples found. Hit the dev environment of this proxy to capture some traces, then retry."));
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
async function publish(ctx) {
|
|
1787
|
+
const patch = ctx.proposal.patch;
|
|
1788
|
+
if (!Array.isArray(patch) || patch.length === 0) {
|
|
1789
|
+
ctx.log(import_chalk12.default.green(" Nothing to publish \u2014 the spec already covers the observed traffic.\n"));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const specSource = ctx.lastData?.spec_source ?? "unknown";
|
|
1793
|
+
const endpoint = specSource === "github" ? "open-pr" : "publish-openapi";
|
|
1794
|
+
const pub = await agentCall(`/projects/${projectId}/${apiVersion}/samples/${endpoint}`, "POST", { patch, sample_ids_used: sampleIds });
|
|
1795
|
+
if (pub.status >= 400) {
|
|
1796
|
+
ctx.log(import_chalk12.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
|
|
1797
|
+
`));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const prUrl = pub.data?.pr_url;
|
|
1801
|
+
ctx.log(import_chalk12.default.green(prUrl ? ` \u2713 Pull request opened: ${prUrl}
|
|
1802
|
+
` : " \u2713 Published updated OpenAPI spec.\n"));
|
|
1803
|
+
}
|
|
1804
|
+
await runAgentChatRepl({
|
|
1805
|
+
title: `API-docs assistant \u2014 ${projectId} ${apiVersion}`,
|
|
1806
|
+
subtitle: `${sampleIds.length} captured sample(s) \xB7 discuss what to document, then make it official.`,
|
|
1807
|
+
endpoint: `/projects/${projectId}/${apiVersion}/openapi/chat`,
|
|
1808
|
+
buildBody: () => ({ included_sample_ids: sampleIds }),
|
|
1809
|
+
seedPrompt: "Compare the captured samples against the current spec and tell me what's missing or wrong. List the changes you'd make \u2014 don't produce a patch yet.",
|
|
1810
|
+
summarizeProposal: (data) => {
|
|
1811
|
+
const patch = data.proposal.patch;
|
|
1812
|
+
const n = Array.isArray(patch) ? patch.length : 0;
|
|
1813
|
+
return n === 0 ? "The spec already covers the observed traffic \u2014 nothing to publish." : `Patch ready: ${n} additive change(s) to the OpenAPI spec.`;
|
|
1814
|
+
},
|
|
1815
|
+
commands: [
|
|
1816
|
+
{ name: "publish", describe: "publish / open PR", needsProposal: true, run: publish }
|
|
1817
|
+
]
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/commands/mcp.ts
|
|
1822
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
1823
|
+
init_auth();
|
|
1824
|
+
init_api();
|
|
1825
|
+
async function runMcp(projectArg, apiVersionArg, opts) {
|
|
1826
|
+
const creds = loadCredentials();
|
|
1827
|
+
if (!creds) throw new Error("Not authenticated. Run `apiblaze login` first.");
|
|
1828
|
+
const projects = await getProjects(creds.teamId ?? "");
|
|
1829
|
+
const match = projects.find((p) => p.projectId === projectArg || p.projectName === projectArg);
|
|
1830
|
+
if (!match) throw new Error(`Project "${projectArg}" not found in your active team. Run \`apiblaze projects\`.`);
|
|
1831
|
+
const projectId = match.projectId;
|
|
1832
|
+
const apiVersion = apiVersionArg || match.apiVersion;
|
|
1833
|
+
const environment = opts.environment || "prod";
|
|
1834
|
+
async function publish(ctx) {
|
|
1835
|
+
const spec = ctx.proposal;
|
|
1836
|
+
const pub = await agentCall(`/projects/${projectId}/${apiVersion}/mcp/spec`, "PUT", { environment, spec });
|
|
1837
|
+
if (pub.status >= 400) {
|
|
1838
|
+
ctx.log(import_chalk13.default.red(` Publish failed (${pub.status}): ${pub.data?.error ?? ""}
|
|
1839
|
+
`));
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
ctx.log(import_chalk13.default.green(` \u2713 Published MCP server \u2014 ${projectId}.mcp.apiblaze.com/${apiVersion}/${environment}.
|
|
1843
|
+
`));
|
|
1844
|
+
}
|
|
1845
|
+
await runAgentChatRepl({
|
|
1846
|
+
title: `MCP builder \u2014 ${projectId} ${apiVersion} (${environment})`,
|
|
1847
|
+
subtitle: "discuss which routes to expose as tools, then make the catalogue official.",
|
|
1848
|
+
endpoint: `/projects/${projectId}/${apiVersion}/mcp/chat`,
|
|
1849
|
+
buildBody: () => ({ environment, included_sample_ids: [] }),
|
|
1850
|
+
seedPrompt: "Look at this API's routes and recommend which should become MCP tools, with good names and descriptions. Don't finalize yet \u2014 explain first.",
|
|
1851
|
+
summarizeProposal: (data) => {
|
|
1852
|
+
const spec = data.proposal;
|
|
1853
|
+
const tools = Array.isArray(spec.tools) ? spec.tools : [];
|
|
1854
|
+
const names = tools.slice(0, 12).map((t) => t.name ?? "(unnamed)").join(", ");
|
|
1855
|
+
return `Catalogue ready: ${tools.length} tool(s)${names ? ` \u2014 ${names}${tools.length > 12 ? ", \u2026" : ""}` : ""}.`;
|
|
1856
|
+
},
|
|
1857
|
+
commands: [
|
|
1858
|
+
{ name: "publish", describe: `to ${projectId}.mcp.apiblaze.com`, needsProposal: true, run: publish }
|
|
1859
|
+
]
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1203
1863
|
// src/index.ts
|
|
1204
1864
|
var program = new import_commander.Command();
|
|
1205
1865
|
program.name("apiblaze").description("APIblaze CLI \u2014 create & manage API proxies and run dev tunnels").version(version);
|
|
@@ -1259,14 +1919,38 @@ program.command("projects").description("List the projects in your team").action
|
|
|
1259
1919
|
process.exit(1);
|
|
1260
1920
|
}
|
|
1261
1921
|
});
|
|
1262
|
-
program.command("
|
|
1922
|
+
program.command("authz").description("Design API authorization interactively (chat), then publish + enable it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
|
|
1923
|
+
try {
|
|
1924
|
+
await runAuthz(project, apiVersion);
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
printError(err);
|
|
1927
|
+
process.exit(1);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
program.command("openapi").description("Design your OpenAPI spec from captured traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").action(async (project, apiVersion) => {
|
|
1931
|
+
try {
|
|
1932
|
+
await runOpenapi(project, apiVersion);
|
|
1933
|
+
} catch (err) {
|
|
1934
|
+
printError(err);
|
|
1935
|
+
process.exit(1);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
program.command("mcp").description("Design an MCP server from the spec + traffic interactively (chat), then publish it").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[apiVersion]", "API version (defaults to the project's version)").option("--environment <env>", "Environment to publish (default: prod)").action(async (project, apiVersion, opts) => {
|
|
1939
|
+
try {
|
|
1940
|
+
await runMcp(project, apiVersion, opts);
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
printError(err);
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
program.command("dev").description("Start a dev tunnel for your localhost projects").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").option("-o, --capture-file <path>", "Stream full request/response traffic to a file (JSON lines)").action(async (port, opts) => {
|
|
1263
1947
|
try {
|
|
1264
1948
|
const resolved = parseInt(port ?? opts.port, 10);
|
|
1265
1949
|
if (Number.isNaN(resolved)) {
|
|
1266
|
-
console.error(
|
|
1950
|
+
console.error(import_chalk14.default.red(`Invalid port: ${port ?? opts.port}`));
|
|
1267
1951
|
process.exit(1);
|
|
1268
1952
|
}
|
|
1269
|
-
await runDev({ port: resolved });
|
|
1953
|
+
await runDev({ port: resolved, captureFile: opts.captureFile });
|
|
1270
1954
|
} catch (err) {
|
|
1271
1955
|
printError(err);
|
|
1272
1956
|
process.exit(1);
|
|
@@ -1286,23 +1970,27 @@ Examples:
|
|
|
1286
1970
|
$ npx apiblaze login
|
|
1287
1971
|
$ npx apiblaze create --name myapi --target https://api.example.com --auth api_key
|
|
1288
1972
|
|
|
1973
|
+
# Dev tunnel \u2014 auto-creates a proxy if none point here, and captures
|
|
1974
|
+
# traffic (full headers + body, secrets masked) until your server is up:
|
|
1975
|
+
$ npx apiblaze dev 3000
|
|
1976
|
+
$ npx apiblaze dev 3000 --capture-file traffic.jsonl
|
|
1977
|
+
|
|
1289
1978
|
# Manage:
|
|
1290
1979
|
$ npx apiblaze whoami
|
|
1291
1980
|
$ npx apiblaze projects
|
|
1292
1981
|
$ npx apiblaze team
|
|
1293
|
-
$ npx apiblaze dev 3000
|
|
1294
1982
|
$ npx apiblaze logout
|
|
1295
1983
|
`
|
|
1296
1984
|
);
|
|
1297
1985
|
function printError(err) {
|
|
1298
1986
|
if (err instanceof ApiError) {
|
|
1299
|
-
console.error(
|
|
1987
|
+
console.error(import_chalk14.default.red(`
|
|
1300
1988
|
API error (${err.status}): ${err.message}`));
|
|
1301
1989
|
} else if (err instanceof Error) {
|
|
1302
|
-
console.error(
|
|
1990
|
+
console.error(import_chalk14.default.red(`
|
|
1303
1991
|
Error: ${err.message}`));
|
|
1304
1992
|
} else {
|
|
1305
|
-
console.error(
|
|
1993
|
+
console.error(import_chalk14.default.red("\nUnknown error"));
|
|
1306
1994
|
}
|
|
1307
1995
|
}
|
|
1308
1996
|
program.parse(process.argv);
|