apiblaze 0.21.41 → 0.21.43
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 +86 -62
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__
|
|
|
34
34
|
var version;
|
|
35
35
|
var init_package = __esm({
|
|
36
36
|
"package.json"() {
|
|
37
|
-
version = "0.21.
|
|
37
|
+
version = "0.21.43";
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -6505,7 +6505,7 @@ paths:
|
|
|
6505
6505
|
post:
|
|
6506
6506
|
operationId: createReservation
|
|
6507
6507
|
summary: Book a table
|
|
6508
|
-
description: Only the time is needed. The restaurant picks a free table and books it in the signed-in person's name, so never ask for a table number or a name.
|
|
6508
|
+
description: Only the time is needed; the day is today unless a date is sent. For tomorrow or any other day, work out the date and send it. The restaurant picks a free table and books it in the signed-in person's name, so never ask for a table number or a name.
|
|
6509
6509
|
requestBody:
|
|
6510
6510
|
required: true
|
|
6511
6511
|
content:
|
|
@@ -6533,7 +6533,7 @@ paths:
|
|
|
6533
6533
|
patch:
|
|
6534
6534
|
operationId: updateReservation
|
|
6535
6535
|
summary: Change a booking
|
|
6536
|
-
description: Move a booking to another table or time, or change the number of guests. Only the person who booked it (or the restaurant's admin) can change it. Get the id from listReservations; never ask for it.
|
|
6536
|
+
description: Move a booking to another day, table or time, or change the number of guests. Only the person who booked it (or the restaurant's admin) can change it. Get the id from listReservations; never ask for it.
|
|
6537
6537
|
requestBody:
|
|
6538
6538
|
required: true
|
|
6539
6539
|
content:
|
|
@@ -6545,7 +6545,7 @@ paths:
|
|
|
6545
6545
|
content:
|
|
6546
6546
|
application/json:
|
|
6547
6547
|
schema: { $ref: '#/components/schemas/Reservation' }
|
|
6548
|
-
'409': { description: That table is already booked at that time }
|
|
6548
|
+
'409': { description: That table is already booked that day at that time }
|
|
6549
6549
|
delete:
|
|
6550
6550
|
operationId: cancelReservation
|
|
6551
6551
|
summary: Cancel a booking
|
|
@@ -6560,12 +6560,14 @@ components:
|
|
|
6560
6560
|
properties:
|
|
6561
6561
|
name: { type: string, example: Ana, description: 'Optional: defaults to the signed-in person. Do not ask for it.' }
|
|
6562
6562
|
table: { type: integer, example: 4, description: 'Optional: the restaurant picks a free table. Do not ask for it.' }
|
|
6563
|
+
date: { type: string, format: date, example: '2026-10-09', description: 'Day of the booking, YYYY-MM-DD. Optional: today if not given.' }
|
|
6563
6564
|
time: { type: string, example: '19:30', description: 'Time of the booking, 24h HH:MM' }
|
|
6564
6565
|
guests: { type: integer, example: 2, description: 'How many people. Optional, 2 if not given.' }
|
|
6565
6566
|
ReservationChange:
|
|
6566
6567
|
type: object
|
|
6567
6568
|
properties:
|
|
6568
6569
|
table: { type: integer, example: 2, description: 'Move to this table.' }
|
|
6570
|
+
date: { type: string, format: date, example: '2026-10-10', description: 'New day, YYYY-MM-DD' }
|
|
6569
6571
|
time: { type: string, example: '20:00', description: 'New time, 24h HH:MM' }
|
|
6570
6572
|
guests: { type: integer, example: 4, description: 'New number of people' }
|
|
6571
6573
|
Reservation:
|
|
@@ -6575,7 +6577,7 @@ components:
|
|
|
6575
6577
|
properties:
|
|
6576
6578
|
id: { type: string, example: r1 }
|
|
6577
6579
|
`,
|
|
6578
|
-
"backend.js": "// ResiResi backend: restaurants (tenants, like Nino Pizza and Gino Pizza) let their customers\n// book, list and cancel tables. It runs on YOUR laptop.\n//\n// `npx apiblaze@latest dev` (which the demo runs for you) puts it on the internet\n// at a public URL and gives AI assistants an MCP address for it. Everything else\n// (API keys, sign-in, who may change what, rate limits) happens in APIblaze, in\n// front of this file. Look at what is NOT here: no login, no keys, no permission\n// checks, no AI code.\n//\n// What APIblaze tells this backend about each call, in two headers:\n// x-abz-tenant-id which customer (pizzeria) is calling\n// x-abz-user-id which person\n// and one shared secret it adds to every call it forwards (x-target-api-key), so a\n// caller who skips APIblaze and hits localhost:3001 directly is refused.\n//\n// Zero dependencies. Run it alone with `node backend.js` (port 3001).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst bookings = new Map();\nlet next = 1;\n// Bumps on every booking made or cancelled. The page asks the app (on this laptop, free)\n// whether it moved before re-listing through APIblaze, so an open, idle page spends nothing.\nlet changes = 0;\n// The last requests this laptop answered \u2014 the demo page shows them live.\nconst recent = [];\nfunction remember(entry) {\n recent.unshift({ at: Date.now(), ...entry });\n recent.length = Math.min(recent.length, 50);\n // One line per request in the terminal (`npx apiblaze@latest demo logs` follows it).\n const who = entry.who ? `${entry.who}${entry.tenant ? ` @ ${entry.tenant}` : ''}` : '-';\n console.log(`${new Date().toISOString().slice(11, 19)} ${entry.status} ${entry.method.padEnd(6)} ${entry.path.padEnd(22)} ${who}`);\n}\n\n// The restaurant picks the table: the lowest one still free at that time, at that restaurant.\nfunction freeTable(tenant, time) {\n const taken = new Set([...bookings.values()].filter((r) => r.tenant === tenant && r.time === time).map((r) => r.table));\n let t = 1;\n while (taken.has(t)) t++;\n return t;\n}\n\nfunction send(res, status, body) {\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(body === undefined ? '' : JSON.stringify(body));\n}\n\nfunction handle(req, res, body) {\n const url = new URL(req.url, 'http://x');\n if (url.pathname === '/openapi.yaml') {\n res.writeHead(200, { 'content-type': 'text/yaml' });\n return res.end(fs.readFileSync(path.join(__dirname, 'openapi.yaml')));\n }\n const who = req.headers['x-abz-user-id'] || '';\n const tenant = req.headers['x-abz-tenant-id'] || '';\n const reply = (status, data) => { remember({ method: req.method, path: url.pathname, who, tenant, status }); send(res, status, data); };\n const secret = process.env.BACKEND_SECRET || '';\n if (secret && req.headers['x-target-api-key'] !== secret) return reply(401, { error: 'missing or wrong backend secret (APIblaze adds it to every call it forwards)' });\n if (!who) return reply(401, { error: 'say who is calling (x-abz-user-id)' });\n\n const m = /^\\/reservations(?:\\/([^/]+))?$/.exec(url.pathname);\n if (!m) return reply(404, { error: 'not found' });\n const id = m[1];\n\n if (!id && req.method === 'POST') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const time = String(input.time || '19:30');\n const row = {\n id: `r${next++}`, name: String(input.name || who), table: Number(input.table) || freeTable(tenant, time),\n time, guests: Number(input.guests) || 2, owner: who, tenant,\n };\n bookings.set(row.id, row); changes++;\n return reply(201, row);\n }\n if (!id && req.method === 'GET') {\n // Lists are the backend's job: each person sees their own bookings, at their own pizzeria.\n return reply(200, [...bookings.values()].filter((r) => r.owner === who && r.tenant === tenant));\n }\n const row = bookings.get(id);\n if (!row) return reply(404, { error: 'no such booking' });\n if (req.method === 'GET') return reply(200, row);\n if (req.method === 'PATCH') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const
|
|
6580
|
+
"backend.js": "// ResiResi backend: restaurants (tenants, like Nino Pizza and Gino Pizza) let their customers\n// book, list and cancel tables. It runs on YOUR laptop.\n//\n// `npx apiblaze@latest dev` (which the demo runs for you) puts it on the internet\n// at a public URL and gives AI assistants an MCP address for it. Everything else\n// (API keys, sign-in, who may change what, rate limits) happens in APIblaze, in\n// front of this file. Look at what is NOT here: no login, no keys, no permission\n// checks, no AI code.\n//\n// What APIblaze tells this backend about each call, in two headers:\n// x-abz-tenant-id which customer (pizzeria) is calling\n// x-abz-user-id which person\n// and one shared secret it adds to every call it forwards (x-target-api-key), so a\n// caller who skips APIblaze and hits localhost:3001 directly is refused.\n//\n// Zero dependencies. Run it alone with `node backend.js` (port 3001).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst bookings = new Map();\nlet next = 1;\n// Bumps on every booking made or cancelled. The page asks the app (on this laptop, free)\n// whether it moved before re-listing through APIblaze, so an open, idle page spends nothing.\nlet changes = 0;\n// The last requests this laptop answered \u2014 the demo page shows them live.\nconst recent = [];\nfunction remember(entry) {\n recent.unshift({ at: Date.now(), ...entry });\n recent.length = Math.min(recent.length, 50);\n // One line per request in the terminal (`npx apiblaze@latest demo logs` follows it).\n const who = entry.who ? `${entry.who}${entry.tenant ? ` @ ${entry.tenant}` : ''}` : '-';\n console.log(`${new Date().toISOString().slice(11, 19)} ${entry.status} ${entry.method.padEnd(6)} ${entry.path.padEnd(22)} ${who}`);\n}\n\n// The restaurant's day, YYYY-MM-DD, on this laptop's clock: a booking without a date is for today.\nfunction today() {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n// A real calendar day written YYYY-MM-DD (2026-02-30 is not one).\nfunction isDay(s) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false;\n const [y, m, d] = s.split('-').map(Number);\n const t = new Date(Date.UTC(y, m - 1, d));\n return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;\n}\n\n// The restaurant picks the table: the lowest one still free that day at that time, at that restaurant.\nfunction freeTable(tenant, date, time) {\n const taken = new Set([...bookings.values()].filter((r) => r.tenant === tenant && r.date === date && r.time === time).map((r) => r.table));\n let t = 1;\n while (taken.has(t)) t++;\n return t;\n}\n\nfunction send(res, status, body) {\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(body === undefined ? '' : JSON.stringify(body));\n}\n\nfunction handle(req, res, body) {\n const url = new URL(req.url, 'http://x');\n if (url.pathname === '/openapi.yaml') {\n res.writeHead(200, { 'content-type': 'text/yaml' });\n return res.end(fs.readFileSync(path.join(__dirname, 'openapi.yaml')));\n }\n const who = req.headers['x-abz-user-id'] || '';\n const tenant = req.headers['x-abz-tenant-id'] || '';\n const reply = (status, data) => { remember({ method: req.method, path: url.pathname, who, tenant, status }); send(res, status, data); };\n const secret = process.env.BACKEND_SECRET || '';\n if (secret && req.headers['x-target-api-key'] !== secret) return reply(401, { error: 'missing or wrong backend secret (APIblaze adds it to every call it forwards)' });\n if (!who) return reply(401, { error: 'say who is calling (x-abz-user-id)' });\n\n const m = /^\\/reservations(?:\\/([^/]+))?$/.exec(url.pathname);\n if (!m) return reply(404, { error: 'not found' });\n const id = m[1];\n\n if (!id && req.method === 'POST') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const date = input.date ? String(input.date) : today();\n if (!isDay(date)) return reply(400, { error: 'date must be a day, YYYY-MM-DD' });\n const time = String(input.time || '19:30');\n const row = {\n id: `r${next++}`, name: String(input.name || who), table: Number(input.table) || freeTable(tenant, date, time),\n date, time, guests: Number(input.guests) || 2, owner: who, tenant,\n };\n bookings.set(row.id, row); changes++;\n return reply(201, row);\n }\n if (!id && req.method === 'GET') {\n // Lists are the backend's job: each person sees their own bookings, at their own pizzeria.\n return reply(200, [...bookings.values()].filter((r) => r.owner === who && r.tenant === tenant)\n .sort((a, b) => `${a.date} ${a.time}`.localeCompare(`${b.date} ${b.time}`)));\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 === 'PATCH') {\n let input = {};\n try { input = JSON.parse(body || '{}'); } catch { return reply(400, { error: 'body must be JSON' }); }\n const date = input.date ? String(input.date) : row.date;\n if (!isDay(date)) return reply(400, { error: 'date must be a day, YYYY-MM-DD' });\n const time = input.time ? String(input.time) : row.time;\n const moved = date !== row.date || time !== row.time;\n const table = input.table ? Number(input.table) : (moved ? freeTable(row.tenant, date, time) : row.table);\n const clash = [...bookings.values()].find((r) => r.id !== row.id && r.tenant === row.tenant && r.date === date && r.time === time && r.table === table);\n if (clash) return reply(409, { error: `table ${table} is already booked on ${date} at ${time}` });\n Object.assign(row, { date, time, table, ...(input.guests ? { guests: Number(input.guests) } : {}) }); changes++;\n return reply(200, row);\n }\n if (req.method === 'DELETE') { bookings.delete(id); changes++; return reply(204); }\n return reply(405, { error: 'method not allowed' });\n}\n\nfunction startBackend(port = Number(process.env.BACKEND_PORT) || 3001) {\n const server = http.createServer((req, res) => {\n let body = '';\n req.on('data', (c) => { body += c; });\n req.on('end', () => {\n try { handle(req, res, body); } catch (e) { send(res, 500, { error: String((e && e.message) || e) }); }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startBackend, recent, changeCount: () => changes };\nif (require.main === module) startBackend().then(() => console.log('backend on http://localhost:3001'));\n",
|
|
6579
6581
|
"app.js": "// ResiResi website server: serves the page and makes its calls.\n//\n// PROTECTED (the default): every call goes to the PUBLIC API \u2014 through APIblaze \u2014 with the\n// pizzeria's server key (kept here, never in the browser) and X-End-User-Id naming the\n// person. APIblaze checks the key, applies the rules and forwards to the backend.\n//\n// DIRECT (the page's \"unprotected\" switch): the same calls go straight to the backend on\n// localhost, with the shared backend secret and whatever user the page claims. No APIblaze:\n// nobody checks who may do what. That is how most apps ship.\n//\n// The chat and the two admin widgets use apiblaze's own server helpers\n// (lib/apiblaze-server.js, copied in by `npx apiblaze@latest demo` \u2014 the same code as\n// `import \u2026 from 'apiblaze/server'`).\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst env = (name, fallback = '') => process.env[name] || fallback;\nconst json = (res, status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };\n\nlet lib = null;\ntry { lib = require('./lib/apiblaze-server.js'); } catch { /* run without the widgets */ }\n\n// The two pizzerias (tenants) and the people the demo lets you be. A real app takes\n// \"who is this\" from its own login.\nconst PIZZERIAS = {\n nino: { label: 'Nino Pizza', tenant: () => env('APIBLAZE_TENANT', env('DEMO_NINO_TENANT')), key: () => env('APIBLAZE_SERVER_KEY'), admin: { userId: 'ana', label: 'Ana', email: 'ana@ninopizza.example' } },\n gino: { label: 'Gino Pizza', tenant: () => env('APIBLAZE_GINO_TENANT', env('DEMO_GINO_TENANT')), key: () => env('APIBLAZE_GINO_KEY'), admin: { userId: 'gina', label: 'Gina', email: 'gina@ginopizza.example' } },\n};\n// Each pizzeria (tenant) has its own MCP address: https://{proxy}-{tenant}.mcp.\u2026\nconst portalFor = (tenant) => (env('APIBLAZE_PORTAL_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_PORTAL_URL').replace(`${env('APIBLAZE_TENANT')}.portal`, `${tenant}.portal`) : '');\nconst mcpFor = (tenant) => (env('APIBLAZE_MCP_URL') && env('APIBLAZE_TENANT') && tenant ? env('APIBLAZE_MCP_URL').replace(`-${env('APIBLAZE_TENANT')}.`, `-${tenant}.`) : '');\nconst CUSTOMERS = { ana: 'Ana', ben: 'Ben' };\nconst pizzeriaOf = (id) => PIZZERIAS[id] || PIZZERIAS.nino;\nfunction queryOf(req) { return new URL(req.url, 'http://x').searchParams; }\n\nasync function call(op, { pizzeria = 'nino', as = 'ana', id, body, direct = false } = {}) {\n const pz = pizzeriaOf(pizzeria);\n const base = direct ? `http://127.0.0.1:${env('BACKEND_PORT', '3001')}` : env('APIBLAZE_URL').replace(/\\/$/, '');\n const headers = { 'content-type': 'application/json' };\n if (direct) {\n // Straight to localhost: the backend trusts whatever these headers say.\n headers['x-target-api-key'] = env('BACKEND_SECRET');\n headers['x-abz-user-id'] = as;\n headers['x-abz-tenant-id'] = pz.tenant();\n } else {\n headers['x-api-key'] = pz.key();\n headers['x-end-user-id'] = as;\n }\n const req = op === 'book' ? { method: 'POST', url: `${base}/reservations`, body: JSON.stringify({ name: CUSTOMERS[as] || as, ...(body || {}) }) }\n : op === 'cancel' ? { method: 'DELETE', url: `${base}/reservations/${encodeURIComponent(id || '')}` }\n : { method: 'GET', url: `${base}/reservations` };\n const r = await fetch(req.url, { method: req.method, headers, body: req.body });\n const text = await r.text();\n let data = null;\n try { data = text ? JSON.parse(text) : null; } catch { data = text; }\n return { status: r.status, body: data };\n}\n\n// Node http <-> the web-standard handlers apiblaze/server exports.\nasync function viaHandler(handler, req, res, raw) {\n const request = new Request(`http://localhost${req.url}`, {\n method: req.method,\n headers: { 'content-type': req.headers['content-type'] || 'application/json' },\n body: ['GET', 'HEAD'].includes(req.method) ? undefined : raw,\n });\n const out = await handler(request);\n const headers = {};\n out.headers.forEach((v, k) => { headers[k] = v; });\n res.writeHead(out.status, headers);\n if (!out.body) return res.end();\n const reader = out.body.getReader();\n for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }\n res.end();\n}\n\nfunction widgets() {\n if (!lib) return {};\n const out = { chat: {} };\n for (const [id, pz] of Object.entries(PIZZERIAS)) {\n if (!pz.tenant() || !pz.key()) continue;\n // The assistant acts as the signed-in customer, at that customer's pizzeria.\n out.chat[id] = lib.createApiblazeChat({\n project: `${env('APIBLAZE_NAME')}-${pz.tenant()}`, apiKey: pz.key(),\n host: 'tryabz.run', environment: 'dev', getUser: (req) => ({ userId: queryOf(req).get('as') || 'ana' }),\n });\n }\n if (env('APIBLAZE_CP_KEY') && env('APIBLAZE_WIDGETS') === '1') {\n // Admin widgets: the admin of the chosen pizzeria.\n const admin = (req) => { const pz = pizzeriaOf(queryOf(req).get('pizzeria')); return { tenant: pz.tenant(), ...pz.admin }; };\n out.keys = lib.createApiblazeKeys({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin, environment: 'dev' });\n out.groups = lib.createApiblazeGroups({ cpKey: env('APIBLAZE_CP_KEY'), getUser: admin });\n }\n return out;\n}\n\n/** One of the two websites: `direct` = straight to the backend (unprotected), else through APIblaze. */\nfunction startApp(port = Number(env('APP_PORT', '3000')), { direct = false } = {}) {\n const w = direct ? {} : widgets();\n const pub = path.join(__dirname, 'public');\n const server = http.createServer((req, res) => {\n let raw = '';\n req.on('data', (c) => { raw += c; });\n req.on('end', async () => {\n try {\n const url = new URL(req.url, 'http://x');\n const p = url.pathname;\n if (req.method === 'GET' && (p === '/' || p === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, 'index.html')));\n }\n for (const asset of ['apiblaze-widgets.js', 'apiblaze-logo.svg']) {\n if (req.method === 'GET' && p === `/${asset}` && fs.existsSync(path.join(pub, asset))) {\n res.writeHead(200, { 'content-type': asset.endsWith('.svg') ? 'image/svg+xml' : 'text/javascript; charset=utf-8' });\n return res.end(fs.readFileSync(path.join(pub, asset)));\n }\n }\n if (p === '/api/config') {\n return json(res, 200, {\n direct, apiUrl: env('APIBLAZE_URL'), portalUrl: env('APIBLAZE_PORTAL_URL'),\n backendPort: env('BACKEND_PORT', '3001'),\n protectedUrl: env('APIBLAZE_URL') ? `http://localhost:${env('APP_PORT', '3000')}` : '',\n directUrl: `http://localhost:${env('DIRECT_PORT', '3002')}`,\n logsCommand: `npx apiblaze@latest logs ${env('APIBLAZE_NAME')} --tenant ${env('APIBLAZE_TENANT')}`,\n pizzerias: Object.entries(PIZZERIAS).filter(([, pz]) => pz.tenant()).map(([id, pz]) => ({ id, label: pz.label, admin: pz.admin.label, mcpUrl: mcpFor(pz.tenant()), portalUrl: portalFor(pz.tenant()) })),\n widgets: { chat: Object.keys(w.chat || {}), keys: !!w.keys, groups: !!w.groups },\n });\n }\n if (p === '/api/changes') {\n // Local and free: has any booking changed since the page last listed? The page\n // re-lists through APIblaze only when it has (the backend runs in this process).\n let n = 0;\n try { n = require('./backend').changeCount(); } catch { /* backend runs elsewhere */ }\n return json(res, 200, { n });\n }\n if (p === '/api/apiblaze/chat') {\n const h = (w.chat || {})[url.searchParams.get('pizzeria') || 'nino'];\n if (h) return viaHandler(h.handler, req, res, raw);\n }\n if (p === '/api/apiblaze/keys' && w.keys) return viaHandler(w.keys.handler, req, res, raw);\n if (p === '/api/apiblaze/groups' && w.groups) return viaHandler(w.groups.handler, req, res, raw);\n if (req.method === 'POST' && p === '/api/act') {\n const b = JSON.parse(raw || '{}');\n return json(res, 200, await call(String(b.op || 'list'), { pizzeria: b.pizzeria, as: String(b.as || 'ana'), id: b.id, body: b.body, direct }));\n }\n json(res, 404, { error: 'not found' });\n } catch (e) {\n json(res, 502, { error: `Could not reach the API: ${String((e && e.message) || e)}` });\n }\n });\n });\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => resolve(server));\n });\n}\n\nmodule.exports = { startApp };\n",
|
|
6580
6582
|
"start.js": "// Starts ResiResi on this laptop: the backend (3001), the unprotected front-end straight to it\n// (3002) and, once APIblaze is set up, the protected website through APIblaze (3000).\n// No APIblaze involved here \u2014 the tunnel is `npx apiblaze@latest demo start|stop|restart`.\n//\n// node start.js run in the foreground (Ctrl-C stops)\n// npm run start run in the background\n// npm run stop stop the background one\n// npm run restart stop, then start again\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst PID = path.join(__dirname, '.demo', 'app.pid');\n\nfunction loadEnv(file = path.join(__dirname, '.env.local')) {\n if (!fs.existsSync(file)) return;\n for (const line of fs.readFileSync(file, 'utf8').split('\\n')) {\n const m = /^\\s*([A-Z0-9_]+)\\s*=\\s*(.*)\\s*$/.exec(line);\n if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^\"(.*)\"$/, '$1');\n }\n}\n\nasync function start() {\n loadEnv();\n const { startBackend } = require('./backend');\n const { startApp } = require('./app');\n const backendPort = Number(process.env.BACKEND_PORT) || 3001;\n const appPort = Number(process.env.APP_PORT) || 3000;\n const directPort = Number(process.env.DIRECT_PORT) || 3002;\n await startBackend(backendPort);\n // The unprotected front-end: straight to the backend, no APIblaze.\n await startApp(directPort, { direct: true });\n // The protected website: through APIblaze (once the demo has written its keys).\n if (process.env.APIBLAZE_URL) await startApp(appPort);\n return { backendPort, appPort, directPort };\n}\n\nconst alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };\nfunction runningPid() {\n try { const pid = Number(fs.readFileSync(PID, 'utf8')); return pid && alive(pid) ? pid : null; } catch { return null; }\n}\nasync function stop() {\n const pid = runningPid();\n if (!pid) { console.log('ResiResi is not running.'); return; }\n process.kill(pid, 'SIGTERM');\n for (let i = 0; i < 30 && alive(pid); i++) await new Promise((r) => setTimeout(r, 100));\n try { fs.unlinkSync(PID); } catch { /* gone */ }\n console.log('Stopped ResiResi (backend and websites).');\n}\nfunction background() {\n if (runningPid()) { console.log('ResiResi is already running.'); return; }\n fs.mkdirSync(path.dirname(PID), { recursive: true });\n const log = fs.openSync(path.join(__dirname, '.demo', 'app.log'), 'a');\n const child = spawn(process.execPath, [__filename], { cwd: __dirname, detached: true, stdio: ['ignore', log, log] });\n child.unref();\n fs.writeFileSync(PID, String(child.pid));\n loadEnv();\n console.log(`Started ResiResi: backend localhost:${process.env.BACKEND_PORT || 3001}, unprotected front-end http://localhost:${process.env.DIRECT_PORT || 3002}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${process.env.APP_PORT || 3000}` : ''}.`);\n}\n\nmodule.exports = { start, loadEnv };\nif (require.main === module) {\n const arg = process.argv[2];\n if (arg === '--stop') stop();\n else if (arg === '--background') background();\n else if (arg === '--restart') stop().then(() => setTimeout(background, 300));\n else {\n start().then(({ appPort, backendPort, directPort }) => console.log(`ResiResi running: backend localhost:${backendPort}, unprotected front-end http://localhost:${directPort}${process.env.APIBLAZE_URL ? `, protected website http://localhost:${appPort}` : ''}. Ctrl-C stops.`))\n .catch((e) => { console.error(`Could not start: ${e.message}`); process.exit(1); });\n }\n}\n",
|
|
6581
6583
|
"public/index.html": `<!doctype html>
|
|
@@ -6693,6 +6695,7 @@ components:
|
|
|
6693
6695
|
<p class="sub">Book here, or ask the assistant. Each person only sees their own.</p>
|
|
6694
6696
|
<div class="form">
|
|
6695
6697
|
<label><input type="number" id="guests" min="1" max="20" value="2"> people</label>
|
|
6698
|
+
<label>on <input type="date" id="date"></label>
|
|
6696
6699
|
<label>at <input type="time" id="time" value="19:30"></label>
|
|
6697
6700
|
<button class="act primary" id="book">Book a table</button>
|
|
6698
6701
|
</div>
|
|
@@ -6722,6 +6725,16 @@ components:
|
|
|
6722
6725
|
<script src="/apiblaze-widgets.js"></script>
|
|
6723
6726
|
<script>
|
|
6724
6727
|
const $ = (s) => document.querySelector(s);
|
|
6728
|
+
// Days as the backend writes them (YYYY-MM-DD), on this laptop's clock; shown as today / tomorrow / Thu, Oct 9.
|
|
6729
|
+
const localDay = (offset = 0) => { const d = new Date(); d.setDate(d.getDate() + offset); return \`\${d.getFullYear()}-\${String(d.getMonth() + 1).padStart(2, '0')}-\${String(d.getDate()).padStart(2, '0')}\`; };
|
|
6730
|
+
const dayLabel = (day) => {
|
|
6731
|
+
if (!day || day === localDay()) return 'today';
|
|
6732
|
+
if (day === localDay(1)) return 'tomorrow';
|
|
6733
|
+
const [y, m, d] = day.split('-').map(Number);
|
|
6734
|
+
return new Date(y, m - 1, d).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
|
|
6735
|
+
};
|
|
6736
|
+
// The form books today unless they pick another day (and never a day already gone).
|
|
6737
|
+
$('#date').value = $('#date').min = localDay();
|
|
6725
6738
|
const W = window.ApiblazeWidgets;
|
|
6726
6739
|
const theme = { accent: '#e4572e' };
|
|
6727
6740
|
// Every switch (pizzeria, tab, person) is a fresh page load, the choice kept in the address:
|
|
@@ -6802,7 +6815,7 @@ async function refresh() {
|
|
|
6802
6815
|
const li = document.createElement('li');
|
|
6803
6816
|
if (!seen.has(b.id)) { li.className = 'new'; seen.add(b.id); }
|
|
6804
6817
|
const t = document.createElement('span');
|
|
6805
|
-
t.textContent = \`\${b.guests || 2} people \xB7 \${b.time} \xB7 table \${b.table} \xB7 #\${b.id}\`;
|
|
6818
|
+
t.textContent = \`\${b.guests || 2} people \xB7 \${dayLabel(b.date)} \${b.time} \xB7 table \${b.table} \xB7 #\${b.id}\`;
|
|
6806
6819
|
const x = document.createElement('button'); x.className = 'act'; x.textContent = 'Cancel';
|
|
6807
6820
|
x.onclick = async () => { await act('cancel', { id: b.id }); refresh(); };
|
|
6808
6821
|
li.append(t, x); ul.append(li);
|
|
@@ -6811,8 +6824,9 @@ async function refresh() {
|
|
|
6811
6824
|
}
|
|
6812
6825
|
$('#book').onclick = async () => {
|
|
6813
6826
|
const guests = Math.max(1, Number($('#guests').value) || 2);
|
|
6827
|
+
const date = $('#date').value || localDay();
|
|
6814
6828
|
const time = $('#time').value || '19:30';
|
|
6815
|
-
await act('book', { body: { guests, time } }); // the restaurant picks the table
|
|
6829
|
+
await act('book', { body: { guests, date, time } }); // the restaurant picks the table
|
|
6816
6830
|
refresh();
|
|
6817
6831
|
};
|
|
6818
6832
|
$('#ben-cancel').onclick = async () => {
|
|
@@ -6849,7 +6863,7 @@ function mountChat() {
|
|
|
6849
6863
|
W.chat('#chat', { endpoint: \`/api/apiblaze/chat?pizzeria=\${state.pizzeria}&as=\${me()}\`, mode: 'inline', title: \`\${pz().label} assistant\`, avatar: '\u{1F355}', height: 520, theme,
|
|
6850
6864
|
storageKey: 'pizza-chat-' + key,
|
|
6851
6865
|
welcome: \`Hi \${who}! I can book, list and cancel your tables at \${pz().label}. I'm using the app on your laptop, through APIblaze.\`,
|
|
6852
|
-
suggestions: ['Book a table for 4 people at 8pm', 'What are my reservations?'] });
|
|
6866
|
+
suggestions: ['Book a table for 4 people tomorrow at 8pm', 'What are my reservations?'] });
|
|
6853
6867
|
}
|
|
6854
6868
|
|
|
6855
6869
|
// \u2500\u2500 admin \u2500\u2500
|
|
@@ -7918,7 +7932,7 @@ async function waitTunnel(dir) {
|
|
|
7918
7932
|
}
|
|
7919
7933
|
async function waitThrough(apiUrl, serverKey) {
|
|
7920
7934
|
let last = 0;
|
|
7921
|
-
for (let i = 0; i <
|
|
7935
|
+
for (let i = 0; i < 120; i++) {
|
|
7922
7936
|
try {
|
|
7923
7937
|
const r = await fetch(`${apiUrl}/reservations`, { headers: { "x-api-key": serverKey, "x-end-user-id": "ana" } });
|
|
7924
7938
|
last = r.status;
|
|
@@ -8109,7 +8123,9 @@ paths:`));
|
|
|
8109
8123
|
console.log(import_chalk24.default.cyan(` curl -X POST http://localhost:${backendPort2}/reservations \\`));
|
|
8110
8124
|
console.log(import_chalk24.default.cyan(` -H "x-target-api-key: $(grep BACKEND_SECRET ${import_path5.default.join(rel, ".env.local")} | cut -d= -f2)" \\`));
|
|
8111
8125
|
console.log(import_chalk24.default.cyan(` -H "x-abz-user-id: ana" -H "x-abz-tenant-id: ${ninoTenant}" \\`));
|
|
8112
|
-
|
|
8126
|
+
const tomorrow = new Date(Date.now() + 864e5);
|
|
8127
|
+
const day = `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, "0")}-${String(tomorrow.getDate()).padStart(2, "0")}`;
|
|
8128
|
+
console.log(import_chalk24.default.cyan(` -H "content-type: application/json" -d '{"date":"${day}","time":"19:30","guests":2}'`));
|
|
8113
8129
|
if (!await confirmStep(opts, `Ok to put an ${B.product} proxy and MCP in front of the reservation backend?`)) {
|
|
8114
8130
|
return stopHere("proxy", [
|
|
8115
8131
|
`Stopped before ${B.product}. ResiResi keeps running on this laptop: http://localhost:${directPort} ${import_chalk24.default.dim(`(${cdDir}npm run stop stops it)`)}`,
|
|
@@ -9655,14 +9671,15 @@ function folderReport(f, opts = {}) {
|
|
|
9655
9671
|
function blockedMarkdown() {
|
|
9656
9672
|
return `## If a command is blocked or fails
|
|
9657
9673
|
|
|
9658
|
-
|
|
9659
|
-
- **A step the rest depends on** (the tunnel, creating the proxy, its key):
|
|
9660
|
-
|
|
9661
|
-
(\`! <command>\` in Claude Code).
|
|
9662
|
-
- **An optional step** (groups, admins, widgets, a rule, limits):
|
|
9663
|
-
|
|
9664
|
-
-
|
|
9665
|
-
|
|
9674
|
+
When a permission prompt is declined, a command is denied, or a step needs access you do not have:
|
|
9675
|
+
- **A step the rest depends on** (the tunnel, creating the proxy, its key): nothing after it can
|
|
9676
|
+
work, so the useful move is to stop there, say what it was for, and give the user the exact
|
|
9677
|
+
command to run themselves (\`! <command>\` in Claude Code).
|
|
9678
|
+
- **An optional step** (groups, admins, widgets, a rule, limits): the rest works without it. Skip
|
|
9679
|
+
it and say what is missing because of it.
|
|
9680
|
+
- Working around a blocked step (retrying it in pieces, another route, code written to replace
|
|
9681
|
+
it) leaves a half-built setup that fails in ways nobody can read. And something only works
|
|
9682
|
+
once it has been checked: a call that answered, or a page that loaded.
|
|
9666
9683
|
`;
|
|
9667
9684
|
}
|
|
9668
9685
|
function staleSkillCopies(cwd = process.cwd(), home = import_os2.default.homedir()) {
|
|
@@ -9682,90 +9699,97 @@ function heroMarkdown(found, stale = []) {
|
|
|
9682
9699
|
const run2 = `npx ${B.cli}@latest`;
|
|
9683
9700
|
const report = found ? folderReport(found) : null;
|
|
9684
9701
|
const port = found ? backendPort(found) : void 0;
|
|
9685
|
-
const folder = report ? `## This folder
|
|
9702
|
+
const folder = report ? `## This folder (the CLI checked it just now)
|
|
9686
9703
|
|
|
9687
9704
|
${report.lines.map((l) => `- ${l}`).join("\n")}
|
|
9688
9705
|
` : `## First, the folder
|
|
9689
9706
|
|
|
9690
|
-
Run \`${run2} skills\` in the project folder
|
|
9691
|
-
|
|
9707
|
+
Run \`${run2} skills\` in the project folder: it checks the folder and prints the current version
|
|
9708
|
+
of this guide, with the next step that fits it.
|
|
9692
9709
|
`;
|
|
9693
|
-
const question = report?.question ?? "
|
|
9710
|
+
const question = report?.question ?? "the question it prints for the folder";
|
|
9694
9711
|
const ownApi = report?.ownApi && found ? (() => {
|
|
9695
9712
|
const specArg = found.specFile ? ` --openapi ${found.specFile}` : "";
|
|
9696
|
-
const steps = [
|
|
9713
|
+
const steps = [];
|
|
9697
9714
|
if (!found.listening) {
|
|
9698
9715
|
steps.push(`Start it first as its own detached process${found.start ? `: \`nohup ${found.start} > app.log 2>&1 < /dev/null &\`` : ", with its usual start command"}. Wait until localhost:${port ?? "<port>"} answers.`);
|
|
9699
9716
|
}
|
|
9700
9717
|
if (!found.specFile && !found.specUrl) {
|
|
9701
|
-
steps.push(
|
|
9718
|
+
steps.push("Write `./openapi.yaml` from its route files first: every path and method, and the id each POST returns.");
|
|
9702
9719
|
}
|
|
9703
9720
|
const spec2 = found.specFile ? specArg : !found.specUrl ? " --openapi ./openapi.yaml" : "";
|
|
9704
|
-
steps.push(`Open the tunnel as its own detached process
|
|
9721
|
+
steps.push(`Open the tunnel as its own detached process (a \`cmd &\` inside a shell call that ends is killed with it):
|
|
9705
9722
|
\`nohup ${run2} dev --port ${port ?? "<port>"}${spec2} --auto > ${B.cli}-dev.log 2>&1 < /dev/null &\`
|
|
9706
|
-
|
|
9707
|
-
steps.push(`
|
|
9708
|
-
steps.push(`
|
|
9723
|
+
${B.cli}-dev.log says "is live on the internet" after about 20 seconds. It saves the key to .env.local itself.`);
|
|
9724
|
+
steps.push(`What they have then, with the two addresses it printed: their API on the internet at the API address, and an address AI assistants can use (MCP). API keys, sign-in, rules for who can change what, and rate limits are already on. The key is saved to .env.local and never printed. The tunnel keeps running.`);
|
|
9725
|
+
steps.push(`The natural next step: ready for real customers, with their own API keys, sign-in, and only-the-owner-can-change rules. \`${run2} skills ship\` has that procedure, from "Adopt".`);
|
|
9709
9726
|
return `
|
|
9710
|
-
## Their API
|
|
9727
|
+
## Their API, if they want it behind ${P}
|
|
9711
9728
|
|
|
9712
9729
|
${steps.map((t, i) => `${i + 1}. ${t}`).join("\n")}
|
|
9713
9730
|
`;
|
|
9714
9731
|
})() : "";
|
|
9715
|
-
|
|
9732
|
+
const frontMatter = found ? "" : `---
|
|
9716
9733
|
name: ${SKILL_NAME}
|
|
9717
9734
|
description: Use when the user asks what ${P} (or ${B.cli}) does or can do for them; says yes to a demo or asks to see / try / demo it; wants to ship, share or expose an API; wants API keys, sign-in, per-user data, rate limits or quotas; wants AI agents to use their app safely; or says users can reach each other's records.
|
|
9718
9735
|
---
|
|
9719
9736
|
|
|
9720
|
-
|
|
9737
|
+
`;
|
|
9738
|
+
return `${frontMatter}# ${P}
|
|
9721
9739
|
|
|
9722
|
-
|
|
9740
|
+
What \`${run2} skills\` reports: what ${P} is, what is in this folder, and the next steps you can
|
|
9741
|
+
offer. Nothing here needs to run before the user says so.
|
|
9723
9742
|
${stale.length ? `
|
|
9724
|
-
##
|
|
9743
|
+
## Older copies of this guide are installed
|
|
9725
9744
|
|
|
9726
9745
|
${stale.map((f) => `- \`${f}\``).join("\n")}
|
|
9727
9746
|
|
|
9728
|
-
They
|
|
9729
|
-
|
|
9747
|
+
They were written for an older CLI; this output is the current one. The user can refresh a copy
|
|
9748
|
+
with \`${run2} skills --install --force\` in its folder.
|
|
9730
9749
|
` : ""}
|
|
9731
9750
|
${folder}
|
|
9732
|
-
##
|
|
9751
|
+
## What ${P} is
|
|
9733
9752
|
|
|
9734
|
-
|
|
9753
|
+
${pitchText()}
|
|
9735
9754
|
|
|
9736
|
-
"
|
|
9755
|
+
For a first "what does it do", that is usually enough: the demo shows the rest. The next step to
|
|
9756
|
+
offer: ${question}
|
|
9737
9757
|
|
|
9738
|
-
|
|
9758
|
+
Most people new to it do not know the words proxy, tenant, OpenAPI or header, so plain words work best.
|
|
9739
9759
|
${ownApi}
|
|
9740
|
-
##
|
|
9760
|
+
## The demo, if they want to see it
|
|
9761
|
+
|
|
9762
|
+
\`${run2} demo\` takes about 30 seconds, needs no login and asks nothing. It:
|
|
9763
|
+
- writes a small table-booking app into ./${B.cli}-demo and runs it on this machine (localhost, from
|
|
9764
|
+
port 3000 up, skipping any port another program uses);
|
|
9765
|
+
- puts a temporary ${P} gateway in front of it (anonymous, no account) with a tunnel to the app;
|
|
9766
|
+
- prints the links once the page is live, then exits. It opens nothing and never prints a key
|
|
9767
|
+
(keys are saved in ./${B.cli}-demo/.env.local).
|
|
9741
9768
|
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
4. If it prints \u2717, say its one sentence and run the one command it gives, once.
|
|
9769
|
+
Run it in the foreground. It is self-contained: nothing in its folder needs reading or changing,
|
|
9770
|
+
and its own output says what happened. When it prints \u2713, the links it printed are the thing to
|
|
9771
|
+
show, in plain words:
|
|
9772
|
+
- the booking app runs on their laptop, and ${P} just gave it a public address, plus one for AI
|
|
9773
|
+
assistants;
|
|
9774
|
+
- on the Reservations tab, asking the assistant to book a table makes it appear in the list; the
|
|
9775
|
+
switch at the top shows the same app straight to localhost, unprotected;
|
|
9776
|
+
- the Admin tab is what each pizzeria's admin gets: their own API keys and people, kept apart.
|
|
9777
|
+
The question it leads to: **"Want to put your own app on the internet like this?"**
|
|
9752
9778
|
|
|
9753
|
-
|
|
9754
|
-
files, open logs, run other commands, or write code. Never show a key: the demo saves it
|
|
9755
|
-
in \`.env.local\` and never prints it.
|
|
9779
|
+
If it prints \u2717, its one sentence says what failed and the one command after it is the fix.
|
|
9756
9780
|
|
|
9757
9781
|
${blockedMarkdown()}
|
|
9758
|
-
## Their own app
|
|
9782
|
+
## Their own app, after the demo
|
|
9759
9783
|
|
|
9760
|
-
|
|
9761
|
-
|
|
9784
|
+
On this machine \u2192 \`${run2} dev\`; deployed \u2192 \`${run2} create\`. \`${run2} skills ship\` has the
|
|
9785
|
+
whole procedure.
|
|
9762
9786
|
|
|
9763
|
-
## One thing at a time
|
|
9787
|
+
## One thing at a time
|
|
9764
9788
|
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
their
|
|
9789
|
+
Each topic has its own page: \`${run2} skills keys\` (API keys) \xB7 \`signin\` (OAuth sign-in and the
|
|
9790
|
+
developer portal) \xB7 \`rules\` (who can change what) \xB7 \`limits\` (rate limits and quotas) \xB7 \`mcp\`
|
|
9791
|
+
(AI agents) \xB7 \`widgets\` (keys, users and chat on their site) \xB7 \`ship\` (the whole procedure for
|
|
9792
|
+
their own app).
|
|
9769
9793
|
`;
|
|
9770
9794
|
}
|
|
9771
9795
|
function shipMarkdown() {
|
|
@@ -9930,7 +9954,7 @@ ONLY for what the scan could not find \u2014 one short list of questions, not an
|
|
|
9930
9954
|
of the job: move the call to a server route, delete the public variable, say so in the report.
|
|
9931
9955
|
- **Both** (monorepo): do the backend first, then the frontend.
|
|
9932
9956
|
|
|
9933
|
-
**Ask for what is missing
|
|
9957
|
+
**Ask only for what is missing.**
|
|
9934
9958
|
- Backend found only on localhost \u2192 "Where is it hosted for production?" (a URL, or "not yet").
|
|
9935
9959
|
- Backend protected by a secret whose value is in an env var \u2192 "What is the value of
|
|
9936
9960
|
\`<VAR>\`, or should I keep it where it is?" Never read secrets out of \`.env\` aloud.
|