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
JavaScript
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,
};