@blanketbyte/wise-mcp 1.0.2 → 1.0.3
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 +1 -0
- package/dist/auth/pages.js +6 -0
- package/dist/auth/provider.js +11 -1
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,7 @@ transfers only: you pay it in the Wise app
|
|
|
143
143
|
| 🧹 **Minimal retention** | A Wise token is deleted as soon as no live session uses it: after revocation, session expiry, or when Wise rejects it. |
|
|
144
144
|
| 🔁 **OAuth hardening** | PKCE (S256), single-use login and authorization codes, rotating refresh tokens, redirect hosts limited to `claude.ai`, `claude.com`, `chatgpt.com` and localhost, so authorization codes can only reach apps you trust. |
|
|
145
145
|
| 🚦 **Rate limiting** | The token forms (`/login`, `/sessions/login`) allow 20 attempts per IP every 15 minutes; OAuth endpoints use the MCP SDK's limits. |
|
|
146
|
+
| 🔐 **HTTPS only** | The server refuses to start with a non-HTTPS `PUBLIC_URL`, and every page shows a red warning if it's opened over plain HTTP, so nobody pastes a Wise token on an unencrypted page. |
|
|
146
147
|
| 🛡️ **Pages** | Strict CSP (`default-src 'none'`, `form-action` limited to the server and the client's own redirect origin), `X-Frame-Options: DENY`, `no-store`, no referrer. |
|
|
147
148
|
|
|
148
149
|
> [!WARNING]
|
package/dist/auth/pages.js
CHANGED
|
@@ -104,6 +104,10 @@ function layout(title, body) {
|
|
|
104
104
|
.sub { color:var(--muted); font-size:13.5px; margin:0; }
|
|
105
105
|
.sub b, .sub a.brand { color:var(--fg); font-weight:600; text-decoration:none; }
|
|
106
106
|
.sub a.brand:hover { text-decoration:underline; }
|
|
107
|
+
.insecure { display:flex; gap:12px; align-items:flex-start; margin:0 0 24px; padding:14px 16px; border-radius:16px;
|
|
108
|
+
background:#b42318; color:#fff; font-size:14px; line-height:1.5; box-shadow:0 0 0 4px rgba(180,35,24,.18); }
|
|
109
|
+
.insecure .ic { width:22px; height:22px; flex:none; margin-top:1px; }
|
|
110
|
+
.insecure strong { display:block; font-size:15px; }
|
|
107
111
|
.notice { display:inline-flex; align-items:center; gap:6px; margin-top:10px; padding:5px 12px 5px 10px; border-radius:999px;
|
|
108
112
|
background:var(--warn-bg); color:var(--warn); border:1px solid var(--warn-line); font-size:12.5px; font-weight:600; }
|
|
109
113
|
.notice .ic { width:15px; height:15px; }
|
|
@@ -201,6 +205,8 @@ function card(inner, opts) {
|
|
|
201
205
|
<footer>${icon("lock")}<span>Token encrypted at rest</span><span class="dot">·</span><a href="/sessions">Manage sessions</a><span class="dot">·</span><a class="gh" href="${REPO_URL}" target="_blank" rel="noopener">${icon("github")}Source</a></footer>
|
|
202
206
|
</main>`;
|
|
203
207
|
}
|
|
208
|
+
/** Red banner shown on every page served over plain HTTP (except localhost). */
|
|
209
|
+
export const INSECURE_BANNER = `<div class="insecure" role="alert">${icon("alert")}<div><strong>Not secure: this page uses HTTP, not HTTPS</strong>Don't paste your Wise token here: anyone on the network could read it. Turn on HTTPS for this domain (e.g. Let's Encrypt in Dokploy or Easypanel), then reload over https://.</div></div>`;
|
|
204
210
|
const alert = (kind, html) => `<div class="alert ${kind}" role="${kind === "err" ? "alert" : "status"}">${icon(kind === "err" ? "alert" : "check")}<div>${html}</div></div>`;
|
|
205
211
|
const TOKEN_STEPS = `<details>
|
|
206
212
|
<summary>Where do I find my token?</summary>
|
package/dist/auth/provider.js
CHANGED
|
@@ -2,7 +2,7 @@ import { InvalidClientMetadataError, InvalidGrantError, InvalidTargetError, Inva
|
|
|
2
2
|
import { randomToken } from "../crypto.js";
|
|
3
3
|
import { wiseClient } from "../wise/connections.js";
|
|
4
4
|
import { WiseAccessRevokedError, WiseApiError } from "../wise/client.js";
|
|
5
|
-
import { loginPage, messagePage, pageCsp, SESSION_CHOICES } from "./pages.js";
|
|
5
|
+
import { INSECURE_BANNER, loginPage, messagePage, pageCsp, SESSION_CHOICES } from "./pages.js";
|
|
6
6
|
const ACCESS_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
7
7
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
8
|
const LOGIN_TTL_MS = 15 * 60 * 1000; // the login page must be submitted within 15 minutes
|
|
@@ -208,7 +208,17 @@ export class WiseOAuthProvider {
|
|
|
208
208
|
this.codes.delete(k);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
212
|
+
/** True when the browser reached us over plain HTTP on a real hostname (behind the proxy, via X-Forwarded-Proto). */
|
|
213
|
+
export function isInsecure(req) {
|
|
214
|
+
return req.protocol !== "https" && !LOCAL_HOSTS.has(req.hostname);
|
|
215
|
+
}
|
|
211
216
|
export function page(res, status = 200, formTargets = []) {
|
|
217
|
+
if (isInsecure(res.req)) {
|
|
218
|
+
// Put a red warning at the top of the card on every page that's served over HTTP.
|
|
219
|
+
const send = res.send.bind(res);
|
|
220
|
+
res.send = ((body) => send(typeof body === "string" ? body.replace('<div class="inner">', `<div class="inner">${INSECURE_BANNER}`) : body));
|
|
221
|
+
}
|
|
212
222
|
res.status(status);
|
|
213
223
|
res.setHeader("Cache-Control", "no-store");
|
|
214
224
|
res.setHeader("Content-Security-Policy", pageCsp(formTargets));
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,10 @@ import { WiseOAuthProvider, identify, page } from "./auth/provider.js";
|
|
|
14
14
|
import { homePage, messagePage, sessionsLoginPage, sessionsPage } from "./auth/pages.js";
|
|
15
15
|
const config = loadServerConfig();
|
|
16
16
|
const publicUrl = new URL(config.PUBLIC_URL);
|
|
17
|
+
if (publicUrl.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(publicUrl.hostname)) {
|
|
18
|
+
console.error(`❌ PUBLIC_URL must be https:// (got ${publicUrl.origin}). Wise tokens must never travel unencrypted: enable HTTPS for the domain and set PUBLIC_URL=https://…`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
17
21
|
const mcpUrl = new URL("/mcp", publicUrl);
|
|
18
22
|
const iconUrl = new URL("/icon.png", publicUrl).href;
|
|
19
23
|
const iconSvgUrl = new URL("/logo.svg", publicUrl).href;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blanketbyte/wise-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MCP server for Wise: read balances, card payments, transfers and recipients, and prepare transfers you approve twice. Runs locally (npx, for Claude, Codex, Cursor…) or as a remote connector for Claude and ChatGPT.",
|
|
5
5
|
"mcpName": "io.github.BlanketByte/wise-mcp",
|
|
6
6
|
"keywords": [
|