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
|
|
@ -1,4 +1,5 @@
|
|||
import { Pool, type PoolClient } from "pg";
|
||||
import type { ArchiveIndexPort, IndexedFile } from "./archive-index.js";
|
||||
|
||||
export type OutboxStatus = "pending" | "done" | "failed";
|
||||
|
||||
|
|
@ -53,6 +54,11 @@ export interface ArchiveStore {
|
|||
*/
|
||||
markRedacted(workspaceId: string, roomId: string, eventId: string, claimKey: string): Promise<number>;
|
||||
countPending(workspaceIds: readonly string[]): Promise<number>;
|
||||
/**
|
||||
* Phase-B export manifest source (ArchiveIndexPort): every archive row for
|
||||
* one room, oldest first. Read-only; no payload bytes leave this interface.
|
||||
*/
|
||||
listRoom(workspaceId: string, roomId: string): Promise<IndexedFile[]>;
|
||||
}
|
||||
|
||||
interface OutboxDbRow {
|
||||
|
|
@ -68,9 +74,44 @@ function toOutbox(row: OutboxDbRow): OutboxRow {
|
|||
}
|
||||
|
||||
const OUTBOX_COLUMNS = "id, status, attempts, last_error, updated_at";
|
||||
|
||||
interface RoomArchiveDbRow {
|
||||
id: string;
|
||||
event_id: string;
|
||||
sender_mxid: string;
|
||||
original_name: string;
|
||||
safe_name: string;
|
||||
mime: string;
|
||||
mime_class: string;
|
||||
byte_size: string | number;
|
||||
sha256: string;
|
||||
ocis_resource_id: string;
|
||||
created_at: Date;
|
||||
available: boolean;
|
||||
/** Extracted from the stored event JSON; NULL for pre-archive rows without one. */
|
||||
mxc_uri: string | null;
|
||||
}
|
||||
|
||||
function toIndexedFile(row: RoomArchiveDbRow): IndexedFile {
|
||||
return {
|
||||
id: row.id,
|
||||
eventId: row.event_id,
|
||||
mxcUri: row.mxc_uri ?? "",
|
||||
originalName: row.original_name,
|
||||
safeName: row.safe_name,
|
||||
mime: row.mime,
|
||||
mimeClass: row.mime_class as IndexedFile["mimeClass"],
|
||||
byteSize: typeof row.byte_size === "string" ? Number(row.byte_size) : row.byte_size,
|
||||
sha256: row.sha256,
|
||||
sender: row.sender_mxid,
|
||||
createdAt: row.created_at.toISOString(),
|
||||
ocisResourceId: row.ocis_resource_id,
|
||||
available: row.available,
|
||||
};
|
||||
}
|
||||
const LAST_ERROR_MAX = 1000;
|
||||
|
||||
export class PgArchiveStore implements ArchiveStore {
|
||||
export class PgArchiveStore implements ArchiveStore, ArchiveIndexPort {
|
||||
constructor(private readonly pool: Pool) {}
|
||||
|
||||
/** Single service connection; tenant context per transaction, like apps/api withTenant. */
|
||||
|
|
@ -222,6 +263,21 @@ export class PgArchiveStore implements ArchiveStore {
|
|||
return total;
|
||||
}
|
||||
|
||||
async listRoom(workspaceId: string, roomId: string): Promise<IndexedFile[]> {
|
||||
return this.withTenant(workspaceId, async (c) => {
|
||||
const res = await c.query<RoomArchiveDbRow>(
|
||||
`SELECT id, event_id, sender_mxid, original_name, safe_name, mime, mime_class,
|
||||
byte_size, sha256, ocis_resource_id, created_at, available,
|
||||
matrix_event_json->'content'->>'url' AS mxc_uri
|
||||
FROM chat_media_archive
|
||||
WHERE workspace_id = $1 AND room_id = $2
|
||||
ORDER BY created_at, id`,
|
||||
[workspaceId, roomId],
|
||||
);
|
||||
return res.rows.map(toIndexedFile);
|
||||
});
|
||||
}
|
||||
|
||||
private async withTenant<T>(workspaceId: string, fn: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue