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

@ -28,10 +28,20 @@ export interface ArchiveRecord {
createdAt: Date;
}
/**
* Cursor rows are keyed by room (029: archive_sync_state(room_id PRIMARY KEY,
* workspace_id NOT NULL, ...)), so the store needs the room → tenant pin the
* config mappings already carry. RoomMapping satisfies this structurally.
*/
export interface RoomCursorTarget {
roomId: string;
workspaceId: string;
}
/** Durable state the processor depends on; faked in tests. */
export interface ArchiveStore {
loadCursor(workspaceIds: readonly string[]): Promise<string | null>;
saveCursor(workspaceIds: readonly string[], cursor: string): Promise<void>;
loadCursor(rooms: readonly RoomCursorTarget[]): Promise<string | null>;
saveCursor(rooms: readonly RoomCursorTarget[], cursor: string): Promise<void>;
claim(workspaceId: string, roomId: string, eventId: string, claimKey: string): Promise<OutboxRow>;
/** Index row + outbox done in one transaction. Returns the archive row id and indexed time. */
commitArchived(record: ArchiveRecord, outboxId: string): Promise<{ id: string; indexedAt: Date }>;
@ -57,14 +67,11 @@ const OUTBOX_COLUMNS = "id, status, attempts, last_error, updated_at";
const LAST_ERROR_MAX = 1000;
export class PgArchiveStore implements ArchiveStore {
constructor(
private readonly pool: Pool,
private readonly cursorId: string,
) {}
constructor(private readonly pool: Pool) {}
/** Single service connection; tenant context per transaction, like apps/api withTenant. */
static create(databaseUrl: string, cursorId: string): PgArchiveStore {
return new PgArchiveStore(new Pool({ connectionString: databaseUrl, max: 1 }), cursorId);
static create(databaseUrl: string): PgArchiveStore {
return new PgArchiveStore(new Pool({ connectionString: databaseUrl, max: 1 }));
}
async close(): Promise<void> {
@ -72,17 +79,17 @@ export class PgArchiveStore implements ArchiveStore {
}
/**
* The sync stream is global but rows are tenant-scoped, so each mapped
* workspace holds a copy. A partial write leaves copies disagreeing: resume
* from the least recently written (oldest) one — replay is idempotent.
* The sync stream is global but cursor rows are per room, so each mapped room
* holds a copy. A partial write leaves copies disagreeing: resume from the
* least recently written (oldest) one — replay is idempotent.
*/
async loadCursor(workspaceIds: readonly string[]): Promise<string | null> {
async loadCursor(rooms: readonly RoomCursorTarget[]): Promise<string | null> {
let oldest: { cursor: string; at: Date } | null = null;
for (const workspaceId of workspaceIds) {
const row = await this.withTenant(workspaceId, async (c) => {
for (const room of rooms) {
const row = await this.withTenant(room.workspaceId, async (c) => {
const res = await c.query<{ sync_cursor: string | null; updated_at: Date }>(
"SELECT sync_cursor, updated_at FROM archive_sync_state WHERE id = $1 AND workspace_id = $2",
[this.stateId(workspaceId), workspaceId],
"SELECT sync_cursor, updated_at FROM archive_sync_state WHERE room_id = $1 AND workspace_id = $2",
[room.roomId, room.workspaceId],
);
return res.rows[0];
});
@ -92,13 +99,13 @@ export class PgArchiveStore implements ArchiveStore {
return oldest?.cursor ?? null;
}
async saveCursor(workspaceIds: readonly string[], cursor: string): Promise<void> {
for (const workspaceId of workspaceIds) {
await this.withTenant(workspaceId, (c) => c.query(
`INSERT INTO archive_sync_state (id, workspace_id, sync_cursor, updated_at)
async saveCursor(rooms: readonly RoomCursorTarget[], cursor: string): Promise<void> {
for (const room of rooms) {
await this.withTenant(room.workspaceId, (c) => c.query(
`INSERT INTO archive_sync_state (room_id, workspace_id, sync_cursor, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (id) DO UPDATE SET sync_cursor = EXCLUDED.sync_cursor, updated_at = now()`,
[this.stateId(workspaceId), workspaceId, cursor],
ON CONFLICT (room_id) DO UPDATE SET sync_cursor = EXCLUDED.sync_cursor, updated_at = now()`,
[room.roomId, room.workspaceId, cursor],
));
}
}
@ -201,10 +208,6 @@ export class PgArchiveStore implements ArchiveStore {
return total;
}
private stateId(workspaceId: string): string {
return `${this.cursorId}:${workspaceId}`;
}
private async withTenant<T>(workspaceId: string, fn: (client: PoolClient) => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {