import { standardize } from './platform.js'; // Host-only project sessions. Authorization codes use PKCE and single-use state. const account='https://account.dorianmarie.com'; const enc=new TextEncoder(); export const digest=async value=>btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest('SHA-256',enc.encode(value))))).replaceAll('+','-').replaceAll('/','_').replaceAll('=',''); const random=()=>btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replaceAll('+','-').replaceAll('/','_').replaceAll('=',''); const cookie=(req,name)=>req.headers.get('Cookie')?.split(';').map(s=>s.trim()).find(s=>s.startsWith(name+'='))?.slice(name.length+1); const set=(name,value,age=2592000)=>`${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${age}`; const redirect=(location,cookies=[])=>{const h=new Headers({'Location':location,'Cache-Control':'no-store','Referrer-Policy':'no-referrer'});for(const c of cookies)h.append('Set-Cookie',c);return new Response(null,{status:302,headers:h});}; async function call(env,path,body){return env.ACCOUNT.fetch(new Request(account+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));} export async function ssoRoute(req,env){const u=new URL(req.url);if(u.pathname==='/sso/login'||u.pathname==='/sso/register'){ const verifier=random(),state=random();const target=new URL('/authorize',account);target.searchParams.set('redirect_uri',u.origin+'/sso/callback');target.searchParams.set('code_challenge',await digest(verifier));target.searchParams.set('state',state);if(u.pathname==='/sso/register')target.searchParams.set('mode','register');return redirect(target.href,[set('__Host-sso-flow-'+state,verifier,300)]); }if(u.pathname==='/sso/callback'){ const state=u.searchParams.get('state')||'';const verifier=/^[A-Za-z0-9_-]{43}$/.test(state)?cookie(req,'__Host-sso-flow-'+state):null;const legacy=cookie(req,'__Host-sso-flow')?.split('.');const flow=verifier?[state,verifier]:legacy;if(!flow||flow[0]!==state)return new Response('invalid sign in request. please start again.',{status:400,headers:{'Cache-Control':'no-store'}}); const response=await call(env,'/sso/exchange',{code:u.searchParams.get('code'),verifier:flow[1],redirect_uri:u.origin+'/sso/callback'});if(!response.ok)return new Response('sign in expired. please start again.',{status:400});const {token}=await response.json();return redirect(u.hostname.startsWith('heartbeats.')?'/app':'/',[set('__Host-dm-session',token),set('__Host-sso-flow-'+state,'',0),set('__Host-sso-flow','',0)]); }if(u.pathname==='/sso/logout'){ if(req.method!=='POST'||req.headers.get('Origin')!==u.origin)return new Response('method not allowed',{status:405}); await ssoSignOut(req,env);return redirect(account,[set('__Host-dm-session','',0)]); }if(u.pathname==='/sso/preferences'){if(!['GET','POST'].includes(req.method))return new Response('method not allowed',{status:405});if(req.method==='POST'&&req.headers.get('Origin')!==u.origin)return new Response('invalid origin',{status:403});const token=cookie(req,'__Host-dm-session');if(!token)return Response.json({error:'sign in required'},{status:401,headers:{'Cache-Control':'no-store'}});let preferences;if(req.method==='POST'){if(Number(req.headers.get('Content-Length'))>2048)return new Response('request too large',{status:413});const body=await req.text();if(body.length>2048)return new Response('request too large',{status:413});preferences=JSON.parse(body);}return call(env,'/sso/preferences',{token,origin:u.origin,preferences});}if(u.pathname==='/api/identity'){const user=await ssoUser(req,env);return Response.json(user?{user}:{error:'sign in required'},{status:user?200:401,headers:{'Cache-Control':'no-store'}});}if(u.pathname==='/sso/me'){const user=await ssoUser(req,env);return Response.json({user},{headers:{'Cache-Control':'no-store'}});} return null; } export async function ssoSignOut(req,env){const token=cookie(req,'__Host-dm-session');if(token){const response=await call(env,'/sso/revoke',{token,origin:new URL(req.url).origin});if(!response.ok)throw Error('could not sign out');}} export async function ssoUser(req,env){const bearer=req.headers.get('Authorization');if(bearer){if(!/^Bearer dm_[A-Za-z0-9_-]{43}$/.test(bearer))return null;const response=await call(env,'/sso/key',{token:bearer.slice(7),origin:new URL(req.url).origin});if(response.status===401)return null;if(!response.ok)throw Error('account service unavailable');return (await response.json()).user;}const token=cookie(req,'__Host-dm-session');if(!token)return null;const response=await call(env,'/sso/session',{token,origin:new URL(req.url).origin});if(response.status===401)return null;if(!response.ok)throw Error('account service unavailable');return (await response.json()).user;} export function wrap(worker,options={}){return standardize({...worker,async fetch(req,env,ctx){try{const route=await ssoRoute(req,env);if(route)return route;const u=new URL(req.url);if(req.headers.get('Authorization')?.startsWith('Bearer dm_')&&!['GET','HEAD','OPTIONS'].includes(req.method)&&req.headers.get('Origin')!==u.origin)return Response.json({error:'invalid origin'},{status:403,headers:{'Cache-Control':'no-store'}});if(options.requirePage&&req.method==='GET'&&req.headers.get('Accept')?.includes('text/html')&&(!options.onlyApp||u.pathname==='/app')&&!await ssoUser(req,env))return redirect('/sso/login');return await worker.fetch(req,env,ctx);}catch{return new Response('account service unavailable. please try again.',{status:503,headers:{'Cache-Control':'no-store'}});}}});}