Replaces phase3.3-testing.md. New doc covers: - Part 1: Phase 3.4 self-managed auth — backend curl matrix, CC UI (cookie refresh + bridge), mitra_app + client_app (anonymous → upgrade, OTP stub codes, social behind flag), cross-app WS handshake - Parts 2-4: Phase 3.3 topic sensitivity + 3.2 overlay/E2E/iOS + 3/3.1 session lifecycle / chat mechanics / navigation — verbatim carry-over - Part 5: Cross-cutting regression after 3.4 merge, platform coverage, security/negative (JWT leak, refresh rotation, cookie flags), and Known Blockers / Deferred updated for 3.4 reality (Valkey revocation, merge-on-link, firebase_uid drop, real Fazpass, social creds, Apple Dev prereqs, JWT rotation procedure) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
29 KiB
Phase 3.4 Testing & Outstanding Regression Checklist
Consolidated testing document covering Phase 3.4 (self-managed auth) plus every outstanding test item carried over from Phases 3.0 – 3.3. Replaces phase3.3-testing.md.
Tick boxes as you verify. Cluster labels in brackets: [BE] backend / curl, [CC] control_center, [M] mitra_app, [C] client_app.
Related docs: phase3.4.md, phase3.4-plan.md, phase3.3.md, phase3.3-plan.md.
Part 1 — Phase 3.4: Self-Managed Auth
1.1 Backend: Schema, Services, Env
- [BE]
npm run db:migrateis idempotent — re-running reports "skipping" on existing objects, adds new 3.4 tables/columns, exits 0 - [BE]
auth_sessions,otp_requeststables exist with correct columns + indexes - [BE]
customershas new nullable columns:email,google_sub (UNIQUE),apple_sub (UNIQUE) - [BE]
control_center_usershaspassword_hash,failed_login_count,lockout_until - [BE]
firebase_uidcolumns still exist but are nullable + unused by code (cleanup migration deferred) - [BE]
app_configseeded with 6 new OTP / CC-lockout keys at default values - [BE]
npm run db:seedcreates the super admin with bcrypt-hashed password (runs twice → second is a no-op) - [BE] Server boots cleanly on
INTERNAL_PORT=3001+PUBLIC_PORT=3000 - [BE] Missing
AUTH_JWT_SECRET(or <32 chars) → server refuses to start / logs a clear error - [BE] CORS config on internal listener allows
CC_ORIGINwithcredentials: true
1.2 Backend: Token & Session Service (curl)
- [BE] Access token claims:
{ sub, user_type, session_id, iat, exp }— HS256, 1h TTL - [BE] Refresh token is opaque (uuid.random), 30d TTL, bcrypt-hashed in
auth_sessions.refresh_token_hash - [BE] Refresh rotates: old refresh after one use →
REFRESH_INVALID, samesession_idpersists, new access token issued - [BE] Logout deletes the
auth_sessionsrow → subsequent refresh →REFRESH_INVALID - [BE] Multi-device: same user signs in twice → two
auth_sessionsrows; logout on one doesn't kill the other - [BE] Tampered JWT (signature broken) → 401
TOKEN_INVALID - [BE] Expired JWT → 401; api_client auto-refreshes and retries once
- [BE]
device_infoJSONB populated withuser_agent+ipon each session - [BE] Session fingerprint survives refresh (no churn)
- [BE] Documented 1h access-token revocation window: post-logout access token still verifies until natural expiry (expected — Valkey revoked_sessions pre-wired but not active)
1.3 Backend: OTP Service (stub mode)
Fazpass is stubbed. Dev code is logged to backend console as
[OTP STUB] phone=… code=… ref=….
- [BE]
POST /api/client/auth/otp/requestwith valid phone → 200 +otp_request_id, stub logs code - [BE]
POST /api/mitra/auth/otp/requestsame, separate user_type - [BE] Invalid phone format (not E.164) → 422
PHONE_INVALID - [BE] Resend within
otp_resend_cooldown_seconds(default 60) → 429OTP_COOLDOWN - [BE] 4th request in an hour from same phone → 429
OTP_RATE_LIMIT_PHONE - [BE] 11th request in an hour from same IP → 429
OTP_RATE_LIMIT_IP - [BE] OTP verify with wrong code → 401
CODE_MISMATCH,attemptsincremented - [BE] 6th verify attempt (after 5 wrongs) → 429
OTP_ATTEMPTS_EXCEEDED - [BE] OTP verify after
expires_at(default 5min) → 410OTP_EXPIRED - [BE] OTP verify twice with correct code → second call → 409
OTP_USED - [BE] Mitra OTP verified via
/api/client/auth/otp/verify→ 400WRONG_FLOW - [BE] Customer OTP verified via
/api/mitra/auth/otp/verify→ 400WRONG_FLOW
1.4 Backend: Social Identity (post-creds)
Deferred until Google/Apple OAuth credentials land. Run this block after setting
GOOGLE_OAUTH_CLIENT_IDS+APPLE_*in.envand flippingENABLE_SOCIAL_AUTH=trueon client_app.
- [BE] Google: valid id_token → customer created / upgraded, tokens issued
- [BE] Google: wrong audience (id_token from different client) → 401
INVALID_ID_TOKEN - [BE] Google: tampered signature → 401
INVALID_ID_TOKEN - [BE] Google:
google_subalready linked to another customer → 409IDENTITY_CONFLICT(merge deferred) - [BE] Apple: valid id_token → customer created / upgraded
- [BE] Apple: invalid signature (JWKS mismatch) → 401
INVALID_ID_TOKEN - [BE] Apple: missing email (user opted out) → account still created,
emailNULL - [BE] Apple:
apple_subalready linked elsewhere → 409IDENTITY_CONFLICT
1.5 Backend: Anonymous + Upgrade
- [BE]
POST /api/shared/auth/anonymous→ creates customer with auto-generateddisplay_name, returns tokens + profile - [BE] Anonymous customer row:
phone=NULL,google_sub=NULL,apple_sub=NULL - [BE] OTP verify with
anonymous_customer_id→ upgrades SAME customer row; customer UUID unchanged; display_name preserved - [BE] Google verify with
anonymous_customer_id→ upgrades SAME customer row; google_sub added; display_name preserved if present, else backfilled from Google profile - [BE] Apple verify with
anonymous_customer_id→ upgrades SAME row - [BE] Upgrade with
anonymous_customer_idwhen identity is ALREADY taken by a different customer → 409IDENTITY_CONFLICT(anonymous row is NOT deleted) - [BE] Upgrade issues a NEW auth_sessions row. Old anonymous refresh still works (separate session) and reflects the upgraded profile on subsequent calls — intentional for multi-device UX; do not "fix" without discussion
1.6 Backend: Auth Middleware + Cross-User-Type Guards
- [BE] Protected route with no
Authorizationheader → 401AUTH_MISSING - [BE] Expired access token → 401 → api_client refreshes → retry succeeds
- [BE] Customer JWT calling
/api/mitra/auth/me→ 403FORBIDDEN - [BE] Mitra JWT calling
/api/client/auth/me→ 403FORBIDDEN - [BE] Customer JWT calling
/internal/*→ 403FORBIDDEN - [BE]
request.auth = { userType, userId, sessionId }populated correctly on every protected route (spot-check in logs)
1.7 Backend: Mitra Activation + Inactive Flow
- [BE] New phone number → auto-creates
mitrasrow withis_active=false; OTP verify returns 403ACCOUNT_INACTIVE - [BE] Admin activates mitra via CC (or direct DB
UPDATE mitras SET is_active=true) → re-request OTP → verify succeeds, tokens returned - [BE] Mitra re-OTP after deactivation → 403
ACCOUNT_INACTIVEon next verify; existing sessions keep working until natural JWT expiry (documented 1h window)
1.8 Backend: Password / CC Login
- [BE]
POST /internal/auth/loginwith correct creds → setscc_refresh_tokenhttpOnly cookie, returns access token + profile + role/permissions - [BE] Wrong password → 401
INVALID_CREDENTIALS,failed_login_countincremented - [BE] 5th wrong password →
lockout_until = NOW() + 15min; subsequent attempts (even with right password) → 423ACCOUNT_LOCKED - [BE] Successful login resets
failed_login_count+lockout_until - [BE]
PATCH /internal/control-center-users/me/passwordwith wrong current → 401INVALID_CREDENTIALS - [BE] Password complexity rejected: <8 chars →
PASSWORD_TOO_SHORT; no digit →PASSWORD_MISSING_DIGIT; no upper →PASSWORD_MISSING_UPPERCASE; no lower →PASSWORD_MISSING_LOWERCASE - [BE] Admin-forced reset via
PATCH /internal/control-center-users/:id/passwordrequirescontrol_center_users:updatepermission - [BE] Create CC user
POST /internal/control-center-usersrequirescontrol_center_users:create; stores bcrypt hash; rejects weak password with same complexity codes - [BE] Seed script does NOT enforce complexity (intentional bootstrap loophole — dev
admin123works)
1.9 Backend: WebSocket Auth
- [BE] WS handshake with valid JWT in first
{type:"auth", token}frame →{type:"auth_ok"} - [BE] WS handshake with missing token → connection closed with code 4401 (or equivalent)
- [BE] WS handshake with expired/tampered token → closed
- [BE]
request.authequivalent available on WS connection (userId + userType + sessionId) - [BE] Old Firebase-era token on WS → rejected (documented expected failure from pre-3.4 builds)
1.10 Control Center: UI + Auth
- [CC]
firebasedep is gone frompackage.json; bundle is ~280KB not ~800KB - [CC] Login page: right creds → redirects to
/dashboard - [CC] Login page: wrong creds → inline error "Email atau password salah."
- [CC] Login page: account-locked → shows backend message (or translated Indonesian)
- [CC] Refresh cookie persists across a hard browser reload (F5) → user stays logged in
- [CC] Close tab + reopen →
AuthContext.bootstrapcalls/internal/auth/refreshwith the cookie → user stays logged in - [CC] Logout clears access token in memory + calls
/logout+ clears cookie → next action redirects to login - [CC] Access token expires mid-session → next api call 401 → bridge auto-refreshes → request succeeds transparently (simulate by breaking the access token in devtools)
- [CC] Refresh fails (cookie expired / revoked) →
onUnauthenticated→ user bounced to login - [CC] Two concurrent 401s (fire two API calls in parallel with a broken token) → only one refresh fires; both retries succeed
- [CC] Sidebar "Ganti password" form: wrong current → "Password saat ini salah."
- [CC] Sidebar "Ganti password" form: weak new password → shows backend complexity message
- [CC] Sidebar "Ganti password" form: success → "Password berhasil diubah." (and next login uses the new password)
- [CC] UsersPage create: all fields required; Generate button produces a compliant password; submit creates user
- [CC] UsersPage create: duplicate email → inline error ("Email sudah digunakan.")
- [CC] UsersPage per-row "Reset password": success → inline "Tersimpan."; the target user can then log in with that password
- [CC] All API calls send
credentials: 'include'(verify via devtools network tab)
1.11 mitra_app: UI + Auth
- [M]
firebase_authdep is gone frompubspec.yaml; app builds debug APK clean - [M] First launch with no stored session → lands on
/login - [M] Phone OTP request → OTP screen; backend console shows
[OTP STUB] phone=… code=… - [M] Enter stub code →
/home - [M] Kill app, relaunch → bootstrap reads refresh from secure storage →
/home(no login screen flash) - [M] Inactive mitra → snackbar "Akun tidak aktif. Hubungi administrator."
- [M] Wrong OTP code → snackbar "Kode OTP salah." Fields cleared, focus returns to first digit
- [M] OTP expired (wait 5min) → "Kode OTP kedaluwarsa. Minta kode baru."
- [M] Resend within cooldown → "Tunggu sebentar..." message
- [M] Logout button → storage cleared; next launch hits
/login - [M] Long session: access token expires mid-chat → api_client auto-refreshes; user sees no disruption
- [M] Refresh token expires / revoked →
onUnauthenticated→ app returns to/loginwith current screen cleared - [M] WebSocket handshake uses JWT from
AuthBridge— verify by logging the token frame in dev - [M] iOS: keychain persists the refresh token across app updates (reinstall DOES wipe it, but update keeps it)
- [M] Android: encrypted SharedPrefs persists across app updates
1.12 client_app: UI + Auth
- [C]
firebase_authdep is gone; app builds debug APK clean - [C] First launch → onboarding → welcome screen
- [C] "Lanjut sebagai Tamu" → DisplayNameScreen → enter name →
/home(anonymous customer created, display_name PATCHed) - [C] Kill app, relaunch → bootstrap succeeds →
/home(anonymous session restored) - [C] "Daftar / Masuk" → phone field → OTP → home (customer created with phone)
- [C] From anonymous state, tap "Daftar / Masuk" → OTP upgrade → same customer_id, display_name preserved, phone added (verify via
/me) - [C] OTP error codes all render correct Indonesian messages (
CODE_MISMATCH,OTP_EXPIRED,IDENTITY_CONFLICT, etc.) - [C] Anonymity config
anonymity_enabled=false→ after anonymous sign-in, router sends to/auth/force-register - [C] From ForceRegister, complete phone OTP → lands in
/homewith upgraded profile - [C] Google/Apple buttons are hidden by default (ENABLE_SOCIAL_AUTH=false)
- [C]
--dart-define=ENABLE_SOCIAL_AUTH=true→ buttons appear; tapping without backend creds returns clear error (no crash) - [C] After Google/Apple creds are live: sign-in with fresh account works; sign-in with existing-elsewhere account →
IDENTITY_CONFLICTsurfaced as "Akun ini sudah terhubung..." - [C] Logout from home → returns to welcome (storage cleared)
- [C] Set display name screen: empty input disabled; success → home
- [C] Long session: access token expires → auto-refresh transparent
- [C] Refresh expires → unauthenticated → welcome screen, not a broken home
1.13 Cross-App / WebSocket / Regression After Auth Change
- [BE][M][C] WS auth in chat (
chat_notifierclient +mitra_chat_notifier) works with the new JWT - [BE][M][C] WS auth in pairing (
pairing_notifierclient +chat_request_notifiermitra) works with the new JWT - [BE][M][C] Full chat flow: pair → chat → extension → closure — using the new JWT end-to-end
- [BE] FCM device-token registration endpoint (
/api/shared/device-token) still works; Authorization header is the new JWT
Part 2 — Phase 3.3 Carry-Over: Topic Sensitivity
2.1 Database / Migration
- Migration runs cleanly on an existing dev DB (no errors, all
IF NOT EXISTS/ON CONFLICTpaths hit) chat_sessions.topic_sensitivitycolumn exists with default'regular'and NOT NULL- Existing sessions (created before the migration) have
topic_sensitivity = 'regular'after migration session_sensitivity_logtable exists with correct FKs (sessions, mitras)idx_chat_sessions_topic_sensitivityindex createdidx_session_sensitivity_log_sessionindex createdapp_confighassensitive_flip_confirmation_enabled = trueby defaultapp_confighassensitive_flag_one_way_latch = falseby default
2.2 Customer Flow (client_app)
Topic selection bottom sheet
- Tap "Mulai Curhat" → topic selection bottom sheet appears
- Sheet cannot be dismissed by tapping outside
- Sheet cannot be dismissed by swiping down
- System back button cancels entire "Mulai Curhat" flow (does NOT open pricing)
- Copy matches PRD (title, body, sub-question, helper line)
- "Topik umum" button styling is primary
- "Topik sensitif" button styling is secondary but equal weight (not de-emphasized)
Request submission
- Tap "Topik umum" → pricing sheet opens with topic pre-selected as regular
- Tap "Topik sensitif" → pricing sheet opens with topic pre-selected as sensitive
- After pricing confirm →
POST /api/client/chat/requestbody includestopic_sensitivity: regular|sensitive - Backend rejects request with missing
topic_sensitivity(400 BAD_REQUEST) - Backend rejects request with invalid
topic_sensitivityvalue (e.g.,"other") - Created
chat_sessionsrow has correcttopic_sensitivityvalue
Customer UI after request
- Chat screen stays pink (no yellow), regardless of flag
- Customer history screen: no badge on any row regardless of flag
- Customer transcript screen: stays pink
- Customer receives
session_topic_updatedWS message after mitra flip → no UI change, no error, no crash
2.3 Mitra Flow — Incoming Request (mitra_app)
- Regular request: overlay has no badge, no yellow accent
- Sensitive request: overlay shows "Topik sensitif" badge + yellow accent
- Overlay payload from WS includes
topic_sensitivity - Overlay payload from FCM fallback includes
topic_sensitivity(or app fetches on open) - Overlay payload from
getPendingRequestsForMitra(app-resume path) includestopic_sensitivity - Mitra accepts sensitive request → lands in active chat screen with correct flag
2.4 Mitra Flow — Active Chat Screen
- Sensitive active session: yellow doodle background
- Regular active session: pink doodle (unchanged behavior)
- Header banner shows "Topik sensitif" label only when sensitive
- App-bar toggle icon visible (flag / flag_outlined depending on state)
Flip toggle — confirmation enabled (default)
- Tap toggle regular → sensitive → dialog appears: "Tandai sesi ini sebagai sensitif?"
- "Batal" cancels, no state change, no audit log entry, no WS broadcast
- "Tandai" flips, background turns yellow instantly, log entry created
- Tap toggle sensitive → regular → dialog: "Tandai sesi ini sebagai topik umum?"
Flip toggle — confirmation disabled (via CC config)
- Toggle
sensitive_flip_confirmation_enabledtofalsein CC - Flip happens immediately, toast "Sesi ditandai sensitif" / "Sesi ditandai topik umum"
- No dialog appears
One-way latch — disabled (default)
- Can flip regular → sensitive → regular → sensitive freely
One-way latch — enabled (via CC config)
- Toggle
sensitive_flag_one_way_latchtotruein CC - Session that was regular: can flip to sensitive; toggle then disabled
- Session that was already sensitive at latch-enable time: toggle disabled with tooltip
- Attempt to flip sensitive → regular with latch on: API returns 409
SENSITIVITY_LATCHED - Error dialog shown: "Sesi sudah ditandai sensitif dan tidak bisa diubah kembali."
Audit trail
- Every successful flip creates a
session_sensitivity_logrow with correctfrom_value,to_value,changed_by_mitra_id - No-op flip (e.g., tap confirm but value didn't actually change) does NOT create a log row
- Log entries ordered correctly (ascending
created_at)
2.5 Mitra Flow — Extension
- Customer requests extension on regular session → mitra extension card has no badge
- Customer requests extension on sensitive session → mitra extension card shows "Topik sensitif" badge + yellow accent
- Mitra flipped session mid-chat regular → sensitive, then customer requests extension → extension card reflects current sensitive flag
- Extension accepted → flag carries over unchanged to extended session
2.6 Mitra Flow — History & Transcript
- Mitra history list row shows "Topik sensitif" badge for sensitive sessions
- Mitra history list row has no badge for regular sessions
- Mitra transcript view: sensitive session → yellow doodle background
- Mitra transcript view: regular session → pink doodle
- Customer-side history and transcript: always pink, no badge
2.7 Mitra Flow — Edge Cases
- Mitra tries to flip flag on a session they don't own → 403 FORBIDDEN
- Mitra tries to flip flag on a
CLOSINGsession → 409 SESSION_NOT_ACTIVE - Mitra tries to flip flag on a
COMPLETEDsession → 409 SESSION_NOT_ACTIVE - Mitra tries to flip flag on an
EXPIREDsession → 409 SESSION_NOT_ACTIVE - Invalid
topic_sensitivityvalue sent to PATCH endpoint → 400 BAD_REQUEST - Customer tries to call PATCH endpoint → 403 FORBIDDEN (only mitra allowed)
2.8 Control Center — Settings
- Settings page has new "Sensitivitas Topik" section
sensitive_flip_confirmation_enabledcheckbox reflects current backend valuesensitive_flag_one_way_latchcheckbox reflects current backend value- PATCH
/internal/config/sensitivitypersists changes - Changes take effect immediately on next mitra flip (no app restart needed on mitra side, if mitra fetches config dynamically)
2.9 Control Center — Sessions Page
- Sessions list has new filter dropdown (All / Umum / Sensitif)
- Filter "Sensitif" returns only sessions with
topic_sensitivity = 'sensitive' - Filter "Umum" returns only
regular - Filter "All" returns everything (backward-compatible)
- Filter works combined with existing status filter
- Session list has new "Topik" column showing badge (green "Umum" / yellow "Sensitif")
2.10 Control Center — Session Detail
- Session detail page shows current
topic_sensitivity - Session detail shows sensitivity audit trail timeline: "Mitra {name} menandai topik sebagai {from→to} pada {timestamp}"
- Timeline ordered ascending
- Sessions with no flips show empty timeline (no error)
2.11 Control Center — Dashboard
- Dashboard shows "Sesi Sensitif" card with total count + 30-day % breakdown
- Percentage math correct (sensitive / total × 100, rounded to 1 decimal)
- Edge case: 0 sessions in last 30 days → shows
0%notNaN
2.12 Control Center — Mitra Activity
- Summary table has new columns: Sensitive Total, Sensitive Accepted, Sensitive Rate (%)
- Mitra with 0 sensitive requests shows
—(not0%) - Sensitive rate computed correctly (sensitive_accepted / sensitive_total × 100)
- Detail log table: optional new "Topik" column with badge
- Date range filter still works with new columns
2.13 Control Center — Integration Regression
- Existing settings (anonymity, free-trial, extension-timeout, early-end, mitra-ping, price-tiers) still work under the new auth
- Existing sessions filter (by status) still works
- Existing dashboard cards still render
Part 3 — Phase 3.2 Carry-Over
3.1 Chat Request Overlay
- Multiple concurrent chat requests — verify queue behavior (one shown at a time, next appears when current resolved)
- Stale request: "cancelled by customer" message shown + requires acknowledge (no auto-dismiss)
- Stale request: "accepted by other bestie" message shown + requires acknowledge
- Stale request: "expired" message shown + requires acknowledge
- Swipe-to-dismiss (ignore) does NOT send reject to backend
- Ignored request eventually logs as
ignoredinchat_request_notificationsafter 60s timeout - Request
missed(another mitra accepted first) logs correctly active_session_countcaptured correctly at notification creation
3.2 End-to-End Flows
- Full chat flow: pair → chat → extension → closure (customer + mitra)
- Goodbye flow: session expires → closing → both submit goodbye → completed
- Extension accepted mid-flow → session resumes, timer extends, no grace timer lingering
- Extension rejected → session moves to closing, both see closure UI
- Extension timeout (no mitra response) → closing
3.3 iOS Coverage
- OTP login on iOS (customer) — update: flow now uses stub Fazpass OTP from backend console, no native reCAPTCHA
- OTP login on iOS (mitra) — same as above
- Push notifications on iOS (customer + mitra)
- FCM token registration on iOS
- Chat screen rendering on iOS
- Back button behavior on iOS (deep-link pop fallback)
- Overlay on iOS (iOS setup started but incomplete per prior memory)
- Splash screen on iOS
- Onboarding carousel on iOS
- Keyboard handling on iOS (chat input, goodbye form)
Part 4 — Phase 3 / 3.1 Carry-Over
4.1 Session Lifecycle
- Server restart mid-session: session timer is restored from DB (
restoreActiveTimers) - Stale active sessions auto-complete on restart
- Closing sessions with stale grace timers auto-complete on restart
- Session expired from customer side (5-min countdown display)
- Abandoned session during closure grace period → auto-completes
- Known limitation: multi-instance backend sessions not supported until Valkey keyspace notifications implemented (out of scope — see
project_session_timer_scalingin memory)
4.2 Chat Mechanics
- Message status transitions (sent → delivered → read) work correctly
- Typing indicator shows/hides correctly on both sides
- Messages received while backgrounded are marked
deliveredon foreground resume - Messages viewed are marked
readand the read receipt propagates back to sender - Unread badge on home screen updates correctly (client_app + mitra_app)
4.3 Navigation / UI
- All navigation uses
GoRouter.context.push/go(no leftoverNavigator.pushNamed) - Deep-linked screens work with
canPopfallback +PopScope notification_serviceusesgo(notpush) for terminal states- Splash screen hides auth loading flash on both apps
- Goodbye views use
SingleChildScrollView(no keyboard overflow)
4.4 Control Center Settings
- Free trial config: toggle + duration edit
- Extension timeout: edit seconds
- Early end: toggle mitra / customer independently
- Mitra ping: toggle require + interval
- Price tiers: add / edit / remove tiers and verify client_app pricing sheet reflects changes
Part 5 — Cross-Cutting / Pre-Release
5.1 Regression Checks (after Phase 3.4 merge)
- Existing customer auth flow still works (welcome → OTP → register → home) — under new JWT
- Existing mitra auth flow still works — under new JWT
- Existing control center login still works (admin with rotated password) — under new cookie-based refresh
- Pairing flow (mulai curhat → matched) still works end-to-end under new auth
- All existing WS messages still processed (no regressions from
session_topic_updatedhandler or JWT handshake)
5.2 Platform Coverage
- Android: client_app on emulator (Medium_Phone_API_36.1)
- Android: mitra_app on physical device (SM-A530F, 52002a5db8e0c46b)
- iOS: client_app (Mac + simulator / physical)
- iOS: mitra_app (Mac + simulator / physical)
- Control center: Chrome latest
- Control center: Firefox / Safari (if required)
5.3 Load / Concurrency (sanity)
- 2 concurrent customers requesting chat at the same time — both find a mitra (or one waits)
- 1 customer, 5 mitras online — blast notification reaches all 5
- Mitra accepts after another mitra already accepted → receives
missedwithaccepted_by_other - Backend restart with active sessions → timers restored, no data loss
- 3 concurrent CC admins logging in → each gets its own
auth_sessionsrow; logout on one doesn't affect others
5.4 Config Flag Interactions
sensitive_flag_one_way_latch = true+ existing sensitive session → toggle disabledsensitive_flip_confirmation_enabled = false+ rapid flips → no race, all logged in order- Anonymity toggle flip (enabled → disabled) while a user has an active anonymous session → next bootstrap routes to ForceRegister
- Anonymity toggle flip (disabled → enabled) → no-op for already-identified users; new users can go anonymous again
5.5 Security / Negative
- JWT secret leak simulation: a manually-signed token with the wrong secret → 401 across all surfaces
- Token from a deleted
auth_sessionsrow still verifies for up to 1h (documented window); revoke immediately with future Valkeyrevoked_sessionsset - Refresh token used twice in parallel → only one succeeds (rotation-on-use); second →
REFRESH_INVALID - Refresh token stolen + used from a different IP → still works (documented design — rotation + bcrypt is the defense);
device_infologs the new IP for audit - Control center cookie
SameSite/Secure/HttpOnlyflags correct in prod (verify via devtools in staging)
5.6 Known Blockers / Deferred
Not tests — tracked so they don't get forgotten:
- Valkey keyspace notifications — required for multi-instance session timers + real token revocation
- Mitra QC auto-flag — auto-flagging high-rejection mitras on CC dashboard
- Merge-on-link for social login (currently reject-on-existing with
IDENTITY_CONFLICT) - Drop
firebase_uidcolumns — cleanup migration once no code path references them - Real Fazpass integration — replace stub in
otp.service.jswhen API docs + creds arrive - Google + Apple OAuth creds — provision then flip
ENABLE_SOCIAL_AUTH=trueand run §1.4 + client_app social blocks - Apple Developer setup — Services ID +
.p8+ Team ID + Key ID before iOS Apple-sign-in E2E - JWT secret rotation procedure — documented but not implemented (dual-secret window plan in
phase3.4-plan.md)