evaldoc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.env.staging.example +19 -0
  2. package/.firebaserc +6 -0
  3. package/.github/workflows/auto-merge-to-main.yml +28 -0
  4. package/.github/workflows/ci.yml +27 -0
  5. package/.github/workflows/npxhub-publish.yml +170 -0
  6. package/.prettierignore +4 -0
  7. package/.prettierrc.json +7 -0
  8. package/README.md +142 -0
  9. package/SRS.md +448 -0
  10. package/apphosting.staging.yaml +48 -0
  11. package/apphosting.yaml +50 -0
  12. package/bin/evaldoc.cjs +105 -0
  13. package/cors.json +8 -0
  14. package/docs/ARCHITECTURE.md +769 -0
  15. package/docs/ARCHITECTURE_SUMMARY.md +323 -0
  16. package/docs/DEPLOYMENT.md +86 -0
  17. package/docs/DEPLOYMENT_STEPS.md +373 -0
  18. package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
  19. package/docs/DEPLOY_STORAGE_RULES.md +82 -0
  20. package/docs/DOCTOR_TUTORIAL.md +467 -0
  21. package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
  22. package/docs/FIXES_APPLIED.md +128 -0
  23. package/docs/IMPLEMENTATION.md +498 -0
  24. package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
  25. package/docs/LLM-SETUP.md +216 -0
  26. package/docs/MANUAL_TEST_GUIDE.md +327 -0
  27. package/docs/PATIENT_GUIDE.md +451 -0
  28. package/docs/PLATFORM_OVERVIEW.md +194 -0
  29. package/docs/QUICKSTART.md +252 -0
  30. package/docs/QUICK_REFERENCE.md +222 -0
  31. package/docs/READY_TO_TEST.md +383 -0
  32. package/docs/RUN.md +237 -0
  33. package/docs/START_TESTING.md +130 -0
  34. package/docs/STRIPE_CONNECT_SETUP.md +454 -0
  35. package/docs/SYSTEM_READY.md +345 -0
  36. package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
  37. package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
  38. package/docs/firebasefiles.md +21 -0
  39. package/docs/homepage.md +1208 -0
  40. package/docs/payments.md +159 -0
  41. package/eslint.config.js +76 -0
  42. package/firebase.json +27 -0
  43. package/firestore.indexes.json +88 -0
  44. package/firestore.rules +125 -0
  45. package/functions/package-lock.json +2884 -0
  46. package/functions/package.json +20 -0
  47. package/functions/src/analytics.js +101 -0
  48. package/functions/src/audit.js +28 -0
  49. package/functions/src/auth.js +19 -0
  50. package/functions/src/drafts.js +128 -0
  51. package/functions/src/feedback.js +164 -0
  52. package/functions/src/firebase-init.js +29 -0
  53. package/functions/src/i18n.js +134 -0
  54. package/functions/src/limits.js +27 -0
  55. package/functions/src/llm.js +271 -0
  56. package/functions/src/ner.js +162 -0
  57. package/functions/src/notifications.js +273 -0
  58. package/functions/src/orgs.js +127 -0
  59. package/functions/src/patient-api.js +545 -0
  60. package/functions/src/payment.js +830 -0
  61. package/functions/src/safety.js +188 -0
  62. package/functions/src/stripe-config.js +33 -0
  63. package/functions/src/topics.js +94 -0
  64. package/functions/src/triage.js +191 -0
  65. package/functions/src/utils.js +28 -0
  66. package/functions/src/validate.js +177 -0
  67. package/functions/test-firebase.js +36 -0
  68. package/jest.config.js +18 -0
  69. package/package.json +52 -0
  70. package/public/404.html +93 -0
  71. package/public/app/analytics.html +336 -0
  72. package/public/app/analytics.js +177 -0
  73. package/public/app/app.css +1400 -0
  74. package/public/app/app.js +1754 -0
  75. package/public/app/config.js +14 -0
  76. package/public/app/inbox-threads.js +843 -0
  77. package/public/app/inbox.html +598 -0
  78. package/public/app/inbox.js +397 -0
  79. package/public/app/index.html +612 -0
  80. package/public/assets/logo.jpg +0 -0
  81. package/public/chat.html +137 -0
  82. package/public/css/chat.css +870 -0
  83. package/public/css/landing-enhance.css +298 -0
  84. package/public/css/styles.css +1198 -0
  85. package/public/favicon.svg +5 -0
  86. package/public/find-doctor.html +220 -0
  87. package/public/index.html +399 -0
  88. package/public/js/chat-threads.js +513 -0
  89. package/public/js/chat.js +832 -0
  90. package/public/js/search.js +41 -0
  91. package/public/privacy.html +101 -0
  92. package/public/robots.txt +6 -0
  93. package/public/site.webmanifest +12 -0
  94. package/public/terms.html +110 -0
  95. package/scripts/create-test-provider.js +55 -0
  96. package/scripts/deploy-staging.sh +51 -0
  97. package/scripts/fix-provider.js +39 -0
  98. package/scripts/quick-test.sh +37 -0
  99. package/scripts/test-chat.sh +54 -0
  100. package/scripts/test-multi-turn.js +137 -0
  101. package/scripts/test-ner-simple.js +28 -0
  102. package/scripts/test-ner.js +60 -0
  103. package/scripts/test-stripe-connect.js +304 -0
  104. package/scripts/test-webhook-secret.sh +24 -0
  105. package/server.js +3864 -0
  106. package/storage.rules +22 -0
  107. package/test-api.json +1 -0
  108. package/tests/emulator/README.md +38 -0
  109. package/tests/emulator/counters.emulator.test.js +81 -0
  110. package/tests/integration/api-account-selfserve.test.js +312 -0
  111. package/tests/integration/api-admin-digest.test.js +202 -0
  112. package/tests/integration/api-chat-satisfaction.test.js +259 -0
  113. package/tests/integration/api-doctor-drafts.test.js +236 -0
  114. package/tests/integration/api-doctor-inbox.test.js +401 -0
  115. package/tests/integration/api-org.test.js +328 -0
  116. package/tests/integration/api-payment.test.js +664 -0
  117. package/tests/integration/api-provider-settings.test.js +475 -0
  118. package/tests/integration/api-qr.test.js +121 -0
  119. package/tests/integration/api-security.test.js +241 -0
  120. package/tests/integration/api-stripe-search.test.js +303 -0
  121. package/tests/integration/api-webhook-deletion.test.js +231 -0
  122. package/tests/mocks/firebase-admin.js +220 -0
  123. package/tests/unit/analytics.test.js +151 -0
  124. package/tests/unit/audit-bugs.test.js +203 -0
  125. package/tests/unit/chat-ui-logic.test.js +283 -0
  126. package/tests/unit/drafts.test.js +104 -0
  127. package/tests/unit/feedback.test.js +148 -0
  128. package/tests/unit/i18n.test.js +229 -0
  129. package/tests/unit/llm-retry.test.js +122 -0
  130. package/tests/unit/notifications.test.js +155 -0
  131. package/tests/unit/orgs.test.js +328 -0
  132. package/tests/unit/payment.test.js +889 -0
  133. package/tests/unit/safety-edge-cases.test.js +509 -0
  134. package/tests/unit/safety-srs-compliance.test.js +252 -0
  135. package/tests/unit/safety.test.js +291 -0
  136. package/tests/unit/server-helpers.test.js +774 -0
  137. package/tests/unit/topics-limits.test.js +158 -0
  138. package/tests/unit/triage.test.js +152 -0
  139. package/tests/unit/validate.test.js +408 -0
package/SRS.md ADDED
@@ -0,0 +1,448 @@
1
+ # EvalDoc Link-First — Software Requirements (Firebase + Node.js)
2
+
3
+ **Version:** 7.3
4
+ **Product:** Personal Healthcare AI Link Platform (Link-as-a-Service)
5
+ **Stack:** Firebase (Auth + Firestore + Storage), Node.js/Express backend on Cloud Run (Firebase App Hosting)
6
+ **Goal:** Zero-friction provider link → patient chat → safe responses → analytics
7
+ **Primary buyer:** Individual clinicians and clinics worldwide (B2B SaaS)
8
+
9
+ > **Changelog (v7.2 → v7.3) — review remediation:**
10
+ > - **Single canonical backend.** `server.js` (Cloud Run via `apphosting.yaml`) is the one backend and owns `/api`. The stale parallel Cloud Function (`functions/src/index.js`) and its `firebase.json /api/** → api` rewrite have been removed (they were 3 months / 45 commits behind and missing every recent feature and security fix). `firebase.json` now only manages Firestore/Storage rules + indexes + static hosting.
11
+ > - **Credit-card numbers are now redacted** (were detected only) — §7.4 claim is now accurate.
12
+ > - **Documentation corrected**: authenticated `/api/patient/*` subsystem documented (§2, §4.2, §13); `analyticsDaily` fields corrected (§5.2); §13 API catalog completed; stale "MVP excludes clinics" bullet reconciled.
13
+ > - **Middleware extraction**: `requireProvider` / `requireMembership(permission)` / `sendError` guards centralize auth+ownership+RBAC (adopted across org + self-serve routes; incremental rollout continuing).
14
+ > - **Scalability**: `trust proxy` for correct client IP; parallelized org-analytics fan-out.
15
+ > - **Tooling**: ESLint + Prettier + GitHub Actions CI + Firestore-emulator test scaffold.
16
+ >
17
+ > **Changelog (v7.1 → v7.2) — security & reliability hardening:**
18
+ > - Fixed three confirmed-exploitable auth bugs: missing admin gate on platform analytics (any doctor could read all providers), unauthenticated satisfaction IDOR (anyone could rate/fabricate conversations), and subscription-cancel IDOR (a doctor could cancel any subscription).
19
+ > - Added fail-closed platform-admin allowlist, constant-time internal-key comparison, request-correlation ids, a process-level async fault net, and defensive LLM response parsing.
20
+ > - New **Section 15 — Security & Reliability Controls** documents the full posture.
21
+ > - New **Section 16 — Next Features (prioritized)**.
22
+ >
23
+ > **Changelog (v7.0 → v7.1):**
24
+ > - **Multi-provider clinics (Phase A–D) implemented**: organizations, RBAC (owner/admin/clinician/staff), email invites with seat enforcement, clinic-wide inbox and aggregate analytics. Added non-breakingly via a lazy "personal org" so the solo flow is unchanged. Remaining: seat-quantity Stripe billing (Phase E) and the clinic admin UI.
25
+ >
26
+ > **Changelog (v6.2 → v7.0)** — reflects the implemented product:
27
+ > - Business model clarified: **B2B SaaS subscription is primary**; patient-pay is an optional per-provider add-on.
28
+ > - New **preview status**: providers get an instant, shareable working link before verification completes.
29
+ > - **Credential verification** is now a documented self-serve workflow (status endpoint, SLA, resubmit, preview fallback).
30
+ > - **Guided onboarding checklist** replaces the blank-dashboard drop.
31
+ > - **GDPR controls** added: data export (portability) and account deletion (erasure).
32
+ > - **Internationalization (i18n)**: locale-aware safety/disclaimer text (6 locales).
33
+ > - **Public pricing** surface added.
34
+ > - REST API is now documented (Section 13), since the product exposes one.
35
+
36
+ ---
37
+
38
+ ## 0) Non-Negotiable Safety/Scope
39
+
40
+ EvalDoc must **never**:
41
+
42
+ * diagnose ("you have X", "you might have X")
43
+ * prescribe or recommend treatment ("I recommend you try…")
44
+ * provide dosing instructions
45
+ * advise starting/stopping medicines
46
+ * interpret medical images
47
+
48
+ EvalDoc may:
49
+
50
+ * provide general health information
51
+ * provide medication facts + safety warnings (non-dosing)
52
+ * answer clinic FAQs (hours, services)
53
+ * appointment prep guidance
54
+ * post-procedure general guidance (non-personalized)
55
+ * escalate to urgent care when red flags appear
56
+
57
+ **Fail-closed:** If uncertain → refuse or escalate. Safety text is localized to the provider's `locale` with English fallback.
58
+
59
+ ---
60
+
61
+ ## 1) MVP Definition (Strict)
62
+
63
+ ### MVP promise
64
+
65
+ A provider gets a working link in **<5 minutes**:
66
+ `evaldoc.com/<handle>`
67
+ Patients open it and chat **without login**.
68
+
69
+ **Activation guarantee:** A new provider reaches a **live preview link + functional dashboard immediately** after onboarding — billing and credential verification are *not* on the critical path to first value.
70
+
71
+ ### MVP includes
72
+
73
+ 1. Provider onboarding (signup → handle → branding → topics → **instant preview link**)
74
+ 2. Guided onboarding checklist (next-step guidance, no blank dashboard)
75
+ 3. Public link page (branded chat UI), preview-marked until verified
76
+ 4. Topic-controlled assistant (no free chat)
77
+ 5. Safety pipeline (input + output + escalation), localized
78
+ 6. Conversation logging + safety events
79
+ 7. Provider dashboard (usage, top questions, escalations)
80
+ 8. Share tools (QR code + copy templates)
81
+ 9. Self-serve account controls (data export, account deletion)
82
+ 10. Plan gating (Free tier limits + paid tiers)
83
+
84
+ ### MVP excludes (roadmap)
85
+
86
+ * EHR integrations
87
+ * Custom domains
88
+ * Multi-provider clinic **admin UI** (the org/RBAC/invite API + data layer is built — see §11; only the dashboard screens and seat-quantity billing remain)
89
+ * Real medical image understanding
90
+ * Full UI translation (only safety-critical text is localized)
91
+
92
+ ---
93
+
94
+ ## 2) Roles
95
+
96
+ * **Platform Admin** (EvalDoc ops) — credential verification, suspensions, platform analytics
97
+ * **Organization** (clinic/practice) — a billing + membership boundary owning one or more providers
98
+ * **Owner** — billing, delete org, manage all members, full access
99
+ * **Admin** — manage members (not owner), org settings, clinic-wide inbox/analytics
100
+ * **Clinician** — own provider profile + own inbox, reply to patients
101
+ * **Staff** — triage/reply in assigned inboxes only (no clinical settings)
102
+ * **Provider** (subscriber) — a clinician's public profile/link; belongs to an organization
103
+ * **Patient/Visitor** — two supported modes:
104
+ * **Anonymous** (default) — chats via `/api/chat` with a browser session id, no login. This is the zero-friction MVP path.
105
+ * **Authenticated patient** (optional) — a logged-in patient account (`/api/patient/*`) with a profile and persistent threads, for providers who want returning-patient continuity. Requires Firebase Auth.
106
+
107
+ ---
108
+
109
+ ## 3) Core UX Flows (Must Build)
110
+
111
+ ### 3.1 Provider Onboarding (Target <5 minutes to a live preview link)
112
+
113
+ 1. Sign up / log in (Firebase Auth)
114
+ 2. Choose handle (unique)
115
+ 3. Add profile: name, specialty, country, locale
116
+ 4. Add branding: photo/logo + colors + welcome message
117
+ 5. Choose topics (checkbox)
118
+ 6. Upload medical license (for verification — does **not** block preview)
119
+ 7. **Provider is created in `preview` status → link works immediately**
120
+ 8. Share kit: link + QR + copy templates
121
+ 9. (Optional, later) Set up payments; (automatic) credential verification → switch to `published`
122
+
123
+ ### 3.2 Patient Flow (Zero friction)
124
+
125
+ 1. Patient opens `evaldoc.com/<handle>`
126
+ 2. Sees provider identity + disclaimers + emergency banner (a **preview notice** appears if unverified)
127
+ 3. Chooses topic (or system auto-detects)
128
+ 4. Chats (localized disclaimers/escalation)
129
+ 5. Optional rating at end
130
+
131
+ ### 3.3 Self-Serve Account Lifecycle
132
+
133
+ * Check verification status + SLA at any time
134
+ * Track onboarding completion via checklist
135
+ * Export all data (JSON)
136
+ * Delete account (cascading erasure) with explicit confirmation
137
+
138
+ ---
139
+
140
+ ## 4) Core Modules
141
+
142
+ ### 4.1 Link Resolver
143
+ Maps `/<handle>` → provider config. Resolves `published` **and** `preview` providers (preview flagged to the client via `isPreview`). Renders branded chat UI.
144
+
145
+ ### 4.2 Conversation Service
146
+ Anonymous sessions (browser session id). Stores conversations/messages. Enforces monthly chat limits by plan. An optional **authenticated-patient** path (`/api/patient/*`) provides logged-in patient accounts with persistent threads (see §13).
147
+
148
+ ### 4.3 Topic Engine (Strict)
149
+ Only allowed topics. Each topic defines: system prompt, allowed/forbidden behaviors, disclaimer, escalation triggers.
150
+
151
+ ### 4.4 Safety Engine (Critical)
152
+ Defense-in-depth: input checks (PII/PHI incl. credit-card, injection, prohibited medical requests, red flags), output checks (strip/deny unsafe content, enforce disclaimers, escalate), NER-assisted PII detection, always logs safety events. **Patient-facing refusal/escalation/disclaimer text is locale-aware.**
153
+
154
+ ### 4.5 LLM Connector
155
+ Provider chain with **exponential-backoff retry** on transient errors (429/5xx/network). Topic prompts + provider metadata. Tight caps: max tokens, low temperature, short structured answers.
156
+
157
+ ### 4.6 Analytics Engine
158
+ Aggregates chats/day, top topics, common questions, escalation + resolution rates, satisfaction, unique patients.
159
+
160
+ ### 4.7 Verification Service (Provider Credentials)
161
+ Tracks `verificationStatus` (`pending`/`verified`/`rejected`). Exposes self-serve status + SLA + rejection reason + resubmit path. Verified is required for `published`; not required for `preview`.
162
+
163
+ ### 4.8 Account Service (GDPR)
164
+ Data export (portability) and account deletion (erasure, cascading across provider/handle/threads/conversations/messages).
165
+
166
+ ### 4.9 Billing Service (Optional)
167
+ B2B SaaS subscription (primary). Optional per-provider patient-pay add-on via Stripe Connect. Skippable; never blocks first value.
168
+
169
+ ### 4.10 Localization (i18n)
170
+ `SUPPORTED_LOCALES = [en, es, fr, de, pt, ar]`. `normalizeLocale()` collapses region/script subtags. Safety-critical strings translated with English fallback.
171
+
172
+ ---
173
+
174
+ ## 5) Firestore Data Model
175
+
176
+ ### 5.1 `providers/{providerId}` (doc id = ownerUid)
177
+ * `handle` (unique, indexed), `displayName`, `specialty`, `country`, `locale`
178
+ * `photoUrl?`, `logoUrl?`, `brandPrimaryColor`, `brandSecondaryColor`, `welcomeMessage`
179
+ * `contactInfo { phone?, email?, address? }`, `businessHours` (json)
180
+ * `enabledTopics` (array)
181
+ * `status` (`draft` | `preview` | `published` | `suspended`)
182
+ * `verificationStatus` (`pending` | `verified` | `rejected`), `verificationRejectionReason?`
183
+ * `licenseNumber?`, `certificateUrl?`, `certificateFileName?`
184
+ * `plan` (`free` | `pro` | `clinic` | `enterprise`)
185
+ * `monthlyChatLimit`, `monthlyChatCount`, `monthlyChatCountMonth`, `totalChatCount`, `totalEscalationCount`
186
+ * Billing: `pricingModel?`, `paymentEnabled`, `perChatPrice?`, `subscriptionPrice?`, `currency?`, `stripeAccountId?`, `stripeOnboardingComplete`
187
+ * `createdAt`, `updatedAt`
188
+
189
+ ### 5.2 `providers/{providerId}/analyticsDaily/{YYYY-MM-DD}`
190
+ `chatCount`, `uniqueSessions` (array of session ids), `escalationCount`, `satisfactionSum`, `satisfactionCount` (average is derived, not stored), `topicCounts.{topic}`, `questionBuckets.{bucket}`, `updatedAt`
191
+
192
+ ### 5.3 `conversations/{conversationId}` & `threads/{threadId}`
193
+ `providerId`/`doctorId`, `handle`, `sessionId`, `topic`, `isEscalated`, `escalationReason`, `triageCategory`, `status`, `satisfaction`, `createdAt`, `updatedAt` (+ messages subcollection)
194
+
195
+ ### 5.4 `safetyEvents/{eventId}`
196
+ `providerId`, `conversationId`, `eventType` (`blocked_input`|`blocked_output`|`escalated`|`pii_detected`|`abuse`), `details`, `createdAt`
197
+
198
+ ### 5.5 `handles/{handle}`
199
+ `providerId`, `createdAt` (unique reservation; released on account deletion)
200
+
201
+ ### 5.6 `waitlist/{id}`
202
+ `email`, `approved`, `createdAt` (gate is env-toggleable via `WAITLIST_ENABLED`)
203
+
204
+ ### 5.7 `auditLogs/{id}`
205
+ `action`, `actorId`, `actorType`, `resourceType`, `resourceId`, `meta`, `createdAt`
206
+
207
+ ### 5.8 `organizations/{orgId}` (multi-provider clinics)
208
+ `ownerUid`, `name`, `type` (`personal` | `clinic`), `plan`, `seats`, `createdAt`, `updatedAt`
209
+ * `organizations/{orgId}/members/{uid}`: `uid`, `email`, `role` (`owner`|`admin`|`clinician`|`staff`), `providerId?`, `status` (`active`|`invited`), `joinedAt`
210
+
211
+ ### 5.9 `memberships/{uid}`
212
+ `orgId`, `role` — fast "which org is this user in" lookup (one active org per user in v1)
213
+
214
+ ### 5.10 `invitations/{token}`
215
+ `orgId`, `email`, `role`, `token` (unguessable, single-use), `status` (`pending`|`accepted`|`revoked`|`expired`), `invitedBy`, `createdAt`, `expiresAt`
216
+
217
+ ---
218
+
219
+ ## 6) Topics (Hard-coded Keys)
220
+
221
+ 1. `medication_info_safety`
222
+ 2. `appointments_preparation`
223
+ 3. `general_health_info`
224
+ 4. `clinic_services_hours`
225
+ 5. `insurance_billing_faq`
226
+ 6. `post_procedure_guidance`
227
+
228
+ Each ships with: system prompt template, disclaimer, forbidden content patterns, escalation triggers.
229
+
230
+ ---
231
+
232
+ ## 7) Safety Rules
233
+
234
+ ### 7.1 Always Block (Refuse)
235
+ * diagnosis claims ("you have", "you might/may/could have", "this is likely")
236
+ * treatment instructions ("take", "use", "apply", "start/stop medication", "I recommend you try")
237
+ * dosing ("500mg", "twice daily", "dose", "how many tablets")
238
+ * off-label suggestions ("not approved but", "try it for…")
239
+ * medical image interpretation ("looks like pneumonia", "tumor")
240
+ * prescriptions ("here is your prescription")
241
+
242
+ ### 7.2 Always Escalate (Emergency)
243
+ chest pain/hurts/tightness/pressure, trouble/difficulty breathing, severe allergic reaction, swelling face/throat, fainting, seizure, stroke signs, severe bleeding, suicidal thoughts / self-harm / "feel like dying" / "kill myself", pregnancy + severe pain/bleeding, newborn/infant severe symptoms, unconsciousness, overdose, sudden vision loss.
244
+
245
+ ### 7.3 Output Constraints
246
+ Conservative, non-alarming, no certainty claims, "seek professional care" on risk, **localized disclaimer footer always present**.
247
+
248
+ ### 7.4 PII/PHI
249
+ Detect + redact: SSN, email, phone, street address, ZIP, DOB, **16-digit credit card**, plus NER-detected person/location/org entities.
250
+
251
+ ---
252
+
253
+ ## 8) Provider Dashboard
254
+
255
+ 1. **Overview**: usage, escalations, resolution rate, satisfaction, unique patients
256
+ 2. **Onboarding checklist**: step completion %, next action (hidden when complete)
257
+ 3. **Verification banner**: status, SLA, rejection reason + resubmit, preview reminder
258
+ 4. **Share**: preview/public link + QR + copy templates
259
+ 5. **Conversations**: anonymized transcripts (PII removed), search, export, delete
260
+ 6. **Settings**: branding, topics, contact info, business hours, notifications
261
+ 7. **Account**: export data, delete account
262
+ 8. **Plan & Billing**: current plan + usage limit; optional payment setup
263
+
264
+ ---
265
+
266
+ ## 9) Hosting & Runtime
267
+ Frontend on Firebase Hosting. Backend: Node.js/Express (`server.js`) deployable as a service or Cloud Functions. Assets in Firebase Storage. Auth via Firebase Auth.
268
+
269
+ **Security:** HSTS, CSP, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy; per-IP rate limiting; 100kb body cap; no internal error/stack leakage to clients.
270
+
271
+ ---
272
+
273
+ ## 10) Business Model
274
+
275
+ **Primary — B2B SaaS subscription (provider/clinic pays EvalDoc):**
276
+
277
+ | Plan | Price/mo | Seats | Chats/mo | Highlights |
278
+ |------|----------|-------|----------|------------|
279
+ | Free | $0 | 1 | 50 | Safety pipeline, basic analytics, branded link + QR |
280
+ | Pro | $29 | 1 | Unlimited | Custom branding, full analytics, AI drafts, priority verification |
281
+ | Clinic | $99 | 5 | Unlimited | Team inbox, shared analytics, clinic branding |
282
+ | Enterprise | Custom | Unlimited | Unlimited | SSO/SAML, data residency, DPA & BAA, SLAs |
283
+
284
+ **Secondary — optional patient-pay add-on (per provider):** per-chat or subscription, processed via Stripe Connect. Platform fee applies on top of processor fees. Public pricing is exposed via the pricing endpoints so patients see cost before chatting.
285
+
286
+ ---
287
+
288
+ ## 11) Multi-Provider Clinics (Phase A–D Implemented)
289
+
290
+ Organizations are a membership + billing boundary owning one or more providers. Added non-breakingly: a solo provider gets a **lazy personal org** (`orgId = providerId`, sole owner) the first time they touch org features, so the existing solo flow is unchanged.
291
+
292
+ * **RBAC** — pure, unit-tested `can(role, action)` core; `canManageMember`/`canAssignRole` guard privilege escalation (admins can't touch owners/each other; nobody mints owners via invite).
293
+ * **Invites & seats** — email invites with unguessable single-use expiring tokens; seat enforcement (active + pending vs plan limit) at both invite and accept time.
294
+ * **Clinic-wide inbox** (`/api/org/inbox`) and **aggregate analytics** (`/api/org/analytics`) — owner/admin only.
295
+
296
+ **Remaining (roadmap):**
297
+ * Phase E — seat-quantity Stripe subscription (consolidated org billing)
298
+ * Clinic admin **UI** (the API/data layer is complete)
299
+ * Ownership transfer flow; multi-org membership per user
300
+ * Full UI internationalization (beyond safety text)
301
+ * Custom domains, EHR integrations
302
+ * SSO/SAML, data residency, BAA/DPA automation for Enterprise
303
+ * 2FA + email change for account security
304
+
305
+ ---
306
+
307
+ ## 12) Definition of Done
308
+
309
+ * Provider publishes a **live preview link in <5 minutes** (no verification/billing wait)
310
+ * Patient chats without login; preview state is clearly disclosed
311
+ * Topics enforced (no generic assistant)
312
+ * Safety blocks diagnosis/treatment/dosing; red flags escalate; text localized
313
+ * Conversations + safety events logged
314
+ * Dashboard shows usage + escalations + onboarding checklist + verification status
315
+ * Free tier limit enforced
316
+ * Provider can export and delete their account data
317
+ * Public pricing is visible
318
+
319
+ ---
320
+
321
+ ## 13) REST API (Implemented)
322
+
323
+ > Auth = Firebase ID token in `Authorization: Bearer <token>` unless marked **public**.
324
+
325
+ **Provider & onboarding**
326
+ * `POST /api/provider` — create provider (starts in `preview`)
327
+ * `POST /api/provider/update` — update profile/branding/topics/status (publish requires verified)
328
+ * `GET /api/me` — current provider profile
329
+ * `GET /api/provider` *(public)* — resolve provider by handle
330
+ * `GET /api/provider/verification-status` — status, SLA, resubmit, preview flag
331
+ * `GET /api/provider/onboarding-status` — checklist + next action
332
+ * `GET /api/search-providers` *(public)* — provider search
333
+ * `GET /api/qr/:handle` *(public)* — QR PNG for the provider link
334
+
335
+ **Account (GDPR)**
336
+ * `GET /api/account/export` — full data export (JSON)
337
+ * `DELETE /api/account` — cascading erasure (`{ confirm: "DELETE" }`)
338
+
339
+ **Organizations & team (multi-provider clinics)**
340
+ * `GET /api/org` — current org, members, seat usage (lazily provisions a personal org)
341
+ * `POST /api/org/invite` — invite a member (RBAC + seat check)
342
+ * `POST /api/org/invite/accept` — accept an invite (`{ token }`)
343
+ * `GET /api/org/members` — list members
344
+ * `PATCH /api/org/members/:uid` — change a member's role (RBAC)
345
+ * `DELETE /api/org/members/:uid` — remove a member (frees a seat)
346
+ * `GET /api/org/inbox` — clinic-wide patient threads (owner/admin)
347
+ * `GET /api/org/analytics` — clinic-wide aggregate analytics (owner/admin)
348
+
349
+ **Pricing (public)**
350
+ * `GET /api/pricing` — SaaS plans + add-on terms
351
+ * `GET /api/pricing/:handle` — patient-facing cost for a provider
352
+
353
+ **Chat & anonymous patient (public)**
354
+ * `POST /api/chat` — send a message (topic-gated, safety-filtered, payment-gated if enabled)
355
+ * `GET /api/chat/history` — session history
356
+ * `POST /api/satisfaction` — rating (requires the owning `sessionId`)
357
+ * `POST /api/waitlist` — join waitlist
358
+
359
+ **Authenticated patient accounts** *(Firebase Auth; dispatched via `app.all('/api/patient/*')`)*
360
+ * `GET /api/patient/me`, `POST /api/patient/create`
361
+ * `POST /api/patient/thread`, `GET /api/patient/threads`, `GET /api/patient/thread/:id/messages`
362
+ * `POST /api/patient/message`, `POST /api/patient/thread/:id/read`
363
+
364
+ **Doctor inbox & async pipeline**
365
+ * `GET /api/doctor/threads`, `GET /api/doctor/thread/:id`, `PATCH /api/doctor/thread/:id`
366
+ * `POST /api/doctor/message`
367
+ * `POST /api/doctor/draft/accept` | `/reject` | `/regenerate`
368
+ * `GET /api/doctor/notifications`, `POST /api/doctor/notifications/preferences`, `POST /api/doctor/notifications/read`
369
+ * `GET /api/doctor/feedback/stats`
370
+ * `GET /api/conversations`, `GET /api/conversation` (query), `GET /api/conversation/:id`, `POST /api/conversation/:id/reply`, `DELETE /api/conversations/:id`
371
+
372
+ **Analytics & safety**
373
+ * `GET /api/analytics`, `GET /api/admin/analytics` (platform-admin only), `GET /api/safety-events`
374
+
375
+ **Billing (Stripe)**
376
+ * `POST /api/stripe/create-connect-account` | `/create-account-link` | `/refresh-account-status` | `/set-pricing`
377
+ * `GET /api/stripe/diagnostics`
378
+ * `GET /api/payment/config/:handle` *(public)*
379
+ * `POST /api/payment/update-pricing` | `/create-subscription` | `/cancel-subscription` | `/create-intent` | `/create-checkout-session` | `/check-access` | `/webhook`
380
+ * `GET /api/payment/transactions`, `GET /api/payment/revenue`
381
+
382
+ **Ops**
383
+ * `GET /health` (+ `?deep=true`), `GET /api/launch-status`, `POST /api/internal/daily-digest` (X-API-Key)
384
+
385
+ ---
386
+
387
+ ## 14) Configuration
388
+
389
+ * `WAITLIST_ENABLED` (`true`/`false`) — gate self-serve signups
390
+ * `ADMIN_UIDS` — comma-separated Firebase UIDs allowed to access platform-admin views (fail-closed if unset)
391
+ * `DEEPSEEK_API_KEY`, `GEMINI_API_KEY` — LLM providers
392
+ * `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PUBLISHABLE_KEY`
393
+ * `INTERNAL_API_KEY` — internal cron endpoints
394
+ * `ALLOWED_ORIGINS` — CORS allowlist
395
+ * Firebase Admin credentials
396
+
397
+ ---
398
+
399
+ ## 15) Security & Reliability Controls
400
+
401
+ **Authentication & authorization**
402
+ * Firebase ID-token auth on all provider/doctor/org endpoints (`requireAuth`).
403
+ * **Platform-admin gate** — `/api/admin/analytics` requires membership in the `ADMIN_UIDS` allowlist (fail-closed).
404
+ * **Ownership/IDOR checks** — every resource accessed by id (threads, conversations, drafts, subscriptions, org members) is verified to belong to the caller / caller's org before any read-modify-write.
405
+ * **RBAC** — org actions go through a pure, unit-tested `can(role, action)` core; `canManageMember`/`canAssignRole` block privilege escalation and owner-minting.
406
+ * **Anonymous-but-scoped patient actions** — satisfaction ratings require the owning `sessionId`; no doc is created for unknown ids.
407
+
408
+ **Secrets & transport**
409
+ * Internal cron key compared in **constant time**; rate-limited.
410
+ * Security headers: HSTS, CSP, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy.
411
+ * Per-IP rate limiting; 100 kb body cap; CORS allowlist.
412
+ * No internal error/stack/Stripe details leaked to clients.
413
+
414
+ **Reliability**
415
+ * **Request-correlation ids** (`X-Request-Id`, echoed; surfaced in 500 responses) for traceable support.
416
+ * **Process-level fault net** — `unhandledRejection` / `uncaughtException` are logged, not silently fatal.
417
+ * **LLM resilience** — exponential-backoff retry, multi-provider fallback chain, defensive response parsing, static safe fallback.
418
+ * Fire-and-forget pipelines (triage/draft/notifications) are wrapped so a failure degrades gracefully and never crashes the request.
419
+ * Bounded queries on platform-wide scans; pagination on list endpoints.
420
+
421
+ **Data protection (GDPR)**
422
+ * Self-serve data export (portability) and cascading account deletion (erasure).
423
+ * PII/PHI detection + redaction (regex + NER) before storage/display.
424
+ * Audit log on sensitive actions (chat, drafts, deletions, member changes).
425
+
426
+ ---
427
+
428
+ ## 16) Next Features (Prioritized)
429
+
430
+ **P0 — trust & safety**
431
+ 1. **Email verification** before publishing or accepting payments (Firebase `email_verified`).
432
+ 2. **Admin verification console** — approve/reject provider credentials with reason + email notification (closes the manual-verification loop).
433
+ 3. **2FA** for provider accounts (TOTP) and email-change with re-auth.
434
+
435
+ **P1 — clinic completion (Phase E + UI)**
436
+ 4. **Seat-quantity Stripe billing** for clinics (consolidated org subscription).
437
+ 5. **Clinic admin UI** — member list, invite form, clinic inbox/analytics (API already shipped).
438
+ 6. **Provider profiles per clinician within an org** (decouple provider creation from the owner uid).
439
+
440
+ **P2 — growth & retention**
441
+ 7. **Webhooks/Zapier** for clinics (new escalation, new conversation) → EHR/CRM.
442
+ 8. **Scheduled reports** (weekly digest email of usage + escalations).
443
+ 9. **Custom domains** and white-label branding for Clinic/Enterprise.
444
+
445
+ **P3 — depth**
446
+ 10. **Full UI internationalization** (beyond safety text), RTL support.
447
+ 11. **Analytics deep-dive**: cohort retention, topic trends, satisfaction over time.
448
+ 12. **Configurable safety rules per org** (within platform guardrails) + audit.
@@ -0,0 +1,48 @@
1
+ # Staging deployment — mirrors production config with lower resources
2
+ runConfig:
3
+ minInstances: 0
4
+ maxInstances: 2
5
+ cpu: 1
6
+ memoryMiB: 256
7
+ concurrency: 40
8
+
9
+ env:
10
+ - variable: NODE_ENV
11
+ value: staging
12
+ availability:
13
+ - BUILD
14
+ - RUNTIME
15
+
16
+ - variable: FIREBASE_CONFIG
17
+ value: auto
18
+ availability:
19
+ - BUILD
20
+ - RUNTIME
21
+
22
+ # API Keys (use test/staging keys)
23
+ - variable: GEMINI_API_KEY
24
+ secret: GEMINI_API_KEY
25
+
26
+ - variable: DEEPSEEK_API_KEY
27
+ secret: DEEPSEEK_API_KEY
28
+
29
+ # Stripe TEST keys (pk_test_ / sk_test_)
30
+ - variable: STRIPE_PUBLISHABLE_KEY
31
+ secret: STRIPE_PUBLISHABLE_KEY_STAGING
32
+
33
+ - variable: STRIPE_SECRET_KEY
34
+ secret: STRIPE_SECRET_KEY_STAGING
35
+
36
+ - variable: STRIPE_WEBHOOK_SECRET
37
+ secret: STRIPE_WEBHOOK_SECRET_STAGING
38
+
39
+ # Staging URL
40
+ - variable: PLATFORM_URL
41
+ value: https://staging.evaldoctor.ai
42
+ availability:
43
+ - RUNTIME
44
+
45
+ - variable: ALLOWED_ORIGINS
46
+ value: https://staging.evaldoctor.ai
47
+ availability:
48
+ - RUNTIME
@@ -0,0 +1,50 @@
1
+ # Settings for Cloud Run
2
+ runConfig:
3
+ minInstances: 0
4
+ maxInstances: 10
5
+ cpu: 1
6
+ memoryMiB: 512
7
+ concurrency: 80
8
+
9
+ # Environment variables and secrets
10
+ env:
11
+ - variable: NODE_ENV
12
+ value: production
13
+ availability:
14
+ - BUILD
15
+ - RUNTIME
16
+
17
+ - variable: FIREBASE_CONFIG
18
+ value: auto
19
+ availability:
20
+ - BUILD
21
+ - RUNTIME
22
+
23
+ # API Keys
24
+ - variable: GEMINI_API_KEY
25
+ secret: GEMINI_API_KEY
26
+
27
+ - variable: DEEPSEEK_API_KEY
28
+ secret: DEEPSEEK_API_KEY
29
+
30
+ # Stripe API Keys
31
+ - variable: STRIPE_PUBLISHABLE_KEY
32
+ secret: STRIPE_PUBLISHABLE_KEY
33
+
34
+ - variable: STRIPE_SECRET_KEY
35
+ secret: STRIPE_SECRET_KEY
36
+
37
+ - variable: STRIPE_WEBHOOK_SECRET
38
+ secret: STRIPE_WEBHOOK_SECRET
39
+
40
+ # Platform URL (HTTPS required for HIPAA compliance)
41
+ - variable: PLATFORM_URL
42
+ value: https://evaldoctor.ai
43
+ availability:
44
+ - RUNTIME
45
+
46
+ # CORS — restrict to production origin
47
+ - variable: ALLOWED_ORIGINS
48
+ value: https://evaldoctor.ai
49
+ availability:
50
+ - RUNTIME
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // Generated by npxhub. Runs evaldoc locally: npx evaldoc [--port <n>] [--no-open]
3
+ "use strict"
4
+ const { spawn } = require("node:child_process")
5
+ const fs = require("node:fs")
6
+ const http = require("node:http")
7
+ const path = require("node:path")
8
+
9
+ const CONFIG = {
10
+ "mode": "script",
11
+ "startScript": "start",
12
+ "staticDir": "",
13
+ "port": 8080,
14
+ "env": [
15
+ {
16
+ "name": "GEMINI_API_KEY",
17
+ "description": "Google Generative AI (Gemini 2.0 Flash) key used by the primary LLM in functions/src/llm.js.",
18
+ "required": true
19
+ },
20
+ {
21
+ "name": "DEEPSEEK_API_KEY",
22
+ "description": "DeepSeek API key for the fallback LLM (called via the openai client).",
23
+ "required": false
24
+ },
25
+ {
26
+ "name": "HUGGINGFACE_API_KEY",
27
+ "description": "Hugging Face token for the medical NER / inference models.",
28
+ "required": false
29
+ },
30
+ {
31
+ "name": "STRIPE_SECRET_KEY",
32
+ "description": "Stripe secret key for payment and Connect flows.",
33
+ "required": false
34
+ },
35
+ {
36
+ "name": "STRIPE_WEBHOOK_SECRET",
37
+ "description": "Stripe webhook signing secret used to verify incoming webhook requests.",
38
+ "required": false
39
+ },
40
+ {
41
+ "name": "GOOGLE_APPLICATION_CREDENTIALS",
42
+ "description": "Path to a Firebase/Firestore service-account JSON used by firebase-admin.",
43
+ "required": true
44
+ }
45
+ ]
46
+ }
47
+ const root = path.join(__dirname, "..")
48
+ const pkg = require(path.join(root, "package.json"))
49
+ const args = process.argv.slice(2)
50
+ const flag = (name) => args.includes(name)
51
+ const portArg = args.indexOf("--port")
52
+ const port = Number(portArg >= 0 ? args[portArg + 1] : process.env.PORT || CONFIG.port)
53
+
54
+ if (flag("--version") || flag("-v")) {
55
+ console.log(pkg.version)
56
+ process.exit(0)
57
+ }
58
+ if (flag("--help") || flag("-h")) {
59
+ console.log(`${pkg.name} ${pkg.version}\n\nUsage: npx ${pkg.name} [--port <n>] [--no-open]`)
60
+ if (CONFIG.env.length) {
61
+ console.log("\nEnvironment variables:")
62
+ for (const e of CONFIG.env) console.log(` ${e.name}${e.required ? " (required)" : ""} ${e.description}`)
63
+ }
64
+ process.exit(0)
65
+ }
66
+
67
+ const missing = CONFIG.env.filter((e) => e.required && !process.env[e.name])
68
+ if (missing.length) {
69
+ console.error("Set these environment variables first:")
70
+ for (const e of missing) console.error(` ${e.name} ${e.description}`)
71
+ process.exit(1)
72
+ }
73
+
74
+ const url = `http://localhost:${port}`
75
+ function openBrowser() {
76
+ if (flag("--no-open")) return
77
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open"
78
+ const cmdArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url]
79
+ spawn(cmd, cmdArgs, { stdio: "ignore", detached: true }).on("error", () => {}).unref()
80
+ }
81
+
82
+ if (CONFIG.mode === "static") {
83
+ const dir = path.join(root, CONFIG.staticDir)
84
+ const types = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".ico": "image/x-icon", ".woff2": "font/woff2" }
85
+ http
86
+ .createServer((req, res) => {
87
+ const clean = path.normalize(decodeURIComponent((req.url || "/").split("?")[0])).replace(/^(\.\.[/\\])+/, "")
88
+ let file = path.join(dir, clean)
89
+ if (!file.startsWith(dir) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) file = path.join(dir, "index.html")
90
+ res.setHeader("Content-Type", types[path.extname(file)] || "application/octet-stream")
91
+ fs.createReadStream(file).on("error", () => res.writeHead(404).end()).pipe(res)
92
+ })
93
+ .listen(port, () => {
94
+ console.log(`${pkg.name} running at ${url}`)
95
+ openBrowser()
96
+ })
97
+ } else {
98
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm"
99
+ const child = spawn(npm, ["run", CONFIG.startScript], { cwd: root, stdio: "inherit", env: { ...process.env, PORT: String(port) }, shell: process.platform === "win32" })
100
+ setTimeout(openBrowser, 2500)
101
+ const stop = () => child.kill("SIGINT")
102
+ process.on("SIGINT", stop)
103
+ process.on("SIGTERM", stop)
104
+ child.on("exit", (code) => process.exit(code ?? 0))
105
+ }
package/cors.json ADDED
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "origin": ["*"],
4
+ "method": ["GET", "POST", "PUT", "DELETE", "HEAD"],
5
+ "maxAgeSeconds": 3600,
6
+ "responseHeader": ["Content-Type", "Authorization"]
7
+ }
8
+ ]