Phase B: encrypted archive export bundles (age), power-level-gated export command, encryption-state linter
This commit is contained in:
parent
a7ad1f059f
commit
699b9412c1
22 changed files with 2325 additions and 9 deletions
84
apps/archive-bot/src/manifest.test.ts
Normal file
84
apps/archive-bot/src/manifest.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { RoomMapping } from "./config.js";
|
||||
import { buildManifest, MAX_ATTACHMENT_BYTES } from "./exporter.js";
|
||||
import type { IndexedFile } from "./archive-index.js";
|
||||
import { payloadPath } from "./archive-index.js";
|
||||
|
||||
const mapping: RoomMapping = {
|
||||
roomId: "!room:example.org",
|
||||
workspaceId: "11111111-1111-4111-8111-111111111111",
|
||||
groupId: "22222222-2222-4222-8222-222222222222",
|
||||
};
|
||||
|
||||
function row(overrides: Partial<IndexedFile> = {}): IndexedFile {
|
||||
return {
|
||||
id: "0d0e0f10-1111-4222-8222-333333333333",
|
||||
eventId: "$ev1",
|
||||
mxcUri: "mxc://example.org/abc",
|
||||
originalName: "cat photo.png",
|
||||
safeName: "cat_photo.png",
|
||||
mime: "image/png",
|
||||
mimeClass: "image",
|
||||
byteSize: 1234,
|
||||
sha256: "a".repeat(64),
|
||||
sender: "@alice:example.org",
|
||||
createdAt: "2026-01-02T03:04:05.000Z",
|
||||
ocisResourceId: "42",
|
||||
available: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("bundle manifest shape", () => {
|
||||
it("carries every index row with the documented field names", () => {
|
||||
const rows = [row(), row({ eventId: "$ev2", available: false, byteSize: 10, safeName: "gone.bin" })];
|
||||
const manifest = buildManifest(mapping, rows, new Date("2026-02-03T04:05:06.000Z"), ["age1testrecipient"]);
|
||||
expect(manifest).toMatchObject({
|
||||
format: "chat-media-archive-bundle",
|
||||
version: 1,
|
||||
roomId: mapping.roomId,
|
||||
workspaceId: mapping.workspaceId,
|
||||
groupId: mapping.groupId,
|
||||
exportedAt: "2026-02-03T04:05:06.000Z",
|
||||
objectKey: null,
|
||||
recipients: ["age1testrecipient"],
|
||||
});
|
||||
expect(manifest.files).toHaveLength(2);
|
||||
expect(manifest.files[0]).toEqual({
|
||||
eventId: "$ev1",
|
||||
mxcUri: "mxc://example.org/abc",
|
||||
originalName: "cat photo.png",
|
||||
safeName: "cat_photo.png",
|
||||
mime: "image/png",
|
||||
mimeClass: "image",
|
||||
byteSize: 1234,
|
||||
sha256: "a".repeat(64),
|
||||
sender: "@alice:example.org",
|
||||
createdAt: "2026-01-02T03:04:05.000Z",
|
||||
ocisResourceId: "42",
|
||||
available: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("counts only available files in fileCount/totalBytes, but records every row", () => {
|
||||
const manifest = buildManifest(mapping, [
|
||||
row({ byteSize: 100 }),
|
||||
row({ eventId: "$ev2", byteSize: 40, available: false }),
|
||||
], new Date(0), []);
|
||||
expect(manifest.fileCount).toBe(1);
|
||||
expect(manifest.totalBytes).toBe(100);
|
||||
expect(manifest.files).toHaveLength(2);
|
||||
expect(manifest.files.map((f) => f.available)).toEqual([true, false]);
|
||||
});
|
||||
|
||||
it("derives deterministic, collision-free tar payload paths", () => {
|
||||
const one = row({ eventId: "$a", safeName: "x.png" });
|
||||
expect(payloadPath(one)).toBe("files/$a/x.png");
|
||||
// Different events never collide even with identical safe names.
|
||||
expect(payloadPath(row({ eventId: "$b", safeName: "x.png" }))).not.toBe(payloadPath(one));
|
||||
});
|
||||
|
||||
it("documents the per-attachment budget constant", () => {
|
||||
expect(MAX_ATTACHMENT_BYTES).toBe(20 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue