@blanketbyte/wise-mcp 1.0.1 → 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 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]
@@ -200,6 +201,8 @@ Optional env: `WRITES_ENABLED=false` for read-only, `MAX_TRANSFER_AMOUNT` for a
200
201
 
201
202
  The container serves plain HTTP on port **3000**; put it behind anything that terminates HTTPS. State (sealed tokens, sessions) lives in **`/data`**.
202
203
 
204
+ A prebuilt image is published with every release: **`ghcr.io/blanketbyte/wise-mcp:<version>`** (or `:latest`), for `linux/amd64` and `linux/arm64`. Use it instead of building from source wherever your platform accepts an image.
205
+
203
206
  ### Dokploy
204
207
 
205
208
  1. **DNS**: point a hostname (e.g. `wise.example.com`) at your Dokploy server.
@@ -303,7 +306,7 @@ The remote end-to-end test boots the server next to a mock Wise API and drives e
303
306
 
304
307
  ### Releasing
305
308
 
306
- Push a tag matching `package.json` (e.g. `v1.1.0`). The release workflow runs the tests, publishes `@blanketbyte/wise-mcp` to npm with provenance (trusted publishing over GitHub OIDC, no npm token stored anywhere), lists it in the [official MCP Registry](https://registry.modelcontextprotocol.io) from `server.json`, and attaches the `.mcpb` to a GitHub release. Before the first release, remove `"private": true` from `package.json`.
309
+ Push a tag matching `package.json` (e.g. `v1.1.0`). The release workflow runs the tests, publishes `@blanketbyte/wise-mcp` to npm with provenance (trusted publishing over GitHub OIDC, no npm token stored anywhere), lists it in the [official MCP Registry](https://registry.modelcontextprotocol.io) from `server.json`, attaches the `.mcpb` to a GitHub release, and pushes the Docker image to `ghcr.io/blanketbyte/wise-mcp`. Before the first release, remove `"private": true` from `package.json`.
307
310
 
308
311
  <details>
309
312
  <summary>Project layout</summary>
@@ -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>
@@ -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.1",
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": [