Files
HealthLog/scripts/env-manifest.json
T
Marc-André Bombeck 88139fb981 feat(integrations): add Oura Cloud v2 OAuth sync
Connect an Oura account over the Cloud v2 API: an authorization-code OAuth
flow (shared app from OURA_CLIENT_ID / OURA_CLIENT_SECRET) on the same signed
state machinery as Polar. Unlike Polar, Oura issues refresh tokens; the sync
refreshes reactively on a 401, rotating and re-encrypting both tokens, and a
403 stays a hard reauth rather than a refresh.

An hourly poll-cohort job maps daily readiness to RECOVERY_SCORE, sleep to
per-stage durations plus efficiency / HRV / resting heart rate / respiratory
rate, and daily activity to steps and active energy — all tagged source OURA,
idempotent on (userId, type, source, externalId). Readiness flows through the
canonical recovery ladder above the computed proxy. Settings gains a connect /
disconnect card. Tokens are never logged or returned; every egress goes
through safeFetch.
2026-06-14 20:10:35 +02:00

155 lines
6.3 KiB
JSON

{
"$schema": "./env-manifest.schema.json",
"description": "Manifest of env vars required by a HealthLog production deployment. Used by `pnpm check-env` to catch missing/empty vars BEFORE a deploy ships. v1.4.42 W6 — born out of the v1.4.40 AP-2 .p8 install gap that shipped silently for 3 days.",
"groups": [
{
"name": "Core",
"description": "Database, app URL, encryption keys. The app refuses to boot without these.",
"required": true,
"variables": [
{
"name": "DATABASE_URL",
"purpose": "Postgres connection string. App boot fails if missing.",
"example": "postgresql://healthlog:<pw>@db:5432/healthlog?schema=public"
},
{
"name": "POSTGRES_PASSWORD",
"purpose": "Database password for the bundled `db` service in docker-compose.yml.",
"example": "<openssl rand -hex 32>"
},
{
"name": "NEXT_PUBLIC_APP_URL",
"purpose": "Public URL embedded in OAuth redirects, webhook callbacks, APNs deep-links."
},
{
"name": "APP_URL",
"purpose": "Server-side mirror of NEXT_PUBLIC_APP_URL; used by background workers."
},
{
"name": "ENCRYPTION_KEY",
"purpose": "AES-256-GCM key for at-rest encryption of provider tokens. 64 hex chars (32 bytes). Required unless ENCRYPTION_KEYS is set."
},
{
"name": "API_TOKEN_HMAC_KEY",
"purpose": "HMAC-SHA256 key for hashing external Bearer tokens. 64 hex chars."
}
]
},
{
"name": "Withings OAuth",
"description": "Required to enable the Withings integration. App boots without these but Withings sync stays disabled.",
"required": false,
"variables": [
{
"name": "WITHINGS_CLIENT_ID",
"purpose": "Withings OAuth client_id from https://developer.withings.com."
},
{
"name": "WITHINGS_CLIENT_SECRET",
"purpose": "Withings OAuth client_secret."
},
{
"name": "WITHINGS_WEBHOOK_SECRET",
"purpose": "Random secret carried as a path segment in the Withings webhook callback URL. Generate with `openssl rand -hex 32`."
}
]
},
{
"name": "Polar OAuth",
"description": "Required to enable the Polar AccessLink integration. App boots without these but the Polar connect button stays disabled. Register an app at https://admin.polaraccesslink.com and set the redirect URI to ${NEXT_PUBLIC_APP_URL}/api/polar/callback.",
"required": false,
"allOrNone": true,
"variables": [
{
"name": "POLAR_CLIENT_ID",
"purpose": "Polar AccessLink OAuth client id."
},
{
"name": "POLAR_CLIENT_SECRET",
"purpose": "Polar AccessLink OAuth client secret."
}
]
},
{
"name": "Oura OAuth",
"description": "Required to enable the Oura Cloud v2 integration. App boots without these but the Oura connect button stays disabled. Register an app at https://cloud.ouraring.com/oauth/applications and set the redirect URI to ${NEXT_PUBLIC_APP_URL}/api/oura/callback.",
"required": false,
"allOrNone": true,
"variables": [
{
"name": "OURA_CLIENT_ID",
"purpose": "Oura Cloud OAuth client id."
},
{
"name": "OURA_CLIENT_SECRET",
"purpose": "Oura Cloud OAuth client secret."
}
]
},
{
"name": "APNs (iOS push)",
"description": "All four core APNS_* vars together. Missing any disables APNs silently — the v1.4.40 AP-2 gap that motivated this manifest. allOrNone trips the synthetic <all-or-none> required row whenever 1-3 of the 4 are set.",
"required": false,
"allOrNone": true,
"variables": [
{
"name": "APNS_KEY_ID",
"purpose": "10-char Key ID from Apple Developer."
},
{
"name": "APNS_TEAM_ID",
"purpose": "10-char Team ID from Apple Developer."
},
{
"name": "APNS_BUNDLE_ID",
"purpose": "iOS app bundle id, e.g. dev.healthlog.ios."
},
{
"name": "APNS_KEY",
"purpose": "APNs auth key (.p8 contents). Use APNS_KEY for 12-factor env-block deploys; APNS_KEY_FILE is the filesystem alternative — at least one of the two must be set.",
"anyOf": ["APNS_KEY", "APNS_KEY_FILE"]
}
]
},
{
"name": "Deploy webhook",
"description": "Coolify -> HealthLog deploy-result webhook. Without this, the auto-deploy success/failure feedback loop is broken.",
"required": false,
"variables": [
{
"name": "DEPLOY_WEBHOOK_SECRET",
"purpose": "Shared secret for the X-Deploy-Webhook-Secret header on POST /api/internal/deploy-webhook. 64 hex chars."
},
{
"name": "DEPLOY_LOGS_URL",
"purpose": "URL of the deployment dashboard (e.g. the Coolify UI) linked in deploy-failure notifications. Optional; when unset the notification omits the link."
}
]
},
{
"name": "TLS leaf pin monitor",
"description": "Baseline for the TLS leaf SPKI-change alarm. The native client pins the served leaf certificate; without this the monitor logs the served pin but cannot alarm on a rotation. See docs/ops/tls-cert-pin.md.",
"required": false,
"variables": [
{
"name": "TLS_LEAF_SPKI_PINS",
"purpose": "Comma-separated known-good leaf SPKI pins, base64(sha256(DER subjectPublicKeyInfo)) — the same value the native client pins. Hold both the current and the next pin during a dual-pin renewal window."
}
]
},
{
"name": "Off-host backups",
"description": "Required ALL-OR-NONE: any of these without the rest disables backups silently.",
"required": false,
"allOrNone": true,
"variables": [
{ "name": "BACKUP_S3_ENDPOINT", "purpose": "S3-compatible endpoint URL." },
{ "name": "BACKUP_S3_BUCKET", "purpose": "Bucket name." },
{ "name": "BACKUP_S3_ACCESS_KEY", "purpose": "S3 access key." },
{ "name": "BACKUP_S3_SECRET_KEY", "purpose": "S3 secret key." },
{ "name": "BACKUP_ENCRYPTION_KEY", "purpose": "Separate AES key for the backup body — MUST NOT match ENCRYPTION_KEY." }
]
}
]
}