server/boat-deletion.ts
Rama main · réplica de lectura del repositorio original.
// Boat's deletion operation: DELETE /sandboxes/{id} answers 202 with one, polled at GET /deletion-operations/{id}
// (https://docs.boat.dev/data-retention/delete, https://docs.boat.dev/api/reference/account/get-deletion-operation).
// The sandbox leaves every list, restore and fork at once, a delete can never be cancelled, and Boat retries the purge
// itself, so `blocked` is not a failure: it says what the background erase waits for.
const OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
const STATUSES = new Set(["pending", "processing", "blocked", "completed"]);
/** Boat finishes these on its own: at `expectedBy` once the last upload link expires (≤ 6 h after the last upload), after a
* restore that reads the data ends, or on Boat's own retry. */
const SELF_FINISHING_STAGES = new Set(["removing", "waiting_for_uploads", "waiting_for_restore", "retrying", "completed"]);
export interface BoatDeletionOperation {
id: string;
kind: "sandbox";
targetId: string;
status: "pending" | "processing" | "blocked" | "completed";
/** removing, waiting_for_uploads, kept_for_newer_snapshots, waiting_for_restore, retrying, completed */
stage: string | null;
}
/** Only the immutable identity fields; a malformed success envelope falls back to a direct read, never journal retirement. */
export function boatDeletionOperation(
body: any,
boxId: string,
expectedOperationId?: string,
): BoatDeletionOperation | null {
const operation = body?.operation;
const id = typeof operation?.id === "string" ? operation.id : "";
const status = typeof operation?.status === "string" ? operation.status : "";
if (
!OPERATION_ID.test(id)
|| (expectedOperationId !== undefined && id !== expectedOperationId)
|| operation?.kind !== "sandbox"
|| operation?.targetId !== boxId
|| !STATUSES.has(status)
) return null;
const stage = typeof operation.stage === "string" ? operation.stage : null;
return { id, kind: "sandbox", targetId: boxId, status: status as BoatDeletionOperation["status"], stage };
}
/** Blocked only until Boat finishes on its own: OpenMausBot's delete is done. */
export const blockedUntilBoatFinishes = (operation: BoatDeletionOperation) =>
operation.status === "blocked" && SELF_FINISHING_STAGES.has(operation.stage ?? "");
/** Blocked until the person deletes a fork or named snapshot that still builds on the data. */
export const keptForNewerSnapshots = (operation: BoatDeletionOperation) =>
operation.status === "blocked" && operation.stage === "kept_for_newer_snapshots";
/** Never "retry": Boat accepted the delete and retries the purge itself. */
export function deletionBlockedError(boxId: string, kept: boolean): Error & { status: number } {
return Object.assign(new Error(kept
? `boat.dev deleted cloud computer ${boxId} but is keeping its stored data, because a fork or named snapshot made from it still uses that data. To erase the data, delete those in boat.dev.`
: `boat.dev accepted the deletion of cloud computer ${boxId}, but says it is blocked without a reason OpenMausBot knows. boat.dev keeps retrying on its own; if ${boxId} is still listed in boat.dev later, delete it there.`,
), { status: 409 });
}