UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities

299 lines (287 loc) 10.2 kB
/** * AI Tool Spec generator for SPAPS * - Produces OpenAI-style function schemas for common SPAPS actions * - Keeps defaults safe for local development (no API key required) */ const { DEFAULT_PORT } = require('./config'); const fs = require('fs'); const path = require('path'); function tryLoadManifest() { const candidates = [ path.resolve(process.cwd(), 'docs/manifest.json'), path.resolve(__dirname, '../../../docs/manifest.json') ]; for (const p of candidates) { try { if (fs.existsSync(p)) { const raw = fs.readFileSync(p, 'utf8'); return JSON.parse(raw); } } catch {} } return null; } function tryLoadOpenAPI() { const candidates = [ path.resolve(process.cwd(), 'docs/api-reference.yaml'), path.resolve(__dirname, '../../../docs/api-reference.yaml') ]; for (const p of candidates) { try { if (fs.existsSync(p)) { const yaml = require('js-yaml'); const raw = fs.readFileSync(p, 'utf8'); return yaml.load(raw); } } catch {} } return null; } function buildOpenAIToolSpec({ port = DEFAULT_PORT } = {}) { const baseUrl = `http://localhost:${port}`; const spec = { name: 'spaps', version: '0.1.0', description: 'Auth + payments via SPAPS (local by default). Start with: npx spaps local', base_url: baseUrl, auth: { local_mode: true, production: { header: 'X-API-Key', env: 'SPAPS_API_KEY' } }, tools: [ { name: 'login', description: 'Login with email/password. Local mode accepts any values.', method: 'POST', path: '/api/auth/login', parameters: { type: 'object', required: ['email', 'password'], properties: { email: { type: 'string', description: 'Email address' }, password: { type: 'string', description: 'Plain text password' } } } }, { name: 'register', description: 'Register a new user with email/password.', method: 'POST', path: '/api/auth/register', parameters: { type: 'object', required: ['email', 'password'], properties: { email: { type: 'string' }, password: { type: 'string' } } } }, { name: 'get_current_user', description: 'Get the currently authenticated user. Uses bearer token from previous login.', method: 'GET', path: '/api/auth/user', parameters: { type: 'object', properties: { authorization: { type: 'string', description: 'Bearer <access_token>' } } } }, { name: 'create_checkout_session', description: 'Create a Stripe Checkout session. In local mode uses Stripe test or mock based on USE_REAL_STRIPE.', method: 'POST', path: '/api/stripe/checkout-sessions', parameters: { type: 'object', required: ['success_url', 'cancel_url'], properties: { price_id: { type: 'string', description: 'Existing Stripe price ID (preferred)' }, product_name: { type: 'string', description: 'Used when price_id not provided' }, amount: { type: 'number', description: 'Amount in cents if creating ad-hoc price' }, currency: { type: 'string', default: 'usd' }, success_url: { type: 'string' }, cancel_url: { type: 'string' } } } }, { name: 'list_products', description: 'List products (Stripe-backed or local).', method: 'GET', path: '/api/stripe/products', parameters: { type: 'object', properties: { active: { type: 'boolean' }, limit: { type: 'number' } } } }, { name: 'request_magic_link', description: 'Send a magic link for passwordless login (local mode simulates delivery).', method: 'POST', path: '/api/auth/magic-link', parameters: { type: 'object', required: ['email'], properties: { email: { type: 'string' } } } }, { name: 'get_wallet_nonce', description: 'Get a nonce to sign for wallet authentication.', method: 'POST', path: '/api/auth/nonce', parameters: { type: 'object', required: ['wallet_address'], properties: { wallet_address: { type: 'string' }, chain_type: { type: 'string', enum: ['solana', 'ethereum', 'bitcoin', 'base'] } } } } ] }; // Default error shapes used for enrichment/merging const defaultErrors = { '400': { type: 'object', properties: { success: { type: 'boolean' }, error: { type: 'object', properties: { code: { type: 'string' }, message: { type: 'string' } }, required: ['message'] } }, required: ['error'] }, '401': { type: 'object', properties: { error: { type: 'string', enum: ['unauthorized'] }, message: { type: 'string' } }, required: ['error'] }, '403': { type: 'object', properties: { error: { type: 'string', enum: ['forbidden'] }, message: { type: 'string' } }, required: ['error'] }, '429': { type: 'object', properties: { error: { type: 'string', enum: ['rate_limited'] }, message: { type: 'string' } }, required: ['error'] }, '500': { type: 'object', properties: { error: { type: 'string', enum: ['server_error'] }, message: { type: 'string' } }, required: ['error'] } }; // Attempt to align paths/methods with docs/manifest.json if available try { const manifest = tryLoadManifest(); if (manifest && Array.isArray(manifest.endpoints)) { const find = (method, pathStr) => manifest.endpoints.find(e => e.method === method && e.path === pathStr); const patchTool = (toolName, method, pathStr) => { const t = spec.tools.find(x => x.name === toolName); const ep = find(method, pathStr); if (t && ep) { t.method = ep.method; t.path = ep.path; } }; patchTool('login', 'POST', '/api/auth/login'); patchTool('register', 'POST', '/api/auth/register'); patchTool('get_current_user', 'GET', '/api/auth/user'); patchTool('create_checkout_session', 'POST', '/api/stripe/checkout-sessions'); patchTool('list_products', 'GET', '/api/stripe/products'); patchTool('request_magic_link', 'POST', '/api/auth/magic-link'); patchTool('get_wallet_nonce', 'POST', '/api/auth/nonce'); } } catch { // Best-effort alignment only } // Attempt to enrich parameter schemas from OpenAPI try { const openapi = tryLoadOpenAPI(); if (openapi && openapi.paths) { const findOp = (method, pathStr) => { const ops = openapi.paths[pathStr]; if (!ops) return null; return ops[String(method).toLowerCase()] || null; }; const setBodySchema = (toolName, method, pathStr) => { const t = spec.tools.find(x => x.name === toolName); const op = findOp(method, pathStr); const schema = op?.requestBody?.content?.['application/json']?.schema; if (t && schema) { t.parameters = schema; } }; const setResponses = (toolName, method, pathStr) => { const t = spec.tools.find(x => x.name === toolName); const op = findOp(method, pathStr); if (t && op && op.responses) { const responses = {}; const examples = {}; for (const [code, obj] of Object.entries(op.responses)) { const schema = obj?.content?.['application/json']?.schema; if (schema) responses[code] = schema; const content = obj?.content?.['application/json']; if (content?.example !== undefined) { examples[code] = content.example; } else if (content?.examples && typeof content.examples === 'object') { const ex = {}; for (const [name, val] of Object.entries(content.examples)) { if (val && typeof val === 'object') { if ('value' in val) ex[name] = val.value; } } if (Object.keys(ex).length) examples[code] = ex; } } if (Object.keys(responses).length) { // Merge with default errors for completeness const merged = { ...defaultErrors, ...responses }; t.responses = merged; } if (Object.keys(examples).length) t.examples = examples; } }; setBodySchema('login', 'POST', '/api/auth/login'); setBodySchema('register', 'POST', '/api/auth/register'); setBodySchema('create_checkout_session', 'POST', '/api/stripe/checkout-sessions'); setBodySchema('request_magic_link', 'POST', '/api/auth/magic-link'); setBodySchema('get_wallet_nonce', 'POST', '/api/auth/nonce'); setResponses('login', 'POST', '/api/auth/login'); setResponses('register', 'POST', '/api/auth/register'); setResponses('get_current_user', 'GET', '/api/auth/user'); setResponses('create_checkout_session', 'POST', '/api/stripe/checkout-sessions'); setResponses('list_products', 'GET', '/api/stripe/products'); setResponses('request_magic_link', 'POST', '/api/auth/magic-link'); setResponses('get_wallet_nonce', 'POST', '/api/auth/nonce'); // For GET endpoints with query/headers, leave minimal schema for simplicity } } catch { // Ignore enrichment errors } // Add default error shapes if responses missing spec.tools.forEach(t => { if (!t.responses) t.responses = defaultErrors; }); return spec; } function buildToolSpec({ format = 'openai', port = DEFAULT_PORT } = {}) { switch (format) { case 'openai': default: return buildOpenAIToolSpec({ port }); } } module.exports = { buildToolSpec };