Add file handoff audit records

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 21:45:49 -04:00
1 parent babbf07cb2
commit 4dbd8997c0
10 files changed
+298 -100

No files matched your search

@@ -0,0 +1,65 @@
export const fileHandoffRecordsSchema = `
create table if not exists file_handoff_records (
id text primary key,
org_id text not null references organizations(id) on delete cascade,
message_id text not null references chat_messages(id) on delete cascade,
file_id text not null references file_objects(id) on delete cascade,
source_principal_type text not null,
source_principal_id text,
target_principal_type text not null,
target_principal_id text not null,
transfer_type text not null,
checksum text not null,
scan_state text not null,
scan_labels_json text not null,
policy_decision text not null,
object_ref text not null,
created_at text not null,
unique(message_id, file_id, target_principal_type, target_principal_id)
);
insert into file_handoff_records(
id, org_id, message_id, file_id, source_principal_type, source_principal_id,
target_principal_type, target_principal_id, transfer_type, checksum, scan_state,
scan_labels_json, policy_decision, object_ref, created_at
)
select
'fhr_' || lower(hex(randomblob(16))),
messages.org_id,
attachments.message_id,
attachments.file_id,
messages.sender_type,
messages.sender_id,
coalesce(grants.granted_to_type, 'org_group'),
coalesce(grants.granted_to_id, messages.org_id || ':workspace'),
attachments.transfer_type,
attachments.checksum,
files.scan_state,
files.scan_labels_json,
case
when files.scan_state = 'approved' then 'approved_for_handoff'
else 'blocked_by_scan'
end,
files.storage_path,
messages.created_at
from chat_attachments attachments
join chat_messages messages on messages.id = attachments.message_id
join file_objects files on files.id = attachments.file_id
left join file_access_grants grants
on grants.file_id = attachments.file_id
and grants.source_message_id = attachments.message_id
where not exists (
select 1 from file_handoff_records records
where records.message_id = attachments.message_id
and records.file_id = attachments.file_id
and records.target_principal_type = coalesce(grants.granted_to_type, 'org_group')
and records.target_principal_id = coalesce(grants.granted_to_id, messages.org_id || ':workspace')
);
create index if not exists idx_file_handoff_org_created on file_handoff_records(org_id, created_at);
create index if not exists idx_file_handoff_file on file_handoff_records(org_id, file_id);
create index if not exists idx_file_handoff_target on file_handoff_records(org_id, target_principal_type, target_principal_id);
insert into schema_migrations(version, name, applied_at)
values (29, 'file_handoff_records', datetime('now'));
`;
@@ -27,6 +27,7 @@ import { chatMessageDeliverySchema } from "./migrations/025-chat-message-deliver
import { browserPlatformGovernanceSchema } from "./migrations/026-browser-platform-governance";
import { socialAuditRecordsSchema } from "./migrations/027-social-audit-records";
import { migrationCutoverJournalSchema } from "./migrations/028-migration-cutover-journal";
import { fileHandoffRecordsSchema } from "./migrations/029-file-handoff-records";
interface Migration {
version: number;
@@ -63,6 +64,7 @@ const MIGRATIONS: Migration[] = [
{ version: 26, name: "browser_platform_governance", sql: browserPlatformGovernanceSchema },
{ version: 27, name: "social_audit_records", sql: socialAuditRecordsSchema },
{ version: 28, name: "migration_cutover_journal", sql: migrationCutoverJournalSchema },
{ version: 29, name: "file_handoff_records", sql: fileHandoffRecordsSchema },
];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
@@ -4,27 +4,26 @@ import {
CHAT_THREAD_BOARD_STATE,
FILE_SCAN_STATE,
RISK_LEVEL,
type FileScanState,
} from "../../shared/domain";
import { createId } from "../../shared/ids";
import { ApprovalService } from "./approval-service";
import { AuditService } from "./audit-service";
import { BoardInterventionService, parseBoardCommand } from "./board-intervention-service";
import { buildChatMessageStructure } from "./chat-message-structure";
import { recordChatMentions, resolveChatMentions, type MentionTarget, type ResolvedMention } from "./chat-mentions";
import { recordChatMentions, resolveChatMentions, type ResolvedMention } from "./chat-mentions";
import { FileHandoffService, type FileHandoffAttachment } from "./file-handoff-service";
const SPACE_TYPES = new Set<CreateChatSpaceInput["spaceType"]>(["company", "department", "project", "direct"]);
const SPACE_VISIBILITIES = new Set<CreateChatSpaceInput["visibility"]>(["workspace", "restricted"]);
const RETENTION_POLICIES = new Set(["standard_365d", "restricted_365d"]);
interface AttachmentCandidate {
fileId: string;
checksum: string;
}
interface FileAttachmentRow {
id: string;
sha256: string;
scan_state: string;
scan_state: FileScanState;
scan_labels_json: string;
storage_path: string;
owner_type: string;
owner_id: string | null;
}
@@ -52,11 +51,13 @@ export class ChatService {
private readonly audit: AuditService;
private readonly approvals: ApprovalService;
private readonly boardInterventions: BoardInterventionService;
private readonly handoffs: FileHandoffService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
this.approvals = new ApprovalService(db);
this.boardInterventions = new BoardInterventionService(db);
this.handoffs = new FileHandoffService(db);
}
createSpace(input: CreateChatSpaceInput): string {
@@ -141,8 +142,17 @@ export class ChatService {
recordChatMentions(this.db, id, mentions);
const slashResultRef = slash ? this.executeSlashCommand(input, space, sender, mentions, slash, id, threadId, now) : null;
if (slash) this.recordSlashInvocation(id, slash, slashResultRef, now);
this.recordAttachments(id, attachments);
this.recordAttachmentGrants(input, id, threadId, attachments, mentions, now);
this.handoffs.record({
orgId: input.orgId,
spaceId: input.spaceId,
messageId: id,
threadId,
senderType: input.senderType,
senderId: input.senderId,
attachments,
mentions,
now,
});
if (input.senderId) {
const presenceStatus = slash?.command === "escalate" ? "waiting_approval" : "active";
this.db.prepare(`
@@ -229,62 +239,6 @@ export class ChatService {
`).run(createId("slash"), messageId, slash.command, JSON.stringify({ raw: slash.args }), resultRef, now);
}
private recordAttachments(messageId: string, attachments: AttachmentCandidate[]): void {
for (const file of attachments) {
this.db.prepare(`
insert into chat_attachments(message_id, file_id, transfer_type, checksum, preview_key)
values (?, ?, ?, ?, ?)
`).run(messageId, file.fileId, "handoff", file.checksum, null);
}
}
private recordAttachmentGrants(
input: SendMessageInput,
messageId: string,
threadId: string,
attachments: AttachmentCandidate[],
mentions: ResolvedMention[],
now: string,
): void {
if (attachments.length === 0) return;
const recipients = this.attachmentGrantRecipients(input.orgId, input.spaceId, mentions);
const statement = this.db.prepare(`
insert or ignore into file_access_grants(
id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id,
source_message_id, source_thread_id, access_level, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const attachment of attachments) {
for (const recipient of recipients) {
const result = statement.run(
createId("fgr"),
input.orgId,
attachment.fileId,
recipient.type,
recipient.id,
input.senderType,
input.senderId,
messageId,
threadId,
"read",
now,
);
if (result.changes > 0) {
this.audit.record({
orgId: input.orgId,
entityType: "file_object",
entityId: attachment.fileId,
actorType: input.senderType,
actorId: input.senderId,
action: "access_granted",
payload: { messageId, threadId, grantedToType: recipient.type, grantedToId: recipient.id },
});
}
}
}
}
private requireSpace(orgId: string, spaceId: string): SpaceRow {
const space = this.db.prepare("select id, org_id, visibility, space_type from chat_spaces where id = ? and org_id = ?")
.get(spaceId, orgId) as SpaceRow | undefined;
@@ -331,18 +285,24 @@ export class ChatService {
return thread;
}
private requireAttachments(orgId: string, fileIds: string[], sender: PrincipalRef | null): AttachmentCandidate[] {
private requireAttachments(orgId: string, fileIds: string[], sender: PrincipalRef | null): FileHandoffAttachment[] {
if (new Set(fileIds).size !== fileIds.length) throw new Error("Attachment file ids must be unique.");
return fileIds.map((fileId) => {
const file = this.db.prepare(`
select id, sha256, scan_state, owner_type, owner_id
select id, sha256, scan_state, scan_labels_json, storage_path, owner_type, owner_id
from file_objects
where id = ? and org_id = ?
`).get(fileId, orgId) as FileAttachmentRow | undefined;
if (!file) throw new Error("Attachment file was not found.");
if (file.scan_state !== FILE_SCAN_STATE.APPROVED) throw new Error("Attachment file is not approved for handoff.");
if (!this.senderCanReadFile(orgId, file, sender)) throw new Error("Attachment file access is required.");
return { fileId, checksum: file.sha256 };
return {
fileId,
checksum: file.sha256,
scanState: file.scan_state,
scanLabelsJson: file.scan_labels_json,
objectRef: file.storage_path,
};
});
}
@@ -364,35 +324,6 @@ export class ChatService {
return Boolean(grant);
}
private attachmentGrantRecipients(orgId: string, spaceId: string, mentions: ResolvedMention[]): MentionTarget[] {
const source = mentions.length > 0 ? mentions.map((mention) => mention.target) : this.spaceRecipients(spaceId);
const seen = new Set<string>();
const recipients: MentionTarget[] = [];
for (const target of source) {
const key = `${target.type}:${target.id}`;
if (seen.has(key)) continue;
seen.add(key);
recipients.push(target);
}
if (recipients.length === 0) throw new Error("Attachment grant recipient is required.");
return recipients;
}
private spaceRecipients(spaceId: string): MentionTarget[] {
const rows = this.db.prepare(`
select principal_type, principal_id
from chat_memberships
where space_id = ?
order by principal_type asc, principal_id asc
`).all(spaceId) as Array<{ principal_type: string; principal_id: string }>;
return rows.map((row) => {
if (row.principal_type === "employee" || row.principal_type === "agent") {
return { type: row.principal_type, id: row.principal_id };
}
throw new Error("Chat membership principal type is unsupported.");
});
}
private requireMembership(spaceId: string, principal: PrincipalRef): void {
const row = this.db.prepare("select space_id from chat_memberships where space_id = ? and principal_type = ? and principal_id = ?")
.get(spaceId, principal.type, principal.id) as { space_id: string } | undefined;
@@ -0,0 +1,149 @@
import type { DatabaseSync } from "node:sqlite";
import type { SendMessageInput } from "../../shared/commands";
import type { FileScanState } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import type { MentionTarget, ResolvedMention } from "./chat-mentions";
export interface FileHandoffAttachment {
fileId: string;
checksum: string;
scanState: FileScanState;
scanLabelsJson: string;
objectRef: string;
}
interface RecordFileHandoffsInput {
orgId: string;
spaceId: string;
messageId: string;
threadId: string;
senderType: SendMessageInput["senderType"];
senderId: string | null;
attachments: FileHandoffAttachment[];
mentions: ResolvedMention[];
now: string;
}
export class FileHandoffService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
record(input: RecordFileHandoffsInput): void {
if (input.attachments.length === 0) return;
const recipients = this.attachmentGrantRecipients(input.orgId, input.spaceId, input.mentions);
const attachmentStatement = this.db.prepare(`
insert into chat_attachments(message_id, file_id, transfer_type, checksum, preview_key)
values (?, ?, ?, ?, ?)
`);
const grantStatement = this.db.prepare(`
insert or ignore into file_access_grants(
id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id,
source_message_id, source_thread_id, access_level, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const handoffStatement = this.db.prepare(`
insert or ignore into file_handoff_records(
id, org_id, message_id, file_id, source_principal_type, source_principal_id,
target_principal_type, target_principal_id, transfer_type, checksum, scan_state,
scan_labels_json, policy_decision, object_ref, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const attachment of input.attachments) {
attachmentStatement.run(input.messageId, attachment.fileId, "handoff", attachment.checksum, null);
for (const recipient of recipients) {
const grant = grantStatement.run(
createId("fgr"),
input.orgId,
attachment.fileId,
recipient.type,
recipient.id,
input.senderType,
input.senderId,
input.messageId,
input.threadId,
"read",
input.now,
);
handoffStatement.run(
createId("fhr"),
input.orgId,
input.messageId,
attachment.fileId,
input.senderType,
input.senderId,
recipient.type,
recipient.id,
"handoff",
attachment.checksum,
attachment.scanState,
attachment.scanLabelsJson,
"approved_for_handoff",
attachment.objectRef,
input.now,
);
if (grant.changes > 0) this.recordGrantAudit(input, attachment.fileId, recipient);
}
}
}
private recordGrantAudit(input: RecordFileHandoffsInput, fileId: string, recipient: MentionTarget): void {
this.audit.record({
orgId: input.orgId,
entityType: "file_object",
entityId: fileId,
actorType: input.senderType,
actorId: input.senderId,
action: "access_granted",
payload: {
messageId: input.messageId,
threadId: input.threadId,
grantedToType: recipient.type,
grantedToId: recipient.id,
},
});
}
private attachmentGrantRecipients(orgId: string, spaceId: string, mentions: ResolvedMention[]): MentionTarget[] {
const source = mentions.length > 0 ? mentions.map((mention) => mention.target) : this.spaceRecipients(spaceId);
const seen = new Set<string>();
const recipients: MentionTarget[] = [];
for (const target of source) {
const key = `${target.type}:${target.id}`;
if (seen.has(key)) continue;
seen.add(key);
recipients.push(target);
}
if (recipients.length === 0) throw new Error("Attachment grant recipient is required.");
return recipients.map((recipient) => recipient.type === "org_group" ? recipient : this.requireRecipient(orgId, recipient));
}
private requireRecipient(orgId: string, recipient: MentionTarget): MentionTarget {
if (recipient.type === "board") return recipient;
const table = recipient.type === "employee" ? "employees" : "agents";
const row = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`)
.get(recipient.id, orgId) as { id: string } | undefined;
if (!row) throw new Error("Attachment grant recipient was not found.");
return recipient;
}
private spaceRecipients(spaceId: string): MentionTarget[] {
const rows = this.db.prepare(`
select principal_type, principal_id
from chat_memberships
where space_id = ?
order by principal_type asc, principal_id asc
`).all(spaceId) as Array<{ principal_type: string; principal_id: string }>;
return rows.map((row) => {
if (row.principal_type === "employee" || row.principal_type === "agent") {
return { type: row.principal_type, id: row.principal_id };
}
throw new Error("Chat membership principal type is unsupported.");
});
}
}
@@ -63,6 +63,7 @@ const LEGACY_TABLES = [
"skill_catalog_tags",
"file_objects",
"file_access_grants",
"file_handoff_records",
"social_accounts",
"social_account_grants",
"browser_session_containers",
@@ -217,6 +218,9 @@ const RELATION_CHECKS: RelationCheck[] = [
{ table: "file_access_grants", column: "file_id", referencedTable: "file_objects", referencedColumn: "id", reason: "dangling_file_id" },
{ table: "file_access_grants", column: "source_message_id", referencedTable: "chat_messages", referencedColumn: "id", reason: "dangling_source_message_id" },
{ table: "file_access_grants", column: "source_thread_id", referencedTable: "chat_threads", referencedColumn: "id", reason: "dangling_source_thread_id" },
{ table: "file_handoff_records", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
{ table: "file_handoff_records", column: "message_id", referencedTable: "chat_messages", referencedColumn: "id", reason: "dangling_message_id" },
{ table: "file_handoff_records", column: "file_id", referencedTable: "file_objects", referencedColumn: "id", reason: "dangling_file_id" },
{ table: "social_accounts", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
{ table: "social_accounts", column: "authorized_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_authorized_employee_id" },
{ table: "social_account_grants", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
@@ -262,7 +262,7 @@ export class SdlcService {
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
{ key: "provider_rate_limit_registry", passed: rateLimitRegistry === 1, value: rateLimitRegistry },
{ key: "provider_runtime_contract", passed: runtimeContractReady, value: runtimeContractReady },
{ key: "schema_version", passed: schemaVersion >= 28, value: schemaVersion },
{ key: "schema_version", passed: schemaVersion >= 29, value: schemaVersion },
];
}
@@ -103,6 +103,7 @@ export class SnapshotService {
chatReadStates: list(this.db, "chat_read_state", "last_read_at desc"),
files,
fileAccessGrants: list(this.db, "file_access_grants", "created_at desc"),
fileHandoffRecords: list(this.db, "file_handoff_records", "created_at desc"),
scheduledPosts: list(this.db, "scheduled_posts", "scheduled_for asc"),
providers,
providerAuthProfiles,
+19
View File
@@ -287,6 +287,24 @@ export interface FileAccessGrantRecord {
createdAt: string;
}
export interface FileHandoffRecord {
id: string;
orgId: string;
messageId: string;
fileId: string;
sourcePrincipalType: "employee" | "agent" | "board" | "system";
sourcePrincipalId: string | null;
targetPrincipalType: "employee" | "agent" | "board" | "org_group";
targetPrincipalId: string;
transferType: string;
checksum: string;
scanState: FileScanState;
scanLabelsJson: string;
policyDecision: "approved_for_handoff" | "blocked_by_scan";
objectRef: string;
createdAt: string;
}
export interface ScheduledPostRecord {
id: string;
orgId: string;
@@ -432,6 +450,7 @@ export interface AppSnapshot {
chatReadStates: ChatReadStateRecord[];
files: FileObjectRecord[];
fileAccessGrants: FileAccessGrantRecord[];
fileHandoffRecords: FileHandoffRecord[];
scheduledPosts: ScheduledPostRecord[];
providers: ProviderRecord[];
providerAuthProfiles: ProviderAuthProfileRecord[];
@@ -23,7 +23,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]);
connection.close();
});
@@ -208,6 +208,7 @@ describe("domain services", () => {
const chat = new ChatService(connection.db);
const snapshot = new SnapshotService(connection.db);
let orgId = "";
let fileId = "";
connection.transaction(() => {
orgId = organization.createOrganization({
@@ -225,7 +226,7 @@ describe("domain services", () => {
});
const employee = connection.db.prepare("select id from employees where org_id = ?")
.get(orgId) as { id: string };
const fileId = createId("file");
fileId = createId("file");
connection.db.prepare(`
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -249,6 +250,15 @@ describe("domain services", () => {
expect(state.chatAttachments).toHaveLength(1);
expect(state.files[0]?.scanState).toBe("approved");
expect(state.fileAccessGrants[0]).toMatchObject({ grantedToType: "board", grantedToId: orgId, accessLevel: "read" });
expect(state.fileHandoffRecords[0]).toMatchObject({
fileId,
targetPrincipalType: "board",
targetPrincipalId: orgId,
checksum: "8f3a",
scanState: "approved",
policyDecision: "approved_for_handoff",
objectRef: "artifacts/runbook.md",
});
expect(state.chatMentions[0]).toMatchObject({ targetType: "board", targetId: orgId });
expect(state.chatSlashInvocations[0]).toMatchObject({ command: "handoff" });
expect(state.chatPresence).toHaveLength(1);
@@ -362,7 +372,20 @@ describe("domain services", () => {
const state = snapshot.load();
const benGrant = state.fileAccessGrants.find((grant) => grant.fileId === approvedFileId && grant.grantedToId === benId);
if (!benGrant) throw new Error("Agent handoff grant was not created.");
const benHandoff = state.fileHandoffRecords.find((record) => (
record.fileId === approvedFileId && record.sourcePrincipalId === adaId && record.targetPrincipalId === benId
));
if (!benHandoff) throw new Error("Agent handoff record was not created.");
expect(benGrant).toMatchObject({ grantedToType: "agent", accessLevel: "read" });
expect(benHandoff).toMatchObject({
sourcePrincipalType: "agent",
sourcePrincipalId: adaId,
targetPrincipalType: "agent",
checksum: "sha-approved",
objectRef: "artifacts/release-notes.md",
scanState: "approved",
policyDecision: "approved_for_handoff",
});
expect(state.chatMessages).toHaveLength(2);
expect(state.auditEvents.some((event) => event.entityType === "file_object" && event.action === "access_granted")).toBe(true);
connection.close();
@@ -163,6 +163,7 @@ describe("migration service", () => {
create table skill_catalog_tags(skill_id text, tag text, tag_type text);
create table file_objects(id text primary key, org_id text, space_id text);
create table file_access_grants(id text primary key, org_id text, file_id text, source_message_id text, source_thread_id text);
create table file_handoff_records(id text primary key, org_id text, message_id text, file_id text);
create table social_accounts(id text primary key, org_id text, authorized_employee_id text);
create table social_account_grants(id text primary key, org_id text, social_account_id text, employee_id text);
create table browser_session_containers(id text primary key, org_id text, social_account_id text);
@@ -218,6 +219,7 @@ describe("migration service", () => {
insert into chat_memberships(space_id, principal_type, principal_id) values ('space-1', 'employee', 'emp-1');
insert into chat_attachments(message_id, file_id) values ('msg-1', 'file-1');
insert into file_access_grants(id, org_id, file_id, source_message_id, source_thread_id) values ('file-grant-1', 'org-1', 'file-1', 'msg-1', 'thread-1');
insert into file_handoff_records(id, org_id, message_id, file_id) values ('handoff-1', 'org-1', 'msg-1', 'file-1');
insert into chat_mentions(message_id, target_type, target_id) values ('msg-1', 'agent', 'agent-1');
insert into chat_slash_invocations(id, message_id) values ('slash-1', 'msg-1');
insert into board_interventions(id, org_id, thread_id, message_id, approval_id) values ('bdi-1', 'org-1', 'thread-1', 'msg-1', 'approval-1');
@@ -283,6 +285,7 @@ describe("migration service", () => {
expect(body.tableCounts.chat_read_state).toBe(1);
expect(body.tableCounts.board_interventions).toBe(1);
expect(body.tableCounts.file_access_grants).toBe(1);
expect(body.tableCounts.file_handoff_records).toBe(1);
expect(body.tableCounts.provider_requests).toBe(1);
expect(body.tableCounts.provider_usage_windows).toBe(1);
expect(body.tableCounts.provider_rate_limit_observations).toBe(1);
@@ -306,6 +309,7 @@ describe("migration service", () => {
expect(body.idMaps.sdlc_review_runs).toEqual(["review-run-1"]);
expect(body.idMaps.eval_runs).toEqual(["eval-run-1"]);
expect(body.idMaps.board_interventions).toEqual(["bdi-1"]);
expect(body.idMaps.file_handoff_records).toEqual(["handoff-1"]);
expect(body.idMaps.retention_freezes).toEqual(["freeze-1"]);
expect(body.idMaps.audit_replay_runs).toEqual(["replay-1"]);
expect(body.idMaps.rollback_snapshots).toEqual(["rollback-1"]);