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
CryptoInvoiceRecords 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, wirerate-lockinto 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 |