Skip to content

Latest commit

 

History

History
165 lines (125 loc) · 10.4 KB

File metadata and controls

165 lines (125 loc) · 10.4 KB

Audit Checklist

For the human reviewer. Sections are independent — work in any order. Mark each with PASS / FAIL / N/A and note the file:line where you confirmed it.


A. Build & deploy readiness

  • pnpm install succeeds on a clean clone with the published lockfile
  • pnpm run typecheck is green (note: useColors.ts TS2352 is a known pre-existing item)
  • pnpm --filter @workspace/api-server run build produces artifacts/api-server/dist/index.mjs
  • pnpm --filter @workspace/api-spec run codegen runs cleanly and does not produce uncommitted diffs
  • pnpm --filter @workspace/db run push succeeds against a fresh Postgres
  • API serves /api/healthz200
  • Mobile boots in Expo Go and reaches the welcome screen without console errors
  • Production build args in artifacts/api-server/.replit-artifact/artifact.toml are sh -c "pnpm --filter @workspace/db run push && pnpm --filter @workspace/api-server run build"
  • app.json has ios.infoPlist keys for NSLocationWhenInUseUsageDescription, NSCameraUsageDescription, NSPhotoLibraryUsageDescription
  • app.json has android.permissions: [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION]

B. Auth & session security

  • Every state-changing route uses requireAuth. The codebase mixes single-line (router.post("/x", requireAuth, handler)) and multiline (router.post(\n "/x",\n requireAuth,\n ...\n)) declarations, so any line-oriented pipeline (e.g. grep ... | grep -v requireAuth) leaks false positives on the multiline form. Use a single-regex PCRE pass that excludes requireAuth inside the matched call:

    rg -UP -n 'router\.(post|patch|put|delete)\((?:(?!requireAuth)[\s\S])*?\)' \
       artifacts/api-server/src/routes/

    Expected output (verified against the current code): exactly one match — routes/storage.ts:57 for POST /storage/uploads/request-url. That route is intentionally gated by an inline requireAuth middleware in routes/index.ts before storageRouter mounts, so it is safe. Any additional matches are real authz gaps and must be investigated. (Note: /storage/objects/* is a GET route; it streams private user content and is gated by the same inline middleware in routes/index.ts — verify by reading that file directly.)

  • Public routes are limited to: /api/healthz, /api/storage/public-objects/*filePath, and the production Clerk proxy at /api/__clerk/*. Everything else (including /api/storage/objects/* and POST /api/storage/uploads/*) requires Clerk auth.

  • app.ts refuses to start in production with no ALLOWED_ORIGINS and no REPLIT_DOMAINS

  • CORS allowlist is enforced both at the CORS layer AND at Clerk's authorizedParties

  • Auth is bearer-token via @clerk/express clerkMiddleware; there is no Express session cookie store, so no SESSION_SECRET to manage despite the placeholder in artifacts/api-server/.env.example (unused at runtime — flag for cleanup)

  • Tokens never appear in error responses or req.log output


C. Database & data integrity

  • Foreign keys cascade-delete from users to: profile_photos, presence_buckets, glimpses, glimpse_responses, blocks, reports, chat messages, connections (verify each table in lib/db/src/schema/index.ts)
  • Unique constraints exist on: users.email, one response per (glimpseId, responderId), one photo per publicUrl
  • Indexes support the production read paths: presence by (bucket, time), glimpses by (bucket, eventTime) and (visibleAfter, status), expiry indexes for the 72-hour purge
  • No production migration depends on data backfill that isn't explicitly run
  • DELETE /api/me actually removes the row and all dependents — confirm in psql after a deletion

D. Location & privacy

  • DTOs returned to other users (Discover, Glimpse detail, Responses, Connections, Chat) NEVER include lat / lng for someone else
  • Times shown to other users are fuzzy labels, not exact eventTime
  • Coordinates are bucketed (floor(lat*100), floor(lng*100)) before any cross-user comparison
  • Presence rows are purged after 72h (verify the expiresAt index is acted on by a job or by query-time filter)
  • Glimpses become visible only after visibleAfter (current code: 1-hour delay via VISIBLE_DELAY_MS in routes/glimpses.ts; reconcile with any product copy that still says 4 hours before App Review)
  • lib/location-validation is the primary source of shared constants and helpers used by both client pre-flight and server enforcement. Known duplication: artifacts/api-server/src/routes/presence.ts re-declares its own BUCKET_FACTOR = 100 instead of importing from the shared package — flag as a P1 cleanup, not a blocker
  • Server enforcement of presence gate is authoritative — client pre-flight is advisory

E. Glimpse creation & discovery

  • Create rejects payloads that fail Zod validation
  • Create rejects when the author has no qualifying presence row
  • /discover only returns Glimpses where viewer has presence in the 3×3 neighborhood (NEIGHBOR_BUCKET_RADIUS = 1) of the Glimpse's bucket and within ±2 hours (PRESENCE_WINDOW_SECONDS = 7200) of the Glimpse's eventTime — see routes/glimpses.ts ~line 309
  • /discover excludes Glimpses authored by the viewer
  • /discover excludes Glimpses involving blocked users in either direction
  • /discover does not return precise coordinates or precise event times of other users' Glimpses
  • /glimpses/me only returns the caller's own Glimpses
  • DELETE /glimpses/:id enforces author ownership
  • Cascade on delete clears responses, connections, chat messages tied to that Glimpse

F. Responses, confirmation, and chat

  • Responder cannot respond to their own Glimpse
  • Responder cannot respond twice to the same Glimpse (unique constraint)
  • Only the Glimpse author can confirm or decline a response
  • Chat is locked until response status is confirmed for both sides
  • Chat list endpoint excludes connections involving blocked users
  • Sending a message verifies the sender is a participant in that connection

G. Moderation, blocks, and reports

  • Block is two-way enforced — blocked user disappears from both users' Discover, Responses, Connections, Chat
  • Existing chat messages from a blocked user are hidden in subsequent fetches
  • Unblock restores visibility
  • POST /reports runs isInappropriate on the detail text before persisting
  • Reports persist with reporterId, target, reason, optional detail, timestamp
  • Rate limits on block POST/DELETE prevent abuse

H. Profile & photos

  • Photo upload uses signed URL flow — server never proxies the bytes
  • User cannot bind an objectPath they did not upload — POST /me/photos parses the user id out of the path and rejects mismatches (see routes/photos.ts ~line 100)
  • PUT /me/photos is reorder-only and does not accept new bind payloads
  • 6-photo cap is enforced server-side
  • Reordering preserves the position integer cleanly
  • DELETE /me/photos/:id removes the row and compacts positions. NOTE: current code does NOT delete the underlying storage object — orphan objects accumulate. Flag as a P1 cleanup, not a blocker
  • Public profile DTO does not leak email or other private fields

I. Account deletion

  • Client calls DELETE /api/me before signOut
  • Server deletes Clerk user (404 treated as success)
  • DB transaction cascades through every dependent table
  • DB failure returns 500 (so client can retry) rather than silently succeeding
  • Re-signing up with the same email is possible afterward

J. App Store / TestFlight readiness

  • app.json has ios.bundleIdentifier and ios.buildNumber set
  • eas.json exists with production profile reading from EAS Secrets
  • EXPO_PUBLIC_TERMS_URL and EXPO_PUBLIC_PRIVACY_URL are set in EAS production secrets and resolve to live, public 200 pages
  • EXPO_PUBLIC_DOMAIN in EAS production points to the deployed *.replit.app, NOT the dev workspace domain
  • EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in EAS production is a pk_live_… from a separate Clerk production instance
  • CLERK_SECRET_KEY in deployment secrets is sk_live_… from the same prod Clerk instance
  • Production DATABASE_URL is a separate database from dev
  • App Store Connect privacy questionnaire is complete and accurate
  • Demo reviewer credentials documented in App Review notes
  • App Review notes explain location use (presence-gate, anti-spam, coarse coords, never shared)
  • Welcome screen shows tappable Terms + Privacy under the "by creating an account…" line

K. Mobile QA on a real device

Walk through these on at least one physical iPhone. Note iOS version.

  • Cold launch → welcome screen renders, fonts loaded, no flash of black
  • Sign up → email verification → 18+ gate → mandatory photo → tabs
  • Location permission denial path is graceful (no crash, clear message)
  • Drop with valid presence succeeds; Drop without presence is blocked with a clear hint
  • Place picker autocomplete returns results within ~1s
  • "Use current location" works when permission is granted
  • Discover renders cards; tapping opens detail
  • Respond → switch account → confirm → both sides see chat unlocked
  • Photo upload from camera and from library both succeed
  • Reorder and "set main" persist after app restart
  • Block → blocked user no longer appears anywhere
  • Report → server returns 200, no PII echoed back
  • Delete account → app returns to welcome → sign-up with same email works
  • Tap Terms and Privacy on welcome and in Settings — both open in-browser, fallback Alert if URL is invalid
  • Background the app for 5+ minutes, foreground — session restores

L. Production posture (post-deploy)

  • Production /api/healthz returns 200 from the deployed domain
  • Production logs (Pino) do not show any tokens, secrets, or stack traces leaking to clients
  • Trying CORS from an unlisted origin is rejected (curl -H "Origin: https://evil.example" -v)
  • Trying any protected route without a Clerk token returns 401
  • Trying to read another user's photo by guessed URL is denied
  • Trying to confirm someone else's response returns 403/404