@zykeco/sync-server 0.5.0 → 0.7.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 CHANGED
@@ -102,6 +102,76 @@ reply: { wiped: { dailyMetrics, weeklyMetrics, sleepSessions: number }, serverNo
102
102
 
103
103
  Deletes every row across the three sync namespaces (blobs untouched). The literal `confirm` string is enforced at the protocol layer.
104
104
 
105
+ ## MCP server
106
+
107
+ The server ships an [MCP](https://modelcontextprotocol.io) endpoint at `POST /mcp` that exposes read-only tools over your synced data. Auth supports two OAuth 2.1 flows:
108
+
109
+ - **`authorization_code` + PKCE** — for interactive clients like the Claude.ai web app and Claude Desktop "Custom Connectors". Dynamic Client Registration (RFC 7591) is supported.
110
+ - **`client_credentials`** — for headless agents / CLIs that hold the `READ_SECRET` directly.
111
+
112
+ ### Required env vars
113
+
114
+ | Var | Default | Purpose |
115
+ | ----------------------- | ------- | -------------------------------------------------------------------------------------------------- |
116
+ | `MCP_ENABLED` | `true` | Set `false` to disable the `/mcp` mount entirely. |
117
+ | `MCP_CLIENT_ID` | _req._ | Bootstrap client ID for the headless `client_credentials` grant. Public — pick anything memorable. |
118
+ | `MCP_TOKEN_SECRET` | _req._ | HMAC key for signing bearer tokens. ≥ 32 random bytes. Treat as a secret. |
119
+ | `MCP_TOKEN_TTL_SECONDS` | `3600` | Bearer token lifetime. |
120
+
121
+ ### Endpoints
122
+
123
+ | Method | Path | Purpose |
124
+ | ------ | ----------------------------------------- | ------------------------------------------------------- |
125
+ | GET | `/.well-known/oauth-authorization-server` | RFC 8414 metadata (root-level, what MCP clients probe). |
126
+ | GET | `/.well-known/oauth-protected-resource` | RFC 9728 resource metadata pointing back to the AS. |
127
+ | POST | `/mcp/register` | Dynamic Client Registration (RFC 7591). |
128
+ | GET | `/mcp/authorize` | Consent screen (PKCE flow). |
129
+ | POST | `/mcp/authorize` | Consent form submit — issues an authorization code. |
130
+ | POST | `/mcp/oauth/token` | Token endpoint (both grants). |
131
+ | POST | `/mcp` | MCP JSON-RPC. Requires `Authorization: Bearer <token>`. |
132
+
133
+ ### Connecting Claude.ai (web or desktop) as a custom connector
134
+
135
+ 1. **Deploy your server with `MCP_*` env vars set** and confirm it answers on a public HTTPS URL. From a browser, `https://YOUR_HOST/.well-known/oauth-authorization-server` should return JSON. If you self-host behind a proxy, make sure that path is forwarded to the sync server.
136
+ 2. **Open Claude.ai → Settings → Connectors → Add custom connector.**
137
+ 3. **Server URL:** enter `https://YOUR_HOST/mcp` (the `/mcp` path matters — that's the JSON-RPC endpoint, not the host root).
138
+ 4. **Click Connect.** Claude.ai will:
139
+ - Fetch the discovery doc at `/.well-known/oauth-authorization-server`.
140
+ - Self-register via `POST /mcp/register`.
141
+ - Open a new tab to `https://YOUR_HOST/mcp/authorize?...`.
142
+ 5. **On the consent screen:** verify the "Redirects to" line shows `https://claude.ai/api/mcp/auth_callback`. Paste your `READ_SECRET` into the form and click **Authorize**.
143
+ 6. **You're done.** Claude.ai redirects back, exchanges the code for a bearer token, and the connector goes online. The eight read tools (`list_daily_metrics`, `list_sleep_sessions`, `get_user_profile`, …) become available in the chat tool picker.
144
+
145
+ Claude Desktop's "Add custom connector" flow is identical — same form, same URL.
146
+
147
+ #### What if Claude.ai shows a 404 at `/authorize`?
148
+
149
+ That means it's hitting the host root instead of the discovered path. Two causes:
150
+
151
+ - The discovery doc isn't reachable. Curl `https://YOUR_HOST/.well-known/oauth-authorization-server` and confirm you get JSON, not your reverse proxy's 404 page. Add the path to your proxy rules.
152
+ - You're on `@zykeco/sync-server < 0.7.0`. Upgrade — the auth-code flow ships from 0.7.0 onward.
153
+
154
+ #### Token / client lifetime
155
+
156
+ - Bearer tokens expire after `MCP_TOKEN_TTL_SECONDS` (default 1 h). Claude.ai re-runs the consent flow when expired.
157
+ - DCR client registrations are **in-memory** — they don't survive a server restart. If you restart the server, remove the connector in Claude.ai and re-add it; takes ten seconds.
158
+
159
+ ### Connecting from headless agents (`client_credentials`)
160
+
161
+ ```bash
162
+ # 1. Get a token using your READ_SECRET as the client secret.
163
+ curl -X POST https://YOUR_HOST/mcp/oauth/token \
164
+ -d grant_type=client_credentials \
165
+ -d client_id=$MCP_CLIENT_ID \
166
+ -d client_secret=$READ_SECRET
167
+
168
+ # 2. Use the access_token to call MCP.
169
+ curl -X POST https://YOUR_HOST/mcp \
170
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
171
+ -H "content-type: application/json" \
172
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
173
+ ```
174
+
105
175
  ## Data model
106
176
 
107
177
  - **`daily_metrics`**, **`weekly_metrics`** — `{ id (PK), isoDate / weekStartIsoDate, metricKey, value, createdAt, updatedAt }`. Composite unique on `(date, metricKey)`.
package/dist/app.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import type { Db } from './db/client.js';
3
+ import type { Env } from './env.js';
4
+ export declare function buildApp(db: Db, env: Env): Hono;
package/dist/app.js ADDED
@@ -0,0 +1,58 @@
1
+ import { Hono } from 'hono';
2
+ import { dailyMetricsRoutes } from './routes/daily-metrics.js';
3
+ import { dailyStressBurdenRoutes } from './routes/daily-stress-burden.js';
4
+ import { healthRoutes } from './routes/health.js';
5
+ import { heartRateMinuteRoutes } from './routes/heart-rate-minute.js';
6
+ import { hrZoneHistoryRoutes } from './routes/hr-zone-history.js';
7
+ import { mcpRoutes } from './routes/mcp.js';
8
+ import { sleepSessionsRoutes } from './routes/sleep-sessions.js';
9
+ import { userProfileRoutes } from './routes/user-profile.js';
10
+ import { userTimezonesRoutes } from './routes/user-timezones.js';
11
+ import { weeklyMetricsRoutes } from './routes/weekly-metrics.js';
12
+ import { wipeRoutes } from './routes/wipe.js';
13
+ import { createDailyMetricsStore } from './stores/daily-metrics.js';
14
+ import { createDailyStressBurdenStore } from './stores/daily-stress-burden.js';
15
+ import { createHeartRateMinuteStore } from './stores/heart-rate-minute.js';
16
+ import { createHrZoneHistoryStore } from './stores/hr-zone-history.js';
17
+ import { createSleepSessionsStore } from './stores/sleep-sessions.js';
18
+ import { createUserProfileStore } from './stores/user-profile.js';
19
+ import { createUserTimezonesStore } from './stores/user-timezones.js';
20
+ import { createWeeklyMetricsStore } from './stores/weekly-metrics.js';
21
+ export function buildApp(db, env) {
22
+ const auth = { readSecret: env.readSecret, writeSecret: env.writeSecret };
23
+ const app = new Hono();
24
+ app.route('/v1/health', healthRoutes(db));
25
+ const dailyMetricsStore = createDailyMetricsStore(db);
26
+ const weeklyMetricsStore = createWeeklyMetricsStore(db);
27
+ const sleepSessionsStore = createSleepSessionsStore(db);
28
+ const userTimezonesStore = createUserTimezonesStore(db);
29
+ const userProfileStore = createUserProfileStore(db);
30
+ const dailyStressBurdenStore = createDailyStressBurdenStore(db);
31
+ const heartRateMinuteStore = createHeartRateMinuteStore(db);
32
+ const hrZoneHistoryStore = createHrZoneHistoryStore(db);
33
+ app.route('/v1/sync/daily-metrics', dailyMetricsRoutes(dailyMetricsStore, auth));
34
+ app.route('/v1/sync/weekly-metrics', weeklyMetricsRoutes(weeklyMetricsStore, auth));
35
+ app.route('/v1/sync/sleep-sessions', sleepSessionsRoutes(sleepSessionsStore, auth));
36
+ app.route('/v1/sync/user-timezones', userTimezonesRoutes(userTimezonesStore, auth));
37
+ app.route('/v1/sync/user-profile', userProfileRoutes(userProfileStore, auth));
38
+ app.route('/v1/sync/daily-stress-burden', dailyStressBurdenRoutes(dailyStressBurdenStore, auth));
39
+ app.route('/v1/sync/heart-rate-minute', heartRateMinuteRoutes(heartRateMinuteStore, auth));
40
+ app.route('/v1/sync/hr-zone-history', hrZoneHistoryRoutes(hrZoneHistoryStore, auth));
41
+ app.route('/v1/sync/wipe', wipeRoutes({
42
+ dailyMetrics: dailyMetricsStore,
43
+ weeklyMetrics: weeklyMetricsStore,
44
+ sleepSessions: sleepSessionsStore,
45
+ userTimezones: userTimezonesStore,
46
+ userProfile: userProfileStore,
47
+ dailyStressBurden: dailyStressBurdenStore,
48
+ heartRateMinute: heartRateMinuteStore,
49
+ hrZoneHistory: hrZoneHistoryStore,
50
+ }, auth));
51
+ if (env.mcp) {
52
+ const { mcp, discovery } = mcpRoutes({ db, env, mcp: env.mcp });
53
+ app.route('/mcp', mcp);
54
+ app.route('/', discovery);
55
+ }
56
+ return app;
57
+ }
58
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
package/dist/env.d.ts CHANGED
@@ -4,5 +4,11 @@ export interface Env {
4
4
  databaseUrl: string;
5
5
  readSecret: string;
6
6
  writeSecret: string;
7
+ mcp: McpEnv | null;
8
+ }
9
+ export interface McpEnv {
10
+ clientId: string;
11
+ tokenSecret: string;
12
+ tokenTtlSeconds: number;
7
13
  }
8
14
  export declare function loadEnv(): Env;
package/dist/env.js CHANGED
@@ -4,7 +4,15 @@ export function loadEnv() {
4
4
  const databaseUrl = process.env.DATABASE_URL ?? 'file:./data/sync.db';
5
5
  const readSecret = required('READ_SECRET');
6
6
  const writeSecret = required('WRITE_SECRET');
7
- return { port, databaseUrl, readSecret, writeSecret };
7
+ const mcpEnabled = (process.env.MCP_ENABLED ?? 'true').toLowerCase() !== 'false';
8
+ const mcp = mcpEnabled
9
+ ? {
10
+ clientId: required('MCP_CLIENT_ID'),
11
+ tokenSecret: required('MCP_TOKEN_SECRET'),
12
+ tokenTtlSeconds: Number(process.env.MCP_TOKEN_TTL_SECONDS ?? 3600),
13
+ }
14
+ : null;
15
+ return { port, databaseUrl, readSecret, writeSecret, mcp };
8
16
  }
9
17
  function required(name) {
10
18
  const value = process.env[name];
package/dist/env.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AASvB,MAAM,UAAU,OAAO;IACrB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACtE,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC7C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;AACxD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAgBvB,MAAM,UAAU,OAAO;IACrB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACtE,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;IAE7C,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC;IACjF,MAAM,GAAG,GAAG,UAAU;QACpB,CAAC,CAAC;YACE,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC;YACnC,WAAW,EAAE,QAAQ,CAAC,kBAAkB,CAAC;YACzC,eAAe,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,IAAI,CAAC;SACnE;QACH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC;AAC7D,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/index.js CHANGED
@@ -1,58 +1,12 @@
1
1
  import { serve } from '@hono/node-server';
2
- import { Hono } from 'hono';
2
+ import { buildApp } from './app.js';
3
3
  import { createDb } from './db/client.js';
4
4
  import { runMigrations } from './db/migrate.js';
5
5
  import { loadEnv } from './env.js';
6
- import { dailyMetricsRoutes } from './routes/daily-metrics.js';
7
- import { dailyStressBurdenRoutes } from './routes/daily-stress-burden.js';
8
- import { healthRoutes } from './routes/health.js';
9
- import { heartRateMinuteRoutes } from './routes/heart-rate-minute.js';
10
- import { hrZoneHistoryRoutes } from './routes/hr-zone-history.js';
11
- import { sleepSessionsRoutes } from './routes/sleep-sessions.js';
12
- import { userProfileRoutes } from './routes/user-profile.js';
13
- import { userTimezonesRoutes } from './routes/user-timezones.js';
14
- import { weeklyMetricsRoutes } from './routes/weekly-metrics.js';
15
- import { wipeRoutes } from './routes/wipe.js';
16
- import { createDailyMetricsStore } from './stores/daily-metrics.js';
17
- import { createDailyStressBurdenStore } from './stores/daily-stress-burden.js';
18
- import { createHeartRateMinuteStore } from './stores/heart-rate-minute.js';
19
- import { createHrZoneHistoryStore } from './stores/hr-zone-history.js';
20
- import { createSleepSessionsStore } from './stores/sleep-sessions.js';
21
- import { createUserProfileStore } from './stores/user-profile.js';
22
- import { createUserTimezonesStore } from './stores/user-timezones.js';
23
- import { createWeeklyMetricsStore } from './stores/weekly-metrics.js';
24
6
  const env = loadEnv();
25
7
  const db = createDb(env.databaseUrl);
26
8
  await runMigrations(db);
27
- const auth = { readSecret: env.readSecret, writeSecret: env.writeSecret };
28
- const app = new Hono();
29
- app.route('/v1/health', healthRoutes(db));
30
- const dailyMetricsStore = createDailyMetricsStore(db);
31
- const weeklyMetricsStore = createWeeklyMetricsStore(db);
32
- const sleepSessionsStore = createSleepSessionsStore(db);
33
- const userTimezonesStore = createUserTimezonesStore(db);
34
- const userProfileStore = createUserProfileStore(db);
35
- const dailyStressBurdenStore = createDailyStressBurdenStore(db);
36
- const heartRateMinuteStore = createHeartRateMinuteStore(db);
37
- const hrZoneHistoryStore = createHrZoneHistoryStore(db);
38
- app.route('/v1/sync/daily-metrics', dailyMetricsRoutes(dailyMetricsStore, auth));
39
- app.route('/v1/sync/weekly-metrics', weeklyMetricsRoutes(weeklyMetricsStore, auth));
40
- app.route('/v1/sync/sleep-sessions', sleepSessionsRoutes(sleepSessionsStore, auth));
41
- app.route('/v1/sync/user-timezones', userTimezonesRoutes(userTimezonesStore, auth));
42
- app.route('/v1/sync/user-profile', userProfileRoutes(userProfileStore, auth));
43
- app.route('/v1/sync/daily-stress-burden', dailyStressBurdenRoutes(dailyStressBurdenStore, auth));
44
- app.route('/v1/sync/heart-rate-minute', heartRateMinuteRoutes(heartRateMinuteStore, auth));
45
- app.route('/v1/sync/hr-zone-history', hrZoneHistoryRoutes(hrZoneHistoryStore, auth));
46
- app.route('/v1/sync/wipe', wipeRoutes({
47
- dailyMetrics: dailyMetricsStore,
48
- weeklyMetrics: weeklyMetricsStore,
49
- sleepSessions: sleepSessionsStore,
50
- userTimezones: userTimezonesStore,
51
- userProfile: userProfileStore,
52
- dailyStressBurden: dailyStressBurdenStore,
53
- heartRateMinute: heartRateMinuteStore,
54
- hrZoneHistory: hrZoneHistoryStore,
55
- }, auth));
9
+ const app = buildApp(db, env);
56
10
  serve({ fetch: app.fetch, port: env.port }, (info) => {
57
11
  console.info(`zyke-sync listening on http://localhost:${info.port}`);
58
12
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AAErC,MAAM,aAAa,CAAC,EAAE,CAAC,CAAC;AAExB,MAAM,GAAG,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;AAE9B,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;IACnD,OAAO,CAAC,IAAI,CAAC,2CAA2C,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;AACvE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ export interface RegisteredClient {
2
+ clientId: string;
3
+ redirectUris: string[];
4
+ clientName: string | null;
5
+ createdAt: number;
6
+ }
7
+ export interface AuthCodeRecord {
8
+ clientId: string;
9
+ redirectUri: string;
10
+ codeChallenge: string;
11
+ codeChallengeMethod: 'S256';
12
+ expiresAt: number;
13
+ }
14
+ export interface AuthStore {
15
+ bootstrap(clientId: string): void;
16
+ registerClient(input: {
17
+ redirectUris: string[];
18
+ clientName?: string | null;
19
+ }, nowMs?: number): RegisteredClient;
20
+ getClient(clientId: string): RegisteredClient | undefined;
21
+ issueCode(rec: Omit<AuthCodeRecord, 'expiresAt'>, nowMs?: number): string;
22
+ consumeCode(code: string, nowMs?: number): AuthCodeRecord | null;
23
+ clear(): void;
24
+ }
25
+ export declare function createAuthStore(): AuthStore;
@@ -0,0 +1,51 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ const AUTH_CODE_TTL_MS = 5 * 60 * 1000;
3
+ export function createAuthStore() {
4
+ const clients = new Map();
5
+ const codes = new Map();
6
+ return {
7
+ bootstrap(clientId) {
8
+ if (!clients.has(clientId)) {
9
+ clients.set(clientId, {
10
+ clientId,
11
+ redirectUris: [],
12
+ clientName: 'bootstrap (client_credentials only)',
13
+ createdAt: Date.now(),
14
+ });
15
+ }
16
+ },
17
+ registerClient({ redirectUris, clientName = null }, nowMs = Date.now()) {
18
+ const clientId = `mcp_${randomBytes(18).toString('base64url')}`;
19
+ const client = {
20
+ clientId,
21
+ redirectUris: [...redirectUris],
22
+ clientName,
23
+ createdAt: nowMs,
24
+ };
25
+ clients.set(clientId, client);
26
+ return client;
27
+ },
28
+ getClient(clientId) {
29
+ return clients.get(clientId);
30
+ },
31
+ issueCode(rec, nowMs = Date.now()) {
32
+ const code = randomBytes(32).toString('base64url');
33
+ codes.set(code, { ...rec, expiresAt: nowMs + AUTH_CODE_TTL_MS });
34
+ return code;
35
+ },
36
+ consumeCode(code, nowMs = Date.now()) {
37
+ const rec = codes.get(code);
38
+ if (!rec)
39
+ return null;
40
+ codes.delete(code); // single-use
41
+ if (nowMs >= rec.expiresAt)
42
+ return null;
43
+ return rec;
44
+ },
45
+ clear() {
46
+ clients.clear();
47
+ codes.clear();
48
+ },
49
+ };
50
+ }
51
+ //# sourceMappingURL=auth-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../../src/mcp/auth-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiB1C,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAcvC,MAAM,UAAU,eAAe;IAC7B,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEhD,OAAO;QACL,SAAS,CAAC,QAAQ;YAChB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE;oBACpB,QAAQ;oBACR,YAAY,EAAE,EAAE;oBAChB,UAAU,EAAE,qCAAqC;oBACjD,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACtB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,cAAc,CAAC,EAAE,YAAY,EAAE,UAAU,GAAG,IAAI,EAAE,EAAE,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;YACpE,MAAM,QAAQ,GAAG,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAChE,MAAM,MAAM,GAAqB;gBAC/B,QAAQ;gBACR,YAAY,EAAE,CAAC,GAAG,YAAY,CAAC;gBAC/B,UAAU;gBACV,SAAS,EAAE,KAAK;aACjB,CAAC;YACF,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC9B,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,SAAS,CAAC,QAAQ;YAChB,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QACD,SAAS,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;YAC/B,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACnD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,KAAK,GAAG,gBAAgB,EAAE,CAAC,CAAC;YACjE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,WAAW,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;YAClC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5B,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAC;YACtB,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;YACjC,IAAI,KAAK,IAAI,GAAG,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YACxC,OAAO,GAAG,CAAC;QACb,CAAC;QACD,KAAK;YACH,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { HtmlEscapedString } from 'hono/utils/html';
2
+ export interface ConsentViewModel {
3
+ clientId: string;
4
+ redirectUri: string;
5
+ state: string;
6
+ codeChallenge: string;
7
+ codeChallengeMethod: 'S256';
8
+ scope: string;
9
+ clientName: string | null;
10
+ error: string | null;
11
+ }
12
+ /**
13
+ * Render the consent screen. All user-controlled values are escaped by the
14
+ * `html` template tag — never inject raw strings into the template.
15
+ */
16
+ export declare function renderConsent(vm: ConsentViewModel): HtmlEscapedString | Promise<HtmlEscapedString>;
@@ -0,0 +1,90 @@
1
+ import { html } from 'hono/html';
2
+ const STYLE = `
3
+ :root { color-scheme: light dark; }
4
+ body {
5
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6
+ max-width: 28rem;
7
+ margin: 3rem auto;
8
+ padding: 0 1.5rem;
9
+ line-height: 1.5;
10
+ }
11
+ h1 { font-size: 1.25rem; margin-bottom: 0.5rem; }
12
+ .meta { font-size: 0.875rem; color: #666; margin-bottom: 1.5rem; }
13
+ .meta dt { font-weight: 600; }
14
+ .meta dd { margin: 0 0 0.5rem 0; word-break: break-all; font-family: ui-monospace, monospace; font-size: 0.8125rem; }
15
+ .warn {
16
+ border: 1px solid #d97706;
17
+ background: #fef3c7;
18
+ color: #78350f;
19
+ padding: 0.75rem 1rem;
20
+ border-radius: 0.375rem;
21
+ font-size: 0.875rem;
22
+ margin-bottom: 1.5rem;
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ .warn { background: #422006; color: #fde68a; border-color: #b45309; }
26
+ .meta { color: #aaa; }
27
+ }
28
+ label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
29
+ input[type="password"] {
30
+ width: 100%; padding: 0.5rem 0.625rem; font-size: 1rem;
31
+ border: 1px solid #ccc; border-radius: 0.375rem; box-sizing: border-box;
32
+ }
33
+ button {
34
+ margin-top: 1rem; padding: 0.625rem 1rem;
35
+ background: #2563eb; color: white; border: 0;
36
+ border-radius: 0.375rem; font-size: 1rem; cursor: pointer;
37
+ }
38
+ button:hover { background: #1d4ed8; }
39
+ .err { color: #b91c1c; font-size: 0.875rem; margin-top: 0.5rem; }
40
+ `;
41
+ /**
42
+ * Render the consent screen. All user-controlled values are escaped by the
43
+ * `html` template tag — never inject raw strings into the template.
44
+ */
45
+ export function renderConsent(vm) {
46
+ const errBlock = vm.error ? html `<p class="err">${vm.error}</p>` : html ``;
47
+ return html `<!doctype html>
48
+ <html lang="en">
49
+ <head>
50
+ <meta charset="utf-8" />
51
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
52
+ <title>Authorize MCP access</title>
53
+ <style>
54
+ ${STYLE}
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <main>
59
+ <h1>Authorize MCP access</h1>
60
+ <p class="meta">
61
+ An MCP client is requesting access to your Zyke data. Verify the redirect target before
62
+ continuing.
63
+ </p>
64
+ <dl class="meta">
65
+ <dt>Client</dt>
66
+ <dd>${vm.clientName ?? vm.clientId}</dd>
67
+ <dt>Redirects to</dt>
68
+ <dd>${vm.redirectUri}</dd>
69
+ </dl>
70
+ <p class="warn">
71
+ Only continue if the redirect target above is a domain you trust (e.g.
72
+ <code>https://claude.ai/api/mcp/auth_callback</code>).
73
+ </p>
74
+ <form method="POST" action="/mcp/authorize" autocomplete="off">
75
+ <label for="password">Read secret</label>
76
+ <input id="password" type="password" name="password" autofocus required />
77
+ <input type="hidden" name="client_id" value="${vm.clientId}" />
78
+ <input type="hidden" name="redirect_uri" value="${vm.redirectUri}" />
79
+ <input type="hidden" name="state" value="${vm.state}" />
80
+ <input type="hidden" name="code_challenge" value="${vm.codeChallenge}" />
81
+ <input type="hidden" name="code_challenge_method" value="${vm.codeChallengeMethod}" />
82
+ <input type="hidden" name="scope" value="${vm.scope}" />
83
+ <button type="submit">Authorize</button>
84
+ ${errBlock}
85
+ </form>
86
+ </main>
87
+ </body>
88
+ </html>`;
89
+ }
90
+ //# sourceMappingURL=consent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent.js","sourceRoot":"","sources":["../../src/mcp/consent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAcjC,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCb,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,EAAoB;IAEpB,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,kBAAkB,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE,CAAC;IAC1E,OAAO,IAAI,CAAA;;;;;;;YAOD,KAAK;;;;;;;;;;;;kBAYC,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,QAAQ;;kBAE5B,EAAE,CAAC,WAAW;;;;;;;;;2DAS2B,EAAE,CAAC,QAAQ;8DACR,EAAE,CAAC,WAAW;uDACrB,EAAE,CAAC,KAAK;gEACC,EAAE,CAAC,aAAa;uEACT,EAAE,CAAC,mBAAmB;uDACtC,EAAE,CAAC,KAAK;;cAEjD,QAAQ;;;;YAIV,CAAC;AACb,CAAC"}
@@ -0,0 +1,2 @@
1
+ export type MetricValueType = 'seconds' | 'minutes' | 'milliseconds' | 'bpm' | 'percent' | 'ratio' | 'score' | 'count' | 'kcal' | 'meters' | 'kilograms' | 'celsius' | 'unknown';
2
+ export declare function inferValueType(metricKey: string): MetricValueType;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Hand-curated overrides for metric keys whose name doesn't follow the suffix
3
+ * convention. Add entries here as we learn about ambiguous keys. The suffix
4
+ * inference below handles the common case.
5
+ */
6
+ const OVERRIDES = {
7
+ steps: 'count',
8
+ active_calories: 'kcal',
9
+ total_calories: 'kcal',
10
+ vo2_max: 'score',
11
+ weight: 'kilograms',
12
+ height: 'meters',
13
+ sleep_efficiency: 'ratio',
14
+ };
15
+ // Order matters — first match wins, so place longer/more specific suffixes first.
16
+ const SUFFIX_RULES = [
17
+ { suffix: '_ms', type: 'milliseconds' },
18
+ { suffix: '_minutes', type: 'minutes' },
19
+ { suffix: '_min', type: 'minutes' },
20
+ { suffix: '_s', type: 'seconds' },
21
+ { suffix: '_bpm', type: 'bpm' },
22
+ { suffix: '_percent', type: 'percent' },
23
+ { suffix: '_pct', type: 'percent' },
24
+ { suffix: '_ratio', type: 'ratio' },
25
+ { suffix: '_score', type: 'score' },
26
+ { suffix: '_count', type: 'count' },
27
+ { suffix: '_kcal', type: 'kcal' },
28
+ { suffix: '_kg', type: 'kilograms' },
29
+ { suffix: '_m', type: 'meters' },
30
+ { suffix: '_c', type: 'celsius' },
31
+ ];
32
+ export function inferValueType(metricKey) {
33
+ const override = OVERRIDES[metricKey];
34
+ if (override)
35
+ return override;
36
+ for (const rule of SUFFIX_RULES) {
37
+ if (metricKey.endsWith(rule.suffix))
38
+ return rule.type;
39
+ }
40
+ return 'unknown';
41
+ }
42
+ //# sourceMappingURL=metric-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric-types.js","sourceRoot":"","sources":["../../src/mcp/metric-types.ts"],"names":[],"mappings":"AAeA;;;;GAIG;AACH,MAAM,SAAS,GAAoC;IACjD,KAAK,EAAE,OAAO;IACd,eAAe,EAAE,MAAM;IACvB,cAAc,EAAE,MAAM;IACtB,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,QAAQ;IAChB,gBAAgB,EAAE,OAAO;CAC1B,CAAC;AAOF,kFAAkF;AAClF,MAAM,YAAY,GAAiB;IACjC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE;IACvC,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;IACvC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;IACnC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;IACjC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE;IAC/B,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;IACvC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;IACnC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IACnC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IACnC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IACnC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;IACjC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE;IACpC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE;IAChC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;CAClC,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC;IACxD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { McpEnv } from '../env.js';
2
+ import type { AuthStore } from './auth-store.js';
3
+ export interface TokenIssueResult {
4
+ accessToken: string;
5
+ expiresIn: number;
6
+ }
7
+ export type TokenVerifyResult = {
8
+ ok: true;
9
+ expiresAt: number;
10
+ } | {
11
+ ok: false;
12
+ reason: 'malformed' | 'bad_signature' | 'expired';
13
+ };
14
+ /**
15
+ * Issue an opaque, HMAC-signed bearer token.
16
+ *
17
+ * Token layout: `${b64url(payload)}.${hex(hmac_sha256(payload, secret))}`
18
+ * where `payload = random(16) || expiryUnixSeconds(uint64BE)`.
19
+ *
20
+ * Stateless — verification needs only the signing secret.
21
+ */
22
+ export declare function issueToken(env: McpEnv, nowMs?: number): TokenIssueResult;
23
+ export declare function verifyToken(token: string, env: McpEnv, nowMs?: number): TokenVerifyResult;
24
+ export interface ClientCredentialsRequest {
25
+ clientId: string;
26
+ clientSecret: string;
27
+ grantType: string;
28
+ }
29
+ export type GrantResult = {
30
+ ok: true;
31
+ token: TokenIssueResult;
32
+ } | {
33
+ ok: false;
34
+ status: number;
35
+ error: string;
36
+ description?: string;
37
+ };
38
+ /**
39
+ * Validate a client_credentials grant request against the configured MCP env
40
+ * + the read secret. Returns the access token on success.
41
+ */
42
+ export declare function grantClientCredentials(req: ClientCredentialsRequest, env: McpEnv, readSecret: string, nowMs?: number): GrantResult;
43
+ /**
44
+ * Compute the PKCE S256 code challenge for a verifier.
45
+ *
46
+ * RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(verifier)))`.
47
+ */
48
+ export declare function pkceS256Challenge(verifier: string): string;
49
+ export interface AuthCodeRequest {
50
+ grantType: string;
51
+ code: string;
52
+ redirectUri: string;
53
+ clientId: string;
54
+ codeVerifier: string;
55
+ }
56
+ /**
57
+ * Validate an authorization_code grant + PKCE verifier and exchange it for a
58
+ * bearer token. The code is single-use — `consumeCode` deletes it regardless
59
+ * of validation outcome past the lookup.
60
+ */
61
+ export declare function grantAuthorizationCode(req: AuthCodeRequest, env: McpEnv, store: AuthStore, nowMs?: number): GrantResult;