trainbud 0.5.0
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/.env.example +11 -0
- package/CHANGELOG.md +675 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +182 -0
- package/README.md +322 -0
- package/dist/appDb.d.ts +83 -0
- package/dist/appDb.d.ts.map +1 -0
- package/dist/appDb.js +239 -0
- package/dist/appDb.js.map +1 -0
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +230 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +457 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +241 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +1004 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/dashboardCharts.d.ts +99 -0
- package/dist/dashboardCharts.d.ts.map +1 -0
- package/dist/dashboardCharts.js +317 -0
- package/dist/dashboardCharts.js.map +1 -0
- package/dist/dashboardData.d.ts +89 -0
- package/dist/dashboardData.d.ts.map +1 -0
- package/dist/dashboardData.js +228 -0
- package/dist/dashboardData.js.map +1 -0
- package/dist/detect/baseline.d.ts +17 -0
- package/dist/detect/baseline.d.ts.map +1 -0
- package/dist/detect/baseline.js +62 -0
- package/dist/detect/baseline.js.map +1 -0
- package/dist/detect/countdown.d.ts +30 -0
- package/dist/detect/countdown.d.ts.map +1 -0
- package/dist/detect/countdown.js +79 -0
- package/dist/detect/countdown.js.map +1 -0
- package/dist/detect/detectors.d.ts +20 -0
- package/dist/detect/detectors.d.ts.map +1 -0
- package/dist/detect/detectors.js +291 -0
- package/dist/detect/detectors.js.map +1 -0
- package/dist/detect/findings.d.ts +29 -0
- package/dist/detect/findings.d.ts.map +1 -0
- package/dist/detect/findings.js +2 -0
- package/dist/detect/findings.js.map +1 -0
- package/dist/detect/forecast.d.ts +33 -0
- package/dist/detect/forecast.d.ts.map +1 -0
- package/dist/detect/forecast.js +122 -0
- package/dist/detect/forecast.js.map +1 -0
- package/dist/detect/index.d.ts +50 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +114 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/detect/sleepQuality.d.ts +21 -0
- package/dist/detect/sleepQuality.d.ts.map +1 -0
- package/dist/detect/sleepQuality.js +164 -0
- package/dist/detect/sleepQuality.js.map +1 -0
- package/dist/detect/trimp.d.ts +22 -0
- package/dist/detect/trimp.d.ts.map +1 -0
- package/dist/detect/trimp.js +77 -0
- package/dist/detect/trimp.js.map +1 -0
- package/dist/detect/week.d.ts +31 -0
- package/dist/detect/week.d.ts.map +1 -0
- package/dist/detect/week.js +184 -0
- package/dist/detect/week.js.map +1 -0
- package/dist/garmin/auth.d.ts +10 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +81 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/cache.d.ts +42 -0
- package/dist/garmin/cache.d.ts.map +1 -0
- package/dist/garmin/cache.js +141 -0
- package/dist/garmin/cache.js.map +1 -0
- package/dist/garmin/client.d.ts +22 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +237 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +24 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +96 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/garminApiTypes.d.ts +70 -0
- package/dist/garmin/garminApiTypes.d.ts.map +1 -0
- package/dist/garmin/garminApiTypes.js +3 -0
- package/dist/garmin/garminApiTypes.js.map +1 -0
- package/dist/garmin/garminConnect.d.ts +17 -0
- package/dist/garmin/garminConnect.d.ts.map +1 -0
- package/dist/garmin/garminConnect.js +5 -0
- package/dist/garmin/garminConnect.js.map +1 -0
- package/dist/garmin/partial.d.ts +32 -0
- package/dist/garmin/partial.d.ts.map +1 -0
- package/dist/garmin/partial.js +38 -0
- package/dist/garmin/partial.js.map +1 -0
- package/dist/garmin/rawApi.d.ts +59 -0
- package/dist/garmin/rawApi.d.ts.map +1 -0
- package/dist/garmin/rawApi.js +113 -0
- package/dist/garmin/rawApi.js.map +1 -0
- package/dist/garmin/types.d.ts +85 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +11 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/history/capture.d.ts +12 -0
- package/dist/history/capture.d.ts.map +1 -0
- package/dist/history/capture.js +63 -0
- package/dist/history/capture.js.map +1 -0
- package/dist/history/context.d.ts +51 -0
- package/dist/history/context.d.ts.map +1 -0
- package/dist/history/context.js +122 -0
- package/dist/history/context.js.map +1 -0
- package/dist/history/fallback.d.ts +72 -0
- package/dist/history/fallback.d.ts.map +1 -0
- package/dist/history/fallback.js +185 -0
- package/dist/history/fallback.js.map +1 -0
- package/dist/history/ingest.d.ts +44 -0
- package/dist/history/ingest.d.ts.map +1 -0
- package/dist/history/ingest.js +347 -0
- package/dist/history/ingest.js.map +1 -0
- package/dist/history/scheduler.d.ts +20 -0
- package/dist/history/scheduler.d.ts.map +1 -0
- package/dist/history/scheduler.js +54 -0
- package/dist/history/scheduler.js.map +1 -0
- package/dist/history/schema.d.ts +41 -0
- package/dist/history/schema.d.ts.map +1 -0
- package/dist/history/schema.js +91 -0
- package/dist/history/schema.js.map +1 -0
- package/dist/history/store.d.ts +112 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/store.js +282 -0
- package/dist/history/store.js.map +1 -0
- package/dist/httpServer.d.ts +55 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1027 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcpConfig.d.ts +56 -0
- package/dist/mcpConfig.d.ts.map +1 -0
- package/dist/mcpConfig.js +106 -0
- package/dist/mcpConfig.js.map +1 -0
- package/dist/pairApi.d.ts +14 -0
- package/dist/pairApi.d.ts.map +1 -0
- package/dist/pairApi.js +38 -0
- package/dist/pairApi.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +105 -0
- package/dist/paths.js.map +1 -0
- package/dist/profile.d.ts +123 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +327 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptApi.d.ts +43 -0
- package/dist/promptApi.d.ts.map +1 -0
- package/dist/promptApi.js +328 -0
- package/dist/promptApi.js.map +1 -0
- package/dist/promptSuggestions.d.ts +16 -0
- package/dist/promptSuggestions.d.ts.map +1 -0
- package/dist/promptSuggestions.js +141 -0
- package/dist/promptSuggestions.js.map +1 -0
- package/dist/selfTest.d.ts +53 -0
- package/dist/selfTest.d.ts.map +1 -0
- package/dist/selfTest.js +225 -0
- package/dist/selfTest.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +210 -0
- package/dist/setup.js.map +1 -0
- package/dist/toolErrors.d.ts +2 -0
- package/dist/toolErrors.d.ts.map +1 -0
- package/dist/toolErrors.js +18 -0
- package/dist/toolErrors.js.map +1 -0
- package/dist/tools/activities.d.ts +18 -0
- package/dist/tools/activities.d.ts.map +1 -0
- package/dist/tools/activities.js +182 -0
- package/dist/tools/activities.js.map +1 -0
- package/dist/tools/bodyComposition.d.ts +10 -0
- package/dist/tools/bodyComposition.d.ts.map +1 -0
- package/dist/tools/bodyComposition.js +139 -0
- package/dist/tools/bodyComposition.js.map +1 -0
- package/dist/tools/context.d.ts +8 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +148 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/findings.d.ts +7 -0
- package/dist/tools/findings.d.ts.map +1 -0
- package/dist/tools/findings.js +51 -0
- package/dist/tools/findings.js.map +1 -0
- package/dist/tools/heartRate.d.ts +10 -0
- package/dist/tools/heartRate.d.ts.map +1 -0
- package/dist/tools/heartRate.js +105 -0
- package/dist/tools/heartRate.js.map +1 -0
- package/dist/tools/index.d.ts +66 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +101 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/payloads.d.ts +148 -0
- package/dist/tools/payloads.d.ts.map +1 -0
- package/dist/tools/payloads.js +2 -0
- package/dist/tools/payloads.js.map +1 -0
- package/dist/tools/recovery.d.ts +31 -0
- package/dist/tools/recovery.d.ts.map +1 -0
- package/dist/tools/recovery.js +350 -0
- package/dist/tools/recovery.js.map +1 -0
- package/dist/tools/sleep.d.ts +10 -0
- package/dist/tools/sleep.d.ts.map +1 -0
- package/dist/tools/sleep.js +124 -0
- package/dist/tools/sleep.js.map +1 -0
- package/dist/tools/stress.d.ts +11 -0
- package/dist/tools/stress.d.ts.map +1 -0
- package/dist/tools/stress.js +108 -0
- package/dist/tools/stress.js.map +1 -0
- package/dist/tools/trainingInsights.d.ts +15 -0
- package/dist/tools/trainingInsights.d.ts.map +1 -0
- package/dist/tools/trainingInsights.js +79 -0
- package/dist/tools/trainingInsights.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/vo2Max.d.ts +11 -0
- package/dist/tools/vo2Max.d.ts.map +1 -0
- package/dist/tools/vo2Max.js +110 -0
- package/dist/tools/vo2Max.js.map +1 -0
- package/dist/tools/week.d.ts +12 -0
- package/dist/tools/week.d.ts.map +1 -0
- package/dist/tools/week.js +74 -0
- package/dist/tools/week.js.map +1 -0
- package/dist/usage.d.ts +104 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +247 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -0
- package/dist/utils/batch.d.ts.map +1 -0
- package/dist/utils/batch.js +18 -0
- package/dist/utils/batch.js.map +1 -0
- package/dist/utils/helpers.d.ts +32 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +188 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/jsonFile.d.ts +19 -0
- package/dist/utils/jsonFile.d.ts.map +1 -0
- package/dist/utils/jsonFile.js +24 -0
- package/dist/utils/jsonFile.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +97 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/secretFile.d.ts +15 -0
- package/dist/utils/secretFile.d.ts.map +1 -0
- package/dist/utils/secretFile.js +47 -0
- package/dist/utils/secretFile.js.map +1 -0
- package/dist/utils/stdio.d.ts +10 -0
- package/dist/utils/stdio.d.ts.map +1 -0
- package/dist/utils/stdio.js +70 -0
- package/dist/utils/stdio.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/watchApi.d.ts +264 -0
- package/dist/watchApi.d.ts.map +1 -0
- package/dist/watchApi.js +333 -0
- package/dist/watchApi.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"week.js","sourceRoot":"","sources":["../../src/detect/week.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GarminConnectInstance } from "./garminConnect.js";
|
|
2
|
+
import type { StoredSession } from "./types.js";
|
|
3
|
+
export declare function sessionDirectory(): string;
|
|
4
|
+
export declare function sessionExists(): boolean;
|
|
5
|
+
export declare function readStoredSession(): StoredSession | null;
|
|
6
|
+
export declare function writeStoredSession(session: StoredSession): void;
|
|
7
|
+
export declare function clearStoredSession(): void;
|
|
8
|
+
export declare function createGarminClient(): GarminConnectInstance;
|
|
9
|
+
export declare function authenticateGarmin(force?: boolean): Promise<GarminConnectInstance>;
|
|
10
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/garmin/auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAMhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,iBAAiB,IAAI,aAAa,GAAG,IAAI,CAmBxD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAG/D;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC;AAID,wBAAgB,kBAAkB,IAAI,qBAAqB,CAM1D;AAED,wBAAsB,kBAAkB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAkCtF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { GarminConnect } from "./garminConnect.js";
|
|
4
|
+
import { appConfig, assertGarminCredentials } from "../config.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { readJsonFile } from "../utils/jsonFile.js";
|
|
7
|
+
import { writeSecretFile } from "../utils/secretFile.js";
|
|
8
|
+
// SECTION: Session Persistence
|
|
9
|
+
export function sessionDirectory() {
|
|
10
|
+
return path.dirname(appConfig.sessionPath);
|
|
11
|
+
}
|
|
12
|
+
export function sessionExists() {
|
|
13
|
+
return fs.existsSync(appConfig.sessionPath);
|
|
14
|
+
}
|
|
15
|
+
export function readStoredSession() {
|
|
16
|
+
const sessionPath = appConfig.sessionPath;
|
|
17
|
+
if (!fs.existsSync(sessionPath)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const parsed = readJsonFile(sessionPath);
|
|
22
|
+
if (!parsed.oauth1 || !parsed.oauth2) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logger.warn({ error }, "Failed to read stored Garmin session");
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function writeStoredSession(session) {
|
|
33
|
+
// Live OAuth tokens for the Connect account -- owner-readable only.
|
|
34
|
+
writeSecretFile(appConfig.sessionPath, JSON.stringify(session, null, 2));
|
|
35
|
+
}
|
|
36
|
+
export function clearStoredSession() {
|
|
37
|
+
const sessionPath = appConfig.sessionPath;
|
|
38
|
+
if (fs.existsSync(sessionPath)) {
|
|
39
|
+
fs.unlinkSync(sessionPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// SECTION: Authentication
|
|
43
|
+
export function createGarminClient() {
|
|
44
|
+
assertGarminCredentials();
|
|
45
|
+
return new GarminConnect({
|
|
46
|
+
username: appConfig.garminEmail,
|
|
47
|
+
password: appConfig.garminPassword,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export async function authenticateGarmin(force = false) {
|
|
51
|
+
const client = createGarminClient();
|
|
52
|
+
if (!force) {
|
|
53
|
+
const storedSession = readStoredSession();
|
|
54
|
+
if (storedSession) {
|
|
55
|
+
client.loadToken(storedSession.oauth1, storedSession.oauth2);
|
|
56
|
+
try {
|
|
57
|
+
await client.getUserProfile();
|
|
58
|
+
logger.info("Restored Garmin session from disk");
|
|
59
|
+
return client;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.warn({ error }, "Stored Garmin session expired, re-authenticating");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
clearStoredSession();
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await client.login();
|
|
71
|
+
const tokens = client.exportToken();
|
|
72
|
+
writeStoredSession(tokens);
|
|
73
|
+
logger.info("Authenticated with Garmin Connect");
|
|
74
|
+
return client;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : "Unknown authentication error";
|
|
78
|
+
throw new Error(`Garmin authentication failed: ${message}. Verify credentials in .env. MFA is not supported yet.`, { cause: error });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/garmin/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a result that is missing days because requests FAILED may be kept.
|
|
3
|
+
* A complete answer earns its full TTL; a degraded one gets long enough to stop
|
|
4
|
+
* a retry storm and no longer.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PARTIAL_CACHE_TTL_SECONDS = 60;
|
|
7
|
+
export declare class GarminCache {
|
|
8
|
+
private readonly db;
|
|
9
|
+
constructor(databasePath?: string);
|
|
10
|
+
private initialize;
|
|
11
|
+
buildKey(tool: string, params: Record<string, unknown>): string;
|
|
12
|
+
get<T>(key: string): T | null;
|
|
13
|
+
set<T>(key: string, value: T, ttlSeconds: number): void;
|
|
14
|
+
delete(key: string): void;
|
|
15
|
+
clear(): number;
|
|
16
|
+
stats(): {
|
|
17
|
+
entries: number;
|
|
18
|
+
expiredEntries: number;
|
|
19
|
+
};
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function getCache(): GarminCache;
|
|
23
|
+
export declare function closeCache(): void;
|
|
24
|
+
export declare function buildToolCacheKey(tool: string, params: Record<string, unknown>): string;
|
|
25
|
+
export interface CacheOptions<T> {
|
|
26
|
+
/**
|
|
27
|
+
* True when the value is incomplete because a request failed rather than
|
|
28
|
+
* because nothing was measured. Such a value is cached for
|
|
29
|
+
* `partialTtlSeconds`, not for the full TTL.
|
|
30
|
+
*/
|
|
31
|
+
isPartial?: (value: T) => boolean;
|
|
32
|
+
partialTtlSeconds?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A thrown fetcher caches nothing -- that has always been true and is the point
|
|
36
|
+
* of the `await` sitting outside `cache.set`. What was not true is that a
|
|
37
|
+
* fetcher which swallowed its own failures and returned an empty result got the
|
|
38
|
+
* full success TTL, so one rate-limited minute froze "no data" in for two hours.
|
|
39
|
+
* A caller that can tell the difference now says so through `isPartial`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function withCache<T>(key: string, ttlSeconds: number, fetcher: () => Promise<T>, options?: CacheOptions<T>): Promise<T>;
|
|
42
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/garmin/cache.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAI5C,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;gBAE3B,YAAY,SAAsB;IAU9C,OAAO,CAAC,UAAU;IAWlB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAI/D,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI;IAwB7B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAqBvD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,KAAK,IAAI,MAAM;IAKf,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE;IAepD,KAAK,IAAI,IAAI;CAGd;AAID,wBAAgB,QAAQ,IAAI,WAAW,CAMtC;AAED,wBAAgB,UAAU,IAAI,IAAI,CAKjC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEvF;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAClC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE,YAAY,CAAC,CAAC,CAAM,GAC5B,OAAO,CAAC,CAAC,CAAC,CAkBZ"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { appConfig } from "../config.js";
|
|
5
|
+
import { hashParams } from "../utils/helpers.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { restrictExistingFile } from "../utils/secretFile.js";
|
|
8
|
+
/**
|
|
9
|
+
* Bumped whenever a cached value changes shape. The cache is keyed by tool and
|
|
10
|
+
* parameters only, so without this a build that starts storing
|
|
11
|
+
* `{ values, unreachableDays }` under a key that already holds a bare array
|
|
12
|
+
* reads the old row back, casts it to the new type and dereferences a field
|
|
13
|
+
* that is not there. An upgrade must invalidate, not reinterpret.
|
|
14
|
+
*/
|
|
15
|
+
const CACHE_SCHEMA_VERSION = "v2";
|
|
16
|
+
/**
|
|
17
|
+
* How long a result that is missing days because requests FAILED may be kept.
|
|
18
|
+
* A complete answer earns its full TTL; a degraded one gets long enough to stop
|
|
19
|
+
* a retry storm and no longer.
|
|
20
|
+
*/
|
|
21
|
+
export const PARTIAL_CACHE_TTL_SECONDS = 60;
|
|
22
|
+
// SECTION: SQLite Cache
|
|
23
|
+
export class GarminCache {
|
|
24
|
+
db;
|
|
25
|
+
constructor(databasePath = appConfig.cachePath) {
|
|
26
|
+
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
|
|
27
|
+
this.db = new Database(databasePath);
|
|
28
|
+
// Holds whatever the tools last fetched: sleep, stress, resting heart rate.
|
|
29
|
+
// Shorter-lived than history.db and exactly as personal.
|
|
30
|
+
restrictExistingFile(databasePath);
|
|
31
|
+
this.initialize();
|
|
32
|
+
}
|
|
33
|
+
initialize() {
|
|
34
|
+
this.db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
36
|
+
key TEXT PRIMARY KEY,
|
|
37
|
+
data TEXT NOT NULL,
|
|
38
|
+
cached_at INTEGER NOT NULL,
|
|
39
|
+
ttl INTEGER NOT NULL
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
buildKey(tool, params) {
|
|
44
|
+
return `${tool}:${CACHE_SCHEMA_VERSION}:${hashParams(params)}`;
|
|
45
|
+
}
|
|
46
|
+
get(key) {
|
|
47
|
+
const row = this.db
|
|
48
|
+
.prepare("SELECT key, data, cached_at, ttl FROM cache WHERE key = ?")
|
|
49
|
+
.get(key);
|
|
50
|
+
if (!row) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const expiresAt = row.cached_at + row.ttl;
|
|
54
|
+
if (Date.now() > expiresAt * 1000) {
|
|
55
|
+
this.delete(key);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(row.data);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.warn({ error, key }, "Failed to parse cached payload");
|
|
63
|
+
this.delete(key);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
set(key, value, ttlSeconds) {
|
|
68
|
+
const cachedAt = Math.floor(Date.now() / 1000);
|
|
69
|
+
this.db
|
|
70
|
+
.prepare(`
|
|
71
|
+
INSERT INTO cache (key, data, cached_at, ttl)
|
|
72
|
+
VALUES (@key, @data, @cached_at, @ttl)
|
|
73
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
74
|
+
data = excluded.data,
|
|
75
|
+
cached_at = excluded.cached_at,
|
|
76
|
+
ttl = excluded.ttl
|
|
77
|
+
`)
|
|
78
|
+
.run({
|
|
79
|
+
key,
|
|
80
|
+
data: JSON.stringify(value),
|
|
81
|
+
cached_at: cachedAt,
|
|
82
|
+
ttl: ttlSeconds,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
delete(key) {
|
|
86
|
+
this.db.prepare("DELETE FROM cache WHERE key = ?").run(key);
|
|
87
|
+
}
|
|
88
|
+
clear() {
|
|
89
|
+
const result = this.db.prepare("DELETE FROM cache").run();
|
|
90
|
+
return result.changes;
|
|
91
|
+
}
|
|
92
|
+
stats() {
|
|
93
|
+
const now = Math.floor(Date.now() / 1000);
|
|
94
|
+
const entries = this.db.prepare("SELECT COUNT(*) as count FROM cache").get();
|
|
95
|
+
const expiredEntries = this.db
|
|
96
|
+
.prepare("SELECT COUNT(*) as count FROM cache WHERE cached_at + ttl < ?")
|
|
97
|
+
.get(now);
|
|
98
|
+
return {
|
|
99
|
+
entries: entries.count,
|
|
100
|
+
expiredEntries: expiredEntries.count,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
close() {
|
|
104
|
+
this.db.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let cacheInstance = null;
|
|
108
|
+
export function getCache() {
|
|
109
|
+
if (!cacheInstance) {
|
|
110
|
+
cacheInstance = new GarminCache();
|
|
111
|
+
}
|
|
112
|
+
return cacheInstance;
|
|
113
|
+
}
|
|
114
|
+
export function closeCache() {
|
|
115
|
+
if (cacheInstance) {
|
|
116
|
+
cacheInstance.close();
|
|
117
|
+
cacheInstance = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function buildToolCacheKey(tool, params) {
|
|
121
|
+
return getCache().buildKey(tool, params);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A thrown fetcher caches nothing -- that has always been true and is the point
|
|
125
|
+
* of the `await` sitting outside `cache.set`. What was not true is that a
|
|
126
|
+
* fetcher which swallowed its own failures and returned an empty result got the
|
|
127
|
+
* full success TTL, so one rate-limited minute froze "no data" in for two hours.
|
|
128
|
+
* A caller that can tell the difference now says so through `isPartial`.
|
|
129
|
+
*/
|
|
130
|
+
export async function withCache(key, ttlSeconds, fetcher, options = {}) {
|
|
131
|
+
const cache = getCache();
|
|
132
|
+
const cached = cache.get(key);
|
|
133
|
+
if (cached !== null) {
|
|
134
|
+
return cached;
|
|
135
|
+
}
|
|
136
|
+
const fresh = await fetcher();
|
|
137
|
+
const partial = options.isPartial?.(fresh) ?? false;
|
|
138
|
+
cache.set(key, fresh, partial ? (options.partialTtlSeconds ?? PARTIAL_CACHE_TTL_SECONDS) : ttlSeconds);
|
|
139
|
+
return fresh;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/garmin/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GarminConnectInstance } from "./garminConnect.js";
|
|
2
|
+
/** Injectable for tests; production always authenticates for real. */
|
|
3
|
+
export type Authenticator = (force: boolean) => Promise<GarminConnectInstance>;
|
|
4
|
+
export declare function getGarminClient(forceAuth?: boolean, authenticator?: Authenticator): Promise<GarminConnectInstance>;
|
|
5
|
+
/**
|
|
6
|
+
* Forget the session. NOT the rate limit.
|
|
7
|
+
*
|
|
8
|
+
* These were one function, and the retry path in `withGarminClient` called it
|
|
9
|
+
* -- so the single code path reached by exactly the failure the cooldown exists
|
|
10
|
+
* for was the one that deleted the cooldown, and then attempted a fresh login
|
|
11
|
+
* into the live block. The doubling could never escalate past its 60-second
|
|
12
|
+
* floor either, because every failure reset the ladder to the bottom rung.
|
|
13
|
+
*
|
|
14
|
+
* Two different jobs had been sharing one name: `trainbud auth` wants both
|
|
15
|
+
* forgotten, since the user has just supplied new credentials and is entitled
|
|
16
|
+
* to one attempt. A failed request wants only the session forgotten.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resetGarminSession(): void;
|
|
19
|
+
/** Forget the session AND the rate limit. What `trainbud auth` asks for. */
|
|
20
|
+
export declare function resetGarminClient(): void;
|
|
21
|
+
export declare function withGarminClient<T>(operation: (client: GarminConnectInstance) => Promise<T>, authenticator?: Authenticator): Promise<T>;
|
|
22
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/garmin/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAyFhE,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAI/E,wBAAsB,eAAe,CACnC,SAAS,UAAQ,EACjB,aAAa,GAAE,aAAoC,GAClD,OAAO,CAAC,qBAAqB,CAAC,CAwChC;AAuDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC;AA+CD,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,SAAS,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,CAAC,CAAC,EACxD,aAAa,GAAE,aAAoC,GAClD,OAAO,CAAC,CAAC,CAAC,CA4BZ"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { authenticateGarmin } from "./auth.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { GarminApiError } from "./types.js";
|
|
4
|
+
import { deleteSetting, getSetting, setSetting } from "../appDb.js";
|
|
5
|
+
// SECTION: Garmin Client Singleton
|
|
6
|
+
let clientInstance = null;
|
|
7
|
+
let clientPromise = null;
|
|
8
|
+
/**
|
|
9
|
+
* When a rate-limited login stops being retried.
|
|
10
|
+
*
|
|
11
|
+
* Garmin's SSO sits behind Cloudflare, which answers a burst of logins with a
|
|
12
|
+
* 429 that says, in the body, "wait at least 30 seconds, then retry with
|
|
13
|
+
* exponential backoff". Nothing here read that. The in-flight login promise is
|
|
14
|
+
* deliberately cleared on failure -- so that fixing a bad password does not
|
|
15
|
+
* require a restart -- and the consequence was that every subsequent caller
|
|
16
|
+
* started a fresh login of its own. `trainbud check` walks nine tools in
|
|
17
|
+
* sequence, so one expired session became nine logins into a rate limit in
|
|
18
|
+
* about five seconds, each one deepening it, each one dumping a kilobyte of
|
|
19
|
+
* Cloudflare JSON into the log. The command that exists to tell you what is
|
|
20
|
+
* wrong was reliably causing it.
|
|
21
|
+
*
|
|
22
|
+
* One shared cooldown, honoured before any login is attempted. Failing fast
|
|
23
|
+
* with the reason is both faster and kinder to the upstream than nine identical
|
|
24
|
+
* failures.
|
|
25
|
+
*/
|
|
26
|
+
const COOLDOWN_UNTIL_KEY = "garmin_auth_blocked_until";
|
|
27
|
+
const COOLDOWN_STEP_KEY = "garmin_auth_backoff_seconds";
|
|
28
|
+
/** Cloudflare's own floor for this limit, from the 1015 body. */
|
|
29
|
+
const MIN_COOLDOWN_SECONDS = 60;
|
|
30
|
+
/** Past this, waiting longer is not the problem and something else is wrong. */
|
|
31
|
+
const MAX_COOLDOWN_SECONDS = 30 * 60;
|
|
32
|
+
/**
|
|
33
|
+
* The cooldown has to outlive the process.
|
|
34
|
+
*
|
|
35
|
+
* It was a module variable, which protects a single `trainbud serve` and does
|
|
36
|
+
* nothing at all for the CLI: every `trainbud backfill` is a new process, so it
|
|
37
|
+
* started with no memory of the limit, attempted a fresh login, and was refused
|
|
38
|
+
* again. Running the command a few times in a row is the single most natural
|
|
39
|
+
* thing for a user to do when it fails, and it was the exact behaviour that kept
|
|
40
|
+
* Garmin's login limit alive. Observed doing precisely that.
|
|
41
|
+
*
|
|
42
|
+
* Stored in the settings table, which every entry point already opens.
|
|
43
|
+
*
|
|
44
|
+
* The window doubles each time the limit is hit again, because that is what the
|
|
45
|
+
* upstream asks for in the 1015 body: "wait at least 30 seconds, then retry with
|
|
46
|
+
* exponential backoff". A fixed 60 seconds ignores an escalating block and walks
|
|
47
|
+
* straight back into it.
|
|
48
|
+
*/
|
|
49
|
+
function readCooldownUntil() {
|
|
50
|
+
const stored = getSetting(COOLDOWN_UNTIL_KEY);
|
|
51
|
+
if (!stored) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const value = Number.parseInt(stored, 10);
|
|
55
|
+
return Number.isFinite(value) ? value : null;
|
|
56
|
+
}
|
|
57
|
+
function remainingCooldownSeconds() {
|
|
58
|
+
const until = readCooldownUntil();
|
|
59
|
+
if (until === null) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
return Math.max(0, Math.ceil((until - Date.now()) / 1000));
|
|
63
|
+
}
|
|
64
|
+
function startCooldown(upstreamRetryAfter) {
|
|
65
|
+
const previous = Number.parseInt(getSetting(COOLDOWN_STEP_KEY) ?? "", 10);
|
|
66
|
+
const base = Number.isFinite(previous) && previous > 0 ? previous * 2 : MIN_COOLDOWN_SECONDS;
|
|
67
|
+
const seconds = Math.min(MAX_COOLDOWN_SECONDS, Math.max(base, upstreamRetryAfter ?? 0, MIN_COOLDOWN_SECONDS));
|
|
68
|
+
setSetting(COOLDOWN_UNTIL_KEY, String(Date.now() + seconds * 1000));
|
|
69
|
+
setSetting(COOLDOWN_STEP_KEY, String(seconds));
|
|
70
|
+
return seconds;
|
|
71
|
+
}
|
|
72
|
+
function clearCooldown() {
|
|
73
|
+
deleteSetting(COOLDOWN_UNTIL_KEY);
|
|
74
|
+
deleteSetting(COOLDOWN_STEP_KEY);
|
|
75
|
+
}
|
|
76
|
+
const defaultAuthenticator = (force) => authenticateGarmin(force);
|
|
77
|
+
export async function getGarminClient(forceAuth = false, authenticator = defaultAuthenticator) {
|
|
78
|
+
// An existing session is still usable while the login endpoint is cooling
|
|
79
|
+
// down: the cooldown is about signing in again, not about the API.
|
|
80
|
+
if (clientInstance && !forceAuth) {
|
|
81
|
+
return clientInstance;
|
|
82
|
+
}
|
|
83
|
+
const cooldown = remainingCooldownSeconds();
|
|
84
|
+
if (cooldown > 0) {
|
|
85
|
+
throw new GarminApiError(`Garmin is rate limiting sign-in. ${cooldown}s left to wait — every attempt before then extends it.`, 429, cooldown);
|
|
86
|
+
}
|
|
87
|
+
if (forceAuth) {
|
|
88
|
+
clientInstance = await trackAuthRateLimit(() => authenticator(true));
|
|
89
|
+
clientPromise = Promise.resolve(clientInstance);
|
|
90
|
+
return clientInstance;
|
|
91
|
+
}
|
|
92
|
+
if (!clientPromise) {
|
|
93
|
+
// The in-flight promise is memoised so concurrent callers share one login.
|
|
94
|
+
// It used to be memoised on failure too: a single bad password poisoned
|
|
95
|
+
// every later call with the same rejected promise, so fixing the
|
|
96
|
+
// credentials changed nothing until the process was restarted. Clear it on
|
|
97
|
+
// rejection and let the next caller try again.
|
|
98
|
+
clientPromise = trackAuthRateLimit(() => authenticator(false))
|
|
99
|
+
.then((client) => {
|
|
100
|
+
clientInstance = client;
|
|
101
|
+
return client;
|
|
102
|
+
})
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
clientPromise = null;
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return clientPromise;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs a login and records a cooldown if the upstream rate limited it.
|
|
112
|
+
*
|
|
113
|
+
* The window comes from the error itself where the upstream supplied one, so a
|
|
114
|
+
* server that asks for longer gets longer. Cleared on success, so a login that
|
|
115
|
+
* works immediately after the window costs nothing.
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* Runs a login without letting the upstream library print to the console.
|
|
119
|
+
*
|
|
120
|
+
* garmin-connect's handleHttpError does `console.error(msg)` and then
|
|
121
|
+
* `throw new Error(msg)` with the same string, so the kilobyte of Cloudflare
|
|
122
|
+
* 1015 JSON a user sees is a duplicate of an error we already catch and render
|
|
123
|
+
* as one actionable line. Printed first and unconditionally, it buried that line
|
|
124
|
+
* and made a "wait 60 seconds" look like a crash -- and the natural response to
|
|
125
|
+
* a crash is to run the command again, which extends the block.
|
|
126
|
+
*
|
|
127
|
+
* Kept at debug level rather than dropped: it is still the upstream's own
|
|
128
|
+
* account of what happened, and LOG_LEVEL=debug brings it back.
|
|
129
|
+
*/
|
|
130
|
+
async function withQuietUpstreamErrors(fn) {
|
|
131
|
+
const original = console.error;
|
|
132
|
+
console.error = (...args) => {
|
|
133
|
+
logger.debug({ upstream: args.map(String).join(" ") }, "garmin-connect console output");
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
return await fn();
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
console.error = original;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function trackAuthRateLimit(login) {
|
|
143
|
+
try {
|
|
144
|
+
const client = await withQuietUpstreamErrors(login);
|
|
145
|
+
clearCooldown();
|
|
146
|
+
return client;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const rateLimited = toRateLimitError(error);
|
|
150
|
+
if (rateLimited) {
|
|
151
|
+
const seconds = startCooldown(rateLimited.retryAfterSeconds ?? null);
|
|
152
|
+
throw new GarminApiError(`Garmin is rate limiting sign-in. Waiting ${seconds}s before the next attempt; retrying sooner makes it last longer.`, 429, seconds);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Forget the session. NOT the rate limit.
|
|
159
|
+
*
|
|
160
|
+
* These were one function, and the retry path in `withGarminClient` called it
|
|
161
|
+
* -- so the single code path reached by exactly the failure the cooldown exists
|
|
162
|
+
* for was the one that deleted the cooldown, and then attempted a fresh login
|
|
163
|
+
* into the live block. The doubling could never escalate past its 60-second
|
|
164
|
+
* floor either, because every failure reset the ladder to the bottom rung.
|
|
165
|
+
*
|
|
166
|
+
* Two different jobs had been sharing one name: `trainbud auth` wants both
|
|
167
|
+
* forgotten, since the user has just supplied new credentials and is entitled
|
|
168
|
+
* to one attempt. A failed request wants only the session forgotten.
|
|
169
|
+
*/
|
|
170
|
+
export function resetGarminSession() {
|
|
171
|
+
clientInstance = null;
|
|
172
|
+
clientPromise = null;
|
|
173
|
+
}
|
|
174
|
+
/** Forget the session AND the rate limit. What `trainbud auth` asks for. */
|
|
175
|
+
export function resetGarminClient() {
|
|
176
|
+
resetGarminSession();
|
|
177
|
+
clearCooldown();
|
|
178
|
+
}
|
|
179
|
+
function isAuthError(error) {
|
|
180
|
+
if (!(error instanceof Error)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const message = error.message.toLowerCase();
|
|
184
|
+
return (message.includes("401") ||
|
|
185
|
+
message.includes("403") ||
|
|
186
|
+
message.includes("unauthorized") ||
|
|
187
|
+
message.includes("token") ||
|
|
188
|
+
message.includes("login failed"));
|
|
189
|
+
}
|
|
190
|
+
function toRateLimitError(error) {
|
|
191
|
+
if (!(error instanceof Error)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
// Already ours: it carries the real remaining cooldown, which is the number
|
|
195
|
+
// the user needs. Re-wrapping it replaced that with a hardcoded 60 seconds and
|
|
196
|
+
// told someone eight minutes into an escalated block to try again in one.
|
|
197
|
+
if (error instanceof GarminApiError && error.statusCode === 429) {
|
|
198
|
+
return error;
|
|
199
|
+
}
|
|
200
|
+
const message = error.message.toLowerCase();
|
|
201
|
+
if (!message.includes("429") && !message.includes("rate limit")) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
// Cloudflare puts the wait in the body it just sent us. Reading it beats
|
|
205
|
+
// guessing, and it is the difference between honouring an escalating block and
|
|
206
|
+
// walking back into it.
|
|
207
|
+
const retryAfter = /"retry_after"\s*:\s*(\d+)/.exec(error.message)?.[1];
|
|
208
|
+
const seconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60;
|
|
209
|
+
return new GarminApiError(`Garmin rate limit reached. Retry in about ${seconds} seconds.`, 429, Number.isFinite(seconds) ? seconds : 60);
|
|
210
|
+
}
|
|
211
|
+
export async function withGarminClient(operation, authenticator = defaultAuthenticator) {
|
|
212
|
+
try {
|
|
213
|
+
const client = await getGarminClient(false, authenticator);
|
|
214
|
+
return await operation(client);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const rateLimitError = toRateLimitError(error);
|
|
218
|
+
if (rateLimitError) {
|
|
219
|
+
throw rateLimitError;
|
|
220
|
+
}
|
|
221
|
+
if (!isAuthError(error)) {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
logger.warn({ error }, "Garmin request failed auth, retrying once");
|
|
225
|
+
// The session, not the cooldown. This used to call `resetGarminClient`,
|
|
226
|
+
// which deletes both -- so the retry erased a live rate-limit block and
|
|
227
|
+
// walked straight back into it, on the single path the block exists to
|
|
228
|
+
// guard. With the cooldown left in place, the `getGarminClient` call below
|
|
229
|
+
// refuses on its own: its cooldown check runs ahead of the `forceAuth`
|
|
230
|
+
// branch, so no extra guard is needed here and adding one would put the
|
|
231
|
+
// same decision in two places.
|
|
232
|
+
resetGarminSession();
|
|
233
|
+
const client = await getGarminClient(true, authenticator);
|
|
234
|
+
return await operation(client);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/garmin/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HeartRateData, IActivity, SleepData, WeightDataResponse } from "./garminApiTypes.js";
|
|
2
|
+
import type { GarminConnectInstance } from "./garminConnect.js";
|
|
3
|
+
import { type DailyStressSummary, type Vo2MaxEntry } from "./rawApi.js";
|
|
4
|
+
import type { ActivitySummary, BodyCompositionEntry, HeartRateDaySummary, SleepNightSummary } from "./types.js";
|
|
5
|
+
export interface DailyFetch<T> {
|
|
6
|
+
raw: unknown;
|
|
7
|
+
mapped: T | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function mapSleepData(date: Date, sleepData: SleepData): SleepNightSummary | null;
|
|
10
|
+
export declare function mapHeartRateData(date: Date, heartRate: HeartRateData | null): HeartRateDaySummary | null;
|
|
11
|
+
/**
|
|
12
|
+
* Connect omits these for activities they do not apply to -- strength training
|
|
13
|
+
* reports no distance or pace, an indoor ride no elevation. They were once
|
|
14
|
+
* typed as plain numbers, which type-checked while crashing on
|
|
15
|
+
* `undefined.toFixed`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mapActivity(activity: IActivity): ActivitySummary;
|
|
18
|
+
export declare function mapWeightData(date: Date, weightData: WeightDataResponse): BodyCompositionEntry[];
|
|
19
|
+
export declare function fetchSleepDay(client: GarminConnectInstance, date: Date): Promise<DailyFetch<SleepNightSummary>>;
|
|
20
|
+
export declare function fetchHeartRateDay(client: GarminConnectInstance, date: Date): Promise<DailyFetch<HeartRateDaySummary>>;
|
|
21
|
+
export declare function fetchStressDay(client: GarminConnectInstance, date: Date): Promise<DailyFetch<DailyStressSummary>>;
|
|
22
|
+
export declare function fetchVo2MaxDay(client: GarminConnectInstance, date: Date): Promise<DailyFetch<Vo2MaxEntry>>;
|
|
23
|
+
export declare function fetchBodyCompositionDay(client: GarminConnectInstance, date: Date): Promise<DailyFetch<BodyCompositionEntry[]>>;
|
|
24
|
+
//# sourceMappingURL=daily.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daily.d.ts","sourceRoot":"","sources":["../../src/garmin/daily.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,SAAS,EACT,kBAAkB,EACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAKL,KAAK,kBAAkB,EACvB,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAepB,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;CAClB;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,iBAAiB,GAAG,IAAI,CAmBvF;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,aAAa,GAAG,IAAI,GAC9B,mBAAmB,GAAG,IAAI,CAyB5B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,eAAe,CAchE;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,kBAAkB,GAC7B,oBAAoB,EAAE,CAQxB;AAID,wBAAsB,aAAa,CACjC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAGxC;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC,CAG1C;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAGzC;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAGlC;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,UAAU,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAI7C"}
|