NobGit
public imalexnord read

Slot

Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.

Languages

Repository composition by tracked source files.

TypeScript
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
crypto.ts
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`;
}