Phase B: encrypted archive export bundles (age), power-level-gated export command, encryption-state linter

This commit is contained in:
glass beadian 2026-10-12 12:46:56 +00:00
parent a7ad1f059f
commit 699b9412c1
22 changed files with 2325 additions and 9 deletions

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { ConfigError, loadConfig, parseRoomMappings, toGroupFileMapping } from "./config.js";
import { ConfigError, loadConfig, parseExportConfig, parseRoomMappings, toGroupFileMapping } from "./config.js";
const WS = "11111111-1111-4111-8111-111111111111";
const GROUP = "22222222-2222-4222-8222-222222222222";
@ -27,8 +27,7 @@ describe("parseRoomMappings", () => {
});
});
describe("loadConfig", () => {
const env = {
const env = {
DATABASE_URL: "postgres://bot@db/hub",
MATRIX_HOMESERVER_URL: "https://matrix.example.org",
MATRIX_USER: "archive-bot",
@ -39,8 +38,10 @@ describe("loadConfig", () => {
FILES_OCID_PASSWORD: "pw2",
ARCHIVE_BOT_HEALTH_PORT: "9100",
MATRIX_MEDIA_REDIRECT_HOSTS: " R2.example.com ,",
};
};
void env;
describe("loadConfig", () => {
it("parses the full env contract", () => {
const config = loadConfig(env);
expect(config.healthPort).toBe(9100);
@ -54,3 +55,51 @@ describe("loadConfig", () => {
expect(() => loadConfig({ ...env, FILES_OCID_WEBDAV_URL: "ftp://x" })).toThrow(/http/);
});
});
describe("parseExportConfig (phase-B encrypted exports)", () => {
const R = "age1" + "q".repeat(58);
const base = {
ARCHIVE_EXPORT_AGE_RECIPIENTS: R,
ARCHIVE_EXPORT_R2_ACCESS_KEY: "k",
ARCHIVE_EXPORT_R2_SECRET_KEY: "s",
};
it("is null when nothing is configured", () => {
expect(parseExportConfig({})).toBeNull();
});
it("is null when explicitly disabled, even with credentials present", () => {
expect(parseExportConfig({ ...base, ARCHIVE_EXPORT_ENABLED: "false" })).toBeNull();
});
it("parses recipients, R2 credentials and defaults", () => {
const cfg = parseExportConfig(base);
expect(cfg).toMatchObject({
enabled: true,
ageRecipients: [R],
every: 500,
r2: { region: "auto", bucket: "hub-matrix-media" },
});
});
it("de-duplicates recipients and honours ARCHIVE_EXPORT_EVERY", () => {
const cfg = parseExportConfig({ ...base, ARCHIVE_EXPORT_AGE_RECIPIENTS: `${R}, ${R}`, ARCHIVE_EXPORT_EVERY: "50" });
expect(cfg?.ageRecipients).toEqual([R]);
expect(cfg?.every).toBe(50);
});
it("rejects malformed recipients and missing credentials", () => {
expect(() => parseExportConfig({ ...base, ARCHIVE_EXPORT_AGE_RECIPIENTS: "AGE-SECRET-KEY-1X" })).toThrow(ConfigError);
expect(() => parseExportConfig({ ARCHIVE_EXPORT_AGE_RECIPIENTS: R })).toThrow(/ACCESS_KEY/);
expect(() => parseExportConfig({ ...base, ARCHIVE_EXPORT_EVERY: "-1" })).toThrow(ConfigError);
expect(() => parseExportConfig({ ...base, ARCHIVE_EXPORT_R2_ENDPOINT: "http://insecure" })).toThrow(ConfigError);
});
it("wires into loadConfig", () => {
const cfg = loadConfig({ ...env, ...base });
expect(cfg.export).not.toBeNull();
expect(cfg.export?.ageRecipients).toEqual([R]);
// and stays null in the default case
expect(loadConfig(env).export).toBeNull();
});
});