Skip to content

feat(purge): exécuteur réel de purge de compte (§16.12) — worker BullMQ + preuve d'effacement auditée - #43

Open
openaxcloud wants to merge 1 commit into
mainfrom
feat/account-deletion-purge-worker
Open

feat(purge): exécuteur réel de purge de compte (§16.12) — worker BullMQ + preuve d'effacement auditée#43
openaxcloud wants to merge 1 commit into
mainfrom
feat/account-deletion-purge-worker

Conversation

@openaxcloud

Copy link
Copy Markdown
Owner

Invariant §16.12 — l'exécuteur de purge manquant (gap HAUTE)

SECURITY_PRIVACY_COMPLIANCE.md v3 (§Rétention, ligne « Comptes & profils ») déclarait le gap HAUTE : la machine d'états de suppression self-serve (data-deletion.ts : request → grâce 14 j → ready_to_purge) et la file admin /admin/account-deletions existaient, mais aucun worker ne consommait ready_to_purgepurgedAt n'était jamais écrit hors des specs, la purge n'était JAMAIS exécutée en réel.

Invariant §16.12 : toute suppression = tombstone → fenêtre de récupération → purge réellePREUVE d'effacement. Cette PR livre l'exécuteur.

Ce que fait la purge (store.purgeUserAccount)

Une transaction Postgres par utilisateur, ouverte par pg_advisory_xact_lock('account-purge:<userId>') :

Action Classes
Supprimées (recomptées à 0) sessions, tokens (email/reset/MFA), clés API, comptes connectés, historique IA (conversations+messages+tool calls), collaboration, projets+imports des orgs à membre unique, memberships, newsletter
Anonymisées (jamais supprimées) AuditLog/AdminAuditLog rédigés (redactAuditLogs-style : ipAddress→NULL, metadata→{redacted:true}), références détachées (UsageEvent, AgentCallLog, LedgerReservation, AgentCheckpoint, ProjectActivity, ImportJob, GalleryListing, SupportTicket), org shells, tombstone User portant purgedAt
Conservées fail-closed, CONSIGNÉES financier < 7 ans (canPurgeFinancialRecord ; les lignes > 2555 j sont effacées), ledger 0078 (triggers d'immutabilité — jamais de DELETE, compté+consigné), contenu des orgs partagées
  • Idempotent : relance sur un compte purgé = already_purged, no-op prouvé, 1 seule preuve.
  • Concurrence : 2 workers sur la même demande → 1 seule purge (verrou advisory ; prouvé avec 2 clients Prisma indépendants).
  • Fail-closed : recomptage post-purge par classe ; toute ligne restante ⇒ rollback complet (une purge partielle ne peut jamais être stampée purgedAt).
  • Preuve persistée : objet structuré par classe (supprimées/anonymisées/conservées+motif, remainingAfterPurge, verifiedZeroRemaining) écrit dans l'AdminAuditLog (account.purge_completed) AVANT de sortir l'id de la file — un effacement ne peut pas être à la fois délisté et non prouvé.

Câblage worker (patron du repo)

  • Route interne POST /internal/account-purge (requireInternalSecret, DRY-RUN par défaut — armement via ACCOUNT_PURGE_ENABLED=true ou body.enabled — miroir exact d'/internal/inactivity-gc).
  • Job BullMQ account.purge (queue enterprise-jobs, triggerAccountPurge) + CronJob Helm accountPurge (30 4 * * *) — rendu helm template vérifié.

Tests (13 nouveaux ; négatifs d'abord)

  • account-purge-routes.spec.ts (9) : 401 sans secret ; fenêtre non échue → refus + données intactes ; annulation pendant la grâce → jamais purgé ; dry-run par défaut ; double exécution → no-op ; course → 1 purge ; rétention financière consignée ; purge complète (0 ligne/classe, tombstone, session morte → 401) ; org partagée conservée+consignée.
  • account-purge-db.spec.ts (4, gaté DATABASE_URL, tourne en CI) : preuves DURABLES vrai Postgres.
  • Suite api complète : 159 fichiers / 1303 tests verts avec DB réelle.

Preuve PG réelle (docker pgvector/pgvector:pg16 jetable)

Harness sur PrismaApiStore : compte semé multi-classes (projet, session, import, conversation IA, usage, audit) → suppression demandée → requestedAt reculé DANS LA DB (jamais l'horloge) → route worker exécutée → vérifs SQL « 0 ligne » par classepreuve relue depuis AdminAuditLog (verifiedZeroRemaining=true, 17 classes, 3 exceptions consignées) → re-run no-op → trigger 0078 refuse le DELETE ledger (append-only). Journal + logs + preuve JSON : docs/deploy-evidence/2026-07-22-account-purge-worker/. Conteneur détruit après.

Fixes CI rejoués (inclus dans le commit — hérités de main)

Les 3 casse-CI connus de main, identiques aux fixes prouvés sur zone/cloud-tenant-factory-iam (4abc301, 147a622) : admin-model.test.ts 31→32 sections + assertion agent-routing ; commentaire values-prod.yaml « fake URL-copy rollback » → « URL-copy pseudo-rollback » ; vitest-config-discovery.spec.ts execFile promisifié (timeout 180 s).

Écarts / notes honnêtes

  • AiTokenUsage (compteur par message) cascade avec les messages par design du schéma — consigné dans la preuve ; la vérité de facturation (AiCostLedger/UsageEvent/Ledger) est org-scopée et retenue.
  • La purge PVC/GCS (gap MOYENNE distinct de la table de rétention) n'est pas dans ce périmètre : la suppression Project ici est la cascade DB ; le nettoyage stockage physique reste tracé comme gap séparé.
  • Prod : le cron est livré dry-run (ACCOUNT_PURGE_ENABLED non armé) — l'armement est une décision d'exploitation d'Avi.

PROVEN_REVIEW_PENDING — pas de merge sans feu vert d'Avi.

🤖 Generated with Claude Code

…MQ + preuve d'effacement auditée

Comble le gap HAUTE de SECURITY_PRIVACY_COMPLIANCE v3 (§Rétention) : la
machine request→grâce 14 j→ready_to_purge existait mais RIEN ne consommait
ready_to_purge — purgedAt n'était jamais écrit hors des specs.

- account-purge.ts (pur) : preuve d'effacement structurée par classe
  (supprimées/anonymisées/conservées+motif, vérif « 0 ligne restante »).
- store.purgeUserAccount (prisma + in-memory) : purge réelle classe par
  classe dans UNE transaction pg_advisory_xact_lock par user — idempotente,
  sûre en concurrence (2 workers → 1 purge) ; rétention financière 7 ans
  fail-closed (canPurgeFinancialRecord) ; audit logs RÉDIGÉS jamais
  supprimés ; ledger 0078 immuable respecté (retenu + consigné) ; tombstone
  User anonymisé portant purgedAt ; recomptage post-purge par classe,
  toute ligne restante ⇒ rollback complet.
- POST /internal/account-purge (secret interne, DRY-RUN par défaut,
  ACCOUNT_PURGE_ENABLED=true pour armer) : consomme la file
  account.pendingDeletionUserIds, persiste la preuve dans l'AdminAuditLog
  (account.purge_completed) AVANT de sortir l'id de la file.
- worker : job account.purge (enterprise-jobs) + CronJob Helm accountPurge
  (30 4 * * *) — même patron que inactivity.gc.
- tests : 9 route-level négatifs d'abord (fenêtre non échue, annulation,
  dry-run, double exécution, course, rétention consignée, purge complète,
  org partagée) + 4 preuves DURABLES vrai Postgres (SQL 0-ligne, preuve
  relue de la DB, 2 clients en course, trigger ledger refuse DELETE).
- preuve PG jouée sur pgvector:pg16 jetable : docs/deploy-evidence/
  2026-07-22-account-purge-worker/ (journal + logs + preuve JSON).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 06:34
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Preview deployment not configured

Name Info
Latest commit e92631e
Status Preview deployment requires Cloudflare secrets

To enable preview deployments, repository maintainers can add:

  • CLOUDFLARE_API_TOKEN secret
  • CLOUDFLARE_ACCOUNT_ID secret

Built with E-Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e549d9266e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +655 to +660
const retainedFinancial = {
UsageEvent: soleOrgIds.length > 0 ? await tx.usageEvent.count({ where: soleOrgWhere }) : 0,
AiCostLedger: soleOrgIds.length > 0 ? await tx.aiCostLedger.count({ where: soleOrgWhere }) : 0,
CreditLedger: soleOrgIds.length > 0 ? await tx.creditLedger.count({ where: soleOrgWhere }) : 0,
StripeEvent: soleOrgIds.length > 0 ? await tx.stripeEvent.count({ where: soleOrgWhere }) : 0,
Subscription: soleOrgIds.length > 0 ? await tx.subscription.count({ where: soleOrgWhere }) : 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel active subscriptions before purging sole owners

When a user is the sole member of an organization with an active paid subscription, this code only counts and retains the Subscription row; neither the deletion request route nor this purge path cancels the external Stripe subscription or changes its status. The purge then removes all sessions and memberships, so the customer can continue being renewed after losing access to the account. Cancel or otherwise terminate active billing before committing the tombstone while retaining the required financial history separately.

Useful? React with 👍 / 👎.

Comment thread services/api/src/app.ts
Comment on lines +26991 to +26994
if (status === 'purged') {
alreadyPurged += 1;
await store.mutateSystemSettingIds(ACCOUNT_DELETION_PENDING_KEY, { remove: userId });
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist the erasure proof atomically with the purge

If purgeUserAccount commits but the subsequent recordAdminAudit call fails transiently, the tombstone already contains purgedAt while no proof was persisted. The next sweep enters this status === 'purged' branch and removes the pending ID without recreating the proof, making the promised erasure evidence permanently unrecoverable. The proof and tombstone need one transaction, or this branch must verify/recover the proof before dequeuing.

Useful? React with 👍 / 👎.

Comment on lines +449 to +451
for (const orgId of orgIds) {
const members = await tx.organizationMember.count({ where: { organizationId: orgId } });
(members === 1 ? soleOrgIds : sharedOrgIds).push(orgId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Lock organization membership before classifying sole-owner data

When an invitation is accepted or a member is added after this count but before the project deletion, the organization is classified as sole-member from a stale snapshot and all of its projects are deleted even though it has become shared. Membership mutation routes serialize on org-members:<orgId>, but the purge only holds account-purge:<userId> and never acquires that lock. Acquire the same per-organization lock before counting and keep it through deletion to prevent this data-loss race.

Useful? React with 👍 / 👎.

Comment on lines +566 to +568
// Marketing: unsubscribe by e-mail BEFORE the tombstone rewrites it.
const newsletter = await tx.newsletterSubscriber.deleteMany({ where: { email: user.email } });
classes.push({ dataClass: 'marketing', action: 'deleted', models: { NewsletterSubscriber: newsletter.count } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Erase transactional email delivery records

For users who received verification, reset, or other transactional mail, deleting only the newsletter row leaves EmailDeliveryEvent records untouched. That model stores the original address and full provider payload without a User foreign key (packages/database/prisma/schema.prisma:1842-1852), so the account can be stamped purged with verifiedZeroRemaining: true while directly identifying data remains. Delete or anonymize events selected by user.email and include them in the post-purge proof.

Useful? React with 👍 / 👎.

Comment on lines +575 to +578
const adminAuditRedacted = await tx.adminAuditLog.updateMany({
where: { actorUserId: userId },
data: { ipAddress: null },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact admin audit metadata during anonymization

When the purged user has acted as a platform administrator, this update clears only ipAddress; AdminAuditLog.metadata remains intact even though the proof reports the entire audit_logs class as anonymized. Admin events can contain free-form reasons and other user-associated details, so this contradicts the redaction applied to ordinary AuditLog rows immediately above. Replace the metadata with the same redaction marker or explicitly consign it as retained.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Cette PR comble le gap §16.12 en ajoutant un exécuteur réel de purge de compte (consommation de ready_to_purge), orchestré via BullMQ + CronJob Helm, avec génération d’une preuve d’effacement structurée persistée en audit, et une suite de tests (in-memory + Postgres réel) pour prouver l’idempotence, la concurrence et la rétention fail-closed.

Changes:

  • Ajout de store.purgeUserAccount (impl Prisma + miroir TestApiStore) + types/helpers de preuve d’effacement (account-purge.ts).
  • Ajout de la route interne POST /internal/account-purge + câblage worker account.purge et CronJob Helm.
  • Ajout/ajustement de tests (routes, DB durable, régression vitest discovery) + pièces de preuve de déploiement.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
services/worker/src/index.ts Ajoute le trigger account.purge côté worker (appel /internal/account-purge).
services/api/src/app.ts Ajoute la route interne /internal/account-purge (scan + purge + audit proof + dequeue).
services/api/src/store.ts Étend l’interface store avec purgeUserAccount.
services/api/src/prisma-store.ts Implémente la purge transactionnelle Postgres + preuve d’effacement + vérifs post-purge.
services/api/src/account-purge.ts Définit les types de preuve + helpers IO-free (buildErasureProof, tombstones).
services/api/src/tests/test-api-store.ts Ajoute un miroir in-memory de purgeUserAccount pour tests route/unit.
services/api/src/tests/account-purge-routes.spec.ts Ajoute tests E2E route interne (négatifs d’abord, puis preuve complète).
services/api/src/tests/account-purge-db.spec.ts Ajoute tests durables sur Postgres réel (idempotence, concurrence, ledger immuable).
services/api/src/tests/vitest-config-discovery.spec.ts Rend le test de discovery vitest non-bloquant (exec async) + timeout.
infra/helm/platform/templates/cronjobs.yaml Ajoute le CronJob accountPurge (enqueue account.purge).
infra/helm/platform/values-prod.yaml Ajuste un commentaire (pseudo-rollback).
apps/admin/src/admin-model.test.ts Met à jour l’attendu sur le nombre de sections admin (+ agent-routing).
docs/deploy-evidence/2026-07-22-account-purge-worker/README.md Documentation de la preuve et du runbook de repro.
docs/deploy-evidence/2026-07-22-account-purge-worker/JOURNAL.md Journal des commandes de preuve Postgres/Helm.
docs/deploy-evidence/2026-07-22-account-purge-worker/proof-sample.json Exemple de preuve (actuellement invalide tel que commité).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +5
{ +
"kind": "account-erasure-proof", +
"userId": "cmrvpdz4t000fi0lnerebnm5i", +
"classes": [ +
{ +
Comment thread services/api/src/app.ts
Comment on lines +26991 to +26995
if (status === 'purged') {
alreadyPurged += 1;
await store.mutateSystemSettingIds(ACCOUNT_DELETION_PENDING_KEY, { remove: userId });
continue;
}
Comment on lines +444 to +452
const memberships = await tx.organizationMember.findMany({ where: { userId }, select: { organizationId: true } });
const orgIds = [...new Set(memberships.map((m) => m.organizationId))];
const soleOrgIds: string[] = [];
const sharedOrgIds: string[] = [];

for (const orgId of orgIds) {
const members = await tx.organizationMember.count({ where: { organizationId: orgId } });
(members === 1 ? soleOrgIds : sharedOrgIds).push(orgId);
}
Comment on lines +575 to +578
const adminAuditRedacted = await tx.adminAuditLog.updateMany({
where: { actorUserId: userId },
data: { ipAddress: null },
});
Comment on lines +459 to +464
for (const event of this.adminAuditLogs) {
if (event.actorUserId === userId && event.ipAddress !== undefined) {
event.ipAddress = undefined;
adminAuditRedacted += 1;
}
}
openaxcloud pushed a commit that referenced this pull request Jul 22, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Jul 22, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Jul 31, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Jul 31, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 3, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 3, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 4, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
… workspace PVCs)

Closes the physical half of the §16.12 deletion gap left by PR #43 (which erases
DB rows only). eraseProjectsStorage() erases a purged account's per-project
object-storage buckets and workspace volumes with the same evidence discipline
as the DB purge — list BEFORE → delete → re-count AFTER — and emits
PurgeClassReport{object_storage,workspace_volumes} so it folds straight into
ErasureProof.verifiedZeroRemaining.

FAIL-CLOSED: any bucket/volume that doesn't re-count to 0 (or whose delete
threw) leaves remainingAfterPurge > 0. Idempotent (missing bucket/workspace =
verified no-op) so the worker can retry. All I/O behind injected ports →
unit-testable + replayable against a throwaway bucket/volume, never prod data.

6 tests: happy-path evidence, fail-closed (bucket won't delete / workspace throws),
idempotent no-op, empty-account vacuous verify, multi-project aggregation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openaxcloud pushed a commit that referenced this pull request Aug 5, 2026
…volume)

A tamper-evident, reproducible proof of physical erasure — mirrors PR #43's DB
evidence discipline for storage.

- scripts/physical-purge-proof.ts: runs the REAL eraseProjectsStorage against a
  throwaway test bucket + workspace volume (never prod data) — seeds objects,
  lists BEFORE, erases, re-counts AFTER (0), folds into an ErasureProof, and
  emits canonical proof.json + its SHA-256. Deterministic (fixed ids/timestamps).
- docs/deploy-evidence/2026-07-22-account-physical-purge/{README,proof.json,
  SHA256SUMS}: the committed artifact (5 objects → 0, 1 workspace → 0, verified).
  JSON, not an ignored .log.
- src/tests/physical-purge-proof.spec.ts: CI replays it and asserts the committed
  artifact reproduces byte-for-byte, so the hashed proof can't drift from code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants