83 lines
2.2 KiB
JavaScript
83 lines
2.2 KiB
JavaScript
"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);
|
||
}
|