Phase two: archive origin facet, redaction-aware reads, share revocation on redaction

- group-files: origin filter facet; GET /files/:id returns 410 for unavailable
  rows after the ACL recheck; fix latent group_members.status filter and unbound
  workspace parameter that would 500 listFiles
- archive-bot: markRedacted revokes live group_file_shares rows in-transaction
- new live-DB integration suite for the archive index surface
This commit is contained in:
glass beadian 2026-10-11 21:44:23 +00:00
parent 7cea73dc0d
commit 7ea0b1e137
6 changed files with 375 additions and 22 deletions

View file

@ -46,7 +46,11 @@ export interface ArchiveStore {
/** 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 }>;
recordFailure(workspaceId: string, outboxId: string, error: string, terminal: boolean): Promise<OutboxRow>;
/** Mark index rows unavailable and stop any not-yet-archived claim. Returns rows affected. */
/**
* Redaction/membership-removal path (contract point 5): mark index rows
* unavailable AND revoke every live share link for them in the same
* transaction, so serving stops immediately. Returns rows affected.
*/
markRedacted(workspaceId: string, roomId: string, eventId: string, claimKey: string): Promise<number>;
countPending(workspaceIds: readonly string[]): Promise<number>;
}
@ -182,9 +186,19 @@ export class PgArchiveStore implements ArchiveStore {
return this.withTenant(workspaceId, async (c) => {
const res = await c.query(
`UPDATE chat_media_archive SET available = false
WHERE workspace_id = $1 AND room_id = $2 AND event_id = $3 AND available`,
WHERE workspace_id = $1 AND room_id = $2 AND event_id = $3 AND available
RETURNING id`,
[workspaceId, roomId, eventId],
);
// "revoke links and mark the index unavailable; stop serving immediately":
// every live share of the now-unavailable rows dies in the same tx.
if (res.rows.length > 0) {
await c.query(
`UPDATE group_file_shares SET revoked_at = now()
WHERE item_id = ANY($1::uuid[]) AND workspace_id = $2 AND revoked_at IS NULL`,
[res.rows.map((r: { id: string }) => r.id), workspaceId],
);
}
await c.query(
`UPDATE archive_sync_outbox SET status = 'failed', last_error = 'redacted', updated_at = now()
WHERE workspace_id = $1 AND claim_key = $2 AND status = 'pending'`,