@codesam/healthsync-cli 1.0.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/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # HealthSync CLI
2
+
3
+ HealthSync CLI lets a signed-in iOS user authorize one macOS or Linux terminal
4
+ to read that user's complete HealthSync history.
5
+
6
+ ```bash
7
+ npm install -g @healthsync/cli
8
+ healthsync login
9
+ healthsync auth status
10
+ healthsync health get
11
+ healthsync health get --from 2026-07-01 --to 2026-07-18
12
+ healthsync health export --all --output ./health-data
13
+ healthsync logout
14
+ ```
15
+
16
+ `health get` returns JSON on stdout and defaults to today's data. The terminal
17
+ authorization expires after seven days without any successful authenticated
18
+ CLI request. `logout` revokes the terminal server-side and then removes the
19
+ local credential.
20
+
21
+ On macOS the device credential is stored in Keychain. On Linux it is stored at
22
+ `~/.config/healthsync/credentials.json` with mode `0600`.
23
+
24
+ The production server defaults to `https://xiaoyan.life`. Set
25
+ `HEALTHSYNC_SERVER_URL` or pass `--server` to `login` for development.
package/dist/api.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { cliTokenSignaturePayload } from './crypto.js';
2
+ import type { AuthStatusResponse, HealthDataResponse, LoginExchangeResponse, LoginRequestResponse, StoredCredentials } from './types.js';
3
+ export declare class ApiError extends Error {
4
+ readonly status: number;
5
+ readonly code: string;
6
+ constructor(status: number, code: string, message: string);
7
+ }
8
+ export declare class NetworkError extends Error {
9
+ readonly cause?: unknown | undefined;
10
+ constructor(message: string, cause?: unknown | undefined);
11
+ }
12
+ export declare function isTerminalAuthError(error: unknown): error is ApiError;
13
+ export declare class HealthSyncApi {
14
+ readonly serverUrl: string;
15
+ constructor(serverUrl: string);
16
+ private request;
17
+ createLoginRequest(input: {
18
+ deviceName: string;
19
+ platform: 'macos' | 'linux';
20
+ publicKey: string;
21
+ verifierChallenge: string;
22
+ }): Promise<LoginRequestResponse>;
23
+ exchangeLoginRequest(requestId: string, verifier: string): Promise<LoginExchangeResponse>;
24
+ private createAccessToken;
25
+ authStatus(credentials: StoredCredentials): Promise<AuthStatusResponse>;
26
+ logout(credentials: StoredCredentials): Promise<void>;
27
+ getHealth(credentials: StoredCredentials, query: URLSearchParams): Promise<HealthDataResponse>;
28
+ }
29
+ export { cliTokenSignaturePayload };
package/dist/api.js ADDED
@@ -0,0 +1,131 @@
1
+ import { cliTokenSignaturePayload, signTokenChallenge, } from './crypto.js';
2
+ export class ApiError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.name = 'ApiError';
10
+ }
11
+ }
12
+ export class NetworkError extends Error {
13
+ cause;
14
+ constructor(message, cause) {
15
+ super(message);
16
+ this.cause = cause;
17
+ this.name = 'NetworkError';
18
+ }
19
+ }
20
+ const TERMINAL_AUTH_ERROR_CODES = new Set([
21
+ 'ACCOUNT_DISABLED',
22
+ 'CLI_NOT_AUTHENTICATED',
23
+ 'DEVICE_AUTH_EXPIRED',
24
+ 'DEVICE_NOT_FOUND',
25
+ 'DEVICE_REVOKED',
26
+ 'INVALID_CLI_TOKEN',
27
+ 'INVALID_DEVICE_SIGNATURE',
28
+ 'INVALID_TOKEN_CHALLENGE',
29
+ ]);
30
+ export function isTerminalAuthError(error) {
31
+ return error instanceof ApiError && TERMINAL_AUTH_ERROR_CODES.has(error.code);
32
+ }
33
+ function normalizeServerUrl(value) {
34
+ return value.replace(/\/+$/, '');
35
+ }
36
+ export class HealthSyncApi {
37
+ serverUrl;
38
+ constructor(serverUrl) {
39
+ this.serverUrl = normalizeServerUrl(serverUrl);
40
+ }
41
+ async request(path, init = {}, accessToken) {
42
+ const headers = new Headers(init.headers);
43
+ headers.set('Accept', 'application/json');
44
+ if (init.body) {
45
+ headers.set('Content-Type', 'application/json');
46
+ }
47
+ if (accessToken) {
48
+ headers.set('Authorization', `Bearer ${accessToken}`);
49
+ }
50
+ let response;
51
+ try {
52
+ response = await fetch(`${this.serverUrl}${path}`, {
53
+ ...init,
54
+ headers,
55
+ signal: AbortSignal.timeout(30_000),
56
+ });
57
+ }
58
+ catch (error) {
59
+ throw new NetworkError('Unable to reach the HealthSync server', error);
60
+ }
61
+ const text = await response.text();
62
+ let body = {};
63
+ if (text) {
64
+ try {
65
+ body = JSON.parse(text);
66
+ }
67
+ catch {
68
+ body = { error: text };
69
+ }
70
+ }
71
+ if (!response.ok) {
72
+ const errorBody = body;
73
+ throw new ApiError(response.status, errorBody.code ?? `HTTP_${response.status}`, errorBody.error ?? `HealthSync server returned HTTP ${response.status}`);
74
+ }
75
+ return body;
76
+ }
77
+ createLoginRequest(input) {
78
+ return this.request('/api/cli/auth/requests', {
79
+ method: 'POST',
80
+ body: JSON.stringify({
81
+ device_name: input.deviceName,
82
+ platform: input.platform,
83
+ public_key: input.publicKey,
84
+ verifier_challenge: input.verifierChallenge,
85
+ }),
86
+ });
87
+ }
88
+ exchangeLoginRequest(requestId, verifier) {
89
+ return this.request(`/api/cli/auth/requests/${encodeURIComponent(requestId)}/exchange`, {
90
+ method: 'POST',
91
+ body: JSON.stringify({ verifier }),
92
+ });
93
+ }
94
+ async createAccessToken(credentials) {
95
+ const challenge = await this.request('/api/cli/auth/token-challenges', {
96
+ method: 'POST',
97
+ body: JSON.stringify({ device_id: credentials.deviceId }),
98
+ });
99
+ const signature = signTokenChallenge({
100
+ privateKey: credentials.privateKey,
101
+ deviceId: credentials.deviceId,
102
+ challengeId: challenge.challenge_id,
103
+ nonce: challenge.nonce,
104
+ });
105
+ const token = await this.request('/api/cli/auth/token', {
106
+ method: 'POST',
107
+ body: JSON.stringify({
108
+ device_id: credentials.deviceId,
109
+ challenge_id: challenge.challenge_id,
110
+ nonce: challenge.nonce,
111
+ signature,
112
+ }),
113
+ });
114
+ return token.access_token;
115
+ }
116
+ async authStatus(credentials) {
117
+ const token = await this.createAccessToken(credentials);
118
+ return this.request('/api/cli/auth/status', {}, token);
119
+ }
120
+ async logout(credentials) {
121
+ const token = await this.createAccessToken(credentials);
122
+ await this.request('/api/cli/auth/logout', { method: 'POST' }, token);
123
+ }
124
+ async getHealth(credentials, query) {
125
+ const token = await this.createAccessToken(credentials);
126
+ const suffix = query.size > 0 ? `?${query.toString()}` : '';
127
+ return this.request(`/api/cli/health${suffix}`, {}, token);
128
+ }
129
+ }
130
+ export { cliTokenSignaturePayload };
131
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export declare const DEFAULT_SERVER_URL = "https://xiaoyan.life";
2
+ export declare class CommandError extends Error {
3
+ readonly code: string;
4
+ readonly exitCode: number;
5
+ constructor(code: string, message: string, exitCode?: number);
6
+ }
7
+ export interface ParsedOptions {
8
+ values: Map<string, string>;
9
+ flags: Set<string>;
10
+ }
11
+ export declare function parseOptions(args: string[]): ParsedOptions;
12
+ export declare function loginCommand(args: string[]): Promise<void>;
13
+ export declare function authStatusCommand(): Promise<void>;
14
+ export declare function logoutCommand(): Promise<void>;
15
+ export declare function healthGetCommand(args: string[]): Promise<void>;
16
+ export declare function healthExportCommand(args: string[]): Promise<void>;
17
+ export declare function helpText(): string;
@@ -0,0 +1,222 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import qrcode from 'qrcode-terminal';
5
+ import { HealthSyncApi, isTerminalAuthError, } from './api.js';
6
+ import { createTerminalKeyPair, createVerifier, sha256Hex, } from './crypto.js';
7
+ import { deleteCredentials, loadCredentials, saveCredentials, } from './credentials.js';
8
+ export const DEFAULT_SERVER_URL = 'https://xiaoyan.life';
9
+ export class CommandError extends Error {
10
+ code;
11
+ exitCode;
12
+ constructor(code, message, exitCode = 3) {
13
+ super(message);
14
+ this.code = code;
15
+ this.exitCode = exitCode;
16
+ this.name = 'CommandError';
17
+ }
18
+ }
19
+ export function parseOptions(args) {
20
+ const values = new Map();
21
+ const flags = new Set();
22
+ for (let index = 0; index < args.length; index += 1) {
23
+ const argument = args[index];
24
+ if (!argument.startsWith('--')) {
25
+ throw new CommandError('UNEXPECTED_ARGUMENT', `Unexpected argument: ${argument}`);
26
+ }
27
+ const key = argument.slice(2);
28
+ const next = args[index + 1];
29
+ if (!next || next.startsWith('--')) {
30
+ flags.add(key);
31
+ continue;
32
+ }
33
+ values.set(key, next);
34
+ index += 1;
35
+ }
36
+ return { values, flags };
37
+ }
38
+ function supportedPlatform() {
39
+ if (process.platform === 'darwin')
40
+ return 'macos';
41
+ if (process.platform === 'linux')
42
+ return 'linux';
43
+ throw new CommandError('UNSUPPORTED_PLATFORM', 'HealthSync CLI currently supports macOS and Linux only');
44
+ }
45
+ function sleep(milliseconds) {
46
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
47
+ }
48
+ function statusLine(message) {
49
+ process.stderr.write(`${message}\n`);
50
+ }
51
+ function requireCredentials() {
52
+ const credentials = loadCredentials();
53
+ if (!credentials) {
54
+ throw new CommandError('NOT_AUTHENTICATED', 'HealthSync CLI is not logged in. Run `healthsync login`.', 1);
55
+ }
56
+ return credentials;
57
+ }
58
+ export async function loginCommand(args) {
59
+ const options = parseOptions(args);
60
+ const platform = supportedPlatform();
61
+ const serverUrl = options.values.get('server')
62
+ ?? process.env.HEALTHSYNC_SERVER_URL
63
+ ?? DEFAULT_SERVER_URL;
64
+ const existing = loadCredentials();
65
+ const keyPair = existing && existing.serverUrl === serverUrl
66
+ ? { publicKey: existing.publicKey, privateKey: existing.privateKey }
67
+ : createTerminalKeyPair();
68
+ const deviceName = options.values.get('name') ?? os.hostname();
69
+ const verifier = createVerifier();
70
+ const api = new HealthSyncApi(serverUrl);
71
+ const request = await api.createLoginRequest({
72
+ deviceName,
73
+ platform,
74
+ publicKey: keyPair.publicKey,
75
+ verifierChallenge: sha256Hex(verifier),
76
+ });
77
+ statusLine('请打开 HealthSync App,点击首页右上角的扫码按钮:');
78
+ qrcode.generate(request.qr_payload, { small: true }, code => {
79
+ process.stdout.write(`${code}\n`);
80
+ });
81
+ statusLine(`二维码将在 ${new Date(request.expires_at).toLocaleTimeString()} 失效,正在等待授权…`);
82
+ const expiresAt = new Date(request.expires_at).getTime();
83
+ while (Date.now() < expiresAt) {
84
+ const result = await api.exchangeLoginRequest(request.request_id, verifier);
85
+ if (result.status === 'approved') {
86
+ saveCredentials({
87
+ version: 1,
88
+ serverUrl: api.serverUrl,
89
+ deviceId: result.device.id,
90
+ deviceName: result.device.name,
91
+ platform,
92
+ fingerprint: result.device.fingerprint,
93
+ publicKey: keyPair.publicKey,
94
+ privateKey: keyPair.privateKey,
95
+ authorizedAt: new Date().toISOString(),
96
+ user: result.user,
97
+ });
98
+ process.stdout.write(`${JSON.stringify({
99
+ authenticated: true,
100
+ user: result.user,
101
+ device: result.device,
102
+ }, null, 2)}\n`);
103
+ return;
104
+ }
105
+ await sleep(request.poll_interval_seconds * 1_000);
106
+ }
107
+ throw new CommandError('LOGIN_REQUEST_EXPIRED', 'The QR code expired. Run `healthsync login` to try again.', 1);
108
+ }
109
+ export async function authStatusCommand() {
110
+ const credentials = loadCredentials();
111
+ if (!credentials) {
112
+ process.stdout.write(`${JSON.stringify({
113
+ authenticated: false,
114
+ reason: 'not_logged_in',
115
+ }, null, 2)}\n`);
116
+ process.exitCode = 1;
117
+ return;
118
+ }
119
+ const api = new HealthSyncApi(credentials.serverUrl);
120
+ const status = await api.authStatus(credentials);
121
+ process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
122
+ }
123
+ export async function logoutCommand() {
124
+ const credentials = requireCredentials();
125
+ const api = new HealthSyncApi(credentials.serverUrl);
126
+ let alreadyInvalid = false;
127
+ try {
128
+ await api.logout(credentials);
129
+ }
130
+ catch (error) {
131
+ if (!isTerminalAuthError(error)) {
132
+ throw error;
133
+ }
134
+ alreadyInvalid = true;
135
+ }
136
+ deleteCredentials();
137
+ process.stdout.write(`${JSON.stringify({
138
+ authenticated: false,
139
+ revoked: true,
140
+ already_invalid: alreadyInvalid,
141
+ local_credentials_deleted: true,
142
+ }, null, 2)}\n`);
143
+ }
144
+ function healthQueryFromOptions(options) {
145
+ const date = options.values.get('date');
146
+ const from = options.values.get('from');
147
+ const to = options.values.get('to');
148
+ const all = options.flags.has('all');
149
+ const datePattern = /^\d{4}-\d{2}-\d{2}$/;
150
+ if (all && (date || from || to)) {
151
+ throw new CommandError('INVALID_DATE_RANGE', '--all cannot be combined with date options');
152
+ }
153
+ for (const [label, value] of [['date', date], ['from', from], ['to', to]]) {
154
+ if (value && !datePattern.test(value)) {
155
+ throw new CommandError('INVALID_DATE', `--${label} must use YYYY-MM-DD`);
156
+ }
157
+ }
158
+ if (date && (from || to)) {
159
+ throw new CommandError('INVALID_DATE_RANGE', '--date cannot be combined with --from or --to');
160
+ }
161
+ if (from && to && from > to) {
162
+ throw new CommandError('INVALID_DATE_RANGE', '--from must not be after --to');
163
+ }
164
+ const query = new URLSearchParams();
165
+ if (date)
166
+ query.set('date', date);
167
+ if (from)
168
+ query.set('from', from);
169
+ if (to)
170
+ query.set('to', to);
171
+ if (all)
172
+ query.set('all', 'true');
173
+ return query;
174
+ }
175
+ async function fetchHealth(options) {
176
+ const credentials = requireCredentials();
177
+ const api = new HealthSyncApi(credentials.serverUrl);
178
+ return api.getHealth(credentials, healthQueryFromOptions(options));
179
+ }
180
+ export async function healthGetCommand(args) {
181
+ const result = await fetchHealth(parseOptions(args));
182
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
183
+ }
184
+ export async function healthExportCommand(args) {
185
+ const options = parseOptions(args);
186
+ const output = options.values.get('output');
187
+ if (!output) {
188
+ throw new CommandError('OUTPUT_REQUIRED', 'health export requires --output <directory>');
189
+ }
190
+ const result = await fetchHealth(options);
191
+ const userDirectory = path.resolve(output, result.user.username);
192
+ fs.mkdirSync(userDirectory, { recursive: true, mode: 0o700 });
193
+ fs.chmodSync(userDirectory, 0o700);
194
+ for (const day of result.days) {
195
+ const filePath = path.join(userDirectory, `${day.date}.json`);
196
+ fs.writeFileSync(filePath, `${JSON.stringify(day, null, 2)}\n`, {
197
+ encoding: 'utf8',
198
+ mode: 0o600,
199
+ });
200
+ fs.chmodSync(filePath, 0o600);
201
+ }
202
+ process.stdout.write(`${JSON.stringify({
203
+ exported: result.count,
204
+ output: userDirectory,
205
+ user: result.user,
206
+ last_synced_at: result.last_synced_at,
207
+ }, null, 2)}\n`);
208
+ }
209
+ export function helpText() {
210
+ return `HealthSync CLI
211
+
212
+ Usage:
213
+ healthsync login [--name <terminal-name>] [--server <url>]
214
+ healthsync auth status
215
+ healthsync logout
216
+ healthsync health get [--date YYYY-MM-DD | --from YYYY-MM-DD --to YYYY-MM-DD | --all]
217
+ healthsync health export --output <directory> [date options]
218
+
219
+ Data commands write JSON to stdout. Diagnostics and login guidance use stderr.
220
+ `;
221
+ }
222
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CommandError, parseOptions } from './commands.js';
4
+ import { ApiError, isTerminalAuthError, } from './api.js';
5
+ import { cliTokenSignaturePayload, createTerminalKeyPair, signTokenChallenge, } from './crypto.js';
6
+ import { createPublicKey, verify } from 'node:crypto';
7
+ test('parseOptions separates flags from values', () => {
8
+ const options = parseOptions([
9
+ '--from', '2026-07-01',
10
+ '--to', '2026-07-18',
11
+ '--all',
12
+ ]);
13
+ assert.equal(options.values.get('from'), '2026-07-01');
14
+ assert.equal(options.values.get('to'), '2026-07-18');
15
+ assert.equal(options.flags.has('all'), true);
16
+ });
17
+ test('parseOptions rejects positional input', () => {
18
+ assert.throws(() => parseOptions(['today']), (error) => error instanceof CommandError && error.code === 'UNEXPECTED_ARGUMENT');
19
+ });
20
+ test('authorization failures are reported as logged-out state', () => {
21
+ assert.equal(isTerminalAuthError(new ApiError(401, 'DEVICE_AUTH_EXPIRED', 'expired')), true);
22
+ assert.equal(isTerminalAuthError(new ApiError(500, 'INTERNAL_ERROR', 'failed')), false);
23
+ });
24
+ test('terminal signature proves possession of the stored Ed25519 private key', () => {
25
+ const keyPair = createTerminalKeyPair();
26
+ const input = {
27
+ privateKey: keyPair.privateKey,
28
+ deviceId: 'device-id',
29
+ challengeId: 'challenge-id',
30
+ nonce: 'nonce',
31
+ };
32
+ const signature = signTokenChallenge(input);
33
+ const publicKey = createPublicKey({
34
+ key: Buffer.from(keyPair.publicKey, 'base64'),
35
+ format: 'der',
36
+ type: 'spki',
37
+ });
38
+ assert.equal(verify(null, Buffer.from(cliTokenSignaturePayload(input.deviceId, input.challengeId, input.nonce)), publicKey, Buffer.from(signature, 'base64')), true);
39
+ });
40
+ //# sourceMappingURL=commands.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.test.js","sourceRoot":"","sources":["../src/commands.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EACL,QAAQ,EACR,mBAAmB,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEtD,IAAI,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACpD,MAAM,OAAO,GAAG,YAAY,CAAC;QAC3B,QAAQ,EAAE,YAAY;QACtB,MAAM,EAAE,YAAY;QACpB,OAAO;KACR,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC;IACvD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IACrD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACjD,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,EAC7B,CAAC,KAAc,EAAE,EAAE,CAAC,KAAK,YAAY,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,qBAAqB,CAC1F,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACnE,MAAM,CAAC,KAAK,CACV,mBAAmB,CAAC,IAAI,QAAQ,CAAC,GAAG,EAAE,qBAAqB,EAAE,SAAS,CAAC,CAAC,EACxE,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CACV,mBAAmB,CAAC,IAAI,QAAQ,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,CAAC,CAAC,EAClE,KAAK,CACN,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wEAAwE,EAAE,GAAG,EAAE;IAClF,MAAM,OAAO,GAAG,qBAAqB,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG;QACZ,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,QAAQ,EAAE,WAAW;QACrB,WAAW,EAAE,cAAc;QAC3B,KAAK,EAAE,OAAO;KACf,CAAC;IACF,MAAM,SAAS,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;QAC7C,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,MAAM;KACb,CAAC,CAAC;IAEH,MAAM,CAAC,KAAK,CACV,MAAM,CACJ,IAAI,EACJ,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EACrF,SAAS,EACT,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CACjC,EACD,IAAI,CACL,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { StoredCredentials } from './types.js';
2
+ export declare function loadCredentials(): StoredCredentials | null;
3
+ export declare function saveCredentials(credentials: StoredCredentials): void;
4
+ export declare function deleteCredentials(): void;
@@ -0,0 +1,128 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ const KEYCHAIN_SERVICE = 'life.xiaoyan.healthsync.cli';
6
+ const CONFIG_DIRECTORY = path.join(os.homedir(), '.config', 'healthsync');
7
+ const CREDENTIALS_PATH = path.join(CONFIG_DIRECTORY, 'credentials.json');
8
+ function parseCredentials(raw) {
9
+ try {
10
+ const parsed = JSON.parse(raw);
11
+ if (parsed.version !== 1
12
+ || !parsed.deviceId
13
+ || !parsed.privateKey
14
+ || !parsed.publicKey
15
+ || !parsed.serverUrl
16
+ || !parsed.user?.id) {
17
+ return null;
18
+ }
19
+ return parsed;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function keychainAccount() {
26
+ return os.userInfo().username;
27
+ }
28
+ function loadFromMacKeychain() {
29
+ try {
30
+ const raw = execFileSync('security', [
31
+ 'find-generic-password',
32
+ '-a', keychainAccount(),
33
+ '-s', KEYCHAIN_SERVICE,
34
+ '-w',
35
+ ], {
36
+ encoding: 'utf8',
37
+ stdio: ['ignore', 'pipe', 'ignore'],
38
+ }).trim();
39
+ return parseCredentials(raw);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function saveToMacKeychain(credentials) {
46
+ execFileSync('security', [
47
+ 'add-generic-password',
48
+ '-U',
49
+ '-a', keychainAccount(),
50
+ '-s', KEYCHAIN_SERVICE,
51
+ '-w', JSON.stringify(credentials),
52
+ ], {
53
+ stdio: ['ignore', 'ignore', 'pipe'],
54
+ });
55
+ }
56
+ function deleteFromMacKeychain() {
57
+ try {
58
+ execFileSync('security', [
59
+ 'delete-generic-password',
60
+ '-a', keychainAccount(),
61
+ '-s', KEYCHAIN_SERVICE,
62
+ ], {
63
+ stdio: ['ignore', 'ignore', 'ignore'],
64
+ });
65
+ }
66
+ catch {
67
+ // Deleting an already absent credential is idempotent.
68
+ }
69
+ }
70
+ function loadFromLinuxFile() {
71
+ try {
72
+ return parseCredentials(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ function saveToLinuxFile(credentials) {
79
+ fs.mkdirSync(CONFIG_DIRECTORY, { recursive: true, mode: 0o700 });
80
+ fs.chmodSync(CONFIG_DIRECTORY, 0o700);
81
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), {
82
+ encoding: 'utf8',
83
+ mode: 0o600,
84
+ });
85
+ fs.chmodSync(CREDENTIALS_PATH, 0o600);
86
+ }
87
+ function deleteFromLinuxFile() {
88
+ try {
89
+ fs.unlinkSync(CREDENTIALS_PATH);
90
+ }
91
+ catch (error) {
92
+ if (error.code !== 'ENOENT') {
93
+ throw error;
94
+ }
95
+ }
96
+ }
97
+ export function loadCredentials() {
98
+ if (process.platform === 'darwin') {
99
+ return loadFromMacKeychain();
100
+ }
101
+ if (process.platform === 'linux') {
102
+ return loadFromLinuxFile();
103
+ }
104
+ return null;
105
+ }
106
+ export function saveCredentials(credentials) {
107
+ if (process.platform === 'darwin') {
108
+ saveToMacKeychain(credentials);
109
+ return;
110
+ }
111
+ if (process.platform === 'linux') {
112
+ saveToLinuxFile(credentials);
113
+ return;
114
+ }
115
+ throw new Error(`Unsupported platform: ${process.platform}`);
116
+ }
117
+ export function deleteCredentials() {
118
+ if (process.platform === 'darwin') {
119
+ deleteFromMacKeychain();
120
+ return;
121
+ }
122
+ if (process.platform === 'linux') {
123
+ deleteFromLinuxFile();
124
+ return;
125
+ }
126
+ throw new Error(`Unsupported platform: ${process.platform}`);
127
+ }
128
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export declare function sha256Hex(value: string | Buffer): string;
2
+ export declare function createVerifier(): string;
3
+ export declare function createTerminalKeyPair(): {
4
+ publicKey: string;
5
+ privateKey: string;
6
+ };
7
+ export declare function cliTokenSignaturePayload(deviceId: string, challengeId: string, nonce: string): string;
8
+ export declare function signTokenChallenge(input: {
9
+ privateKey: string;
10
+ deviceId: string;
11
+ challengeId: string;
12
+ nonce: string;
13
+ }): string;
package/dist/crypto.js ADDED
@@ -0,0 +1,26 @@
1
+ import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, sign, } from 'node:crypto';
2
+ export function sha256Hex(value) {
3
+ return createHash('sha256').update(value).digest('hex');
4
+ }
5
+ export function createVerifier() {
6
+ return randomBytes(32).toString('base64url');
7
+ }
8
+ export function createTerminalKeyPair() {
9
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
10
+ return {
11
+ publicKey: publicKey.export({ format: 'der', type: 'spki' }).toString('base64'),
12
+ privateKey: privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64'),
13
+ };
14
+ }
15
+ export function cliTokenSignaturePayload(deviceId, challengeId, nonce) {
16
+ return ['healthsync-cli-token', 'v1', deviceId, challengeId, nonce].join('\n');
17
+ }
18
+ export function signTokenChallenge(input) {
19
+ const privateKey = createPrivateKey({
20
+ key: Buffer.from(input.privateKey, 'base64'),
21
+ format: 'der',
22
+ type: 'pkcs8',
23
+ });
24
+ return sign(null, Buffer.from(cliTokenSignaturePayload(input.deviceId, input.challengeId, input.nonce)), privateKey).toString('base64');
25
+ }
26
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,IAAI,GACL,MAAM,aAAa,CAAC;AAErB,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,qBAAqB;IAInC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;IACjE,OAAO;QACL,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC/E,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;KACnF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,QAAgB,EAChB,WAAmB,EACnB,KAAa;IAEb,OAAO,CAAC,sBAAsB,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAKlC;IACC,MAAM,UAAU,GAAG,gBAAgB,CAAC;QAClC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,QAAQ,CAAC;QAC5C,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,OAAO;KACd,CAAC,CAAC;IACH,OAAO,IAAI,CACT,IAAI,EACJ,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EACrF,UAAU,CACX,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { ApiError, isTerminalAuthError, NetworkError, } from './api.js';
3
+ import { authStatusCommand, CommandError, healthExportCommand, healthGetCommand, helpText, loginCommand, logoutCommand, } from './commands.js';
4
+ async function main(args) {
5
+ const [command, subcommand, ...rest] = args;
6
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
7
+ process.stdout.write(helpText());
8
+ return;
9
+ }
10
+ if (command === 'login') {
11
+ await loginCommand([subcommand, ...rest].filter(Boolean));
12
+ return;
13
+ }
14
+ if (command === 'logout') {
15
+ if (subcommand) {
16
+ throw new CommandError('UNEXPECTED_ARGUMENT', `Unexpected argument: ${subcommand}`);
17
+ }
18
+ await logoutCommand();
19
+ return;
20
+ }
21
+ if ((command === 'auth' && subcommand === 'status') || command === 'status') {
22
+ await authStatusCommand();
23
+ return;
24
+ }
25
+ if (command === 'health' && subcommand === 'get') {
26
+ await healthGetCommand(rest);
27
+ return;
28
+ }
29
+ if (command === 'health' && subcommand === 'export') {
30
+ await healthExportCommand(rest);
31
+ return;
32
+ }
33
+ throw new CommandError('UNKNOWN_COMMAND', `Unknown command: ${[command, subcommand].filter(Boolean).join(' ')}`);
34
+ }
35
+ main(process.argv.slice(2)).catch(error => {
36
+ if (error instanceof ApiError) {
37
+ process.stderr.write(`${JSON.stringify({
38
+ error: {
39
+ code: error.code,
40
+ message: error.message,
41
+ },
42
+ authenticated: isTerminalAuthError(error) ? false : undefined,
43
+ })}\n`);
44
+ process.exitCode = isTerminalAuthError(error) ? 1 : 3;
45
+ return;
46
+ }
47
+ if (error instanceof NetworkError) {
48
+ process.stderr.write(`${JSON.stringify({
49
+ error: {
50
+ code: 'NETWORK_ERROR',
51
+ message: error.message,
52
+ },
53
+ authenticated: 'unknown',
54
+ })}\n`);
55
+ process.exitCode = 2;
56
+ return;
57
+ }
58
+ if (error instanceof CommandError) {
59
+ process.stderr.write(`${JSON.stringify({
60
+ error: {
61
+ code: error.code,
62
+ message: error.message,
63
+ },
64
+ })}\n`);
65
+ process.exitCode = error.exitCode;
66
+ return;
67
+ }
68
+ process.stderr.write(`${JSON.stringify({
69
+ error: {
70
+ code: 'INTERNAL_ERROR',
71
+ message: error instanceof Error ? error.message : 'Unknown error',
72
+ },
73
+ })}\n`);
74
+ process.exitCode = 3;
75
+ });
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ export type SupportedPlatform = 'macos' | 'linux';
2
+ export interface StoredCredentials {
3
+ version: 1;
4
+ serverUrl: string;
5
+ deviceId: string;
6
+ deviceName: string;
7
+ platform: SupportedPlatform;
8
+ fingerprint: string;
9
+ publicKey: string;
10
+ privateKey: string;
11
+ authorizedAt: string;
12
+ user: {
13
+ id: string;
14
+ username: string;
15
+ };
16
+ }
17
+ export interface LoginRequestResponse {
18
+ request_id: string;
19
+ qr_payload: string;
20
+ expires_at: string;
21
+ poll_interval_seconds: number;
22
+ }
23
+ export type LoginExchangeResponse = {
24
+ status: 'pending';
25
+ } | {
26
+ status: 'approved';
27
+ device: {
28
+ id: string;
29
+ name: string;
30
+ platform: SupportedPlatform;
31
+ fingerprint: string;
32
+ };
33
+ user: {
34
+ id: string;
35
+ username: string;
36
+ };
37
+ };
38
+ export interface TokenChallengeResponse {
39
+ challenge_id: string;
40
+ nonce: string;
41
+ expires_at: string;
42
+ }
43
+ export interface TokenResponse {
44
+ access_token: string;
45
+ token_type: 'Bearer';
46
+ expires_in: number;
47
+ }
48
+ export interface AuthStatusResponse {
49
+ authenticated: true;
50
+ user: {
51
+ id: string;
52
+ username: string;
53
+ };
54
+ device: {
55
+ id: string;
56
+ name: string;
57
+ platform: SupportedPlatform;
58
+ fingerprint: string;
59
+ expires_at: string;
60
+ };
61
+ }
62
+ export interface HealthDataResponse {
63
+ schema_version: number;
64
+ user: {
65
+ id: string;
66
+ username: string;
67
+ };
68
+ range: {
69
+ from: string | null;
70
+ to: string | null;
71
+ all: boolean;
72
+ };
73
+ last_synced_at: string | null;
74
+ count: number;
75
+ days: Array<Record<string, unknown> & {
76
+ date: string;
77
+ }>;
78
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@codesam/healthsync-cli",
3
+ "version": "1.0.0",
4
+ "description": "Device-authorized CLI for reading HealthSync records",
5
+ "type": "module",
6
+ "bin": {
7
+ "healthsync": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsx src/index.ts",
20
+ "test": "node --import tsx --test src/*.test.ts"
21
+ },
22
+ "dependencies": {
23
+ "qrcode-terminal": "^0.12.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.9.0",
27
+ "@types/qrcode-terminal": "^0.12.2",
28
+ "tsx": "^4.19.2",
29
+ "typescript": "^5.6.3"
30
+ }
31
+ }