feat: implement sensitive operation audit retention (TASK-WP6-04)
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import type BetterSqlite3 from "better-sqlite3";
|
||||
|
||||
import type {
|
||||
AdminAuditQuery,
|
||||
AdminOperationAuditItem,
|
||||
AdminOperationAuditResponse,
|
||||
PrivateContentAccessAuditItem,
|
||||
PrivateContentAccessAuditResponse,
|
||||
} from "@dada/shared-contracts";
|
||||
|
||||
interface AuditCursor {
|
||||
logId: string;
|
||||
occurredAt: number;
|
||||
}
|
||||
|
||||
interface AdminOperationRow {
|
||||
actor_ref: string;
|
||||
actor_type: "system" | "super_admin";
|
||||
after_summary: string | null;
|
||||
before_summary: string | null;
|
||||
expires_at: number;
|
||||
log_id: string;
|
||||
occurred_at: number;
|
||||
operation_type: string;
|
||||
result: "failed" | "succeeded";
|
||||
target_ref: string;
|
||||
target_type: string;
|
||||
}
|
||||
|
||||
interface PrivateContentAccessRow {
|
||||
actor_ref: string;
|
||||
content_type: "image" | "prompt";
|
||||
expires_at: number;
|
||||
log_id: string;
|
||||
occurred_at: number;
|
||||
target_ref: string;
|
||||
}
|
||||
|
||||
export class AdminAuditQueryError extends Error {
|
||||
constructor() {
|
||||
super("admin_audit_query_invalid");
|
||||
this.name = "AdminAuditQueryError";
|
||||
}
|
||||
}
|
||||
|
||||
function encodeCursor(row: { log_id: string; occurred_at: number }) {
|
||||
return Buffer.from(JSON.stringify([row.occurred_at, row.log_id]), "utf8").toString("base64url");
|
||||
}
|
||||
|
||||
function decodeCursor(cursor: string | undefined): AuditCursor | undefined {
|
||||
if (!cursor) return undefined;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
||||
if (!Array.isArray(parsed) || parsed.length !== 2 || !Number.isSafeInteger(parsed[0])
|
||||
|| typeof parsed[1] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(parsed[1])) {
|
||||
throw new AdminAuditQueryError();
|
||||
}
|
||||
return { occurredAt: parsed[0] as number, logId: parsed[1] };
|
||||
} catch (error) {
|
||||
if (error instanceof AdminAuditQueryError) throw error;
|
||||
throw new AdminAuditQueryError();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLimit(limit: number | undefined) {
|
||||
if (limit === undefined) return 50;
|
||||
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new AdminAuditQueryError();
|
||||
return limit;
|
||||
}
|
||||
|
||||
function pageRows<Row extends { log_id: string; occurred_at: number }>(rows: Row[], limit: number) {
|
||||
const hasMore = rows.length > limit;
|
||||
const items = hasMore ? rows.slice(0, limit) : rows;
|
||||
return { items, nextCursor: hasMore ? encodeCursor(items[items.length - 1]!) : null };
|
||||
}
|
||||
|
||||
function iso(value: number) {
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
|
||||
export function listAdminOperationAudit(
|
||||
database: BetterSqlite3.Database,
|
||||
query: AdminAuditQuery,
|
||||
clock: () => number = Date.now,
|
||||
): AdminOperationAuditResponse {
|
||||
const cursor = decodeCursor(query.cursor);
|
||||
const limit = normalizeLimit(query.limit);
|
||||
const rows = (cursor
|
||||
? database.prepare(`
|
||||
SELECT actor_ref, actor_type, after_summary, before_summary, expires_at, log_id,
|
||||
occurred_at, operation_type, result, target_ref, target_type
|
||||
FROM admin_operation_logs
|
||||
WHERE occurred_at < ? OR (occurred_at = ? AND log_id < ?)
|
||||
ORDER BY occurred_at DESC, log_id DESC LIMIT ?
|
||||
`).all(cursor.occurredAt, cursor.occurredAt, cursor.logId, limit + 1)
|
||||
: database.prepare(`
|
||||
SELECT actor_ref, actor_type, after_summary, before_summary, expires_at, log_id,
|
||||
occurred_at, operation_type, result, target_ref, target_type
|
||||
FROM admin_operation_logs
|
||||
ORDER BY occurred_at DESC, log_id DESC LIMIT ?
|
||||
`).all(limit + 1)) as AdminOperationRow[];
|
||||
const page = pageRows(rows, limit);
|
||||
const items: AdminOperationAuditItem[] = page.items.map((row) => ({
|
||||
actor_ref: row.actor_ref,
|
||||
actor_type: row.actor_type,
|
||||
after_summary: row.after_summary,
|
||||
before_summary: row.before_summary,
|
||||
expires_at: iso(row.expires_at),
|
||||
log_id: row.log_id,
|
||||
occurred_at: iso(row.occurred_at),
|
||||
operation_type: row.operation_type,
|
||||
result: row.result,
|
||||
target_ref: row.target_ref,
|
||||
target_type: row.target_type,
|
||||
}));
|
||||
return { generated_at: iso(clock()), items, next_cursor: page.nextCursor };
|
||||
}
|
||||
|
||||
export function listPrivateContentAccessAudit(
|
||||
database: BetterSqlite3.Database,
|
||||
query: AdminAuditQuery,
|
||||
clock: () => number = Date.now,
|
||||
): PrivateContentAccessAuditResponse {
|
||||
const cursor = decodeCursor(query.cursor);
|
||||
const limit = normalizeLimit(query.limit);
|
||||
const rows = (cursor
|
||||
? database.prepare(`
|
||||
SELECT actor_ref, content_type, expires_at, log_id, occurred_at, target_ref
|
||||
FROM private_content_access_logs
|
||||
WHERE occurred_at < ? OR (occurred_at = ? AND log_id < ?)
|
||||
ORDER BY occurred_at DESC, log_id DESC LIMIT ?
|
||||
`).all(cursor.occurredAt, cursor.occurredAt, cursor.logId, limit + 1)
|
||||
: database.prepare(`
|
||||
SELECT actor_ref, content_type, expires_at, log_id, occurred_at, target_ref
|
||||
FROM private_content_access_logs
|
||||
ORDER BY occurred_at DESC, log_id DESC LIMIT ?
|
||||
`).all(limit + 1)) as PrivateContentAccessRow[];
|
||||
const page = pageRows(rows, limit);
|
||||
const items: PrivateContentAccessAuditItem[] = page.items.map((row) => ({
|
||||
actor_ref: row.actor_ref,
|
||||
content_type: row.content_type,
|
||||
expires_at: iso(row.expires_at),
|
||||
log_id: row.log_id,
|
||||
occurred_at: iso(row.occurred_at),
|
||||
target_ref: row.target_ref,
|
||||
}));
|
||||
return { generated_at: iso(clock()), items, next_cursor: page.nextCursor };
|
||||
}
|
||||
Reference in New Issue
Block a user