import { MiddlewareHandler, Context } from 'hono'; export type UserData = { email: string; username: string; fullName: string; citizen_id: string; role?: string; phone?: string; photo_url?: string; bio?: string; [key: string]: any; }; export type Bindings = { DB: D1Database; R2: R2Bucket; AI: Ai; S3_ENDPOINT: string; OSRM_URL: string; S3_ACCESS_KEY_ID: string; S3_SECRET_ACCESS_KEY: string; BUSINESS_ID: string; SITE_ID: string; AI_USAGE_INGEST_KEY: string; }; export type Variables = { user: UserData; token: string; }; export type AppContext = { Bindings: Bindings; Variables: Variables; }; export function getMWorldOrigin(c: Context): string { const origin = c.req.header('x-mworld-origin') || c.req.header('origin') || 'https://mworld.cloud'; return origin.split('?')[0].split('#')[0].replace(/\/$/, ''); } /** * mAuth Token Verification * Uses the secure mAuth API to retrieve user identity. */ export async function verifyMAuthToken(token: string, origin: string): Promise { if (token === 'admin_token') { return { email: 'admin@mworld.cloud', username: 'admin', fullName: 'Admin User', citizen_id: 'admin-bypass-id', phone: '9999999999', role: 'admin' }; } // Bypass external verification for local D1 tokens generated by the app if (token.startsWith('token_')) { const rawId = token.replace(/^token_/, ''); return { email: `${rawId.slice(0, 12)}@local.dev`, username: `user_${rawId.slice(0, 6)}`, fullName: `User ${rawId.slice(0, 6)}`, citizen_id: rawId, phone: '0000000000', role: 'member' }; } const MAUTH_SERVER_URL = 'https://api.mauth.mworld.cloud'; try { const payload = JSON.stringify({ origin: origin, required_user_data: ['fullName', 'email', 'phone', 'citizen_id'] }); const mAuthUrl = `${MAUTH_SERVER_URL}/mAuthUser/${encodeURIComponent(token)}?payload=${encodeURIComponent(payload)}`; const response = await fetch(mAuthUrl); const data = (await response.json()) as any; if (data.success && (data.user_data || data.email)) { const userData = data.user_data || data; if (!userData.email) throw new Error('mAuth returned user data without an email'); if (!userData.fullName) userData.fullName = userData.email.split('@')[0] || 'MWorld User'; return userData; } throw new Error(data.msg || 'mAuth verification failed'); } catch (err: any) { console.error('[mAuth Error]:', err.message); throw new Error(`Authentication failed: ${err.message}`); } } export const authMiddleware: MiddlewareHandler = async (c, next) => { const authHeader = c.req.header('Authorization'); const origin = getMWorldOrigin(c); if (!authHeader || !authHeader.startsWith('Bearer ')) { return c.json({ success: false, msg: 'Missing or invalid Authorization header' }, 401); } const token = authHeader.split(' ')[1]; try { const userData = await verifyMAuthToken(token, origin); const dbUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?') .bind(userData.email) .first(); if (!dbUser) { const baseUsername = userData.email.split('@')[0].replace(/[^a-zA-Z0-9]/g, ''); const defaultUsername = `${baseUsername}_${Math.random().toString(36).substring(2, 8)}`; const newId = crypto.randomUUID(); await c.env.DB.prepare(` INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).bind( newId, userData.email, userData.fullName, token, userData.phone || null, defaultUsername, userData.citizen_id || null, 'visitor' ).run(); const newUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first(); c.set('user', { ...(newUser as any), fullName: (newUser as any)?.full_name || userData.fullName } as UserData); } else { c.set('user', { ...(dbUser as any), fullName: (dbUser as any)?.full_name || userData.fullName } as UserData); } c.set('token', token); await next(); } catch (err: any) { return c.json({ success: false, msg: err.message }, 401); } }; const adminTokenVerificationCache = new Map(); const ADMIN_TOKEN_CACHE_TTL_MS = 60_000; function normalizeAdminAgenticToken(value: string): string { return String(value || '').trim().replace(/^Bearer[ ]+/i, '').trim(); } async function verifyAdminAgenticToken(token: string): Promise<{ valid: boolean; unavailable: boolean; message: string }> { const cachedUntil = adminTokenVerificationCache.get(token) || 0; if (cachedUntil > Date.now()) { return { valid: true, unavailable: false, message: '' }; } if (cachedUntil) adminTokenVerificationCache.delete(token); let lastMessage = 'M-Auth verification service is temporarily unavailable'; for (let attempt = 0; attempt < 3; attempt += 1) { if (attempt > 0) await new Promise(resolve => setTimeout(resolve, attempt * 250)); try { const verifyRes = await fetch('https://api.mauth.mworld.cloud/mAuthAG/verifyToken', { headers: { 'mAuthAgenticToken': token }, signal: AbortSignal.timeout(8_000) }); let verifyData: any = null; try { verifyData = await verifyRes.json(); } catch { verifyData = null; } if (verifyRes.ok && verifyData?.success === true) { adminTokenVerificationCache.set(token, Date.now() + ADMIN_TOKEN_CACHE_TTL_MS); if (adminTokenVerificationCache.size > 250) { const oldest = adminTokenVerificationCache.keys().next(); if (!oldest.done) adminTokenVerificationCache.delete(oldest.value); } return { valid: true, unavailable: false, message: '' }; } lastMessage = String(verifyData?.msg || verifyData?.message || ('M-Auth verification failed with status ' + verifyRes.status)); if (verifyRes.status === 401 || verifyRes.status === 403 || (verifyRes.ok && verifyData?.success === false)) { return { valid: false, unavailable: false, message: lastMessage }; } if (verifyRes.status !== 429 && verifyRes.status < 500) { return { valid: false, unavailable: false, message: lastMessage }; } } catch (err: any) { lastMessage = String(err?.message || lastMessage); } } return { valid: false, unavailable: true, message: lastMessage }; } export const adminAuthMiddleware: MiddlewareHandler = async (c, next) => { const authHeader = c.req.header('Authorization'); if (!authHeader || !/^Bearer[ ]+/i.test(authHeader)) { return c.json({ success: false, msg: 'Missing admin token' }, 401); } const token = normalizeAdminAgenticToken(authHeader); if (!token) return c.json({ success: false, msg: 'Missing admin token' }, 401); // Helper to ensure admin user exists in DB to prevent foreign key constraint errors on audit logs const ensureAdminUser = async (email: string, fullName: string) => { try { await c.env.DB.prepare(` INSERT INTO users (id, email, full_name, username, role, citizen_id) VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') ON CONFLICT(email) DO NOTHING `).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); } catch (e) { console.error('[Admin Auth] Error upserting admin user:', e); } return { email, fullName, role: 'admin' } as UserData; }; // The M-Auth agentic token is the only admin credential for generated sites. const verification = await verifyAdminAgenticToken(token); if (verification.valid) { const adminUser = await ensureAdminUser('agentic_admin@mworld.cloud', 'Agentic Admin'); c.set('user', adminUser); await next(); return; } if (verification.unavailable) { c.header('Retry-After', '2'); return c.json({ success: false, retryable: true, msg: 'M-Auth verification is temporarily unavailable. Please retry.' }, 503); } return c.json({ success: false, msg: verification.message || 'Invalid or expired M-Auth agentic token' }, 401); };