hearth-dash 1.1.0 → 1.1.2

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.
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, chmodSync } from "node:fs";
1
+ import { readFileSync, existsSync, chmodSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { banner, bold, dim, cyan, warn, info } from "../lib/ui.js";
@@ -14,13 +14,13 @@ export default async function mcpCommand(args) {
14
14
  }
15
15
 
16
16
  const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
17
- const mcpUrl = config.workerUrl + "/mcp";
17
+ const mcpUrl = config.workerUrl + "/mcp";
18
18
 
19
19
  console.log(bold(" MCP Configuration\n"));
20
20
 
21
- console.log(` ${bold("Endpoint:")} ${cyan(mcpUrl)}\n`);
22
- info("Hearth uses OAuth 2.1. Your dashboard password stays out of the connector URL and Claude stores only revocable tokens.");
23
- console.log();
21
+ console.log(` ${bold("Endpoint:")} ${cyan(mcpUrl)}\n`);
22
+ info("Hearth uses OAuth 2.1. Your dashboard password stays out of the connector URL and Claude stores only revocable tokens.");
23
+ console.log();
24
24
 
25
25
  console.log(` ${bold("For Claude Code")} (add to ${dim("~/.claude.json")}):\n`);
26
26
  console.log(` {`);
@@ -31,10 +31,10 @@ export default async function mcpCommand(args) {
31
31
  console.log(` }`);
32
32
  console.log(` }\n`);
33
33
 
34
- console.log(` ${bold("For Claude Desktop")} (Settings > Developer > MCP):\n`);
34
+ console.log(` ${bold("For Claude Desktop")} (Settings > Developer > MCP):\n`);
35
35
  console.log(` Name: hearth-dash`);
36
- console.log(` URL: ${mcpUrl}\n`);
37
- console.log(` ${dim("When prompted, sign in with the Hearth dashboard password and approve access.")}\n`);
36
+ console.log(` URL: ${mcpUrl}\n`);
37
+ console.log(` ${dim("When prompted, sign in with the Hearth dashboard password and approve access.")}\n`);
38
38
 
39
39
  console.log(` ${bold("Connector URL")} (for Claude.ai mobile):`);
40
40
  console.log(` ${cyan(mcpUrl)}\n`);
@@ -65,9 +65,9 @@ export default async function mcpCommand(args) {
65
65
  try { claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8")); } catch {}
66
66
  }
67
67
  if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
68
- claudeConfig.mcpServers["hearth-dash"] = { url: mcpUrl };
69
- writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2) + "\n", "utf-8");
70
- try { chmodSync(claudeConfigPath, 0o600); } catch {}
71
- info("MCP config written to " + claudeConfigPath);
68
+ claudeConfig.mcpServers["hearth-dash"] = { url: mcpUrl };
69
+ writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2) + "\n", "utf-8");
70
+ try { chmodSync(claudeConfigPath, 0o600); } catch {}
71
+ info("MCP config written to " + claudeConfigPath);
72
72
  }
73
73
  }
package/cli/index.js CHANGED
@@ -54,7 +54,7 @@ ${bold("Quick start:")}
54
54
  ${dim("$")} npx hearth-dash mcp
55
55
 
56
56
  ${bold("Requirements:")}
57
- - Node.js 22+
57
+ - Node.js 22+
58
58
  - Cloudflare account (free tier works)
59
59
  - OpenWeatherMap API key (free at ${dim("https://openweathermap.org/api")})
60
60
  `);
@@ -9,7 +9,7 @@ function spawnCmd(cmd, args, opts) {
9
9
  return spawn(cmd, args, opts);
10
10
  }
11
11
 
12
- export function execWrangler(args, cwd, stdinData) {
12
+ export function execWrangler(args, cwd, stdinData) {
13
13
  return new Promise((resolve) => {
14
14
  const proc = spawnCmd("npx", ["wrangler", ...args], {
15
15
  cwd, stdio: ["pipe", "pipe", "pipe"],
@@ -22,20 +22,20 @@ export function execWrangler(args, cwd, stdinData) {
22
22
  else { proc.stdin.end(); }
23
23
  proc.on("close", (code) => resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code }));
24
24
  });
25
- }
26
-
27
- export function execCommand(command, args, cwd) {
28
- return new Promise((resolve) => {
29
- const proc = spawnCmd(command, args, {
30
- cwd, stdio: ['ignore', 'pipe', 'pipe'],
31
- env: { ...process.env, FORCE_COLOR: '0' },
32
- });
33
- let stdout = '', stderr = '';
34
- proc.stdout.on('data', (data) => { stdout += data.toString(); });
35
- proc.stderr.on('data', (data) => { stderr += data.toString(); });
36
- proc.on('close', (code) => resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code }));
37
- });
38
- }
25
+ }
26
+
27
+ export function execCommand(command, args, cwd) {
28
+ return new Promise((resolve) => {
29
+ const proc = spawnCmd(command, args, {
30
+ cwd, stdio: ['ignore', 'pipe', 'pipe'],
31
+ env: { ...process.env, FORCE_COLOR: '0' },
32
+ });
33
+ let stdout = '', stderr = '';
34
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
35
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
36
+ proc.on('close', (code) => resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code }));
37
+ });
38
+ }
39
39
 
40
40
  export function parseD1CreateOutput(output) {
41
41
  const combined = output.stdout + "\n" + output.stderr;
@@ -79,22 +79,22 @@ export async function executeSchema(dbName, schemaPath, cwd) {
79
79
  return { ok: true };
80
80
  }
81
81
 
82
- export async function listD1Databases(cwd) {
82
+ export async function listD1Databases(cwd) {
83
83
  const result = await execWrangler(["d1", "list", "--json"], cwd);
84
84
  if (result.code !== 0) return [];
85
85
  try { return JSON.parse(result.stdout); } catch { return []; }
86
- }
87
-
88
- export async function listKvNamespaces(cwd) {
89
- const result = await execWrangler(['kv', 'namespace', 'list'], cwd);
90
- if (result.code !== 0) return [];
91
- try { return JSON.parse(result.stdout); } catch { return []; }
92
- }
93
-
94
- export function parseKvCreateOutput(output) {
95
- const combined = output.stdout + '\n' + output.stderr;
96
- const toml = combined.match(/id\s*=\s*"([a-f0-9]{32})"/i);
97
- if (toml) return toml[1];
98
- const json = combined.match(/"id"\s*:\s*"([a-f0-9]{32})"/i);
99
- return json ? json[1] : null;
100
- }
86
+ }
87
+
88
+ export async function listKvNamespaces(cwd) {
89
+ const result = await execWrangler(['kv', 'namespace', 'list'], cwd);
90
+ if (result.code !== 0) return [];
91
+ try { return JSON.parse(result.stdout); } catch { return []; }
92
+ }
93
+
94
+ export function parseKvCreateOutput(output) {
95
+ const combined = output.stdout + '\n' + output.stderr;
96
+ const toml = combined.match(/id\s*=\s*"([a-f0-9]{32})"/i);
97
+ if (toml) return toml[1];
98
+ const json = combined.match(/"id"\s*:\s*"([a-f0-9]{32})"/i);
99
+ return json ? json[1] : null;
100
+ }
package/oauth-entry.js CHANGED
@@ -1,41 +1,47 @@
1
- import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
2
- import { oauthApiHandler, oauthDefaultHandler, withinRateLimit } from './worker.js';
3
-
4
- const OAUTH_SCOPES = ['hearth:read', 'hearth:write'];
5
-
6
- function createOAuthProvider(request, env) {
7
- const origin = new URL(request.url).origin;
8
- const resource = `${origin}/mcp`;
9
- return new OAuthProvider({
10
- apiRoute: '/mcp',
11
- apiHandler: oauthApiHandler,
12
- defaultHandler: oauthDefaultHandler,
13
- authorizeEndpoint: '/authorize',
14
- tokenEndpoint: '/oauth/token',
15
- clientRegistrationEndpoint: '/oauth/register',
16
- async clientRegistrationCallback({ request: registrationRequest }) {
17
- const allowed = await withinRateLimit(env, registrationRequest, 'oauth-register', 20, 3600);
18
- if (!allowed) {
19
- return { code: 'access_denied', description: 'Too many client registrations. Try again later.', status: 429 };
20
- }
21
- },
22
- clientIdMetadataDocumentEnabled: true,
23
- scopesSupported: OAUTH_SCOPES,
24
- accessTokenTTL: 3600,
25
- refreshTokenTTL: 30 * 24 * 60 * 60,
26
- clientRegistrationTTL: 90 * 24 * 60 * 60,
27
- resourceMetadata: {
28
- resource,
29
- authorization_servers: [origin],
30
- scopes_supported: ['hearth:read'],
31
- bearer_methods_supported: ['header'],
32
- resource_name: 'Hearth Dash',
33
- },
34
- });
35
- }
36
-
37
- export default {
38
- fetch(request, env, ctx) {
39
- return createOAuthProvider(request, env).fetch(request, env, ctx);
40
- },
41
- };
1
+ import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
2
+ import { applicationHandler, isOAuthRoute, oauthApiHandler, oauthDefaultHandler, withinRateLimit } from './worker.js';
3
+
4
+ const OAUTH_SCOPES = ['hearth:read', 'hearth:write'];
5
+
6
+ function createOAuthProvider(request, env) {
7
+ const origin = new URL(request.url).origin;
8
+ const resource = `${origin}/mcp`;
9
+ return new OAuthProvider({
10
+ apiRoute: '/mcp',
11
+ apiHandler: oauthApiHandler,
12
+ defaultHandler: oauthDefaultHandler,
13
+ authorizeEndpoint: '/authorize',
14
+ tokenEndpoint: '/oauth/token',
15
+ clientRegistrationEndpoint: '/oauth/register',
16
+ async clientRegistrationCallback({ request: registrationRequest }) {
17
+ const allowed = await withinRateLimit(env, registrationRequest, 'oauth-register', 20, 3600);
18
+ if (!allowed) {
19
+ return { code: 'access_denied', description: 'Too many client registrations. Try again later.', status: 429 };
20
+ }
21
+ },
22
+ clientIdMetadataDocumentEnabled: true,
23
+ scopesSupported: OAUTH_SCOPES,
24
+ accessTokenTTL: 3600,
25
+ refreshTokenTTL: 30 * 24 * 60 * 60,
26
+ clientRegistrationTTL: 90 * 24 * 60 * 60,
27
+ resourceMetadata: {
28
+ resource,
29
+ authorization_servers: [origin],
30
+ scopes_supported: ['hearth:read'],
31
+ bearer_methods_supported: ['header'],
32
+ resource_name: 'Hearth Dash',
33
+ },
34
+ });
35
+ }
36
+
37
+ export default {
38
+ fetch(request, env, ctx) {
39
+ // Keep ordinary dashboard traffic out of the OAuth provider. Besides being
40
+ // unnecessary, wrapping /login and /api requests can obscure the browser's
41
+ // public origin on some Cloudflare routes and make valid CSRF checks fail.
42
+ if (!isOAuthRoute(new URL(request.url).pathname)) {
43
+ return applicationHandler.fetch(request, env, ctx);
44
+ }
45
+ return createOAuthProvider(request, env).fetch(request, env, ctx);
46
+ },
47
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hearth-dash",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Personal dashboard — moods, moments, food diary, weather, and more. Deploy to Cloudflare in one command.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,8 +28,8 @@
28
28
  "type": "git",
29
29
  "url": "git+https://github.com/martusha89/hearth-dash.git"
30
30
  },
31
- "engines": {
32
- "node": ">=22"
31
+ "engines": {
32
+ "node": ">=22"
33
33
  },
34
34
  "files": [
35
35
  "worker.js",
package/schema.sql CHANGED
@@ -1,14 +1,14 @@
1
1
  -- Hearth Dash — Database Schema
2
2
  -- Deploy: npx wrangler d1 execute hearth-dash-db --file schema.sql
3
3
 
4
- CREATE TABLE IF NOT EXISTS config (
5
- key TEXT PRIMARY KEY,
6
- value TEXT NOT NULL
7
- );
8
-
9
- -- v1.0.1's first-visit setup stored the dashboard password as plaintext.
10
- -- Authentication now uses Cloudflare Worker secrets; remove any legacy copy.
11
- DELETE FROM config WHERE key = 'password';
4
+ CREATE TABLE IF NOT EXISTS config (
5
+ key TEXT PRIMARY KEY,
6
+ value TEXT NOT NULL
7
+ );
8
+
9
+ -- v1.0.1's first-visit setup stored the dashboard password as plaintext.
10
+ -- Authentication now uses Cloudflare Worker secrets; remove any legacy copy.
11
+ DELETE FROM config WHERE key = 'password';
12
12
 
13
13
  CREATE TABLE IF NOT EXISTS moods (
14
14
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -77,19 +77,19 @@ CREATE TABLE IF NOT EXISTS water_log (
77
77
  );
78
78
  CREATE INDEX IF NOT EXISTS idx_water_log_date ON water_log(date);
79
79
 
80
- CREATE TABLE IF NOT EXISTS food_reviews (
80
+ CREATE TABLE IF NOT EXISTS food_reviews (
81
81
  id INTEGER PRIMARY KEY AUTOINCREMENT,
82
82
  date TEXT UNIQUE NOT NULL,
83
83
  review TEXT NOT NULL,
84
84
  reviewer TEXT NOT NULL DEFAULT 'AI',
85
85
  created_at TEXT DEFAULT (datetime('now'))
86
86
  );
87
- CREATE INDEX IF NOT EXISTS idx_food_reviews_date ON food_reviews(date);
88
-
89
- -- Coarse per-IP request buckets. IP addresses are hashed before storage.
90
- CREATE TABLE IF NOT EXISTS rate_limits (
91
- bucket_key TEXT PRIMARY KEY,
92
- count INTEGER NOT NULL DEFAULT 1,
93
- expires_at INTEGER NOT NULL
94
- );
95
- CREATE INDEX IF NOT EXISTS idx_rate_limits_expires ON rate_limits(expires_at);
87
+ CREATE INDEX IF NOT EXISTS idx_food_reviews_date ON food_reviews(date);
88
+
89
+ -- Coarse per-IP request buckets. IP addresses are hashed before storage.
90
+ CREATE TABLE IF NOT EXISTS rate_limits (
91
+ bucket_key TEXT PRIMARY KEY,
92
+ count INTEGER NOT NULL DEFAULT 1,
93
+ expires_at INTEGER NOT NULL
94
+ );
95
+ CREATE INDEX IF NOT EXISTS idx_rate_limits_expires ON rate_limits(expires_at);