apiblaze 0.21.25 → 0.21.26
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 +245 -147
- package/dist/server/index.js +12 -1
- package/dist/server/index.mjs +12 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__
|
|
|
34
34
|
var version;
|
|
35
35
|
var init_package = __esm({
|
|
36
36
|
"package.json"() {
|
|
37
|
-
version = "0.21.
|
|
37
|
+
version = "0.21.26";
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -6366,16 +6366,16 @@ var init_files = __esm({
|
|
|
6366
6366
|
"use strict";
|
|
6367
6367
|
DEMO_FILES = {
|
|
6368
6368
|
"README.md": "# Nino's Pizza: the APIblaze demo\n\nA tiny table-booking app for pizzerias, used by `npx apiblaze@latest demo` to show what APIblaze does.\n\n```\nnpx apiblaze@latest demo\n```\n\nThat one command writes this folder to `./apiblaze-demo`, puts APIblaze in front of the backend,\nstarts everything and opens the page. No login, no prompts, no dependencies.\n\n## What the page shows\n\n1. **Your API is live on the internet.** The backend runs on your laptop; APIblaze gives it a public\n URL and an MCP address for AI assistants. A live ticker shows each request reaching your laptop.\n2. **An AI assistant uses it.** Ask it to book a table and watch the request arrive.\n3. **Included, no code:** only the owner can change a booking, each customer (Nino Pizza,\n Gino Pizza) is separate, rate limits, sign-in, and drop-in widgets for API keys and roles.\n\n## The files\n\n| File | What it is |\n| --- | --- |\n| `backend.js` | The pizzeria's API. No login, no keys, no permission checks. |\n| `openapi.yaml` | Its endpoints. APIblaze builds the AI tools and the ownership rules from it. |\n| `app.js` | The website's server. Calls the API with one key and says who is acting. |\n| `public/index.html` | The page. |\n| `start.js` | Starts the backend (port 3001) and the website (port 3000). |\n\n`npx apiblaze@latest demo` also adds `lib/apiblaze-server.js` and `public/apiblaze-widgets.js`, the same\nhelpers you get from `npm i apiblaze`. Stop the demo with `npx apiblaze@latest demo stop`.\n",
|
|
6369
|
-
"openapi.yaml": "openapi: 3.0.3\ninfo:\n title:
|
|
6370
|
-
"backend.js": "// Nino
|
|
6371
|
-
"app.js": "//
|
|
6372
|
-
"start.js": "// Starts
|
|
6369
|
+
"openapi.yaml": "openapi: 3.0.3\ninfo:\n title: ResiResi API\n version: 1.0.0\n description: Table bookings at restaurants (each restaurant is a tenant, like Nino Pizza and Gino Pizza). APIblaze reads this file to build the MCP tools for AI assistants and to work out who may change what.\npaths:\n /reservations:\n get:\n operationId: listReservations\n summary: List my bookings\n responses:\n '200':\n description: The caller's bookings\n content:\n application/json:\n schema:\n type: array\n items: { $ref: '#/components/schemas/Reservation' }\n post:\n operationId: createReservation\n summary: Book a table\n requestBody:\n required: true\n content:\n application/json:\n schema: { $ref: '#/components/schemas/NewReservation' }\n responses:\n '201':\n description: Booked\n content:\n application/json:\n schema: { $ref: '#/components/schemas/Reservation' }\n /reservations/{id}:\n parameters:\n - { name: id, in: path, required: true, schema: { type: string } }\n get:\n operationId: getReservation\n summary: Get one booking\n responses:\n '200':\n description: The booking\n content:\n application/json:\n schema: { $ref: '#/components/schemas/Reservation' }\n delete:\n operationId: cancelReservation\n summary: Cancel a booking\n responses:\n '204': { description: Cancelled }\ncomponents:\n schemas:\n NewReservation:\n type: object\n required: [table, time]\n properties:\n name: { type: string, example: Ana, description: 'Optional: defaults to the signed-in person. Do not ask for it.' }\n table: { type: integer, example: 4 }\n time: { type: string, example: '19:30', description: 'Time of the booking, 24h HH:MM' }\n guests: { type: integer, example: 2, description: 'How many people' }\n Reservation:\n allOf:\n - $ref: '#/components/schemas/NewReservation'\n - type: object\n properties:\n id: { type: string, example: r1 }\n",
|
|
6370
|
+
"backend.js": "// ResiResi backend: restaurants (tenants, like Nino Pizza and Gino Pizza) let their customers\n// book, list and cancel tables. It runs on YOUR laptop.\n//\n// `npx apiblaze@latest dev` (which the demo runs for you) puts it on the internet\n// at a public URL and gives AI assistants an MCP address for it. Everything else\n// (API keys, sign-in, who may change what, rate limits) happens in APIblaze, in\n// front of this file. Look at what is NOT here: no login, no keys, no permission\n// checks, no AI code.\n//\n// What APIblaze tells this backend about each call, in two headers:\n// x-abz-tenant-id which customer (pizzeria) is calling\n// x-abz-user-id which person\n// and one shared secret it adds to every call it forwards (x-target-api-key), so a\n// caller who skips APIblaze and hits localhost:3001 directly is refused.\n//\n// Zero dependencies. Run it alone with `node backend.js` (port 3001).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst bookings = new Map();\nlet next = 1;\n// Bumps on every booking made or cancelled. The page asks the app (on this laptop, free)\n// whether it moved before re-listing through APIblaze, so an open, idle page spends nothing.\nlet changes = 0;\n// The last requests this laptop answered \u2014 the demo page shows them live.\nconst recent = [];\nfunction remember(entry) {\n recent.unshift({ at: Date.now(), ...entry });\n recent.length = Math.min(recent.length, 50);\n // One line per request in the terminal (`npx apiblaze@latest demo logs` follows it).\n const who = entry.who ? `${entry.who}${entry.tenant ? ` @ ${entry.tenant}` : ''}` : '-';\n console.log(`${new Date().toISOString().slice(11, 19)} ${entry.status} ${entry.method.padEnd(6)} ${entry.path.padEnd(22)} ${who}`);\n}\n\nfunction send(res, status, body) {\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(body === undefined ? '' : JSON.stringify(body));\n}\n\nfunction handle(req, res, body) {\n const url = new URL(req.url, 'http://x');\n if (url.pathname === '/openapi.yaml') {\n res.writeHead(200, { 'content-type': 'text/yaml' });\n return res.end(fs.readFileSync(path.join(__dirname, 'openapi.yaml')));\n }\n const who = req.headers['x-abz-user-id'] || '';\n const tenant = req.headers['x-abz-tenant-id'] || '';\n const reply = (status, data) => { remember({ method: req.method, path: url.pathname, who, tenant, status }); send(res, status, data); };\n const secret = process.env.BACKEND_SECRET || '';\n if (secret && req.headers['x-target-api-key'] !== secret) return reply(401, { error: 'missing or wrong backend secret (APIblaze adds it to every call it forwards)' });\n if (!who) return reply(401, { error: 'say who is calling (x-abz-user-id)' });\n\n const m = /^\\/reservations(?:\\/([^/]+))?$/.exec(url.pathname);\n if (!m) return reply(404, { error: 'not found' });\n const id = m[1];\n\n if (!id && req.method === 'POST') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const row = {\n id: `r${next++}`, name: String(input.name || who), table: Number(input.table) || 1,\n time: String(input.time || '19:30'), guests: Number(input.guests) || 2, owner: who, tenant,\n };\n bookings.set(row.id, row); changes++;\n return reply(201, row);\n }\n if (!id && req.method === 'GET') {\n // Lists are the backend's job: each person sees their own bookings, at their own pizzeria.\n return reply(200, [...bookings.values()].filter((r) => r.owner === who && r.tenant === tenant));\n }\n const row = bookings.get(id);\n if (!row) return reply(404, { error: 'no such booking' });\n if (req.method === 'GET') return reply(200, row);\n if (req.method === 'DELETE') { bookings.delete(id); changes++; return reply(204); }\n return reply(405, { error: 'method not allowed' });\n}\n\nfunction startBackend(port = Number(process.env.BACKEND_PORT) || 3001) {\n const server = http.createServer((req, res) => {\n let body = '';\n req.on('data', (c) => { body += c; });\n req.on('end', () => {\n try { handle(req, res, body); } catch (e) { send(res, 500, { error: String((e && e.message) || e) }); }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startBackend, recent, changeCount: () => changes };\nif (require.main === module) startBackend().then(() => console.log('backend on http://localhost:3001'));\n",
|
|
6371
|
+
"app.js": "// ResiResi website server: serves the page and makes its calls.\n//\n// PROTECTED (the default): every call goes to the PUBLIC API \u2014 through APIblaze \u2014 with the\n// pizzeria's server key (kept here, never in the browser) and X-End-User-Id naming the\n// person. APIblaze checks the key, applies the rules and forwards to the backend.\n//\n// DIRECT (the page's \"unprotected\" switch): the same calls go straight to the backend on\n// localhost, with the shared backend secret and whatever user the page claims. No APIblaze:\n// nobody checks who may do what. That is how most apps ship.\n//\n// The chat and the two admin widgets use apiblaze's own server helpers\n// (lib/apiblaze-server.js, copied in by `npx apiblaze@latest demo` \u2014 the same code as\n// `import \u2026 from 'apiblaze/server'`).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst env = (name, fallback = '') => process.env[name] || fallback;\nconst json = (res, status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };\n\nlet lib = null;\ntry { lib = require('./lib/apiblaze-server.js'); } catch { /* run without the widgets */ }\n\n// The two pizzerias (tenants) and the people the demo lets you be. A real app takes\n// \"who is this\" from its own login.\nconst PIZZERIAS = {\n nino: { label: 'Nino Pizza', tenant: () => env('APIBLAZE_TENANT', env('DEMO_NINO_TENANT')), key: () => env('APIBLAZE_SERVER_KEY'), admin: { userId: 'ana', label: 'Ana', email: 'ana@ninopizza.example' } },\n gino: { label: 'Gino Pizza', tenant: () => env('APIBLAZE_GINO_TENANT', env('DEMO_GINO_TENANT')), key: () => env('APIBLAZE_GINO_KEY'), admin: { userId: 'gina', label: 'Gina', email: 'gina@ginopizza.example' } },\n};\n// Each pizzeria (tenant) has its own MCP address: https://{proxy}-{tenant}.mcp.\u2026\nconst portalFor = (tenant) => (env('APIBLAZE_PORTAL_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_PORTAL_URL').replace(`${env('APIBLAZE_TENANT')}.portal`, `${tenant}.portal`) : '');\nconst mcpFor = (tenant) => (env('APIBLAZE_MCP_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_MCP_URL').replace(`-${env('APIBLAZE_TENANT')}.`, `-${tenant}.`) : '');\nconst CUSTOMERS = { ana: 'Ana', ben: 'Ben' };\nconst pizzeriaOf = (id) => PIZZERIAS[id] || PIZZERIAS.nino;\nfunction queryOf(req) { return new URL(req.url, 'http://x').searchParams; }\n\nasync function call(op, { pizzeria = 'nino', as = 'ana', id, body, direct = false } = {}) {\n const pz = pizzeriaOf(pizzeria);\n const base = direct ? `http://127.0.0.1:${env('BACKEND_PORT', '3001')}` : env('APIBLAZE_URL').replace(/\\/$/, '');\n const headers = { 'content-type': 'application/json' };\n if (direct) {\n // Straight to localhost: the backend trusts whatever these headers say.\n headers['x-target-api-key'] = env('BACKEND_SECRET');\n headers['x-abz-user-id'] = as;\n headers['x-abz-tenant-id'] = pz.tenant();\n } else {\n headers['x-api-key'] = pz.key();\n headers['x-end-user-id'] = as;\n }\n const req = op === 'book' ? { method: 'POST', url: `${base}/reservations`, body: JSON.stringify({ name: CUSTOMERS[as] || as, ...(body || {}) }) }\n : op === 'cancel' ? { method: 'DELETE', url: `${base}/reservations/${encodeURIComponent(id || '')}` }\n : { method: 'GET', url: `${base}/reservations` };\n const r = await fetch(req.url, { method: req.method, headers, body: req.body });\n const text = await r.text();\n let data = null;\n try { data = text ? JSON.parse(text) : null; } catch { data = text; }\n return { status: r.status, body: data };\n}\n\n// Node http <-> the web-standard handlers apiblaze/server exports.\nasync function viaHandler(handler, req, res, raw) {\n const request = new Request(`http://localhost${req.url}`, {\n method: req.method,\n headers: { 'content-type': req.headers['content-type'] || 'application/json' },\n body: ['GET', 'HEAD'].includes(req.method) ? undefined : raw,\n });\n const out = await handler(request);\n const headers = {};\n out.headers.forEach((v, k) => { headers[k] = v; });\n res.writeHead(out.status, headers);\n if (!out.body) return res.end();\n const reader = out.body.getReader();\n for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }\n res.end();\n}\n\nfunction widgets() {\n if (!lib) return {};\n const out = { chat: {} };\n for (const [id, pz] of Object.entries(PIZZERIAS)) {\n if (!pz.tenant() || !pz.key()) continue;\n // The assistant acts as the signed-in customer, at that customer's pizzeria.\n out.chat[id] = lib.createApiblazeChat({\n project: `${env('APIBLAZE_NAME')}-${pz.tenant()}`, apiKey: pz.key(),\n host: 'tryabz.run', environment: 'dev', getUser: (req) => ({ userId: queryOf(req).get('as') || 'ana' }),\n });\n }\n if (env('APIBLAZE_CP_KEY') && env('APIBLAZE_WIDGETS') === '1') {\n // Admin widgets: the admin of the chosen pizzeria.\n const admin = (req) => { const pz = pizzeriaOf(queryOf(req).get('pizzeria')); return { tenant: pz.tenant(), ...pz.admin }; };\n out.keys = lib.createApiblazeKeys({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin, environment: 'dev' });\n out.groups = lib.createApiblazeGroups({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin });\n }\n return out;\n}\n\n/** One of the two websites: `direct` = straight to the backend (unprotected), else through APIblaze. */\nfunction startApp(port = Number(env('APP_PORT', '3000')), { direct = false } = {}) {\n const w = direct ? {} : widgets();\n const pub = path.join(__dirname, 'public');\n const server = http.createServer((req, res) => {\n let raw = '';\n req.on('data', (c) => { raw += c; });\n req.on('end', async () => {\n try {\n const url = new URL(req.url, 'http://x');\n const p = url.pathname;\n if (req.method === 'GET' && (p === '/' || p === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, 'index.html')));\n }\n for (const asset of ['apiblaze-widgets.js', 'apiblaze-logo.svg']) {\n if (req.method === 'GET' && p === `/${asset}` && fs.existsSync(path.join(pub, asset))) {\n res.writeHead(200, { 'content-type': asset.endsWith('.svg') ? 'image/svg+xml' : 'text/javascript; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, asset)));\n }\n }\n if (p === '/api/config') {\n return json(res, 200, {\n direct, apiUrl: env('APIBLAZE_URL'), portalUrl: env('APIBLAZE_PORTAL_URL'),\n backendPort: env('BACKEND_PORT', '3001'),\n protectedUrl: env('APIBLAZE_URL') ? `http://localhost:${env('APP_PORT', '3000')}` : '',\n directUrl: `http://localhost:${env('DIRECT_PORT', '3002')}`,\n logsCommand: `npx apiblaze@latest logs ${env('APIBLAZE_NAME')} --tenant ${env('APIBLAZE_TENANT')}`,\n pizzerias: Object.entries(PIZZERIAS).filter(([, pz]) => pz.tenant()).map(([id, pz]) => ({ id, label: pz.label, admin: pz.admin.label, mcpUrl: mcpFor(pz.tenant()), portalUrl: portalFor(pz.tenant()) })),\n widgets: { chat: Object.keys(w.chat || {}), keys: !!w.keys, groups: !!w.groups },\n });\n }\n if (p === '/api/changes') {\n // Local and free: has any booking changed since the page last listed? The page\n // re-lists through APIblaze only when it has (the backend runs in this process).\n let n = 0;\n try { n = require('./backend').changeCount(); } catch { /* backend runs elsewhere */ }\n return json(res, 200, { n });\n }\n if (p === '/api/apiblaze/chat') {\n const h = (w.chat || {})[url.searchParams.get('pizzeria') || 'nino'];\n if (h) return viaHandler(h.handler, req, res, raw);\n }\n if (p === '/api/apiblaze/keys' && w.keys) return viaHandler(w.keys.handler, req, res, raw);\n if (p === '/api/apiblaze/groups' && w.groups) return viaHandler(w.groups.handler, req, res, raw);\n if (req.method === 'POST' && p === '/api/act') {\n const b = JSON.parse(raw || '{}');\n return json(res, 200, await call(String(b.op || 'list'), { pizzeria: b.pizzeria, as: String(b.as || 'ana'), id: b.id, body: b.body, direct }));\n }\n json(res, 404, { error: 'not found' });\n } catch (e) {\n json(res, 502, { error: `Could not reach the API: ${String((e && e.message) || e)}` });\n }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startApp };\n",
|
|
6372
|
+
"start.js": "// Starts ResiResi on this laptop: the backend (3001), the unprotected front-end straight to it\n// (3002) and, once APIblaze is set up, the protected website through APIblaze (3000).\n// No APIblaze involved here \u2014 the tunnel is `npx apiblaze@latest demo start|stop|restart`.\n//\n// node start.js run in the foreground (Ctrl-C stops)\n// npm run start run in the background\n// npm run stop stop the background one\n// npm run restart stop, then start again\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst PID = path.join(__dirname, '.demo', 'app.pid');\n\nfunction loadEnv(file = path.join(__dirname, '.env.local')) {\n if (!fs.existsSync(file)) return;\n for (const line of fs.readFileSync(file, 'utf8').split('\\n')) {\n const m = /^\\s*([A-Z0-9_]+)\\s*=\\s*(.*)\\s*$/.exec(line);\n if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^\"(.*)\"$/, '$1');\n }\n}\n\nasync function start() {\n loadEnv();\n const { startBackend } = require('./backend');\n const { startApp } = require('./app');\n const backendPort = Number(process.env.BACKEND_PORT) || 3001;\n const appPort = Number(process.env.APP_PORT) || 3000;\n const directPort = Number(process.env.DIRECT_PORT) || 3002;\n await startBackend(backendPort);\n // The unprotected front-end: straight to the backend, no APIblaze.\n await startApp(directPort, { direct: true });\n // The protected website: through APIblaze (once the demo has written its keys).\n if (process.env.APIBLAZE_URL) await startApp(appPort);\n return { backendPort, appPort, directPort };\n}\n\nconst alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };\nfunction runningPid() {\n try { const pid = Number(fs.readFileSync(PID, 'utf8')); return pid && alive(pid) ? pid : null; } catch { return null; }\n}\nasync function stop() {\n const pid = runningPid();\n if (!pid) { console.log('ResiResi is not running.'); return; }\n process.kill(pid, 'SIGTERM');\n for (let i = 0; i < 30 && alive(pid); i++) await new Promise((r) => setTimeout(r, 100));\n try { fs.unlinkSync(PID); } catch { /* gone */ }\n console.log('Stopped ResiResi (backend and websites).');\n}\nfunction background() {\n if (runningPid()) { console.log('ResiResi is already running.'); return; }\n fs.mkdirSync(path.dirname(PID), { recursive: true });\n const log = fs.openSync(path.join(__dirname, '.demo', 'app.log'), 'a');\n const child = spawn(process.execPath, [__filename], { cwd: __dirname, detached: true, stdio: ['ignore', log, log] });\n child.unref();\n fs.writeFileSync(PID, String(child.pid));\n loadEnv();\n console.log(`Started ResiResi: backend localhost:${process.env.BACKEND_PORT || 3001}, unprotected front-end http://localhost:${process.env.DIRECT_PORT || 3002}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${process.env.APP_PORT || 3000}` : ''}.`);\n}\n\nmodule.exports = { start, loadEnv };\nif (require.main === module) {\n const arg = process.argv[2];\n if (arg === '--stop') stop();\n else if (arg === '--background') background();\n else if (arg === '--restart') stop().then(() => setTimeout(background, 300));\n else {\n start().then(({ appPort, backendPort, directPort }) => console.log(`ResiResi running: backend localhost:${backendPort}, unprotected front-end http://localhost:${directPort}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${appPort}` : ''}. Ctrl-C stops.`))\n .catch((e) => { console.error(`Could not start: ${e.message}`); process.exit(1); });\n }\n}\n",
|
|
6373
6373
|
"public/index.html": `<!doctype html>
|
|
6374
6374
|
<html lang="en">
|
|
6375
6375
|
<head>
|
|
6376
6376
|
<meta charset="utf-8">
|
|
6377
6377
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6378
|
-
<title>
|
|
6378
|
+
<title>ResiResi demo</title>
|
|
6379
6379
|
<style>
|
|
6380
6380
|
:root {
|
|
6381
6381
|
--bg: #faf7f2; --card: #fff; --ink: #1d1b19; --muted: #6b6560; --line: #e8e2d9; --soft: #f3eee6;
|
|
@@ -6443,25 +6443,27 @@ var init_files = __esm({
|
|
|
6443
6443
|
.next a { color: var(--accent); cursor: pointer; text-decoration: none; font-weight: 600; }
|
|
6444
6444
|
.next code { font-family: var(--mono); font-size: 13px; }
|
|
6445
6445
|
.stack { display: grid; gap: 16px; }
|
|
6446
|
+
button.copy { font-size: 11.5px; padding: 1px 8px; border-radius: 8px; border: 1px solid var(--line); background: var(--card); color: var(--ink); cursor: pointer; vertical-align: 1px; }
|
|
6447
|
+
button.copy:hover { border-color: var(--muted); }
|
|
6446
6448
|
</style>
|
|
6447
6449
|
</head>
|
|
6448
6450
|
<body>
|
|
6449
6451
|
<main>
|
|
6450
6452
|
<div class="mode" role="group" aria-label="How the website reaches the backend">
|
|
6451
6453
|
<button class="protected" id="mode-protected" aria-pressed="true">
|
|
6452
|
-
<img src="/apiblaze-logo.svg" alt="APIblaze"><span><b>Protected and enhanced by APIblaze</b><small>keys,
|
|
6454
|
+
<img src="/apiblaze-logo.svg" alt="APIblaze"><span><b>Protected and enhanced by APIblaze</b><small>on the internet, AI-ready (MCP), keys, sign-in, tenants, rules, limits</small></span>
|
|
6453
6455
|
</button>
|
|
6454
6456
|
<button class="direct" id="mode-direct" aria-pressed="false">
|
|
6455
|
-
<span class="icon">\u26A0</span><span><b>Direct to localhost, unprotected</b><small>
|
|
6457
|
+
<span class="icon">\u26A0</span><span><b>Direct to localhost, unprotected</b><small>this laptop only: no public address, no AI access, no keys, no rules</small></span>
|
|
6456
6458
|
</button>
|
|
6457
6459
|
</div>
|
|
6458
6460
|
<div class="live" id="live-protected">
|
|
6459
|
-
<div>API <code id="api">\u2026</code> <span class="arrow">\u2014 APIblaze tunnel \u2192</span> <code id="local">localhost</code></div>
|
|
6460
|
-
<div>MCP <code id="mcp">\u2026</code> <span>for AI assistants, at <span class="pz-name">Nino Pizza</span></span></div>
|
|
6461
|
-
<div>Dev portal <a id="portal-top" target="_blank" rel="noopener"><code id="portal-url">\u2026</code></a> <span>sign in, get a key, try the API</span></div>
|
|
6461
|
+
<div>API <code id="api">\u2026</code> <button class="copy" data-copy="#api">Copy</button> <span class="arrow">\u2014 APIblaze tunnel \u2192</span> <code id="local">localhost</code></div>
|
|
6462
|
+
<div>MCP <code id="mcp">\u2026</code> <button class="copy" data-copy="#mcp">Copy</button> <span>for AI assistants, at <span class="pz-name">Nino Pizza</span></span></div>
|
|
6463
|
+
<div>Dev portal <a id="portal-top" target="_blank" rel="noopener"><code id="portal-url">\u2026</code></a> <button class="copy" data-copy="#portal-url">Copy</button> <span>sign in, get a key, try the API</span></div>
|
|
6462
6464
|
</div>
|
|
6463
6465
|
<div class="live" id="live-direct" hidden>
|
|
6464
|
-
<div>Calling <code id="local2">localhost</code> directly with the backend secret.
|
|
6466
|
+
<div>Calling <code id="local2">localhost</code> <button class="copy" data-copy="#local2">Copy</button> directly with the backend secret. Only this laptop can reach it: no public address, no MCP for AI assistants, no API keys or sign-in, no tenants, rules or limits.</div>
|
|
6465
6467
|
</div>
|
|
6466
6468
|
|
|
6467
6469
|
<div class="bar">
|
|
@@ -6495,7 +6497,7 @@ var init_files = __esm({
|
|
|
6495
6497
|
</div>
|
|
6496
6498
|
<div class="card chatcard"><div id="chat"></div><div class="note" id="chat-off" hidden>The assistant only reaches your app through APIblaze (its MCP address). Switch back to <b>Protected</b> to chat.</div></div>
|
|
6497
6499
|
</div>
|
|
6498
|
-
<p class="next"><span id="foot-protected">Every request here reaches your laptop through APIblaze: <code id="logs-cmd">npx apiblaze@latest logs</code> shows them live.</span><span id="foot-direct" hidden>Every request here goes straight to <code class="local3">localhost</code
|
|
6500
|
+
<p class="next"><span id="foot-protected">Every request here reaches your laptop through APIblaze: <code id="logs-cmd">npx apiblaze@latest logs</code> shows them live.</span><span id="foot-direct" hidden>Every request here goes straight to <code class="local3">localhost</code>: nobody outside this laptop can reach it, AI assistants cannot use it, and nothing checks who is asking.</span> Next: <a id="go-admin">the admin side \u2192</a></p>
|
|
6499
6501
|
</section>
|
|
6500
6502
|
|
|
6501
6503
|
<!-- ADMIN: what each pizzeria's admin sees -->
|
|
@@ -6517,11 +6519,10 @@ const theme = { accent: '#e4572e' };
|
|
|
6517
6519
|
// Every switch (pizzeria, tab, person) is a fresh page load, the choice kept in the address:
|
|
6518
6520
|
// ?pizzeria=gino&as=ben#admin. Nothing half-mounted is ever left behind.
|
|
6519
6521
|
const params = new URLSearchParams(location.search);
|
|
6520
|
-
const state = { direct: false, pizzeria: params.get('pizzeria') || 'nino', tab: location.hash === '#admin' ? 'admin' : 'app' };
|
|
6522
|
+
const state = { direct: false, pizzeria: params.get('pizzeria') || 'nino', tab: (params.get('tab') || (location.hash === '#admin' ? 'admin' : 'app')) === 'admin' ? 'admin' : 'app' };
|
|
6521
6523
|
function reloadWith(next) {
|
|
6522
|
-
const p = new URLSearchParams({ pizzeria: next.pizzeria ?? state.pizzeria, as: next.as ?? $('#as').value });
|
|
6523
|
-
location.
|
|
6524
|
-
if ((next.tab ?? state.tab) !== state.tab && !next.pizzeria && !next.as) location.reload(); // a hash-only change does not reload by itself
|
|
6524
|
+
const p = new URLSearchParams({ pizzeria: next.pizzeria ?? state.pizzeria, as: next.as ?? $('#as').value, tab: next.tab ?? state.tab });
|
|
6525
|
+
location.assign(\`\${location.pathname}?\${p}\`);
|
|
6525
6526
|
}
|
|
6526
6527
|
let cfg = null;
|
|
6527
6528
|
const ready = fetch('/api/config').then((r) => r.json()).then((c) => { cfg = c; return c; });
|
|
@@ -6531,6 +6532,15 @@ const me = () => $('#as').value;
|
|
|
6531
6532
|
const pz = () => (cfg?.pizzerias || []).find((p) => p.id === state.pizzeria) || { label: 'Nino Pizza', admin: 'Ana' };
|
|
6532
6533
|
const anaBooking = {};
|
|
6533
6534
|
|
|
6535
|
+
// Copy buttons: every address on the page.
|
|
6536
|
+
document.querySelectorAll('button.copy').forEach((b) => b.onclick = async () => {
|
|
6537
|
+
const el = $(b.dataset.copy);
|
|
6538
|
+
const text = (el.textContent || '').trim().replace(/^localhost/, 'http://localhost');
|
|
6539
|
+
try { await navigator.clipboard.writeText(text); }
|
|
6540
|
+
catch { const r = document.createRange(); r.selectNodeContents(el); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); document.execCommand('copy'); sel.removeAllRanges(); }
|
|
6541
|
+
b.textContent = 'Copied'; setTimeout(() => { b.textContent = 'Copy'; }, 1200);
|
|
6542
|
+
});
|
|
6543
|
+
|
|
6534
6544
|
// \u2500\u2500 mode (which site this is), pizzeria, tab \u2500\u2500
|
|
6535
6545
|
// The two websites are two servers: the protected one through APIblaze and the unprotected one
|
|
6536
6546
|
// straight to the backend. The switch at the top goes from one to the other.
|
|
@@ -6540,7 +6550,7 @@ function setMode(direct) {
|
|
|
6540
6550
|
$('#live-protected').hidden = direct; $('#live-direct').hidden = !direct;
|
|
6541
6551
|
$('#foot-protected').hidden = direct; $('#foot-direct').hidden = !direct;
|
|
6542
6552
|
}
|
|
6543
|
-
function goTo(url) { if (url) location.
|
|
6553
|
+
function goTo(url) { if (url) location.assign(url + location.search); }
|
|
6544
6554
|
$('#mode-protected').onclick = () => { if (state.direct) goTo(cfg.protectedUrl); };
|
|
6545
6555
|
$('#mode-direct').onclick = () => { if (!state.direct) goTo(cfg.directUrl); };
|
|
6546
6556
|
function setTab(tab) {
|
|
@@ -6570,7 +6580,8 @@ async function refresh() {
|
|
|
6570
6580
|
const rows = Array.isArray(r.body) ? r.body : [];
|
|
6571
6581
|
if (me() === 'ana') anaBooking[state.pizzeria] = rows.length ? rows[0].id : null;
|
|
6572
6582
|
const ul = $('#list'); ul.replaceChildren();
|
|
6573
|
-
if (r.status
|
|
6583
|
+
if (r.status === 402) { const li = document.createElement('li'); li.className = 'empty'; li.textContent = 'This demo has used its free requests. For a fresh one, run: npx apiblaze@latest demo'; ul.append(li); }
|
|
6584
|
+
else if (r.status && r.status !== 200) { const li = document.createElement('li'); li.className = 'empty'; li.textContent = 'Could not load reservations (HTTP ' + r.status + ').'; ul.append(li); }
|
|
6574
6585
|
else if (!rows.length) { const li = document.createElement('li'); li.className = 'empty'; li.textContent = 'No reservations yet.'; ul.append(li); }
|
|
6575
6586
|
for (const b of rows) {
|
|
6576
6587
|
const li = document.createElement('li');
|
|
@@ -6599,7 +6610,16 @@ $('#ben-cancel').onclick = async () => {
|
|
|
6599
6610
|
};
|
|
6600
6611
|
$('#as').onchange = () => reloadWith({ as: $('#as').value });
|
|
6601
6612
|
if (params.get('as')) $('#as').value = params.get('as');
|
|
6602
|
-
|
|
6613
|
+
// The assistant's bookings appear here. Every 2 s the page asks this laptop (free) whether a
|
|
6614
|
+
// booking changed, and re-lists through APIblaze only when one did: an idle page spends nothing.
|
|
6615
|
+
let lastChange = null;
|
|
6616
|
+
setInterval(async () => {
|
|
6617
|
+
if (state.tab !== 'app' || document.hidden) return;
|
|
6618
|
+
const c = await fetch('/api/changes').then((x) => x.json()).catch(() => null);
|
|
6619
|
+
if (!c) return;
|
|
6620
|
+
if (lastChange !== null && c.n !== lastChange) refresh();
|
|
6621
|
+
lastChange = c.n;
|
|
6622
|
+
}, 2000);
|
|
6603
6623
|
|
|
6604
6624
|
// \u2500\u2500 chat \u2500\u2500
|
|
6605
6625
|
let chatKey = '';
|
|
@@ -7486,6 +7506,25 @@ function writeRegistry(rows) {
|
|
|
7486
7506
|
} catch {
|
|
7487
7507
|
}
|
|
7488
7508
|
}
|
|
7509
|
+
function registerDemo(dir, pid = 0) {
|
|
7510
|
+
writeRegistry([...readRegistry().filter((r) => r.dir !== dir), { dir, pid }]);
|
|
7511
|
+
}
|
|
7512
|
+
async function waitPorts(ports, ms2 = 1e4) {
|
|
7513
|
+
const listening = (port) => new Promise((resolve4) => {
|
|
7514
|
+
const sock = import_net.default.connect({ port, host: "127.0.0.1" });
|
|
7515
|
+
sock.once("connect", () => {
|
|
7516
|
+
sock.destroy();
|
|
7517
|
+
resolve4(true);
|
|
7518
|
+
});
|
|
7519
|
+
sock.once("error", () => resolve4(false));
|
|
7520
|
+
});
|
|
7521
|
+
const until = Date.now() + ms2;
|
|
7522
|
+
for (; ; ) {
|
|
7523
|
+
if ((await Promise.all(ports.map(listening))).every(Boolean)) return true;
|
|
7524
|
+
if (Date.now() > until) return false;
|
|
7525
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7489
7528
|
async function stopTunnels() {
|
|
7490
7529
|
const rows = readRegistry();
|
|
7491
7530
|
let stopped = 0;
|
|
@@ -7632,6 +7671,24 @@ async function waitThrough(apiUrl, serverKey) {
|
|
|
7632
7671
|
}
|
|
7633
7672
|
throw new DemoError(`The demo is running but ${B.product} cannot reach it yet (last answer ${last}).`, `Run again: ${LATEST} demo`);
|
|
7634
7673
|
}
|
|
7674
|
+
async function confirmStep(opts, question) {
|
|
7675
|
+
if (opts.yes || !interactive()) return true;
|
|
7676
|
+
console.log();
|
|
7677
|
+
return ask(`${import_chalk27.default.bold("?")} ${question} ${import_chalk27.default.dim("(Y/n)")} `);
|
|
7678
|
+
}
|
|
7679
|
+
async function busy(text, fn, minMs = 0) {
|
|
7680
|
+
if (!process.stdout.isTTY) return fn();
|
|
7681
|
+
const t = Date.now();
|
|
7682
|
+
const spinner = (0, import_ora10.default)({ text: import_chalk27.default.dim(text), indent: 3, discardStdin: false }).start();
|
|
7683
|
+
try {
|
|
7684
|
+
const out = await fn();
|
|
7685
|
+
const left = minMs - (Date.now() - t);
|
|
7686
|
+
if (left > 0) await new Promise((r) => setTimeout(r, left));
|
|
7687
|
+
return out;
|
|
7688
|
+
} finally {
|
|
7689
|
+
spinner.stop();
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7635
7692
|
async function ask(question) {
|
|
7636
7693
|
const readline3 = await import("readline");
|
|
7637
7694
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -7649,7 +7706,7 @@ function openBrowser3(url) {
|
|
|
7649
7706
|
}
|
|
7650
7707
|
}
|
|
7651
7708
|
async function offerBrowserAndLogs(dir, opts) {
|
|
7652
|
-
if (opts.open === false || !interactive()) return;
|
|
7709
|
+
if (opts.open === false || opts.yes || !interactive()) return;
|
|
7653
7710
|
const env = readEnvFile(import_path4.default.join(dir, ".env.local"));
|
|
7654
7711
|
const url = `http://localhost:${env.APP_PORT}`;
|
|
7655
7712
|
console.log();
|
|
@@ -7697,9 +7754,9 @@ async function runDemo(opts = {}) {
|
|
|
7697
7754
|
}
|
|
7698
7755
|
}
|
|
7699
7756
|
function printTable(rows) {
|
|
7700
|
-
const width = Math.max(100, Math.min(process.stdout.columns || 140,
|
|
7701
|
-
const leftW = 64;
|
|
7702
|
-
const rightW = Math.max(
|
|
7757
|
+
const width = Math.max(100, Math.min(process.stdout.columns || 140, 220));
|
|
7758
|
+
const leftW = Math.min(64, Math.max(...rows.map((r) => `\u2713 ${r.label.padEnd(17)} ${r.what}`.length)));
|
|
7759
|
+
const rightW = Math.max(40, width - leftW - 8);
|
|
7703
7760
|
const wrap2 = (text) => {
|
|
7704
7761
|
const out = [];
|
|
7705
7762
|
let line = "";
|
|
@@ -7713,13 +7770,13 @@ function printTable(rows) {
|
|
|
7713
7770
|
return out;
|
|
7714
7771
|
};
|
|
7715
7772
|
const head = (l, r) => console.log(` ${import_chalk27.default.dim(l.padEnd(leftW))} ${import_chalk27.default.dim(r)}`);
|
|
7716
|
-
head(
|
|
7773
|
+
head(`What ${B.product} set up`, "The command that does the same");
|
|
7717
7774
|
head("\u2500".repeat(leftW), "\u2500".repeat(Math.min(rightW, 60)));
|
|
7718
7775
|
for (const r of rows) {
|
|
7719
7776
|
const left = `${import_chalk27.default.green("\u2713")} ${import_chalk27.default.bold(r.label.padEnd(17))} ${r.what}`;
|
|
7720
7777
|
const leftPlain = `\u2713 ${r.label.padEnd(17)} ${r.what}`;
|
|
7721
7778
|
const right = r.cmds.length ? r.cmds.flatMap(wrap2) : ["\u2014"];
|
|
7722
|
-
console.log(` ${left}${" ".repeat(Math.max(
|
|
7779
|
+
console.log(` ${left}${" ".repeat(Math.max(0, leftW - leftPlain.length))} ${import_chalk27.default.cyan(right[0])}`);
|
|
7723
7780
|
for (const extra of right.slice(1)) console.log(` ${" ".repeat(leftW)} ${import_chalk27.default.cyan(extra)}`);
|
|
7724
7781
|
}
|
|
7725
7782
|
}
|
|
@@ -7731,6 +7788,10 @@ async function startDemo(opts) {
|
|
|
7731
7788
|
const cdDir = rel === "." ? "" : `cd ${rel} && `;
|
|
7732
7789
|
const shell = (c, note = "") => console.log(` ${import_chalk27.default.dim("$")} ${import_chalk27.default.cyan(c)}${note ? ` ${import_chalk27.default.dim(note)}` : ""}`);
|
|
7733
7790
|
const ok = (text, note = "") => console.log(` ${import_chalk27.default.green("\u2713")} ${text}${note ? ` ${import_chalk27.default.dim(note)}` : ""}`);
|
|
7791
|
+
const stopHere = (lines) => {
|
|
7792
|
+
console.log();
|
|
7793
|
+
for (const l of lines) console.log(` ${l}`);
|
|
7794
|
+
};
|
|
7734
7795
|
await stopDemos();
|
|
7735
7796
|
writeApp(dir);
|
|
7736
7797
|
const appPort = await freePort(3e3);
|
|
@@ -7740,15 +7801,18 @@ async function startDemo(opts) {
|
|
|
7740
7801
|
const ninoTenant = `ninopizza${import_crypto.default.randomBytes(2).toString("hex")}`;
|
|
7741
7802
|
const backendSecret = import_crypto.default.randomBytes(18).toString("base64url");
|
|
7742
7803
|
const openapi = DEMO_FILES["openapi.yaml"];
|
|
7743
|
-
const L = (c) => c
|
|
7804
|
+
const L = (c) => `${LATEST} ${c}`;
|
|
7744
7805
|
writeEnv(dir, { BACKEND_SECRET: backendSecret, DEMO_NINO_TENANT: ninoTenant, APP_PORT: appPort, BACKEND_PORT: backendPort, DIRECT_PORT: directPort });
|
|
7745
7806
|
console.log();
|
|
7746
|
-
console.log(import_chalk27.default.bold(
|
|
7747
|
-
console.log(
|
|
7748
|
-
for (const
|
|
7807
|
+
console.log(import_chalk27.default.bold("1. The app: ResiResi, a table booking app for Tenants (Nino and Gino Pizzas) to allow end-users to book tables in their restaurants") + import_chalk27.default.dim(` (no ${B.product} yet)`));
|
|
7808
|
+
console.log(` Written to ${rel}. Its routes, from openapi.yaml:`);
|
|
7809
|
+
for (const r of routesOf(openapi)) console.log(` ${r.method.padEnd(7)}${r.path.padEnd(21)}${import_chalk27.default.dim(r.summary)}`);
|
|
7749
7810
|
console.log(import_chalk27.default.dim(" No login, no API keys, no permission checks in its code. It only checks one shared backend secret (in .env.local)."));
|
|
7750
7811
|
shell(`${cdDir}npm run start`, "npm run stop \xB7 npm run restart");
|
|
7751
|
-
if (!appCtl(dir, "--background")) throw new DemoError("The
|
|
7812
|
+
if (!appCtl(dir, "--background")) throw new DemoError("The ResiResi app could not start.", `Run again: ${LATEST} demo`);
|
|
7813
|
+
registerDemo(dir);
|
|
7814
|
+
const appUp = await busy("Starting the reservation backend and its front-end\u2026", () => waitPorts([backendPort, directPort]), 1800);
|
|
7815
|
+
if (!appUp) throw new DemoError(`The ResiResi app did not start (see ${import_path4.default.join(rel, STATE_DIR, "app.log")}).`, `Run again: ${LATEST} demo`);
|
|
7752
7816
|
ok(`${import_chalk27.default.bold("Reservation backend")}: localhost:${backendPort}`, "unprotected");
|
|
7753
7817
|
ok(`${import_chalk27.default.bold("Unprotected reservation front-end")}, going directly to the reservation backend: ${import_chalk27.default.bold(`http://localhost:${directPort}`)}`);
|
|
7754
7818
|
console.log(" Book a table directly, the way the unprotected front-end does:");
|
|
@@ -7756,40 +7820,55 @@ async function startDemo(opts) {
|
|
|
7756
7820
|
console.log(import_chalk27.default.cyan(` -H "x-target-api-key: $(grep BACKEND_SECRET ${import_path4.default.join(rel, ".env.local")} | cut -d= -f2)" \\`));
|
|
7757
7821
|
console.log(import_chalk27.default.cyan(` -H "x-abz-user-id: ana" -H "x-abz-tenant-id: ${ninoTenant}" \\`));
|
|
7758
7822
|
console.log(import_chalk27.default.cyan(` -H "content-type: application/json" -d '{"table":4,"time":"19:30","guests":2}'`));
|
|
7823
|
+
if (!await confirmStep(opts, `Ok to put an ${B.product} proxy and MCP in front of the reservation backend?`)) {
|
|
7824
|
+
return stopHere([
|
|
7825
|
+
`Stopped before ${B.product}. ResiResi keeps running on this laptop: http://localhost:${directPort} ${import_chalk27.default.dim(`(${cdDir}npm run stop stops it)`)}`,
|
|
7826
|
+
`Run the whole demo again any time: ${LATEST} demo`
|
|
7827
|
+
]);
|
|
7828
|
+
}
|
|
7759
7829
|
console.log();
|
|
7760
7830
|
console.log(import_chalk27.default.bold(`2. ${B.product}, in front of it`));
|
|
7761
7831
|
const tCreate = Date.now();
|
|
7762
|
-
const
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7832
|
+
const setup = await busy(`Creating the proxy, its tenants, admins, rules and limits\u2026`, async () => {
|
|
7833
|
+
const r = await createProxy2(name, backendPort, openapi, ninoTenant);
|
|
7834
|
+
const cpKey2 = r.cp_key;
|
|
7835
|
+
const teamId2 = r.team_id;
|
|
7836
|
+
const keys = r.api_keys ?? {};
|
|
7837
|
+
const serverKey2 = r.apiKey ?? keys.dev ?? Object.values(keys)[0];
|
|
7838
|
+
const project2 = r.project_id ?? name;
|
|
7839
|
+
const version3 = r.api_version || "1.0.0";
|
|
7840
|
+
const rr = r;
|
|
7841
|
+
const tenant3 = rr.tenant ?? tenantFromPortal(rr.portal ?? rr.devPortal) ?? "";
|
|
7842
|
+
const missing = [!cpKey2 && "workspace key", !teamId2 && "team", !serverKey2 && "API key", !tenant3 && "tenant"].filter(Boolean);
|
|
7843
|
+
if (missing.length) throw new DemoError(`${B.product} did not return everything the demo needs (${missing.join(", ")}).`, `Run again: ${LATEST} demo`);
|
|
7844
|
+
const createSecs2 = ((Date.now() - tCreate) / 1e3).toFixed(1);
|
|
7845
|
+
adoptWorkspace(cpKey2, teamId2, rr.claim_code);
|
|
7846
|
+
const cp = (p, init) => cpFetch(cpKey2, p, init).then(() => true).catch(() => false);
|
|
7847
|
+
const gino2 = (rr.tenants ?? []).find((t) => t.api_key);
|
|
7848
|
+
const [adminOk2, ginaOk2, { limited: limited2, secretOk }] = await Promise.all([
|
|
7849
|
+
cp(`/teams/${encodeURIComponent(teamId2)}/tenants/${encodeURIComponent(tenant3)}/admin-emails`, { method: "POST", body: JSON.stringify({ email: DEMO_ADMIN_EMAIL }) }),
|
|
7850
|
+
gino2?.tenant ? cp(`/teams/${encodeURIComponent(teamId2)}/tenants/${encodeURIComponent(gino2.tenant)}/admin-emails`, { method: "POST", body: JSON.stringify({ email: DEMO_GINO_ADMIN_EMAIL }) }) : Promise.resolve(false),
|
|
7851
|
+
(async () => {
|
|
7852
|
+
const lim = await cp(`/projects/${encodeURIComponent(project2)}/${encodeURIComponent(version3)}/limits`, { method: "PUT", body: JSON.stringify({ consumerRps: DEMO_RPS, endUserRps: DEMO_RPS }) });
|
|
7853
|
+
const sec = await cp(`/projects/${encodeURIComponent(project2)}/${encodeURIComponent(version3)}/transforms`, {
|
|
7854
|
+
method: "POST",
|
|
7855
|
+
body: JSON.stringify({
|
|
7856
|
+
name: "set header x-target-api-key",
|
|
7857
|
+
phase: "request",
|
|
7858
|
+
enabled: true,
|
|
7859
|
+
action: { type: "hardcode", destination: "header:x-target-api-key", value: backendSecret, value_secret: true }
|
|
7860
|
+
})
|
|
7861
|
+
});
|
|
7862
|
+
return { limited: lim, secretOk: sec };
|
|
7863
|
+
})()
|
|
7864
|
+
]);
|
|
7865
|
+
if (!secretOk) throw new DemoError(`Could not give ${B.product} the backend secret.`, `Run again: ${LATEST} demo`);
|
|
7866
|
+
return { cpKey: cpKey2, teamId: teamId2, serverKey: serverKey2, project: project2, version: version3, tenant: tenant3, gino: gino2, createSecs: createSecs2, adminOk: adminOk2, ginaOk: ginaOk2, limited: limited2, claimCode: rr.claim_code, applied: rr.secure?.applied ?? [] };
|
|
7867
|
+
});
|
|
7868
|
+
const { cpKey, teamId, serverKey, project, version: version2, tenant: tenant2, gino, createSecs, adminOk, ginaOk, limited } = setup;
|
|
7776
7869
|
const apiUrl = restUrl(project, version2, "dev", { anon: true });
|
|
7777
|
-
const gino = (rr.tenants ?? []).find((t) => t.api_key);
|
|
7778
7870
|
const agents = mcpUrl(project, version2, "dev", { tenant: tenant2, anon: true });
|
|
7779
|
-
const
|
|
7780
|
-
const ginaOk = gino?.tenant ? await cp(`/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(gino.tenant)}/admin-emails`, { method: "POST", body: JSON.stringify({ email: DEMO_GINO_ADMIN_EMAIL }) }) : false;
|
|
7781
|
-
const limited = await cp(`/projects/${encodeURIComponent(project)}/${encodeURIComponent(version2)}/limits`, { method: "PUT", body: JSON.stringify({ consumerRps: DEMO_RPS, endUserRps: DEMO_RPS }) });
|
|
7782
|
-
const secretOk = await cp(`/projects/${encodeURIComponent(project)}/${encodeURIComponent(version2)}/transforms`, {
|
|
7783
|
-
method: "POST",
|
|
7784
|
-
body: JSON.stringify({
|
|
7785
|
-
name: "set header x-target-api-key",
|
|
7786
|
-
phase: "request",
|
|
7787
|
-
enabled: true,
|
|
7788
|
-
action: { type: "hardcode", destination: "header:x-target-api-key", value: backendSecret, value_secret: true }
|
|
7789
|
-
})
|
|
7790
|
-
});
|
|
7791
|
-
if (!secretOk) throw new DemoError(`Could not give ${B.product} the backend secret.`, `Run again: ${LATEST} demo`);
|
|
7792
|
-
const applied = (rr.secure?.applied ?? []).map((k) => k.replace(/^\//, "").split("/")[0]);
|
|
7871
|
+
const applied = setup.applied.map((k) => k.replace(/^\//, "").split("/")[0]);
|
|
7793
7872
|
const tools = routesOf(openapi).map((x) => x.op).filter(Boolean);
|
|
7794
7873
|
writeEnv(dir, {
|
|
7795
7874
|
APIBLAZE_NAME: project,
|
|
@@ -7808,7 +7887,7 @@ async function startDemo(opts) {
|
|
|
7808
7887
|
BACKEND_PORT: backendPort,
|
|
7809
7888
|
DIRECT_PORT: directPort
|
|
7810
7889
|
});
|
|
7811
|
-
const state = { name: project, version: version2, tenant: tenant2, teamId, cpKey, appPort, backendPort, apiUrl, claimCode:
|
|
7890
|
+
const state = { name: project, version: version2, tenant: tenant2, teamId, cpKey, appPort, backendPort, apiUrl, claimCode: setup.claimCode };
|
|
7812
7891
|
saveState(dir, state);
|
|
7813
7892
|
printTable([
|
|
7814
7893
|
{ label: "Proxy", what: `${project} (${createSecs} s, no account)`, cmds: [L(`dev --port ${backendPort} --openapi ./openapi.yaml --name ${name} --tenant ${ninoTenant} --auto`)] },
|
|
@@ -7828,18 +7907,36 @@ async function startDemo(opts) {
|
|
|
7828
7907
|
{ label: "Backend secret", what: "added to every call APIblaze forwards", cmds: [L(`transform ${project} set-header x-target-api-key --value-env BACKEND_SECRET --secret`)] },
|
|
7829
7908
|
{ label: "Keys", what: "saved to .env.local, never printed", cmds: [] }
|
|
7830
7909
|
]);
|
|
7910
|
+
if (!await confirmStep(opts, `Ok to create a tunnel allowing ${project} to forward requests to localhost:${backendPort}?`)) {
|
|
7911
|
+
return stopHere([
|
|
7912
|
+
`Stopped before the tunnel. The proxy exists but cannot reach this laptop yet.`,
|
|
7913
|
+
`Open the tunnel any time: ${LATEST} demo start`
|
|
7914
|
+
]);
|
|
7915
|
+
}
|
|
7831
7916
|
console.log();
|
|
7832
7917
|
console.log(import_chalk27.default.bold("3. Live on the internet"));
|
|
7833
7918
|
shell(`${LATEST} dev --port ${backendPort} --project ${project} --auto`, "the tunnel, in the background");
|
|
7834
|
-
await
|
|
7835
|
-
|
|
7919
|
+
await busy(`Opening the tunnel to localhost:${backendPort} and checking it with a real call\u2026`, async () => {
|
|
7920
|
+
await spawnTunnel(dir, state);
|
|
7921
|
+
await waitThrough(apiUrl, serverKey);
|
|
7922
|
+
}, 1200);
|
|
7836
7923
|
ok(`${import_chalk27.default.bold("Tunnel")}: the public address reaches localhost:${backendPort} on this laptop`, `checked with a real call \xB7 ${Math.round((Date.now() - t0) / 1e3)} s so far`);
|
|
7837
7924
|
printAddresses(dir);
|
|
7925
|
+
if (!await confirmStep(opts, "Ok to launch the protected website, which will use the proxy to reach the backend?")) {
|
|
7926
|
+
return stopHere([
|
|
7927
|
+
`Stopped before the protected website. The API and MCP addresses above work now.`,
|
|
7928
|
+
`Start the website any time: ${cdDir}npm run restart`
|
|
7929
|
+
]);
|
|
7930
|
+
}
|
|
7838
7931
|
console.log();
|
|
7839
7932
|
console.log(import_chalk27.default.bold(`4. The protected website, through ${B.product}`));
|
|
7840
7933
|
shell(`${cdDir}npm run restart`, "picks up the keys in .env.local");
|
|
7841
|
-
|
|
7842
|
-
|
|
7934
|
+
const siteUp = await busy("Starting the protected website\u2026", async () => {
|
|
7935
|
+
appCtl(dir, "--stop");
|
|
7936
|
+
appCtl(dir, "--background");
|
|
7937
|
+
return waitPorts([appPort, directPort, backendPort]);
|
|
7938
|
+
}, 1500);
|
|
7939
|
+
if (!siteUp) throw new DemoError(`The protected website did not start (see ${import_path4.default.join(rel, STATE_DIR, "app.log")}).`, `Try: ${cdDir}npm run restart`);
|
|
7843
7940
|
ok(`${import_chalk27.default.bold("Protected reservation website")}, going through the ${B.product} proxy ${apiUrl}: ${import_chalk27.default.bold(`http://localhost:${appPort}`)}`);
|
|
7844
7941
|
ok(`${import_chalk27.default.bold("Unprotected reservation front-end")}, still going directly to the backend: http://localhost:${directPort}`, "the switch at the top of the page goes between them");
|
|
7845
7942
|
console.log();
|
|
@@ -7858,7 +7955,7 @@ async function spawnTunnel(dir, state) {
|
|
|
7858
7955
|
const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "demo", "--serve", dir], { cwd: dir, detached: true, stdio: ["ignore", log4, log4] });
|
|
7859
7956
|
child.unref();
|
|
7860
7957
|
saveState(dir, { ...state, pid: child.pid, ready: false, error: void 0 });
|
|
7861
|
-
|
|
7958
|
+
registerDemo(dir, child.pid ?? 0);
|
|
7862
7959
|
await waitTunnel(dir);
|
|
7863
7960
|
}
|
|
7864
7961
|
async function runDemoStart(opts = {}) {
|
|
@@ -7883,7 +7980,7 @@ async function runDemoStart(opts = {}) {
|
|
|
7883
7980
|
console.log(`${import_chalk27.default.green("\u2713")} Tunnel open: the public address reaches localhost:${env.BACKEND_PORT} again ${import_chalk27.default.dim(`(${Math.round((Date.now() - t0) / 1e3)} s)`)}`);
|
|
7884
7981
|
printAddresses(dir);
|
|
7885
7982
|
const appUp = await fetch(`http://127.0.0.1:${env.APP_PORT}/api/config`).then(() => true).catch(() => false);
|
|
7886
|
-
if (!appUp) console.log(import_chalk27.default.yellow(`
|
|
7983
|
+
if (!appUp) console.log(import_chalk27.default.yellow(` ResiResi is not running: cd ${import_path4.default.relative(process.cwd(), dir) || "."} && npm run start`));
|
|
7887
7984
|
await offerBrowserAndLogs(dir, opts);
|
|
7888
7985
|
}
|
|
7889
7986
|
async function runDemoLogs() {
|
|
@@ -7899,7 +7996,7 @@ async function runDemoLogs() {
|
|
|
7899
7996
|
}
|
|
7900
7997
|
async function runDemoStop() {
|
|
7901
7998
|
const n = await stopTunnels();
|
|
7902
|
-
console.log(n ? `${import_chalk27.default.green("\u2713")} Stopped the tunnel.
|
|
7999
|
+
console.log(n ? `${import_chalk27.default.green("\u2713")} Stopped the tunnel. ResiResi still runs on this laptop (npm run stop stops it).` : "No demo tunnel is running.");
|
|
7903
8000
|
}
|
|
7904
8001
|
async function serveTunnel(dir) {
|
|
7905
8002
|
const state = loadState(dir);
|
|
@@ -7957,7 +8054,7 @@ async function serveTunnel(dir) {
|
|
|
7957
8054
|
await new Promise(() => {
|
|
7958
8055
|
});
|
|
7959
8056
|
}
|
|
7960
|
-
var import_fs5, import_path4, import_net, import_child_process4, import_crypto, import_chalk27, DIR_NAME, STATE_DIR, LATEST, DEMO_RPS, DEMO_ADMIN_EMAIL, DEMO_GINO_ADMIN_EMAIL, RULE_SENTENCE, DemoError, ADJ, NOUN, pick2, registryFile, alive, interactive, logsCommand;
|
|
8057
|
+
var import_fs5, import_path4, import_net, import_child_process4, import_crypto, import_chalk27, import_ora10, DIR_NAME, STATE_DIR, LATEST, DEMO_RPS, DEMO_ADMIN_EMAIL, DEMO_GINO_ADMIN_EMAIL, RULE_SENTENCE, DemoError, ADJ, NOUN, pick2, registryFile, alive, interactive, logsCommand;
|
|
7961
8058
|
var init_demo = __esm({
|
|
7962
8059
|
"src/commands/demo.ts"() {
|
|
7963
8060
|
"use strict";
|
|
@@ -7967,6 +8064,7 @@ var init_demo = __esm({
|
|
|
7967
8064
|
import_child_process4 = require("child_process");
|
|
7968
8065
|
import_crypto = __toESM(require("crypto"));
|
|
7969
8066
|
import_chalk27 = __toESM(require("chalk"));
|
|
8067
|
+
import_ora10 = __toESM(require("ora"));
|
|
7970
8068
|
init_api();
|
|
7971
8069
|
init_tunnel_client();
|
|
7972
8070
|
init_auth();
|
|
@@ -8083,7 +8181,7 @@ async function addApiblazeHostedLogin(teamId, tenant2, opts = {}) {
|
|
|
8083
8181
|
if (a.callbacks.trim()) clientBody.authorizedCallbackUrls = a.callbacks.split(/\s*,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
8084
8182
|
if (a.scopes.trim() && opt.own) providerBody.scopes = a.scopes.split(/\s+/).filter(Boolean);
|
|
8085
8183
|
}
|
|
8086
|
-
const spinner = (0,
|
|
8184
|
+
const spinner = (0, import_ora21.default)("Setting up login...").start();
|
|
8087
8185
|
try {
|
|
8088
8186
|
const client = await admin({ method: "POST", path: `${base2}/app-clients`, body: clientBody, summary: `Create login for ${tenant2}` });
|
|
8089
8187
|
const clientId = client?.clientId ?? client?.client_id;
|
|
@@ -8113,12 +8211,12 @@ async function createTenantInteractive(teamId) {
|
|
|
8113
8211
|
await addApiblazeHostedLogin(teamId, tenant2, { allowSkip: true });
|
|
8114
8212
|
return tenant2;
|
|
8115
8213
|
}
|
|
8116
|
-
var import_chalk51,
|
|
8214
|
+
var import_chalk51, import_ora21, PROVIDERS2, DEFAULT_SCOPES;
|
|
8117
8215
|
var init_tenant_create = __esm({
|
|
8118
8216
|
"src/lib/tenant-create.ts"() {
|
|
8119
8217
|
"use strict";
|
|
8120
8218
|
import_chalk51 = __toESM(require("chalk"));
|
|
8121
|
-
|
|
8219
|
+
import_ora21 = __toESM(require("ora"));
|
|
8122
8220
|
init_admin();
|
|
8123
8221
|
PROVIDERS2 = [
|
|
8124
8222
|
{ id: "apiblaze", label: "APIblaze (via GitHub) \u2014 zero setup, recommended", own: false },
|
|
@@ -8167,7 +8265,7 @@ async function pickTenant(teamId, opts = {}) {
|
|
|
8167
8265
|
const active = loadCredentials()?.activeTenant;
|
|
8168
8266
|
let q = opts.initialQuery ?? "";
|
|
8169
8267
|
for (; ; ) {
|
|
8170
|
-
const spinner = (0,
|
|
8268
|
+
const spinner = (0, import_ora22.default)(q ? `Searching tenants for "${q}"...` : "Loading tenants...").start();
|
|
8171
8269
|
const page = await fetchPage2(teamId, q).finally(() => spinner.stop());
|
|
8172
8270
|
if (!page.total && !q) {
|
|
8173
8271
|
if (opts.allowCreate) {
|
|
@@ -8217,12 +8315,12 @@ async function createTenantInline(teamId) {
|
|
|
8217
8315
|
const { createTenantInteractive: createTenantInteractive2 } = await Promise.resolve().then(() => (init_tenant_create(), tenant_create_exports));
|
|
8218
8316
|
return createTenantInteractive2(teamId);
|
|
8219
8317
|
}
|
|
8220
|
-
var import_chalk52,
|
|
8318
|
+
var import_chalk52, import_ora22, PAGE;
|
|
8221
8319
|
var init_tenant_pick = __esm({
|
|
8222
8320
|
"src/lib/tenant-pick.ts"() {
|
|
8223
8321
|
"use strict";
|
|
8224
8322
|
import_chalk52 = __toESM(require("chalk"));
|
|
8225
|
-
|
|
8323
|
+
import_ora22 = __toESM(require("ora"));
|
|
8226
8324
|
init_admin();
|
|
8227
8325
|
init_auth();
|
|
8228
8326
|
PAGE = 15;
|
|
@@ -9591,7 +9689,7 @@ async function runWhoami(opts = {}) {
|
|
|
9591
9689
|
|
|
9592
9690
|
// src/commands/team.ts
|
|
9593
9691
|
var import_chalk30 = __toESM(require("chalk"));
|
|
9594
|
-
var
|
|
9692
|
+
var import_ora11 = __toESM(require("ora"));
|
|
9595
9693
|
init_auth();
|
|
9596
9694
|
init_api();
|
|
9597
9695
|
init_admin();
|
|
@@ -9620,7 +9718,7 @@ async function createAndActivate(creds, name) {
|
|
|
9620
9718
|
if (!creds.apiblazeUserId) {
|
|
9621
9719
|
fail5("Your login is missing its user id.", "Run `apiblaze login` again, then retry.");
|
|
9622
9720
|
}
|
|
9623
|
-
const spinner = (0,
|
|
9721
|
+
const spinner = (0, import_ora11.default)(`Creating team "${name}"...`).start();
|
|
9624
9722
|
let created;
|
|
9625
9723
|
try {
|
|
9626
9724
|
created = await admin({
|
|
@@ -10032,7 +10130,7 @@ async function runAuthz(projectArg, apiVersionArg) {
|
|
|
10032
10130
|
|
|
10033
10131
|
// src/commands/rule.ts
|
|
10034
10132
|
var import_chalk35 = __toESM(require("chalk"));
|
|
10035
|
-
var
|
|
10133
|
+
var import_ora12 = __toESM(require("ora"));
|
|
10036
10134
|
init_auth();
|
|
10037
10135
|
init_api();
|
|
10038
10136
|
init_create();
|
|
@@ -10054,7 +10152,7 @@ async function runRule(projectArg, ruleText, opts) {
|
|
|
10054
10152
|
if (!rule) return proposeFromSpecBranch(ctx, opts);
|
|
10055
10153
|
const { ids: sampleIds, total: sampleTotal } = await gatherSampleIds(projectId, apiVersion);
|
|
10056
10154
|
maybeWarnNoTraffic(sampleTotal, projectArg);
|
|
10057
|
-
const spinner = (0,
|
|
10155
|
+
const spinner = (0, import_ora12.default)(sampleIds.length ? `Designing the rule from ${sampleIds.length} traffic sample(s)\u2026` : "Designing the rule\u2026").start();
|
|
10058
10156
|
const messages = [{
|
|
10059
10157
|
role: "user",
|
|
10060
10158
|
content: `Author this authorization rule and generate the COMPLETE proposal (OpenFGA model + per-route rules) NOW for every affected route. Do not ask clarifying questions; make reasonable assumptions and state them briefly. Rule: "${rule}"`
|
|
@@ -10082,7 +10180,7 @@ async function runRule(projectArg, ruleText, opts) {
|
|
|
10082
10180
|
async function proposeFromSpecBranch(ctx, opts) {
|
|
10083
10181
|
const { projectId, apiVersion, projectArg } = ctx;
|
|
10084
10182
|
const interactive2 = !!process.stdin.isTTY;
|
|
10085
|
-
const spinner = (0,
|
|
10183
|
+
const spinner = (0, import_ora12.default)("Reading the spec for things to protect\u2026").start();
|
|
10086
10184
|
const first = await agentCall(`/projects/${projectId}/${apiVersion}/authz/propose`, "POST", {});
|
|
10087
10185
|
if (first.status >= 400) {
|
|
10088
10186
|
spinner.fail(`Could not work out what to protect (${first.status}): ${first.data?.details ?? first.data?.error ?? ""}`);
|
|
@@ -10499,7 +10597,7 @@ async function runMcpUi(project, tool, opts) {
|
|
|
10499
10597
|
|
|
10500
10598
|
// src/commands/delete.ts
|
|
10501
10599
|
var import_chalk39 = __toESM(require("chalk"));
|
|
10502
|
-
var
|
|
10600
|
+
var import_ora13 = __toESM(require("ora"));
|
|
10503
10601
|
init_admin();
|
|
10504
10602
|
init_resolve();
|
|
10505
10603
|
|
|
@@ -10590,7 +10688,7 @@ async function settleDelete(accepted, opts) {
|
|
|
10590
10688
|
async function runDelete(project, version2, opts) {
|
|
10591
10689
|
const { teamId } = await resolveTeam(opts.team);
|
|
10592
10690
|
const proj2 = await resolveProject(teamId, project, version2);
|
|
10593
|
-
const spinner = (0,
|
|
10691
|
+
const spinner = (0, import_ora13.default)("Checking delete impact...").start();
|
|
10594
10692
|
let impact;
|
|
10595
10693
|
try {
|
|
10596
10694
|
impact = await admin({
|
|
@@ -10625,7 +10723,7 @@ async function runDelete(project, version2, opts) {
|
|
|
10625
10723
|
}
|
|
10626
10724
|
}
|
|
10627
10725
|
}
|
|
10628
|
-
const s2 = (0,
|
|
10726
|
+
const s2 = (0, import_ora13.default)("Deleting...").start();
|
|
10629
10727
|
let accepted;
|
|
10630
10728
|
try {
|
|
10631
10729
|
accepted = await admin({
|
|
@@ -10751,7 +10849,7 @@ async function runExport(projectArg, versionArg, opts) {
|
|
|
10751
10849
|
|
|
10752
10850
|
// src/commands/recipe-search.ts
|
|
10753
10851
|
var import_chalk42 = __toESM(require("chalk"));
|
|
10754
|
-
var
|
|
10852
|
+
var import_ora14 = __toESM(require("ora"));
|
|
10755
10853
|
init_caller();
|
|
10756
10854
|
|
|
10757
10855
|
// src/lib/recipes.ts
|
|
@@ -10937,7 +11035,7 @@ function upstreamHosts(recipe) {
|
|
|
10937
11035
|
|
|
10938
11036
|
// src/commands/recipe-search.ts
|
|
10939
11037
|
async function runRecipeSearch(query, opts) {
|
|
10940
|
-
const spinner = !opts.json ? (0,
|
|
11038
|
+
const spinner = !opts.json ? (0, import_ora14.default)("Searching recipes...").start() : null;
|
|
10941
11039
|
let hits;
|
|
10942
11040
|
try {
|
|
10943
11041
|
hits = await searchRecipes(query ?? "", opts.limit ? Number(opts.limit) : void 0);
|
|
@@ -10972,7 +11070,7 @@ async function runRecipeShow(nameArg, opts) {
|
|
|
10972
11070
|
const wantsSection = !!(opts.transforms || opts.settings || opts.auth || opts.openapi);
|
|
10973
11071
|
const caller = resolveCaller();
|
|
10974
11072
|
const head = ref.revision === void 0 ? await getRecipeHead(ref).catch(() => null) : null;
|
|
10975
|
-
const spinner = !opts.json ? (0,
|
|
11073
|
+
const spinner = !opts.json ? (0, import_ora14.default)(`Reading ${ref.name}...`).start() : null;
|
|
10976
11074
|
let recipe;
|
|
10977
11075
|
try {
|
|
10978
11076
|
recipe = await fetchRecipeFile(ref, caller);
|
|
@@ -11792,7 +11890,7 @@ async function teamTenants(caller, teamId) {
|
|
|
11792
11890
|
|
|
11793
11891
|
// src/commands/recipe-publish.ts
|
|
11794
11892
|
var import_chalk44 = __toESM(require("chalk"));
|
|
11795
|
-
var
|
|
11893
|
+
var import_ora15 = __toESM(require("ora"));
|
|
11796
11894
|
init_caller();
|
|
11797
11895
|
init_auth();
|
|
11798
11896
|
async function runRecipePublish(projectArg, opts) {
|
|
@@ -11816,7 +11914,7 @@ async function runRecipePublish(projectArg, opts) {
|
|
|
11816
11914
|
const caller = requireCaller();
|
|
11817
11915
|
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
11818
11916
|
const proj2 = await resolveProjectVia(caller, teamId, projectArg, opts.apiversion);
|
|
11819
|
-
const spinner = !opts.json ? (0,
|
|
11917
|
+
const spinner = !opts.json ? (0, import_ora15.default)(`Reading ${proj2.projectId} v${proj2.apiVersion}...`).start() : null;
|
|
11820
11918
|
let prep;
|
|
11821
11919
|
try {
|
|
11822
11920
|
prep = await prepare(caller, proj2.projectId, proj2.apiVersion, ref.name, opts.tenant);
|
|
@@ -11918,7 +12016,7 @@ async function runRecipePublish(projectArg, opts) {
|
|
|
11918
12016
|
return;
|
|
11919
12017
|
}
|
|
11920
12018
|
}
|
|
11921
|
-
const spin2 = !opts.json ? (0,
|
|
12019
|
+
const spin2 = !opts.json ? (0, import_ora15.default)(`Publishing ${ref.name}...`).start() : null;
|
|
11922
12020
|
let out;
|
|
11923
12021
|
try {
|
|
11924
12022
|
out = await producer(caller, {
|
|
@@ -12050,7 +12148,7 @@ async function runRecipeWithdraw(nameArg, opts) {
|
|
|
12050
12148
|
console.error(import_chalk44.default.red("Withdrawing needs a typed confirmation and there is no terminal to ask on."));
|
|
12051
12149
|
process.exit(1);
|
|
12052
12150
|
}
|
|
12053
|
-
const spinner = !opts.json ? (0,
|
|
12151
|
+
const spinner = !opts.json ? (0, import_ora15.default)("Withdrawing...").start() : null;
|
|
12054
12152
|
let out;
|
|
12055
12153
|
try {
|
|
12056
12154
|
out = await producer(caller, {
|
|
@@ -12171,14 +12269,14 @@ async function runConsumerLogsList(opts) {
|
|
|
12171
12269
|
|
|
12172
12270
|
// src/commands/config.ts
|
|
12173
12271
|
var import_chalk46 = __toESM(require("chalk"));
|
|
12174
|
-
var
|
|
12272
|
+
var import_ora16 = __toESM(require("ora"));
|
|
12175
12273
|
init_caller();
|
|
12176
12274
|
init_paging();
|
|
12177
12275
|
async function patchConfig(project, opts, body, summary) {
|
|
12178
12276
|
const caller = requireCaller();
|
|
12179
12277
|
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
12180
12278
|
const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
|
|
12181
|
-
const spinner = (0,
|
|
12279
|
+
const spinner = (0, import_ora16.default)(summary + "...").start();
|
|
12182
12280
|
try {
|
|
12183
12281
|
const out = await producer(caller, {
|
|
12184
12282
|
method: "PATCH",
|
|
@@ -12233,7 +12331,7 @@ async function runRename(project, opts) {
|
|
|
12233
12331
|
|
|
12234
12332
|
// src/commands/limits.ts
|
|
12235
12333
|
var import_chalk47 = __toESM(require("chalk"));
|
|
12236
|
-
var
|
|
12334
|
+
var import_ora17 = __toESM(require("ora"));
|
|
12237
12335
|
init_caller();
|
|
12238
12336
|
init_admin();
|
|
12239
12337
|
init_resolve();
|
|
@@ -12371,7 +12469,7 @@ async function runLimitsSet(project, opts) {
|
|
|
12371
12469
|
const needsCurrent = opts.quota !== void 0 && opts.period === void 0 || opts.period !== void 0 && opts.quota === void 0 || opts.credentialQuota !== void 0;
|
|
12372
12470
|
const current = needsCurrent ? (await getLimits(caller, proj2).catch(() => null))?.limits ?? null : null;
|
|
12373
12471
|
const body = buildLimitsPatch(opts, current);
|
|
12374
|
-
const spinner = (0,
|
|
12472
|
+
const spinner = (0, import_ora17.default)(`Update limits for ${proj2.projectName}...`).start();
|
|
12375
12473
|
let r;
|
|
12376
12474
|
try {
|
|
12377
12475
|
r = await producer(caller, { method: "PUT", path: limitsPath(proj2), body, summary: `Set limits ${JSON.stringify(body)}` });
|
|
@@ -12408,7 +12506,7 @@ async function runLimitsRequestMax(project, opts) {
|
|
|
12408
12506
|
if (rps === void 0) throw new UsageError("--rps <n> is required: the ceiling you are asking for, in requests per second.");
|
|
12409
12507
|
const { caller, proj: proj2 } = await resolve2(project, opts);
|
|
12410
12508
|
const body = { requested_rps: rps, ...opts.useCase ? { use_case: opts.useCase } : {} };
|
|
12411
|
-
const spinner = (0,
|
|
12509
|
+
const spinner = (0, import_ora17.default)(`Request a ${rps}/s ceiling for ${proj2.projectName}...`).start();
|
|
12412
12510
|
let r;
|
|
12413
12511
|
try {
|
|
12414
12512
|
r = await producer(caller, { method: "POST", path: `${limitsPath(proj2)}/ceiling-request`, body, summary: `Request ceiling ${rps}/s` });
|
|
@@ -12489,7 +12587,7 @@ async function runThrottleAlias(project, opts) {
|
|
|
12489
12587
|
|
|
12490
12588
|
// src/commands/billing.ts
|
|
12491
12589
|
var import_chalk48 = __toESM(require("chalk"));
|
|
12492
|
-
var
|
|
12590
|
+
var import_ora18 = __toESM(require("ora"));
|
|
12493
12591
|
init_admin();
|
|
12494
12592
|
init_resolve();
|
|
12495
12593
|
init_brand();
|
|
@@ -12747,7 +12845,7 @@ async function runBillingRebuy(mode, opts) {
|
|
|
12747
12845
|
throw new UsageError(`--amount must be between $${TOPUP_MIN_USD} and $${TOPUP_MAX_USD} (one card charge).`);
|
|
12748
12846
|
}
|
|
12749
12847
|
}
|
|
12750
|
-
const spinner = (0,
|
|
12848
|
+
const spinner = (0, import_ora18.default)(`Auto-rebuy ${which}...`).start();
|
|
12751
12849
|
let r;
|
|
12752
12850
|
try {
|
|
12753
12851
|
r = await admin({ method: "PUT", path: `/credits/auto-rebuy?teamId=${encodeURIComponent(teamId)}`, body, summary: `Auto-rebuy ${which}` });
|
|
@@ -12762,7 +12860,7 @@ async function runBillingRebuy(mode, opts) {
|
|
|
12762
12860
|
|
|
12763
12861
|
// src/commands/transform.ts
|
|
12764
12862
|
var import_chalk49 = __toESM(require("chalk"));
|
|
12765
|
-
var
|
|
12863
|
+
var import_ora19 = __toESM(require("ora"));
|
|
12766
12864
|
init_caller();
|
|
12767
12865
|
var HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
12768
12866
|
var ruleNameFor = (header) => `set header ${header.toLowerCase()}`;
|
|
@@ -12826,7 +12924,7 @@ async function runTransformSetHeader(project, header, opts) {
|
|
|
12826
12924
|
};
|
|
12827
12925
|
const existing = (await listRules(t)).find((r) => r.name === name && r.phase === "request");
|
|
12828
12926
|
const summary = `${existing ? "Update" : "Add"} transform: send ${header} to the upstream`;
|
|
12829
|
-
const spinner = opts.json ? null : (0,
|
|
12927
|
+
const spinner = opts.json ? null : (0, import_ora19.default)(summary + "...").start();
|
|
12830
12928
|
try {
|
|
12831
12929
|
const out = await producer(t.caller, existing ? { method: "PUT", path: `${t.base}/${encodeURIComponent(existing.id)}`, body: { action: action2, enabled: true }, summary } : { method: "POST", path: t.base, body: { name, action: action2, phase: "request", enabled: true }, summary });
|
|
12832
12930
|
spinner?.succeed(`${summary} \u2014 ${t.proj.projectName} v${t.proj.apiVersion}${opts.secret ? " (stored encrypted)" : ""}.`);
|
|
@@ -12916,7 +13014,7 @@ async function runTransformMap(project, source, destination, opts) {
|
|
|
12916
13014
|
|
|
12917
13015
|
// src/commands/config-browse.ts
|
|
12918
13016
|
var import_chalk58 = __toESM(require("chalk"));
|
|
12919
|
-
var
|
|
13017
|
+
var import_ora27 = __toESM(require("ora"));
|
|
12920
13018
|
init_admin();
|
|
12921
13019
|
init_paging();
|
|
12922
13020
|
init_auth();
|
|
@@ -12924,7 +13022,7 @@ init_resolve();
|
|
|
12924
13022
|
|
|
12925
13023
|
// src/commands/domain.ts
|
|
12926
13024
|
var import_chalk50 = __toESM(require("chalk"));
|
|
12927
|
-
var
|
|
13025
|
+
var import_ora20 = __toESM(require("ora"));
|
|
12928
13026
|
init_admin();
|
|
12929
13027
|
init_resolve();
|
|
12930
13028
|
var KINDS = ["api", "mcp", "portal", "auth"];
|
|
@@ -12978,7 +13076,7 @@ async function runDomainAdd(project, opts) {
|
|
|
12978
13076
|
process.exit(1);
|
|
12979
13077
|
}
|
|
12980
13078
|
const scope = await scopeFor(project, opts);
|
|
12981
|
-
const spinner = (0,
|
|
13079
|
+
const spinner = (0, import_ora20.default)(`Registering ${opts.domain}...`).start();
|
|
12982
13080
|
try {
|
|
12983
13081
|
const body = scope.kind === "project" ? { hostname: opts.domain, api_version: scope.apiVersion, kind, ...opts.tenant ? { tenant_name: opts.tenant.toLowerCase() } : {} } : { hostname: opts.domain, kind };
|
|
12984
13082
|
const out = await admin({
|
|
@@ -13053,7 +13151,7 @@ async function runDomainRemove(project, opts) {
|
|
|
13053
13151
|
process.exit(1);
|
|
13054
13152
|
}
|
|
13055
13153
|
const scope = await scopeFor(project, opts);
|
|
13056
|
-
const spinner = (0,
|
|
13154
|
+
const spinner = (0, import_ora20.default)("Removing domain...").start();
|
|
13057
13155
|
try {
|
|
13058
13156
|
await admin({
|
|
13059
13157
|
method: "DELETE",
|
|
@@ -13070,7 +13168,7 @@ async function runDomainSetBase(project, opts) {
|
|
|
13070
13168
|
const env = opts.env ?? "prod";
|
|
13071
13169
|
const { teamId } = await resolveTeam(opts.team);
|
|
13072
13170
|
const proj2 = await resolveProject(teamId, project, opts.apiversion);
|
|
13073
|
-
const spinner = (0,
|
|
13171
|
+
const spinner = (0, import_ora20.default)("Setting base domain...").start();
|
|
13074
13172
|
try {
|
|
13075
13173
|
await admin({
|
|
13076
13174
|
method: "PUT",
|
|
@@ -13087,7 +13185,7 @@ async function runDomainSetBase(project, opts) {
|
|
|
13087
13185
|
|
|
13088
13186
|
// src/commands/tenant.ts
|
|
13089
13187
|
var import_chalk53 = __toESM(require("chalk"));
|
|
13090
|
-
var
|
|
13188
|
+
var import_ora23 = __toESM(require("ora"));
|
|
13091
13189
|
init_admin();
|
|
13092
13190
|
init_caller();
|
|
13093
13191
|
init_resolve();
|
|
@@ -13154,7 +13252,7 @@ async function runTenantCreate(opts) {
|
|
|
13154
13252
|
}
|
|
13155
13253
|
const caller = requireCaller();
|
|
13156
13254
|
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
13157
|
-
const spinner = (0,
|
|
13255
|
+
const spinner = (0, import_ora23.default)("Creating tenant...").start();
|
|
13158
13256
|
try {
|
|
13159
13257
|
const out = await producer(caller, {
|
|
13160
13258
|
method: "POST",
|
|
@@ -13177,7 +13275,7 @@ async function runTenantAttach(project, opts) {
|
|
|
13177
13275
|
const caller = requireCaller();
|
|
13178
13276
|
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
13179
13277
|
const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
|
|
13180
|
-
const spinner = (0,
|
|
13278
|
+
const spinner = (0, import_ora23.default)("Attaching tenant...").start();
|
|
13181
13279
|
try {
|
|
13182
13280
|
const out = await producer(caller, {
|
|
13183
13281
|
method: "POST",
|
|
@@ -13210,7 +13308,7 @@ async function runTenantDelete(slug, opts) {
|
|
|
13210
13308
|
return;
|
|
13211
13309
|
}
|
|
13212
13310
|
}
|
|
13213
|
-
const spinner = (0,
|
|
13311
|
+
const spinner = (0, import_ora23.default)(hard ? "Trashing tenant..." : "Deleting tenant...").start();
|
|
13214
13312
|
try {
|
|
13215
13313
|
await admin({
|
|
13216
13314
|
method: "DELETE",
|
|
@@ -13261,7 +13359,7 @@ async function runTenantCors(opts) {
|
|
|
13261
13359
|
const { teamId } = await resolveTeam(opts.team);
|
|
13262
13360
|
const origins = (opts.origins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
13263
13361
|
const cors = origins.length ? { allowed_origins: origins } : null;
|
|
13264
|
-
const spinner = (0,
|
|
13362
|
+
const spinner = (0, import_ora23.default)("Updating CORS...").start();
|
|
13265
13363
|
try {
|
|
13266
13364
|
await admin({
|
|
13267
13365
|
method: "PUT",
|
|
@@ -13278,7 +13376,7 @@ async function runTenantCors(opts) {
|
|
|
13278
13376
|
|
|
13279
13377
|
// src/commands/tenant-drill.ts
|
|
13280
13378
|
var import_chalk54 = __toESM(require("chalk"));
|
|
13281
|
-
var
|
|
13379
|
+
var import_ora24 = __toESM(require("ora"));
|
|
13282
13380
|
var import_crypto2 = require("crypto");
|
|
13283
13381
|
init_admin();
|
|
13284
13382
|
init_auth();
|
|
@@ -13318,7 +13416,7 @@ async function tenantHome(teamId, tenant2) {
|
|
|
13318
13416
|
Tenant ${tenant2}`));
|
|
13319
13417
|
console.log(import_chalk54.default.dim("Tenant auth/settings are SHARED: changes apply to every proxy this tenant serves.\n"));
|
|
13320
13418
|
for (; ; ) {
|
|
13321
|
-
const spinner = (0,
|
|
13419
|
+
const spinner = (0, import_ora24.default)("Reading tenant state...").start();
|
|
13322
13420
|
const [iam, cors, emails, issuers, opaque, clients] = await Promise.all([
|
|
13323
13421
|
admin({ method: "GET", path: `${base2}/iam`, summary: "Read IAM toggle" }).catch(() => null),
|
|
13324
13422
|
admin({ method: "GET", path: `${base2}/cors`, summary: "Read tenant CORS" }).catch(() => null),
|
|
@@ -13385,7 +13483,7 @@ Tenant ${tenant2}`));
|
|
|
13385
13483
|
async function loginMethodsMenu(teamId, tenant2, base2) {
|
|
13386
13484
|
const { default: inquirer3 } = await import("inquirer");
|
|
13387
13485
|
for (; ; ) {
|
|
13388
|
-
const spinner = (0,
|
|
13486
|
+
const spinner = (0, import_ora24.default)("Loading login methods...").start();
|
|
13389
13487
|
const [rawClients, rawIssuers, rawOpaque] = await Promise.all([
|
|
13390
13488
|
admin({ method: "GET", path: `${base2}/app-clients`, summary: "List APIblaze-hosted logins" }).catch(() => []),
|
|
13391
13489
|
admin({ method: "GET", path: `${base2}/external-issuers`, summary: "List your-own-JWT logins" }).catch(() => null),
|
|
@@ -13584,7 +13682,7 @@ async function clientHome(base2, summary) {
|
|
|
13584
13682
|
const id = summary.clientId ?? summary.client_id;
|
|
13585
13683
|
const cBase = `${base2}/app-clients/${encodeURIComponent(id)}`;
|
|
13586
13684
|
for (; ; ) {
|
|
13587
|
-
const spinner = (0,
|
|
13685
|
+
const spinner = (0, import_ora24.default)("Reading app client...").start();
|
|
13588
13686
|
const c = await admin({ method: "GET", path: cBase, summary: `Read app client ${id}` }).catch(() => summary);
|
|
13589
13687
|
spinner.stop();
|
|
13590
13688
|
const cb = c.authorizedCallbackUrls ?? c.authorized_callback_urls ?? [];
|
|
@@ -13679,7 +13777,7 @@ var DEFAULT_SCOPES2 = {
|
|
|
13679
13777
|
async function providersMenu(cBase, clientLabel) {
|
|
13680
13778
|
const { default: inquirer3 } = await import("inquirer");
|
|
13681
13779
|
for (; ; ) {
|
|
13682
|
-
const spinner = (0,
|
|
13780
|
+
const spinner = (0, import_ora24.default)("Loading providers...").start();
|
|
13683
13781
|
const raw = await admin({ method: "GET", path: `${cBase}/providers`, summary: "List login providers" }).catch(() => []);
|
|
13684
13782
|
spinner.stop();
|
|
13685
13783
|
const providers = Array.isArray(raw) ? raw : [];
|
|
@@ -13770,7 +13868,7 @@ async function providersMenu(cBase, clientLabel) {
|
|
|
13770
13868
|
// src/commands/spec.ts
|
|
13771
13869
|
var fs15 = __toESM(require("fs"));
|
|
13772
13870
|
var import_chalk55 = __toESM(require("chalk"));
|
|
13773
|
-
var
|
|
13871
|
+
var import_ora25 = __toESM(require("ora"));
|
|
13774
13872
|
init_admin();
|
|
13775
13873
|
init_api();
|
|
13776
13874
|
init_resolve();
|
|
@@ -13798,7 +13896,7 @@ async function runSpecSet(project, opts) {
|
|
|
13798
13896
|
}
|
|
13799
13897
|
const { teamId } = await resolveTeam(opts.team);
|
|
13800
13898
|
const proj2 = await resolveProject(teamId, project, opts.apiversion);
|
|
13801
|
-
const spinner = (0,
|
|
13899
|
+
const spinner = (0, import_ora25.default)("Uploading spec...").start();
|
|
13802
13900
|
try {
|
|
13803
13901
|
const out = await admin({
|
|
13804
13902
|
method: "POST",
|
|
@@ -13843,7 +13941,7 @@ async function runSpecCarry(project, opts) {
|
|
|
13843
13941
|
}
|
|
13844
13942
|
const { teamId } = await resolveTeam(opts.team);
|
|
13845
13943
|
const proj2 = await resolveProject(teamId, project, opts.apiversion);
|
|
13846
|
-
const spinner = (0,
|
|
13944
|
+
const spinner = (0, import_ora25.default)(`Carrying rules ${opts.from} \u2192 ${opts.to}...`).start();
|
|
13847
13945
|
const res = await agentCall(`/projects/${proj2.projectId}/${proj2.apiVersion}/policies/route/carry`, "POST", {
|
|
13848
13946
|
method: opts.method.toUpperCase(),
|
|
13849
13947
|
from_resource: opts.from,
|
|
@@ -13866,7 +13964,7 @@ async function runSpecDeleteRule(project, opts) {
|
|
|
13866
13964
|
const { teamId } = await resolveTeam(opts.team);
|
|
13867
13965
|
const proj2 = await resolveProject(teamId, project, opts.apiversion);
|
|
13868
13966
|
const encoded = opts.route.replace(/\{/g, "%7B").replace(/\}/g, "%7D");
|
|
13869
|
-
const spinner = (0,
|
|
13967
|
+
const spinner = (0, import_ora25.default)(`Deleting rules for ${opts.method.toUpperCase()} ${opts.route}...`).start();
|
|
13870
13968
|
const res = await agentCall(
|
|
13871
13969
|
`/projects/${proj2.projectId}/${proj2.apiVersion}/policies/route/${opts.method.toUpperCase()}${encoded.startsWith("/") ? encoded : `/${encoded}`}`,
|
|
13872
13970
|
"DELETE"
|
|
@@ -13881,7 +13979,7 @@ async function runSpecDeleteRule(project, opts) {
|
|
|
13881
13979
|
|
|
13882
13980
|
// src/commands/agent.ts
|
|
13883
13981
|
var import_chalk57 = __toESM(require("chalk"));
|
|
13884
|
-
var
|
|
13982
|
+
var import_ora26 = __toESM(require("ora"));
|
|
13885
13983
|
init_auth();
|
|
13886
13984
|
init_admin();
|
|
13887
13985
|
init_resolve();
|
|
@@ -14117,7 +14215,7 @@ async function runAgent(opts) {
|
|
|
14117
14215
|
if (["exit", "quit", ":q"].includes(text.toLowerCase())) break;
|
|
14118
14216
|
history.push({ role: "user", content: text });
|
|
14119
14217
|
for (let step = 0; step < MAX_TOOL_STEPS; step++) {
|
|
14120
|
-
const spinner = (0,
|
|
14218
|
+
const spinner = (0, import_ora26.default)({ text: "thinking...", color: "magenta" }).start();
|
|
14121
14219
|
let resp;
|
|
14122
14220
|
try {
|
|
14123
14221
|
resp = await callAgent(history, teamId);
|
|
@@ -14139,7 +14237,7 @@ async function runAgent(opts) {
|
|
|
14139
14237
|
history.push({ role: "user", content: `TOOL_RESULT ${resp.action.tool}: error \u2014 unknown tool` });
|
|
14140
14238
|
continue;
|
|
14141
14239
|
}
|
|
14142
|
-
const runSpinner = (0,
|
|
14240
|
+
const runSpinner = (0, import_ora26.default)({ text: `running ${tool.name}...`, color: "cyan" }).start();
|
|
14143
14241
|
try {
|
|
14144
14242
|
const result = await tool.run(resp.action.args, { teamId });
|
|
14145
14243
|
runSpinner.succeed(`${tool.name} \u2713`);
|
|
@@ -14557,7 +14655,7 @@ async function patchSetting(proj2, s, value, cfg) {
|
|
|
14557
14655
|
if (!tenant2) throw new Error(`Could not tell which tenant ${proj2.projectName} serves \u2014 pass it via \`apiblaze config ${proj2.projectName} --tenant <slug>\` is not supported yet; run \`apiblaze projects\` and check the project has a tenant.`);
|
|
14558
14656
|
body.tenant = tenant2;
|
|
14559
14657
|
}
|
|
14560
|
-
const spinner = (0,
|
|
14658
|
+
const spinner = (0, import_ora27.default)(`Set ${s.key}...`).start();
|
|
14561
14659
|
try {
|
|
14562
14660
|
await admin({
|
|
14563
14661
|
method,
|
|
@@ -14973,7 +15071,7 @@ async function transformsMenu(proj2) {
|
|
|
14973
15071
|
};
|
|
14974
15072
|
}
|
|
14975
15073
|
const condition = await buildCondition(ans.phase);
|
|
14976
|
-
const spinner = (0,
|
|
15074
|
+
const spinner = (0, import_ora27.default)("Creating rule...").start();
|
|
14977
15075
|
try {
|
|
14978
15076
|
await admin({
|
|
14979
15077
|
method: "POST",
|
|
@@ -15166,7 +15264,7 @@ async function agentsMenu(proj2, opts) {
|
|
|
15166
15264
|
|
|
15167
15265
|
// src/commands/group.ts
|
|
15168
15266
|
var import_chalk59 = __toESM(require("chalk"));
|
|
15169
|
-
var
|
|
15267
|
+
var import_ora28 = __toESM(require("ora"));
|
|
15170
15268
|
init_auth();
|
|
15171
15269
|
init_consumer_auth();
|
|
15172
15270
|
init_admin();
|
|
@@ -15305,7 +15403,7 @@ async function runGroupCreate(name, opts) {
|
|
|
15305
15403
|
}
|
|
15306
15404
|
}
|
|
15307
15405
|
}
|
|
15308
|
-
const spinner = (0,
|
|
15406
|
+
const spinner = (0, import_ora28.default)(`Creating group ${name}...`).start();
|
|
15309
15407
|
try {
|
|
15310
15408
|
await t.call("/v1/admin/groups", "POST", { name, admins: [adminSub] });
|
|
15311
15409
|
spinner.succeed(`Group ${import_chalk59.default.bold(name)} created in ${t.tenant}.`);
|
|
@@ -15317,7 +15415,7 @@ async function runGroupCreate(name, opts) {
|
|
|
15317
15415
|
async function runGroupDelete(name, opts) {
|
|
15318
15416
|
const t = await resolveTransport(opts);
|
|
15319
15417
|
const g = await groupByName(t, name);
|
|
15320
|
-
const spinner = (0,
|
|
15418
|
+
const spinner = (0, import_ora28.default)(`Deleting group ${name}...`).start();
|
|
15321
15419
|
try {
|
|
15322
15420
|
await t.call(`/v1/admin/groups/${encodeURIComponent(g.id)}`, "DELETE");
|
|
15323
15421
|
spinner.succeed(`Group ${import_chalk59.default.bold(name)} deleted.`);
|
|
@@ -15350,7 +15448,7 @@ async function runGroupAddUser(userRef, groupName, opts) {
|
|
|
15350
15448
|
const g = await groupByName(t, groupName);
|
|
15351
15449
|
const u = await userByRefOrCreate(t, userRef);
|
|
15352
15450
|
const role = opts.role === "admin" ? "admin" : "member";
|
|
15353
|
-
const spinner = (0,
|
|
15451
|
+
const spinner = (0, import_ora28.default)(`Adding ${label2(u)} to ${g.name}...`).start();
|
|
15354
15452
|
try {
|
|
15355
15453
|
await t.call(`/v1/admin/groups/${encodeURIComponent(g.id)}/members`, "POST", { abz_sub: u.abz_sub, role });
|
|
15356
15454
|
spinner.succeed(`${import_chalk59.default.bold(label2(u))} added to ${import_chalk59.default.bold(g.name)} as ${role}.`);
|
|
@@ -15362,7 +15460,7 @@ async function runGroupAddUser(userRef, groupName, opts) {
|
|
|
15362
15460
|
async function runGroupRemoveUser(userRef, groupName, opts) {
|
|
15363
15461
|
const t = await resolveTransport(opts);
|
|
15364
15462
|
const [g, u] = [await groupByName(t, groupName), await userByRef(t, userRef)];
|
|
15365
|
-
const spinner = (0,
|
|
15463
|
+
const spinner = (0, import_ora28.default)(`Removing ${label2(u)} from ${g.name}...`).start();
|
|
15366
15464
|
try {
|
|
15367
15465
|
await t.call(`/v1/admin/groups/${encodeURIComponent(g.id)}/members/_?abz_sub=${encodeURIComponent(u.abz_sub)}`, "DELETE");
|
|
15368
15466
|
spinner.succeed(`${import_chalk59.default.bold(label2(u))} removed from ${import_chalk59.default.bold(g.name)}.`);
|
|
@@ -15374,7 +15472,7 @@ async function runGroupRemoveUser(userRef, groupName, opts) {
|
|
|
15374
15472
|
async function runGroupAddGroup(childName, parentName, opts) {
|
|
15375
15473
|
const t = await resolveTransport(opts);
|
|
15376
15474
|
const [child, parent] = [await groupByName(t, childName), await groupByName(t, parentName)];
|
|
15377
|
-
const spinner = (0,
|
|
15475
|
+
const spinner = (0, import_ora28.default)(`Nesting ${child.name} under ${parent.name}...`).start();
|
|
15378
15476
|
try {
|
|
15379
15477
|
await t.call(`/v1/admin/groups/${encodeURIComponent(parent.id)}/subgroups`, "POST", { child_group_id: child.id });
|
|
15380
15478
|
spinner.succeed(`${import_chalk59.default.bold(child.name)} is now a subgroup of ${import_chalk59.default.bold(parent.name)}.`);
|
|
@@ -15386,7 +15484,7 @@ async function runGroupAddGroup(childName, parentName, opts) {
|
|
|
15386
15484
|
async function runGroupRemoveGroup(childName, parentName, opts) {
|
|
15387
15485
|
const t = await resolveTransport(opts);
|
|
15388
15486
|
const [child, parent] = [await groupByName(t, childName), await groupByName(t, parentName)];
|
|
15389
|
-
const spinner = (0,
|
|
15487
|
+
const spinner = (0, import_ora28.default)(`Un-nesting ${child.name} from ${parent.name}...`).start();
|
|
15390
15488
|
try {
|
|
15391
15489
|
await t.call(`/v1/admin/groups/${encodeURIComponent(parent.id)}/subgroups/${encodeURIComponent(child.id)}`, "DELETE");
|
|
15392
15490
|
spinner.succeed(`${import_chalk59.default.bold(child.name)} un-nested from ${import_chalk59.default.bold(parent.name)}.`);
|
|
@@ -15417,7 +15515,7 @@ init_admins();
|
|
|
15417
15515
|
|
|
15418
15516
|
// src/commands/preapprove.ts
|
|
15419
15517
|
var import_chalk60 = __toESM(require("chalk"));
|
|
15420
|
-
var
|
|
15518
|
+
var import_ora29 = __toESM(require("ora"));
|
|
15421
15519
|
init_preapproval();
|
|
15422
15520
|
async function runPreapprove(who, opts) {
|
|
15423
15521
|
const tenant2 = await resolveProducerTenant(opts.tenant);
|
|
@@ -15441,13 +15539,13 @@ async function runPreapprove(who, opts) {
|
|
|
15441
15539
|
throw new Error("Who? Pass an email or a domain: `apiblaze preapprove someone@acme.com` (or `apiblaze preapprove --list`).");
|
|
15442
15540
|
}
|
|
15443
15541
|
if (opts.remove) {
|
|
15444
|
-
const spinner2 = (0,
|
|
15542
|
+
const spinner2 = (0, import_ora29.default)(`Removing ${who} from ${tenant2}\u2026`).start();
|
|
15445
15543
|
const { removed, value } = await removePreapprovalRule(tenant2, who);
|
|
15446
15544
|
if (removed) spinner2.succeed(`${import_chalk60.default.bold(value)} is no longer pre-approved for ${tenant2}.`);
|
|
15447
15545
|
else spinner2.warn(`No pre-approval rule for ${import_chalk60.default.bold(value)} in ${tenant2} \u2014 nothing to remove.`);
|
|
15448
15546
|
return;
|
|
15449
15547
|
}
|
|
15450
|
-
const spinner = (0,
|
|
15548
|
+
const spinner = (0, import_ora29.default)(`Pre-approving ${who} for ${tenant2}\u2026`).start();
|
|
15451
15549
|
try {
|
|
15452
15550
|
const { kind, value } = await addPreapprovalRule(tenant2, who, opts.group);
|
|
15453
15551
|
const what = kind === "domain" ? `Anyone @${value}` : value;
|
|
@@ -15466,7 +15564,7 @@ init_llm();
|
|
|
15466
15564
|
|
|
15467
15565
|
// src/commands/consumer.ts
|
|
15468
15566
|
var import_chalk61 = __toESM(require("chalk"));
|
|
15469
|
-
var
|
|
15567
|
+
var import_ora30 = __toESM(require("ora"));
|
|
15470
15568
|
init_admin();
|
|
15471
15569
|
init_resolve();
|
|
15472
15570
|
init_caller();
|
|
@@ -15512,7 +15610,7 @@ async function runConsumerLogin(opts) {
|
|
|
15512
15610
|
if (!picked) process.exit(1);
|
|
15513
15611
|
tenant2 = picked;
|
|
15514
15612
|
}
|
|
15515
|
-
const s2 = (0,
|
|
15613
|
+
const s2 = (0, import_ora30.default)("Finding the login app...").start();
|
|
15516
15614
|
const clients = await admin({ method: "GET", path: `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/app-clients`, summary: `List app clients for ${tenant2}` }).catch(() => []);
|
|
15517
15615
|
s2.stop();
|
|
15518
15616
|
const usable = (Array.isArray(clients) ? clients : []).filter((c) => c && (c.client_id || c.clientId));
|
|
@@ -15573,7 +15671,7 @@ async function runConsumerTokens(opts) {
|
|
|
15573
15671
|
async function runConsumerApikeys(opts) {
|
|
15574
15672
|
const creds = requireConsumer2();
|
|
15575
15673
|
const { default: inquirer3 } = await import("inquirer");
|
|
15576
|
-
const spinner = (0,
|
|
15674
|
+
const spinner = (0, import_ora30.default)("Loading your API keys...").start();
|
|
15577
15675
|
const list = await consumerFetch(creds, withPage("/apikeys", null));
|
|
15578
15676
|
if (list.status >= 400) {
|
|
15579
15677
|
spinner.stop();
|
|
@@ -15630,7 +15728,7 @@ async function runConsumerApikeys(opts) {
|
|
|
15630
15728
|
const body = { environment: answers.environment };
|
|
15631
15729
|
if (answers.description) body.description = answers.description;
|
|
15632
15730
|
if (answers.expiresDays) body.expires_in_seconds = Number(answers.expiresDays) * 86400;
|
|
15633
|
-
const s2 = (0,
|
|
15731
|
+
const s2 = (0, import_ora30.default)("Creating key...").start();
|
|
15634
15732
|
const created = await consumerFetch(list.creds, "/apikeys", { method: "POST", body: JSON.stringify(body) });
|
|
15635
15733
|
if (created.status >= 400) {
|
|
15636
15734
|
s2.fail(`Create failed (${created.status}): ${created.data?.error ?? ""}`);
|
|
@@ -15644,7 +15742,7 @@ async function runConsumerApikeys(opts) {
|
|
|
15644
15742
|
|
|
15645
15743
|
// src/commands/sidecar.ts
|
|
15646
15744
|
var import_chalk62 = __toESM(require("chalk"));
|
|
15647
|
-
var
|
|
15745
|
+
var import_ora31 = __toESM(require("ora"));
|
|
15648
15746
|
var fs16 = __toESM(require("fs"));
|
|
15649
15747
|
var path12 = __toESM(require("path"));
|
|
15650
15748
|
init_admin();
|
|
@@ -15691,7 +15789,7 @@ function installSidecarPackage(root) {
|
|
|
15691
15789
|
}
|
|
15692
15790
|
const has = (f) => fs16.existsSync(path12.join(root, f));
|
|
15693
15791
|
const pm = has("bun.lockb") || has("bun.lock") ? { cmd: "bun", add: "add" } : has("pnpm-lock.yaml") ? { cmd: "pnpm", add: "add" } : has("yarn.lock") ? { cmd: "yarn", add: "add" } : { cmd: "npm", add: "install" };
|
|
15694
|
-
const spinner = (0,
|
|
15792
|
+
const spinner = (0, import_ora31.default)(`Installing the apiblaze package (${pm.cmd})\u2026`).start();
|
|
15695
15793
|
try {
|
|
15696
15794
|
const { execSync } = require("child_process");
|
|
15697
15795
|
execSync(`${pm.cmd} ${pm.add} apiblaze`, { cwd: root, stdio: "ignore" });
|
|
@@ -15840,7 +15938,7 @@ async function runAnonymousInit(root, router, opts) {
|
|
|
15840
15938
|
const { sidecarInitAnonymous: sidecarInitAnonymous2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
15841
15939
|
const { saveAnonCred: saveAnonCred2, clearAnonCred: clearAnonCred2 } = await Promise.resolve().then(() => (init_anon_cred(), anon_cred_exports));
|
|
15842
15940
|
if (opts.newSession) clearAnonCred2();
|
|
15843
|
-
const spinner = (0,
|
|
15941
|
+
const spinner = (0, import_ora31.default)("Setting up a sidecar (no login needed)...").start();
|
|
15844
15942
|
let out;
|
|
15845
15943
|
try {
|
|
15846
15944
|
out = await sidecarInitAnonymous2();
|
|
@@ -15897,7 +15995,7 @@ async function runSidecar(opts) {
|
|
|
15897
15995
|
const mustMint = !existingKey || opts.rotate || switchingTeam;
|
|
15898
15996
|
let token = existingKey ?? "";
|
|
15899
15997
|
if (mustMint) {
|
|
15900
|
-
const spinner = (0,
|
|
15998
|
+
const spinner = (0, import_ora31.default)(existingKey ? "Re-establishing the sidecar (minting a fresh invoke key)..." : "Setting up the sidecar (tenant + non-expiring invoke key)...").start();
|
|
15901
15999
|
try {
|
|
15902
16000
|
const out = await admin({
|
|
15903
16001
|
method: "POST",
|
|
@@ -15943,7 +16041,7 @@ async function runSidecar(opts) {
|
|
|
15943
16041
|
|
|
15944
16042
|
// src/commands/origins.ts
|
|
15945
16043
|
var import_chalk63 = __toESM(require("chalk"));
|
|
15946
|
-
var
|
|
16044
|
+
var import_ora32 = __toESM(require("ora"));
|
|
15947
16045
|
init_admin();
|
|
15948
16046
|
init_resolve();
|
|
15949
16047
|
init_auth();
|
|
@@ -15994,7 +16092,7 @@ async function runOriginsApprove(origin, opts) {
|
|
|
15994
16092
|
console.error(import_chalk63.default.red("Not logged in and no anonymous workspace. Run `apiblaze init` first."));
|
|
15995
16093
|
process.exit(1);
|
|
15996
16094
|
}
|
|
15997
|
-
const spinner2 = (0,
|
|
16095
|
+
const spinner2 = (0, import_ora32.default)(`Approving ${origin} (anonymous)...`).start();
|
|
15998
16096
|
try {
|
|
15999
16097
|
const out = await cpFetch(cred.cp_key, `/teams/${encodeURIComponent(cred.team_id)}/sidecar/approve`, { method: "POST", body: JSON.stringify({ origin }) });
|
|
16000
16098
|
spinner2.succeed(`Approved ${origin} \u2192 proxy ${out.project_id}. Routing within ~5 min.`);
|
|
@@ -16005,7 +16103,7 @@ async function runOriginsApprove(origin, opts) {
|
|
|
16005
16103
|
return;
|
|
16006
16104
|
}
|
|
16007
16105
|
const { teamId } = await resolveTeam(opts.team);
|
|
16008
|
-
const spinner = (0,
|
|
16106
|
+
const spinner = (0, import_ora32.default)(`Approving ${origin}...`).start();
|
|
16009
16107
|
try {
|
|
16010
16108
|
const out = await admin({
|
|
16011
16109
|
method: "POST",
|
|
@@ -16022,7 +16120,7 @@ async function runOriginsApprove(origin, opts) {
|
|
|
16022
16120
|
}
|
|
16023
16121
|
async function runOriginsDeny(origin, opts) {
|
|
16024
16122
|
const { teamId } = await resolveTeam(opts.team);
|
|
16025
|
-
const spinner = (0,
|
|
16123
|
+
const spinner = (0, import_ora32.default)(`Dismissing ${origin}...`).start();
|
|
16026
16124
|
try {
|
|
16027
16125
|
await admin({ method: "POST", path: `/teams/${encodeURIComponent(teamId)}/sidecar/dismiss`, body: { origin }, summary: `Dismiss sidecar origin ${origin}` });
|
|
16028
16126
|
spinner.succeed(`Dismissed ${origin}. It won't be suggested again.`);
|
|
@@ -16033,7 +16131,7 @@ async function runOriginsDeny(origin, opts) {
|
|
|
16033
16131
|
}
|
|
16034
16132
|
async function runOriginsRemove(origin, opts) {
|
|
16035
16133
|
const { teamId } = await resolveTeam(opts.team);
|
|
16036
|
-
const spinner = (0,
|
|
16134
|
+
const spinner = (0, import_ora32.default)(`Removing the proxy for ${origin}...`).start();
|
|
16037
16135
|
try {
|
|
16038
16136
|
await admin({ method: "POST", path: `/teams/${encodeURIComponent(teamId)}/sidecar/remove`, body: { origin }, summary: `Un-route sidecar origin ${origin}` });
|
|
16039
16137
|
spinner.succeed(`Removed ${origin}. Your app will stop routing it (goes direct) within ~5 min.`);
|
|
@@ -17611,7 +17709,7 @@ program.command("create").description(`Put an app that is already DEPLOYED behin
|
|
|
17611
17709
|
}
|
|
17612
17710
|
});
|
|
17613
17711
|
program.command("integration").description(`What to add to your frontend and frontend-server for ${B.cli === "mcpblaze" ? "an MCP server" : "a proxy"} \u2014 the same kit the ${B.product} MCP hands an agent: widgets, the call-through with the key + X-End-User-Id, what your backend must trust, env vars, what stays open`).argument("<name>", `The ${B.thingShort} (project name)`).option("--stack <stack>", "nextjs | express | fastapi | other", "nextjs").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--json", "Print the raw kit as JSON").action(action((name, opts) => runIntegration(name, opts)));
|
|
17614
|
-
program.command("demo").description(`See what ${B.product} does in
|
|
17712
|
+
program.command("demo").description(`See what ${B.product} does in a minute: a small booking app on this machine, with ${B.product} in front of it (no login)`).argument("[action]", "stop \xB7 start / restart (the same demo again: same addresses and keys) \xB7 logs (follow the requests reaching your laptop)").option("--no-open", "Do not open the browser").option("-y, --yes", "Skip the questions: set everything up, open nothing (an AI assistant is never asked)").addOption(new import_commander.Option("--serve <dir>", "internal: run the demo servers and tunnel").hideHelp()).action(async (actionArg, opts) => {
|
|
17615
17713
|
if (actionArg === "stop") return runDemoStop();
|
|
17616
17714
|
if (actionArg === "start" || actionArg === "restart") return runDemoStart(opts);
|
|
17617
17715
|
if (actionArg === "logs") return runDemoLogs();
|