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.
- package/.env.staging.example +19 -0
- package/.firebaserc +6 -0
- package/.github/workflows/auto-merge-to-main.yml +28 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npxhub-publish.yml +170 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +7 -0
- package/README.md +142 -0
- package/SRS.md +448 -0
- package/apphosting.staging.yaml +48 -0
- package/apphosting.yaml +50 -0
- package/bin/evaldoc.cjs +105 -0
- package/cors.json +8 -0
- package/docs/ARCHITECTURE.md +769 -0
- package/docs/ARCHITECTURE_SUMMARY.md +323 -0
- package/docs/DEPLOYMENT.md +86 -0
- package/docs/DEPLOYMENT_STEPS.md +373 -0
- package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
- package/docs/DEPLOY_STORAGE_RULES.md +82 -0
- package/docs/DOCTOR_TUTORIAL.md +467 -0
- package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
- package/docs/FIXES_APPLIED.md +128 -0
- package/docs/IMPLEMENTATION.md +498 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
- package/docs/LLM-SETUP.md +216 -0
- package/docs/MANUAL_TEST_GUIDE.md +327 -0
- package/docs/PATIENT_GUIDE.md +451 -0
- package/docs/PLATFORM_OVERVIEW.md +194 -0
- package/docs/QUICKSTART.md +252 -0
- package/docs/QUICK_REFERENCE.md +222 -0
- package/docs/READY_TO_TEST.md +383 -0
- package/docs/RUN.md +237 -0
- package/docs/START_TESTING.md +130 -0
- package/docs/STRIPE_CONNECT_SETUP.md +454 -0
- package/docs/SYSTEM_READY.md +345 -0
- package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
- package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
- package/docs/firebasefiles.md +21 -0
- package/docs/homepage.md +1208 -0
- package/docs/payments.md +159 -0
- package/eslint.config.js +76 -0
- package/firebase.json +27 -0
- package/firestore.indexes.json +88 -0
- package/firestore.rules +125 -0
- package/functions/package-lock.json +2884 -0
- package/functions/package.json +20 -0
- package/functions/src/analytics.js +101 -0
- package/functions/src/audit.js +28 -0
- package/functions/src/auth.js +19 -0
- package/functions/src/drafts.js +128 -0
- package/functions/src/feedback.js +164 -0
- package/functions/src/firebase-init.js +29 -0
- package/functions/src/i18n.js +134 -0
- package/functions/src/limits.js +27 -0
- package/functions/src/llm.js +271 -0
- package/functions/src/ner.js +162 -0
- package/functions/src/notifications.js +273 -0
- package/functions/src/orgs.js +127 -0
- package/functions/src/patient-api.js +545 -0
- package/functions/src/payment.js +830 -0
- package/functions/src/safety.js +188 -0
- package/functions/src/stripe-config.js +33 -0
- package/functions/src/topics.js +94 -0
- package/functions/src/triage.js +191 -0
- package/functions/src/utils.js +28 -0
- package/functions/src/validate.js +177 -0
- package/functions/test-firebase.js +36 -0
- package/jest.config.js +18 -0
- package/package.json +52 -0
- package/public/404.html +93 -0
- package/public/app/analytics.html +336 -0
- package/public/app/analytics.js +177 -0
- package/public/app/app.css +1400 -0
- package/public/app/app.js +1754 -0
- package/public/app/config.js +14 -0
- package/public/app/inbox-threads.js +843 -0
- package/public/app/inbox.html +598 -0
- package/public/app/inbox.js +397 -0
- package/public/app/index.html +612 -0
- package/public/assets/logo.jpg +0 -0
- package/public/chat.html +137 -0
- package/public/css/chat.css +870 -0
- package/public/css/landing-enhance.css +298 -0
- package/public/css/styles.css +1198 -0
- package/public/favicon.svg +5 -0
- package/public/find-doctor.html +220 -0
- package/public/index.html +399 -0
- package/public/js/chat-threads.js +513 -0
- package/public/js/chat.js +832 -0
- package/public/js/search.js +41 -0
- package/public/privacy.html +101 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +12 -0
- package/public/terms.html +110 -0
- package/scripts/create-test-provider.js +55 -0
- package/scripts/deploy-staging.sh +51 -0
- package/scripts/fix-provider.js +39 -0
- package/scripts/quick-test.sh +37 -0
- package/scripts/test-chat.sh +54 -0
- package/scripts/test-multi-turn.js +137 -0
- package/scripts/test-ner-simple.js +28 -0
- package/scripts/test-ner.js +60 -0
- package/scripts/test-stripe-connect.js +304 -0
- package/scripts/test-webhook-secret.sh +24 -0
- package/server.js +3864 -0
- package/storage.rules +22 -0
- package/test-api.json +1 -0
- package/tests/emulator/README.md +38 -0
- package/tests/emulator/counters.emulator.test.js +81 -0
- package/tests/integration/api-account-selfserve.test.js +312 -0
- package/tests/integration/api-admin-digest.test.js +202 -0
- package/tests/integration/api-chat-satisfaction.test.js +259 -0
- package/tests/integration/api-doctor-drafts.test.js +236 -0
- package/tests/integration/api-doctor-inbox.test.js +401 -0
- package/tests/integration/api-org.test.js +328 -0
- package/tests/integration/api-payment.test.js +664 -0
- package/tests/integration/api-provider-settings.test.js +475 -0
- package/tests/integration/api-qr.test.js +121 -0
- package/tests/integration/api-security.test.js +241 -0
- package/tests/integration/api-stripe-search.test.js +303 -0
- package/tests/integration/api-webhook-deletion.test.js +231 -0
- package/tests/mocks/firebase-admin.js +220 -0
- package/tests/unit/analytics.test.js +151 -0
- package/tests/unit/audit-bugs.test.js +203 -0
- package/tests/unit/chat-ui-logic.test.js +283 -0
- package/tests/unit/drafts.test.js +104 -0
- package/tests/unit/feedback.test.js +148 -0
- package/tests/unit/i18n.test.js +229 -0
- package/tests/unit/llm-retry.test.js +122 -0
- package/tests/unit/notifications.test.js +155 -0
- package/tests/unit/orgs.test.js +328 -0
- package/tests/unit/payment.test.js +889 -0
- package/tests/unit/safety-edge-cases.test.js +509 -0
- package/tests/unit/safety-srs-compliance.test.js +252 -0
- package/tests/unit/safety.test.js +291 -0
- package/tests/unit/server-helpers.test.js +774 -0
- package/tests/unit/topics-limits.test.js +158 -0
- package/tests/unit/triage.test.js +152 -0
- 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
|
package/apphosting.yaml
ADDED
|
@@ -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
|
package/bin/evaldoc.cjs
ADDED
|
@@ -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
|
+
}
|