57 lines
2.9 KiB
TypeScript
57 lines
2.9 KiB
TypeScript
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
import { openSiteDatabase } from "./site-database";
|
|
|
|
export type DocumentKeyScope = "read" | "read-write";
|
|
export type DocumentKey = { id: string; name: string; scope: DocumentKeyScope;
|
|
createdAt: number; expiresAt: number; revokedAt: number | null; lastUsedAt: number | null };
|
|
function database() {
|
|
const db = openSiteDatabase();
|
|
db.exec(`CREATE TABLE IF NOT EXISTS document_api_keys (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, digest TEXT NOT NULL UNIQUE,
|
|
scope TEXT NOT NULL CHECK(scope IN ('read','read-write')), created_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL, revoked_at INTEGER, last_used_at INTEGER
|
|
);`);
|
|
return db;
|
|
}
|
|
export function createDocumentKey(name: string, scope: DocumentKeyScope = "read-write", days = 90) {
|
|
if (!name.trim() || name.length > 100 || !["read", "read-write"].includes(scope) ||
|
|
!Number.isSafeInteger(days) || days < 1 || days > 365) throw Error("Invalid key name, scope or lifetime");
|
|
const id = randomUUID();
|
|
const token = "tdoc_" + randomBytes(32).toString("base64url");
|
|
const createdAt = Date.now();
|
|
const expiresAt = createdAt + days * 86400000;
|
|
const db = database();
|
|
try {
|
|
db.prepare("INSERT INTO document_api_keys(id,name,digest,scope,created_at,expires_at) VALUES (?,?,?,?,?,?)")
|
|
.run(id, name.trim(), createHash("sha256").update(token).digest("hex"), scope, createdAt, expiresAt);
|
|
return { id, token, name: name.trim(), scope, createdAt, expiresAt };
|
|
} finally { db.close(); }
|
|
}
|
|
export function authenticateDocumentKey(token: string | undefined, now = Date.now()) {
|
|
if (!token || !/^tdoc_[A-Za-z0-9_-]{43}$/.test(token)) return null;
|
|
const db = database();
|
|
try {
|
|
const row = db.prepare("SELECT id,scope,last_used_at AS lastUsedAt FROM document_api_keys WHERE digest=? AND revoked_at IS NULL AND expires_at>?")
|
|
.get(createHash("sha256").update(token).digest("hex"), now) as { id: string; scope: DocumentKeyScope; lastUsedAt: number | null } | undefined;
|
|
if (!row) return null;
|
|
if (row.lastUsedAt === null || row.lastUsedAt < now - 60000)
|
|
db.prepare("UPDATE document_api_keys SET last_used_at=? WHERE id=? AND (last_used_at IS NULL OR last_used_at<?)")
|
|
.run(now, row.id, now - 60000);
|
|
return { id: row.id, scope: row.scope };
|
|
} finally { db.close(); }
|
|
}
|
|
export function listDocumentKeys(): DocumentKey[] {
|
|
const db = database();
|
|
try {
|
|
return db.prepare(`SELECT id,name,scope,created_at AS createdAt,expires_at AS expiresAt,
|
|
revoked_at AS revokedAt,last_used_at AS lastUsedAt FROM document_api_keys ORDER BY created_at,id`).all() as DocumentKey[];
|
|
} finally { db.close(); }
|
|
}
|
|
export function revokeDocumentKey(id: string) {
|
|
const db = database();
|
|
try {
|
|
const result = db.prepare("UPDATE document_api_keys SET revoked_at=COALESCE(revoked_at,?) WHERE id=?").run(Date.now(), id);
|
|
if (!result.changes) throw Error("Document key not found");
|
|
} finally { db.close(); }
|
|
}
|