const COOKIE_NAME = 'soulwall_session'; const INTENT_COOKIE_NAME = 'soulwall_intent'; const WEEKLY_COVENANT_SECONDS = 24 * 60 * 60; const INTENT_TTL_SECONDS = 30 * 60; const localMemory = globalThis.__soulwallLocalMemory || { values: new Map() }; globalThis.__soulwallLocalMemory = localMemory; function json(data, status = 200, headers = {}) { return new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', ...headers } }); } function base64UrlEncode(input) { const bytes = input instanceof Uint8Array ? input : new TextEncoder().encode(String(input)); let binary = ''; bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function base64UrlDecode(input) { const normalized = String(input).replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '='); const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return new TextDecoder().decode(bytes); } async function hmac(secret, payload) { const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload)); return base64UrlEncode(new Uint8Array(signature)); } function getSecret(env) { if (env.SOULWALL_SESSION_SECRET) return env.SOULWALL_SESSION_SECRET; if (env.SOULWALL_ALLOW_INSECURE_PREVIEW === '1') return 'soulwall-insecure-preview-secret'; return null; } function parseCookies(request) { const header = request.headers.get('cookie') || ''; return Object.fromEntries( header .split(';') .map((part) => part.trim()) .filter(Boolean) .map((part) => { const equalsIndex = part.indexOf('='); if (equalsIndex === -1) return [part, '']; return [part.slice(0, equalsIndex), decodeURIComponent(part.slice(equalsIndex + 1))]; }) ); } function buildCookie(value, maxAgeSeconds, name = COOKIE_NAME) { const secureFlag = maxAgeSeconds > 0 ? '; Secure' : ''; return `${name}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}${secureFlag}`; } function isLocalPreview(env) { return env.SOULWALL_ALLOW_INSECURE_PREVIEW === '1' || env.CF_PAGES_BRANCH === 'local'; } function getSoulwallStore(env) { if (env.SOULWALL_KV) { return { async get(key) { return env.SOULWALL_KV.get(key); }, async put(key, value, options = {}) { return env.SOULWALL_KV.put(key, value, options); } }; } if (!isLocalPreview(env)) { return null; } return { async get(key) { const item = localMemory.values.get(key); if (!item) return null; if (item.expiresAt && item.expiresAt <= Date.now()) { localMemory.values.delete(key); return null; } return item.value; }, async put(key, value, options = {}) { const ttl = Number(options.expirationTtl || 0); localMemory.values.set(key, { value, expiresAt: ttl > 0 ? Date.now() + ttl * 1000 : null }); } }; } async function createSessionToken(env, source = 'preview', expSeconds = null) { const secret = getSecret(env); if (!secret) { return null; } const nowSeconds = Math.floor(Date.now() / 1000); const payload = { iat: nowSeconds, exp: Number.isFinite(expSeconds) ? Number(expSeconds) : nowSeconds + WEEKLY_COVENANT_SECONDS, source }; const encodedPayload = base64UrlEncode(JSON.stringify(payload)); const signature = await hmac(secret, encodedPayload); return { token: `${encodedPayload}.${signature}`, payload }; } async function verifySessionToken(env, token) { const secret = getSecret(env); if (!secret || !token || !token.includes('.')) { return { access: false, reason: 'missing' }; } const [encodedPayload, signature] = token.split('.'); const expectedSignature = await hmac(secret, encodedPayload); if (signature !== expectedSignature) { return { access: false, reason: 'bad_signature' }; } let payload; try { payload = JSON.parse(base64UrlDecode(encodedPayload)); } catch (error) { return { access: false, reason: 'bad_payload' }; } const nowSeconds = Math.floor(Date.now() / 1000); if (!Number.isFinite(payload.exp) || payload.exp <= nowSeconds) { return { access: false, reason: 'expired', payload }; } return { access: true, reason: 'ok', payload, expiresAt: new Date(payload.exp * 1000).toISOString(), secondsRemaining: payload.exp - nowSeconds }; } export { COOKIE_NAME, INTENT_COOKIE_NAME, INTENT_TTL_SECONDS, WEEKLY_COVENANT_SECONDS, buildCookie, createSessionToken, getSoulwallStore, json, parseCookies, verifySessionToken };