Skip to content

feat: add data export, account deactivation, and deletion lifecycle API with cooling-off, anonymization, and audited idempotent jobs - #105

Merged
3m1n3nc3 merged 1 commit into
learnault:mainfrom
simplicityf:feat/data-export
Jul 20, 2026
Merged

feat: add data export, account deactivation, and deletion lifecycle API with cooling-off, anonymization, and audited idempotent jobs#105
3m1n3nc3 merged 1 commit into
learnault:mainfrom
simplicityf:feat/data-export

Conversation

@simplicityf

Copy link
Copy Markdown
Contributor

Related Issue

Closes #88

Summary

Implements the Data Export, Deactivation and Deletion API (API Roadmap Phase 1)
as a new privacy/account-lifecycle domain at /api/v1/account.

Endpoint Behavior
POST /export 202; async generation, one active request per user (409 with existingRequestId)
GET /export/:id Owner-scoped status; other users' ids → 404 (no existence leak)
GET /export/:id/download JSON attachment; 409 not ready, 410 expired/purged/failed
POST /deactivate Step-up (password re-entry); revokes sessions; 409 if not active
POST /reactivate Public + credentialed + rate-limited; 409 if pending deletion
POST /deletion Step-up; 202 with scheduledFor = now + cooling-off (default 30d)
GET /deletion Latest request status
POST /deletion/cancel Public + credentialed; 410 if already finalized

Reactivate/cancel are public-with-credentials because deactivation and deletion
requests revoke sessions and block login, a JWT-gated endpoint would be
unreachable. Both use authLimiter and neutral 401s to prevent account-state
enumeration.

Deletion/anonymization matrix

Applied by the finalization job in a single transaction (all operations
idempotent, safe to re-run):

Data Action
Sessions, verification tokens, device tokens, sync events, notification logs/prefs, email deliveries, completions, referral code, export artifacts Hard delete
Transactions, referrals, credentials (on-chain), stellar fundings Retained — financial/on-chain records with no in-row PII; FKs point at the tombstone
Audit logs Retained; ip/userAgent/metadata scrubbed
User row Anonymized in place (deleted+<id>@anon.invalid, random unloginable password, wallet nulled, status DELETED) — preserves FK integrity for retained records

Jobs & concurrency

  • Reuses the repo's outbox pattern (status/attemptCount/nextAttemptAt,
    exponential backoff, dead-letter at max attempts).
  • Claim-based processing: rows are claimed via status-guarded updateMany;
    count === 0 means another runner won concurrent sweeps and crash
    re-runs are safe. Cancel-vs-finalize races resolve the same way.
  • DB-level guarantee: partial unique indexes allow at most one active
    export/deletion request per user.
  • No dedicated worker exists yet: the sweep runs lazily from account
    endpoints, optionally on LIFECYCLE_SWEEP_INTERVAL_MS, and sweep() is
    exported for the future WORKER_SCRIPT.

Known limitation

JWTs are stateless: tokens issued before deactivation remain valid (up to
JWT_EXPIRES_IN, 1d) on routes that don't yet use requireActiveAccount.
This PR applies it to the mutating lifecycle endpoints; adopting it across the
other v1 routers is a recommended follow-up.

Verification evidence

  • Lifecycle tests: 58 new tests (363 total passing) -> cross-user access
    scoping, duplicate requests, unique-index races, claim races, expiry/410,
    job failure → backoff → dead-letter, cancel-after-finalize.
  • Redacted export manifest: tests/data-export.service.test.ts asserts the
    generated artifact contains no password, token, tokenHash, or
    refreshToken fields; sessions export metadata only.
  • Anonymization verification: tests/account-lifecycle.service.test.ts
    asserts every matrix row — hard-deletes issued for all PII tables, no
    deletes for retained financial/on-chain tables, audit-log scrub payload,
    and the exact tombstone shape of the user update.
  • Lint, tsc, prisma validate, and the Swagger spec (all 7 paths + new
    component schemas) verified.

Screenshot

0

…PI with cooling-off, anonymization, and audited idempotent jobs
@3m1n3nc3
3m1n3nc3 merged commit ca089c0 into learnault:main Jul 20, 2026
1 check passed
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.

Feature: Add Data Export Deactivation and Deletion API

2 participants