Keep client secrets in add-on storage; host only verifies and serves the local API.

This commit is contained in:
alexveley
2026-09-22 07:42:10 -04:00
parent de0814f3ed
commit 41c83dabac
13 changed files with 185 additions and 290 deletions
+82
View File
@@ -0,0 +1,82 @@
"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 164 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);
}