@crvouga/mockingbird-service-garmin-健康(英语:
本地模拟器。 真正的API合同. 部分 嘲笑鸟。 。 。 .
WIP Garmin Connect 开发者程序健康API模拟器用于计划同步流:OAuth 2.0与PKCE,用户代号,回填请求(每类型和窗口一次),推并按下交付给您的应用程序的网络hooks,并注销对合成数据的注册. Garmin的端点规格在经过批准的-开发者门户网站后方,因此每条有线细节都是一份真正的消费者发送和剖析的合同(问题#545),而不是供应商验证. 见 证据和最佳猜测。 。 。 .
安装
bun add @crvouga/mockingbird-service-garmin-health
使用量
import { createServer } from "@crvouga/mockingbird-service-garmin-health/server"
// Test setup: a local Garmin. One origin serves the API, token and consent routes, so it replaces
// https://apis.garmin.com, https://diauth.garmin.com and https://connect.garmin.com in the app. The
// emulator posts webhooks to your receiver, so tell it your webhook base URL.
const server = await createServer({
webhooks: { baseUrl: "http://localhost:8000/api/v1/garmin/webhooks", clientId: "my-client-id" },
})
const garmin = server.url
// Application code: the requests it sends to Garmin (default user garmin-user-1 and its token).
const headers = { authorization: "Bearer mock_garmin_token", accept: "application/json" }
const me = (await (await fetch(`${garmin}/wellness-api/rest/user/id`, { headers })).json()) as {
userId: string
}
const day = 1767225600 // 2026-01-01T00:00:00Z
const backfill = await fetch(
`${garmin}/wellness-api/rest/backfill/sleeps?summaryStartTimeInSeconds=${day}&summaryEndTimeInSeconds=${day + 86400}`,
{ headers },
)
// 202 with an empty body; a POST to http://localhost:8000/api/v1/garmin/webhooks/push follows.
console.log(me.userId, backfill.status)
await server.close()
运行 mockingbird-garmin-health serve --port 12131 --webhook-base-url http://localhost:8000/api/v1/garmin/webhooks. Garmin没有发布公众JS SDK,消费者使用生HTTP:指其API出处,符号URL并授权URL(三个设置)在模拟器出处. 浏览器在 HTTP 上到达同意页面, 因此使用已服务仿真器而不是在进程内 fetch 为那一步. 无 --webhook-base-url 任何物品都无法交付,直到 PUT /__admin/webhook-base-url 设一.
路线
| 路线 | 它会做什么 |
|---|---|
GET /oauth2Confirm | 同意页https://connect.garmin.com/oauth2Confirm (关于供应商)。 需求 response_type=code, (中文(简体) ), client_id, (中文(简体) ), redirect_uri, (中文(简体) ), code_challenge 财务报告和审定财务报表 code_challenge_method=S256· ; state 是可选的,并重复;否 scope重新编号(302) redirect_uri?code=<code>&state=<state>,或 ?error=access_denied&error_description=...&state=... 当 denyConsent 设置开始。 未知客户端或未注册的重定向 URI 从未被重定向到 (400) invalid_request) (中文(简体) ). |
POST /di-oauth2-service/oauth/token | 表格机构、机构内的客户证书client_id, (中文(简体) ), client_secret;基本内容也被接受). authorization_code (与 redirect_uri 财务报告和审定财务报表 code_verifier和 refresh_token 赠款。 答复 200 access_token, (中文(简体) ), token_type: "bearer", (中文(简体) ), expires_in (整数秒), refresh_token。重复使用或过期的代码、错误的核查者或错误 redirect_uri 为 400 invalid_grant; 不良客户资格 400 invalid_client (401与Basic;不明、已退休或已注销登记的新订证券400) invalid_grant。 。 。 . |
GET /wellness-api/rest/user/id | { "userId": "<id>" } 为无记名符号背后的用户. |
GET /wellness-api/rest/backfill/{type} | type 实值 activities, (中文(简体) ), sleeps, (中文(简体) ), dailies, (中文(简体) ), epochs, (中文(简体) ), bodyComps 或者说 hrv· ; summaryStartTimeInSeconds 财务报告和审定财务报表 summaryEndTimeInSeconds 是整数秒(启动后结束,最多相隔90天)。 202 有一具空躯. 同一用户,类型和窗口又是409. 未知型号为404,坏界为400. |
DELETE /wellness-api/rest/user/registration | 204,没有尸体。 用户的每个符号,然后回答401,刷新符号是 invalid_grant。 。 。 . |
GET /wellness-api/rest/{type}?uploadStartTimeInSeconds=&uploadEndTimeInSeconds=&token= | 这个 callbackURL 电话通知。 由其认证 token 查询参数,而不是由标题; 200 带有 JSON 摘要阵列, 401 表示未知的符号、另一种类型或已注销的用户. |
API 拨打无记名信使,或无记名信使,过期(在模拟时钟上)或注销注册,回答401 {"error":"unauthorized","message":...}。授权代码为单用并持续了10分钟;访问符号为最后 tokenTtlSeconds (默认86400)在模拟器时钟上;刷新会旋转刷新符并让旧符退休(rotateRefreshTokens: false 保持它,省略 refresh_token 从反应,所以消费者保留了旧的。 失败的交换( 错误的校验或重定向 URI) 不消耗代码 。 默认客户端是 mock_garmin_client 编号 : mock_garmin_secret (任何方向调整的URI,除非 clients[].redirectUris ;默认用户是 garmin-user-1 无记名者 mock_garmin_token 并刷新标志 mock_garmin_refresh直到2100年时钟结束.
后补和交货
每个用户、类型和窗口(起止秒)接受回填。 当它被接受时, 模拟器会选择用户的种子摘要 [start, end) (单位:千美元)startTimeInSeconds,或 measurementTimeInSeconds 页:1 bodyComps),并按照用户的 delivery 模式 :
- 推动 (默认) :
POST <baseUrl>/push与{ "<type>": [ { "userId": ..., ...summary } ] },摘要与种子加userId。 。 。 . - ing数字 :
POST <baseUrl>/ping与{ "<type>": [ { "userId": ..., "callbackURL": "<emulator origin>/wellness-api/rest/<type>?uploadStartTimeInSeconds=S&uploadEndTimeInSeconds=E&token=<token>" } ] }复选需要取消Authorization头并服务与 JSON 阵列相同的摘要 。 在命名空间中, URL 携带/__admin/ns/<name>因此,它达到了正确的数据 没有标题。 这个publicUrl设置取代消费者在另一个地址到达模拟器时的源头,而不是回填调用.
每一次送货都带着 garmin-client-id 页眉( clientId 选项,默认 mock_garmin_client无签名。 没有种子摘要的窗口仍然是202个,没有交付;窗口按要求计算。 默认种子给出 garmin-user-1 2026-01-01(UTC)上各有1个类型汇总;上方的第一个窗口覆盖了全部. 摘要为通过:一项活动没有种子 durationInSeconds 由消费者决定怎么做.
Webhook 路径 /push, (中文(简体) ), /ping, (中文(简体) ), /deregistration 财务报告和审定财务报表 /permissions 在基准 URL 下 (paths 覆盖每个). 标准 Webhook 枢机运行送货:在非-2xx 答案上用默认的后退来重试(retryDelaysMs 改变它, GET /__admin/webhooks * 列出每批交货的状态(delivered, (中文(简体) ), failed, (中文(简体) ), dropped和接收器的状态 每一次尝试, GET /__admin/webhooks/events 这些尸体, POST /__admin/webhooks/:id/replay 财务报告和审定财务报表 /webhooks/flush 重新运行它们.
测试控制
种子与 createRuntime编号 :createServer 选项(users, (中文(简体) ), clients, (中文(简体) ), grants, (中文(简体) ), codes, (中文(简体) ), summaries, (中文(简体) ), settings, (中文(简体) ), webhooks)或以下管理路线(相对于 /__admin,可通过 adminPrefix:
| 管理路线 | 对 |
|---|---|
GET编号 :PUT /settings | autoDeliver (虚假持有接受的通知), denyConsent, (中文(简体) ), consentUserId (谁将同意页的符号输入;默认第一个用户), rotateRefreshTokens, (中文(简体) ), tokenTtlSeconds, (中文(简体) ), publicUrl。 。 。 . |
GET编号 :PUT /users | 列表,或添加或替换 { userId, delivery: "push" | "ping", registered?, permissions? }。 。 。 . |
PUT /summaries | 种子 [{ userId, type, data, key? }]。 。 。 . data 需求 startTimeInSeconds (单位:千美元)measurementTimeInSeconds 页:1 bodyComps); 重复 key 替换. |
GET /backfills | 接收回填请求的历史和通知(编号、类型、模式、, pending, (中文(简体) ), sent 或者说 dropped,项目计数;绝不是尸体. |
POST /notifications/deliver { ids? } 编号 : POST /notifications/drop { ids? } | 发送或永远不发送持有的通知(全部或所命名的通知). |
PUT /webhook-base-url { baseUrl, clientId?, paths? } | 设置命名空间的 Webhook 端点 。 启动时设定的终点( E)webhooks.baseUrl, (中文(简体) ), --webhook-base-url)服务于每个取名空间并保留;这些被添加. |
POST /users/:userId/deregister | 在 Garmin Connect 中切断用户的功能:他们的代号停止工作 POST <baseUrl>/deregistration 携带 { "deregistrations": [ { "userId", "userAccessToken" } ] }。 。 。 . |
POST /users/:userId/permissions { permissions: { "<name>": boolean } } | 合并用户权限 POST <baseUrl>/permissions 与 { "userPermissionsChange": [ { "userId", "permissions" } ] }。 。 。 . |
POST /users/:userId/expire-token { revokeRefresh? } | 在不移动时钟的情况下过期用户的访问符; revokeRefresh 还退休 刷新符号. |
POST /clock {"advance": "2h"} 移动命名空间时钟:访问比 expires_in 然后回答401, 超过10分钟的代码被拒绝。 标准 /__admin/health, (中文(简体) ), /state (会费) users, (中文(简体) ), clients, (中文(简体) ), grants, (中文(简体) ), codes, (中文(简体) ), summaries, (中文(简体) ), backfills, (中文(简体) ), notifications, (中文(简体) ), callbacks, (中文(简体) ), settings, (中文(简体) ), /reset, (中文(简体) ), /requests,有时间线检查站和故障。 标题 x-mockingbird-namespace, (中文(简体) ), /__admin/ns/<name> 路径和证书映射(承接符)将每个命名空间的用户、摘要、符号和回填历史隔离。 期刊中包含元数据,从不摘要或符号.
预设POST /__admin/faults {"preset": "<name>", "count": n}: unauthorized (卫生API 401), conflict (回填409个), rate_limited (429处) Retry-After: 1, (中文(简体) ), server_error (500个), refresh_rejected (端点400) invalid_grant, (中文(简体) ), connection_drop (回填下套接字),和webhook模式 duplicate, (中文(简体) ), webhook_drop 财务报告和审定财务报表 webhook_reorder (下一次交货是两次发送,从未发送,或与之后的一次互换). 预设体是被脚本固定的.
证据和最佳猜测
健康API需要一个经批准的开发商账户, 来源是第545期(消费者寄送和剖析的内容)中的消毒线和概览 https://developer.garmin.com/gc-developer-program/health-api/ 互联网档案馆的存檔,存档日期2013-12-21。下面的一切都是一个 最好猜,不是甲骨文验证,跟踪为 needs-oracle-check数字 :
- 同意重定向形状和PKCE检查(仅S256,一个43至128字符核查器,单用10分钟代码,一个无法使用代码的交换);
- 400个
invalid_grant重新使用的代码、错误的验证器、错误的重新定向URI和一个未知或已退役的刷新符,以及400invalid_client(a) 不良证书(该期说400或401是一个被拒绝的刷新令牌); - 刷新键旋转( 默认; 旋转设置存在, 因为不知道供应商的行为) 和
token_type: "bearer"· ; - 用户 id 形状 (
garmin-user-1;真实的id是不透明的)和expires_in* 86400; - 202 带有被接受回填的空正文, 409 为重复类型和窗口(密钥为用户,类型,始末),90天窗口盖有400个,404为未知类型,JSON出错正文
{"error","message"}· ; - a. 一个空窗口,回答202,但无所提供(问题留下“不交付或空交付”);
- 其中摘要选择窗口(
[start, end)在摘要本身的开始时间,measurementTimeInSeconds(a) 每件接受回填一次,按其类型键归类; - 429个尸体和
Retry-After标题,401机构,和204,删除登记(该问题只需要成功状态); - 交货没有签名,只有
garmin-client-id,并重试非-2xx答案的政策(枢纽的标准后退); - 铃声
callbackURL形状,其符号是唯一的认证,并且该符号在用户取消注册之前仍然重复请求.
如果问题和供应商文件可能不同,模拟者就沿着问题的线走.
甲骨文和测试
bun test 运行接受测试(服务请求的每个编号行为一次,通过 test/consumer.ts,是消费者的 httpx 供应商级和 FastAPI 接收器的端口,其中Webhook 枢接器向接收器发送并超过一个已服务过的 HTTP 槽),并且每一次操作都具有分解检测的自平. bun run parity 需求 GARMIN_HEALTH_ACCESS_TOKEN,只执行保险箱 user/id ,并永远不要打印标识符或证书。 后补,去注册和网络呼喊 并不是运行对加明.
故意不模拟
下方的拉出点 /wellness-api/rest/{type}?uploadStartTimeInSeconds=...&token=... 作为被调查的读取路径( 仅限平声) callbackURL ),牵引电源生成,活动细节,压力,呼吸,脉冲-牛,皮肤温度,血压,MoveIQ和所有其他回填类型,Garmin Connection(非健康)APIs,更古老的OAuth 1.0a流量,同意UI(同意由决定) denyConsent 设定和浏览器仅被重定向), 跨范围同意和开发者门户的固定范围, 权限执行(仅宣布许可变更), 费率限制核算和供应商的回填配额 超出每个类型和窗口一个请求(限制仅通过预设来脚本), webhook 认证超出 garmin-client-id 页: 1.
API 密码
根运行时间导出 : GarminHealthAPI, (中文(简体) ), GARMIN_HEALTH_NAMESPACE, (中文(简体) ), SUMMARY_TYPES, (中文(简体) ), MAX_BACKFILL_SECONDS, (中文(简体) ), isSummaryType, (中文(简体) ), summaryTime, (中文(简体) ), createRuntime, (中文(简体) ), GARMIN_HEALTH_PRESETS, (中文(简体) ), WEBHOOK_PATHS, (中文(简体) ), webhookEndpoints, (中文(简体) ), document, (中文(简体) ), operationIds, (中文(简体) ), supportedOperationIds类型包括: GarminHealthAPIOptions, (中文(简体) ), GarminUser, (中文(简体) ), DeliveryMode, (中文(简体) ), Client, (中文(简体) ), Grant, (中文(简体) ), OAuthCode, (中文(简体) ), Summary, (中文(简体) ), SummarySeed, (中文(简体) ), SummaryType, (中文(简体) ), BackfillRecord, (中文(简体) ), Notification, (中文(简体) ), CallbackToken, (中文(简体) ), Settings, (中文(简体) ), GarminWebhookEvent, (中文(简体) ), GarminHealthRuntimeOptions, (中文(简体) ), GarminHealthRuntime, (中文(简体) ), GarminHealthWebhooks, (中文(简体) ), OperationId, (中文(简体) ), SupportedOperationId。 。 。 .
/server 出口 createServer, (中文(简体) ), serveTarget, (中文(简体) ), DEFAULT_PORT 类型 GarminHealthServerOptions, (中文(简体) ), GarminHealthServer。 。 。 .