public
imalexnord
read
Slot
Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.
Languages
Repository composition by tracked source files.
TypeScript
79%
CSS
19%
SQL
2%
HTML
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
import type { Env } from './types';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
}
function base64Url(bytes: Uint8Array): string {
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
function fromBase64(value: string): Uint8Array {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
const binary = atob(padded);
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
}
async function hmacKey(secret: string): Promise<CryptoKey> {
return crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify'],
);
}
export async function signSession(env: Env, email: string): Promise<string> {
const payload = base64Url(encoder.encode(JSON.stringify({ email, exp: Date.now() + 7 * 86400_000 })));
const signature = new Uint8Array(
await crypto.subtle.sign('HMAC', await hmacKey(env.SESSION_SECRET), encoder.encode(payload)),
);
return `${payload}.${base64Url(signature)}`;
}
export async function verifySession(env: Env, token: string | null): Promise<string | null> {
if (!token) return null;
const [payload, signature] = token.split('.');
if (!payload || !signature) return null;
const ok = await crypto.subtle.verify(
'HMAC',
await hmacKey(env.SESSION_SECRET),
toArrayBuffer(fromBase64(signature)),
encoder.encode(payload),
);
if (!ok) return null;
try {
const parsed = JSON.parse(decoder.decode(fromBase64(payload))) as { email?: string; exp?: number };
if (!parsed.email || !parsed.exp || parsed.exp < Date.now()) return null;
return parsed.email;
} catch {
return null;
}
}
async function encryptionKey(env: Env): Promise<CryptoKey> {
const raw = fromBase64(env.TOKEN_ENCRYPTION_KEY);
if (raw.byteLength !== 32) throw new Error('TOKEN_ENCRYPTION_KEY must decode to 32 bytes');
return crypto.subtle.importKey('raw', toArrayBuffer(raw), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
export async function encryptToken(env: Env, value: string): Promise<string> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toArrayBuffer(iv) }, await encryptionKey(env), encoder.encode(value)),
);
return `${base64Url(iv)}.${base64Url(ciphertext)}`;
}
export async function decryptToken(env: Env, value: string): Promise<string> {
const [ivText, ciphertextText] = value.split('.');
if (!ivText || !ciphertextText) throw new Error('Invalid encrypted token');
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: toArrayBuffer(fromBase64(ivText)) },
await encryptionKey(env),
toArrayBuffer(fromBase64(ciphertextText)),
);
return decoder.decode(plaintext);
}
export function randomUrlToken(bytes = 32): string {
return base64Url(crypto.getRandomValues(new Uint8Array(bytes)));
}
export async function sha256Base64Url(value: string): Promise<string> {
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(value)));
return base64Url(digest);
}
export function getCookie(request: Request, key: string): string | null {
const cookie = request.headers.get('Cookie') ?? '';
for (const part of cookie.split(';')) {
const [name, ...rest] = part.trim().split('=');
if (name === key) return decodeURIComponent(rest.join('='));
}
return null;
}
export function sessionCookie(value: string, maxAge = 7 * 86400, secure = true): string {
const securePart = secure ? '; Secure' : '';
return `meet_session=${encodeURIComponent(value)}; Path=/; HttpOnly${securePart}; SameSite=Lax; Max-Age=${maxAge}`;
}
export function clearSessionCookie(secure = true): string {
const securePart = secure ? '; Secure' : '';
return `meet_session=; Path=/; HttpOnly${securePart}; SameSite=Lax; Max-Age=0`;
}