UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware

274 lines (242 loc) 6.92 kB
const axios = require('axios'); const { DEFAULT_PORT } = require('./config'); const REQUEST_TIMEOUT_MS = 1200; function buildBaseUrl(port = DEFAULT_PORT) { return `http://localhost:${port}`; } function buildDocsUrl(port = DEFAULT_PORT) { return `${buildBaseUrl(port)}/docs`; } function normalizeServerUrl(url) { return String(url || '').trim().replace(/\/+$/, ''); } function isLocalhostUrl(url) { try { const parsed = new URL(url); return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'; } catch { return false; } } function unwrapEnvelope(payload) { if ( payload && typeof payload === 'object' && payload.success === true && Object.prototype.hasOwnProperty.call(payload, 'data') ) { return payload.data; } return payload; } function extractError(payload, status) { if (payload && typeof payload === 'object') { if (payload.error && typeof payload.error === 'object') { return { code: payload.error.code || `HTTP_${status}`, message: payload.error.message || `Request failed with status ${status}`, }; } return { code: payload.code || payload.error || `HTTP_${status}`, message: payload.message || payload.detail || `Request failed with status ${status}`, }; } return { code: `HTTP_${status}`, message: `Request failed with status ${status}`, }; } async function requestJson({ method, url, data = null, headers = {}, timeoutMs = REQUEST_TIMEOUT_MS }) { try { const response = await axios({ method, url, data, headers, timeout: timeoutMs, validateStatus: () => true, }); const ok = response.status >= 200 && response.status < 300; return { ok, status: response.status, raw: response.data, data: unwrapEnvelope(response.data), error: ok ? null : extractError(response.data, response.status), network_error: false, }; } catch (error) { return { ok: false, status: null, raw: null, data: null, error: { code: error.code || 'REQUEST_FAILED', message: error.message || 'Request failed', }, network_error: true, }; } } async function getServerRuntime({ port = DEFAULT_PORT, serverUrl = null, timeoutMs = REQUEST_TIMEOUT_MS } = {}) { const url = serverUrl ? normalizeServerUrl(serverUrl) : buildBaseUrl(port); const docs = `${url}/docs`; const health = await requestJson({ method: 'GET', url: `${url}/health`, timeoutMs, }); if (!health.ok) { return { running: false, port, url, docs, message: health.network_error ? 'Server not running' : 'Health endpoint returned an error', start_command: isLocalhostUrl(url) ? `npx spaps local --port ${port}` : null, error: health.error, }; } const localMode = await requestJson({ method: 'GET', url: `${url}/health/local-mode`, timeoutMs, }); const localModeData = localMode.ok && localMode.data && typeof localMode.data === 'object' ? localMode.data : null; return { running: true, port, url, docs, health: health.data || health.raw, local_mode: { known: localMode.ok, active: localModeData ? Boolean(localModeData.local_mode_active) : null, environment: localModeData?.environment || null, spaps_local_mode_env: typeof localModeData?.spaps_local_mode_env === 'boolean' ? localModeData.spaps_local_mode_env : null, test_users: Array.isArray(localModeData?.test_users) ? localModeData.test_users : [], test_application: localModeData?.test_application || null, hints: localModeData?.hints || {}, raw: localModeData, }, }; } function readSelfServicePassword() { return process.env.SELF_SERVICE_PASSWORD || process.env.SELF_SERVICE_ADMIN_PASSWORD || ''; } async function provisionStarterApplication({ port = DEFAULT_PORT, name, slug, blueprintKey, allowedOrigins = [], }) { const runtime = await getServerRuntime({ port }); if (!runtime.running) { return { status: 'scaffold_only', reason: 'server_unreachable', runtime, warnings: [ `Local server was unreachable at ${runtime.url}. Starter files were created without provisioning.`, ], }; } if (runtime.local_mode.active) { return { status: 'local_mode', reason: null, runtime, application: { id: runtime.local_mode.test_application?.id || null, slug: runtime.local_mode.test_application?.slug || slug, }, warnings: [ `Local mode is active on ${runtime.url}. Starter files were created without provisioning because API key validation is bypassed.`, ], }; } const password = readSelfServicePassword(); if (!password) { return { status: 'scaffold_only', reason: 'self_service_password_missing', runtime, warnings: [ `The server at ${runtime.url} requires a provisioned application key. Set SELF_SERVICE_PASSWORD and re-run this command to provision one automatically.`, ], }; } const auth = await requestJson({ method: 'POST', url: `${runtime.url}/api/self-service/auth`, data: { password }, }); if (!auth.ok || !auth.data?.token) { return { status: 'scaffold_only', reason: 'self_service_auth_failed', runtime, warnings: [ `Self-service authentication failed: ${auth.error?.message || 'no token returned'}. Starter files were created without provisioning.`, ], error: auth.error, }; } const creation = await requestJson({ method: 'POST', url: `${runtime.url}/api/self-service/applications`, headers: { Authorization: `Bearer ${auth.data.token}`, }, data: { name, slug, blueprint_key: blueprintKey, allowed_origins: allowedOrigins, }, }); if (!creation.ok || !creation.data?.application) { return { status: 'scaffold_only', reason: 'application_provision_failed', runtime, warnings: [ `Application provisioning failed: ${creation.error?.message || 'unexpected response'}. Starter files were created without provisioning.`, ], error: creation.error, }; } return { status: 'provisioned', reason: null, runtime, application: { id: creation.data.application.id, slug: creation.data.application.slug, }, keys: { publishable: creation.data.publishable_key || null, secret: creation.data.secret_key || creation.data.api_key || null, }, warnings: [], }; } module.exports = { buildBaseUrl, buildDocsUrl, getServerRuntime, isLocalhostUrl, normalizeServerUrl, provisionStarterApplication, readSelfServicePassword, requestJson, unwrapEnvelope, };