apiblaze 0.21.25 → 0.21.27

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 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.25";
37
+ version = "0.21.27";
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: Nino's Pizza API\n version: 1.0.0\n description: Table bookings at Nino's 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": "// Nino's Pizza backend: 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// 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);\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); 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 };\nif (require.main === module) startBackend().then(() => console.log('backend on http://localhost:3001'));\n",
6371
- "app.js": "// Nino's Pizza 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/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 the pizzeria 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('The pizzeria 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 the pizzeria (backend and website).');\n}\nfunction background() {\n if (runningPid()) { console.log('The pizzeria 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 the pizzeria: 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(`Pizzeria 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",
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 description: Only the time is needed. The restaurant picks a free table and books it in the signed-in person's name, so never ask for a table number or a name.\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: [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, description: 'Optional: the restaurant picks a free table. Do not ask for it.' }\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. Optional, 2 if not given.' }\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\n// The restaurant picks the table: the lowest one still free at that time, at that restaurant.\nfunction freeTable(tenant, time) {\n const taken = new Set([...bookings.values()].filter((r) => r.tenant === tenant && r.time === time).map((r) => r.table));\n let t = 1;\n while (taken.has(t)) t++;\n return t;\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 time = String(input.time || '19:30');\n const row = {\n id: `r${next++}`, name: String(input.name || who), table: Number(input.table) || freeTable(tenant, time),\n time, 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>Nino's Pizza demo</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, rules, tenants, AI tools</small></span>
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>no APIblaze: anyone can do anything</small></span>
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. No API keys, no rules, no tenants checked.</div>
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>, skipping APIblaze: nothing checks who is asking.</span> Next: <a id="go-admin">the admin side \u2192</a></p>
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.href = \`\${location.pathname}?\${p}\${(next.tab ?? state.tab) === 'admin' ? '#admin' : ''}\`;
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.href = url + location.search + location.hash; }
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 && r.status !== 200) { const li = document.createElement('li'); li.className = 'empty'; li.textContent = 'Could not load reservations (HTTP ' + r.status + ').'; ul.append(li); }
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');
@@ -6586,7 +6597,7 @@ async function refresh() {
6586
6597
  $('#book').onclick = async () => {
6587
6598
  const guests = Math.max(1, Number($('#guests').value) || 2);
6588
6599
  const time = $('#time').value || '19:30';
6589
- await act('book', { body: { guests, time, table: 4 } });
6600
+ await act('book', { body: { guests, time } }); // the restaurant picks the table
6590
6601
  refresh();
6591
6602
  };
6592
6603
  $('#ben-cancel').onclick = async () => {
@@ -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
- setInterval(() => { if (state.tab === 'app') refresh(); }, 2000); // the assistant's bookings appear here
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, 200));
7701
- const leftW = 64;
7702
- const rightW = Math.max(30, width - leftW - 8);
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("What APIblaze set up", `The command that does the same: ${LATEST} \u2026`);
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(1, leftW - leftPlain.length))} ${import_chalk27.default.cyan(right[0])}`);
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,56 +7801,74 @@ 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(`1. The app: Nino's Pizza, a table-booking app on this laptop, in ${rel}`) + import_chalk27.default.dim(" (no APIblaze yet)"));
7747
- console.log(" Its routes, from openapi.yaml:");
7748
- for (const r2 of routesOf(openapi)) console.log(` ${r2.method.padEnd(7)}${r2.path.padEnd(21)}${import_chalk27.default.dim(r2.summary)}`);
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 pizzeria app could not start.", `Run again: ${LATEST} demo`);
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:");
7755
7819
  console.log(import_chalk27.default.cyan(` curl -X POST http://localhost:${backendPort}/reservations \\`));
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
- console.log(import_chalk27.default.cyan(` -H "content-type: application/json" -d '{"table":4,"time":"19:30","guests":2}'`));
7822
+ console.log(import_chalk27.default.cyan(` -H "content-type: application/json" -d '{"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 r = await createProxy2(name, backendPort, openapi, ninoTenant);
7763
- const cpKey = r.cp_key;
7764
- const teamId = r.team_id;
7765
- const keys = r.api_keys ?? {};
7766
- const serverKey = r.apiKey ?? keys.dev ?? Object.values(keys)[0];
7767
- const project = r.project_id ?? name;
7768
- const version2 = r.api_version || "1.0.0";
7769
- const rr = r;
7770
- const tenant2 = rr.tenant ?? tenantFromPortal(rr.portal ?? rr.devPortal) ?? "";
7771
- const missing = [!cpKey && "workspace key", !teamId && "team", !serverKey && "API key", !tenant2 && "tenant"].filter(Boolean);
7772
- if (missing.length) throw new DemoError(`${B.product} did not return everything the demo needs (${missing.join(", ")}).`, `Run again: ${LATEST} demo`);
7773
- const createSecs = ((Date.now() - tCreate) / 1e3).toFixed(1);
7774
- adoptWorkspace(cpKey, teamId, rr.claim_code);
7775
- const cp = (p, init) => cpFetch(cpKey, p, init).then(() => true).catch(() => false);
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 adminOk = await cp(`/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/admin-emails`, { method: "POST", body: JSON.stringify({ email: DEMO_ADMIN_EMAIL }) });
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: rr.claim_code };
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 spawnTunnel(dir, state);
7835
- await waitThrough(apiUrl, serverKey);
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
- appCtl(dir, "--stop");
7842
- appCtl(dir, "--background");
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
- writeRegistry([...readRegistry().filter((r) => r.dir !== dir), { dir, pid: child.pid ?? 0 }]);
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(` The pizzeria app is not running: cd ${import_path4.default.relative(process.cwd(), dir) || "."} && npm run start`));
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. The pizzeria app still runs on this laptop (npm run stop stops it).` : "No demo tunnel is running.");
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, import_ora20.default)("Setting up login...").start();
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, import_ora20, PROVIDERS2, DEFAULT_SCOPES;
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
- import_ora20 = __toESM(require("ora"));
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, import_ora21.default)(q ? `Searching tenants for "${q}"...` : "Loading tenants...").start();
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, import_ora21, PAGE;
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
- import_ora21 = __toESM(require("ora"));
8323
+ import_ora22 = __toESM(require("ora"));
8226
8324
  init_admin();
8227
8325
  init_auth();
8228
8326
  PAGE = 15;
@@ -8296,7 +8394,7 @@ async function journey(label3, work, home, checks) {
8296
8394
  for (let i = 0; i < 10; i++) codes.push((await http(`${api}/reservations`, { headers: as("ana") })).status);
8297
8395
  add("free-requests", !codes.includes(402) && codes.every((c) => c === 200), `codes ${codes.join(",")}`);
8298
8396
  add("no-stale-tunnel", !codes.includes(503), codes.includes(503) ? "503 tunnel-offline seen" : "none");
8299
- const booked = await http(`${api}/reservations`, { method: "POST", headers: as("ana"), body: JSON.stringify({ name: "Ana", table: 4, time: "19:30" }) });
8397
+ const booked = await http(`${api}/reservations`, { method: "POST", headers: as("ana"), body: JSON.stringify({ time: "19:30" }) });
8300
8398
  const id = booked.body?.id;
8301
8399
  const benCancel = id ? await http(`${api}/reservations/${id}`, { method: "DELETE", headers: as("ben") }) : { status: 0 };
8302
8400
  const anaOpen = id ? await http(`${api}/reservations/${id}`, { headers: as("ana") }) : { status: 0 };
@@ -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 import_ora10 = __toESM(require("ora"));
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, import_ora10.default)(`Creating team "${name}"...`).start();
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 import_ora11 = __toESM(require("ora"));
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, import_ora11.default)(sampleIds.length ? `Designing the rule from ${sampleIds.length} traffic sample(s)\u2026` : "Designing the rule\u2026").start();
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, import_ora11.default)("Reading the spec for things to protect\u2026").start();
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 import_ora12 = __toESM(require("ora"));
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, import_ora12.default)("Checking delete impact...").start();
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, import_ora12.default)("Deleting...").start();
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 import_ora13 = __toESM(require("ora"));
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, import_ora13.default)("Searching recipes...").start() : null;
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, import_ora13.default)(`Reading ${ref.name}...`).start() : null;
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 import_ora14 = __toESM(require("ora"));
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, import_ora14.default)(`Reading ${proj2.projectId} v${proj2.apiVersion}...`).start() : null;
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, import_ora14.default)(`Publishing ${ref.name}...`).start() : null;
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, import_ora14.default)("Withdrawing...").start() : null;
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 import_ora15 = __toESM(require("ora"));
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, import_ora15.default)(summary + "...").start();
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 import_ora16 = __toESM(require("ora"));
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, import_ora16.default)(`Update limits for ${proj2.projectName}...`).start();
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, import_ora16.default)(`Request a ${rps}/s ceiling for ${proj2.projectName}...`).start();
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 import_ora17 = __toESM(require("ora"));
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, import_ora17.default)(`Auto-rebuy ${which}...`).start();
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 import_ora18 = __toESM(require("ora"));
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, import_ora18.default)(summary + "...").start();
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 import_ora26 = __toESM(require("ora"));
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 import_ora19 = __toESM(require("ora"));
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, import_ora19.default)(`Registering ${opts.domain}...`).start();
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, import_ora19.default)("Removing domain...").start();
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, import_ora19.default)("Setting base domain...").start();
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 import_ora22 = __toESM(require("ora"));
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, import_ora22.default)("Creating tenant...").start();
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,15 +13275,19 @@ 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, import_ora22.default)("Attaching tenant...").start();
13278
+ const spinner = (0, import_ora23.default)("Attaching tenant...").start();
13181
13279
  try {
13182
13280
  const out = await producer(caller, {
13183
13281
  method: "POST",
13184
13282
  path: `/projects/${proj2.projectId}/${proj2.apiVersion}/tenants`,
13185
- body: { tenant_name: opts.tenant, ...opts.authConfig ? { auth_config_id: opts.authConfig } : {} },
13283
+ // `login: {}`: the tenant's dev portal gets the same zero-config sign-in a new proxy's
13284
+ // first tenant gets (reused if the tenant already has one). Without it the portal
13285
+ // answers "No app clients found".
13286
+ body: { tenant_name: opts.tenant, ...opts.authConfig ? { auth_config_id: opts.authConfig } : {}, ...opts.login === false ? {} : { login: {} } },
13186
13287
  summary: `Attach tenant ${opts.tenant} \u2192 ${proj2.projectName} v${proj2.apiVersion}`
13187
13288
  });
13188
13289
  spinner.succeed(`Attached ${opts.tenant} to ${proj2.projectName} v${proj2.apiVersion}.`);
13290
+ if (out?.login_app_client_id) console.log(import_chalk53.default.dim(` Its dev portal signs people in: https://${opts.tenant}.portal.apiblaze.com/${proj2.apiVersion}`));
13189
13291
  if (opts.json) console.log(JSON.stringify(out));
13190
13292
  } catch (err) {
13191
13293
  spinner.fail("Attach failed.");
@@ -13210,7 +13312,7 @@ async function runTenantDelete(slug, opts) {
13210
13312
  return;
13211
13313
  }
13212
13314
  }
13213
- const spinner = (0, import_ora22.default)(hard ? "Trashing tenant..." : "Deleting tenant...").start();
13315
+ const spinner = (0, import_ora23.default)(hard ? "Trashing tenant..." : "Deleting tenant...").start();
13214
13316
  try {
13215
13317
  await admin({
13216
13318
  method: "DELETE",
@@ -13261,7 +13363,7 @@ async function runTenantCors(opts) {
13261
13363
  const { teamId } = await resolveTeam(opts.team);
13262
13364
  const origins = (opts.origins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
13263
13365
  const cors = origins.length ? { allowed_origins: origins } : null;
13264
- const spinner = (0, import_ora22.default)("Updating CORS...").start();
13366
+ const spinner = (0, import_ora23.default)("Updating CORS...").start();
13265
13367
  try {
13266
13368
  await admin({
13267
13369
  method: "PUT",
@@ -13278,7 +13380,7 @@ async function runTenantCors(opts) {
13278
13380
 
13279
13381
  // src/commands/tenant-drill.ts
13280
13382
  var import_chalk54 = __toESM(require("chalk"));
13281
- var import_ora23 = __toESM(require("ora"));
13383
+ var import_ora24 = __toESM(require("ora"));
13282
13384
  var import_crypto2 = require("crypto");
13283
13385
  init_admin();
13284
13386
  init_auth();
@@ -13318,7 +13420,7 @@ async function tenantHome(teamId, tenant2) {
13318
13420
  Tenant ${tenant2}`));
13319
13421
  console.log(import_chalk54.default.dim("Tenant auth/settings are SHARED: changes apply to every proxy this tenant serves.\n"));
13320
13422
  for (; ; ) {
13321
- const spinner = (0, import_ora23.default)("Reading tenant state...").start();
13423
+ const spinner = (0, import_ora24.default)("Reading tenant state...").start();
13322
13424
  const [iam, cors, emails, issuers, opaque, clients] = await Promise.all([
13323
13425
  admin({ method: "GET", path: `${base2}/iam`, summary: "Read IAM toggle" }).catch(() => null),
13324
13426
  admin({ method: "GET", path: `${base2}/cors`, summary: "Read tenant CORS" }).catch(() => null),
@@ -13385,7 +13487,7 @@ Tenant ${tenant2}`));
13385
13487
  async function loginMethodsMenu(teamId, tenant2, base2) {
13386
13488
  const { default: inquirer3 } = await import("inquirer");
13387
13489
  for (; ; ) {
13388
- const spinner = (0, import_ora23.default)("Loading login methods...").start();
13490
+ const spinner = (0, import_ora24.default)("Loading login methods...").start();
13389
13491
  const [rawClients, rawIssuers, rawOpaque] = await Promise.all([
13390
13492
  admin({ method: "GET", path: `${base2}/app-clients`, summary: "List APIblaze-hosted logins" }).catch(() => []),
13391
13493
  admin({ method: "GET", path: `${base2}/external-issuers`, summary: "List your-own-JWT logins" }).catch(() => null),
@@ -13584,7 +13686,7 @@ async function clientHome(base2, summary) {
13584
13686
  const id = summary.clientId ?? summary.client_id;
13585
13687
  const cBase = `${base2}/app-clients/${encodeURIComponent(id)}`;
13586
13688
  for (; ; ) {
13587
- const spinner = (0, import_ora23.default)("Reading app client...").start();
13689
+ const spinner = (0, import_ora24.default)("Reading app client...").start();
13588
13690
  const c = await admin({ method: "GET", path: cBase, summary: `Read app client ${id}` }).catch(() => summary);
13589
13691
  spinner.stop();
13590
13692
  const cb = c.authorizedCallbackUrls ?? c.authorized_callback_urls ?? [];
@@ -13679,7 +13781,7 @@ var DEFAULT_SCOPES2 = {
13679
13781
  async function providersMenu(cBase, clientLabel) {
13680
13782
  const { default: inquirer3 } = await import("inquirer");
13681
13783
  for (; ; ) {
13682
- const spinner = (0, import_ora23.default)("Loading providers...").start();
13784
+ const spinner = (0, import_ora24.default)("Loading providers...").start();
13683
13785
  const raw = await admin({ method: "GET", path: `${cBase}/providers`, summary: "List login providers" }).catch(() => []);
13684
13786
  spinner.stop();
13685
13787
  const providers = Array.isArray(raw) ? raw : [];
@@ -13770,7 +13872,7 @@ async function providersMenu(cBase, clientLabel) {
13770
13872
  // src/commands/spec.ts
13771
13873
  var fs15 = __toESM(require("fs"));
13772
13874
  var import_chalk55 = __toESM(require("chalk"));
13773
- var import_ora24 = __toESM(require("ora"));
13875
+ var import_ora25 = __toESM(require("ora"));
13774
13876
  init_admin();
13775
13877
  init_api();
13776
13878
  init_resolve();
@@ -13798,7 +13900,7 @@ async function runSpecSet(project, opts) {
13798
13900
  }
13799
13901
  const { teamId } = await resolveTeam(opts.team);
13800
13902
  const proj2 = await resolveProject(teamId, project, opts.apiversion);
13801
- const spinner = (0, import_ora24.default)("Uploading spec...").start();
13903
+ const spinner = (0, import_ora25.default)("Uploading spec...").start();
13802
13904
  try {
13803
13905
  const out = await admin({
13804
13906
  method: "POST",
@@ -13843,7 +13945,7 @@ async function runSpecCarry(project, opts) {
13843
13945
  }
13844
13946
  const { teamId } = await resolveTeam(opts.team);
13845
13947
  const proj2 = await resolveProject(teamId, project, opts.apiversion);
13846
- const spinner = (0, import_ora24.default)(`Carrying rules ${opts.from} \u2192 ${opts.to}...`).start();
13948
+ const spinner = (0, import_ora25.default)(`Carrying rules ${opts.from} \u2192 ${opts.to}...`).start();
13847
13949
  const res = await agentCall(`/projects/${proj2.projectId}/${proj2.apiVersion}/policies/route/carry`, "POST", {
13848
13950
  method: opts.method.toUpperCase(),
13849
13951
  from_resource: opts.from,
@@ -13866,7 +13968,7 @@ async function runSpecDeleteRule(project, opts) {
13866
13968
  const { teamId } = await resolveTeam(opts.team);
13867
13969
  const proj2 = await resolveProject(teamId, project, opts.apiversion);
13868
13970
  const encoded = opts.route.replace(/\{/g, "%7B").replace(/\}/g, "%7D");
13869
- const spinner = (0, import_ora24.default)(`Deleting rules for ${opts.method.toUpperCase()} ${opts.route}...`).start();
13971
+ const spinner = (0, import_ora25.default)(`Deleting rules for ${opts.method.toUpperCase()} ${opts.route}...`).start();
13870
13972
  const res = await agentCall(
13871
13973
  `/projects/${proj2.projectId}/${proj2.apiVersion}/policies/route/${opts.method.toUpperCase()}${encoded.startsWith("/") ? encoded : `/${encoded}`}`,
13872
13974
  "DELETE"
@@ -13881,7 +13983,7 @@ async function runSpecDeleteRule(project, opts) {
13881
13983
 
13882
13984
  // src/commands/agent.ts
13883
13985
  var import_chalk57 = __toESM(require("chalk"));
13884
- var import_ora25 = __toESM(require("ora"));
13986
+ var import_ora26 = __toESM(require("ora"));
13885
13987
  init_auth();
13886
13988
  init_admin();
13887
13989
  init_resolve();
@@ -14117,7 +14219,7 @@ async function runAgent(opts) {
14117
14219
  if (["exit", "quit", ":q"].includes(text.toLowerCase())) break;
14118
14220
  history.push({ role: "user", content: text });
14119
14221
  for (let step = 0; step < MAX_TOOL_STEPS; step++) {
14120
- const spinner = (0, import_ora25.default)({ text: "thinking...", color: "magenta" }).start();
14222
+ const spinner = (0, import_ora26.default)({ text: "thinking...", color: "magenta" }).start();
14121
14223
  let resp;
14122
14224
  try {
14123
14225
  resp = await callAgent(history, teamId);
@@ -14139,7 +14241,7 @@ async function runAgent(opts) {
14139
14241
  history.push({ role: "user", content: `TOOL_RESULT ${resp.action.tool}: error \u2014 unknown tool` });
14140
14242
  continue;
14141
14243
  }
14142
- const runSpinner = (0, import_ora25.default)({ text: `running ${tool.name}...`, color: "cyan" }).start();
14244
+ const runSpinner = (0, import_ora26.default)({ text: `running ${tool.name}...`, color: "cyan" }).start();
14143
14245
  try {
14144
14246
  const result = await tool.run(resp.action.args, { teamId });
14145
14247
  runSpinner.succeed(`${tool.name} \u2713`);
@@ -14557,7 +14659,7 @@ async function patchSetting(proj2, s, value, cfg) {
14557
14659
  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
14660
  body.tenant = tenant2;
14559
14661
  }
14560
- const spinner = (0, import_ora26.default)(`Set ${s.key}...`).start();
14662
+ const spinner = (0, import_ora27.default)(`Set ${s.key}...`).start();
14561
14663
  try {
14562
14664
  await admin({
14563
14665
  method,
@@ -14973,7 +15075,7 @@ async function transformsMenu(proj2) {
14973
15075
  };
14974
15076
  }
14975
15077
  const condition = await buildCondition(ans.phase);
14976
- const spinner = (0, import_ora26.default)("Creating rule...").start();
15078
+ const spinner = (0, import_ora27.default)("Creating rule...").start();
14977
15079
  try {
14978
15080
  await admin({
14979
15081
  method: "POST",
@@ -15166,7 +15268,7 @@ async function agentsMenu(proj2, opts) {
15166
15268
 
15167
15269
  // src/commands/group.ts
15168
15270
  var import_chalk59 = __toESM(require("chalk"));
15169
- var import_ora27 = __toESM(require("ora"));
15271
+ var import_ora28 = __toESM(require("ora"));
15170
15272
  init_auth();
15171
15273
  init_consumer_auth();
15172
15274
  init_admin();
@@ -15305,7 +15407,7 @@ async function runGroupCreate(name, opts) {
15305
15407
  }
15306
15408
  }
15307
15409
  }
15308
- const spinner = (0, import_ora27.default)(`Creating group ${name}...`).start();
15410
+ const spinner = (0, import_ora28.default)(`Creating group ${name}...`).start();
15309
15411
  try {
15310
15412
  await t.call("/v1/admin/groups", "POST", { name, admins: [adminSub] });
15311
15413
  spinner.succeed(`Group ${import_chalk59.default.bold(name)} created in ${t.tenant}.`);
@@ -15317,7 +15419,7 @@ async function runGroupCreate(name, opts) {
15317
15419
  async function runGroupDelete(name, opts) {
15318
15420
  const t = await resolveTransport(opts);
15319
15421
  const g = await groupByName(t, name);
15320
- const spinner = (0, import_ora27.default)(`Deleting group ${name}...`).start();
15422
+ const spinner = (0, import_ora28.default)(`Deleting group ${name}...`).start();
15321
15423
  try {
15322
15424
  await t.call(`/v1/admin/groups/${encodeURIComponent(g.id)}`, "DELETE");
15323
15425
  spinner.succeed(`Group ${import_chalk59.default.bold(name)} deleted.`);
@@ -15350,7 +15452,7 @@ async function runGroupAddUser(userRef, groupName, opts) {
15350
15452
  const g = await groupByName(t, groupName);
15351
15453
  const u = await userByRefOrCreate(t, userRef);
15352
15454
  const role = opts.role === "admin" ? "admin" : "member";
15353
- const spinner = (0, import_ora27.default)(`Adding ${label2(u)} to ${g.name}...`).start();
15455
+ const spinner = (0, import_ora28.default)(`Adding ${label2(u)} to ${g.name}...`).start();
15354
15456
  try {
15355
15457
  await t.call(`/v1/admin/groups/${encodeURIComponent(g.id)}/members`, "POST", { abz_sub: u.abz_sub, role });
15356
15458
  spinner.succeed(`${import_chalk59.default.bold(label2(u))} added to ${import_chalk59.default.bold(g.name)} as ${role}.`);
@@ -15362,7 +15464,7 @@ async function runGroupAddUser(userRef, groupName, opts) {
15362
15464
  async function runGroupRemoveUser(userRef, groupName, opts) {
15363
15465
  const t = await resolveTransport(opts);
15364
15466
  const [g, u] = [await groupByName(t, groupName), await userByRef(t, userRef)];
15365
- const spinner = (0, import_ora27.default)(`Removing ${label2(u)} from ${g.name}...`).start();
15467
+ const spinner = (0, import_ora28.default)(`Removing ${label2(u)} from ${g.name}...`).start();
15366
15468
  try {
15367
15469
  await t.call(`/v1/admin/groups/${encodeURIComponent(g.id)}/members/_?abz_sub=${encodeURIComponent(u.abz_sub)}`, "DELETE");
15368
15470
  spinner.succeed(`${import_chalk59.default.bold(label2(u))} removed from ${import_chalk59.default.bold(g.name)}.`);
@@ -15374,7 +15476,7 @@ async function runGroupRemoveUser(userRef, groupName, opts) {
15374
15476
  async function runGroupAddGroup(childName, parentName, opts) {
15375
15477
  const t = await resolveTransport(opts);
15376
15478
  const [child, parent] = [await groupByName(t, childName), await groupByName(t, parentName)];
15377
- const spinner = (0, import_ora27.default)(`Nesting ${child.name} under ${parent.name}...`).start();
15479
+ const spinner = (0, import_ora28.default)(`Nesting ${child.name} under ${parent.name}...`).start();
15378
15480
  try {
15379
15481
  await t.call(`/v1/admin/groups/${encodeURIComponent(parent.id)}/subgroups`, "POST", { child_group_id: child.id });
15380
15482
  spinner.succeed(`${import_chalk59.default.bold(child.name)} is now a subgroup of ${import_chalk59.default.bold(parent.name)}.`);
@@ -15386,7 +15488,7 @@ async function runGroupAddGroup(childName, parentName, opts) {
15386
15488
  async function runGroupRemoveGroup(childName, parentName, opts) {
15387
15489
  const t = await resolveTransport(opts);
15388
15490
  const [child, parent] = [await groupByName(t, childName), await groupByName(t, parentName)];
15389
- const spinner = (0, import_ora27.default)(`Un-nesting ${child.name} from ${parent.name}...`).start();
15491
+ const spinner = (0, import_ora28.default)(`Un-nesting ${child.name} from ${parent.name}...`).start();
15390
15492
  try {
15391
15493
  await t.call(`/v1/admin/groups/${encodeURIComponent(parent.id)}/subgroups/${encodeURIComponent(child.id)}`, "DELETE");
15392
15494
  spinner.succeed(`${import_chalk59.default.bold(child.name)} un-nested from ${import_chalk59.default.bold(parent.name)}.`);
@@ -15417,7 +15519,7 @@ init_admins();
15417
15519
 
15418
15520
  // src/commands/preapprove.ts
15419
15521
  var import_chalk60 = __toESM(require("chalk"));
15420
- var import_ora28 = __toESM(require("ora"));
15522
+ var import_ora29 = __toESM(require("ora"));
15421
15523
  init_preapproval();
15422
15524
  async function runPreapprove(who, opts) {
15423
15525
  const tenant2 = await resolveProducerTenant(opts.tenant);
@@ -15441,13 +15543,13 @@ async function runPreapprove(who, opts) {
15441
15543
  throw new Error("Who? Pass an email or a domain: `apiblaze preapprove someone@acme.com` (or `apiblaze preapprove --list`).");
15442
15544
  }
15443
15545
  if (opts.remove) {
15444
- const spinner2 = (0, import_ora28.default)(`Removing ${who} from ${tenant2}\u2026`).start();
15546
+ const spinner2 = (0, import_ora29.default)(`Removing ${who} from ${tenant2}\u2026`).start();
15445
15547
  const { removed, value } = await removePreapprovalRule(tenant2, who);
15446
15548
  if (removed) spinner2.succeed(`${import_chalk60.default.bold(value)} is no longer pre-approved for ${tenant2}.`);
15447
15549
  else spinner2.warn(`No pre-approval rule for ${import_chalk60.default.bold(value)} in ${tenant2} \u2014 nothing to remove.`);
15448
15550
  return;
15449
15551
  }
15450
- const spinner = (0, import_ora28.default)(`Pre-approving ${who} for ${tenant2}\u2026`).start();
15552
+ const spinner = (0, import_ora29.default)(`Pre-approving ${who} for ${tenant2}\u2026`).start();
15451
15553
  try {
15452
15554
  const { kind, value } = await addPreapprovalRule(tenant2, who, opts.group);
15453
15555
  const what = kind === "domain" ? `Anyone @${value}` : value;
@@ -15466,7 +15568,7 @@ init_llm();
15466
15568
 
15467
15569
  // src/commands/consumer.ts
15468
15570
  var import_chalk61 = __toESM(require("chalk"));
15469
- var import_ora29 = __toESM(require("ora"));
15571
+ var import_ora30 = __toESM(require("ora"));
15470
15572
  init_admin();
15471
15573
  init_resolve();
15472
15574
  init_caller();
@@ -15512,7 +15614,7 @@ async function runConsumerLogin(opts) {
15512
15614
  if (!picked) process.exit(1);
15513
15615
  tenant2 = picked;
15514
15616
  }
15515
- const s2 = (0, import_ora29.default)("Finding the login app...").start();
15617
+ const s2 = (0, import_ora30.default)("Finding the login app...").start();
15516
15618
  const clients = await admin({ method: "GET", path: `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/app-clients`, summary: `List app clients for ${tenant2}` }).catch(() => []);
15517
15619
  s2.stop();
15518
15620
  const usable = (Array.isArray(clients) ? clients : []).filter((c) => c && (c.client_id || c.clientId));
@@ -15573,7 +15675,7 @@ async function runConsumerTokens(opts) {
15573
15675
  async function runConsumerApikeys(opts) {
15574
15676
  const creds = requireConsumer2();
15575
15677
  const { default: inquirer3 } = await import("inquirer");
15576
- const spinner = (0, import_ora29.default)("Loading your API keys...").start();
15678
+ const spinner = (0, import_ora30.default)("Loading your API keys...").start();
15577
15679
  const list = await consumerFetch(creds, withPage("/apikeys", null));
15578
15680
  if (list.status >= 400) {
15579
15681
  spinner.stop();
@@ -15630,7 +15732,7 @@ async function runConsumerApikeys(opts) {
15630
15732
  const body = { environment: answers.environment };
15631
15733
  if (answers.description) body.description = answers.description;
15632
15734
  if (answers.expiresDays) body.expires_in_seconds = Number(answers.expiresDays) * 86400;
15633
- const s2 = (0, import_ora29.default)("Creating key...").start();
15735
+ const s2 = (0, import_ora30.default)("Creating key...").start();
15634
15736
  const created = await consumerFetch(list.creds, "/apikeys", { method: "POST", body: JSON.stringify(body) });
15635
15737
  if (created.status >= 400) {
15636
15738
  s2.fail(`Create failed (${created.status}): ${created.data?.error ?? ""}`);
@@ -15644,7 +15746,7 @@ async function runConsumerApikeys(opts) {
15644
15746
 
15645
15747
  // src/commands/sidecar.ts
15646
15748
  var import_chalk62 = __toESM(require("chalk"));
15647
- var import_ora30 = __toESM(require("ora"));
15749
+ var import_ora31 = __toESM(require("ora"));
15648
15750
  var fs16 = __toESM(require("fs"));
15649
15751
  var path12 = __toESM(require("path"));
15650
15752
  init_admin();
@@ -15691,7 +15793,7 @@ function installSidecarPackage(root) {
15691
15793
  }
15692
15794
  const has = (f) => fs16.existsSync(path12.join(root, f));
15693
15795
  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, import_ora30.default)(`Installing the apiblaze package (${pm.cmd})\u2026`).start();
15796
+ const spinner = (0, import_ora31.default)(`Installing the apiblaze package (${pm.cmd})\u2026`).start();
15695
15797
  try {
15696
15798
  const { execSync } = require("child_process");
15697
15799
  execSync(`${pm.cmd} ${pm.add} apiblaze`, { cwd: root, stdio: "ignore" });
@@ -15840,7 +15942,7 @@ async function runAnonymousInit(root, router, opts) {
15840
15942
  const { sidecarInitAnonymous: sidecarInitAnonymous2 } = await Promise.resolve().then(() => (init_api(), api_exports));
15841
15943
  const { saveAnonCred: saveAnonCred2, clearAnonCred: clearAnonCred2 } = await Promise.resolve().then(() => (init_anon_cred(), anon_cred_exports));
15842
15944
  if (opts.newSession) clearAnonCred2();
15843
- const spinner = (0, import_ora30.default)("Setting up a sidecar (no login needed)...").start();
15945
+ const spinner = (0, import_ora31.default)("Setting up a sidecar (no login needed)...").start();
15844
15946
  let out;
15845
15947
  try {
15846
15948
  out = await sidecarInitAnonymous2();
@@ -15897,7 +15999,7 @@ async function runSidecar(opts) {
15897
15999
  const mustMint = !existingKey || opts.rotate || switchingTeam;
15898
16000
  let token = existingKey ?? "";
15899
16001
  if (mustMint) {
15900
- const spinner = (0, import_ora30.default)(existingKey ? "Re-establishing the sidecar (minting a fresh invoke key)..." : "Setting up the sidecar (tenant + non-expiring invoke key)...").start();
16002
+ 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
16003
  try {
15902
16004
  const out = await admin({
15903
16005
  method: "POST",
@@ -15943,7 +16045,7 @@ async function runSidecar(opts) {
15943
16045
 
15944
16046
  // src/commands/origins.ts
15945
16047
  var import_chalk63 = __toESM(require("chalk"));
15946
- var import_ora31 = __toESM(require("ora"));
16048
+ var import_ora32 = __toESM(require("ora"));
15947
16049
  init_admin();
15948
16050
  init_resolve();
15949
16051
  init_auth();
@@ -15994,7 +16096,7 @@ async function runOriginsApprove(origin, opts) {
15994
16096
  console.error(import_chalk63.default.red("Not logged in and no anonymous workspace. Run `apiblaze init` first."));
15995
16097
  process.exit(1);
15996
16098
  }
15997
- const spinner2 = (0, import_ora31.default)(`Approving ${origin} (anonymous)...`).start();
16099
+ const spinner2 = (0, import_ora32.default)(`Approving ${origin} (anonymous)...`).start();
15998
16100
  try {
15999
16101
  const out = await cpFetch(cred.cp_key, `/teams/${encodeURIComponent(cred.team_id)}/sidecar/approve`, { method: "POST", body: JSON.stringify({ origin }) });
16000
16102
  spinner2.succeed(`Approved ${origin} \u2192 proxy ${out.project_id}. Routing within ~5 min.`);
@@ -16005,7 +16107,7 @@ async function runOriginsApprove(origin, opts) {
16005
16107
  return;
16006
16108
  }
16007
16109
  const { teamId } = await resolveTeam(opts.team);
16008
- const spinner = (0, import_ora31.default)(`Approving ${origin}...`).start();
16110
+ const spinner = (0, import_ora32.default)(`Approving ${origin}...`).start();
16009
16111
  try {
16010
16112
  const out = await admin({
16011
16113
  method: "POST",
@@ -16022,7 +16124,7 @@ async function runOriginsApprove(origin, opts) {
16022
16124
  }
16023
16125
  async function runOriginsDeny(origin, opts) {
16024
16126
  const { teamId } = await resolveTeam(opts.team);
16025
- const spinner = (0, import_ora31.default)(`Dismissing ${origin}...`).start();
16127
+ const spinner = (0, import_ora32.default)(`Dismissing ${origin}...`).start();
16026
16128
  try {
16027
16129
  await admin({ method: "POST", path: `/teams/${encodeURIComponent(teamId)}/sidecar/dismiss`, body: { origin }, summary: `Dismiss sidecar origin ${origin}` });
16028
16130
  spinner.succeed(`Dismissed ${origin}. It won't be suggested again.`);
@@ -16033,7 +16135,7 @@ async function runOriginsDeny(origin, opts) {
16033
16135
  }
16034
16136
  async function runOriginsRemove(origin, opts) {
16035
16137
  const { teamId } = await resolveTeam(opts.team);
16036
- const spinner = (0, import_ora31.default)(`Removing the proxy for ${origin}...`).start();
16138
+ const spinner = (0, import_ora32.default)(`Removing the proxy for ${origin}...`).start();
16037
16139
  try {
16038
16140
  await admin({ method: "POST", path: `/teams/${encodeURIComponent(teamId)}/sidecar/remove`, body: { origin }, summary: `Un-route sidecar origin ${origin}` });
16039
16141
  spinner.succeed(`Removed ${origin}. Your app will stop routing it (goes direct) within ~5 min.`);
@@ -17611,7 +17713,7 @@ program.command("create").description(`Put an app that is already DEPLOYED behin
17611
17713
  }
17612
17714
  });
17613
17715
  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 30 seconds: a small booking app on this machine, with ${B.product} in front of it (no login, no prompts)`).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").addOption(new import_commander.Option("--serve <dir>", "internal: run the demo servers and tunnel").hideHelp()).action(async (actionArg, opts) => {
17716
+ 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
17717
  if (actionArg === "stop") return runDemoStop();
17616
17718
  if (actionArg === "start" || actionArg === "restart") return runDemoStart(opts);
17617
17719
  if (actionArg === "logs") return runDemoLogs();
@@ -17774,7 +17876,7 @@ tenant.command("manage").description("Browse & edit one tenant: settings, login
17774
17876
  tenant.command("use").description("Set the sticky tenant scope for future commands (search-first; --clear to unset)").argument("[query]", "Search by tenant name/display name").option("--clear", "Clear the tenant scope").option("--team <id|name>", "Team (defaults to active team)").action(action((query, opts) => runTenantUse(query, opts)));
17775
17877
  tenant.command("list").description("List tenants in your team (--q searches server-side)").option("--q <search>", "Filter by tenant name or display name").option("--limit <n>", "Page size (max 200)").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantList(opts)));
17776
17878
  tenant.command("create").description("Create a tenant in your team (tenant names are lowercase alphanumeric and globally unique)").requiredOption("--name <name>", "Tenant name \u2014 becomes the slug ({name}.portal.apiblaze.com) unless --slug overrides; also the display label").option("--slug <tenant_name>", "Explicit tenant slug when it should differ from --name").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Output machine-readable JSON").action(action((opts) => runTenantCreate(opts)));
17777
- tenant.command("attach").description("Attach a tenant to a proxy").argument("<project>", "Project name or id").requiredOption("--tenant <slug>", "Tenant slug to attach").option("--auth-config <id>", "Auth config id to bind").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTenantAttach(project, opts)));
17879
+ tenant.command("attach").description("Attach a tenant to a proxy").argument("<project>", "Project name or id").requiredOption("--tenant <slug>", "Tenant slug to attach").option("--auth-config <id>", "Auth config id to bind").option("--no-login", "Do not give the tenant a dev-portal sign-in (by default it gets the zero-config GitHub one, unless it already has one)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTenantAttach(project, opts)));
17778
17880
  tenant.command("delete").description("Move a tenant to the trash (reversible); --purge destroys it and everything under it").argument("<slug>", "Tenant slug to delete").option("--team <id|name>", "Team (defaults to active team)").option("-y, --yes", "Skip the confirmation prompt").option("--purge", "Permanently destroy the tenant and all its data (cannot be undone)").option("--no-wait", "With --purge: return as soon as the teardown is accepted instead of waiting for it to finish").option("--json", "Output machine-readable JSON").action(action((slug, opts) => runTenantDelete(slug, opts)));
17779
17881
  tenant.command("cors").description("Set the CORS allow-list for a tenant").requiredOption("--tenant <slug>", "Tenant slug").option("--origins <list>", 'Comma-separated origins (or "*"); empty clears').option("--team <id|name>", "Team (defaults to active team)").action(action((opts) => runTenantCors(opts)));
17780
17882
  var admins = new import_commander.Command("admins").description("Manage who can administer a tenant's users & groups (the first-admin bootstrap the widget needs)").option("--tenant <slug>", "Tenant slug (e.g. nino)").option("--team <id|name>", "Team (defaults to active team, or your anonymous workspace)").option("--json", "Output machine-readable JSON").action(action((opts) => runAdminsList(opts)));