"use strict"; const CLIENT_NAME_RE = /^[A-Za-z0-9._-]{1,64}$/; const TOKEN_PREFIX = "fab_"; function nowIso() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); } async function sha256Hex(text) { const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join(""); } function publicClient(row) { return { id: row.id, name: row.name, created: row.created, last_used: row.last_used || null, }; } async function loadClients() { const data = await browser.storage.local.get("clients"); return Array.isArray(data.clients) ? data.clients : []; } async function saveClients(clients) { await browser.storage.local.set({ clients }); } async function listClients() { return (await loadClients()).map(publicClient); } async function createClient(name) { name = String(name || "").trim(); if (!CLIENT_NAME_RE.test(name)) { throw new Error("Name must be 1–64 characters: A-Za-z0-9._-"); } const clients = await loadClients(); if (clients.some((row) => row.name === name)) { throw new Error("A client with that name already exists"); } const raw = new Uint8Array(32); crypto.getRandomValues(raw); const token = TOKEN_PREFIX + Array.from(raw, (b) => b.toString(16).padStart(2, "0")).join(""); const idBytes = new Uint8Array(16); crypto.getRandomValues(idBytes); const id = Array.from(idBytes, (b) => b.toString(16).padStart(2, "0")).join(""); clients.push({ id, name, token_hash: await sha256Hex(token), created: nowIso(), last_used: null, }); await saveClients(clients); return { id, name, token }; } async function revokeClient(id) { const clients = await loadClients(); const next = clients.filter((row) => row.id !== id && row.name !== id); if (next.length === clients.length) { throw new Error("No such client"); } await saveClients(next); return id; } async function findClientByTokenHash(tokenHash) { const clients = await loadClients(); const row = clients.find((item) => item.token_hash === tokenHash); if (!row) { return null; } row.last_used = nowIso(); await saveClients(clients); return publicClient(row); }