File size: 3,290 Bytes
739400f
 
 
 
a63c2c0
 
 
739400f
a63c2c0
 
 
 
 
739400f
 
 
 
a63c2c0
739400f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a63c2c0
739400f
 
a63c2c0
739400f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import './server-tls';
import {createCipheriv,createDecipheriv,randomBytes,createHash} from 'node:crypto';
import {cookies} from 'next/headers';

export const SESSION_COOKIE='kiku-session';
export const FLOW_COOKIE='kiku-oauth';
export const CLIENT_ID=process.env.HF_OAUTH_CLIENT_ID||'https://reubencf-kiku-engine.hf.space/.well-known/oauth-cimd';
type Session={token:string;expires:number;user:{id:string;name:string}};
export type OAuthFlow={state:string;verifier:string;expires:number;popup?:boolean;next?:string};
/** Pages sign-in may return to. A fixed list, so the parameter can never become an open redirect. */
const RETURNS=new Set(['/','/score']);
export const returnPath=(value:string|null)=>value&&RETURNS.has(value)?value:'/';
const devGlobal=globalThis as typeof globalThis & {kikuSessionKey?:Buffer};
function key(){
 const secret=process.env.SESSION_SECRET;
 if(secret){if(!/^[a-f\d]{64}$/i.test(secret))throw new Error('SESSION_SECRET must contain 32 random bytes encoded as hex.');return Buffer.from(secret,'hex');}
 if(process.env.NODE_ENV==='production')throw new Error('SESSION_SECRET is required.');
 return devGlobal.kikuSessionKey??=(randomBytes(32));
}
export function seal(value:unknown){
 const iv=randomBytes(12),cipher=createCipheriv('aes-256-gcm',key(),iv);
 const encrypted=Buffer.concat([cipher.update(JSON.stringify(value),'utf8'),cipher.final()]);
 return Buffer.concat([iv,cipher.getAuthTag(),encrypted]).toString('base64url');
}
export function unseal<T>(value?:string):T|null{
 if(!value)return null;
 try{const bytes=Buffer.from(value,'base64url');if(bytes.length<29)return null;
  const decipher=createDecipheriv('aes-256-gcm',key(),bytes.subarray(0,12));decipher.setAuthTag(bytes.subarray(12,28));
  return JSON.parse(Buffer.concat([decipher.update(bytes.subarray(28)),decipher.final()]).toString('utf8')) as T;
 }catch{return null;}
}
export function appOrigin(){
 const configured=process.env.APP_ORIGIN||(process.env.SPACE_HOST?'https://'+process.env.SPACE_HOST:undefined);
 if(!configured&&process.env.NODE_ENV==='production')throw new Error('APP_ORIGIN is required.');
 return new URL(configured||'http://localhost:3000').origin;
}
export function cookieOptions(maxAge:number){return {httpOnly:true,secure:appOrigin().startsWith('https:'),sameSite:'lax' as const,path:'/',maxAge};}
export async function session(){
 const value=unseal<Session>((await cookies()).get(SESSION_COOKIE)?.value);
 return value&&typeof value.token==='string'&&value.expires>Date.now()&&value.user?.id?value:null;
}
export async function authorize(req:Request){
 const user=await session();
 if(!user)return {error:Response.json({error:'Sign in with Hugging Face to continue.',signIn:true},{status:401})} as const;
 if(req.method!=='GET'&&(req.headers.get('origin')!==appOrigin()||req.headers.get('sec-fetch-site')==='cross-site'))
  return {error:Response.json({error:'Please submit from Kiku AI.'},{status:403})} as const;
 return {session:user} as const;
}
export function createFlow(popup=false,next='/'):OAuthFlow{return {state:randomBytes(32).toString('base64url'),verifier:randomBytes(32).toString('base64url'),expires:Date.now()+600000,popup,next};}
export function challenge(verifier:string){return createHash('sha256').update(verifier).digest('base64url');}