feat: make WHOOP the primary health source - #10
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
api | 730b861 | Commit Preview URL Branch Preview URL |
Aug 21 2026, 04:29 PM |
| return crypto.subtle.importKey( | ||
| "raw", | ||
| decodeWhoopTokenEncryptionKey(keyMaterial), | ||
| { name: "AES-GCM" }, |
There was a problem hiding this comment.
The importEncryptionKey function is called on every encrypt/decrypt operation, re-importing the key material each time. While Web Crypto handles this efficiently, you could cache the imported CryptoKey if performance becomes a concern in high-throughput scenarios. Not a blocker, just something to keep in mind for later optimization.
| if (typeof value !== "string" || value.length > 64) return false; | ||
| if (resource === "cycles") return /^(?:[1-9][0-9]*)$/.test(value); | ||
| return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); | ||
| }; |
There was a problem hiding this comment.
The constant-time string comparison implementation here is sound. Just noting that the early left.length ^ right.length assignment means length differences are already factored in, and the padded loop ensures timing doesn't leak length info. Well done.
| throw new Error("WHOOP activity ID must be a UUID"); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The input validation for cycle IDs and activity UUIDs before constructing paths is a good defense-in-depth measure against path traversal. The UUID regex validation using Zod here ensures malicious input like ../../secrets can't be injected into the path.
| if (operation !== "token refresh") throw error; | ||
| if (error instanceof WhoopRequestError | ||
| && error.status !== undefined | ||
| && error.status >= 400 |
There was a problem hiding this comment.
Consider adding an explicit timeout to the fetch calls. Cloudflare Workers have a default timeout, but a hung upstream WHOOP request could tie up worker resources. Something like AbortSignal.timeout(30_000) would provide defense against slow upstream responses.
| raw_json TEXT NOT NULL | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_whoop_recoveries_user_updated ON whoop_recoveries(whoop_user_id, upstream_updated_at); |
There was a problem hiding this comment.
The nap column stores a boolean as INTEGER (SQLite convention), which is fine. Just noting the sleep stage millisecond fields use INTEGER while some other numeric fields use REAL - this is intentional since durations are whole milliseconds from the provider. Consistent approach.
| weight_kilogram: z.number(), | ||
| max_heart_rate: z.number().int(), | ||
| }).passthrough(); | ||
|
|
There was a problem hiding this comment.
Using .passthrough() on provider schemas is the right call here - it ensures new fields WHOOP adds won't cause validation failures and will be preserved in raw_json. Good forward compatibility.
| @@ -20,7 +20,7 @@ A practical guide to creating iOS Shortcuts that export your Apple Health data t | |||
| - Apple Watch (recommended for comprehensive data) | |||
| - iOS Shortcuts app (pre-installed) | |||
| - Your API endpoint: `https://api.anuragd.me` | |||
There was a problem hiding this comment.
Good call replacing the hardcoded token with ${API_TOKEN} placeholder. The PR description mentions rotating the previously exposed credential before production - make sure that happens as part of the rollout checklist.
| }) as unknown as MessageBatch<WhoopQueueMessage>; | ||
|
|
||
| const TEST_WHOOP_CLIENT_SECRET = "test-whoop-client-secret"; | ||
| const encoder = new TextEncoder(); |
There was a problem hiding this comment.
The signedWebhook helper properly computes the HMAC over timestamp + body which matches the verification logic. This is a nice test utility that ensures webhook tests actually exercise the real signature verification path.
Summary
Verification
Rollout gates
This PR is intentionally a draft. Before production rollout:
No live provider calls, OAuth consent, remote migration, deployment, or Apple cutover were performed by this branch.
Preview verification
not_connectedcontract after migration0020_whoop.sql.