# Residual Audit — Governance, Safety, Support, and Privacy (features.md 5204–5760)

Auditor slice: Trust & Safety (moderation/appeals/crisis/abuse-patterns/operator
surfaces/evals), Support/Entitlements/Billing/Customer-Ops, Crypto Payments,
Privacy/Consent/DSAR/Retention/Residency. Date: 2026-06-11. Evidence rule:
current code only; every delegation chain followed to source.

**Recorded non-findings honored**: A1/A-phase auth gates, B1 DSAR export bundle,
C10 settlement→entitlement leg, E3 reminder consent/quiet-hours, the Ed25519
deletion fan-out, the 30-day grace-expiry deletion worker
(`data-deletion/deletion-worker.ts` + `state.ts:68` — real), takedown +
counter-notice + repeat-infringer wiring, chargeback screening, crisis-frame
cascade + payment-path crisis suppression, disclosure-gate enforcement on
tier-B/C quotes (`quote-builder.ts:92-93`), and everything that is genuinely
deploy-bound (chain watcher, invoice-target provisioner, self-hosted nodes,
payment-processor creds).

---

### 1. `GET /v1/payments/invoices` serves every customer's invoice ledger to anonymous callers

**Severity**: P0-SEC

**Evidence**: `apps/oshun/bff/src/routes/domain-stubs.ts:2349-2364` — the route
is registered with **no preHandler at all** (no auth, no origin/CSRF guard,
unlike the quote route at :2393 which carries `[originGuard, csrfGuard]`); there
is no global auth hook (`app.ts:707` registers only an `onResponse` metrics
hook). It returns `listCryptoInvoices(...)` verbatim —
`apps/oshun/bff/src/payments/invoice-store.ts:219-239` returns full
`CryptoInvoiceRecord`s including `purchaserUserId` (:71), `entitlementPlan`
(:73), `receivingTargetSummary` (the payment address/summary, :64),
`fiatAmountMinor`, `chainAmount`, `txId`, and status. The `tenantId` query param
is an optional _filter_, not a scope. The by-id route (:2369) is also
unauthenticated but the id is a `crypto.randomUUID` capability (:2453) — the
list endpoint destroys that property by enumerating every id.

**Spec promise**: features.md 5485–5497 — per-invoice Monero subaddresses so
"two paying customers can never link each other's payments"; telemetry "stores
only the hash of the subaddress, never the address itself"; 5374–5378 billing
history is an account surface. Also the store's own header comment
(`invoice-store.ts:6`) calls this "the customer's" list.

**What the code actually does**: any anonymous HTTP client can dump the entire
durable invoice ledger — every user's plan purchases, user ids, amounts,
receiving targets, and settlement txids. This simultaneously breaks
cross-customer payment unlinkability (the exact privacy property the crypto
section exists for) and discloses billing PII.

**Fix sketch**: require a session; scope the list to
`authContext.userId === purchaserUserId` (plus an admin-scoped operator
variant). Keep the by-id route as an unauthenticated capability URL if the
paywall needs it pre-login, but strip `purchaserUserId` from that DTO.

---

### 2. `@oshun/billing-support` is still fully orphaned — the entire customer-ops machinery has zero runtime consumers

**Severity**: P0-STRUCT

**Evidence**: `grep -rn "@oshun/billing-support" apps/ libs/ tools/` → **zero
hits outside the library itself**. The library is real, domain-specific engine
code: `libs/oshun/billing-support/src/dunning/dunning.ts:83-176`
(payment-failure → dunning-stage state machine, grace windows, lapse
degradation, trial-prompt schedule), `support-cases/support-cases.ts:88-258`
(case routing queues, per-queue SLA budgets, SLA evaluation, CSAT capture +
aggregation, grounded copilot answers), `self-serve/self-serve.ts:45-203`
(refund eligibility, pause/resume, promo codes),
`entitlements/entitlements.ts:77-348` (entitlement classes, feature gates,
subscription transitions, upgrade proration, intro offers),
`metered/metered.ts:75-174` (usage accrual, budget caps, meter cards),
`tax-billing/tax-billing.ts:68-158` (VAT/GST application, locale currency
formatting, receipt summaries). None of it is reachable from any route, worker,
or app.

**Spec promise**: features.md 5361–5407 — entitlement classes, trials,
upgrade/downgrade, dunning sequences, grace windows, graceful degradation,
self-serve refund/credit/pause/promo, tax/VAT-aware invoicing, metered billing
with alerts and budget caps, support-case intake/routing/SLAs/CSAT.

**What the code actually does**: the runtime has exactly two entitlement
mechanisms — the persisted `free|pro|premium` plan read by
`middleware/entitlements.ts:28-39` and the generation-tier resolver — and
nothing else from this feature family. The engines exist; the product does not.

**Fix sketch**: wire the highest-leverage seams first: (a) dunning + lapse
degradation around the plan field (see finding 3), (b) a support-case
intake/queue route pair over `routeCase`/`slaForCase`/`evaluateCaseSla` (see
finding 4), (c) `featureGate`/`featuresFor` as the entitlement middleware's
feature vocabulary.

---

### 3. No subscription at runtime: crypto settlement grants a perpetual plan — no renewal invoices, no renewal reminders, no grace window, no dunning

**Severity**: P0-STRUCT

**Evidence**: `apps/oshun/bff/src/payments/settlement-route.ts:85-93` flips
`customerAuthStateStore.updatePlan(...)` — the plan field
(`auth/customer-auth-store.ts:184,229`) carries **no expiry, period, or renewal
date**. Nothing anywhere creates a renewal invoice (`issueCryptoInvoice` is
called only from the quote route), the renewal-reminder engine
`libs/oshun/payments-bridge/src/customer-surface/reminder-cadence.ts` has zero
app consumers (symbol grep: NONE), and the dunning machinery is orphaned
(finding 2). The reminders dispatcher (E3) carries no payment-renewal producer
(`apps/oshun/bff/src/reminders/` — session/assignment/content-drop/streak
producers only).

**Spec promise**: features.md 5572–5575 — "subscription renewals create a new
invoice at renewal time… notification 7 days, 24 hours, and 1 hour before
renewal; failure to settle by expiry triggers the same grace-window
degradation"; 5381–5386 dunning sequences and graceful degradation on lapse.

**What the code actually does**: one settled invoice = lifetime entitlement.
There is no concept of a billing period, so renewal, lapse, degradation, and
dunning can never trigger. This is the largest behavioral gap between the
payment spec and the in-repo half (it requires no chain watcher — it's pure
period bookkeeping + invoice issuance + the existing reminder dispatcher).

**Fix sketch**: add `planPeriodEndsAtUnixSeconds` to the auth record (stamped at
settlement); a renewal worker (mirror the deletion/reminder worker pattern) that
issues a renewal invoice via the quote builder, feeds `reminder-cadence` offsets
into the messaging dispatcher, and on unpaid expiry applies
`degradedEntitlementOnLapse()` through `recordPaymentFailure`/dunning stages.

---

### 4. Support-case intake does not exist; the operator support workspace triages fabricated cases

**Severity**: P0-STRUCT

**Evidence**: no `/v1/support*` route exists anywhere in `apps/oshun/bff/src`
(route grep across all 487 route files). Customer-side, the only intakes are
`POST /v1/feedback` (B14 bug-report intake) and `POST /v1/user-reports` (safety
reports, `routes/user-reports.ts:107`). The admin app's support console
(`apps/oshun/admin/src/app/support/page.tsx`) renders the seeded per-operator
workspace — `admin/state.ts` ships fixture refund tickets
(`AdminRefundTicketRecord`, :1153) and support cases cloned per operator from
`ADMIN_USER_SEED` (:18971). The real case engine
(`billing-support/support-cases.ts` — routing, SLA, CSAT) is orphaned (finding
2).

**Spec promise**: features.md 5397–5404 — support case intake, routing, member
timeline, refund/credit, escalation, first-response/resolution SLAs, CSAT
capture, omnichannel history.

**What the code actually does**: a member who needs help is pointed at
`mailto:billing@oshun.app` (`apps/oshun/web/src/app/billing/page.tsx:103-110`);
an operator who opens Support sees fabricated tickets. No real case can enter,
move through, or exit the system.

**Fix sketch**: `POST /v1/support/cases` (session-stamped) + durable store +
`routeCase`/`slaForCase` for queue/SLA assignment + an admin-scoped queue
read/decide pair; surface intake from the existing help/feedback affordances.

---

### 5. The `@oshun/trust-safety` enforcement engines are orphaned: severity SLAs, repeat-offender ladder, coordinated-abuse detection, safety dashboard, eval gates

**Severity**: P0-STRUCT

**Evidence**: runtime imports of `@oshun/trust-safety` are crisis-only
(`apps/oshun/bff/src/safety/crisis-frame-runtime.ts:20-29`; mobile
`companionAppModel.ts` uses `classifySeverity`/`slaFor` in a descriptive model).
Zero app consumers (non-test grep: NONE) for: `applyOffense` (warn→restrict→
suspend→ban ladder, `abuse-patterns.ts:37`), `detectCoordinationClusters`
(:101), `classifyBotBehavior` (:172), `detectClassifierDrift` (:201),
`classifyTenantAbuseRates` (:254), `buildAppealEvidencePack`
(`operator-surfaces.ts:40`), `buildRepeatOffenderTimeline` (:72),
`buildSafetyDashboard` (:133), trend builders (:287-305), `evaluateSla`
(`severity.ts:184`), and the whole evaluation module (release-gate verdicts,
calibration error — `evaluation.ts`).

**Spec promise**: features.md 5258–5276 (severity classes with triage/action
SLAs and escalation rules), 5314–5329 (repeat-offender rules, coordinated-abuse
detection, bot signals, drift detection, tenant aggregates), 5331–5343 (appeal
evidence pack, repeat-offender summary, safety dashboard, trend dashboards),
5345–5359 (eval suites, regression blocking).

**What the code actually does**: moderation decisions happen with no severity
classification, no SLA clock, no offense counter, no escalation, and operators
have no safety dashboard. The engines compute all of this correctly — nothing
feeds them or reads them.

**Fix sketch**: stamp `classifySeverity` + `slaFor` onto every
moderation-queue/abuse-report item at enqueue; run `applyOffense` on each upheld
decision and persist the per-user counter; an admin
`GET /v1/admin/safety/dashboard` over `buildSafetyDashboard` fed from the real
moderation store + abuse-report store.

---

### 6. The Trust & Safety operator inbox interleaves REAL customer reports with unlabeled fabricated cases (including fake CSAM/NCMEC records)

**Severity**: P0-HONESTY

**Evidence**: `apps/oshun/bff/src/admin/state.ts:13848-13851` — "…prepends a
clone of this queue to the per-admin `userReports` array so customer-submitted
flags appear ahead of the **pre-seeded fixture rows**" (the code's own words).
Real reports enter via `POST /v1/user-reports` (`routes/user-reports.ts:91,107`
→ `recordCustomerUserReport`, `state.ts:13867`). The fixture rows they're mixed
with include fabricated critical cases: `state.ts:8317-8336` an image-moderation
case with `signals: [{ kind: 'csam', confidence: 0.98 }]`,
`matchedHashset: 'ncmec-csam-v3'`, named fake reviewers; plus fabricated
avatar-deception, refund-ticket, DSAR, incident, and compliance records — all
cloned per operator from `ADMIN_USER_SEED` (:18971) with **no fixture/seed
marker on any served record**.

**Spec promise**: features.md 5258–5263 (P0 = CSAM, triage < 5 min, parallel
safety-incident record — i.e., these queues are operationally load-bearing);
5331–5343 (operator surfaces must show real account/incident context).

**What the code actually does**: the per-operator-clone workspace model is a
recorded baseline (ground-truth audit C13 ships against "the same per-operator
snapshot"), so the architecture itself is not re-litigated here. The honesty
defect is narrower and new relative to that record: **live customer reports and
fabricated cases are indistinguishable in the same array**, and a fabricated
"NCMEC hash match" case sits in a real operator's queue. An operator can spend
P0-SLA attention on a fake CSAM case, or — worse — learn to dismiss CSAM-flagged
rows as "the demo data".

**Fix sketch**: stamp `origin: 'seed' | 'live'` on every workspace record at the
DTO boundary and render seeds with an explicit register (the D1
`DomainPreviewBanner` precedent); route real `userReports` to a shared durable
store rather than a prepend into per-operator clones.

---

### 7. Moderation bans are written but never enforced anywhere

**Severity**: P1

**Evidence**: `POST /v1/admin/moderation/bans` issues a real
`@aja/content-moderation` `BanRecord` (`routes/admin-moderation.ts:240-280` →
`moderation-store.ts:issueBan`), and `banStatus` is read **only** by the admin
GET (`admin-moderation.ts:330`). Repo-wide grep for
`banStatus`/`isBanned`/`activeBan` outside the moderation files: zero
user-facing call sites. No auth middleware, message route, persona route,
generation route, or upload path consults the ban store.

**Spec promise**: features.md 5314–5318 — escalating consequences "warn →
restrict feature → suspend → ban"; a ban is a consequence, not a record.

**What the code actually does**: a "permanently banned" user retains full
product access; the ban exists only as an admin-readable row.
Stored-config-never-enforced.

**Fix sketch**: consult `moderationStore.banStatus(userId)` in
`createAuthPreHandler` (or a dedicated pre-handler on mutating customer routes);
map `feature_specific`/`content_specific` scopes to the relevant route families;
surface 403 `account_restricted` with the appeal affordance.

---

### 8. Appeals: no reviewer rotation, no two-reviewer signoff, and the actor identity is client-supplied

**Severity**: P1

**Evidence**: `libs/aja/content-moderation/src/moderation-workflow.ts:449-485` —
`resolveAppeal` accepts any `reviewerId`, never compares it to the original
decision's moderator; `startReview(appealId, _reviewerId)` (:438) ignores the
reviewer entirely. No two-reviewer path exists for any decision class. Worse,
the route takes `moderatorId`/`reviewerId`/`issuedBy` from the **request body**
(`routes/admin-moderation.ts:145,219,256`) rather than
`request.authContext.userId` — an authenticated operator can stamp decisions,
appeal resolutions, and bans as any other operator.

**Spec promise**: features.md 5285–5295 — appeals triaged by "a different
reviewer than the original decision", mandatory cooling-off, two-reviewer
signoff for P0/permanent-ban/public-figure/cloned-voice decisions.

**What the code actually does**: the same operator can decide, "appeal" to
themselves, and resolve — and can do all three under a colleague's name. The
audit trail's actor field is unverified input.

**Fix sketch**: derive actor ids from `authContext` (drop them from the body);
in `resolveAppeal`, reject `reviewerId === decisionHistory.last().moderatorId`;
add a `requiresSecondReviewer` flag derived from severity/ban-type with a
two-step approve.

---

### 9. The canonical consent ledger has no customer surface; the privacy page reads a fixture; voice/avatar consent is ungrantable in-product

**Severity**: P1

**Evidence**: the real V1-PRIV-006 ledger is complete and auth-gated —
`/v1/consent` + per-flow `grant/withdraw/revoke/renew/audit`
(`routes/consent.ts:295-346` over `consent/state.ts`, 999 lines, real taxonomy,
append-only history). **Zero web/mobile consumers** (repo grep for `v1/consent`
outside the BFF: only Lilith-domain files and an e2e spec). The customer privacy
page `/profile/data` instead reads `GET /v1/data-rights/consent` — a
`guardedFixtureRoute` serving three fabricated consents
(`routes/domain-stubs.ts:1867-1901`; 503 in production), so in prod the page's
consent panel renders empty (`page.tsx:69,73` `consent?.consents ?? []`) and it
has no grant/withdraw controls at all. Meanwhile the ledger IS enforced: persona
voice/avatar modality requires a granted `voice`/`avatar` flow
(`routes/personas-consumer.ts:42-47`) — which no surface can grant, so premium
voice personas are a permanent dead-end for every member.

**Spec promise**: features.md 5654–5670 (per-feature opt-in, withdrawal at any
time), 5714–5726 (privacy center "listing every consent…"; consent prompts "at
first relevant feature use").

**What the code actually does**: a correct, audited consent engine with no door.
Two consent sources of truth (`/v1/consent` real vs `/v1/data-rights/consent`
fixture), and a consent-gated feature whose grant path doesn't exist.

**Fix sketch**: re-point `/profile/data` at `GET /v1/consent` and add
grant/withdraw toggles posting to the real per-flow verbs; mount a first-use
consent prompt on the persona voice/avatar picker; delete the
`data-rights/consent` fixture route.

---

### 10. Crisis residuals: resources are not region-aware, and the post-crisis machinery (check-in, cooldown, mandatory reporting) is orphaned

**Severity**: P1

**Evidence**: `GET /v1/safety/crisis-resources` hardcodes `region: 'global'`
(`routes/safety.ts:63`) and serves the `@iris/emotional-wellbeing` curated
catalog — real 988 / Crisis Text Line 741741 data
(`safety/wellbeing-resource-service.ts:17,103`), i.e., **US numbers for every
caller in every country**. The region-and-language-aware selector exists in
`libs/oshun/trust-safety/src/crisis/crisis.ts:71-100` (`routeCrisis` filters
catalog by `detection.region` + language) and is unconsumed, as are
`planCheckIn` (:146, post-crisis opt-in check-in), `isInCooldown` (:173,
no-recommendation cooldown), `selectLegalContact` (:197) and
`createMandatoryReport` (:208, jurisdiction-aware reporting). The wired part —
frame activation, cascade, payment suppression — is real and recorded.

**Spec promise**: features.md 5300–5312 — "region-aware crisis resources…,
language-matched"; post-crisis opt-in check-in, no-recommendation cooldown,
jurisdiction-aware mandatory reporting with per-region legal contact.

**What the code actually does**: a UK user in crisis is shown a US hotline; a
resolved crisis frame triggers no check-in plan, no recommendation cooldown, and
no reporting-obligation evaluation.

**Fix sketch**: pass the caller's locale/region (Accept-Language + profile
region) into `routeCrisis` over a region-keyed catalog; on crisis-frame
resolution, persist `planCheckIn` output into the reminders dispatcher and
consult `isInCooldown` in the recommendations route.

---

### 11. Signed payment receipts are never produced — the configured `ReceiptSigner` has no call site

**Severity**: P1

**Evidence**: `payments-composition.ts:78-94` resolves the Ed25519
`ReceiptSigner` from env and binds it into `PaymentsRuntime` ("used to sign the
SETTLEMENT receipt", :8-9) — but `settlement-route.ts` never imports the runtime
or the signer; it confirms the invoice and returns a plain JSON ack (:106-115).
Repo grep: no call to any `receipt-signer` signing function from any app. There
is also no receipt read endpoint, so even the unsigned settlement facts (txid,
confirmedAt) reach the customer only via the leaking invoice list (finding 1).

**Spec promise**: features.md 5564–5568 — receipts "signed by the Oshun
audit-platform signing key so a customer holding only the receipt and the chain
can verify payment without trusting Oshun's API", provenance-bundled with
txid/block data; 5374–5378 billing history with receipts.

**What the code actually does**: even a fully-configured deploy (signer key +
provisioner + watcher) settles invoices without ever emitting the
cryptographically verifiable receipt the spec centers on.

**Fix sketch**: in the settlement route, build the receipt payload (invoice
fields + txid + confirmedAt), sign with `getBoundPaymentsRuntime().signer`,
persist it on the invoice record, and serve it on an authenticated
`GET /v1/payments/invoices/:id/receipt`.

---

### 12. Analytics consent is recorded but never enforced — withdrawal has zero effect

**Severity**: P1

**Evidence**: the consent store models an `analytics` flow (soft opt-in,
`consent/state.ts:47,598`) with a withdraw verb — but the web analytics
transport posts unconditionally
(`apps/oshun/web/src/analytics/transport.ts:30-37`, no consent/DNT check), and
the BFF ingest accepts session-less batches with no consent consultation
(`analytics/events-ingest.ts:86-125`; its only privacy property is the 7-day /
5000-event prune). No code path reads the analytics consent flow (consent-store
consumers: consent routes, export bundle, residency middleware,
personas-consumer — nothing analytics-side).

**Spec promise**: features.md 5639–5641 (analytics-granularity consent),
5663–5666 ("withdrawal is effective immediately on storage and recall"),
5749–5752 (every consent transition enforced + audited).

**What the code actually does**: a member who withdraws analytics consent in the
ledger (could they reach it — see finding 9) keeps emitting and storing events
identically.

**Fix sketch**: gate the shared sink on the member's analytics consent (hydrate
once per session from `/v1/consent/analytics`, default-on per spec);
server-side, drop events whose bearer session resolves to a withdrawn flow.

---

### 13. The BFF invoice state machine diverges from the spec contract, and the rail-agnostic entitlement bus is bypassed

**Severity**: P1

**Evidence**: `invoice-store.ts:23` —
`'pending' | 'confirmed' | 'expired' | 'cancelled'`. The spec's
`created → seen → confirmed → settled → entitlement_granted` with
`overpaid`/`underpaid`/`refunded` terminals lives in the bridge's
`state-mapper.ts:52-69` (`payment.invoice.underpaid`, `refunded`, amount-tier
confirmation policies) and the cross-rail event emitter in
`entitlement-bus/emitter.ts` — **both unconsumed by the BFF** (grep:
`EntitlementEmitter`/`emitEntitlement` → NONE; the settlement route calls
`customerAuthStateStore.updatePlan` directly, `settlement-route.ts:85-93`).
There is no underpayment-tolerance auto-credit or overpayment refund-or-credit
surface, and a settlement webhook carries no amount at all (:41 — body is
`{invoiceId, txId, confirmedAtUnixSeconds}`).

**Spec promise**: features.md 5554–5558 (lifecycle incl. underpaid/overpaid
handling), 5569–5571 ("confirmation events publish to `libs/shared/event-bus`
with the same topic schema used by the fiat adapter, so the entitlement service
is payment-rail-agnostic").

**What the code actually does**: a second, thinner state machine; partial
payments are indistinguishable from full ones at the webhook contract level;
entitlement granting is hard-wired to the crypto route rather than bus-mediated,
so a future fiat rail cannot share the machinery the spec requires.

**Fix sketch**: extend the webhook body with `paidAmountAtomic`; run it through
`state-mapper` to classify settled/underpaid/overpaid; publish the resulting
`V1PaymentEvent` on the shared event bus and move the plan flip into a bus
consumer.

---

### 14. No refund path exists in-repo: refund intake, refund-initiation, and the cold-spend queue are all orphaned

**Severity**: P1

**Evidence**: no customer or admin refund route exists (BFF grep for "refund"
outside fixtures: only creator-analytics fixture numbers and `admin/state.ts`
seeded refund tickets). The purpose-built modules are unconsumed:
`payments-bridge/src/admin-surface/refund-initiation.ts` (grep `initiateRefund`
→ only a Lilith fiat-ramp app), the entire `cold-spend-queue/` (queue,
sweep-policy, hw-signing fixture, audit attestation —
`ColdSpendQueue`/`enqueueUnsignedTransaction` → NONE), and
`admin-surface/invoice-timeline.ts` + `node-health-panel.ts` (NONE). Refund
_eligibility_ logic is also orphaned (finding 2,
`self-serve.ts:45 evaluateRefundEligibility`).

**Spec promise**: features.md 5576–5581 — customer-initiated refunds with
explicit destination address (Monero) / originating-address default (BTC/EVM),
queued unsigned transactions, operator hardware-wallet co-sign, audit-logged
broadcast; 5384–5388 customer self-serve refund.

**What the code actually does**: only the final _signing/broadcast_ is
deploy-bound; the locally-buildable half — refund request intake, destination
capture, eligibility, the unsigned-tx queue, the operator initiation console —
exists as library code with no runtime. A customer's refund today is a mailto
link.

**Fix sketch**: `POST /v1/payments/invoices/:id/refund-request` (session-scoped,
destination-address capture per asset rules) → `evaluateRefundEligibility` →
enqueue via the real `ColdSpendQueue` → an admin-scoped initiation/list route
over `refund-initiation.ts`; broadcast stays deploy-bound.

---

### 15. Moderation queue, appeals, and bans are memory-only — every governance decision evaporates on restart

**Severity**: P1

**Evidence**: `moderation-store.ts:105-108` — `ModerationStore` holds
`createModerationQueue()/createAppealManager()/createBanManager()` in plain
class fields; no `wireDurable*`/snapshot sink exists (contrast
`invoice-store.ts:117-124`, the feedback store, reminder store — the codebase's
established durable-snapshot convention). The takedown store is the same pattern
(`takedown-store.ts:17-30`, in-memory providers; its own header says so).

**Spec promise**: features.md 5210–5214 (immutable audit events, retention
enforcement, chain-of-custody); 5285–5292 (appeal lifecycle with audit) —
moderation decisions and bans are compliance records, not ephemera.

**What the code actually does**: a deploy or crash deletes the moderation queue,
all decision history, all appeals, and all active bans (which were unenforced
anyway — finding 7).

**Fix sketch**: wrap both stores with the existing
`createSnapshotSink(DurableSnapshotStore, key)` write-through, serializing the
queue/appeal/ban maps; hydrate at boot like the invoice ledger.

---

### 16. The per-artifact residency guard (V1-PRIV-018) has zero callers

**Severity**: P2

**Evidence**: `middleware/residency-guard.ts` wraps
`ResidencyEnforcementService` with audit emission and 403 translation; repo grep
for `enforceResidency` outside the guard + its spec: **none**. What _is_
enforced is transport-level routing: the onRequest context middleware
(`residency-routing.ts:32`, registered at `app.ts:589`) plus fail-loud zone
routing in `adapters/domain-service-adapters.ts:2593-2629` (503 when a plane
can't satisfy the zone) — real, and credited. But no BFF route ever performs the
artifact-type read/write residency check or emits its audit events.

**Spec promise**: features.md 5676–5690 — residency honored per data artifact
with audit; cross-region exceptions logged.

**Fix sketch**: call `enforceResidency(request, reply, { artifactType })` from
the customer-data route families that the guard's own header lists (memory,
exports, deletion), passing the deployment zone.

---

### 17. Per-data-class retention schedules exist as an orphaned table — nothing enforces them

**Severity**: P2

**Evidence**: `libs/oshun/privacy/src/export-deletion/deletion.ts:38`
`retentionDaysFor` (raw chat 30d, billing 7y, durable profile, per-artifact
policy) — zero app consumers. Conversation history is bounded by a FIFO turn
cap, not time (`conversation/conversation-history-store.ts:12,36`). The
domain-specific retention consoles (isis `retention-policy-store.ts:128`
operator-triggered scan, aja/studio stores) are real but cover their domains
only; no worker applies the platform data-class schedule to chat, support, or
billing records. (The 30-day deletion _grace window_ is enforced —
`data-deletion/state.ts:68` + worker — that is a different mechanism.)

**Spec promise**: features.md 5705–5712 — per-data-class retention rules.

**Fix sketch**: a retention sweep worker (deletion-worker pattern) that walks
the durable stores with `retentionDaysFor` per class; start with raw
conversation turns at 30 days.

---

### 18. `@oshun/privacy` carries a second, orphaned consent taxonomy plus unconsumed compliance/breach/DSAR-intake/privacy-center modules

**Severity**: P2

**Evidence**: the BFF's live consent system is `@oshun/contracts` +
`consent/state.ts`; `libs/oshun/privacy/src/consent/consent.ts` defines a
parallel taxonomy/state machine (`transitionConsent`, `defaultState`,
sensitivity rules) — zero app consumers. Same for `compliance/compliance.ts`
(`regimesFor`, `advanceBreachStage`, `resolveDisclosure`,
`evaluateAuditCompleteness` — the spec's breach-response clock and regime
mapping), `privacy-surface/privacy-surface.ts` (`buildPrivacyCenter`,
reading-grade check), and the `dsar/` intake module (identity verification /
jurisdictional eligibility). Only `deletion-erasers` + `export-deletion`'s
`enqueueDeletion`/`advanceToSoftDelete` are consumed (`server.ts:40`,
`privacy/dsar-erasure-route.ts:20`).

**Spec promise**: features.md 5744–5758 (regulatory regimes, breach response
detect→notify clock, audit completeness tests), 5728–5733 (DSAR intake with
identity verification), 5714–5719 (privacy center).

**What the code actually does**: two consent vocabularies that can drift; no
breach-response machinery anywhere in the runtime; DSAR intake is
operator-constructed JSON rather than a verified customer intake.

**Fix sketch**: either consume these modules (privacy-center route over
`buildPrivacyCenter`; DSAR intake route over `dsar/`) or collapse the duplicate
taxonomy into `@oshun/contracts` and delete the rest — one source of truth.

---

### 19. Settlement HMAC is computed over a re-serialized body, not the raw payload

**Severity**: P2

**Evidence**: `settlement-route.ts:57` —
`const rawBody = JSON.stringify(request.body ?? {})` then HMAC-compares. The
signature therefore depends on Fastify's parse + V8 key ordering matching the
watcher's serialization byte-for-byte; any whitespace, key-order, or number
formatting difference in the sender produces a spurious 401 (or forces the
deploy-bound watcher to mirror this exact re-serialization quirk).

**Spec promise**: features.md 5569–5571 (reliable webhook bus).

**Fix sketch**: register a raw-body content parser for this route and HMAC the
received bytes; keep the timing-safe compare.

---

### 20. UX — there is no privacy center: privacy controls are scattered across four surfaces, two of which are fixtures

**Severity**: UX

**Evidence**: spec 5714–5719 promises "a single in-product surface listing every
consent, memory state, export, deletion, residency choice, and
audit-of-operator-access". Actual: `/profile/data` (real exports/deletions;
**fixture** consent panel, no controls — finding 9), `/profile/memory` (real,
C4), notification preferences only inside the `/settings` + `/profile` panel
(`components/ProfileSettingsPanel.tsx:61`), while the parallel
`/profile/notifications` page renders the prod-503 fixture
`/v1/profile/notifications` with toggles that are "static reads"
(`app/profile/notifications/page.tsx:5,52` vs the real
`/v1/notifications/preferences` PATCH route in
`routes/notifications-preferences.ts:283-298`) — a dead duplicate surface. No
residency-choice surface and no member-visible operator-access audit exist
anywhere.

**Fix sketch**: make `/profile/data` the privacy center: real consent ledger +
memory link + exports/deletions (already there) + residency display; delete or
redirect `/profile/notifications` to the settings panel.

---

### 21. UX — the consumer billing surface has no plan, history, or receipt view

**Severity**: UX

**Evidence**: `/billing` is a static link card (crypto link, profile link,
AAA-upgrade link, refund mailto — `app/billing/page.tsx`). `/billing/crypto`
renders a single invoice by id (paywall flow). No page shows the member's
current plan, invoice history, or receipts even though the data exists
(`customerAuthStateStore` plan; `listCryptoInvoices` — currently only exposed
via the unauthenticated leak, finding 1). Spec 5374–5378 promises billing
history, invoices/receipts, plan details.

**Fix sketch**: an authenticated `/billing` server component reading the session
plan + a user-scoped invoice list (the corrected endpoint from finding 1) with
per-invoice receipt links (finding 11).

---

### 22. UX — operator governance is four parallel consoles with three severity vocabularies and no cross-links

**Severity**: UX

**Evidence**: (1) admin-app workspaces over the seeded per-operator state
(`apps/oshun/admin/src/app/trust-safety|support|privacy`); (2) the
`@aja/content-moderation` queue console (`/v1/admin/moderation`, web
`operator-depth.ts`); (3) the living-scenes abuse inbox
(`/v1/admin/abuse-reports`, severities S1/S2/S3 —
`routes/admin-abuse-reports.ts:18-32`); (4) takedowns + infringers
(`/v1/admin/takedowns`, `/v1/admin/infringers`). Severity is S1–S3 in one,
priority 1–10 in another, P0–P3 in the orphaned trust-safety lib, and
`low…critical` in the admin workspace. A customer `user-report` lands in console
(1); a scene report in (3); a moderation flag in (2) — an operator triaging
"abuse" must know which of four doors to open, and nothing links a user across
them.

**Fix sketch**: adopt the trust-safety lib's P0–P3 as the shared vocabulary
(finding 5 wiring), and add cross-links by subject userId/contentId between the
four stores on each item view.

---

### 23. DEPLOY — recorded deploy-bound remainder (not findings)

**Severity**: DEPLOY

For completeness, the residual deploy-bound set this slice confirmed: the chain
watcher + invoice-target provisioner (quote route correctly 503s;
`payments-composition.ts:16-19`), self-hosted node fleet + multi-RPC consensus

- Tor rate-fetch egress (`oracle-aggregator/` is engine-complete but its
  price-feed needs live exchange creds; note it is _also_ unconsumed in-repo —
  when the provisioner lands, wire `rate-lock` into the quote builder rather
  than re-implementing), hardware-wallet co-signing, and BTCPay/LND
  infrastructure. `OSHUN_CRYPTO_SETTLEMENT_WEBHOOK_SECRET`,
  `OSHUN_PAYMENTS_RECEIPT_*` are env-gated creds = DEPLOY.

---

## Severity counts

| Severity   | Count  |
| ---------- | ------ |
| P0-SEC     | 1      |
| P0-HONESTY | 1      |
| P0-STRUCT  | 4      |
| P1         | 9      |
| P2         | 4      |
| UX         | 3      |
| DEPLOY     | 1      |
| **Total**  | **23** |
