···33import indexHTML from "./html/index.html";
44import loginHTML from "./html/login.html";
55import profileHTML from "./html/profile.html";
66+import oauthTestHTML from "./html/oauth-test.html";
67import { canRegister, registerOptions, registerVerify, loginOptions, loginVerify } from "./routes/auth";
78import { hello, listUsers, getProfile, updateProfile } from "./routes/api";
99+import { authorizeGet, authorizePost, token, logout, userProfile, createInvite } from "./routes/indieauth";
810911(() => {
1012 const required = ["ORIGIN", "RP_ID"];
···2527 "/": indexHTML,
2628 "/login": loginHTML,
2729 "/profile": profileHTML,
3030+ "/oauth-test": oauthTestHTML,
2831 // API endpoints
2932 "/api/hello": hello,
3033 "/api/users": listUsers,
···3336 if (req.method === "PUT") return updateProfile(req);
3437 return new Response("Method not allowed", { status: 405 });
3538 },
3939+ "/api/invites/create": (req: Request) => {
4040+ if (req.method === "POST") return createInvite(req);
4141+ return new Response("Method not allowed", { status: 405 });
4242+ },
4343+ // IndieAuth/OAuth 2.0 endpoints
4444+ "/auth/authorize": (req: Request) => {
4545+ if (req.method === "GET") return authorizeGet(req);
4646+ if (req.method === "POST") return authorizePost(req);
4747+ return new Response("Method not allowed", { status: 405 });
4848+ },
4949+ "/auth/token": (req: Request) => {
5050+ if (req.method === "POST") return token(req);
5151+ return new Response("Method not allowed", { status: 405 });
5252+ },
5353+ "/auth/logout": (req: Request) => {
5454+ if (req.method === "POST") return logout(req);
5555+ return new Response("Method not allowed", { status: 405 });
5656+ },
5757+ // Passkey auth endpoints
3658 "/auth/can-register": canRegister,
3759 "/auth/register/options": registerOptions,
3860 "/auth/register/verify": registerVerify,
···4062 "/auth/login/verify": loginVerify,
4163 },
4264 development: process.env.NODE_ENV === "dev",
6565+ fetch(req) {
6666+ // Handle dynamic routes like /u/:username
6767+ const url = new URL(req.url);
6868+ const match = url.pathname.match(/^\/u\/([^\/]+)$/);
6969+ if (match) {
7070+ const username = match[1];
7171+ return userProfile(req, username);
7272+ }
7373+7474+ // Let Bun handle static routes
7575+ return undefined as never;
7676+ },
4377});
44784579console.log("[Indiko] running on", env.ORIGIN);
-1
src/migrations/002_add_user_status_role.sql
···11-- Add status and role columns to users table
22ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'suspended', 'inactive'));
33ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';
44-54-- Update existing admin users to have 'admin' role
65UPDATE users SET role = 'admin' WHERE is_admin = 1;
+43
src/migrations/003_add_indieauth_tables.sql
···11+-- Add tables for IndieAuth/OAuth 2.0 support
22+-- Apps (auto-registered on first authorization request)
33+CREATE TABLE IF NOT EXISTS apps (
44+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55+ client_id TEXT NOT NULL UNIQUE,
66+ redirect_uris TEXT NOT NULL, -- JSON array
77+ name TEXT,
88+ first_seen INTEGER NOT NULL DEFAULT (strftime('%s','now')),
99+ last_used INTEGER NOT NULL DEFAULT (strftime('%s','now'))
1010+);
1111+-- User permissions per app
1212+CREATE TABLE IF NOT EXISTS permissions (
1313+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1414+ user_id INTEGER NOT NULL,
1515+ client_id TEXT NOT NULL,
1616+ scopes TEXT NOT NULL, -- JSON array
1717+ granted_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
1818+ last_used INTEGER NOT NULL DEFAULT (strftime('%s','now')),
1919+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
2020+ FOREIGN KEY (client_id) REFERENCES apps(client_id) ON DELETE CASCADE,
2121+ UNIQUE(user_id, client_id)
2222+);
2323+-- Authorization codes (short-lived, single-use)
2424+CREATE TABLE IF NOT EXISTS authcodes (
2525+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2626+ code TEXT NOT NULL UNIQUE,
2727+ user_id INTEGER NOT NULL,
2828+ client_id TEXT NOT NULL,
2929+ redirect_uri TEXT NOT NULL,
3030+ scopes TEXT NOT NULL, -- JSON array
3131+ code_challenge TEXT NOT NULL,
3232+ code_challenge_method TEXT NOT NULL DEFAULT 'S256',
3333+ expires_at INTEGER NOT NULL,
3434+ used INTEGER NOT NULL DEFAULT 0,
3535+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
3636+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
3737+);
3838+-- Indexes
3939+CREATE INDEX IF NOT EXISTS idx_apps_client_id ON apps(client_id);
4040+CREATE INDEX IF NOT EXISTS idx_permissions_user_id ON permissions(user_id);
4141+CREATE INDEX IF NOT EXISTS idx_permissions_client_id ON permissions(client_id);
4242+CREATE INDEX IF NOT EXISTS idx_authcodes_code ON authcodes(code);
4343+CREATE INDEX IF NOT EXISTS idx_authcodes_expires_at ON authcodes(expires_at);