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
JavaScript
/**
* 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 };