apiblaze 0.21.37 → 0.21.39
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 +160 -27
- 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.39";
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -6457,8 +6457,99 @@ var init_files = __esm({
|
|
|
6457
6457
|
"use strict";
|
|
6458
6458
|
DEMO_FILES = {
|
|
6459
6459
|
"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",
|
|
6460
|
-
"openapi.yaml":
|
|
6461
|
-
|
|
6460
|
+
"openapi.yaml": `openapi: 3.0.3
|
|
6461
|
+
info:
|
|
6462
|
+
title: ResiResi API
|
|
6463
|
+
version: 1.0.0
|
|
6464
|
+
description: Table bookings at restaurants (each restaurant is a tenant, like Nino Pizza and Gino Pizza). APIblaze reads this file to build the MCP tools for AI assistants and to work out who may change what.
|
|
6465
|
+
paths:
|
|
6466
|
+
/reservations:
|
|
6467
|
+
get:
|
|
6468
|
+
operationId: listReservations
|
|
6469
|
+
summary: List my bookings
|
|
6470
|
+
description: The signed-in person's own bookings at this restaurant, and nobody else's. Use it for "my bookings" or "the one I just made", and call it again whenever asked, since bookings change.
|
|
6471
|
+
responses:
|
|
6472
|
+
'200':
|
|
6473
|
+
description: The caller's bookings
|
|
6474
|
+
content:
|
|
6475
|
+
application/json:
|
|
6476
|
+
schema:
|
|
6477
|
+
type: array
|
|
6478
|
+
items: { $ref: '#/components/schemas/Reservation' }
|
|
6479
|
+
post:
|
|
6480
|
+
operationId: createReservation
|
|
6481
|
+
summary: Book a table
|
|
6482
|
+
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.
|
|
6483
|
+
requestBody:
|
|
6484
|
+
required: true
|
|
6485
|
+
content:
|
|
6486
|
+
application/json:
|
|
6487
|
+
schema: { $ref: '#/components/schemas/NewReservation' }
|
|
6488
|
+
responses:
|
|
6489
|
+
'201':
|
|
6490
|
+
description: Booked
|
|
6491
|
+
content:
|
|
6492
|
+
application/json:
|
|
6493
|
+
schema: { $ref: '#/components/schemas/Reservation' }
|
|
6494
|
+
/reservations/{id}:
|
|
6495
|
+
parameters:
|
|
6496
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
6497
|
+
get:
|
|
6498
|
+
operationId: getReservation
|
|
6499
|
+
summary: Get one booking
|
|
6500
|
+
description: Only the person who booked it (or the restaurant's admin) can see it. Anyone else is refused.
|
|
6501
|
+
responses:
|
|
6502
|
+
'200':
|
|
6503
|
+
description: The booking
|
|
6504
|
+
content:
|
|
6505
|
+
application/json:
|
|
6506
|
+
schema: { $ref: '#/components/schemas/Reservation' }
|
|
6507
|
+
patch:
|
|
6508
|
+
operationId: updateReservation
|
|
6509
|
+
summary: Change a booking
|
|
6510
|
+
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.
|
|
6511
|
+
requestBody:
|
|
6512
|
+
required: true
|
|
6513
|
+
content:
|
|
6514
|
+
application/json:
|
|
6515
|
+
schema: { $ref: '#/components/schemas/ReservationChange' }
|
|
6516
|
+
responses:
|
|
6517
|
+
'200':
|
|
6518
|
+
description: Changed
|
|
6519
|
+
content:
|
|
6520
|
+
application/json:
|
|
6521
|
+
schema: { $ref: '#/components/schemas/Reservation' }
|
|
6522
|
+
'409': { description: That table is already booked at that time }
|
|
6523
|
+
delete:
|
|
6524
|
+
operationId: cancelReservation
|
|
6525
|
+
summary: Cancel a booking
|
|
6526
|
+
description: Only the person who booked it (or the restaurant's admin) can cancel it.
|
|
6527
|
+
responses:
|
|
6528
|
+
'204': { description: Cancelled }
|
|
6529
|
+
components:
|
|
6530
|
+
schemas:
|
|
6531
|
+
NewReservation:
|
|
6532
|
+
type: object
|
|
6533
|
+
required: [time]
|
|
6534
|
+
properties:
|
|
6535
|
+
name: { type: string, example: Ana, description: 'Optional: defaults to the signed-in person. Do not ask for it.' }
|
|
6536
|
+
table: { type: integer, example: 4, description: 'Optional: the restaurant picks a free table. Do not ask for it.' }
|
|
6537
|
+
time: { type: string, example: '19:30', description: 'Time of the booking, 24h HH:MM' }
|
|
6538
|
+
guests: { type: integer, example: 2, description: 'How many people. Optional, 2 if not given.' }
|
|
6539
|
+
ReservationChange:
|
|
6540
|
+
type: object
|
|
6541
|
+
properties:
|
|
6542
|
+
table: { type: integer, example: 2, description: 'Move to this table.' }
|
|
6543
|
+
time: { type: string, example: '20:00', description: 'New time, 24h HH:MM' }
|
|
6544
|
+
guests: { type: integer, example: 4, description: 'New number of people' }
|
|
6545
|
+
Reservation:
|
|
6546
|
+
allOf:
|
|
6547
|
+
- $ref: '#/components/schemas/NewReservation'
|
|
6548
|
+
- type: object
|
|
6549
|
+
properties:
|
|
6550
|
+
id: { type: string, example: r1 }
|
|
6551
|
+
`,
|
|
6552
|
+
"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 time = input.time ? String(input.time) : row.time;\n const table = input.table ? Number(input.table) : (time === row.time ? row.table : freeTable(row.tenant, time));\n const clash = [...bookings.values()].find((r) => r.id !== row.id && r.tenant === row.tenant && r.time === time && r.table === table);\n if (clash) return reply(409, { error: `table ${table} is already booked at ${time}` });\n Object.assign(row, { 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",
|
|
6462
6553
|
"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",
|
|
6463
6554
|
"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",
|
|
6464
6555
|
"public/index.html": `<!doctype html>
|
|
@@ -8612,7 +8703,7 @@ async function journey(label3, work, home, checks) {
|
|
|
8612
8703
|
}
|
|
8613
8704
|
async function runOpAnon(o = {}) {
|
|
8614
8705
|
const t0 = Date.now();
|
|
8615
|
-
const root = import_fs10.default.mkdtempSync(import_path7.default.join(
|
|
8706
|
+
const root = import_fs10.default.mkdtempSync(import_path7.default.join(import_os3.default.tmpdir(), `${B.cli}-op-anon-`));
|
|
8616
8707
|
const home = import_path7.default.join(root, "home");
|
|
8617
8708
|
const work = import_path7.default.join(root, "work");
|
|
8618
8709
|
import_fs10.default.mkdirSync(home);
|
|
@@ -8654,12 +8745,12 @@ async function runOpAnon(o = {}) {
|
|
|
8654
8745
|
if (failed) process.exitCode = 1;
|
|
8655
8746
|
return { ok: !failed, checks };
|
|
8656
8747
|
}
|
|
8657
|
-
var import_fs10,
|
|
8748
|
+
var import_fs10, import_os3, import_path7, import_child_process5, import_chalk70, SECRET;
|
|
8658
8749
|
var init_op_anon = __esm({
|
|
8659
8750
|
"src/commands/op-anon.ts"() {
|
|
8660
8751
|
"use strict";
|
|
8661
8752
|
import_fs10 = __toESM(require("fs"));
|
|
8662
|
-
|
|
8753
|
+
import_os3 = __toESM(require("os"));
|
|
8663
8754
|
import_path7 = __toESM(require("path"));
|
|
8664
8755
|
import_child_process5 = require("child_process");
|
|
8665
8756
|
import_chalk70 = __toESM(require("chalk"));
|
|
@@ -9189,6 +9280,7 @@ init_create();
|
|
|
9189
9280
|
|
|
9190
9281
|
// src/commands/skill.ts
|
|
9191
9282
|
var import_fs7 = __toESM(require("fs"));
|
|
9283
|
+
var import_os2 = __toESM(require("os"));
|
|
9192
9284
|
var import_path6 = __toESM(require("path"));
|
|
9193
9285
|
var import_chalk25 = __toESM(require("chalk"));
|
|
9194
9286
|
init_brand();
|
|
@@ -9511,7 +9603,32 @@ function folderReport(f, opts = {}) {
|
|
|
9511
9603
|
ownApi: true
|
|
9512
9604
|
};
|
|
9513
9605
|
}
|
|
9514
|
-
function
|
|
9606
|
+
function blockedMarkdown() {
|
|
9607
|
+
return `## If a command is blocked or fails
|
|
9608
|
+
|
|
9609
|
+
A permission prompt declined, a command denied, or a step needing access you do not have:
|
|
9610
|
+
- **A step the rest depends on** (the tunnel, creating the proxy, its key): stop there. In one
|
|
9611
|
+
line, say what it was for and give the user the exact command to run themselves
|
|
9612
|
+
(\`! <command>\` in Claude Code). Then wait. If they would rather not, say it stops here.
|
|
9613
|
+
- **An optional step** (groups, admins, widgets, a rule, limits): skip it. Say in one line what
|
|
9614
|
+
is missing because of it, and carry on with what does not need it.
|
|
9615
|
+
- **Always:** never retry it in pieces, take another route, or write code to replace it. Never
|
|
9616
|
+
say something works until you have checked it: a call that answered, or a page you loaded.
|
|
9617
|
+
`;
|
|
9618
|
+
}
|
|
9619
|
+
function staleSkillCopies(cwd = process.cwd(), home = import_os2.default.homedir()) {
|
|
9620
|
+
const current = skillMarkdown();
|
|
9621
|
+
const out = [];
|
|
9622
|
+
for (const dir of /* @__PURE__ */ new Set([cwd, home])) {
|
|
9623
|
+
const f = import_path6.default.join(dir, SKILL_RELATIVE_PATH);
|
|
9624
|
+
try {
|
|
9625
|
+
if (import_fs7.default.readFileSync(f, "utf8") !== current) out.push(f);
|
|
9626
|
+
} catch {
|
|
9627
|
+
}
|
|
9628
|
+
}
|
|
9629
|
+
return out;
|
|
9630
|
+
}
|
|
9631
|
+
function heroMarkdown(found, stale = []) {
|
|
9515
9632
|
const P = B.product;
|
|
9516
9633
|
const run2 = `npx ${B.cli}@latest`;
|
|
9517
9634
|
const report = found ? folderReport(found) : null;
|
|
@@ -9553,6 +9670,15 @@ description: Use when the user asks what ${P} (or ${B.cli}) does or can do for t
|
|
|
9553
9670
|
|
|
9554
9671
|
# ${P}
|
|
9555
9672
|
|
|
9673
|
+
This output is current: it replaces any copy of this skill loaded earlier.
|
|
9674
|
+
${stale.length ? `
|
|
9675
|
+
## Outdated copies are installed: ignore them
|
|
9676
|
+
|
|
9677
|
+
${stale.map((f) => `- \`${f}\``).join("\n")}
|
|
9678
|
+
|
|
9679
|
+
They hold an older walkthrough. Follow only what this output says. Tell the user once that the
|
|
9680
|
+
file is outdated (re-run \`${run2} skills --install --force\` in that folder to refresh it).
|
|
9681
|
+
` : ""}
|
|
9556
9682
|
${folder}
|
|
9557
9683
|
## Explain: they ask what ${P} does or can do for them
|
|
9558
9684
|
|
|
@@ -9579,6 +9705,7 @@ Rules for the demo: keep each update to one line. Do not narrate steps, read the
|
|
|
9579
9705
|
files, open logs, run other commands, or write code. Never show a key: the demo saves it
|
|
9580
9706
|
in \`.env.local\` and never prints it.
|
|
9581
9707
|
|
|
9708
|
+
${blockedMarkdown()}
|
|
9582
9709
|
## Their own app: they say yes after the demo
|
|
9583
9710
|
|
|
9584
9711
|
Their app runs on this machine \u2192 it will use \`${run2} dev\`; it is deployed \u2192 \`${run2} create\`.
|
|
@@ -9611,7 +9738,8 @@ No code yet? Write the smallest plain backend that does exactly that, plus its \
|
|
|
9611
9738
|
`;
|
|
9612
9739
|
return withLatest(`# Put ${B.product} in front of their app
|
|
9613
9740
|
|
|
9614
|
-
${backendRules}${
|
|
9741
|
+
${backendRules}${blockedMarkdown()}
|
|
9742
|
+
${at >= 0 ? legacy.slice(at) : legacy}`);
|
|
9615
9743
|
}
|
|
9616
9744
|
function skillMarkdown() {
|
|
9617
9745
|
return `${heroMarkdown()}
|
|
@@ -10078,7 +10206,7 @@ async function runSkill(opts = {}) {
|
|
|
10078
10206
|
}
|
|
10079
10207
|
const found = await detectBackend(process.cwd());
|
|
10080
10208
|
if (readByAgent(opts)) {
|
|
10081
|
-
process.stdout.write(heroMarkdown(found));
|
|
10209
|
+
process.stdout.write(heroMarkdown(found, staleSkillCopies()));
|
|
10082
10210
|
return;
|
|
10083
10211
|
}
|
|
10084
10212
|
await runForPerson(found);
|
|
@@ -11828,7 +11956,7 @@ function printAuth(recipe) {
|
|
|
11828
11956
|
console.log(` ${import_chalk42.default.dim("provider ")} ${a.provider.type ?? "(unknown)"}`);
|
|
11829
11957
|
if (a.provider.scopes) console.log(` ${import_chalk42.default.dim("scopes ")} ${a.provider.scopes}`);
|
|
11830
11958
|
if (a.provider.target_server_token) console.log(` ${import_chalk42.default.dim("sends to API")} ${a.provider.target_server_token}`);
|
|
11831
|
-
console.log(import_chalk42.default.dim(" (no credentials travel in a recipe \u2014 install asks
|
|
11959
|
+
console.log(a.provider.managed === true ? import_chalk42.default.dim(" (APIblaze's own GitHub app \u2014 installers get the same zero-config door, nothing to create)") : import_chalk42.default.dim(" (no credentials travel in a recipe \u2014 install asks for your own OAuth app)"));
|
|
11832
11960
|
} else {
|
|
11833
11961
|
console.log(import_chalk42.default.dim(" no login provider \u2014 consumers call it with an API key"));
|
|
11834
11962
|
}
|
|
@@ -11915,7 +12043,7 @@ async function runRecipeInstall(nameArg, opts) {
|
|
|
11915
12043
|
if (reuseTenant) {
|
|
11916
12044
|
recipe = { ...recipe, auth: { app_client: null, provider: null, key_types: [] }, questions: [] };
|
|
11917
12045
|
}
|
|
11918
|
-
const declined = assumeYes ? declineAll(recipe.requires_confirmation ?? []) : await askConsent(recipe.requires_confirmation ?? [], interactive2);
|
|
12046
|
+
const declined = assumeYes ? declineAll(recipe.requires_confirmation ?? []) : await askConsent(recipe.requires_confirmation ?? [], interactive2, !!opts.into);
|
|
11919
12047
|
const answers = await askQuestions(recipe.questions ?? [], ctx, interactive2);
|
|
11920
12048
|
const started = Date.now();
|
|
11921
12049
|
const steps = buildSteps({ recipe, caller, teamId, chosenTeamId, teamName, ctx, answers, declined, apiVersion, reinstall, pinned });
|
|
@@ -12052,16 +12180,14 @@ function buildSteps(inp) {
|
|
|
12052
12180
|
skip: !filled.auth.provider,
|
|
12053
12181
|
run: async () => {
|
|
12054
12182
|
const p = filled.auth.provider;
|
|
12055
|
-
const
|
|
12056
|
-
const
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
path: `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/app-clients/${encodeURIComponent(appClientId)}/providers`,
|
|
12064
|
-
body: {
|
|
12183
|
+
const managed = p.managed === true;
|
|
12184
|
+
const body = managed ? { type: p.type, managed: true } : (() => {
|
|
12185
|
+
const clientId = answers.oauth_client_id;
|
|
12186
|
+
const clientSecret = answers.oauth_client_secret;
|
|
12187
|
+
if (!clientId || !clientSecret) {
|
|
12188
|
+
throw new Error("the OAuth client id and secret were not supplied, so the login provider cannot be created");
|
|
12189
|
+
}
|
|
12190
|
+
return {
|
|
12065
12191
|
type: p.type,
|
|
12066
12192
|
clientId,
|
|
12067
12193
|
clientSecret,
|
|
@@ -12070,10 +12196,16 @@ function buildSteps(inp) {
|
|
|
12070
12196
|
targetServerToken: p.target_server_token,
|
|
12071
12197
|
includeApiblazeAccessTokenHeader: p.include_apiblaze_access_token_header === true,
|
|
12072
12198
|
includeApiblazeIdTokenHeader: p.include_apiblaze_id_token_header === true
|
|
12073
|
-
}
|
|
12074
|
-
|
|
12199
|
+
};
|
|
12200
|
+
})();
|
|
12201
|
+
const appClientId = await defaultAppClientId(caller, teamId, tenant2);
|
|
12202
|
+
await producer(caller, {
|
|
12203
|
+
method: "POST",
|
|
12204
|
+
path: `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(tenant2)}/app-clients/${encodeURIComponent(appClientId)}/providers`,
|
|
12205
|
+
body,
|
|
12206
|
+
summary: managed ? `Set up APIblaze's managed GitHub login on ${tenant2}` : `Create the ${String(p.type)} login provider on ${tenant2}`
|
|
12075
12207
|
});
|
|
12076
|
-
return String(p.type);
|
|
12208
|
+
return managed ? "APIblaze-hosted GitHub (no setup needed)" : String(p.type);
|
|
12077
12209
|
}
|
|
12078
12210
|
},
|
|
12079
12211
|
{
|
|
@@ -12167,7 +12299,8 @@ function buildSteps(inp) {
|
|
|
12167
12299
|
function providerLabel(provider) {
|
|
12168
12300
|
if (!provider) return "login provider";
|
|
12169
12301
|
const t = String(provider.type ?? "oauth");
|
|
12170
|
-
|
|
12302
|
+
const pretty = `${t.charAt(0).toUpperCase()}${t.slice(1)}`;
|
|
12303
|
+
return provider.managed === true ? `${pretty} sign-in (APIblaze's own app \u2014 no setup)` : `${pretty} provider`;
|
|
12171
12304
|
}
|
|
12172
12305
|
function transformsLabel(rules, tables) {
|
|
12173
12306
|
const parts = [];
|
|
@@ -12342,13 +12475,13 @@ async function confirmYesNo(message, interactive2) {
|
|
|
12342
12475
|
const { ok } = await inquirer3.prompt([{ type: "confirm", name: "ok", message, default: false }]);
|
|
12343
12476
|
return !!ok;
|
|
12344
12477
|
}
|
|
12345
|
-
async function askConsent(items, interactive2) {
|
|
12478
|
+
async function askConsent(items, interactive2, reinstalling) {
|
|
12346
12479
|
const declined = /* @__PURE__ */ new Set();
|
|
12347
12480
|
if (!items.length) return declined;
|
|
12348
12481
|
const count = items.length;
|
|
12349
12482
|
const word = ["", "it", "both", "all three", "all four", "all five", "all six"][count] ?? `all ${count}`;
|
|
12350
12483
|
console.log();
|
|
12351
|
-
console.log(import_chalk43.default.yellow("\u26A0 This recipe changes who can reach your proxy, what it trusts, and what it can cost."));
|
|
12484
|
+
console.log(import_chalk43.default.yellow(reinstalling ? "\u26A0 This recipe changes who can reach your proxy, what it trusts, and what it can cost." : "\u26A0 The proxy this creates will be reachable by others, trust an upstream host, and can cost you money:"));
|
|
12352
12485
|
console.log();
|
|
12353
12486
|
for (const item of items) {
|
|
12354
12487
|
console.log(` ${import_chalk43.default.bold(item.kind.padEnd(16))} ${item.value}`);
|