@elizaos/plugin-health 2.0.0-beta.1
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/README.md +107 -0
- package/dist/actions/index.d.ts +20 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +5 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/anchors/index.d.ts +19 -0
- package/dist/anchors/index.d.ts.map +1 -0
- package/dist/anchors/index.js +9 -0
- package/dist/anchors/index.js.map +1 -0
- package/dist/connectors/contract-stubs.d.ts +112 -0
- package/dist/connectors/contract-stubs.d.ts.map +1 -0
- package/dist/connectors/contract-stubs.js +1 -0
- package/dist/connectors/contract-stubs.js.map +1 -0
- package/dist/connectors/index.d.ts +28 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +202 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/contracts/circadian-default.d.ts +15 -0
- package/dist/contracts/circadian-default.d.ts.map +1 -0
- package/dist/contracts/circadian-default.js +30 -0
- package/dist/contracts/circadian-default.js.map +1 -0
- package/dist/contracts/circadian.d.ts +92 -0
- package/dist/contracts/circadian.d.ts.map +1 -0
- package/dist/contracts/circadian.js +14 -0
- package/dist/contracts/circadian.js.map +1 -0
- package/dist/contracts/health.d.ts +9 -0
- package/dist/contracts/health.d.ts.map +1 -0
- package/dist/contracts/health.js +21 -0
- package/dist/contracts/health.js.map +1 -0
- package/dist/contracts/lifeops-connector-degradation.d.ts +9 -0
- package/dist/contracts/lifeops-connector-degradation.d.ts.map +1 -0
- package/dist/contracts/lifeops-connector-degradation.js +17 -0
- package/dist/contracts/lifeops-connector-degradation.js.map +1 -0
- package/dist/contracts/lifeops.d.ts +3123 -0
- package/dist/contracts/lifeops.d.ts.map +1 -0
- package/dist/contracts/lifeops.js +635 -0
- package/dist/contracts/lifeops.js.map +1 -0
- package/dist/contracts/permissions.d.ts +39 -0
- package/dist/contracts/permissions.d.ts.map +1 -0
- package/dist/contracts/permissions.js +1 -0
- package/dist/contracts/permissions.js.map +1 -0
- package/dist/default-packs/bedtime.d.ts +14 -0
- package/dist/default-packs/bedtime.d.ts.map +1 -0
- package/dist/default-packs/bedtime.js +48 -0
- package/dist/default-packs/bedtime.js.map +1 -0
- package/dist/default-packs/contract-stubs.d.ts +161 -0
- package/dist/default-packs/contract-stubs.d.ts.map +1 -0
- package/dist/default-packs/contract-stubs.js +1 -0
- package/dist/default-packs/contract-stubs.js.map +1 -0
- package/dist/default-packs/index.d.ts +18 -0
- package/dist/default-packs/index.d.ts.map +1 -0
- package/dist/default-packs/index.js +39 -0
- package/dist/default-packs/index.js.map +1 -0
- package/dist/default-packs/sleep-recap.d.ts +14 -0
- package/dist/default-packs/sleep-recap.d.ts.map +1 -0
- package/dist/default-packs/sleep-recap.js +51 -0
- package/dist/default-packs/sleep-recap.js.map +1 -0
- package/dist/default-packs/wake-up.d.ts +14 -0
- package/dist/default-packs/wake-up.d.ts.map +1 -0
- package/dist/default-packs/wake-up.js +61 -0
- package/dist/default-packs/wake-up.js.map +1 -0
- package/dist/health-bridge/health-bridge.d.ts +57 -0
- package/dist/health-bridge/health-bridge.d.ts.map +1 -0
- package/dist/health-bridge/health-bridge.js +558 -0
- package/dist/health-bridge/health-bridge.js.map +1 -0
- package/dist/health-bridge/health-connectors.d.ts +23 -0
- package/dist/health-bridge/health-connectors.d.ts.map +1 -0
- package/dist/health-bridge/health-connectors.js +1018 -0
- package/dist/health-bridge/health-connectors.js.map +1 -0
- package/dist/health-bridge/health-oauth.d.ts +62 -0
- package/dist/health-bridge/health-oauth.d.ts.map +1 -0
- package/dist/health-bridge/health-oauth.js +432 -0
- package/dist/health-bridge/health-oauth.js.map +1 -0
- package/dist/health-bridge/health-provider-registry.d.ts +89 -0
- package/dist/health-bridge/health-provider-registry.d.ts.map +1 -0
- package/dist/health-bridge/health-provider-registry.js +141 -0
- package/dist/health-bridge/health-provider-registry.js.map +1 -0
- package/dist/health-bridge/health-records.d.ts +14 -0
- package/dist/health-bridge/health-records.d.ts.map +1 -0
- package/dist/health-bridge/health-records.js +45 -0
- package/dist/health-bridge/health-records.js.map +1 -0
- package/dist/health-bridge/index.d.ts +22 -0
- package/dist/health-bridge/index.d.ts.map +1 -0
- package/dist/health-bridge/index.js +7 -0
- package/dist/health-bridge/index.js.map +1 -0
- package/dist/health-bridge/service-normalize-health.d.ts +3 -0
- package/dist/health-bridge/service-normalize-health.d.ts.map +1 -0
- package/dist/health-bridge/service-normalize-health.js +96 -0
- package/dist/health-bridge/service-normalize-health.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/screen-time/index.d.ts +23 -0
- package/dist/screen-time/index.d.ts.map +1 -0
- package/dist/screen-time/index.js +1 -0
- package/dist/screen-time/index.js.map +1 -0
- package/dist/sleep/awake-probability.d.ts +11 -0
- package/dist/sleep/awake-probability.d.ts.map +1 -0
- package/dist/sleep/awake-probability.js +163 -0
- package/dist/sleep/awake-probability.js.map +1 -0
- package/dist/sleep/circadian-rules.d.ts +45 -0
- package/dist/sleep/circadian-rules.d.ts.map +1 -0
- package/dist/sleep/circadian-rules.js +258 -0
- package/dist/sleep/circadian-rules.js.map +1 -0
- package/dist/sleep/index.d.ts +21 -0
- package/dist/sleep/index.d.ts.map +1 -0
- package/dist/sleep/index.js +11 -0
- package/dist/sleep/index.js.map +1 -0
- package/dist/sleep/sleep-cycle-dispatch.d.ts +75 -0
- package/dist/sleep/sleep-cycle-dispatch.d.ts.map +1 -0
- package/dist/sleep/sleep-cycle-dispatch.js +102 -0
- package/dist/sleep/sleep-cycle-dispatch.js.map +1 -0
- package/dist/sleep/sleep-cycle.d.ts +38 -0
- package/dist/sleep/sleep-cycle.d.ts.map +1 -0
- package/dist/sleep/sleep-cycle.js +418 -0
- package/dist/sleep/sleep-cycle.js.map +1 -0
- package/dist/sleep/sleep-episode-store.d.ts +25 -0
- package/dist/sleep/sleep-episode-store.d.ts.map +1 -0
- package/dist/sleep/sleep-episode-store.js +69 -0
- package/dist/sleep/sleep-episode-store.js.map +1 -0
- package/dist/sleep/sleep-episode-types.d.ts +38 -0
- package/dist/sleep/sleep-episode-types.d.ts.map +1 -0
- package/dist/sleep/sleep-episode-types.js +14 -0
- package/dist/sleep/sleep-episode-types.js.map +1 -0
- package/dist/sleep/sleep-recap.d.ts +19 -0
- package/dist/sleep/sleep-recap.d.ts.map +1 -0
- package/dist/sleep/sleep-recap.js +1 -0
- package/dist/sleep/sleep-recap.js.map +1 -0
- package/dist/sleep/sleep-regularity.d.ts +19 -0
- package/dist/sleep/sleep-regularity.d.ts.map +1 -0
- package/dist/sleep/sleep-regularity.js +242 -0
- package/dist/sleep/sleep-regularity.js.map +1 -0
- package/dist/sleep/sleep-wake-events.d.ts +58 -0
- package/dist/sleep/sleep-wake-events.d.ts.map +1 -0
- package/dist/sleep/sleep-wake-events.js +135 -0
- package/dist/sleep/sleep-wake-events.js.map +1 -0
- package/dist/sleep/source-reliability.d.ts +38 -0
- package/dist/sleep/source-reliability.d.ts.map +1 -0
- package/dist/sleep/source-reliability.js +62 -0
- package/dist/sleep/source-reliability.js.map +1 -0
- package/dist/util/index.d.ts +10 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +3 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/normalize.d.ts +22 -0
- package/dist/util/normalize.d.ts.map +1 -0
- package/dist/util/normalize.js +62 -0
- package/dist/util/normalize.js.map +1 -0
- package/dist/util/time-util.d.ts +10 -0
- package/dist/util/time-util.d.ts.map +1 -0
- package/dist/util/time-util.js +14 -0
- package/dist/util/time-util.js.map +1 -0
- package/dist/util/time.d.ts +17 -0
- package/dist/util/time.d.ts.map +1 -0
- package/dist/util/time.js +152 -0
- package/dist/util/time.js.map +1 -0
- package/dist/util/token-encryption.d.ts +42 -0
- package/dist/util/token-encryption.d.ts.map +1 -0
- package/dist/util/token-encryption.js +96 -0
- package/dist/util/token-encryption.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/health-bridge/health-oauth.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { resolveOAuthDir } from \"@elizaos/core\";\nimport type {\n LifeOpsConnectorMode,\n LifeOpsConnectorSide,\n LifeOpsHealthConnectorCapability,\n LifeOpsHealthConnectorProvider,\n StartLifeOpsHealthConnectorResponse,\n} from \"../contracts/health.js\";\nimport {\n decryptTokenEnvelope,\n encryptTokenPayload,\n isEncryptedTokenEnvelope,\n resolveTokenEncryptionKey,\n} from \"../util/token-encryption.js\";\nimport {\n type HealthProviderSpec,\n requireHealthProviderSpec,\n} from \"./health-provider-registry.js\";\n\nconst HEALTH_OAUTH_SESSION_TTL_MS = 10 * 60 * 1000;\nconst ACCESS_TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\nconst pendingHealthOAuthSessions = new Map<string, PendingHealthOAuthSession>();\n\nexport class HealthOAuthError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n ) {\n super(message);\n this.name = \"HealthOAuthError\";\n }\n}\n\nexport interface StoredHealthConnectorToken {\n provider: LifeOpsHealthConnectorProvider;\n agentId: string;\n side: LifeOpsConnectorSide;\n mode: LifeOpsConnectorMode;\n clientId: string;\n clientSecret: string | null;\n redirectUri: string;\n accessToken: string;\n refreshToken: string | null;\n tokenType: string;\n grantedScopes: string[];\n expiresAt: number | null;\n identity: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HealthConnectorCallbackResult {\n agentId: string;\n provider: LifeOpsHealthConnectorProvider;\n side: LifeOpsConnectorSide;\n mode: LifeOpsConnectorMode;\n tokenRef: string;\n identity: Record<string, unknown>;\n grantedCapabilities: LifeOpsHealthConnectorCapability[];\n grantedScopes: string[];\n expiresAt: string | null;\n hasRefreshToken: boolean;\n}\n\nexport interface ResolvedHealthOAuthConfig {\n provider: LifeOpsHealthConnectorProvider;\n mode: LifeOpsConnectorMode;\n defaultMode: LifeOpsConnectorMode;\n availableModes: LifeOpsConnectorMode[];\n configured: boolean;\n clientId: string | null;\n clientSecret: string | null;\n redirectUri: string;\n}\n\ninterface PendingHealthOAuthSession {\n state: string;\n provider: LifeOpsHealthConnectorProvider;\n agentId: string;\n side: LifeOpsConnectorSide;\n mode: LifeOpsConnectorMode;\n clientId: string;\n clientSecret: string | null;\n redirectUri: string;\n codeVerifier: string | null;\n requestedCapabilities: LifeOpsHealthConnectorCapability[];\n createdAt: number;\n}\n\ninterface HealthTokenResponse {\n access_token?: string;\n refresh_token?: string;\n token_type?: string;\n expires_in?: number;\n expires_at?: number;\n scope?: string;\n athlete?: Record<string, unknown>;\n user_id?: string;\n userid?: string | number;\n}\n\ntype HealthTokenApiResponse =\n | HealthTokenResponse\n | { status?: number; body?: HealthTokenResponse; error?: string };\n\nfunction isWrappedHealthTokenResponse(\n value: HealthTokenApiResponse,\n): value is { status?: number; body?: HealthTokenResponse; error?: string } {\n return \"status\" in value || \"body\" in value || \"error\" in value;\n}\n\nfunction unwrapHealthTokenResponse(\n value: HealthTokenApiResponse,\n provider: LifeOpsHealthConnectorProvider,\n): HealthTokenResponse {\n if (isWrappedHealthTokenResponse(value)) {\n if (value.status !== undefined && value.status !== 0) {\n throw new HealthOAuthError(502, `Token exchange failed for ${provider}`);\n }\n if (value.body) {\n return value.body;\n }\n }\n return value as HealthTokenResponse;\n}\n\nfunction providerSpec(\n provider: LifeOpsHealthConnectorProvider,\n): HealthProviderSpec {\n // The dispatcher iterates the health-provider registry instead of switching\n // on provider name; URLs / scopes / token-request style come from the\n // registered ConnectorContribution's spec, never from a hardcoded table.\n return requireHealthProviderSpec(provider);\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n const normalized = hostname.trim().toLowerCase();\n return (\n normalized === \"localhost\" ||\n normalized === \"127.0.0.1\" ||\n normalized === \"::1\" ||\n normalized === \"[::1]\"\n );\n}\n\nfunction readEnv(\n env: NodeJS.ProcessEnv,\n provider: LifeOpsHealthConnectorProvider,\n suffix: \"CLIENT_ID\" | \"CLIENT_SECRET\" | \"PUBLIC_BASE_URL\",\n): string | null {\n const spec = providerSpec(provider);\n const key = `ELIZA_${spec.envPrefix}_${suffix}`;\n const value = env[key]?.trim();\n return value ?? null;\n}\n\nfunction normalizeBaseUrl(value: string): string {\n return value.endsWith(\"/\") ? value.slice(0, -1) : value;\n}\n\nexport function healthConnectorCapabilities(\n provider: LifeOpsHealthConnectorProvider,\n): LifeOpsHealthConnectorCapability[] {\n return [...providerSpec(provider).capabilities];\n}\n\nexport function healthConnectorScopes(\n provider: LifeOpsHealthConnectorProvider,\n): string[] {\n return [...providerSpec(provider).oauth.defaultScopes];\n}\n\nexport function healthScopesToCapabilities(\n provider: LifeOpsHealthConnectorProvider,\n scopes: readonly string[],\n): LifeOpsHealthConnectorCapability[] {\n const set = new Set(scopes);\n if (provider === \"strava\") {\n return set.has(\"activity:read\") || set.has(\"activity:read_all\")\n ? [\"health.activity.read\", \"health.workouts.read\"]\n : [];\n }\n if (provider === \"oura\") {\n const capabilities: LifeOpsHealthConnectorCapability[] = [];\n if (set.has(\"daily\")) {\n capabilities.push(\n \"health.activity.read\",\n \"health.sleep.read\",\n \"health.readiness.read\",\n );\n }\n if (set.has(\"workout\")) capabilities.push(\"health.workouts.read\");\n if (set.has(\"heartrate\") || set.has(\"spo2\"))\n capabilities.push(\"health.vitals.read\");\n if (set.has(\"personal\")) capabilities.push(\"health.body.read\");\n return [...new Set(capabilities)];\n }\n if (provider === \"fitbit\") {\n const capabilities: LifeOpsHealthConnectorCapability[] = [];\n if (set.has(\"activity\")) {\n capabilities.push(\"health.activity.read\", \"health.workouts.read\");\n }\n if (set.has(\"sleep\")) capabilities.push(\"health.sleep.read\");\n if (set.has(\"heartrate\")) capabilities.push(\"health.vitals.read\");\n if (set.has(\"weight\")) capabilities.push(\"health.body.read\");\n return [...new Set(capabilities)];\n }\n const capabilities: LifeOpsHealthConnectorCapability[] = [];\n if (set.has(\"user.activity\")) {\n capabilities.push(\"health.activity.read\", \"health.sleep.read\");\n }\n if (set.has(\"user.sleepevents\")) capabilities.push(\"health.sleep.read\");\n if (set.has(\"user.metrics\")) {\n capabilities.push(\"health.body.read\", \"health.vitals.read\");\n }\n return [...new Set(capabilities)];\n}\n\nexport function resolveHealthOAuthConfig(\n provider: LifeOpsHealthConnectorProvider,\n requestUrl: URL,\n requestedMode?: LifeOpsConnectorMode,\n env: NodeJS.ProcessEnv = process.env,\n): ResolvedHealthOAuthConfig {\n const localClientId = readEnv(env, provider, \"CLIENT_ID\");\n const localClientSecret = readEnv(env, provider, \"CLIENT_SECRET\");\n const publicBaseUrl = readEnv(env, provider, \"PUBLIC_BASE_URL\");\n const availableModes: LifeOpsConnectorMode[] = [];\n if (localClientId && localClientSecret) {\n availableModes.push(\"local\");\n }\n if (localClientId && localClientSecret && publicBaseUrl) {\n availableModes.push(\"remote\");\n }\n const defaultMode =\n isLoopbackHostname(requestUrl.hostname) && availableModes.includes(\"local\")\n ? \"local\"\n : (availableModes[0] ??\n (isLoopbackHostname(requestUrl.hostname) ? \"local\" : \"remote\"));\n const mode = requestedMode ?? defaultMode;\n const port =\n requestUrl.port || (requestUrl.protocol === \"https:\" ? \"443\" : \"80\");\n const redirectUri =\n mode === \"remote\" && publicBaseUrl\n ? `${normalizeBaseUrl(publicBaseUrl)}/api/lifeops/connectors/health/${provider}/callback`\n : `http://127.0.0.1:${port}/api/lifeops/connectors/health/${provider}/callback`;\n\n return {\n provider,\n mode,\n defaultMode,\n availableModes,\n configured: Boolean(localClientId && localClientSecret),\n clientId: localClientId,\n clientSecret: localClientSecret,\n redirectUri,\n };\n}\n\nfunction requireHealthOAuthConfig(\n config: ResolvedHealthOAuthConfig,\n requestUrl: URL,\n): asserts config is ResolvedHealthOAuthConfig & {\n clientId: string;\n clientSecret: string;\n} {\n if (config.mode === \"local\" && !isLoopbackHostname(requestUrl.hostname)) {\n throw new HealthOAuthError(\n 400,\n \"Local health OAuth requires the API to be addressed over a loopback host.\",\n );\n }\n if (!config.configured || !config.clientId || !config.clientSecret) {\n throw new HealthOAuthError(\n 503,\n `${config.provider} OAuth ${config.mode} mode is not configured.`,\n );\n }\n}\n\nfunction base64Url(buffer: Buffer): string {\n return buffer\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nfunction sha256Base64Url(value: string): string {\n return base64Url(crypto.createHash(\"sha256\").update(value).digest());\n}\n\nfunction parseGrantedScopes(\n provider: LifeOpsHealthConnectorProvider,\n value: unknown,\n): string[] {\n if (typeof value !== \"string\") {\n return healthConnectorScopes(provider);\n }\n return value\n .split(/[,\\s]+/)\n .map((scope) => scope.trim())\n .filter(Boolean);\n}\n\nfunction tokenExpiresAt(response: HealthTokenResponse): number | null {\n if (typeof response.expires_at === \"number\") {\n return response.expires_at * 1_000;\n }\n if (typeof response.expires_in === \"number\") {\n return Date.now() + response.expires_in * 1_000;\n }\n return null;\n}\n\nfunction tokenRefFor(args: {\n provider: LifeOpsHealthConnectorProvider;\n agentId: string;\n side: LifeOpsConnectorSide;\n mode: LifeOpsConnectorMode;\n}): string {\n return path.join(args.agentId, args.side, args.mode, `${args.provider}.json`);\n}\n\nfunction tokenStorageRoot(env: NodeJS.ProcessEnv = process.env): string {\n return path.join(resolveOAuthDir(env), \"lifeops\", \"health\");\n}\n\nfunction tokenPath(tokenRef: string, env: NodeJS.ProcessEnv): string {\n return path.join(tokenStorageRoot(env), tokenRef);\n}\n\nfunction writeStoredHealthToken(\n tokenRef: string,\n token: StoredHealthConnectorToken,\n env: NodeJS.ProcessEnv = process.env,\n): void {\n const filePath = tokenPath(tokenRef, env);\n fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });\n const key = resolveTokenEncryptionKey(tokenStorageRoot(env), env);\n const payload = JSON.stringify(token, null, 2);\n const encoded = JSON.stringify(encryptTokenPayload(payload, key), null, 2);\n fs.writeFileSync(filePath, encoded, { mode: 0o600 });\n}\n\nexport function readStoredHealthToken(\n tokenRef: string | null | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): StoredHealthConnectorToken | null {\n if (!tokenRef) {\n return null;\n }\n const filePath = tokenPath(tokenRef, env);\n if (!fs.existsSync(filePath)) {\n return null;\n }\n const raw = fs.readFileSync(filePath, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (!isEncryptedTokenEnvelope(parsed)) {\n throw new Error(\n \"Stored health token is not encrypted. Re-link the account.\",\n );\n }\n const text = decryptTokenEnvelope(\n parsed,\n resolveTokenEncryptionKey(tokenStorageRoot(env), env),\n );\n return JSON.parse(text) as StoredHealthConnectorToken;\n}\n\nexport function deleteStoredHealthToken(\n tokenRef: string | null | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): void {\n if (!tokenRef) {\n return;\n }\n fs.rmSync(tokenPath(tokenRef, env), { force: true });\n}\n\nasync function exchangeToken(\n session: PendingHealthOAuthSession,\n code: string,\n): Promise<HealthTokenResponse> {\n const oauth = providerSpec(session.provider).oauth;\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: session.redirectUri,\n });\n if (oauth.tokenRequestStyle === \"withings\") {\n body.set(\"action\", \"requesttoken\");\n }\n if (oauth.tokenRequestStyle !== \"basic\") {\n body.set(\"client_id\", session.clientId);\n if (session.clientSecret) body.set(\"client_secret\", session.clientSecret);\n }\n if (session.codeVerifier) {\n body.set(\"code_verifier\", session.codeVerifier);\n }\n\n const response = await fetch(oauth.tokenUrl, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n ...(oauth.tokenRequestStyle === \"basic\" && session.clientSecret\n ? {\n Authorization: `Basic ${Buffer.from(\n `${session.clientId}:${session.clientSecret}`,\n ).toString(\"base64\")}`,\n }\n : {}),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n });\n const json = (await response.json()) as HealthTokenApiResponse;\n if (!response.ok) {\n throw new HealthOAuthError(\n response.status,\n `Token exchange failed for ${session.provider}`,\n );\n }\n return unwrapHealthTokenResponse(json, session.provider);\n}\n\nexport async function refreshStoredHealthToken(\n tokenRef: string | null | undefined,\n): Promise<StoredHealthConnectorToken | null> {\n const token = readStoredHealthToken(tokenRef);\n if (!token?.refreshToken) {\n return token;\n }\n if (\n token.expiresAt !== null &&\n token.expiresAt - ACCESS_TOKEN_REFRESH_BUFFER_MS > Date.now()\n ) {\n return token;\n }\n const oauth = providerSpec(token.provider).oauth;\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: token.refreshToken,\n });\n if (oauth.tokenRequestStyle === \"withings\") {\n body.set(\"action\", \"requesttoken\");\n body.set(\"client_id\", token.clientId);\n if (token.clientSecret) body.set(\"client_secret\", token.clientSecret);\n } else if (oauth.tokenRequestStyle !== \"basic\") {\n body.set(\"client_id\", token.clientId);\n if (token.clientSecret) body.set(\"client_secret\", token.clientSecret);\n }\n const response = await fetch(oauth.tokenUrl, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n ...(oauth.tokenRequestStyle === \"basic\" && token.clientSecret\n ? {\n Authorization: `Basic ${Buffer.from(\n `${token.clientId}:${token.clientSecret}`,\n ).toString(\"base64\")}`,\n }\n : {}),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new HealthOAuthError(\n response.status,\n `${token.provider} token refresh failed`,\n );\n }\n const json = (await response.json()) as HealthTokenApiResponse;\n const payload = unwrapHealthTokenResponse(json, token.provider);\n if (!payload.access_token) {\n throw new HealthOAuthError(502, `${token.provider} token refresh failed`);\n }\n const next: StoredHealthConnectorToken = {\n ...token,\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token ?? token.refreshToken,\n tokenType: payload.token_type ?? token.tokenType,\n grantedScopes: parseGrantedScopes(token.provider, payload.scope),\n expiresAt: tokenExpiresAt(payload),\n updatedAt: new Date().toISOString(),\n };\n writeStoredHealthToken(tokenRef ?? tokenRefFor(token), next);\n return next;\n}\n\nexport function startHealthConnectorOAuth(args: {\n provider: LifeOpsHealthConnectorProvider;\n agentId: string;\n side: LifeOpsConnectorSide;\n mode?: LifeOpsConnectorMode;\n requestUrl: URL;\n redirectUrl?: string;\n capabilities?: LifeOpsHealthConnectorCapability[];\n}): StartLifeOpsHealthConnectorResponse {\n const config = resolveHealthOAuthConfig(\n args.provider,\n args.requestUrl,\n args.mode,\n );\n requireHealthOAuthConfig(config, args.requestUrl);\n const oauth = providerSpec(args.provider).oauth;\n const state = crypto.randomBytes(24).toString(\"hex\");\n const codeVerifier = oauth.usePkce ? base64Url(crypto.randomBytes(32)) : null;\n const scopes = healthConnectorScopes(args.provider);\n pendingHealthOAuthSessions.set(state, {\n state,\n provider: args.provider,\n agentId: args.agentId,\n side: args.side,\n mode: config.mode,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirectUri: config.redirectUri,\n codeVerifier,\n requestedCapabilities:\n args.capabilities && args.capabilities.length > 0\n ? [...new Set(args.capabilities)]\n : healthConnectorCapabilities(args.provider),\n createdAt: Date.now(),\n });\n\n const authUrl = new URL(oauth.authorizeUrl);\n authUrl.searchParams.set(\"client_id\", config.clientId);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"redirect_uri\", config.redirectUri);\n authUrl.searchParams.set(\n \"scope\",\n scopes.join(oauth.scopeSeparator === \"comma\" ? \",\" : \" \"),\n );\n authUrl.searchParams.set(\"state\", state);\n if (oauth.extraAuthorizeParams) {\n for (const [key, value] of Object.entries(oauth.extraAuthorizeParams)) {\n authUrl.searchParams.set(key, value);\n }\n }\n if (codeVerifier) {\n authUrl.searchParams.set(\"code_challenge\", sha256Base64Url(codeVerifier));\n authUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n }\n\n return {\n provider: args.provider,\n side: args.side,\n mode: config.mode,\n requestedCapabilities:\n args.capabilities && args.capabilities.length > 0\n ? [...new Set(args.capabilities)]\n : healthConnectorCapabilities(args.provider),\n redirectUri: config.redirectUri,\n authUrl: authUrl.toString(),\n };\n}\n\nexport async function completeHealthConnectorOAuth(\n callbackUrl: URL,\n): Promise<HealthConnectorCallbackResult> {\n const state = callbackUrl.searchParams.get(\"state\") ?? \"\";\n const session = pendingHealthOAuthSessions.get(state);\n if (!session) {\n throw new HealthOAuthError(400, \"Unknown or expired health OAuth session.\");\n }\n pendingHealthOAuthSessions.delete(state);\n if (Date.now() - session.createdAt > HEALTH_OAUTH_SESSION_TTL_MS) {\n throw new HealthOAuthError(400, \"Health OAuth session expired.\");\n }\n const error = callbackUrl.searchParams.get(\"error\");\n if (error) {\n throw new HealthOAuthError(\n 400,\n `${session.provider} authorization failed: ${error}`,\n );\n }\n const code = callbackUrl.searchParams.get(\"code\");\n if (!code) {\n throw new HealthOAuthError(400, \"Missing health OAuth authorization code.\");\n }\n\n const payload = await exchangeToken(session, code);\n if (!payload.access_token) {\n throw new HealthOAuthError(\n 502,\n `${session.provider} token response missing access token.`,\n );\n }\n const scopes = parseGrantedScopes(session.provider, payload.scope);\n const identity =\n session.provider === \"strava\" && payload.athlete\n ? payload.athlete\n : {\n userId:\n payload.user_id ??\n payload.userid ??\n callbackUrl.searchParams.get(\"userid\") ??\n null,\n };\n const nowIso = new Date().toISOString();\n const tokenRef = tokenRefFor(session);\n const token: StoredHealthConnectorToken = {\n provider: session.provider,\n agentId: session.agentId,\n side: session.side,\n mode: session.mode,\n clientId: session.clientId,\n clientSecret: session.clientSecret,\n redirectUri: session.redirectUri,\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token ?? null,\n tokenType: payload.token_type ?? \"Bearer\",\n grantedScopes: scopes,\n expiresAt: tokenExpiresAt(payload),\n identity,\n createdAt: nowIso,\n updatedAt: nowIso,\n };\n writeStoredHealthToken(tokenRef, token);\n return {\n agentId: session.agentId,\n provider: session.provider,\n side: session.side,\n mode: session.mode,\n tokenRef,\n identity,\n grantedCapabilities: healthScopesToCapabilities(session.provider, scopes),\n grantedScopes: scopes,\n expiresAt: token.expiresAt ? new Date(token.expiresAt).toISOString() : null,\n hasRefreshToken: Boolean(token.refreshToken),\n };\n}\n"],"mappings":"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","names":["capabilities"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health-connector provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Audit C top-3 (medium severity) — `rigidity-hunt-audit.md` §3:
|
|
5
|
+
* Strava / Fitbit / Withings / Oura URLs used to live in per-provider
|
|
6
|
+
* `switch (provider) { case "strava": ... }` arms inside `health-oauth.ts`
|
|
7
|
+
* and `health-connectors.ts`. Adding a fifth provider required editing both
|
|
8
|
+
* files. This registry centralises every per-provider URL + token-exchange
|
|
9
|
+
* shape so the dispatchers iterate a single typed table.
|
|
10
|
+
*
|
|
11
|
+
* The OAuth + base URLs are also surfaced on the connector contributions
|
|
12
|
+
* registered through `connectors/index.ts` (`ConnectorContribution.oauth`,
|
|
13
|
+
* `ConnectorContribution.apiBaseUrl`). External plugins that contribute
|
|
14
|
+
* additional health connectors register their spec via
|
|
15
|
+
* `setHealthProviderSpec(...)` instead of patching this module.
|
|
16
|
+
*/
|
|
17
|
+
import type { LifeOpsHealthConnectorCapability } from "../contracts/health.js";
|
|
18
|
+
/**
|
|
19
|
+
* OAuth surface for one health provider. Every URL / scope-shape the OAuth
|
|
20
|
+
* dispatcher needs lives here; `health-oauth.ts` does not hardcode any
|
|
21
|
+
* provider-specific URLs.
|
|
22
|
+
*/
|
|
23
|
+
export interface HealthProviderOAuthSpec {
|
|
24
|
+
/** OAuth 2.0 authorize endpoint. URL provided by the connector contribution; the dispatcher does not hardcode. */
|
|
25
|
+
readonly authorizeUrl: string;
|
|
26
|
+
/** OAuth 2.0 token endpoint. URL provided by the connector contribution; the dispatcher does not hardcode. */
|
|
27
|
+
readonly tokenUrl: string;
|
|
28
|
+
/** Optional OAuth 2.0 token-revocation endpoint. */
|
|
29
|
+
readonly revokeUrl: string | null;
|
|
30
|
+
/** Default scope set requested at authorize time. */
|
|
31
|
+
readonly defaultScopes: readonly string[];
|
|
32
|
+
/** Scope-list separator on the authorize URL: `space` or `comma`. */
|
|
33
|
+
readonly scopeSeparator: "space" | "comma";
|
|
34
|
+
/** Whether to attach PKCE (S256) on the authorize + token requests. */
|
|
35
|
+
readonly usePkce: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Token-request body/auth shape:
|
|
38
|
+
* - `form` — credentials in the form body (Strava, Withings).
|
|
39
|
+
* - `basic` — credentials in the `Authorization: Basic` header (Fitbit,
|
|
40
|
+
* Oura).
|
|
41
|
+
* - `withings` — `form` plus the Withings-specific `action=requesttoken`
|
|
42
|
+
* marker.
|
|
43
|
+
*/
|
|
44
|
+
readonly tokenRequestStyle: "form" | "basic" | "withings";
|
|
45
|
+
/**
|
|
46
|
+
* Provider-specific extra query parameters appended to the authorize URL
|
|
47
|
+
* (e.g. Strava's `approval_prompt=auto`). Externalising these keeps every
|
|
48
|
+
* provider quirk inside the registry and out of dispatcher branches.
|
|
49
|
+
*/
|
|
50
|
+
readonly extraAuthorizeParams?: Readonly<Record<string, string>>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Full provider record. Combines OAuth, API base URL, and capability mapping
|
|
54
|
+
* so the dispatchers can resolve every per-provider value in one lookup.
|
|
55
|
+
*/
|
|
56
|
+
export interface HealthProviderSpec {
|
|
57
|
+
readonly provider: string;
|
|
58
|
+
/** Env-var prefix for `ELIZA_<PREFIX>_CLIENT_ID` etc. */
|
|
59
|
+
readonly envPrefix: string;
|
|
60
|
+
readonly oauth: HealthProviderOAuthSpec;
|
|
61
|
+
/** Base URL for authenticated API requests. URL provided by the connector contribution; the dispatcher does not hardcode. */
|
|
62
|
+
readonly apiBaseUrl: string;
|
|
63
|
+
/** Capabilities advertised by the connector when fully authorised. */
|
|
64
|
+
readonly capabilities: readonly LifeOpsHealthConnectorCapability[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Thrown when a caller asks for OAuth-related operations on a provider whose
|
|
68
|
+
* registered spec does not include an `oauth` block. Surfaces loudly rather
|
|
69
|
+
* than falling back to a default endpoint.
|
|
70
|
+
*/
|
|
71
|
+
export declare class MissingOauthConfigError extends Error {
|
|
72
|
+
readonly provider: string;
|
|
73
|
+
constructor(provider: string);
|
|
74
|
+
}
|
|
75
|
+
export declare function listHealthProviderSpecs(): HealthProviderSpec[];
|
|
76
|
+
export declare function getHealthProviderSpec(provider: string): HealthProviderSpec | null;
|
|
77
|
+
/**
|
|
78
|
+
* Look up a provider spec, throwing if it is not registered. The dispatcher
|
|
79
|
+
* uses this in places where a missing entry indicates a programming error.
|
|
80
|
+
*/
|
|
81
|
+
export declare function requireHealthProviderSpec(provider: string): HealthProviderSpec;
|
|
82
|
+
/**
|
|
83
|
+
* Register or replace a provider spec. Used by tests and out-of-tree plugins
|
|
84
|
+
* to add a fifth provider without editing this module.
|
|
85
|
+
*/
|
|
86
|
+
export declare function setHealthProviderSpec(spec: HealthProviderSpec): void;
|
|
87
|
+
export declare function deleteHealthProviderSpec(provider: string): void;
|
|
88
|
+
export declare function resetHealthProviderRegistry(): void;
|
|
89
|
+
//# sourceMappingURL=health-provider-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-provider-registry.d.ts","sourceRoot":"","sources":["../../src/health-bridge/health-provider-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EACV,gCAAgC,EAEjC,MAAM,wBAAwB,CAAC;AAEhC;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,kHAAkH;IAClH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,8GAA8G;IAC9G,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qDAAqD;IACrD,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,qEAAqE;IACrE,QAAQ,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,CAAC;IAC3C,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClE;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,uBAAuB,CAAC;IACxC,6HAA6H;IAC7H,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,YAAY,EAAE,SAAS,gCAAgC,EAAE,CAAC;CACpE;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAgB,QAAQ,EAAE,MAAM,CAAC;gBACrB,QAAQ,EAAE,MAAM;CAO7B;AAuGD,wBAAgB,uBAAuB,IAAI,kBAAkB,EAAE,CAE9D;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GACf,kBAAkB,GAAG,IAAI,CAE3B;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,GACf,kBAAkB,CAMpB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAEpE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
class MissingOauthConfigError extends Error {
|
|
2
|
+
provider;
|
|
3
|
+
constructor(provider) {
|
|
4
|
+
super(
|
|
5
|
+
`Health connector '${provider}' has no registered OAuth config; refusing to proceed.`
|
|
6
|
+
);
|
|
7
|
+
this.name = "MissingOauthConfigError";
|
|
8
|
+
this.provider = provider;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_HEALTH_PROVIDER_SPECS = {
|
|
12
|
+
strava: {
|
|
13
|
+
provider: "strava",
|
|
14
|
+
envPrefix: "STRAVA",
|
|
15
|
+
oauth: {
|
|
16
|
+
authorizeUrl: "https://www.strava.com/oauth/authorize",
|
|
17
|
+
tokenUrl: "https://www.strava.com/oauth/token",
|
|
18
|
+
revokeUrl: "https://www.strava.com/oauth/deauthorize",
|
|
19
|
+
defaultScopes: ["read", "profile:read_all", "activity:read_all"],
|
|
20
|
+
scopeSeparator: "comma",
|
|
21
|
+
usePkce: false,
|
|
22
|
+
tokenRequestStyle: "form",
|
|
23
|
+
extraAuthorizeParams: { approval_prompt: "auto" }
|
|
24
|
+
},
|
|
25
|
+
apiBaseUrl: "https://www.strava.com/api/v3",
|
|
26
|
+
capabilities: ["health.activity.read", "health.workouts.read"]
|
|
27
|
+
},
|
|
28
|
+
fitbit: {
|
|
29
|
+
provider: "fitbit",
|
|
30
|
+
envPrefix: "FITBIT",
|
|
31
|
+
oauth: {
|
|
32
|
+
authorizeUrl: "https://www.fitbit.com/oauth2/authorize",
|
|
33
|
+
tokenUrl: "https://api.fitbit.com/oauth2/token",
|
|
34
|
+
revokeUrl: "https://api.fitbit.com/oauth2/revoke",
|
|
35
|
+
defaultScopes: ["profile", "activity", "heartrate", "sleep", "weight"],
|
|
36
|
+
scopeSeparator: "space",
|
|
37
|
+
usePkce: true,
|
|
38
|
+
tokenRequestStyle: "basic"
|
|
39
|
+
},
|
|
40
|
+
apiBaseUrl: "https://api.fitbit.com",
|
|
41
|
+
capabilities: [
|
|
42
|
+
"health.activity.read",
|
|
43
|
+
"health.workouts.read",
|
|
44
|
+
"health.sleep.read",
|
|
45
|
+
"health.body.read",
|
|
46
|
+
"health.vitals.read"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
withings: {
|
|
50
|
+
provider: "withings",
|
|
51
|
+
envPrefix: "WITHINGS",
|
|
52
|
+
oauth: {
|
|
53
|
+
authorizeUrl: "https://account.withings.com/oauth2_user/authorize2",
|
|
54
|
+
tokenUrl: "https://wbsapi.withings.net/v2/oauth2",
|
|
55
|
+
revokeUrl: null,
|
|
56
|
+
defaultScopes: [
|
|
57
|
+
"user.info",
|
|
58
|
+
"user.metrics",
|
|
59
|
+
"user.activity",
|
|
60
|
+
"user.sleepevents"
|
|
61
|
+
],
|
|
62
|
+
scopeSeparator: "comma",
|
|
63
|
+
usePkce: false,
|
|
64
|
+
tokenRequestStyle: "withings"
|
|
65
|
+
},
|
|
66
|
+
apiBaseUrl: "https://wbsapi.withings.net",
|
|
67
|
+
capabilities: [
|
|
68
|
+
"health.activity.read",
|
|
69
|
+
"health.sleep.read",
|
|
70
|
+
"health.body.read",
|
|
71
|
+
"health.vitals.read"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
oura: {
|
|
75
|
+
provider: "oura",
|
|
76
|
+
envPrefix: "OURA",
|
|
77
|
+
oauth: {
|
|
78
|
+
authorizeUrl: "https://cloud.ouraring.com/oauth/authorize",
|
|
79
|
+
tokenUrl: "https://api.ouraring.com/oauth/token",
|
|
80
|
+
revokeUrl: "https://api.ouraring.com/oauth/revoke",
|
|
81
|
+
defaultScopes: [
|
|
82
|
+
"email",
|
|
83
|
+
"personal",
|
|
84
|
+
"daily",
|
|
85
|
+
"heartrate",
|
|
86
|
+
"workout",
|
|
87
|
+
"spo2"
|
|
88
|
+
],
|
|
89
|
+
scopeSeparator: "space",
|
|
90
|
+
usePkce: false,
|
|
91
|
+
tokenRequestStyle: "basic"
|
|
92
|
+
},
|
|
93
|
+
apiBaseUrl: "https://api.ouraring.com",
|
|
94
|
+
capabilities: [
|
|
95
|
+
"health.activity.read",
|
|
96
|
+
"health.workouts.read",
|
|
97
|
+
"health.sleep.read",
|
|
98
|
+
"health.readiness.read",
|
|
99
|
+
"health.body.read",
|
|
100
|
+
"health.vitals.read"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const healthProviderSpecs = new Map(
|
|
105
|
+
Object.entries(DEFAULT_HEALTH_PROVIDER_SPECS)
|
|
106
|
+
);
|
|
107
|
+
function listHealthProviderSpecs() {
|
|
108
|
+
return Array.from(healthProviderSpecs.values());
|
|
109
|
+
}
|
|
110
|
+
function getHealthProviderSpec(provider) {
|
|
111
|
+
return healthProviderSpecs.get(provider) ?? null;
|
|
112
|
+
}
|
|
113
|
+
function requireHealthProviderSpec(provider) {
|
|
114
|
+
const spec = healthProviderSpecs.get(provider);
|
|
115
|
+
if (!spec) {
|
|
116
|
+
throw new MissingOauthConfigError(provider);
|
|
117
|
+
}
|
|
118
|
+
return spec;
|
|
119
|
+
}
|
|
120
|
+
function setHealthProviderSpec(spec) {
|
|
121
|
+
healthProviderSpecs.set(spec.provider, spec);
|
|
122
|
+
}
|
|
123
|
+
function deleteHealthProviderSpec(provider) {
|
|
124
|
+
healthProviderSpecs.delete(provider);
|
|
125
|
+
}
|
|
126
|
+
function resetHealthProviderRegistry() {
|
|
127
|
+
healthProviderSpecs.clear();
|
|
128
|
+
for (const [name, spec] of Object.entries(DEFAULT_HEALTH_PROVIDER_SPECS)) {
|
|
129
|
+
healthProviderSpecs.set(name, spec);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
MissingOauthConfigError,
|
|
134
|
+
deleteHealthProviderSpec,
|
|
135
|
+
getHealthProviderSpec,
|
|
136
|
+
listHealthProviderSpecs,
|
|
137
|
+
requireHealthProviderSpec,
|
|
138
|
+
resetHealthProviderRegistry,
|
|
139
|
+
setHealthProviderSpec
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=health-provider-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/health-bridge/health-provider-registry.ts"],"sourcesContent":["/**\n * Health-connector provider registry.\n *\n * Audit C top-3 (medium severity) — `rigidity-hunt-audit.md` §3:\n * Strava / Fitbit / Withings / Oura URLs used to live in per-provider\n * `switch (provider) { case \"strava\": ... }` arms inside `health-oauth.ts`\n * and `health-connectors.ts`. Adding a fifth provider required editing both\n * files. This registry centralises every per-provider URL + token-exchange\n * shape so the dispatchers iterate a single typed table.\n *\n * The OAuth + base URLs are also surfaced on the connector contributions\n * registered through `connectors/index.ts` (`ConnectorContribution.oauth`,\n * `ConnectorContribution.apiBaseUrl`). External plugins that contribute\n * additional health connectors register their spec via\n * `setHealthProviderSpec(...)` instead of patching this module.\n */\n\nimport type {\n LifeOpsHealthConnectorCapability,\n LifeOpsHealthConnectorProvider,\n} from \"../contracts/health.js\";\n\n/**\n * OAuth surface for one health provider. Every URL / scope-shape the OAuth\n * dispatcher needs lives here; `health-oauth.ts` does not hardcode any\n * provider-specific URLs.\n */\nexport interface HealthProviderOAuthSpec {\n /** OAuth 2.0 authorize endpoint. URL provided by the connector contribution; the dispatcher does not hardcode. */\n readonly authorizeUrl: string;\n /** OAuth 2.0 token endpoint. URL provided by the connector contribution; the dispatcher does not hardcode. */\n readonly tokenUrl: string;\n /** Optional OAuth 2.0 token-revocation endpoint. */\n readonly revokeUrl: string | null;\n /** Default scope set requested at authorize time. */\n readonly defaultScopes: readonly string[];\n /** Scope-list separator on the authorize URL: `space` or `comma`. */\n readonly scopeSeparator: \"space\" | \"comma\";\n /** Whether to attach PKCE (S256) on the authorize + token requests. */\n readonly usePkce: boolean;\n /**\n * Token-request body/auth shape:\n * - `form` — credentials in the form body (Strava, Withings).\n * - `basic` — credentials in the `Authorization: Basic` header (Fitbit,\n * Oura).\n * - `withings` — `form` plus the Withings-specific `action=requesttoken`\n * marker.\n */\n readonly tokenRequestStyle: \"form\" | \"basic\" | \"withings\";\n /**\n * Provider-specific extra query parameters appended to the authorize URL\n * (e.g. Strava's `approval_prompt=auto`). Externalising these keeps every\n * provider quirk inside the registry and out of dispatcher branches.\n */\n readonly extraAuthorizeParams?: Readonly<Record<string, string>>;\n}\n\n/**\n * Full provider record. Combines OAuth, API base URL, and capability mapping\n * so the dispatchers can resolve every per-provider value in one lookup.\n */\nexport interface HealthProviderSpec {\n readonly provider: string;\n /** Env-var prefix for `ELIZA_<PREFIX>_CLIENT_ID` etc. */\n readonly envPrefix: string;\n readonly oauth: HealthProviderOAuthSpec;\n /** Base URL for authenticated API requests. URL provided by the connector contribution; the dispatcher does not hardcode. */\n readonly apiBaseUrl: string;\n /** Capabilities advertised by the connector when fully authorised. */\n readonly capabilities: readonly LifeOpsHealthConnectorCapability[];\n}\n\n/**\n * Thrown when a caller asks for OAuth-related operations on a provider whose\n * registered spec does not include an `oauth` block. Surfaces loudly rather\n * than falling back to a default endpoint.\n */\nexport class MissingOauthConfigError extends Error {\n public readonly provider: string;\n constructor(provider: string) {\n super(\n `Health connector '${provider}' has no registered OAuth config; refusing to proceed.`,\n );\n this.name = \"MissingOauthConfigError\";\n this.provider = provider;\n }\n}\n\nconst DEFAULT_HEALTH_PROVIDER_SPECS: Record<\n LifeOpsHealthConnectorProvider,\n HealthProviderSpec\n> = {\n strava: {\n provider: \"strava\",\n envPrefix: \"STRAVA\",\n oauth: {\n authorizeUrl: \"https://www.strava.com/oauth/authorize\",\n tokenUrl: \"https://www.strava.com/oauth/token\",\n revokeUrl: \"https://www.strava.com/oauth/deauthorize\",\n defaultScopes: [\"read\", \"profile:read_all\", \"activity:read_all\"],\n scopeSeparator: \"comma\",\n usePkce: false,\n tokenRequestStyle: \"form\",\n extraAuthorizeParams: { approval_prompt: \"auto\" },\n },\n apiBaseUrl: \"https://www.strava.com/api/v3\",\n capabilities: [\"health.activity.read\", \"health.workouts.read\"],\n },\n fitbit: {\n provider: \"fitbit\",\n envPrefix: \"FITBIT\",\n oauth: {\n authorizeUrl: \"https://www.fitbit.com/oauth2/authorize\",\n tokenUrl: \"https://api.fitbit.com/oauth2/token\",\n revokeUrl: \"https://api.fitbit.com/oauth2/revoke\",\n defaultScopes: [\"profile\", \"activity\", \"heartrate\", \"sleep\", \"weight\"],\n scopeSeparator: \"space\",\n usePkce: true,\n tokenRequestStyle: \"basic\",\n },\n apiBaseUrl: \"https://api.fitbit.com\",\n capabilities: [\n \"health.activity.read\",\n \"health.workouts.read\",\n \"health.sleep.read\",\n \"health.body.read\",\n \"health.vitals.read\",\n ],\n },\n withings: {\n provider: \"withings\",\n envPrefix: \"WITHINGS\",\n oauth: {\n authorizeUrl: \"https://account.withings.com/oauth2_user/authorize2\",\n tokenUrl: \"https://wbsapi.withings.net/v2/oauth2\",\n revokeUrl: null,\n defaultScopes: [\n \"user.info\",\n \"user.metrics\",\n \"user.activity\",\n \"user.sleepevents\",\n ],\n scopeSeparator: \"comma\",\n usePkce: false,\n tokenRequestStyle: \"withings\",\n },\n apiBaseUrl: \"https://wbsapi.withings.net\",\n capabilities: [\n \"health.activity.read\",\n \"health.sleep.read\",\n \"health.body.read\",\n \"health.vitals.read\",\n ],\n },\n oura: {\n provider: \"oura\",\n envPrefix: \"OURA\",\n oauth: {\n authorizeUrl: \"https://cloud.ouraring.com/oauth/authorize\",\n tokenUrl: \"https://api.ouraring.com/oauth/token\",\n revokeUrl: \"https://api.ouraring.com/oauth/revoke\",\n defaultScopes: [\n \"email\",\n \"personal\",\n \"daily\",\n \"heartrate\",\n \"workout\",\n \"spo2\",\n ],\n scopeSeparator: \"space\",\n usePkce: false,\n tokenRequestStyle: \"basic\",\n },\n apiBaseUrl: \"https://api.ouraring.com\",\n capabilities: [\n \"health.activity.read\",\n \"health.workouts.read\",\n \"health.sleep.read\",\n \"health.readiness.read\",\n \"health.body.read\",\n \"health.vitals.read\",\n ],\n },\n};\n\nconst healthProviderSpecs: Map<string, HealthProviderSpec> = new Map(\n Object.entries(DEFAULT_HEALTH_PROVIDER_SPECS),\n);\n\nexport function listHealthProviderSpecs(): HealthProviderSpec[] {\n return Array.from(healthProviderSpecs.values());\n}\n\nexport function getHealthProviderSpec(\n provider: string,\n): HealthProviderSpec | null {\n return healthProviderSpecs.get(provider) ?? null;\n}\n\n/**\n * Look up a provider spec, throwing if it is not registered. The dispatcher\n * uses this in places where a missing entry indicates a programming error.\n */\nexport function requireHealthProviderSpec(\n provider: string,\n): HealthProviderSpec {\n const spec = healthProviderSpecs.get(provider);\n if (!spec) {\n throw new MissingOauthConfigError(provider);\n }\n return spec;\n}\n\n/**\n * Register or replace a provider spec. Used by tests and out-of-tree plugins\n * to add a fifth provider without editing this module.\n */\nexport function setHealthProviderSpec(spec: HealthProviderSpec): void {\n healthProviderSpecs.set(spec.provider, spec);\n}\n\nexport function deleteHealthProviderSpec(provider: string): void {\n healthProviderSpecs.delete(provider);\n}\n\nexport function resetHealthProviderRegistry(): void {\n healthProviderSpecs.clear();\n for (const [name, spec] of Object.entries(DEFAULT_HEALTH_PROVIDER_SPECS)) {\n healthProviderSpecs.set(name, spec);\n }\n}\n"],"mappings":"AA6EO,MAAM,gCAAgC,MAAM;AAAA,EACjC;AAAA,EAChB,YAAY,UAAkB;AAC5B;AAAA,MACE,qBAAqB,QAAQ;AAAA,IAC/B;AACA,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EAClB;AACF;AAEA,MAAM,gCAGF;AAAA,EACF,QAAQ;AAAA,IACN,UAAU;AAAA,IACV,WAAW;AAAA,IACX,OAAO;AAAA,MACL,cAAc;AAAA,MACd,UAAU;AAAA,MACV,WAAW;AAAA,MACX,eAAe,CAAC,QAAQ,oBAAoB,mBAAmB;AAAA,MAC/D,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,sBAAsB,EAAE,iBAAiB,OAAO;AAAA,IAClD;AAAA,IACA,YAAY;AAAA,IACZ,cAAc,CAAC,wBAAwB,sBAAsB;AAAA,EAC/D;AAAA,EACA,QAAQ;AAAA,IACN,UAAU;AAAA,IACV,WAAW;AAAA,IACX,OAAO;AAAA,MACL,cAAc;AAAA,MACd,UAAU;AAAA,MACV,WAAW;AAAA,MACX,eAAe,CAAC,WAAW,YAAY,aAAa,SAAS,QAAQ;AAAA,MACrE,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,mBAAmB;AAAA,IACrB;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,UAAU;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,IACX,OAAO;AAAA,MACL,cAAc;AAAA,MACd,UAAU;AAAA,MACV,WAAW;AAAA,MACX,eAAe;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,mBAAmB;AAAA,IACrB;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ,UAAU;AAAA,IACV,WAAW;AAAA,IACX,OAAO;AAAA,MACL,cAAc;AAAA,MACd,UAAU;AAAA,MACV,WAAW;AAAA,MACX,eAAe;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,mBAAmB;AAAA,IACrB;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,sBAAuD,IAAI;AAAA,EAC/D,OAAO,QAAQ,6BAA6B;AAC9C;AAEO,SAAS,0BAAgD;AAC9D,SAAO,MAAM,KAAK,oBAAoB,OAAO,CAAC;AAChD;AAEO,SAAS,sBACd,UAC2B;AAC3B,SAAO,oBAAoB,IAAI,QAAQ,KAAK;AAC9C;AAMO,SAAS,0BACd,UACoB;AACpB,QAAM,OAAO,oBAAoB,IAAI,QAAQ;AAC7C,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,wBAAwB,QAAQ;AAAA,EAC5C;AACA,SAAO;AACT;AAMO,SAAS,sBAAsB,MAAgC;AACpE,sBAAoB,IAAI,KAAK,UAAU,IAAI;AAC7C;AAEO,SAAS,yBAAyB,UAAwB;AAC/D,sBAAoB,OAAO,QAAQ;AACrC;AAEO,SAAS,8BAAoC;AAClD,sBAAoB,MAAM;AAC1B,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,6BAA6B,GAAG;AACxE,wBAAoB,IAAI,MAAM,IAAI;AAAA,EACpC;AACF;","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health-record factories — small pure helpers that build typed
|
|
3
|
+
* `LifeOpsHealth*` records with stable id + timestamp fields.
|
|
4
|
+
*
|
|
5
|
+
* Originally lived in `app-lifeops/src/lifeops/repository.ts`; moved into
|
|
6
|
+
* plugin-health in Wave-1 (W1-B). app-lifeops re-exports them from the
|
|
7
|
+
* repository module for backward compatibility.
|
|
8
|
+
*/
|
|
9
|
+
import type { LifeOpsHealthMetricSample, LifeOpsHealthSleepEpisode, LifeOpsHealthSyncState, LifeOpsHealthWorkout } from "../contracts/health.js";
|
|
10
|
+
export declare function createLifeOpsHealthMetricSample(params: Omit<LifeOpsHealthMetricSample, "id" | "createdAt" | "updatedAt">): LifeOpsHealthMetricSample;
|
|
11
|
+
export declare function createLifeOpsHealthWorkout(params: Omit<LifeOpsHealthWorkout, "id" | "createdAt" | "updatedAt">): LifeOpsHealthWorkout;
|
|
12
|
+
export declare function createLifeOpsHealthSleepEpisode(params: Omit<LifeOpsHealthSleepEpisode, "id" | "createdAt" | "updatedAt">): LifeOpsHealthSleepEpisode;
|
|
13
|
+
export declare function createLifeOpsHealthSyncState(params: Omit<LifeOpsHealthSyncState, "id" | "updatedAt">): LifeOpsHealthSyncState;
|
|
14
|
+
//# sourceMappingURL=health-records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-records.d.ts","sourceRoot":"","sources":["../../src/health-bridge/health-records.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,sBAAsB,EACtB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAMhC,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,IAAI,CAAC,yBAAyB,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GACxE,yBAAyB,CAQ3B;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GACnE,oBAAoB,CAQtB;AAED,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,IAAI,CAAC,yBAAyB,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GACxE,yBAAyB,CAQ3B;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,IAAI,CAAC,sBAAsB,EAAE,IAAI,GAAG,WAAW,CAAC,GACvD,sBAAsB,CAMxB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
function isoNow() {
|
|
3
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4
|
+
}
|
|
5
|
+
function createLifeOpsHealthMetricSample(params) {
|
|
6
|
+
const timestamp = isoNow();
|
|
7
|
+
return {
|
|
8
|
+
...params,
|
|
9
|
+
id: crypto.randomUUID(),
|
|
10
|
+
createdAt: timestamp,
|
|
11
|
+
updatedAt: timestamp
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function createLifeOpsHealthWorkout(params) {
|
|
15
|
+
const timestamp = isoNow();
|
|
16
|
+
return {
|
|
17
|
+
...params,
|
|
18
|
+
id: crypto.randomUUID(),
|
|
19
|
+
createdAt: timestamp,
|
|
20
|
+
updatedAt: timestamp
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function createLifeOpsHealthSleepEpisode(params) {
|
|
24
|
+
const timestamp = isoNow();
|
|
25
|
+
return {
|
|
26
|
+
...params,
|
|
27
|
+
id: crypto.randomUUID(),
|
|
28
|
+
createdAt: timestamp,
|
|
29
|
+
updatedAt: timestamp
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createLifeOpsHealthSyncState(params) {
|
|
33
|
+
return {
|
|
34
|
+
...params,
|
|
35
|
+
id: crypto.randomUUID(),
|
|
36
|
+
updatedAt: isoNow()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
createLifeOpsHealthMetricSample,
|
|
41
|
+
createLifeOpsHealthSleepEpisode,
|
|
42
|
+
createLifeOpsHealthSyncState,
|
|
43
|
+
createLifeOpsHealthWorkout
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=health-records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/health-bridge/health-records.ts"],"sourcesContent":["/**\n * Health-record factories — small pure helpers that build typed\n * `LifeOpsHealth*` records with stable id + timestamp fields.\n *\n * Originally lived in `app-lifeops/src/lifeops/repository.ts`; moved into\n * plugin-health in Wave-1 (W1-B). app-lifeops re-exports them from the\n * repository module for backward compatibility.\n */\n\nimport crypto from \"node:crypto\";\nimport type {\n LifeOpsHealthMetricSample,\n LifeOpsHealthSleepEpisode,\n LifeOpsHealthSyncState,\n LifeOpsHealthWorkout,\n} from \"../contracts/health.js\";\n\nfunction isoNow(): string {\n return new Date().toISOString();\n}\n\nexport function createLifeOpsHealthMetricSample(\n params: Omit<LifeOpsHealthMetricSample, \"id\" | \"createdAt\" | \"updatedAt\">,\n): LifeOpsHealthMetricSample {\n const timestamp = isoNow();\n return {\n ...params,\n id: crypto.randomUUID(),\n createdAt: timestamp,\n updatedAt: timestamp,\n };\n}\n\nexport function createLifeOpsHealthWorkout(\n params: Omit<LifeOpsHealthWorkout, \"id\" | \"createdAt\" | \"updatedAt\">,\n): LifeOpsHealthWorkout {\n const timestamp = isoNow();\n return {\n ...params,\n id: crypto.randomUUID(),\n createdAt: timestamp,\n updatedAt: timestamp,\n };\n}\n\nexport function createLifeOpsHealthSleepEpisode(\n params: Omit<LifeOpsHealthSleepEpisode, \"id\" | \"createdAt\" | \"updatedAt\">,\n): LifeOpsHealthSleepEpisode {\n const timestamp = isoNow();\n return {\n ...params,\n id: crypto.randomUUID(),\n createdAt: timestamp,\n updatedAt: timestamp,\n };\n}\n\nexport function createLifeOpsHealthSyncState(\n params: Omit<LifeOpsHealthSyncState, \"id\" | \"updatedAt\">,\n): LifeOpsHealthSyncState {\n return {\n ...params,\n id: crypto.randomUUID(),\n updatedAt: isoNow(),\n };\n}\n"],"mappings":"AASA,OAAO,YAAY;AAQnB,SAAS,SAAiB;AACxB,UAAO,oBAAI,KAAK,GAAE,YAAY;AAChC;AAEO,SAAS,gCACd,QAC2B;AAC3B,QAAM,YAAY,OAAO;AACzB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,OAAO,WAAW;AAAA,IACtB,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,SAAS,2BACd,QACsB;AACtB,QAAM,YAAY,OAAO;AACzB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,OAAO,WAAW;AAAA,IACtB,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,SAAS,gCACd,QAC2B;AAC3B,QAAM,YAAY,OAAO;AACzB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,OAAO,WAAW;AAAA,IACtB,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,SAAS,6BACd,QACwB;AACxB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,OAAO,WAAW;AAAA,IACtB,WAAW,OAAO;AAAA,EACpB;AACF;","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health-bridge domain entry point.
|
|
3
|
+
*
|
|
4
|
+
* `health-bridge.ts` shells out to a HealthKit native helper on darwin and
|
|
5
|
+
* to the Google Fit REST API as a cross-platform fallback. `health-connectors.ts`
|
|
6
|
+
* implements the Strava / Fitbit / Withings / Oura OAuth-bridged readers.
|
|
7
|
+
* `health-oauth.ts` owns the per-provider OAuth dance and pendingsession state.
|
|
8
|
+
* `service-normalize-health.ts` normalises inbound health-signal payloads.
|
|
9
|
+
*
|
|
10
|
+
* All four were moved verbatim from `eliza/plugins/app-lifeops/src/lifeops/`
|
|
11
|
+
* in Wave-1 (W1-B). The dependency on app-lifeops' SQL repository was
|
|
12
|
+
* inverted: plugin-health owns the `createLifeOpsHealth*` factories, and
|
|
13
|
+
* app-lifeops re-exports them from its repository module for backward
|
|
14
|
+
* compatibility.
|
|
15
|
+
*/
|
|
16
|
+
export * from "./health-bridge.js";
|
|
17
|
+
export * from "./health-connectors.js";
|
|
18
|
+
export * from "./health-oauth.js";
|
|
19
|
+
export * from "./health-provider-registry.js";
|
|
20
|
+
export * from "./health-records.js";
|
|
21
|
+
export * from "./service-normalize-health.js";
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/health-bridge/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./health-bridge.js";
|
|
2
|
+
export * from "./health-connectors.js";
|
|
3
|
+
export * from "./health-oauth.js";
|
|
4
|
+
export * from "./health-provider-registry.js";
|
|
5
|
+
export * from "./health-records.js";
|
|
6
|
+
export * from "./service-normalize-health.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/health-bridge/index.ts"],"sourcesContent":["/**\n * Health-bridge domain entry point.\n *\n * `health-bridge.ts` shells out to a HealthKit native helper on darwin and\n * to the Google Fit REST API as a cross-platform fallback. `health-connectors.ts`\n * implements the Strava / Fitbit / Withings / Oura OAuth-bridged readers.\n * `health-oauth.ts` owns the per-provider OAuth dance and pendingsession state.\n * `service-normalize-health.ts` normalises inbound health-signal payloads.\n *\n * All four were moved verbatim from `eliza/plugins/app-lifeops/src/lifeops/`\n * in Wave-1 (W1-B). The dependency on app-lifeops' SQL repository was\n * inverted: plugin-health owns the `createLifeOpsHealth*` factories, and\n * app-lifeops re-exports them from its repository module for backward\n * compatibility.\n */\n\nexport * from \"./health-bridge.js\";\nexport * from \"./health-connectors.js\";\nexport * from \"./health-oauth.js\";\nexport * from \"./health-provider-registry.js\";\nexport * from \"./health-records.js\";\nexport * from \"./service-normalize-health.js\";\n"],"mappings":"AAgBA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-normalize-health.d.ts","sourceRoot":"","sources":["../../src/health-bridge/service-normalize-health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AA0BlE,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,GACZ,mBAAmB,GAAG,IAAI,CAsF5B"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { LIFEOPS_HEALTH_SIGNAL_SOURCES } from "../contracts/health.js";
|
|
2
|
+
import {
|
|
3
|
+
fail,
|
|
4
|
+
normalizeOptionalBoolean,
|
|
5
|
+
normalizeOptionalFiniteNumber,
|
|
6
|
+
normalizeOptionalIsoString,
|
|
7
|
+
normalizeOptionalString,
|
|
8
|
+
requireNonEmptyString
|
|
9
|
+
} from "../util/normalize.js";
|
|
10
|
+
function requireRecord(value, field) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12
|
+
fail(400, `${field} must be an object`);
|
|
13
|
+
}
|
|
14
|
+
return { ...value };
|
|
15
|
+
}
|
|
16
|
+
function normalizeOptionalRecord(value, field) {
|
|
17
|
+
if (value === void 0) return void 0;
|
|
18
|
+
return requireRecord(value, field);
|
|
19
|
+
}
|
|
20
|
+
function normalizeHealthSignal(value, field) {
|
|
21
|
+
if (value === null || value === void 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const record = requireRecord(value, field);
|
|
25
|
+
const sleep = normalizeOptionalRecord(record.sleep, `${field}.sleep`) ?? {};
|
|
26
|
+
const biometrics = normalizeOptionalRecord(record.biometrics, `${field}.biometrics`) ?? {};
|
|
27
|
+
const permissions = normalizeOptionalRecord(record.permissions, `${field}.permissions`) ?? {};
|
|
28
|
+
const source = normalizeOptionalString(record.source) ?? "healthkit";
|
|
29
|
+
if (!LIFEOPS_HEALTH_SIGNAL_SOURCES.includes(source)) {
|
|
30
|
+
fail(
|
|
31
|
+
400,
|
|
32
|
+
`${field}.source must be one of: ${LIFEOPS_HEALTH_SIGNAL_SOURCES.join(", ")}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const warnings = Array.isArray(record.warnings) ? record.warnings.map(
|
|
36
|
+
(warning, index) => requireNonEmptyString(warning, `${field}.warnings[${index}]`)
|
|
37
|
+
) : [];
|
|
38
|
+
return {
|
|
39
|
+
source,
|
|
40
|
+
permissions: {
|
|
41
|
+
sleep: normalizeOptionalBoolean(
|
|
42
|
+
permissions.sleep,
|
|
43
|
+
`${field}.permissions.sleep`
|
|
44
|
+
) ?? false,
|
|
45
|
+
biometrics: normalizeOptionalBoolean(
|
|
46
|
+
permissions.biometrics,
|
|
47
|
+
`${field}.permissions.biometrics`
|
|
48
|
+
) ?? false
|
|
49
|
+
},
|
|
50
|
+
sleep: {
|
|
51
|
+
available: normalizeOptionalBoolean(sleep.available, `${field}.sleep.available`) ?? false,
|
|
52
|
+
isSleeping: normalizeOptionalBoolean(
|
|
53
|
+
sleep.isSleeping,
|
|
54
|
+
`${field}.sleep.isSleeping`
|
|
55
|
+
) ?? false,
|
|
56
|
+
asleepAt: normalizeOptionalIsoString(sleep.asleepAt, `${field}.sleep.asleepAt`) ?? null,
|
|
57
|
+
awakeAt: normalizeOptionalIsoString(sleep.awakeAt, `${field}.sleep.awakeAt`) ?? null,
|
|
58
|
+
durationMinutes: normalizeOptionalFiniteNumber(
|
|
59
|
+
sleep.durationMinutes,
|
|
60
|
+
`${field}.sleep.durationMinutes`
|
|
61
|
+
),
|
|
62
|
+
stage: normalizeOptionalString(sleep.stage) ?? null
|
|
63
|
+
},
|
|
64
|
+
biometrics: {
|
|
65
|
+
sampleAt: normalizeOptionalIsoString(
|
|
66
|
+
biometrics.sampleAt,
|
|
67
|
+
`${field}.biometrics.sampleAt`
|
|
68
|
+
) ?? null,
|
|
69
|
+
heartRateBpm: normalizeOptionalFiniteNumber(
|
|
70
|
+
biometrics.heartRateBpm,
|
|
71
|
+
`${field}.biometrics.heartRateBpm`
|
|
72
|
+
),
|
|
73
|
+
restingHeartRateBpm: normalizeOptionalFiniteNumber(
|
|
74
|
+
biometrics.restingHeartRateBpm,
|
|
75
|
+
`${field}.biometrics.restingHeartRateBpm`
|
|
76
|
+
),
|
|
77
|
+
heartRateVariabilityMs: normalizeOptionalFiniteNumber(
|
|
78
|
+
biometrics.heartRateVariabilityMs,
|
|
79
|
+
`${field}.biometrics.heartRateVariabilityMs`
|
|
80
|
+
),
|
|
81
|
+
respiratoryRate: normalizeOptionalFiniteNumber(
|
|
82
|
+
biometrics.respiratoryRate,
|
|
83
|
+
`${field}.biometrics.respiratoryRate`
|
|
84
|
+
),
|
|
85
|
+
bloodOxygenPercent: normalizeOptionalFiniteNumber(
|
|
86
|
+
biometrics.bloodOxygenPercent,
|
|
87
|
+
`${field}.biometrics.bloodOxygenPercent`
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
warnings
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
normalizeHealthSignal
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=service-normalize-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/health-bridge/service-normalize-health.ts"],"sourcesContent":["import type { LifeOpsHealthSignal } from \"../contracts/health.js\";\nimport { LIFEOPS_HEALTH_SIGNAL_SOURCES } from \"../contracts/health.js\";\nimport {\n fail,\n normalizeOptionalBoolean,\n normalizeOptionalFiniteNumber,\n normalizeOptionalIsoString,\n normalizeOptionalString,\n requireNonEmptyString,\n} from \"../util/normalize.js\";\n\nfunction requireRecord(value: unknown, field: string): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n fail(400, `${field} must be an object`);\n }\n return { ...value } as Record<string, unknown>;\n}\n\nfunction normalizeOptionalRecord(\n value: unknown,\n field: string,\n): Record<string, unknown> | undefined {\n if (value === undefined) return undefined;\n return requireRecord(value, field);\n}\n\nexport function normalizeHealthSignal(\n value: unknown,\n field: string,\n): LifeOpsHealthSignal | null {\n if (value === null || value === undefined) {\n return null;\n }\n const record = requireRecord(value, field);\n const sleep = normalizeOptionalRecord(record.sleep, `${field}.sleep`) ?? {};\n const biometrics =\n normalizeOptionalRecord(record.biometrics, `${field}.biometrics`) ?? {};\n const permissions =\n normalizeOptionalRecord(record.permissions, `${field}.permissions`) ?? {};\n const source = normalizeOptionalString(record.source) ?? \"healthkit\";\n if (!(LIFEOPS_HEALTH_SIGNAL_SOURCES as readonly string[]).includes(source)) {\n fail(\n 400,\n `${field}.source must be one of: ${LIFEOPS_HEALTH_SIGNAL_SOURCES.join(\", \")}`,\n );\n }\n const warnings = Array.isArray(record.warnings)\n ? record.warnings.map((warning, index) =>\n requireNonEmptyString(warning, `${field}.warnings[${index}]`),\n )\n : [];\n return {\n source: source as LifeOpsHealthSignal[\"source\"],\n permissions: {\n sleep:\n normalizeOptionalBoolean(\n permissions.sleep,\n `${field}.permissions.sleep`,\n ) ?? false,\n biometrics:\n normalizeOptionalBoolean(\n permissions.biometrics,\n `${field}.permissions.biometrics`,\n ) ?? false,\n },\n sleep: {\n available:\n normalizeOptionalBoolean(sleep.available, `${field}.sleep.available`) ??\n false,\n isSleeping:\n normalizeOptionalBoolean(\n sleep.isSleeping,\n `${field}.sleep.isSleeping`,\n ) ?? false,\n asleepAt:\n normalizeOptionalIsoString(sleep.asleepAt, `${field}.sleep.asleepAt`) ??\n null,\n awakeAt:\n normalizeOptionalIsoString(sleep.awakeAt, `${field}.sleep.awakeAt`) ??\n null,\n durationMinutes: normalizeOptionalFiniteNumber(\n sleep.durationMinutes,\n `${field}.sleep.durationMinutes`,\n ),\n stage: normalizeOptionalString(sleep.stage) ?? null,\n },\n biometrics: {\n sampleAt:\n normalizeOptionalIsoString(\n biometrics.sampleAt,\n `${field}.biometrics.sampleAt`,\n ) ?? null,\n heartRateBpm: normalizeOptionalFiniteNumber(\n biometrics.heartRateBpm,\n `${field}.biometrics.heartRateBpm`,\n ),\n restingHeartRateBpm: normalizeOptionalFiniteNumber(\n biometrics.restingHeartRateBpm,\n `${field}.biometrics.restingHeartRateBpm`,\n ),\n heartRateVariabilityMs: normalizeOptionalFiniteNumber(\n biometrics.heartRateVariabilityMs,\n `${field}.biometrics.heartRateVariabilityMs`,\n ),\n respiratoryRate: normalizeOptionalFiniteNumber(\n biometrics.respiratoryRate,\n `${field}.biometrics.respiratoryRate`,\n ),\n bloodOxygenPercent: normalizeOptionalFiniteNumber(\n biometrics.bloodOxygenPercent,\n `${field}.biometrics.bloodOxygenPercent`,\n ),\n },\n warnings,\n };\n}\n"],"mappings":"AACA,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,cAAc,OAAgB,OAAwC;AAC7E,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,SAAK,KAAK,GAAG,KAAK,oBAAoB;AAAA,EACxC;AACA,SAAO,EAAE,GAAG,MAAM;AACpB;AAEA,SAAS,wBACP,OACA,OACqC;AACrC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,cAAc,OAAO,KAAK;AACnC;AAEO,SAAS,sBACd,OACA,OAC4B;AAC5B,MAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,WAAO;AAAA,EACT;AACA,QAAM,SAAS,cAAc,OAAO,KAAK;AACzC,QAAM,QAAQ,wBAAwB,OAAO,OAAO,GAAG,KAAK,QAAQ,KAAK,CAAC;AAC1E,QAAM,aACJ,wBAAwB,OAAO,YAAY,GAAG,KAAK,aAAa,KAAK,CAAC;AACxE,QAAM,cACJ,wBAAwB,OAAO,aAAa,GAAG,KAAK,cAAc,KAAK,CAAC;AAC1E,QAAM,SAAS,wBAAwB,OAAO,MAAM,KAAK;AACzD,MAAI,CAAE,8BAAoD,SAAS,MAAM,GAAG;AAC1E;AAAA,MACE;AAAA,MACA,GAAG,KAAK,2BAA2B,8BAA8B,KAAK,IAAI,CAAC;AAAA,IAC7E;AAAA,EACF;AACA,QAAM,WAAW,MAAM,QAAQ,OAAO,QAAQ,IAC1C,OAAO,SAAS;AAAA,IAAI,CAAC,SAAS,UAC5B,sBAAsB,SAAS,GAAG,KAAK,aAAa,KAAK,GAAG;AAAA,EAC9D,IACA,CAAC;AACL,SAAO;AAAA,IACL;AAAA,IACA,aAAa;AAAA,MACX,OACE;AAAA,QACE,YAAY;AAAA,QACZ,GAAG,KAAK;AAAA,MACV,KAAK;AAAA,MACP,YACE;AAAA,QACE,YAAY;AAAA,QACZ,GAAG,KAAK;AAAA,MACV,KAAK;AAAA,IACT;AAAA,IACA,OAAO;AAAA,MACL,WACE,yBAAyB,MAAM,WAAW,GAAG,KAAK,kBAAkB,KACpE;AAAA,MACF,YACE;AAAA,QACE,MAAM;AAAA,QACN,GAAG,KAAK;AAAA,MACV,KAAK;AAAA,MACP,UACE,2BAA2B,MAAM,UAAU,GAAG,KAAK,iBAAiB,KACpE;AAAA,MACF,SACE,2BAA2B,MAAM,SAAS,GAAG,KAAK,gBAAgB,KAClE;AAAA,MACF,iBAAiB;AAAA,QACf,MAAM;AAAA,QACN,GAAG,KAAK;AAAA,MACV;AAAA,MACA,OAAO,wBAAwB,MAAM,KAAK,KAAK;AAAA,IACjD;AAAA,IACA,YAAY;AAAA,MACV,UACE;AAAA,QACE,WAAW;AAAA,QACX,GAAG,KAAK;AAAA,MACV,KAAK;AAAA,MACP,cAAc;AAAA,QACZ,WAAW;AAAA,QACX,GAAG,KAAK;AAAA,MACV;AAAA,MACA,qBAAqB;AAAA,QACnB,WAAW;AAAA,QACX,GAAG,KAAK;AAAA,MACV;AAAA,MACA,wBAAwB;AAAA,QACtB,WAAW;AAAA,QACX,GAAG,KAAK;AAAA,MACV;AAAA,MACA,iBAAiB;AAAA,QACf,WAAW;AAAA,QACX,GAAG,KAAK;AAAA,MACV;AAAA,MACA,oBAAoB;AAAA,QAClB,WAAW;AAAA,QACX,GAAG,KAAK;AAAA,MACV;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elizaos/plugin-health — Wave-1 (W1-B) extraction.
|
|
3
|
+
*
|
|
4
|
+
* Owns the sleep / circadian / health-metric / screen-time domain previously
|
|
5
|
+
* intermingled with `app-lifeops`. LifeOps consumes plugin-health through:
|
|
6
|
+
*
|
|
7
|
+
* - `ConnectorRegistry` contributions (apple_health, google_fit, strava,
|
|
8
|
+
* fitbit, withings, oura)
|
|
9
|
+
* - `ActivitySignalBus` publications (`health.sleep.detected`,
|
|
10
|
+
* `health.wake.observed`, `health.wake.confirmed`,
|
|
11
|
+
* `health.bedtime.imminent`, `health.regularity.changed`,
|
|
12
|
+
* `health.workout.completed`, …)
|
|
13
|
+
* - `AnchorRegistry` contributions (`wake.observed`, `wake.confirmed`,
|
|
14
|
+
* `bedtime.target`, `nap.start`)
|
|
15
|
+
* - Default-pack `ScheduledTask` records (bedtime / wake-up / sleep-recap)
|
|
16
|
+
*
|
|
17
|
+
* See `eliza/plugins/app-lifeops/docs/audit/IMPLEMENTATION_PLAN.md` §3.2 and
|
|
18
|
+
* `wave1-interfaces.md` §5 for the canonical scope.
|
|
19
|
+
*/
|
|
20
|
+
import type { Plugin } from "@elizaos/core";
|
|
21
|
+
export * from "./actions/index.js";
|
|
22
|
+
export * from "./anchors/index.js";
|
|
23
|
+
export * from "./connectors/index.js";
|
|
24
|
+
export * from "./contracts/circadian.js";
|
|
25
|
+
export * from "./contracts/circadian-default.js";
|
|
26
|
+
export * from "./contracts/health.js";
|
|
27
|
+
export * from "./default-packs/index.js";
|
|
28
|
+
export * from "./health-bridge/index.js";
|
|
29
|
+
export * from "./screen-time/index.js";
|
|
30
|
+
export * from "./sleep/index.js";
|
|
31
|
+
export * from "./util/index.js";
|
|
32
|
+
export declare const HEALTH_PLUGIN_NAME = "plugin-health";
|
|
33
|
+
/**
|
|
34
|
+
* elizaOS plugin entry. Registers connector / anchor / bus-family / default-pack
|
|
35
|
+
* contributions when the W1-A and W1-F runtime registries are available; logs
|
|
36
|
+
* a one-line skip reason when they are not (Wave-1 soft dependency posture
|
|
37
|
+
* per `IMPLEMENTATION_PLAN.md` §3.2).
|
|
38
|
+
*/
|
|
39
|
+
export declare const healthPlugin: Plugin;
|
|
40
|
+
export default healthPlugin;
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAiB,MAAM,EAAE,MAAM,eAAe,CAAC;AAoB3D,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAEhC,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,MAkC1B,CAAC;AAEF,eAAe,YAAY,CAAC"}
|