hearth-dash 1.1.1 → 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.
- package/README.md +7 -3
- package/oauth-entry.js +12 -12
- package/package.json +1 -1
- package/worker.js +40 -20
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ claude mcp add --transport http hearth-dash https://your-worker.example/mcp
|
|
|
95
95
|
|
|
96
96
|
Then open `/mcp` inside Claude Code and complete authentication. Claude Code uses a loopback callback rather than Claude.ai's hosted callback; DCR handles its varying local port.
|
|
97
97
|
|
|
98
|
-
## Upgrading from 1.0.1
|
|
98
|
+
## Upgrading from 1.0.1
|
|
99
99
|
|
|
100
100
|
Version 1.0.1 labelled a custom `{ "tool": ..., "params": ... }` HTTP handler as MCP. It did not implement MCP JSON-RPC or tool discovery and could not work as a Claude.ai custom connector. Version 1.1.0 replaces it with Streamable HTTP MCP and OAuth 2.1. The old payload and secret-bearing URL formats are intentionally rejected.
|
|
101
101
|
|
|
@@ -110,11 +110,15 @@ Existing deployments must:
|
|
|
110
110
|
7. Remove and re-add the custom connector using `https://your-worker.example/mcp`.
|
|
111
111
|
8. Delete the obsolete secret with `npx wrangler secret delete MCP_SECRET` after the new deployment works.
|
|
112
112
|
|
|
113
|
-
The first-visit password setup page has also been removed. A public, unclaimed setup page allowed the first visitor—not necessarily the owner—to take control of a new deployment. Configure `DASHBOARD_PASSWORD` as a Worker secret instead.
|
|
114
|
-
|
|
113
|
+
The first-visit password setup page has also been removed. A public, unclaimed setup page allowed the first visitor—not necessarily the owner—to take control of a new deployment. Configure `DASHBOARD_PASSWORD` as a Worker secret instead.
|
|
114
|
+
|
|
115
115
|
### 1.1.1 dashboard-login fix
|
|
116
116
|
|
|
117
117
|
Version 1.1.1 keeps ordinary dashboard, login and API requests outside the OAuth provider and makes same-origin form validation resilient when a trusted Cloudflare wrapper reconstructs the internal request URL. Cross-site browser submissions remain rejected. Upgrade with `npx hearth-dash@latest deploy` if a 1.1.0 deployment returns plain `Forbidden` after submitting `/login`.
|
|
118
|
+
|
|
119
|
+
### 1.1.2 Chrome null-Origin fix
|
|
120
|
+
|
|
121
|
+
Version 1.1.2 accepts Chrome's legitimate `Origin: null` on a form submission only when the browser's unforgeable Fetch Metadata independently classifies the request as `same-origin`. Mismatched, malformed, same-site and cross-site requests remain rejected.
|
|
118
122
|
|
|
119
123
|
## Security notes
|
|
120
124
|
|
package/oauth-entry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
2
|
-
import { applicationHandler, isOAuthRoute, oauthApiHandler, oauthDefaultHandler, withinRateLimit } from './worker.js';
|
|
2
|
+
import { applicationHandler, isOAuthRoute, oauthApiHandler, oauthDefaultHandler, withinRateLimit } from './worker.js';
|
|
3
3
|
|
|
4
4
|
const OAUTH_SCOPES = ['hearth:read', 'hearth:write'];
|
|
5
5
|
|
|
@@ -34,14 +34,14 @@ function createOAuthProvider(request, env) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
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
|
-
};
|
|
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
package/worker.js
CHANGED
|
@@ -85,31 +85,51 @@ function sessionCookie(value, maxAge = 604800) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function sameOrigin(request) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
try { parsedOrigin = new URL(origin); } catch { return false; }
|
|
92
|
-
if (parsedOrigin.origin === new URL(request.url).origin) return true;
|
|
88
|
+
const targetOrigin = new URL(request.url).origin;
|
|
89
|
+
const fetchSite = request.headers.get('Sec-Fetch-Site');
|
|
90
|
+
const suppliedOrigin = classifyOrigin(request.headers.get('Origin'));
|
|
93
91
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// origin
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
// Fetch Metadata is browser-controlled and cannot be forged by hostile page
|
|
93
|
+
// JavaScript. Chrome can legitimately serialize Origin as "null" here, so a
|
|
94
|
+
// same-origin classification is the decisive signal for null/missing Origin.
|
|
95
|
+
if (fetchSite === 'same-origin') {
|
|
96
|
+
if (suppliedOrigin.kind === 'missing' || suppliedOrigin.kind === 'null') return true;
|
|
97
|
+
return suppliedOrigin.kind === 'origin' && suppliedOrigin.value === targetOrigin;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reject explicit cross-origin classifications even if another header is
|
|
101
|
+
// malformed or contradictory. "same-site" can still be another subdomain.
|
|
102
|
+
if (fetchSite === 'cross-site' || fetchSite === 'same-site' || fetchSite === 'none') return false;
|
|
103
|
+
|
|
104
|
+
// Older/non-browser clients without Fetch Metadata must provide one exact,
|
|
105
|
+
// valid HTTP(S) Origin. Unknown future Fetch Metadata values use this same
|
|
106
|
+
// conservative fallback.
|
|
107
|
+
return suppliedOrigin.kind === 'origin' && suppliedOrigin.value === targetOrigin;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
||
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
function classifyOrigin(value) {
|
|
111
|
+
if (value === null) return { kind: 'missing' };
|
|
112
|
+
if (value === 'null') return { kind: 'null' };
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(value);
|
|
115
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== value) return { kind: 'invalid' };
|
|
116
|
+
return { kind: 'origin', value: parsed.origin };
|
|
117
|
+
} catch {
|
|
118
|
+
return { kind: 'invalid' };
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
export function isOAuthRoute(pathname) {
|
|
123
|
+
return pathname === '/mcp'
|
|
124
|
+
|| pathname.startsWith('/mcp/')
|
|
125
|
+
|| pathname === '/authorize'
|
|
126
|
+
|| pathname === '/oauth/token'
|
|
127
|
+
|| pathname === '/oauth/register'
|
|
128
|
+
|| pathname === '/.well-known/oauth-authorization-server'
|
|
129
|
+
|| pathname === '/.well-known/oauth-protected-resource'
|
|
130
|
+
|| pathname.startsWith('/.well-known/oauth-protected-resource/');
|
|
131
|
+
}
|
|
132
|
+
|
|
113
133
|
export async function withinRateLimit(env, request, scope, limit, windowSeconds) {
|
|
114
134
|
const address = request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
115
135
|
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(address)));
|