feat: configure signed Memos webhooks when supported
This commit is contained in:
@@ -125,6 +125,9 @@ applyColumnMigration(20, "sources", "attachment_storage_mode", "ALTER TABLE sour
|
||||
applyColumnMigration(21, "sources", "attachment_cache_limit_bytes", "ALTER TABLE sources ADD COLUMN attachment_cache_limit_bytes INTEGER NOT NULL DEFAULT 104857600");
|
||||
applyColumnMigration(22, "sources", "attachment_cache_error", "ALTER TABLE sources ADD COLUMN attachment_cache_error TEXT");
|
||||
db.prepare("INSERT OR IGNORE INTO schema_migrations(version) VALUES(23)").run();
|
||||
applyColumnMigration(24, "sources", "webhook_mode", "ALTER TABLE sources ADD COLUMN webhook_mode TEXT NOT NULL DEFAULT 'manual'");
|
||||
applyColumnMigration(25, "sources", "webhook_remote_name", "ALTER TABLE sources ADD COLUMN webhook_remote_name TEXT");
|
||||
applyColumnMigration(26, "sources", "webhook_signing_secret_encrypted", "ALTER TABLE sources ADD COLUMN webhook_signing_secret_encrypted TEXT");
|
||||
|
||||
const admin = process.env.ADMIN_USERNAME;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
|
||||
@@ -6,6 +6,10 @@ async function request(url: string, token: string, init?: RequestInit) {
|
||||
const res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(init?.headers || {}) }, cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`Memos API ${res.status}: ${await res.text()}`); return res;
|
||||
}
|
||||
export async function createUserWebhook(baseUrl: string, token: string, userName: string, webhook: { url: string; displayName: string; signingSecret: string }) {
|
||||
const user = userName.split("/").at(-1); if (!user) throw new Error("Invalid Memos user");
|
||||
return (await request(`${base(baseUrl)}/users/${encodeURIComponent(user)}/webhooks`, token, { method: "POST", body: JSON.stringify(webhook) })).json() as Promise<{ name: string }>;
|
||||
}
|
||||
export async function verifyMemos(baseUrl: string, token: string) { await request(`${base(baseUrl)}/memos?pageSize=1`, token); }
|
||||
export async function getMemosIdentity(baseUrl: string, token: string) {
|
||||
const user = await (await request(`${base(baseUrl)}/auth/status`, token, { method: "POST", body: "{}" })).json() as MemosIdentity;
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
||||
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
||||
|
||||
export function createWebhookSecret() { return randomBytes(32).toString("base64url"); }
|
||||
export function webhookSecretHash(secret: string) { return createHash("sha256").update(secret).digest("hex"); }
|
||||
@@ -8,3 +8,8 @@ export function webhookSecretMatches(secret: string, expectedHash: string | null
|
||||
const expected = Buffer.from(expectedHash, "hex");
|
||||
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
||||
}
|
||||
export function standardWebhookMatches(secret: string, id: string | null, timestamp: string | null, signature: string | null, body: string) {
|
||||
if (!id || !timestamp || !signature || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
|
||||
const expected = createHmac("sha256", secret).update(`${id}.${timestamp}.${body}`).digest("base64");
|
||||
return signature.split(" ").some((item) => { const value = item.split(",")[1]; if (!value) return false; const actual = Buffer.from(value); const target = Buffer.from(expected); return actual.length === target.length && timingSafeEqual(actual, target); });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user