From 86c3d591b043a8169aebbe2f5b91c87862b84c7e Mon Sep 17 00:00:00 2001 From: "nekomimi.pet" Date: Wed, 21 Jan 2026 23:55:37 -0500 Subject: [PATCH] api docs --- apps/main-app/src/routes/admin.ts | 50 ++- apps/main-app/src/routes/auth.ts | 25 ++ apps/main-app/src/routes/domain.ts | 45 ++- apps/main-app/src/routes/site.ts | 15 + apps/main-app/src/routes/user.ts | 24 ++ apps/main-app/src/routes/wisp.ts | 12 + docs/astro.config.mjs | 4 + .../content/docs/reference/main-app-api.md | 287 ++++++++++++++++++ 8 files changed, 460 insertions(+), 2 deletions(-) create mode 100644 docs/src/content/docs/reference/main-app-api.md diff --git a/apps/main-app/src/routes/admin.ts b/apps/main-app/src/routes/admin.ts index 1c4d6b8..1d7b6de 100644 --- a/apps/main-app/src/routes/admin.ts +++ b/apps/main-app/src/routes/admin.ts @@ -13,6 +13,11 @@ export const adminRoutes = (cookieSecret: string) => } }) // Login + /** + * POST /api/admin/login + * Success: { success: true } with admin_session cookie set. + * Failure (401): { error: 'Invalid credentials' } + */ .post( '/login', async ({ body, cookie, set }) => { @@ -52,6 +57,10 @@ export const adminRoutes = (cookieSecret: string) => ) // Logout + /** + * POST /api/admin/logout + * Success: { success: true } and clears admin_session cookie. + */ .post('/logout', ({ cookie }) => { const sessionId = cookie.admin_session?.value if (sessionId && typeof sessionId === 'string') { @@ -69,6 +78,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Check auth status + /** + * GET /api/admin/status + * Authenticated: { authenticated: true, username } + * Not authenticated: { authenticated: false } + */ .get('/status', ({ cookie }) => { const sessionId = cookie.admin_session?.value if (!sessionId || typeof sessionId !== 'string') { @@ -94,6 +108,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get logs (protected) + /** + * GET /api/admin/logs + * Success: { logs } + * Unauthorized (401): { error: 'Unauthorized' } + */ .get('/logs', async ({ query, cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -145,6 +164,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get errors (protected) + /** + * GET /api/admin/errors + * Success: { errors } + * Unauthorized (401): { error: 'Unauthorized' } + */ .get('/errors', async ({ query, cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -190,6 +214,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get metrics (protected) + /** + * GET /api/admin/metrics + * Success: { overall, mainApp, hostingService, timeWindow } + * Unauthorized (401): { error: 'Unauthorized' } + */ .get('/metrics', async ({ query, cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -239,6 +268,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get database stats (protected) + /** + * GET /api/admin/database + * Success: { stats, recentSites, recentDomains } + * Failure (500): { error, message } + */ .get('/database', async ({ cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -292,6 +326,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get cache stats (protected) + /** + * GET /api/admin/cache + * Success: hosting service cache stats payload. + * Failure (503|500): { error, message } + */ .get('/cache', async ({ cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -327,6 +366,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get sites listing (protected) + /** + * GET /api/admin/sites + * Success: { sites, customDomains } + * Failure (500): { error, message } + */ .get('/sites', async ({ query, cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -381,6 +425,11 @@ export const adminRoutes = (cookieSecret: string) => }) // Get system health (protected) + /** + * GET /api/admin/health + * Success: { uptime, memory, timestamp } + * Unauthorized (401): { error: 'Unauthorized' } + */ .get('/health', ({ cookie, set }) => { const check = requireAdmin({ cookie, set }) if (check) return check @@ -405,4 +454,3 @@ export const adminRoutes = (cookieSecret: string) => sign: ['admin_session'] }) }) - diff --git a/apps/main-app/src/routes/auth.ts b/apps/main-app/src/routes/auth.ts index c3ab04d..1991700 100644 --- a/apps/main-app/src/routes/auth.ts +++ b/apps/main-app/src/routes/auth.ts @@ -13,6 +13,11 @@ export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new sign: ['did'] } }) + /** + * GET /api/auth/login + * 302 redirect to the AT Protocol OAuth authorize URL. + * On error, redirects to /?error=missing_handle or /?error=auth_failed. + */ .get('/api/auth/login', async (c) => { // GET endpoint for initiating OAuth via atproto.wisp.place entryway // Accepts: login_hint (handle) or pds (server) @@ -42,6 +47,11 @@ export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new return c.redirect('/?error=auth_failed') } }) + /** + * POST /api/auth/signin + * Success: { url } where url is the OAuth authorize URL. + * Failure: { error, details }. + */ .post('/api/auth/signin', async (c) => { let handle = 'unknown' try { @@ -58,6 +68,11 @@ export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new return { error: 'Authentication failed', details: err instanceof Error ? err.message : String(err) } } }) + /** + * GET /api/auth/callback + * 302 redirect to /onboarding (new users) or /editor (existing users). + * On error, redirects to /?error=auth_failed. + */ .get('/api/auth/callback', async (c) => { try { const params = new URLSearchParams(c.query) @@ -111,6 +126,11 @@ export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new return c.redirect('/?error=auth_failed') } }) + /** + * POST /api/auth/logout + * Success: { success: true } + * Failure: { error: 'Logout failed' } + */ .post('/api/auth/logout', async (c) => { try { const cookieSession = c.cookie @@ -136,6 +156,11 @@ export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new return { error: 'Logout failed' } } }) + /** + * GET /api/auth/status + * Authenticated: { authenticated: true, did } + * Not authenticated: { authenticated: false } + */ .get('/api/auth/status', async (c) => { try { const auth = await authenticateRequest(client, c.cookie) diff --git a/apps/main-app/src/routes/domain.ts b/apps/main-app/src/routes/domain.ts index 447f5e2..729befe 100644 --- a/apps/main-app/src/routes/domain.ts +++ b/apps/main-app/src/routes/domain.ts @@ -35,6 +35,11 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => } }) // Public endpoints (no auth required) + /** + * GET /api/domain/check + * Success: { available, domain } or { available: false, reason: 'invalid' }. + * Failure: { available: false }. + */ .get('/check', async ({ query }) => { try { const handle = (query.handle || "") @@ -60,6 +65,12 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => }; } }) + /** + * GET /api/domain/registered + * 200: { registered: true, type: 'wisp' | 'custom', domain, did, rkey, verified? } + * 404: { registered: false } + * 400: { error: 'Domain parameter required' } + */ .get('/registered', async ({ query, set }) => { try { const domain = (query.domain || "").trim().toLowerCase(); @@ -90,6 +101,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => const auth = await requireAuth(client, cookie) return { auth } }) + /** + * POST /api/domain/claim + * Success: { success: true, domain } + */ .post('/claim', async ({ body, auth }) => { try { const { handle } = body as { handle?: string }; @@ -133,6 +148,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to claim: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * POST /api/domain/update + * Success: { success: true, domain } + */ .post('/update', async ({ body, auth }) => { try { const { handle } = body as { handle?: string }; @@ -175,6 +194,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * POST /api/domain/custom/add + * Success: { success: true, id, domain, verified: false } + */ .post('/custom/add', async ({ body, auth }) => { try { const { domain } = body as { domain: string }; @@ -266,6 +289,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * POST /api/domain/custom/verify + * Success: { success: true, verified, error, found } + */ .post('/custom/verify', async ({ body, auth }) => { try { const { id } = body as { id: string }; @@ -294,6 +321,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to verify domain: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * DELETE /api/domain/custom/:id + * Success: { success: true } + */ .delete('/custom/:id', async ({ params, auth }) => { try { const { id } = params; @@ -317,6 +348,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * POST /api/domain/wisp/map-site + * Success: { success: true } + */ .post('/wisp/map-site', async ({ body, auth }) => { try { const { domain, siteRkey } = body as { domain: string; siteRkey: string | null }; @@ -334,6 +369,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * DELETE /api/domain/wisp/:domain + * Success: { success: true } + */ .delete('/wisp/:domain', async ({ params, auth }) => { try { const { domain } = params; @@ -373,6 +412,10 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`); } }) + /** + * POST /api/domain/custom/:id/map-site + * Success: { success: true } + */ .post('/custom/:id/map-site', async ({ params, body, auth }) => { try { const { id } = params; @@ -396,4 +439,4 @@ export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => logger.error('[Domain] Custom domain map error', err); throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`); } - }); \ No newline at end of file + }); diff --git a/apps/main-app/src/routes/site.ts b/apps/main-app/src/routes/site.ts index 02d1e52..309db82 100644 --- a/apps/main-app/src/routes/site.ts +++ b/apps/main-app/src/routes/site.ts @@ -20,6 +20,11 @@ export const siteRoutes = (client: NodeOAuthClient, cookieSecret: string) => const auth = await requireAuth(client, cookie) return { auth } }) + /** + * DELETE /api/site/:rkey + * Success: { success: true, message } + * Failure: { success: false, error } + */ .delete('/:rkey', async ({ params, auth }) => { const { rkey } = params @@ -120,6 +125,11 @@ export const siteRoutes = (client: NodeOAuthClient, cookieSecret: string) => } } }) + /** + * GET /api/site/:rkey/settings + * Success: place.wisp.settings record or default settings object. + * Failure: { success: false, error } + */ .get('/:rkey/settings', async ({ params, auth }) => { const { rkey } = params @@ -171,6 +181,11 @@ export const siteRoutes = (client: NodeOAuthClient, cookieSecret: string) => } } }) + /** + * POST /api/site/:rkey/settings + * Success: { success: true, uri, cid } + * Failure: { success: false, error } + */ .post('/:rkey/settings', async ({ params, body, auth }) => { const { rkey } = params diff --git a/apps/main-app/src/routes/user.ts b/apps/main-app/src/routes/user.ts index f3919ea..64fe159 100644 --- a/apps/main-app/src/routes/user.ts +++ b/apps/main-app/src/routes/user.ts @@ -21,6 +21,10 @@ export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => const auth = await requireAuth(client, cookie) return { auth } }) + /** + * GET /api/user/status + * Success: { did, hasSites, hasDomain, domain, sitesCount } + */ .get('/status', async ({ auth }) => { try { // Check if user has any sites @@ -41,6 +45,10 @@ export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error('Failed to get user status') } }) + /** + * GET /api/user/info + * Success: { did, handle } + */ .get('/info', async ({ auth }) => { try { let handle = 'unknown' @@ -65,6 +73,10 @@ export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error('Failed to get user info') } }) + /** + * GET /api/user/sites + * Success: { sites } + */ .get('/sites', async ({ auth }) => { try { const sites = await getSitesByDid(auth.did) @@ -74,6 +86,10 @@ export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error('Failed to get sites') } }) + /** + * GET /api/user/domains + * Success: { wispDomains: [{ domain, rkey }], customDomains } + */ .get('/domains', async ({ auth }) => { try { // Get all wisp.place subdomains with mappings (up to 3) @@ -94,6 +110,10 @@ export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error('Failed to get domains') } }) + /** + * POST /api/user/sync + * Success: { success: true, synced, errors } + */ .post('/sync', async ({ auth }) => { try { logger.debug('[User] Manual sync requested for', { did: auth.did }) @@ -109,6 +129,10 @@ export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => throw new Error('Failed to sync sites') } }) + /** + * GET /api/user/site/:rkey/domains + * Success: { rkey, domains } + */ .get('/site/:rkey/domains', async ({ auth, params }) => { try { const { rkey } = params diff --git a/apps/main-app/src/routes/wisp.ts b/apps/main-app/src/routes/wisp.ts index 8ba7648..90479c9 100644 --- a/apps/main-app/src/routes/wisp.ts +++ b/apps/main-app/src/routes/wisp.ts @@ -861,6 +861,12 @@ export const wispRoutes = (client: NodeOAuthClient, cookieSecret: string) => const auth = await requireAuth(client, cookie) return { auth } }) + /** + * GET /wisp/upload-progress/:jobId + * SSE stream of upload progress events for the current user. + * 404: { error: 'Job not found' } + * 403: { error: 'Unauthorized' } + */ .get( '/upload-progress/:jobId', async ({ params: { jobId }, auth, set }) => { @@ -951,6 +957,12 @@ export const wispRoutes = (client: NodeOAuthClient, cookieSecret: string) => return new Response(stream); } ) + /** + * POST /wisp/upload-files + * Success (empty upload): { success: true, uri, cid, fileCount: 0, siteName } + * Success (async upload): { success: true, jobId, message } + * Failure: throws error with message "Failed to upload files: ..." + */ .post( '/upload-files', async ({ body, auth }) => { diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 94317e5..6de2b4b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -30,6 +30,10 @@ export default defineConfig({ { label: 'Redirects & Rewrites', slug: 'redirects' }, ], }, + { + label: 'Reference', + autogenerate: { directory: 'reference' }, + }, ], customCss: ['./src/styles/custom.css'], }), diff --git a/docs/src/content/docs/reference/main-app-api.md b/docs/src/content/docs/reference/main-app-api.md new file mode 100644 index 0000000..d5a83ab --- /dev/null +++ b/docs/src/content/docs/reference/main-app-api.md @@ -0,0 +1,287 @@ +--- +title: Main App API +description: Expected responses from the main-app Elysia routes. +--- + +These endpoints power the main wisp.place backend (Bun + Elysia). Responses below are the shapes returned by the handlers in `apps/main-app/src/routes/*`. + +Notes: +- Authenticated routes rely on the signed `did` cookie. If authentication fails, the handler throws and Elysia returns an error response. +- Admin routes rely on the signed `admin_session` cookie. Unauthorized requests return `401 { error: 'Unauthorized' }`. + +## Auth (`/api/auth/*`) + +### `GET /api/auth/login` +Redirects to the AT Protocol OAuth authorize URL. + +- **302** → OAuth URL +- **302** → `/?error=missing_handle` if no handle/PDS provided +- **302** → `/?error=auth_failed` on failure + +### `POST /api/auth/signin` +```json +{ "url": "https://..." } +``` +On failure: +```json +{ "error": "Authentication failed", "details": "..." } +``` + +### `GET /api/auth/callback` +Redirects after OAuth completes. + +- **302** → `/onboarding` (no sites or domain) +- **302** → `/editor` (existing user) +- **302** → `/?error=auth_failed` on failure + +### `POST /api/auth/logout` +```json +{ "success": true } +``` +On failure: +```json +{ "error": "Logout failed" } +``` + +### `GET /api/auth/status` +Authenticated: +```json +{ "authenticated": true, "did": "did:plc:..." } +``` +Not authenticated: +```json +{ "authenticated": false } +``` + +## User (`/api/user/*`) + +### `GET /api/user/status` +```json +{ + "did": "did:plc:...", + "hasSites": true, + "hasDomain": false, + "domain": null, + "sitesCount": 3 +} +``` + +### `GET /api/user/info` +```json +{ "did": "did:plc:...", "handle": "user.bsky.social" } +``` + +### `GET /api/user/sites` +```json +{ "sites": [/* site rows */] } +``` + +### `GET /api/user/domains` +```json +{ + "wispDomains": [{ "domain": "name.wisp.place", "rkey": "site-rkey" }], + "customDomains": [/* custom domain rows */] +} +``` + +### `POST /api/user/sync` +```json +{ "success": true, "synced": 2, "errors": [] } +``` + +### `GET /api/user/site/:rkey/domains` +```json +{ "rkey": "site-rkey", "domains": [/* domain rows */] } +``` + +## Domain (`/api/domain/*`) + +### `GET /api/domain/check` +```json +{ "available": true, "domain": "name.wisp.place" } +``` +Invalid handle: +```json +{ "available": false, "reason": "invalid" } +``` + +### `GET /api/domain/registered` +Registered: +```json +{ "registered": true, "type": "wisp", "domain": "name.wisp.place", "did": "did:plc:...", "rkey": "site-rkey" } +``` +Custom domain: +```json +{ "registered": true, "type": "custom", "domain": "example.com", "did": "did:plc:...", "rkey": "site-rkey", "verified": true } +``` +Unregistered: +```json +{ "registered": false } +``` +Missing domain: +```json +{ "error": "Domain parameter required" } +``` + +### `POST /api/domain/claim` +```json +{ "success": true, "domain": "name.wisp.place" } +``` + +### `POST /api/domain/update` +```json +{ "success": true, "domain": "name.wisp.place" } +``` + +### `POST /api/domain/custom/add` +```json +{ "success": true, "id": "abcdef1234567890", "domain": "example.com", "verified": false } +``` + +### `POST /api/domain/custom/verify` +```json +{ "success": true, "verified": true, "error": null, "found": true } +``` + +### `DELETE /api/domain/custom/:id` +```json +{ "success": true } +``` + +### `POST /api/domain/wisp/map-site` +```json +{ "success": true } +``` + +### `DELETE /api/domain/wisp/:domain` +```json +{ "success": true } +``` + +### `POST /api/domain/custom/:id/map-site` +```json +{ "success": true } +``` + +## Site (`/api/site/*`) + +### `DELETE /api/site/:rkey` +```json +{ "success": true, "message": "Site deleted successfully" } +``` +On failure: +```json +{ "success": false, "error": "..." } +``` + +### `GET /api/site/:rkey/settings` +Returns the `place.wisp.settings` record when present, otherwise defaults: +```json +{ "indexFiles": ["index.html"], "cleanUrls": false, "directoryListing": false } +``` +On failure: +```json +{ "success": false, "error": "..." } +``` + +### `POST /api/site/:rkey/settings` +```json +{ "success": true, "uri": "at://...", "cid": "bafy..." } +``` +On validation failure: +```json +{ "success": false, "error": "Only one of spaMode, directoryListing, or custom404 can be enabled" } +``` + +## Wisp Uploads (`/wisp/*`) + +### `GET /wisp/upload-progress/:jobId` +Server-sent events stream for upload progress. + +- **event:** `progress` → `{ status, progress, result, error }` +- **event:** `done` → `result` +- **event:** `error` → `{ error }` + +Errors: +```json +{ "error": "Job not found" } +``` +```json +{ "error": "Unauthorized" } +``` + +### `POST /wisp/upload-files` +Empty upload (no files): +```json +{ "success": true, "uri": "at://...", "cid": "bafy...", "fileCount": 0, "siteName": "my-site" } +``` +Async upload: +```json +{ "success": true, "jobId": "...", "message": "Upload started. Connect to /wisp/upload-progress/..." } +``` + +## Admin (`/api/admin/*`) + +### `POST /api/admin/login` +```json +{ "success": true } +``` +Invalid credentials (401): +```json +{ "error": "Invalid credentials" } +``` + +### `POST /api/admin/logout` +```json +{ "success": true } +``` + +### `GET /api/admin/status` +Authenticated: +```json +{ "authenticated": true, "username": "admin" } +``` +Not authenticated: +```json +{ "authenticated": false } +``` + +### `GET /api/admin/logs` +```json +{ "logs": [/* combined logs */] } +``` + +### `GET /api/admin/errors` +```json +{ "errors": [/* combined errors */] } +``` + +### `GET /api/admin/metrics` +```json +{ "overall": {}, "mainApp": {}, "hostingService": {}, "timeWindow": 3600000 } +``` + +### `GET /api/admin/database` +```json +{ "stats": {}, "recentSites": [], "recentDomains": [] } +``` + +### `GET /api/admin/cache` +Returns the hosting service cache stats payload or: +```json +{ "error": "Failed to fetch cache stats from hosting service", "message": "Hosting service unavailable" } +``` + +### `GET /api/admin/sites` +```json +{ "sites": [/* sites */], "customDomains": [/* domains */] } +``` + +### `GET /api/admin/health` +```json +{ + "uptime": 12345, + "memory": { "heapUsed": 123, "heapTotal": 456, "rss": 789 }, + "timestamp": "2026-01-22T00:00:00.000Z" +} +``` -- 2.54.0