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 +328 -201
- package/dist/server/index.d.mts +3 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +7 -3
- package/dist/server/index.mjs +6 -3
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__
|
|
|
34
34
|
var version;
|
|
35
35
|
var init_package = __esm({
|
|
36
36
|
"package.json"() {
|
|
37
|
-
version = "0.21.
|
|
37
|
+
version = "0.21.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');\
|
|
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
|
-
--
|
|
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:
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
.
|
|
6395
|
-
.
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
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
|
-
.
|
|
6406
|
-
.
|
|
6407
|
-
|
|
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
|
-
.
|
|
6413
|
+
ul.res li span { flex: 1; }
|
|
6414
6414
|
.empty { color: var(--muted); font-size: 14px; }
|
|
6415
|
-
|
|
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
|
-
|
|
6425
|
-
.
|
|
6426
|
-
|
|
6427
|
-
.
|
|
6428
|
-
|
|
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
|
-
<
|
|
6434
|
-
|
|
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
|
-
<
|
|
6437
|
-
<
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
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="
|
|
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
|
-
<
|
|
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
|
-
|
|
6459
|
-
<section
|
|
6460
|
-
<div class="
|
|
6461
|
-
<
|
|
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="
|
|
6467
|
-
<
|
|
6468
|
-
<
|
|
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((
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
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
|
-
|
|
6509
|
-
|
|
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
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
$('#
|
|
6519
|
-
if (!
|
|
6520
|
-
const
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
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
|
|
6530
|
-
if (
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
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
|
-
//
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
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
|
-
$('#
|
|
6562
|
-
if (c.
|
|
6563
|
-
if (c.
|
|
6564
|
-
|
|
6565
|
-
if (
|
|
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.
|
|
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:
|
|
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
|
|
6748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6778
|
-
|
|
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
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
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(
|
|
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
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
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
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
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(`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7897
|
-
- "
|
|
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
|
|
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
|
|
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
|
|
12856
|
-
const
|
|
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
|
|
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]",
|
|
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);
|
package/dist/server/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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:
|
|
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
|
|
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,
|
package/dist/server/index.mjs
CHANGED
|
@@ -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:
|
|
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
|
|
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.
|
|
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
|
}
|