Harden archive bot for production; exempt container egress in edge firewall

- archive_sync_state keyed by room per migration 029 (drop synthetic id cursor)
- putStaging buffers the stream and sends content-length (oCIS rejects chunked)
- mimeClass passthrough from sniffed payload
- edge-firewall: RETURN docker-bridge ingress before the CF-range catch-all so
  container egress (Matrix/R2/platform) survives the 80/443 source filter
- files scaffold: keep the ocdav data-gateway hop on-loopback
This commit is contained in:
glass beadian 2026-10-11 16:12:43 +00:00
parent 23bd4dd5ad
commit 2cb77ae6b4
9 changed files with 211 additions and 38 deletions

View file

@ -7,7 +7,7 @@ import type { MatrixEvent, RoomEncryption } from "./events.js";
import { claimKey, replyTxnId } from "./keys.js";
import type { MatrixPort, MediaDownload, SyncResult } from "./matrix.js";
import { ArchiveProcessor } from "./processor.js";
import type { ArchiveRecord, ArchiveStore, OutboxRow } from "./store.js";
import type { ArchiveRecord, ArchiveStore, OutboxRow, RoomCursorTarget } from "./store.js";
import type { LibraryPort, LibraryResource } from "./webdav.js";
const mapping: RoomMapping = {
@ -87,12 +87,14 @@ class FakeLibrary implements LibraryPort {
class FakeStore implements ArchiveStore {
cursor: string | null = null;
roomsSaved = new Set<string>();
outbox = new Map<string, OutboxRow & { claimKey: string }>();
archive: (ArchiveRecord & { available: boolean })[] = [];
async loadCursor(): Promise<string | null> {
async loadCursor(_rooms: readonly RoomCursorTarget[]): Promise<string | null> {
return this.cursor;
}
async saveCursor(_ws: readonly string[], cursor: string): Promise<void> {
async saveCursor(rooms: readonly RoomCursorTarget[], cursor: string): Promise<void> {
for (const room of rooms) this.roomsSaved.add(room.roomId);
this.cursor = cursor;
}
async claim(_ws: string, _room: string, _event: string, key: string): Promise<OutboxRow> {
@ -165,6 +167,7 @@ describe("ArchiveProcessor", () => {
expect(await processor.runBatch(signal)).toEqual({ advanced: true, retryAt: null });
expect(store.cursor).toBe("s1");
expect([...store.roomsSaved]).toEqual([mapping.roomId]);
expect(store.archive).toHaveLength(1);
const row = store.archive[0];
expect(row).toMatchObject({ mime: "image/png", mimeClass: "image", safeName: "cat.png", originalName: "../cat", byteSize: PNG.byteLength });