# Journey: Account deletion and DSAR

This journey joins two shipped customer surfaces and the operator review path.
`/profile/memory` manages Iris memory, produces a downloadable local bundle, and
records a BFF export receipt when Iris is available. The data-rights surface at
`/profile?path=data-rights` and its read-only `/profile/data` summary create and
display BFF-backed export and deletion requests. The wider six-domain tombstone
fan-out remains only partially implemented. The walk therefore distinguishes a
local memory clear, a scoped data-rights deletion, and the still-open
cross-service erasure promise instead of treating them as one event.

## Personas

- **Signed-in memory customer** exporting, pausing, or clearing Iris memory at
  `/profile/memory`
- **Signed-in data-rights customer** creating a scoped export or deletion from
  `/profile?path=data-rights` and reviewing it at `/profile/data`
- **DSAR operator** in `/operator/admin` triaging the case and attaching
  per-tombstone attestation
- **Legal operator** taking the `dsar-fulfillment` admin-inspection role for
  sensitive-category reads required during fulfillment
- **Out-of-band data subject** — a target persona for email-link or
  operator-signoff verification; no anonymous browser flow ships today

## Pre-conditions

- Customer is signed in for the shipped browser flows; admin review requires an
  admin-scoped bearer
- Iris adapter routes (`/v1/iris/adapter/review`, `/pause`, `/export`) and the
  data-rights routes (`/v1/data-rights/exports`, `/deletions`) are reachable
- The canonical `IrisDataRightsRequest` state machine exists in
  `libs/oshun/memory-iris/src/data-rights.ts`, but the current BFF export and
  deletion stores do not yet persist that contract end to end

## Steps

### 1. Customer arrives at the privacy controls

- [ ] Customer signs in, opens `/profile`, follows the Memory row to
      `/profile/memory`
- [ ] **Verify**:
      [`customer/09-account/profile.md`](../customer/09-account/profile.md)
      surface resolver + sub-route nav
- [ ] **Verify**:
      [`customer/09-account/profile-memory.md`](../customer/09-account/profile-memory.md)
      populated state, scope/category chips, export + forget affordances

### 2. Submit a DSAR export request

- [x] Customer clicks "Export JSON" (`data-profile-memory-export=""`); optional
      DSAR case id via `data-profile-memory-dsar-input=""` flips the button to
      "Sign DSAR bundle"
- [x] `exportIrisUserMemoryBundle` returns `{ json, fileName }`; notice reads
      "DSAR export <id> signed and ready to download." (or generic export)
- [x] When the Iris adapter is hydrated, the same action POSTs
      `/v1/iris/adapter/export?role=settings` and renders a synced server
      receipt; if that call fails, the downloadable local bundle remains
      available with an honest `unavailable` receipt state
- [x] On `/profile?path=data-rights`, a scoped export POSTs
      `/v1/data-rights/exports`; `/profile/data` reads the completed request and
      its SHA-256 integrity manifest back from the BFF

### 3. Submit a deletion request

- [x] On `/profile/memory`, the customer opens "Forget everything", types the
      confirmation phrase, and passes `ensureStepUp('forget-all')`
- [x] `requestIrisUserMemoryFullClear` returns `{ ok: true, grace }`; the
      `data-profile-memory-grace-banner=""` shows the seven-day grace window
- [x] This memory-panel grace request is local profile state; cancelling it
      clears the banner. It is not evidence that the account-wide BFF deletion
      fan-out ran
- [x] On `/profile?path=data-rights`, the customer can separately POST a scoped
      deletion to `/v1/data-rights/deletions`, choosing grace-period or
      immediate mode; `/profile/data` reads the scheduled/completed request and
      any supported erasure attestations back

### 4. Authentication and verification boundary

- [x] Shipped customer data-rights routes require the signed-in bearer; missing
      authentication returns 401
- [x] Per-fact / per-category forgets on `/profile/memory` use the
      `STEP_UP_CHALLENGE` phrase via `issueIrisUserMemoryStepUpToken` — no email
      round-trip
- [ ] The canonical `session-token`, `email-link`, and `operator-signoff`
      transitions exist in `data-rights.ts`, but an email-link browser
      round-trip is not wired into the current data-rights route

### 5. Cross-service fan-out — partially shipped

Per `V1/ARCHITECTURE.md` § Privacy, Consent, Residency, DSAR.

- [x] The BFF fan-out and signed-receipt path is implemented for the
      `voice_profile` consumer and for subject-owned Isis `generated_artifact`
      rows
- [ ] Tara, Arete, Veritas, Nyx, Nisaba, and Metis do not yet expose a proven
      six-service erasure fan-out from this browser journey
- [ ] The canonical `verified → in-progress → completed` transition sequence is
      modeled in `data-rights.ts` but is not the persisted record used by every
      current data-rights endpoint

### 6. Attestation per supported erasure consumer

- [x] Immediate voice-profile and generated-artifact deletions return signed
      per-consumer receipts with status, row count, audit key id, and signature
- [ ] The broader `@oshun/audit-platform` attestation fan-out and a single
      scope-complete gate across every domain remain open

### 7. DSAR receipt back to user

- [x] Export requests expose a real SHA-256 integrity manifest; supported
      immediate deletions expose signed erasure receipts
- [x] **Verify**: [`operator/operator-admin.md`](../operator/operator-admin.md)
      inbox detail card reflects the closed DSAR case with attestation bundle
- [x] `/profile/data` and the profile data-rights surface render the BFF request
      state and supported receipts in-app
- [ ] Email delivery of the DSAR receipt is not wired in the current harness

### 8. Read back the affected scope

- [x] Customer returns to `/profile/data` and sees the completed export or
      scheduled/completed deletion row from the BFF
- [x] A supported immediate generated-artifact deletion proves the seeded Isis
      row is gone before the signed receipt is persisted
- [ ] A local "Forget everything" action can empty `/profile/memory`, but that
      local state is not presented as proof of account-wide erasure
- [x] **Verify**:
      [`customer/09-account/profile-memory.md`](../customer/09-account/profile-memory.md)
      local empty and grace-window states
- [x] **Verify**:
      [`customer/09-account/profile-safety.md`](../customer/09-account/profile-safety.md)
      empty-state copy avoids alarming framing post-deletion
- [x] **Verify**:
      [`customer/00-public/legal-privacy.md`](../customer/00-public/legal-privacy.md)
      `data-retention` section consistent with what the receipt retained vs
      purged

## Post-conditions

- `/profile/memory` always produces a downloadable bundle; when Iris is
  reachable it also shows the synced export receipt
- BFF export/deletion requests remain readable at `/profile/data`; grace-period
  deletions can be cancelled before `cancelDeadlineAt`
- Supported immediate erasures persist signed per-consumer receipts, and the
  generated-artifact path proves the subject-owned row was removed
- No claim is made that the unimplemented six-domain fan-out completed; that
  remains a release-gate requirement

## Failure modes

- **Iris export adapter unavailable** — `/profile/memory` labels the receipt
  unavailable and still offers the local download; it never fabricates a synced
  receipt
- **Wrong deletion confirmation token** — BFF returns 400; no erasure begins
- **Second active deletion** — BFF returns 409 rather than creating competing
  requests
- **Unsupported erasure category** — the request must not imply that a signed
  per-consumer receipt exists; the six-domain fan-out remains incomplete
- **Verification window elapses** — the canonical state machine supports an
  `expired` terminal state, but this email/operator verification path is not yet
  wired into the shipped browser flow
- **Silent re-creation from old conversations** — treat any reappearance after a
  supported hard-delete receipt as P0; the broader cross-service guarantee is
  not yet proven
- **Full memory clear cancelled within grace** — `cancelIrisUserMemoryFullClear`
  removes the local grace request; BFF data-rights cancellation is a separate
  endpoint and state transition

## E2E coverage

- Backed by
  [`apps/oshun/web/e2e/profile-customer-data-deletion.spec.ts`](../../apps/oshun/web/e2e/profile-customer-data-deletion.spec.ts),
  [`apps/oshun/web/e2e/profile-data-rights.spec.ts`](../../apps/oshun/web/e2e/profile-data-rights.spec.ts),
  [`apps/oshun/web/e2e/profile-customer-data-export.spec.ts`](../../apps/oshun/web/e2e/profile-customer-data-export.spec.ts),
  [`apps/oshun/web/e2e/profile-data-readback.spec.ts`](../../apps/oshun/web/e2e/profile-data-readback.spec.ts)
  — the deletion spec asserts the UI controls (scope/mode/token selectors),
  schedules an account deletion through the confirmation modal, cancels it via
  the active banner, and _(2026-06-23)_ drives the **real signed-receipt UI
  path**: Playwright boots the BFF with Redis-backed deletion fan-out, the
  browser submits an immediate `voice` category deletion, the BFF runs the
  `voice_profile` consumer and attaches a dev-key Ed25519 receipt, and the
  profile UI renders the resulting
  `data-profile-deletion-attestation="voice_profile"` row from the live BFF
  response (`status=not_found`, zero rows, audit key id, truncated signature).
  _(2026-06-26)_ The same browser surface now seeds a subject-owned Isis
  `generated_outputs` row, submits an immediate `generated_artifacts` deletion,
  renders the live signed `generated_artifact` receipt (`status=erased`,
  `rowsRemoved=1`, audit key id, truncated signature), and verifies the Isis row
  is hard-deleted. The older page-boundary snapshot case remains only as a
  negative render guard: an attested tombstone renders a row and an unattested
  tombstone renders none. The data-rights spec exercises scoped export, deletion
  scheduling, **cancellation within the grace window** (the button reverts and a
  "Deletion request cancelled" toast fires), and the **immediate-mode** path
  (switch to immediate erasure → confirm → "Account deletion completed"); the
  export spec validates the full category/format list, a submitted export
  appearing in history as completed, and rejection when no category is selected.
  The readback spec _(2026-06-24)_ verifies the standalone `/profile/data` page
  against the same real BFF contracts: authenticated empty export/deletion
  states, `/profile?path=data-rights` handoff, and server-rendered completed
  export + scheduled deletion rows seeded by live POSTs. _(2026-06-27)_ It also
  warms `/profile/data` under the real Chromium service worker, verifies the
  route document is cached after online live-BFF render, switches the browser
  offline, reloads `/profile/data` from the SW cache, and asserts the completed
  export + scheduled deletion rows remain visible while `navigator.onLine` is
  false and without falling back to the generic offline shell card.
- Backed by
  [`apps/oshun/web/e2e/data-rights-dsar-bff.spec.ts`](../../apps/oshun/web/e2e/data-rights-dsar-bff.spec.ts)
  — real HTTP integration against the live BFF store-backed privacy core (steps
  2, 3, 7): customer export (`POST`/`GET /v1/data-rights/exports` → 201
  `completed` with a real sha256 `integrityManifest` surfaced back in the
  snapshot; malformed/empty-category bodies → 400; anonymous → 401); customer
  deletion (`POST`/`GET /v1/data-rights/deletions` → grace-period request
  `scheduled` with a `cancelDeadlineAt` and materialised tombstones that persist
  on `GET`-by-id; immediate `voice` category deletion over Redis fan-out →
  `completed` with a signed `voice_profile` tombstone attestation persisted on
  `GET`-by-id; _(2026-06-25)_ immediate `generated_artifacts` category deletion
  seeds a real Isis `generated_outputs` row for the subject, runs the live
  Redis-backed `generated_artifact` eraser, returns a signed `erased`
  attestation with `rowsRemoved=1`, persists that receipt on `GET`-by-id, and
  verifies the Isis row is hard-deleted; wrong `confirmationToken` → 400; a
  second active deletion → 409; anonymous → 401); the admin DSAR review
  lifecycle (`POST /v1/admin/privacy/requests/:id/review-action` driving
  generate_export_package → record_export_download → complete with specific
  state transitions, generate refused on a terminal request → 409 and on a
  delete-type request → 409); and a consent-review → privacy audit-log
  round-trip (`/admin/privacy/consent-records/:id/review` then
  `/admin/privacy/audit-log`). Admin 403 (`admin_scope_missing`) is asserted for
  a non-admin caller.
- [`apps/oshun/web/e2e/account-deletion-dsar.spec.ts`](../../apps/oshun/web/e2e/account-deletion-dsar.spec.ts)
  — the memory-surface counterpart to the profile-dashboard specs above: drives
  the **grace-window banner** on `/profile/memory` (a grace-period deletion
  request renders the banner; cancel clears it — round-trip) and the
  **DSAR-mode** case-id input on the memory export panel (step 2).
- **Coverage depth**: deep — see [`coverage.md`](./coverage.md). The
  export/deletion/DSAR lifecycle is driven over the **real** store-backed BFF:
  scoped export → 201 `completed` with a real sha256 `integrityManifest`;
  grace-period deletion → `scheduled` + `cancelDeadlineAt` + materialised
  tombstones that persist on `GET`-by-id; immediate voice-profile deletion runs
  through the real Redis fan-out and persists a signed receipt; immediate
  generated-artifact deletion uses the real Isis `generated_outputs` table in
  both the BFF API leg and the browser `/profile?path=data-rights` leg, proving
  the subject-owned row is removed before persisting/rendering the signed
  receipt; the admin DSAR review state machine (generate_export_package →
  record_export_download → complete, with `409` refusals on terminal/delete-type
  requests); and the signed per-service erasure-receipt rows render from live
  BFF data. State transitions + post-conditions + multiple failure modes (wrong
  `confirmationToken` → 400, second active deletion → 409, anonymous → 401,
  admin scope-missing → 403, grace-window cancel round-trip) are all asserted,
  so the journey clears the deep bar. The remaining gaps are cross-service or
  identity-verification product gaps, not missing assertions over shipped
  browser/BFF behavior.
- **Uncovered**:
  - Steps 5–6, the literal per-domain six-service tombstone **fan-out** is only
    partially driven. The BFF-owned Redis fan-out and signed receipt are now
    covered for `voice_profile` and the local Isis `generated_artifact` store,
    but Tara/Arete/Veritas/Nyx/Nisaba/Metis service-specific erasure plus
    `@oshun/audit-platform` attestation remains open.
  - Step 4 identity verification via the `email-link` round-trip is not driven
    (no real mailer in the harness; the BFF data-rights flow auto-completes
    without an email hop).
  - The customer-facing `/profile/memory` export + forget **UI** is not driven
    by this spec (it is a pure-BFF integration test); that web surface is
    covered by [`memory-edit-pause-forget.md`](./memory-edit-pause-forget.md).
  - Failure modes around domain RPC failure mid fan-out and tombstone
    propagation drift remain uncovered (same fan-out gap as above).

## Per-view files touched

- [`customer/09-account/profile.md`](../customer/09-account/profile.md)
- [`customer/09-account/profile-data.md`](../customer/09-account/profile-data.md)
- [`customer/09-account/profile-memory.md`](../customer/09-account/profile-memory.md)
- [`customer/09-account/profile-safety.md`](../customer/09-account/profile-safety.md)
- [`customer/00-public/legal-privacy.md`](../customer/00-public/legal-privacy.md)
- [`operator/operator-admin.md`](../operator/operator-admin.md)

## Cross-references

- `V1/features.md` § User-Facing Memory Controls; § Customer memory UX flows; §
  Admin inspection state machine; § Customer-Facing Privacy Surface; § Admin
  DSAR and Operator-Access Workflow
- `V1/ARCHITECTURE.md` § Privacy, Consent, Residency, DSAR (the DSAR / deletion
  sequence mermaid is canonical)
- `libs/oshun/memory-iris/src/data-rights.ts` — `IrisDataRightsRequest`,
  `ALLOWED_TRANSITIONS`, default verify/deadline windows
- `libs/oshun/memory-iris/src/admin-inspection/state-machine.ts` — only
  `dsar-fulfillment` may transition from `requested` for the legal role
- Sibling journeys:
  [`memory-edit-pause-forget.md`](./memory-edit-pause-forget.md),
  [`incident-triage.md`](./incident-triage.md)

## Open questions / known gaps

- [ ] Unify the BFF export/deletion records with the canonical
      `IrisDataRightsRequest` state machine, or document why they remain
      separate contracts
- [ ] Wire and test the `email-link` / `operator-signoff` verification paths for
      an out-of-band requester
- [ ] Decide the email template id for the DSAR receipt and link it from
      [`customer/13-system/system-emails.md`](../customer/13-system/system-emails.md)
- [ ] Confirm `/profile/safety` tombstones along with the rest or runs a tighter
      retention than `MemoryEntry`
- [ ] Define telemetry event names: `dsar_export_built`, `dsar_export_signed`,
      `dsar_delete_requested`, `dsar_grace_cancelled` (none exist today in
      `apps/oshun/web/src/analytics/`)
- [ ] Specify the per-tombstone attestation export format (CSV vs signed JSON
      bundle); needs an operator-runbook entry
