apiblaze 0.21.22 → 0.21.23

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.22";
37
+ version = "0.21.23";
38
38
  }
39
39
  });
40
40
 
@@ -6367,8 +6367,8 @@ var init_files = __esm({
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
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: [name, table, time]\n properties:\n name: { type: string, example: Ana }\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//\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}\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 if (!who) return reply(401, { error: 'calls come through APIblaze, which says who is calling' });\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// Every call goes through APIblaze with ONE server key (kept here, never in the\n// browser) and says who is acting with X-End-User-Id. The chat and the two\n// widgets use apiblaze's own server helpers (lib/apiblaze-server.js, copied in by\n// `npx apiblaze@latest demo` \u2014 the same code as `import \u2026 from 'apiblaze/server'`).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\nconst { recent } = require('./backend');\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)); };\nconst mask = (k) => (k ? `${k.slice(0, 7)}\u2022\u2022\u2022\u2022${k.slice(-4)}` : '');\n\nlet lib = null;\ntry { lib = require('./lib/apiblaze-server.js'); } catch { /* run without the widgets */ }\n\n// The person using the page. A real app takes this from its own login.\nconst ME = { userId: 'ana', email: 'ana@ninopizza.example', label: 'Ana' };\n\nasync function call(op, { as = 'ana', id, key = 'nino', body } = {}) {\n const base = env('APIBLAZE_URL').replace(/\\/$/, '');\n const headers = {\n 'content-type': 'application/json',\n 'x-api-key': key === 'gino' ? env('APIBLAZE_GINO_KEY') : env('APIBLAZE_SERVER_KEY'),\n 'x-end-user-id': as,\n };\n const req = op === 'book' ? { method: 'POST', url: `${base}/reservations`, body: JSON.stringify(body || { name: 'Ana', table: 4, time: '19:30', guests: 2 }) }\n : op === 'cancel' ? { method: 'DELETE', url: `${base}/reservations/${encodeURIComponent(id || '')}` }\n : op === 'open' ? { method: 'GET', 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 project = env('APIBLAZE_NAME');\n const tenant = env('APIBLAZE_TENANT');\n const out = {};\n out.chat = lib.createApiblazeChat({\n project: `${project}-${tenant}`, apiKey: env('APIBLAZE_SERVER_KEY'),\n host: 'tryabz.run', environment: 'dev', getUser: () => ({ userId: ME.userId }),\n });\n if (env('APIBLAZE_CP_KEY') && env('APIBLAZE_WIDGETS') === '1') {\n const user = () => ({ tenant, userId: ME.userId, email: ME.email, label: ME.label });\n out.keys = lib.createApiblazeKeys({ cpKey: env('APIBLAZE_CP_KEY'), getUser: user, environment: 'dev' });\n out.groups = lib.createApiblazeGroups({ cpKey: env('APIBLAZE_CP_KEY'), getUser: user });\n }\n return out;\n}\n\nfunction startApp(port = Number(env('APP_PORT', '3000'))) {\n const w = 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 if (req.method === 'GET' && p === '/apiblaze-widgets.js' && fs.existsSync(path.join(pub, 'apiblaze-widgets.js'))) {\n res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, 'apiblaze-widgets.js')));\n }\n if (p === '/api/config') {\n return json(res, 200, {\n name: env('APIBLAZE_NAME'), apiUrl: env('APIBLAZE_URL'), mcpUrl: env('APIBLAZE_MCP_URL'),\n portalUrl: env('APIBLAZE_PORTAL_URL'), key: mask(env('APIBLAZE_SERVER_KEY')), ginoKey: mask(env('APIBLAZE_GINO_KEY')),\n backendPort: env('BACKEND_PORT', '3001'), rps: Number(env('APIBLAZE_DEMO_RPS')) || null, rateCard: env('APIBLAZE_RATE_CARD') === '1', widgets: { chat: !!w.chat, keys: !!w.keys, groups: !!w.groups },\n });\n }\n if (p === '/api/ticker') return json(res, 200, recent.slice(0, 12));\n if (p === '/api/apiblaze/chat' && w.chat) return viaHandler(w.chat.handler, req, res, raw);\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'), { as: String(b.as || 'ana'), id: b.id, key: b.key, body: b.body }));\n }\n if (req.method === 'POST' && p === '/api/hammer') {\n // 30 calls at once, as one customer: the demo's rate limit lets only some through.\n const codes = await Promise.all(Array.from({ length: 30 }, () => call('list').then((r) => r.status).catch(() => 0)));\n const served = codes.filter((c) => c === 200).length;\n const limited = codes.filter((c) => c === 429).length;\n return json(res, 200, { sent: codes.length, served, limited, other: codes.length - served - limited });\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",
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//\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 if (!who) return reply(401, { error: 'calls come through APIblaze, which says who is calling' });\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// Every call goes through APIblaze with ONE server key (kept here, never in the\n// browser) and says who is acting with X-End-User-Id. The chat and the two\n// widgets use apiblaze's own server helpers (lib/apiblaze-server.js, copied in by\n// `npx apiblaze@latest demo` \u2014 the same code as `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)); };\nconst mask = (k) => (k ? `${k.slice(0, 7)}\u2022\u2022\u2022\u2022${k.slice(-4)}` : '');\n\nlet lib = null;\ntry { lib = require('./lib/apiblaze-server.js'); } catch { /* run without the widgets */ }\n\n// Who is using the page. A real app takes this from its own login; the demo lets you switch.\n// App tab: ana, ben \u2014 customers of Nino Pizza\n// Admin tab: ana \u2014 admin of Nino Pizza; gina \u2014 admin of Gino Pizza\nconst PEOPLE = {\n ana: { userId: 'ana', label: 'Ana', email: 'ana@ninopizza.example', tenant: () => env('APIBLAZE_TENANT') },\n ben: { userId: 'ben', label: 'Ben', email: 'ben@ninopizza.example', tenant: () => env('APIBLAZE_TENANT') },\n gina: { userId: 'gina', label: 'Gina', email: 'gina@ginopizza.example', tenant: () => env('APIBLAZE_GINO_TENANT') },\n};\nfunction personOf(req) {\n const as = new URL(req.url, 'http://x').searchParams.get('as') || 'ana';\n return PEOPLE[as] || PEOPLE.ana;\n}\n\nasync function call(op, { as = 'ana', id, key = 'nino', body } = {}) {\n const base = env('APIBLAZE_URL').replace(/\\/$/, '');\n const headers = {\n 'content-type': 'application/json',\n 'x-api-key': key === 'gino' ? env('APIBLAZE_GINO_KEY') : env('APIBLAZE_SERVER_KEY'),\n 'x-end-user-id': as,\n };\n const req = op === 'book' ? { method: 'POST', url: `${base}/reservations`, body: JSON.stringify(body || { name: (PEOPLE[as] || PEOPLE.ana).label, table: 4, time: '19:30', guests: 2 }) }\n : op === 'cancel' ? { method: 'DELETE', url: `${base}/reservations/${encodeURIComponent(id || '')}` }\n : op === 'open' ? { method: 'GET', 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 = {};\n out.chat = lib.createApiblazeChat({\n project: `${env('APIBLAZE_NAME')}-${env('APIBLAZE_TENANT')}`, apiKey: env('APIBLAZE_SERVER_KEY'),\n host: 'tryabz.run', environment: 'dev', getUser: (req) => ({ userId: personOf(req).userId }),\n });\n if (env('APIBLAZE_CP_KEY') && env('APIBLAZE_WIDGETS') === '1') {\n const user = (req) => { const p = personOf(req); return { tenant: p.tenant(), userId: p.userId, email: p.email, label: p.label }; };\n out.keys = lib.createApiblazeKeys({ cpKey: env('APIBLAZE_CP_KEY'), getUser: user, environment: 'dev' });\n out.groups = lib.createApiblazeGroups({ cpKey: env('APIBLAZE_CP_KEY'), getUser: user });\n }\n return out;\n}\n\nfunction startApp(port = Number(env('APP_PORT', '3000'))) {\n const w = 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 if (req.method === 'GET' && p === '/apiblaze-widgets.js' && fs.existsSync(path.join(pub, 'apiblaze-widgets.js'))) {\n res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, 'apiblaze-widgets.js')));\n }\n if (p === '/api/config') {\n return json(res, 200, {\n name: env('APIBLAZE_NAME'), apiUrl: env('APIBLAZE_URL'), mcpUrl: env('APIBLAZE_MCP_URL'),\n portalUrl: env('APIBLAZE_PORTAL_URL'), gino: !!env('APIBLAZE_GINO_TENANT'),\n backendPort: env('BACKEND_PORT', '3001'), widgets: { chat: !!w.chat, keys: !!w.keys, groups: !!w.groups },\n });\n }\n if (p === '/api/apiblaze/chat' && w.chat) return viaHandler(w.chat.handler, req, res, raw);\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'), { as: String(b.as || 'ana'), id: b.id, key: b.key, body: b.body }));\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
6372
  "start.js": "// Starts the pizzeria: the backend (port 3001) and the website (port 3000).\n// `npx apiblaze@latest demo` runs this for you and connects APIblaze to the backend.\n// To run it yourself: `node start.js`, then in another terminal\n// npx apiblaze@latest dev --port 3001 --project <the name in .env.local> --auto\n\nconst fs = require('fs');\nconst path = require('path');\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 await startBackend(backendPort);\n await startApp(appPort);\n return { backendPort, appPort };\n}\n\nmodule.exports = { start, loadEnv };\nif (require.main === module) {\n start().then(({ appPort }) => console.log(`Pizzeria running: http://localhost:${appPort}`))\n .catch((e) => { console.error(`Could not start: ${e.message}`); process.exit(1); });\n}\n",
6373
6373
  "public/index.html": `<!doctype html>
6374
6374
  <html lang="en">
@@ -6380,7 +6380,7 @@ var init_files = __esm({
6380
6380
  :root {
6381
6381
  --bg: #faf7f2; --card: #fff; --ink: #1d1b19; --muted: #6b6560; --line: #e8e2d9; --soft: #f3eee6;
6382
6382
  --accent: #e4572e; --accent-ink: #fff; --good: #15803d; --good-bg: #ecfdf3; --bad: #c2410c; --bad-bg: #fff1e8;
6383
- --live: #16a34a; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
6383
+ --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
6384
6384
  }
6385
6385
  @media (prefers-color-scheme: dark) {
6386
6386
  :root { --bg: #161412; --card: #1f1c1a; --ink: #f3efe9; --muted: #a39b92; --line: #34302c; --soft: #26221f;
@@ -6388,188 +6388,163 @@ var init_files = __esm({
6388
6388
  }
6389
6389
  * { box-sizing: border-box; }
6390
6390
  body { margin: 0; background: var(--bg); color: var(--ink); font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
6391
- main { max-width: 1080px; margin: 0 auto; padding: 28px 16px 64px; }
6392
- h1 { font-size: 30px; margin: 0; letter-spacing: -0.02em; }
6393
- .lede { font-size: 18px; color: var(--muted); margin: 6px 0 20px; }
6394
- .card { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 20px; }
6395
- .hero { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 16px; }
6396
- @media (max-width: 820px) { .hero, .grid { grid-template-columns: 1fr !important; } }
6397
- .addr { display: grid; gap: 14px; }
6398
- .addr .label { font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
6399
- .addr .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
6400
- code, .mono { font-family: var(--mono); font-size: 13px; }
6401
- .url { background: var(--soft); padding: 8px 10px; border-radius: 10px; word-break: break-all; flex: 1; min-width: 0; }
6402
- button { background: var(--soft); color: var(--ink); border: 1px solid var(--line); border-radius: 10px; padding: 9px 13px; font-size: 14px; cursor: pointer; }
6403
- button:hover { border-color: var(--muted); }
6391
+ main { max-width: 1000px; margin: 0 auto; padding: 20px 16px 56px; }
6392
+ .live { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; font-size: 13px; color: var(--muted);
6393
+ background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 8px 12px; }
6394
+ .live b { color: var(--ink); font-weight: 600; }
6395
+ .live code { font-family: var(--mono); font-size: 12px; color: var(--ink); word-break: break-all; }
6396
+ .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #16a34a; margin-right: 6px; }
6397
+ nav { display: flex; gap: 6px; margin: 18px 0 14px; }
6398
+ nav button { border: 0; background: transparent; font-size: 17px; padding: 8px 14px; border-radius: 10px; cursor: pointer; color: var(--muted); }
6399
+ nav button[aria-selected="true"] { background: var(--card); color: var(--ink); font-weight: 600; box-shadow: 0 0 0 1px var(--line); }
6400
+ .who { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; font-size: 15px; color: var(--muted); }
6401
+ select { font-size: 15px; padding: 6px 10px; border-radius: 10px; border: 1px solid var(--line); background: var(--card); color: var(--ink); }
6402
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
6403
+ @media (max-width: 820px) { .cols { grid-template-columns: 1fr; } }
6404
+ .card { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 18px; }
6405
+ h2 { font-size: 19px; margin: 0 0 4px; }
6406
+ .sub { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
6407
+ button.act { background: var(--soft); color: var(--ink); border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; font-size: 14px; cursor: pointer; }
6404
6408
  button.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600; }
6405
- .hint { color: var(--muted); font-size: 14px; margin: 2px 0 0; }
6406
- .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--live); margin-right: 6px; box-shadow: 0 0 0 0 rgba(22,163,74,.6); animation: pulse 1.6s infinite; }
6407
- @keyframes pulse { 70% { box-shadow: 0 0 0 8px rgba(22,163,74,0); } 100% { box-shadow: 0 0 0 0 rgba(22,163,74,0); } }
6408
- .ticker { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 12px; }
6409
- .ticker ol { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; max-height: 230px; overflow: auto; }
6410
- .ticker li { font-family: var(--mono); font-size: 12.5px; background: var(--soft); border-radius: 8px; padding: 6px 9px; display: flex; gap: 8px; }
6411
- .ticker li.new { animation: flash 1.2s ease-out; }
6409
+ ul.res { list-style: none; padding: 0; margin: 12px 0 0; display: grid; gap: 8px; }
6410
+ ul.res li { display: flex; align-items: center; gap: 10px; background: var(--soft); border-radius: 10px; padding: 10px 12px; }
6411
+ ul.res li.new { animation: flash 1.5s ease-out; }
6412
6412
  @keyframes flash { from { background: var(--good-bg); } }
6413
- .ticker .st { margin-left: auto; color: var(--muted); }
6413
+ ul.res li span { flex: 1; }
6414
6414
  .empty { color: var(--muted); font-size: 14px; }
6415
- h2 { font-size: 20px; margin: 0 0 4px; }
6416
- h3 { font-size: 17px; margin: 0 0 4px; }
6417
- .section { margin: 28px 0 10px; font-size: 13px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
6418
- .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
6419
- .grid .card { padding: 16px; }
6420
- .sub { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
6421
- .out { border-radius: 10px; padding: 8px 10px; font-size: 14px; margin-top: 10px; background: var(--soft); }
6415
+ .out { border-radius: 10px; padding: 8px 10px; font-size: 14px; margin-top: 10px; }
6422
6416
  .out.good { background: var(--good-bg); color: var(--good); font-weight: 600; }
6423
6417
  .out.bad { background: var(--bad-bg); color: var(--bad); font-weight: 600; }
6424
- .out small { display: block; font-weight: 400; color: var(--muted); font-family: var(--mono); font-size: 12px; }
6425
- .wide { grid-column: span 3; }
6426
- @media (max-width: 820px) { .wide { grid-column: auto; } }
6427
- .footer { margin-top: 26px; font-size: 17px; }
6428
- #chat { min-height: 420px; }
6418
+ #chat { min-height: 460px; }
6419
+ .chatcard { padding: 0; overflow: hidden; }
6420
+ .next { margin-top: 16px; color: var(--muted); font-size: 15px; }
6421
+ .next a { color: var(--accent); cursor: pointer; text-decoration: none; font-weight: 600; }
6422
+ .next code { font-family: var(--mono); font-size: 13px; }
6423
+ .stack { display: grid; gap: 16px; }
6429
6424
  </style>
6430
6425
  </head>
6431
6426
  <body>
6432
6427
  <main>
6433
- <h1>\u{1F355} Nino's Pizza</h1>
6434
- <p class="lede">This booking app is running on your laptop. One command put it on the internet, with a door for AI assistants. You wrote no code for it.</p>
6428
+ <div class="live"><span><span class="dot"></span><b>Running on your laptop, live on the internet</b></span>
6429
+ <span>API <code id="api">\u2026</code></span><span>MCP <code id="mcp">\u2026</code></span></div>
6435
6430
 
6436
- <section class="hero">
6437
- <div class="card">
6438
- <div class="addr">
6439
- <div>
6440
- <div class="label"><span class="dot"></span>Your API, live on the internet</div>
6441
- <div class="row"><span class="url mono" id="api">\u2026</span><button data-copy="api">Copy</button></div>
6442
- <p class="hint">Anyone you give a key to can call it from anywhere. It's answered by <span class="mono" id="local">localhost</span> on this machine.</p>
6443
- </div>
6444
- <div>
6445
- <div class="label">\u{1F916} Its MCP address, for AI assistants</div>
6446
- <div class="row"><span class="url mono" id="mcp">\u2026</span><button data-copy="mcp">Copy</button></div>
6447
- <p class="hint">Paste it into Claude, ChatGPT or Cursor and they can use your app, under the same rules.</p>
6431
+ <nav role="tablist">
6432
+ <button role="tab" id="tab-app" aria-selected="true">\u{1F355} Nino's Pizza</button>
6433
+ <button role="tab" id="tab-admin" aria-selected="false">\u2699 Admin</button>
6434
+ </nav>
6435
+
6436
+ <!-- THE APP: what a customer of the pizzeria sees -->
6437
+ <section id="app">
6438
+ <div class="who">Signed in as
6439
+ <select id="as"><option value="ana">Ana</option><option value="ben">Ben</option></select>
6440
+ </div>
6441
+ <div class="cols">
6442
+ <div class="card">
6443
+ <h2>Your reservations</h2>
6444
+ <p class="sub">Book here, or ask the assistant. Each person only sees their own.</p>
6445
+ <button class="act primary" id="book">Book table 4 at 7:30pm</button>
6446
+ <ul class="res" id="list"><li class="empty">No reservations yet.</li></ul>
6447
+ <div id="try-ben" hidden>
6448
+ <p class="sub" style="margin:14px 0 6px">Ana booked a table. Can Ben cancel it?</p>
6449
+ <button class="act" id="ben-cancel">Cancel Ana's booking</button>
6450
+ <div id="ben-out"></div>
6448
6451
  </div>
6449
6452
  </div>
6450
- <div class="ticker">
6451
- <div class="label" style="font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)">Requests reaching your laptop, live</div>
6452
- <ol id="ticker"><li class="empty" id="ticker-empty">Nothing yet. Ask the assistant to book a table \u2192</li></ol>
6453
- </div>
6453
+ <div class="card chatcard"><div id="chat"></div></div>
6454
6454
  </div>
6455
- <div class="card" style="padding:0;overflow:hidden"><div id="chat"></div></div>
6455
+ <p class="next">Every request here went from the internet, through APIblaze, to your laptop: <code>npx apiblaze@latest demo logs</code> shows them. Next: <a id="go-admin">the admin side \u2192</a></p>
6456
6456
  </section>
6457
6457
 
6458
- <div class="section">Included with every APIblaze API, still no code</div>
6459
- <section class="grid">
6460
- <div class="card">
6461
- <h3>\u{1F6E1} Only the owner can change it</h3>
6462
- <p class="sub">Ana books a table. Ben tries to cancel it.</p>
6463
- <button id="rule">Ben cancels Ana's booking</button>
6464
- <div id="rule-out"></div>
6458
+ <!-- ADMIN: what each pizzeria's admin sees -->
6459
+ <section id="admin" hidden>
6460
+ <div class="who">Signed in as
6461
+ <select id="admin-as"><option value="ana">Ana, admin of Nino Pizza</option><option value="gina">Gina, admin of Gino Pizza</option></select>
6465
6462
  </div>
6466
- <div class="card" id="tenants" hidden>
6467
- <h3>\u{1F3E2} Each customer is separate</h3>
6468
- <p class="sub">Nino Pizza and Gino Pizza use the same app, each with its own key.</p>
6469
- <button id="gino">Gino's key asks for Nino's booking</button>
6470
- <div id="gino-out"></div>
6471
- </div>
6472
- <div class="card" id="rate-card" hidden>
6473
- <h3>\u23F1 Rate limits</h3>
6474
- <p class="sub">Each customer gets <span id="rps">a few</span> calls a second. Send 30 at once and see what happens.</p>
6475
- <button id="hammer">Send 30 calls at once</button>
6476
- <div id="hammer-out"></div>
6477
- </div>
6478
- <div class="card">
6479
- <h3>\u{1F510} Sign-in</h3>
6480
- <p class="sub">People and AI agents can sign in with GitHub instead of a key. Your API has a developer portal for that.</p>
6481
- <a id="portal" href="#" target="_blank" rel="noopener"><button>Open the developer portal</button></a>
6482
- </div>
6483
- <div class="card wide" id="keys-card" hidden>
6484
- <h3>\u{1F511} API keys, self-serve</h3>
6485
- <p class="sub">Drop this widget into your site and each customer makes their own keys.</p>
6486
- <div id="keys"></div>
6487
- </div>
6488
- <div class="card wide" id="groups-card" hidden>
6489
- <h3>\u{1F465} Users and roles</h3>
6490
- <p class="sub">Put people in groups. Rules can say "only staff may cancel".</p>
6491
- <div id="groups"></div>
6463
+ <div class="stack">
6464
+ <div class="card"><div id="keys"></div></div>
6465
+ <div class="card"><div id="groups"></div></div>
6492
6466
  </div>
6467
+ <p class="next">Each pizzeria only sees its own keys and people. Developers can also sign in and get a key at the <a id="portal" target="_blank" rel="noopener">developer portal</a>.</p>
6493
6468
  </section>
6494
-
6495
- <p class="footer">This is exactly what <code>npx apiblaze@latest dev</code> does for your own app. Ask Claude: <b>"Put my app on APIblaze."</b></p>
6496
6469
  </main>
6497
6470
  <script src="/apiblaze-widgets.js"></script>
6498
6471
  <script>
6499
6472
  const $ = (s) => document.querySelector(s);
6500
- const post = (url, body) => fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}) }).then((r) => r.json()).catch((e) => ({ status: 0, error: String(e) }));
6501
- function out(el, cls, text, detail) {
6502
- const d = document.createElement('div'); d.className = 'out ' + (cls || ''); d.textContent = text;
6503
- if (detail) { const s = document.createElement('small'); s.textContent = detail; d.appendChild(s); }
6504
- $(el).replaceChildren(d);
6505
- }
6506
- document.querySelectorAll('[data-copy]').forEach((b) => b.onclick = () => { navigator.clipboard?.writeText($('#' + b.dataset.copy).textContent); b.textContent = 'Copied'; setTimeout(() => b.textContent = 'Copy', 1200); });
6473
+ const post = (url, body) => fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}) }).then((r) => r.json()).catch(() => ({ status: 0 }));
6474
+ const W = window.ApiblazeWidgets;
6475
+ const theme = { accent: '#e4572e' };
6476
+ let cfg = {};
6477
+ let anaBooking = null;
6507
6478
 
6508
- let bookingId = null;
6509
- async function book() { const r = await post('/api/act', { op: 'book', as: 'ana' }); if (r.status === 201) bookingId = r.body.id; return r; }
6479
+ // \u2500\u2500 tabs \u2500\u2500
6480
+ function show(tab) {
6481
+ $('#app').hidden = tab !== 'app'; $('#admin').hidden = tab !== 'admin';
6482
+ $('#tab-app').setAttribute('aria-selected', tab === 'app'); $('#tab-admin').setAttribute('aria-selected', tab === 'admin');
6483
+ if (tab === 'admin') mountAdmin();
6484
+ }
6485
+ $('#tab-app').onclick = () => show('app');
6486
+ $('#tab-admin').onclick = () => show('admin');
6487
+ $('#go-admin').onclick = () => show('admin');
6510
6488
 
6511
- $('#rule').onclick = async () => {
6512
- if (!bookingId) await book();
6513
- const r = await post('/api/act', { op: 'cancel', as: 'ben', id: bookingId });
6514
- if (r.status === 403) out('#rule-out', 'good', "\u{1F6E1} Blocked. Only Ana can cancel Ana's booking.", 'HTTP 403 \xB7 decided by APIblaze, not your code');
6515
- else if (r.status === 204) { out('#rule-out', 'bad', 'Cancelled. That should not happen.', 'HTTP 204'); bookingId = null; }
6516
- else out('#rule-out', 'bad', explain(r), 'HTTP ' + r.status);
6517
- };
6518
- $('#gino').onclick = async () => {
6519
- if (!bookingId) await book();
6520
- const r = await post('/api/act', { op: 'open', as: 'ana', id: bookingId, key: 'gino' });
6521
- if (r.status === 403 || r.status === 404) out('#gino-out', 'good', "\u{1F6E1} Blocked. Gino Pizza only sees Gino Pizza's bookings.", 'HTTP ' + r.status);
6522
- else out('#gino-out', 'bad', r.status === 200 ? "Gino Pizza can read Nino's booking." : explain(r), 'HTTP ' + r.status);
6523
- };
6524
- $('#hammer').onclick = async () => {
6525
- out('#hammer-out', '', 'Sending\u2026');
6526
- const r = await post('/api/hammer');
6527
- out('#hammer-out', 'good', \`\${r.served} served, \${r.limited} told to slow down.\`, \`\${r.sent} calls \xB7 HTTP 200 \xD7 \${r.served}, 429 \xD7 \${r.limited}\`);
6489
+ // \u2500\u2500 the app \u2500\u2500
6490
+ const me = () => $('#as').value;
6491
+ const seen = new Set();
6492
+ async function refresh() {
6493
+ const r = await post('/api/act', { op: 'list', as: me() });
6494
+ const rows = Array.isArray(r.body) ? r.body : [];
6495
+ if (me() === 'ana') anaBooking = rows.length ? rows[0].id : null;
6496
+ const ul = $('#list'); ul.replaceChildren();
6497
+ if (!rows.length) { const li = document.createElement('li'); li.className = 'empty'; li.textContent = 'No reservations yet.'; ul.append(li); }
6498
+ for (const b of rows) {
6499
+ const li = document.createElement('li');
6500
+ if (!seen.has(b.id)) { li.className = 'new'; seen.add(b.id); }
6501
+ const t = document.createElement('span');
6502
+ t.textContent = \`Table \${b.table} \xB7 \${b.time} \xB7 \${b.guests || 2} people \xB7 #\${b.id}\`;
6503
+ const x = document.createElement('button'); x.className = 'act'; x.textContent = 'Cancel';
6504
+ x.onclick = async () => { await post('/api/act', { op: 'cancel', as: me(), id: b.id }); refresh(); };
6505
+ li.append(t, x); ul.append(li);
6506
+ }
6507
+ $('#try-ben').hidden = !(me() === 'ben' && anaBooking);
6508
+ }
6509
+ $('#book').onclick = async () => { await post('/api/act', { op: 'book', as: me() }); refresh(); };
6510
+ $('#ben-cancel').onclick = async () => {
6511
+ const r = await post('/api/act', { op: 'cancel', as: 'ben', id: anaBooking });
6512
+ const d = document.createElement('div');
6513
+ if (r.status === 403) { d.className = 'out good'; d.textContent = "\u{1F6E1} Blocked. Only Ana can cancel Ana's booking. APIblaze decided, not the app's code."; }
6514
+ else { d.className = 'out bad'; d.textContent = r.status === 204 ? 'Cancelled. That should not happen.' : 'Something went wrong (HTTP ' + r.status + ').'; }
6515
+ $('#ben-out').replaceChildren(d);
6528
6516
  };
6529
- function explain(r) {
6530
- if (r.status === 402) return 'The free demo calls ran out. Run again: npx apiblaze@latest demo';
6531
- if (r.status === 429) return 'Too many calls at once. Wait a second.';
6532
- if (!r.status || r.status >= 500) return 'The connection to APIblaze dropped. Run again: npx apiblaze@latest demo';
6533
- return (r.body && (r.body.detail || r.body.error)) || 'Something went wrong';
6534
- }
6517
+ function mountChat() {
6518
+ if (!W || !cfg.widgets || !cfg.widgets.chat) return;
6519
+ const who = me() === 'ben' ? 'Ben' : 'Ana';
6520
+ $('#chat').replaceChildren();
6521
+ W.chat('#chat', { endpoint: '/api/apiblaze/chat?as=' + me(), mode: 'inline', title: "Nino's Pizza assistant", avatar: '\u{1F355}', height: 520, theme,
6522
+ storageKey: 'nino-chat-' + me(),
6523
+ welcome: \`Hi \${who}! I can book, list and cancel your tables. I'm using the app on your laptop, through APIblaze.\`,
6524
+ suggestions: ['Book table 2 for 4 people at 8pm', 'What are my reservations?'] });
6525
+ }
6526
+ $('#as').onchange = () => { $('#ben-out').replaceChildren(); refresh(); mountChat(); };
6527
+ setInterval(() => { if (!$('#app').hidden) refresh(); }, 2000); // the assistant's bookings appear here
6535
6528
 
6536
- // Live ticker: what the backend on this laptop just answered.
6537
- const seen = new Set();
6538
- async function tick() {
6539
- try {
6540
- const rows = await fetch('/api/ticker').then((r) => r.json());
6541
- if (rows.length) $('#ticker-empty')?.remove();
6542
- for (const r of rows.slice().reverse()) {
6543
- const k = r.at + r.method + r.path + r.who;
6544
- if (seen.has(k)) continue; seen.add(k);
6545
- const li = document.createElement('li'); li.className = 'new';
6546
- const t = new Date(r.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
6547
- const who = r.who ? \`\${r.who}\${r.tenant ? ' @ ' + r.tenant.replace(/[0-9a-f]{4}$/, '') : ''}\` : '';
6548
- // Text only: these values come from requests made on the internet.
6549
- for (const [tag, text, cls] of [['span', t], ['b', r.method], ['span', r.path], ['span', who], ['span', String(r.status), 'st']]) {
6550
- const el = document.createElement(tag); el.textContent = text; if (cls) el.className = cls; li.appendChild(el);
6551
- }
6552
- $('#ticker').prepend(li);
6553
- }
6554
- } catch { /* the page keeps working */ }
6555
- }
6556
- setInterval(tick, 1000);
6529
+ // \u2500\u2500 admin \u2500\u2500
6530
+ function mountAdmin() {
6531
+ if (!W) return;
6532
+ const as = $('#admin-as').value;
6533
+ for (const id of ['#keys', '#groups']) $(id).replaceChildren();
6534
+ if (cfg.widgets && cfg.widgets.keys) W.keys('#keys', { endpoint: '/api/apiblaze/keys?as=' + as, theme, title: 'API keys' });
6535
+ if (cfg.widgets && cfg.widgets.groups) W.groups('#groups', { endpoint: '/api/apiblaze/groups?as=' + as, theme, title: 'People and groups' });
6536
+ }
6537
+ $('#admin-as').onchange = mountAdmin;
6557
6538
 
6558
6539
  fetch('/api/config').then((r) => r.json()).then((c) => {
6540
+ cfg = c;
6559
6541
  $('#api').textContent = c.apiUrl || 'not connected';
6560
6542
  $('#mcp').textContent = c.mcpUrl || 'not connected';
6561
- $('#local').textContent = 'localhost:' + c.backendPort;
6562
- if (c.portalUrl) $('#portal').href = c.portalUrl; else $('#portal').hidden = true;
6563
- if (c.ginoKey) $('#tenants').hidden = false;
6564
- if (c.rps) $('#rps').textContent = c.rps;
6565
- if (c.rateCard) $('#rate-card').hidden = false;
6566
- const W = window.ApiblazeWidgets;
6567
- const theme = { accent: '#e4572e' };
6568
- if (W && c.widgets.chat) W.chat('#chat', { endpoint: '/api/apiblaze/chat', mode: 'inline', title: "Nino's Pizza assistant", avatar: '\u{1F355}', height: 520, theme,
6569
- welcome: "Hi Ana! I can book, list and cancel tables at Nino's Pizza. I'm using the app on your laptop, through APIblaze.",
6570
- suggestions: ['Book table 4 for 2 people at 7:30pm', 'What are my bookings?', "Cancel booking r1"] });
6571
- if (W && c.widgets.keys) { $('#keys-card').hidden = false; W.keys('#keys', { endpoint: '/api/apiblaze/keys', theme }); }
6572
- if (W && c.widgets.groups) { $('#groups-card').hidden = false; W.groups('#groups', { endpoint: '/api/apiblaze/groups', theme }); }
6543
+ if (c.portalUrl) $('#portal').href = c.portalUrl;
6544
+ if (!c.gino) { const o = $('#admin-as').querySelector('[value="gina"]'); if (o) o.remove(); }
6545
+ if (!c.widgets || (!c.widgets.keys && !c.widgets.groups)) $('#tab-admin').hidden = true;
6546
+ refresh(); mountChat();
6547
+ if (location.hash === '#admin') show('admin');
6573
6548
  });
6574
6549
  </script>
6575
6550
  </body>
@@ -6583,9 +6558,13 @@ fetch('/api/config').then((r) => r.json()).then((c) => {
6583
6558
  var demo_exports = {};
6584
6559
  __export(demo_exports, {
6585
6560
  DEMO_ADMIN_EMAIL: () => DEMO_ADMIN_EMAIL,
6561
+ DEMO_GINO_ADMIN_EMAIL: () => DEMO_GINO_ADMIN_EMAIL,
6586
6562
  DEMO_RPS: () => DEMO_RPS,
6587
6563
  demoName: () => demoName,
6564
+ routesOf: () => routesOf,
6588
6565
  runDemo: () => runDemo,
6566
+ runDemoLogs: () => runDemoLogs,
6567
+ runDemoStart: () => runDemoStart,
6589
6568
  runDemoStop: () => runDemoStop,
6590
6569
  stopDemos: () => stopDemos
6591
6570
  });
@@ -6641,7 +6620,8 @@ async function stopDemos() {
6641
6620
  return stopped;
6642
6621
  }
6643
6622
  function writeApp(dir) {
6644
- import_fs5.default.rmSync(dir, { recursive: true, force: true });
6623
+ import_fs5.default.mkdirSync(dir, { recursive: true });
6624
+ for (const entry of import_fs5.default.readdirSync(dir)) import_fs5.default.rmSync(import_path4.default.join(dir, entry), { recursive: true, force: true });
6645
6625
  for (const [rel, text] of Object.entries(DEMO_FILES)) {
6646
6626
  const f = import_path4.default.join(dir, rel);
6647
6627
  import_fs5.default.mkdirSync(import_path4.default.dirname(f), { recursive: true });
@@ -6667,7 +6647,7 @@ function loadState(dir) {
6667
6647
  return null;
6668
6648
  }
6669
6649
  }
6670
- async function createProxy2(name, backendPort, openapi) {
6650
+ async function createProxy2(name, backendPort, openapi, tenantName) {
6671
6651
  const auth = resolveAuth({});
6672
6652
  const proposed = await proposeFromSpec(openapi).catch(() => null);
6673
6653
  const families = proposed?.status === 200 && Array.isArray(proposed.data?.families) ? proposed.data.families.filter((f) => !f.skipped_reason).map((f) => f.key) : [];
@@ -6680,7 +6660,7 @@ async function createProxy2(name, backendPort, openapi) {
6680
6660
  environments: { dev: { target: `http://localhost:${backendPort}`, description: `the ${B.cli} demo on this machine` } },
6681
6661
  // Two customers of the same app, each with its own key: the proxy's own tenant
6682
6662
  // is Nino Pizza, and the server adds Gino Pizza and returns its key.
6683
- tenant: `ninopizza${import_crypto.default.randomBytes(2).toString("hex")}`,
6663
+ tenant: tenantName,
6684
6664
  extra_tenants: ["Gino Pizza"],
6685
6665
  ...families.length ? { secure: { families } } : {}
6686
6666
  };
@@ -6721,6 +6701,14 @@ async function waitReady(dir, s, serverKey) {
6721
6701
  }
6722
6702
  throw new DemoError(`The demo is running but ${B.product} cannot reach it yet (last answer ${last}).`, `Run again: ${LATEST} demo`);
6723
6703
  }
6704
+ async function offerBrowser(url, opts) {
6705
+ if (opts.open === false || !process.stdin.isTTY || !process.stdout.isTTY) return;
6706
+ const readline3 = await import("readline");
6707
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
6708
+ const answer = await new Promise((res) => rl.question(` Open ${url} in your browser? (Y/n) `, res));
6709
+ rl.close();
6710
+ if (!/^n/i.test(answer.trim())) openBrowser3(url);
6711
+ }
6724
6712
  function openBrowser3(url) {
6725
6713
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
6726
6714
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -6742,17 +6730,56 @@ async function runDemo(opts = {}) {
6742
6730
  process.exit(1);
6743
6731
  }
6744
6732
  }
6733
+ function routesOf(openapi) {
6734
+ const out = [];
6735
+ let curPath = "";
6736
+ let cur = null;
6737
+ for (const line of openapi.split("\n")) {
6738
+ const p = /^ {2}(\/[^:]*):\s*$/.exec(line);
6739
+ if (p) {
6740
+ curPath = p[1];
6741
+ cur = null;
6742
+ continue;
6743
+ }
6744
+ const mth = /^ {4}(get|post|put|patch|delete):\s*$/.exec(line);
6745
+ if (mth && curPath) {
6746
+ cur = { method: mth[1].toUpperCase(), path: curPath, summary: "", op: "" };
6747
+ out.push(cur);
6748
+ continue;
6749
+ }
6750
+ const sum = /^ {6}summary:\s*(.+)$/.exec(line);
6751
+ if (sum && cur) cur.summary = sum[1].trim();
6752
+ const op2 = /^ {6}operationId:\s*(.+)$/.exec(line);
6753
+ if (op2 && cur) cur.op = op2[1].trim();
6754
+ }
6755
+ return out;
6756
+ }
6745
6757
  async function startDemo(opts) {
6746
6758
  const t0 = Date.now();
6747
- const dir = import_path4.default.resolve(process.cwd(), DIR_NAME);
6748
- console.log(import_chalk25.default.dim(`Setting up the demo in ./${DIR_NAME} (about 30 seconds)\u2026`));
6759
+ const here = process.cwd();
6760
+ const dir = import_fs5.default.existsSync(import_path4.default.join(here, STATE_DIR, "state.json")) ? here : import_path4.default.resolve(here, DIR_NAME);
6761
+ const rel = import_path4.default.relative(here, dir) || ".";
6762
+ const run2 = (...cmds) => {
6763
+ for (const c of cmds) console.log(import_chalk25.default.dim(` $ ${LATEST} ${c}`));
6764
+ };
6765
+ const step = (label3, text, note = "") => console.log(` ${import_chalk25.default.green("\u2713")} ${import_chalk25.default.bold(label3.padEnd(17))} ${text}${note ? ` ${import_chalk25.default.dim(note)}` : ""}`);
6749
6766
  await stopDemos();
6750
6767
  writeApp(dir);
6751
6768
  const appPort = await freePort(3e3);
6752
6769
  const backendPort = await freePort(3001, [appPort]);
6753
6770
  const name = demoName();
6754
6771
  const openapi = DEMO_FILES["openapi.yaml"];
6755
- const r = await createProxy2(name, backendPort, openapi);
6772
+ console.log();
6773
+ console.log(import_chalk25.default.bold(`1. The app: Nino's Pizza, a table-booking backend, now in ${rel}`));
6774
+ console.log(import_chalk25.default.dim(` backend.js runs on this laptop (localhost:${backendPort}). Its routes, from openapi.yaml:`));
6775
+ for (const r2 of routesOf(openapi)) console.log(` ${r2.method.padEnd(7)}${r2.path.padEnd(21)}${import_chalk25.default.dim(r2.summary)}`);
6776
+ console.log(import_chalk25.default.dim(" No login, no API keys, no permission checks in its code. That is APIblaze's job."));
6777
+ console.log();
6778
+ console.log(import_chalk25.default.bold(`2. ${B.product}, in front of it`) + import_chalk25.default.dim(" (each step does what the command above it does)"));
6779
+ const ninoTenant = `ninopizza${import_crypto.default.randomBytes(2).toString("hex")}`;
6780
+ run2(`dev --port ${backendPort} --openapi ./openapi.yaml --name ${name} --tenant ${ninoTenant} --auto`);
6781
+ const tCreate = Date.now();
6782
+ const r = await createProxy2(name, backendPort, openapi, ninoTenant);
6756
6783
  const cpKey = r.cp_key;
6757
6784
  const teamId = r.team_id;
6758
6785
  const keys = r.api_keys ?? {};
@@ -6764,24 +6791,40 @@ async function startDemo(opts) {
6764
6791
  const missing = [!cpKey && "workspace key", !teamId && "team", !serverKey && "API key", !tenant2 && "tenant"].filter(Boolean);
6765
6792
  if (missing.length) throw new DemoError(`${B.product} did not return everything the demo needs (${missing.join(", ")}).`, `Run again: ${LATEST} demo`);
6766
6793
  const apiUrl = restUrl(project, version2, "dev", { anon: true });
6767
- await cpFetch(cpKey, `/projects/${encodeURIComponent(project)}/${encodeURIComponent(version2)}/limits`, {
6794
+ const gino = (rr.tenants ?? []).find((t) => t.api_key);
6795
+ const agents = mcpUrl(project, version2, "dev", { tenant: tenant2, anon: true });
6796
+ step("Proxy", `${project}, created from openapi.yaml`, `${((Date.now() - tCreate) / 1e3).toFixed(1)} s \xB7 no account needed`);
6797
+ const applied = (rr.secure?.applied ?? []).map((k) => k.replace(/^\//, "").split("/")[0]);
6798
+ if (applied.length) step("Rules", `${applied.join(", ")}: only the person who made one can change or cancel it`);
6799
+ const tools0 = routesOf(openapi).map((x) => x.op).filter(Boolean);
6800
+ step("AI", `${tools0.length} tools for AI assistants, one per route (${tools0.join(", ")})`, "the MCP address");
6801
+ if (gino?.tenant) run2(`tenant create --name "Gino Pizza" --slug ${gino.tenant}`, `tenant attach ${project} --tenant ${gino.tenant}`, `apikeys mint --tenant ${gino.tenant} --backend --env dev`);
6802
+ step("Customers/Tenants", gino ? `Nino Pizza and Gino Pizza, each with its own API key` : `Nino Pizza, with its own API key`, "one API, kept apart");
6803
+ run2(`admins add ${DEMO_ADMIN_EMAIL} --tenant ${tenant2}`, ...gino?.tenant ? [`admins add ${DEMO_GINO_ADMIN_EMAIL} --tenant ${gino.tenant}`] : []);
6804
+ const limited = await cpFetch(cpKey, `/projects/${encodeURIComponent(project)}/${encodeURIComponent(version2)}/limits`, {
6768
6805
  method: "PUT",
6769
6806
  body: JSON.stringify({ consumerRps: DEMO_RPS, endUserRps: DEMO_RPS })
6770
- }).catch(() => {
6771
- });
6772
- await cpFetch(cpKey, `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/admin-emails`, {
6807
+ }).then(() => true).catch(() => false);
6808
+ const adminOk = await cpFetch(cpKey, `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/admin-emails`, {
6773
6809
  method: "POST",
6774
6810
  body: JSON.stringify({ email: DEMO_ADMIN_EMAIL })
6775
- }).catch(() => {
6776
- });
6777
- const gino = (r.tenants ?? []).find((t) => t.api_key);
6778
- const agents = mcpUrl(project, version2, "dev", { tenant: tenant2, anon: true });
6811
+ }).then(() => true).catch(() => false);
6812
+ let ginaOk = false;
6813
+ if (gino?.tenant) {
6814
+ ginaOk = await cpFetch(cpKey, `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(gino.tenant)}/admin-emails`, {
6815
+ method: "POST",
6816
+ body: JSON.stringify({ email: DEMO_GINO_ADMIN_EMAIL })
6817
+ }).then(() => true).catch(() => false);
6818
+ }
6819
+ if (adminOk) step("Admins", ginaOk ? "Ana runs Nino Pizza, Gina runs Gino Pizza" : "Ana runs Nino Pizza", "they manage keys and people on the Admin tab");
6820
+ run2(`limits set ${project} --rate ${DEMO_RPS} --end-user-rate ${DEMO_RPS}`);
6821
+ if (limited) step("Limits", `${DEMO_RPS} calls a second per customer`);
6779
6822
  writeEnv(dir, {
6780
6823
  APIBLAZE_NAME: project,
6781
6824
  APIBLAZE_TENANT: tenant2,
6782
6825
  APIBLAZE_URL: apiUrl,
6783
6826
  APIBLAZE_SERVER_KEY: serverKey,
6784
- ...gino?.api_key ? { APIBLAZE_GINO_KEY: gino.api_key } : {},
6827
+ ...gino?.api_key ? { APIBLAZE_GINO_KEY: gino.api_key, APIBLAZE_GINO_TENANT: gino.tenant ?? "" } : {},
6785
6828
  APIBLAZE_MCP_URL: agents,
6786
6829
  APIBLAZE_PORTAL_URL: `https://${tenant2}.portal.apiblaze.com/${version2}`,
6787
6830
  // The keys + users widgets mint through the workspace's control key (server-side only).
@@ -6793,29 +6836,107 @@ async function startDemo(opts) {
6793
6836
  });
6794
6837
  const state = { name: project, version: version2, tenant: tenant2, teamId, cpKey, appPort, backendPort, apiUrl, claimCode: r.claim_code };
6795
6838
  saveState(dir, state);
6796
- const log4 = import_fs5.default.openSync(import_path4.default.join(dir, STATE_DIR, "demo.log"), "a");
6797
- const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "demo", "--serve", dir], { cwd: dir, detached: true, stdio: ["ignore", log4, log4] });
6798
- child.unref();
6799
- saveState(dir, { ...state, pid: child.pid });
6800
- writeRegistry([...readRegistry(), { dir, pid: child.pid ?? 0 }]);
6801
- await waitReady(dir, state, serverKey);
6839
+ step("Keys", "saved to .env.local for the website's server, never printed");
6840
+ await spawnServe(dir, state, serverKey);
6841
+ step("Tunnel", `the public address now reaches localhost:${backendPort} on this laptop`, "the dev command above keeps it open \xB7 checked with a real call");
6842
+ console.log(import_chalk25.default.dim(` $ node start.js`) + import_chalk25.default.dim(" (in the demo folder: the backend and the website)"));
6843
+ step("Website", `http://localhost:${appPort}, calling ${B.product} with one server key`);
6802
6844
  const url = `http://localhost:${appPort}`;
6803
- if (opts.open !== false) openBrowser3(url);
6804
6845
  const secs = Math.round((Date.now() - t0) / 1e3);
6846
+ printReady(dir, secs);
6847
+ await offerBrowser(url, opts);
6848
+ }
6849
+ function printReady(dir, secs) {
6850
+ const env = readEnvFile(import_path4.default.join(dir, ".env.local"));
6851
+ const row = (label3, value, note = "") => console.log(` ${label3.padEnd(12)} ${value}${note ? ` ${import_chalk25.default.dim(note)}` : ""}`);
6805
6852
  console.log();
6806
- console.log(`${import_chalk25.default.green("\u2713")} Nino's Pizza, running on this laptop, is live on the internet: ${import_chalk25.default.bold(url)} ${import_chalk25.default.dim(`(${secs} s)`)}`);
6853
+ console.log(import_chalk25.default.bold(`3. Live: Nino's Pizza runs on this laptop and is on the internet`) + ` ${import_chalk25.default.dim(`(${secs} s)`)}`);
6807
6854
  console.log();
6808
- console.log(" Tell the user this, in plain words:");
6809
- console.log(` 1. "This booking app runs on your laptop. ${B.product} just gave it a public address, plus an MCP address so AI assistants can use it."`);
6810
- console.log(' 2. "On the page, ask the assistant to book a table, and watch the request arrive on your laptop."');
6811
- console.log(' 3. "API keys, sign-in and who-can-change-what are already on, with no code. Try the cards below."');
6855
+ row("App", import_chalk25.default.bold(`http://localhost:${env.APP_PORT}`));
6856
+ row("Public API", env.APIBLAZE_URL ?? "", `answered by localhost:${env.BACKEND_PORT}`);
6857
+ row("MCP", env.APIBLAZE_MCP_URL ?? "", "for Claude, ChatGPT, Cursor");
6858
+ if (env.APIBLAZE_PORTAL_URL) row("Dev portal", env.APIBLAZE_PORTAL_URL, "sign in, get a key, try the API");
6812
6859
  console.log();
6813
- console.log(` Public API ${apiUrl}`);
6814
- console.log(` MCP ${agents}`);
6815
- console.log(` Same as ${LATEST} dev --port ${backendPort} --openapi ./openapi.yaml ${import_chalk25.default.dim("(the command for your own app)")}`);
6816
- console.log(import_chalk25.default.dim(` Code in ./${DIR_NAME} \xB7 key saved in .env.local, never shown \xB7 stop: ${LATEST} demo stop`));
6860
+ row("Logs", `${LATEST} demo logs`, "every request reaching your laptop");
6861
+ row("Stop", `${LATEST} demo stop`);
6862
+ row("Restart", `${LATEST} demo restart`, "same addresses and keys");
6863
+ row("Start over", `${LATEST} demo`, "a brand-new demo");
6817
6864
  console.log();
6818
- console.log(` Ask the user one question: "Want to put your own app on the internet like this?"`);
6865
+ console.log(import_chalk25.default.dim(` Code in ${import_path4.default.relative(process.cwd(), dir) || "."} \xB7 keys in .env.local, never shown here`));
6866
+ console.log(import_chalk25.default.dim(` Your own app: ${LATEST} dev --port <port> \xB7 deployed already: ${LATEST} create --target <url>`));
6867
+ }
6868
+ function readEnvFile(file2) {
6869
+ const out = {};
6870
+ try {
6871
+ for (const line of import_fs5.default.readFileSync(file2, "utf8").split("\n")) {
6872
+ const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
6873
+ if (m) out[m[1]] = m[2];
6874
+ }
6875
+ } catch {
6876
+ }
6877
+ return out;
6878
+ }
6879
+ function demoDir() {
6880
+ const here = process.cwd();
6881
+ if (import_fs5.default.existsSync(import_path4.default.join(here, STATE_DIR, "state.json"))) return here;
6882
+ return import_path4.default.resolve(here, DIR_NAME);
6883
+ }
6884
+ async function spawnServe(dir, state, serverKey) {
6885
+ const log4 = import_fs5.default.openSync(import_path4.default.join(dir, STATE_DIR, "demo.log"), "a");
6886
+ const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "demo", "--serve", dir], { cwd: dir, detached: true, stdio: ["ignore", log4, log4] });
6887
+ child.unref();
6888
+ saveState(dir, { ...state, pid: child.pid, ready: false, error: void 0 });
6889
+ writeRegistry([...readRegistry(), { dir, pid: child.pid ?? 0 }]);
6890
+ await waitReady(dir, state, serverKey);
6891
+ }
6892
+ async function runDemoStart(opts = {}) {
6893
+ const t0 = Date.now();
6894
+ const dir = demoDir();
6895
+ const state = loadState(dir);
6896
+ const env = readEnvFile(import_path4.default.join(dir, ".env.local"));
6897
+ if (!state || !env.APIBLAZE_SERVER_KEY) {
6898
+ console.log(`${import_chalk25.default.red("\u2717")} There is no demo here to start.`);
6899
+ console.log(` Make one: ${LATEST} demo`);
6900
+ process.exit(1);
6901
+ }
6902
+ try {
6903
+ await stopDemos();
6904
+ await spawnServe(dir, state, env.APIBLAZE_SERVER_KEY);
6905
+ } catch (err) {
6906
+ const e = err instanceof DemoError ? err : new DemoError(`The demo could not start (${err.message}).`, `Start over: ${LATEST} demo`);
6907
+ await stopDemos().catch(() => 0);
6908
+ console.log(`${import_chalk25.default.red("\u2717")} ${e.message}`);
6909
+ console.log(` ${e.fix}`);
6910
+ process.exit(1);
6911
+ }
6912
+ printReady(dir, Math.round((Date.now() - t0) / 1e3));
6913
+ await offerBrowser(`http://localhost:${env.APP_PORT}`, opts);
6914
+ }
6915
+ async function runDemoLogs() {
6916
+ const file2 = import_path4.default.join(demoDir(), STATE_DIR, "demo.log");
6917
+ if (!import_fs5.default.existsSync(file2)) {
6918
+ console.log(`${import_chalk25.default.red("\u2717")} There is no demo log here.`);
6919
+ console.log(` Make a demo: ${LATEST} demo`);
6920
+ process.exit(1);
6921
+ }
6922
+ console.log(import_chalk25.default.dim(`Following ${import_path4.default.relative(process.cwd(), file2)} \u2014 Ctrl-C stops following (the demo keeps running).`));
6923
+ let pos = Math.max(0, import_fs5.default.statSync(file2).size - 4e3);
6924
+ const pump = () => {
6925
+ const size = import_fs5.default.statSync(file2).size;
6926
+ if (size < pos) pos = 0;
6927
+ if (size > pos) {
6928
+ const fd = import_fs5.default.openSync(file2, "r");
6929
+ const buf = Buffer.alloc(size - pos);
6930
+ import_fs5.default.readSync(fd, buf, 0, buf.length, pos);
6931
+ import_fs5.default.closeSync(fd);
6932
+ pos = size;
6933
+ process.stdout.write(buf.toString("utf8"));
6934
+ }
6935
+ };
6936
+ pump();
6937
+ setInterval(pump, 500);
6938
+ await new Promise(() => {
6939
+ });
6819
6940
  }
6820
6941
  async function runDemoStop() {
6821
6942
  const n = await stopDemos();
@@ -6884,7 +7005,7 @@ async function serveDemo(dir) {
6884
7005
  await new Promise(() => {
6885
7006
  });
6886
7007
  }
6887
- var import_fs5, import_path4, import_net, import_child_process4, import_crypto, import_module, import_chalk25, DIR_NAME, STATE_DIR, LATEST, DEMO_RPS, DEMO_ADMIN_EMAIL, DemoError, ADJ, NOUN, pick2, registryFile, alive;
7008
+ var import_fs5, import_path4, import_net, import_child_process4, import_crypto, import_module, import_chalk25, DIR_NAME, STATE_DIR, LATEST, DEMO_RPS, DEMO_ADMIN_EMAIL, DEMO_GINO_ADMIN_EMAIL, DemoError, ADJ, NOUN, pick2, registryFile, alive;
6888
7009
  var init_demo = __esm({
6889
7010
  "src/commands/demo.ts"() {
6890
7011
  "use strict";
@@ -6907,6 +7028,7 @@ var init_demo = __esm({
6907
7028
  LATEST = `npx ${B.cli}@latest`;
6908
7029
  DEMO_RPS = 2;
6909
7030
  DEMO_ADMIN_EMAIL = "ana@ninopizza.example";
7031
+ DEMO_GINO_ADMIN_EMAIL = "gina@ginopizza.example";
6910
7032
  DemoError = class extends Error {
6911
7033
  constructor(message, fix) {
6912
7034
  super(message);
@@ -7877,7 +7999,7 @@ description: Use when the user asks what ${P} (or ${B.cli}) does or can do for t
7877
7999
 
7878
8000
  ## Explain: they ask what ${P} does
7879
8001
 
7880
- Run nothing. Say this, in these words or very close to them, then stop:
8002
+ Run nothing. Say exactly this and nothing else: no list of use cases, no commands, no options. Then stop:
7881
8003
 
7882
8004
  "${P} puts the app on your laptop on the internet in one command. It gets a public address,
7883
8005
  plus an address AI assistants like me can use to work with it. API keys, sign-in and
@@ -7889,14 +8011,13 @@ Do not use the words proxy, MCP, tenant, OpenAPI or header unless the user does
7889
8011
 
7890
8012
  1. Say one line: "Starting the demo, about 30 seconds."
7891
8013
  2. Run exactly this, in the foreground. It needs no login and asks nothing. It sets
7892
- everything up, opens the browser and exits by itself once the page is live:
8014
+ everything up and exits by itself once the page is live. It opens nothing: give the user the App link it prints:
7893
8015
  \`${run2} demo\`
7894
- 3. When it prints \u2713, tell the story it printed, in plain words, with the link:
8016
+ 3. When it prints \u2713, tell this story in plain words, with the App link it printed:
7895
8017
  - "This booking app runs on your laptop. ${P} just gave it a public address, and one for AI assistants."
7896
- - "On the page, ask the assistant to book a table and watch the request arrive on your laptop."
7897
- - "Keys, sign-in and who-can-change-what are already on, with no code. Try the cards."
8018
+ - "On the Nino's Pizza tab, ask the assistant to book a table, and it appears in your list."
8019
+ - "The Admin tab is what each pizzeria's admin gets: their own API keys and people, kept apart."
7898
8020
  Then ask one question: **"Want to put your own app on the internet like this?"**
7899
- If they say yes: their app runs locally \u2192 \`${run2} dev\`; it is deployed \u2192 \`${run2} create\`.
7900
8021
  4. If it prints \u2717, say its one sentence and run the one command it gives, once.
7901
8022
 
7902
8023
  Rules for the demo: keep each update to one line. Do not narrate steps, read the demo's
@@ -7905,6 +8026,7 @@ in \`.env.local\` and never prints it.
7905
8026
 
7906
8027
  ## Their own app: they say yes to that
7907
8028
 
8029
+ Their app runs on this machine \u2192 it will use \`${run2} dev\`; it is deployed \u2192 \`${run2} create\`.
7908
8030
  Run \`${run2} skills --ship\` and follow what it prints.
7909
8031
  `;
7910
8032
  }
@@ -12769,6 +12891,7 @@ async function runDomainSetBase(project, opts) {
12769
12891
  var import_chalk53 = __toESM(require("chalk"));
12770
12892
  var import_ora22 = __toESM(require("ora"));
12771
12893
  init_admin();
12894
+ init_caller();
12772
12895
  init_resolve();
12773
12896
  init_auth();
12774
12897
  init_tenant_pick();
@@ -12831,10 +12954,11 @@ async function runTenantCreate(opts) {
12831
12954
  console.error(import_chalk53.default.red("--name (display name) is required."));
12832
12955
  process.exit(1);
12833
12956
  }
12834
- const { teamId } = await resolveTeam(opts.team);
12957
+ const caller = requireCaller();
12958
+ const { teamId } = await resolveActingTeam(caller, opts.team);
12835
12959
  const spinner = (0, import_ora22.default)("Creating tenant...").start();
12836
12960
  try {
12837
- const out = await admin({
12961
+ const out = await producer(caller, {
12838
12962
  method: "POST",
12839
12963
  path: `/teams/${encodeURIComponent(teamId)}/tenants`,
12840
12964
  body: { display_name: opts.name, ...opts.slug ? { tenant_name: opts.slug } : {} },
@@ -12852,11 +12976,12 @@ async function runTenantAttach(project, opts) {
12852
12976
  console.error(import_chalk53.default.red("--tenant <slug> is required."));
12853
12977
  process.exit(1);
12854
12978
  }
12855
- const { teamId } = await resolveTeam(opts.team);
12856
- const proj2 = await resolveProject(teamId, project, opts.apiversion);
12979
+ const caller = requireCaller();
12980
+ const { teamId } = await resolveActingTeam(caller, opts.team);
12981
+ const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
12857
12982
  const spinner = (0, import_ora22.default)("Attaching tenant...").start();
12858
12983
  try {
12859
- const out = await admin({
12984
+ const out = await producer(caller, {
12860
12985
  method: "POST",
12861
12986
  path: `/projects/${proj2.projectId}/${proj2.apiVersion}/tenants`,
12862
12987
  body: { tenant_name: opts.tenant, ...opts.authConfig ? { auth_config_id: opts.authConfig } : {} },
@@ -17288,8 +17413,10 @@ program.command("create").description(`Put an app that is already DEPLOYED behin
17288
17413
  }
17289
17414
  });
17290
17415
  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)));
17291
- 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" stops a running demo').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) => {
17416
+ 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) => {
17292
17417
  if (actionArg === "stop") return runDemoStop();
17418
+ if (actionArg === "start" || actionArg === "restart") return runDemoStart(opts);
17419
+ if (actionArg === "logs") return runDemoLogs();
17293
17420
  if (actionArg) {
17294
17421
  console.log(`Unknown demo action "${actionArg}". Run: npx ${B.cli}@latest demo`);
17295
17422
  process.exit(1);
@@ -67,6 +67,8 @@ declare class PlaneError extends Error {
67
67
  * `apiblaze authz` / the dashboard).
68
68
  */
69
69
 
70
+ /** Marks the short-lived per-admin keys this widget mints; the API-key widget hides them. */
71
+ declare const GROUPS_SESSION_KEY_DESCRIPTION = "Users & Groups widget (minted per admin session)";
70
72
  interface ApiblazeGroupsConfig {
71
73
  /** The producer CP key — the SAME widget key the API-key widget uses. Server-only.
72
74
  * Here it only MINTS each admin's personal key, so it must be allowed to grant
@@ -318,4 +320,4 @@ declare function createApiblazeKeys(cfg: ApiblazeKeysConfig): {
318
320
  }>;
319
321
  };
320
322
 
321
- export { type ApiblazeChatConfig, type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type ChatRelayUser, type GroupsAction, PlaneError, createApiblazeChat, createApiblazeGroups, createApiblazeKeys };
323
+ export { type ApiblazeChatConfig, type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type ChatRelayUser, GROUPS_SESSION_KEY_DESCRIPTION, type GroupsAction, PlaneError, createApiblazeChat, createApiblazeGroups, createApiblazeKeys };
@@ -67,6 +67,8 @@ declare class PlaneError extends Error {
67
67
  * `apiblaze authz` / the dashboard).
68
68
  */
69
69
 
70
+ /** Marks the short-lived per-admin keys this widget mints; the API-key widget hides them. */
71
+ declare const GROUPS_SESSION_KEY_DESCRIPTION = "Users & Groups widget (minted per admin session)";
70
72
  interface ApiblazeGroupsConfig {
71
73
  /** The producer CP key — the SAME widget key the API-key widget uses. Server-only.
72
74
  * Here it only MINTS each admin's personal key, so it must be allowed to grant
@@ -318,4 +320,4 @@ declare function createApiblazeKeys(cfg: ApiblazeKeysConfig): {
318
320
  }>;
319
321
  };
320
322
 
321
- export { type ApiblazeChatConfig, type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type ChatRelayUser, type GroupsAction, PlaneError, createApiblazeChat, createApiblazeGroups, createApiblazeKeys };
323
+ export { type ApiblazeChatConfig, type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type ChatRelayUser, GROUPS_SESSION_KEY_DESCRIPTION, type GroupsAction, PlaneError, createApiblazeChat, createApiblazeGroups, createApiblazeKeys };
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/server/index.ts
21
21
  var server_exports = {};
22
22
  __export(server_exports, {
23
+ GROUPS_SESSION_KEY_DESCRIPTION: () => GROUPS_SESSION_KEY_DESCRIPTION,
23
24
  PlaneError: () => PlaneError,
24
25
  createApiblazeChat: () => createApiblazeChat,
25
26
  createApiblazeGroups: () => createApiblazeGroups,
@@ -87,6 +88,7 @@ function createPlaneCore(cfg) {
87
88
  }
88
89
 
89
90
  // src/server/groups.ts
91
+ var GROUPS_SESSION_KEY_DESCRIPTION = "Users & Groups widget (minted per admin session)";
90
92
  function createApiblazeGroups(cfg) {
91
93
  if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId, email }.");
92
94
  const core = createPlaneCore({ cpKey: cfg.cpKey, apikeysBase: cfg.base, iamBase: cfg.iamBase });
@@ -113,7 +115,7 @@ function createApiblazeGroups(cfg) {
113
115
  environment: "prod",
114
116
  permissions: ["call", "manage-access"],
115
117
  expires_in_seconds: KEY_TTL_SECONDS,
116
- description: "Users & Groups widget (minted per admin session)"
118
+ description: GROUPS_SESSION_KEY_DESCRIPTION
117
119
  })
118
120
  });
119
121
  if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
@@ -372,13 +374,14 @@ function createApiblazeKeys(cfg) {
372
374
  catalogNames(user, tenant2)
373
375
  ]);
374
376
  const d = await listRes.json();
375
- const keys = [...d.keys ?? []];
377
+ const visible = (k) => k?.description !== GROUPS_SESSION_KEY_DESCRIPTION;
378
+ const keys = [...d.keys ?? []].filter(visible);
376
379
  let cursor = d.page?.next_cursor ?? null;
377
380
  for (let hop = 0; hop < 50 && cursor; hop++) {
378
381
  const more = await planeFetch("GET", "/apikeys", user, { query: { tenant: tenant2, limit: "200", cursor } });
379
382
  if (!more.ok) break;
380
383
  const md = await more.json();
381
- keys.push(...md.keys ?? []);
384
+ keys.push(...(md.keys ?? []).filter(visible));
382
385
  cursor = md.page?.next_cursor ?? null;
383
386
  }
384
387
  const eligible = allowed.filter((t) => catalog.includes(t));
@@ -447,6 +450,7 @@ function createApiblazeKeys(cfg) {
447
450
  }
448
451
  // Annotate the CommonJS export names for ESM import in node:
449
452
  0 && (module.exports = {
453
+ GROUPS_SESSION_KEY_DESCRIPTION,
450
454
  PlaneError,
451
455
  createApiblazeChat,
452
456
  createApiblazeGroups,
@@ -58,6 +58,7 @@ function createPlaneCore(cfg) {
58
58
  }
59
59
 
60
60
  // src/server/groups.ts
61
+ var GROUPS_SESSION_KEY_DESCRIPTION = "Users & Groups widget (minted per admin session)";
61
62
  function createApiblazeGroups(cfg) {
62
63
  if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId, email }.");
63
64
  const core = createPlaneCore({ cpKey: cfg.cpKey, apikeysBase: cfg.base, iamBase: cfg.iamBase });
@@ -84,7 +85,7 @@ function createApiblazeGroups(cfg) {
84
85
  environment: "prod",
85
86
  permissions: ["call", "manage-access"],
86
87
  expires_in_seconds: KEY_TTL_SECONDS,
87
- description: "Users & Groups widget (minted per admin session)"
88
+ description: GROUPS_SESSION_KEY_DESCRIPTION
88
89
  })
89
90
  });
90
91
  if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
@@ -343,13 +344,14 @@ function createApiblazeKeys(cfg) {
343
344
  catalogNames(user, tenant2)
344
345
  ]);
345
346
  const d = await listRes.json();
346
- const keys = [...d.keys ?? []];
347
+ const visible = (k) => k?.description !== GROUPS_SESSION_KEY_DESCRIPTION;
348
+ const keys = [...d.keys ?? []].filter(visible);
347
349
  let cursor = d.page?.next_cursor ?? null;
348
350
  for (let hop = 0; hop < 50 && cursor; hop++) {
349
351
  const more = await planeFetch("GET", "/apikeys", user, { query: { tenant: tenant2, limit: "200", cursor } });
350
352
  if (!more.ok) break;
351
353
  const md = await more.json();
352
- keys.push(...md.keys ?? []);
354
+ keys.push(...(md.keys ?? []).filter(visible));
353
355
  cursor = md.page?.next_cursor ?? null;
354
356
  }
355
357
  const eligible = allowed.filter((t) => catalog.includes(t));
@@ -417,6 +419,7 @@ function createApiblazeKeys(cfg) {
417
419
  return { handler, run };
418
420
  }
419
421
  export {
422
+ GROUPS_SESSION_KEY_DESCRIPTION,
420
423
  PlaneError,
421
424
  createApiblazeChat,
422
425
  createApiblazeGroups,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.21.22",
3
+ "version": "0.21.23",
4
4
  "description": "APIblaze CLI — Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
5
5
  "keywords": [
6
6
  "apiblaze",
@@ -93,5 +93,9 @@
93
93
  "react": {
94
94
  "optional": true
95
95
  }
96
+ },
97
+ "allowScripts": {
98
+ "esbuild": true,
99
+ "fsevents": true
96
100
  }
97
101
  }