sync: 2026-03-15 23:00 — 32 files from Alexandria
Some checks failed
Lint & Format / detect (push) Failing after 32s
Monorepo Lint / lint-shell (push) Failing after 31s
Monorepo Lint / lint-js (push) Failing after 30s
Lint & Format / js-lint (push) Has been skipped
Lint & Format / py-lint (push) Has been skipped
Lint & Format / sh-lint (push) Has been skipped
Lint & Format / go-lint (push) Has been skipped
Some checks failed
Lint & Format / detect (push) Failing after 32s
Monorepo Lint / lint-shell (push) Failing after 31s
Monorepo Lint / lint-js (push) Failing after 30s
Lint & Format / js-lint (push) Has been skipped
Lint & Format / py-lint (push) Has been skipped
Lint & Format / sh-lint (push) Has been skipped
Lint & Format / go-lint (push) Has been skipped
RoadChain-SHA2048: 692327ce2e990f37 RoadChain-Identity: alexa@sovereign RoadChain-Full: 692327ce2e990f37649b83e948241ac858c0d07146c6b42043e4770d638c44d5bada5639ad82c7aa8911d7042912c1d75b6bbce9a453637621b3903dc912a3a9537696cedf7a0870e3bf962ca44677793082aaae5c5433615885ad20fab1e80417202d11e93284483551ba9558f06809d2f3fa53c00a657277d7c183abe3ba187c1af6856a455071771757cca67ff2b74c5f855f23dd8cc8f5b3596c966b2344361fcbb74843e9d9d9ad66c5321ef64ce787f9d255d11e0d4e0ee571af4e09697964e22f6f629a11279b315c9a4563860b169ad93fa500b485297516ef2ba2039f76348c0d547cfa182e9b0bccee73f5e8b7db7e33d61e8199bb4464c2c30d03
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
// Own your customers, subscriptions, invoices, and payments.
|
||||
// Stripe is just a dumb card charger underneath.
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
const VERSION = '2.0.0';
|
||||
|
||||
// ─── Schema ──────────────────────────────────────────────────────────────
|
||||
const SCHEMA = [
|
||||
@@ -114,6 +114,37 @@ const SCHEMA = [
|
||||
`CREATE INDEX IF NOT EXISTS idx_payments_customer ON payments(customer_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_events_type ON events(type)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_events_customer ON events(customer_id)`,
|
||||
// v2: API keys for customers
|
||||
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
customer_id TEXT NOT NULL REFERENCES customers(id),
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
name TEXT DEFAULT 'default',
|
||||
scopes TEXT DEFAULT '["api:read"]',
|
||||
rate_limit INTEGER DEFAULT 1000,
|
||||
last_used TEXT,
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'active',
|
||||
expires_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)`,
|
||||
// v2: Usage metering
|
||||
`CREATE TABLE IF NOT EXISTS usage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_id TEXT NOT NULL REFERENCES customers(id),
|
||||
api_key_id TEXT REFERENCES api_keys(id),
|
||||
endpoint TEXT NOT NULL,
|
||||
method TEXT DEFAULT 'GET',
|
||||
status_code INTEGER,
|
||||
latency_ms INTEGER,
|
||||
tokens_used INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_api_keys_customer ON api_keys(customer_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_usage_customer ON usage(customer_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_usage_date ON usage(created_at)`,
|
||||
];
|
||||
|
||||
// ─── Seed Plans ──────────────────────────────────────────────────────────
|
||||
@@ -965,10 +996,129 @@ function handleHealth() {
|
||||
'GET /invoices?customer_id=',
|
||||
'GET /payments?customer_id=',
|
||||
'POST /webhook',
|
||||
'GET /keys?customer_id=',
|
||||
'POST /keys',
|
||||
'DELETE /keys?id=',
|
||||
'GET /usage?customer_id=&days=30',
|
||||
'POST /usage/record',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// ─── API Keys ───────────────────────────────────────────────────────────
|
||||
async function handleApiKeys(request, db) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method === 'GET') {
|
||||
const customerId = url.searchParams.get('customer_id');
|
||||
if (!customerId) return err('customer_id required');
|
||||
const keys = await db.prepare(
|
||||
"SELECT id, customer_id, key_prefix, name, scopes, rate_limit, last_used, usage_count, status, expires_at, created_at FROM api_keys WHERE customer_id = ? ORDER BY created_at DESC"
|
||||
).bind(customerId).all();
|
||||
return json({ keys: keys.results.map(k => ({ ...k, scopes: JSON.parse(k.scopes || '[]') })) });
|
||||
}
|
||||
|
||||
if (request.method === 'POST') {
|
||||
const body = await request.json();
|
||||
const { customer_id, name, scopes, rate_limit, expires_in_days } = body;
|
||||
if (!customer_id) return err('customer_id required');
|
||||
|
||||
// Generate API key: rp_live_ + 32 random hex chars
|
||||
const rawKey = `rp_live_${randHex(16)}`;
|
||||
const keyPrefix = rawKey.slice(0, 12);
|
||||
// Hash the key for storage (don't store raw)
|
||||
const enc = new TextEncoder();
|
||||
const hashBuf = await crypto.subtle.digest('SHA-256', enc.encode(rawKey));
|
||||
const keyHash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
const id = `key_${uid()}`;
|
||||
const expiresAt = expires_in_days
|
||||
? new Date(Date.now() + expires_in_days * 86400000).toISOString()
|
||||
: null;
|
||||
|
||||
await db.prepare(
|
||||
'INSERT INTO api_keys (id, customer_id, key_hash, key_prefix, name, scopes, rate_limit, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
).bind(id, customer_id, keyHash, keyPrefix, name || 'default', JSON.stringify(scopes || ['api:read']), rate_limit || 1000, expiresAt).run();
|
||||
|
||||
// Return the raw key ONLY on creation — can never be retrieved again
|
||||
return json({ id, key: rawKey, prefix: keyPrefix, name: name || 'default', warning: 'Save this key now. It cannot be retrieved again.' }, 201);
|
||||
}
|
||||
|
||||
if (request.method === 'DELETE') {
|
||||
const keyId = url.searchParams.get('id');
|
||||
if (!keyId) return err('id required');
|
||||
await db.prepare("UPDATE api_keys SET status = 'revoked' WHERE id = ?").bind(keyId).run();
|
||||
return json({ ok: true, revoked: keyId });
|
||||
}
|
||||
|
||||
return err('Method not allowed', 405);
|
||||
}
|
||||
|
||||
// ─── Usage / Metering ────────────────────────────────────────────────────
|
||||
async function handleUsage(request, db) {
|
||||
const url = new URL(request.url);
|
||||
const customerId = url.searchParams.get('customer_id');
|
||||
if (!customerId) return err('customer_id required');
|
||||
|
||||
const days = parseInt(url.searchParams.get('days') || '30');
|
||||
const since = new Date(Date.now() - days * 86400000).toISOString();
|
||||
|
||||
// Daily breakdown
|
||||
const daily = await db.prepare(
|
||||
`SELECT date(created_at) as day, COUNT(*) as requests, SUM(tokens_used) as tokens,
|
||||
AVG(latency_ms) as avg_latency, SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) as errors
|
||||
FROM usage WHERE customer_id = ? AND created_at >= ? GROUP BY day ORDER BY day DESC`
|
||||
).bind(customerId, since).all();
|
||||
|
||||
// Totals
|
||||
const totals = await db.prepare(
|
||||
`SELECT COUNT(*) as total_requests, SUM(tokens_used) as total_tokens,
|
||||
AVG(latency_ms) as avg_latency, COUNT(DISTINCT endpoint) as unique_endpoints
|
||||
FROM usage WHERE customer_id = ? AND created_at >= ?`
|
||||
).bind(customerId, since).first();
|
||||
|
||||
// Top endpoints
|
||||
const endpoints = await db.prepare(
|
||||
`SELECT endpoint, method, COUNT(*) as count, AVG(latency_ms) as avg_latency
|
||||
FROM usage WHERE customer_id = ? AND created_at >= ?
|
||||
GROUP BY endpoint, method ORDER BY count DESC LIMIT 10`
|
||||
).bind(customerId, since).all();
|
||||
|
||||
return json({
|
||||
customer_id: customerId,
|
||||
period: { days, since },
|
||||
totals,
|
||||
daily: daily.results,
|
||||
top_endpoints: endpoints.results,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Record Usage (internal — called by API gateway) ─────────────────────
|
||||
async function handleRecordUsage(request, db) {
|
||||
const body = await request.json();
|
||||
const { customer_id, api_key_id, endpoint, method, status_code, latency_ms, tokens_used } = body;
|
||||
if (!customer_id || !endpoint) return err('customer_id and endpoint required');
|
||||
|
||||
await db.prepare(
|
||||
'INSERT INTO usage (customer_id, api_key_id, endpoint, method, status_code, latency_ms, tokens_used) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).bind(customer_id, api_key_id || null, endpoint, method || 'GET', status_code || 200, latency_ms || 0, tokens_used || 0).run();
|
||||
|
||||
// Update API key usage count
|
||||
if (api_key_id) {
|
||||
await db.prepare(
|
||||
"UPDATE api_keys SET usage_count = usage_count + 1, last_used = datetime('now') WHERE id = ?"
|
||||
).bind(api_key_id).run();
|
||||
}
|
||||
|
||||
return json({ ok: true });
|
||||
}
|
||||
|
||||
function randHex(bytes) {
|
||||
const arr = new Uint8Array(bytes);
|
||||
crypto.getRandomValues(arr);
|
||||
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
// ─── Rate limiting ───────────────────────────────────────────────────────
|
||||
const rl = new Map();
|
||||
function rateLimit(ip, max = 30, windowSec = 60) {
|
||||
@@ -1040,6 +1190,15 @@ export default {
|
||||
case path === '/stats':
|
||||
response = await handleStats(db);
|
||||
break;
|
||||
case path === '/keys' || path === '/api-keys':
|
||||
response = await handleApiKeys(request, db);
|
||||
break;
|
||||
case path === '/usage':
|
||||
response = await handleUsage(request, db);
|
||||
break;
|
||||
case path === '/usage/record' && request.method === 'POST':
|
||||
response = await handleRecordUsage(request, db);
|
||||
break;
|
||||
default:
|
||||
response = err('Not found', 404);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user