Ctrl K
ComunidadGuías…

server/boat-deletion.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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 });
}