Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 46s
Forgejo Docker Build / Root app tests (push) Successful in 56s
Forgejo Android APK / Build signed APK (push) Successful in 2m6s
Forgejo Docker Build / Build Docker image (push) Successful in 15s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The sign-in screen asks for an email first, then offers both ways in together: a six-digit code sent to that address, or the password. Beside rather than instead — a code depends on mail being delivered and a password does not, so neither may be the only route. "Use a different email" goes back a step, and creating an account stays where it was. What keeps it from being a second, weaker front door: - Only a bcrypt hash is stored, so a code read out of the database is not a working credential. - Ten minutes, single use, marked used before the session is issued so a replay cannot race it, and requesting a new one deletes the old. - Five wrong guesses burn it. Six digits is a million possibilities, which is plenty against a person and nothing against a script with unlimited tries. - Requesting a code answers identically whether or not the address exists, and every verify failure returns one message. A sign-in screen that says "no such account" is a way of finding out who has one. - Two-factor still applies: a code proves you can read the mailbox, which is one factor, and an account that asked for a second still wants it. - Its own rate limits, tighter for requesting than for attempting, because requesting sends mail to someone else's address. These had to be separate limiters: Express matches app.use paths on segment boundaries, so /api/auth/login does not cover /api/auth/login-code — checked against a real router rather than assumed. Two bugs found while building it, both mine: authFetch keeps an allowlist of endpoints callable with no verified owner and rejects everything else before it is sent. The new endpoints were not on it, so the request never left the browser and surfaced as "Connection error". reveal() hid elements by appending 'hidden' to className and showed them with a non-global replace, so hiding twice left two copies and showing stripped one. The "use a different email" link never reappeared. It uses classList now, which is idempotent. Verified against the running server: correct code signs in, the same code again is refused, a superseded code is refused, five wrong guesses burn it, an expired one is refused, and the stored value is a hash. In the browser: requesting a code advances the screen, a wrong code is refused without losing the screen, and the password route still signs in. Not yet demonstrated: a correct code typed into the browser. The harness keeps racing the one-live-code rule — the page's own request supersedes whatever code the test holds, and with SMTP off the delivered one cannot be read. The same request reaches the server on the wrong-code path, and the endpoint itself is verified, but that last step is untested end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
298 lines
14 KiB
Text
298 lines
14 KiB
Text
# ============================================================
|
|
# OPENBAO (optional — recommended for production)
|
|
# ============================================================
|
|
# When these three are set, the container fetches everything else below
|
|
# from OpenBao at kv/ped-ai/prod and ignores the equivalent .env values.
|
|
# Leave them unset (or blank) to fall back to .env-only (local dev, e2e).
|
|
#
|
|
# OPENBAO_ADDR=https://app.danvics.com
|
|
# OPENBAO_ROLE_ID=<from: bao read auth/approle/role/ped-ai/role-id>
|
|
# OPENBAO_SECRET_ID=<from: bao write -f auth/approle/role/ped-ai/secret-id>
|
|
# OPENBAO_KV_PATH=kv/ped-ai/prod # override path if needed
|
|
|
|
# ============================================================
|
|
# Everything below is sourced from OpenBao when OPENBAO_ADDR is set.
|
|
# Only fill these in for local dev / e2e / when running without vault.
|
|
# ============================================================
|
|
|
|
# ============================================================
|
|
# AI PROVIDER (choose one)
|
|
# ============================================================
|
|
|
|
# Option 1: OpenRouter (default, cheapest, NOT HIPAA)
|
|
AI_PROVIDER=openrouter
|
|
OPENROUTER_API_KEY=sk-or-v1-your-key
|
|
|
|
# Option 2: AWS Bedrock (HIPAA compliant with BAA)
|
|
# AI_PROVIDER=bedrock
|
|
# AWS_BEDROCK_REGION=us-east-1
|
|
# AWS_ACCESS_KEY_ID=AKIA...
|
|
# AWS_SECRET_ACCESS_KEY=...
|
|
# (Or use IAM role — no keys needed if running on EC2/ECS)
|
|
|
|
# Option 3: Azure OpenAI (HIPAA compliant with BAA)
|
|
# AI_PROVIDER=azure
|
|
# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
|
|
# AZURE_OPENAI_API_KEY=your-key
|
|
# AZURE_DEPLOYMENT_NAME=gpt-4o-mini
|
|
# AZURE_OPENAI_API_VERSION=2024-02-01
|
|
|
|
# Option 4: Google Vertex AI (HIPAA compliant with BAA)
|
|
# AI_PROVIDER=vertex
|
|
# Google models (Gemini and friends) are reached through LiteLLM, not the
|
|
# Google SDK — that dependency was removed. Configure them on the gateway
|
|
# and select them by their LiteLLM model id.
|
|
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
|
|
# (Or use default credentials if running on GCE/GKE/Cloud Run)
|
|
#
|
|
# Google STT — Gemini inline audio (auto-detected when GOOGLE_VERTEX_PROJECT set)
|
|
# TRANSCRIBE_PROVIDER=google
|
|
# GOOGLE_STT_MODEL=gemini-2.0-flash # or gemini-2.5-flash for better accuracy
|
|
#
|
|
# Google TTS — Google Cloud Text-to-Speech (auto-detected when GOOGLE_VERTEX_PROJECT set)
|
|
# TTS_PROVIDER=google
|
|
# GOOGLE_TTS_VOICE=en-US-Journey-F # female | en-US-Journey-D = male
|
|
# Other options: en-US-Studio-O, en-US-Neural2-C, en-US-Neural2-J
|
|
|
|
# Option 5: LiteLLM Proxy (self-hosted, routes to any provider)
|
|
# AI_PROVIDER=litellm
|
|
# LITELLM_API_BASE=http://localhost:4000
|
|
# LITELLM_API_KEY=sk-litellm-your-key
|
|
# Admin can discover available models via the admin panel
|
|
#
|
|
# LiteLLM Speech-to-Text
|
|
# TRANSCRIBE_PROVIDER=litellm
|
|
# LITELLM_STT_MODEL=whisper-1 # Use the model name from your LiteLLM model_list
|
|
# If your LiteLLM config uses full paths as model names, use the full path:
|
|
# LITELLM_STT_MODEL=openai/whisper-1
|
|
# NOTE: vertex_ai/chirp does NOT work via LiteLLM audio proxy.
|
|
# For Vertex AI speech, use TRANSCRIBE_PROVIDER=google (Gemini inline audio).
|
|
#
|
|
# LiteLLM TTS
|
|
# TTS_PROVIDER=litellm (auto-detected when LITELLM_API_BASE set)
|
|
# LITELLM_TTS_MODEL=tts-1 # Use model name from your LiteLLM model_list
|
|
# If your config uses full paths: LITELLM_TTS_MODEL=vertex_ai/google-tts
|
|
# LITELLM_TTS_VOICE=en-US-Journey-F # Google Cloud voice name (or alloy/nova for OpenAI)
|
|
|
|
# ============================================================
|
|
# TRANSCRIPTION (speech-to-text)
|
|
# ============================================================
|
|
|
|
# Option A: OpenAI Whisper (default if no AWS configured)
|
|
OPENAI_API_KEY=sk-your-openai-key
|
|
|
|
# Option B: Amazon Transcribe (HIPAA eligible, no S3 needed)
|
|
# Uses same AWS credentials as Bedrock above.
|
|
# Set TRANSCRIBE_PROVIDER=aws to force AWS even if OPENAI_API_KEY is set.
|
|
# Leave unset to auto-detect (uses AWS when AWS_BEDROCK_REGION is configured).
|
|
# TRANSCRIBE_PROVIDER=aws
|
|
|
|
# Option C: Local Whisper (privacy-first, no cloud API needed)
|
|
# Requires whisper.cpp or faster-whisper installed on the server.
|
|
# TRANSCRIBE_PROVIDER=local
|
|
# WHISPER_MODEL_SIZE=small # tiny, base, small, medium, large
|
|
# WHISPER_BINARY=whisper-cpp # or: whisper, faster-whisper
|
|
# WHISPER_MODEL_PATH= # custom path to .bin model file
|
|
# WHISPER_LANGUAGE=en
|
|
# WHISPER_THREADS=4 # defaults to CPU count - 1
|
|
|
|
# Amazon Transcribe Medical — better accuracy for clinical dictation
|
|
# Knows drug names, diagnoses, procedures, SOAP terminology
|
|
# HIPAA eligible (ensure your AWS account has a BAA)
|
|
# AWS_TRANSCRIBE_MEDICAL=true
|
|
# AWS_TRANSCRIBE_SPECIALTY=PRIMARYCARE
|
|
# Other options: CARDIOLOGY, NEUROLOGY, ONCOLOGY, RADIOLOGY, UROLOGY
|
|
|
|
# Optional
|
|
ELEVENLABS_API_KEY=
|
|
|
|
# Push Notifications (ntfy — self-hosted, optional)
|
|
# NTFY_URL=https://ntfy.yourdomain.com
|
|
# NTFY_TOKEN=tk_your_token_here
|
|
|
|
# App
|
|
PORT=3000
|
|
APP_URL=https://your-domain.com
|
|
|
|
# Cloudflare Turnstile (anti-bot on registration, optional)
|
|
# TURNSTILE_SITE_KEY=your-site-key
|
|
# TURNSTILE_SECRET_KEY=your-secret-key
|
|
JWT_SECRET=generate-a-random-64-char-string-here
|
|
|
|
# Application-layer encryption key for PHI at rest (Nextcloud tokens, audio backups)
|
|
# Generate with: openssl rand -hex 32
|
|
# REQUIRED in production. Rotating invalidates existing encrypted data.
|
|
DATA_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32
|
|
|
|
# Email (for verification & password reset)
|
|
SMTP_HOST=smtp.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=your-email@gmail.com
|
|
SMTP_PASS=your-app-password
|
|
SMTP_FROM=noreply@yourdomain.com
|
|
|
|
# Nextcloud (optional)
|
|
NEXTCLOUD_URL=https://cloud.yourdomain.com
|
|
|
|
# S3 Document Storage (optional — works with AWS S3, Backblaze B2, MinIO)
|
|
# S3_BUCKET=your-bucket-name
|
|
# S3_REGION=us-east-1
|
|
# S3_PREFIX=documents/
|
|
#
|
|
# For AWS S3: uses same AWS credentials as Bedrock above, or set S3-specific keys:
|
|
# S3_ACCESS_KEY_ID=...
|
|
# S3_SECRET_ACCESS_KEY=...
|
|
#
|
|
# For Backblaze B2:
|
|
# S3_ENDPOINT=https://s3.us-west-004.backblazeb2.com
|
|
# S3_REGION=us-west-004
|
|
# S3_ACCESS_KEY_ID=your-b2-application-key-id
|
|
# S3_SECRET_ACCESS_KEY=your-b2-application-key
|
|
#
|
|
# For MinIO (self-hosted):
|
|
# S3_ENDPOINT=http://minio:9000
|
|
# S3_REGION=us-east-1
|
|
# S3_ACCESS_KEY_ID=minio-access-key
|
|
# S3_SECRET_ACCESS_KEY=minio-secret-key
|
|
# S3_FORCE_PATH_STYLE=true
|
|
|
|
# ============================================================
|
|
# EMBEDDINGS (for Learning Hub semantic search)
|
|
# ============================================================
|
|
# Enables vector-based semantic search in Learning Hub
|
|
# Requires pgvector extension: apt-get install postgresql-16-pgvector
|
|
|
|
# Default model (Vertex AI text-embedding-005, 768 dims, English + code optimized)
|
|
EMBEDDING_MODEL=vertex_ai/text-embedding-005
|
|
EMBEDDING_DIMENSIONS=768
|
|
|
|
# Other Vertex AI embedding models:
|
|
# - vertex_ai/text-embedding-005 → 768 dims, English + code (recommended)
|
|
# - vertex_ai/gemini-embedding-001 → up to 3072 dims, multilingual + code
|
|
# - vertex_ai/text-multilingual-embedding-002 → 768 dims, multilingual focus
|
|
#
|
|
# LiteLLM usage (if using LiteLLM proxy):
|
|
# EMBEDDING_MODEL=text-embedding-005 # LiteLLM will route to configured provider
|
|
#
|
|
# OpenAI fallback (NOT HIPAA-eligible):
|
|
# Uses text-embedding-3-small if OPENAI_API_KEY is set and no Vertex/LiteLLM configured
|
|
|
|
# ============================================================
|
|
# DATABASE
|
|
# ============================================================
|
|
DATABASE_URL=postgresql://pedscribe:<password>@postgres:5432/pedscribe
|
|
DB_PASSWORD=pedscribe_secret_change_me
|
|
|
|
# ── Audio backups (optional) ────────────────────────────────────────────────
|
|
# Every recording is kept for 24 hours, whether its transcription succeeded or
|
|
# not. Unset, the audio lives in the audio_backups table; set, it goes to a
|
|
# bucket instead and only metadata stays in Postgres. The generated-images key
|
|
# is scoped to that bucket and cannot be reused here — create a bucket and a
|
|
# user of its own, and give the bucket a 24h expiry rule as a backstop.
|
|
# Audio is gzipped and AES-256-GCM encrypted before it is stored, either way.
|
|
# AUDIO_BACKUPS_S3_ENDPOINT=http://assets:9000
|
|
# AUDIO_BACKUPS_S3_BUCKET=audio-backups
|
|
# AUDIO_BACKUPS_S3_REGION=us-east-1
|
|
# Prefer the _FILE variants: credentials then stay out of the process environment.
|
|
# AUDIO_BACKUPS_S3_ACCESS_KEY_FILE=/run/secrets/audio-backups-access-key
|
|
# AUDIO_BACKUPS_S3_SECRET_KEY_FILE=/run/secrets/audio-backups-secret-key
|
|
# AUDIO_BACKUPS_S3_ACCESS_KEY=
|
|
# AUDIO_BACKUPS_S3_SECRET_KEY=
|
|
|
|
# ============================================================
|
|
# EVERY REMAINING VARIABLE THE APP READS
|
|
# Completed 2026-09-11: this file listed 18 of the 67 variables src/ actually
|
|
# reads. Each entry below shows the default the code falls back to, so an
|
|
# unset line means "this default", not "broken".
|
|
# ============================================================
|
|
|
|
# ── Mail ────────────────────────────────────────────────────────────────────
|
|
# SMTP_SECURE=false # true for implicit TLS (port 465)
|
|
|
|
# ── Admin lockdown ──────────────────────────────────────────────────────────
|
|
# With several admins, everything in the admin panel is editable by all of
|
|
# them. Set this and configuration becomes read-only in the panel and refused
|
|
# by the server: prompts, model policy, retrieval limits and budgets, TTS/STT,
|
|
# embeddings, SMTP and email templates. Day-to-day operation stays available —
|
|
# announcements, registration and invitations, feature flags, site details.
|
|
# A setting added later is locked until it is deliberately added to the
|
|
# editable list, rather than defaulting to open.
|
|
#
|
|
# It is deliberately an environment variable: a setting could be switched off
|
|
# by the very admin it restrains, so lifting it needs host access and a
|
|
# restart. Read access is unaffected — locked settings stay visible.
|
|
# ADMIN_LOCKDOWN=false
|
|
|
|
# ── Identity and limits ─────────────────────────────────────────────────────
|
|
# SITE_NAME=Pediatric AI Scribe
|
|
# API_RATE_LIMIT_MAX=200 # requests per window across /api
|
|
# LOGIN_RATE_LIMIT_MAX=10 # login attempts per 15 minutes
|
|
# Codes emailed for sign-in, per IP per hour. Lower than the login limit
|
|
# because each request sends mail to somebody else's address (default 5).
|
|
#LOGIN_CODE_RATE_LIMIT_MAX=5
|
|
# NODE_ENV=production # with APP_URL, puts the app in production mode:
|
|
# refuses to start without JWT_SECRET or a CORS origin
|
|
# CORS_ORIGINS= # extra allowed origins, comma-separated, beyond APP_URL
|
|
|
|
# ── Services ────────────────────────────────────────────────────────────────
|
|
# REDIS_URL=redis://ped-ai-redis:6379
|
|
# LOKI_URL=http://monitoring-loki:3100 # log shipping; unset disables it
|
|
# MCP_SERVER_URL= # legacy single MCP URL
|
|
|
|
# ── LiteLLM gateway ─────────────────────────────────────────────────────────
|
|
# LITELLM_MASTER_KEY= # falls back to OPENAI_API_KEY, then 'sk-litellm'
|
|
# LITELLM_DEFAULT_MODEL= # falls back to OPENAI_MODEL
|
|
# LITELLM_FALLBACK_MODEL= # tried when the default model fails
|
|
# OPENAI_MODEL= # legacy name for the default chat model
|
|
# LITELLM_TTS_VOICES= # comma-separated voices offered in the pickers
|
|
|
|
# ── Clinical Assistant: retrieval ───────────────────────────────────────────
|
|
# CLINICAL_ASSISTANT_MCP_URL=http://mcp:8000/mcp
|
|
# CLINICAL_ASSISTANT_MCP_URLS= # comma-separated, tried in order
|
|
# CLINICAL_ASSISTANT_SEARCH_TOOL=clinical_semantic_search # the only accepted value
|
|
# CLINICAL_ASSISTANT_MCP_INITIALIZE_TIMEOUT_MS=30000
|
|
# CLINICAL_ASSISTANT_MCP_REQUEST_TIMEOUT_MS=90000
|
|
# CLINICAL_ASSISTANT_MCP_SESSION_TTL_MS=600000
|
|
# CLINICAL_ASSISTANT_MCP_WARMUP= # open a session at boot
|
|
# CLINICAL_ASSISTANT_MCP_WARMUP_DELAY_MS=
|
|
|
|
# ── Clinical Assistant: models and prompts ──────────────────────────────────
|
|
# CLINICAL_ASSISTANT_IMAGE_MODEL=openai-gpt-image-1
|
|
# CLINICAL_ASSISTANT_PROMPT_MODEL= # model that writes the starter prompts
|
|
# CLINICAL_ASSISTANT_PROMPT_POOL_TARGET=1000
|
|
# CLINICAL_ASSISTANT_PROMPT_POOL_KEY=clinical-assistant:prompt-pool:v2
|
|
# CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS=
|
|
# CLINICAL_ASSISTANT_PROMPT_POOL_WARMUP_DELAY_MS=
|
|
# CLINICAL_ASSISTANT_EXAMPLE_CACHE_MS=600000
|
|
# PATIENT_TAKEHOME_BEHAVIOR= # overrides the admin-editable take-home guidance
|
|
|
|
# ============================================================
|
|
# OPENBAO (optional)
|
|
# The entrypoint fetches secrets from OpenBao only when OPENBAO_ADDR is set;
|
|
# unset, it uses this file and nothing else. A value set here or in the compose
|
|
# environment always wins over the OpenBao copy, so an override needs no
|
|
# OpenBao change.
|
|
#
|
|
# To use OpenBao: set OPENBAO_ADDR, OPENBAO_ROLE_ID, OPENBAO_SECRET_ID
|
|
# (and OPENBAO_KV_PATH if not kv/ped-ai/prod)
|
|
# To stop using it: unset OPENBAO_ADDR, and put the keys it held in this
|
|
# file. List them without printing values:
|
|
# bao kv get -format=json kv/ped-ai/prod | jq 'keys'
|
|
# ============================================================
|
|
# OPENBAO_ADDR=
|
|
# OPENBAO_ROLE_ID=
|
|
# OPENBAO_SECRET_ID=
|
|
# OPENBAO_KV_PATH=kv/ped-ai/prod
|
|
|
|
# Optional. Lets something outside this host scrape /metrics with
|
|
# `Authorization: Bearer <token>`. Unset by default, and an unset token can
|
|
# never match — Prometheus scrapes pediatric-ai-scribe:3000 directly over the
|
|
# Docker network, which needs no token. Requests arriving through the reverse
|
|
# proxy (they carry X-Forwarded-For) get a 404 either way.
|
|
METRICS_TOKEN=
|
|
|
|
# Gotenberg (LibreOffice behind an HTTP API), used to turn a generated deck or
|
|
# document into PDF. Defaults to http://gotenberg:3000 on the danvics_convert
|
|
# network. PDF is the one export allowed to fail: if this is unreachable the
|
|
# PowerPoint and Word downloads still work.
|
|
GOTENBERG_URL=
|