sitezen-mcp 1.3.0 → 1.4.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/dist/beta-keys.js +65 -0
- package/dist/errors.js +3 -2
- package/dist/license.js +49 -40
- package/dist/tools-session.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Self-contained BETA license keys — no server required.
|
|
2
|
+
//
|
|
3
|
+
// A key embeds the tester's name + an expiry date, and is SIGNED with a secret
|
|
4
|
+
// so it can't be forged or date-edited. The MCP verifies the signature and the
|
|
5
|
+
// expiry locally on every run. Generate one per tester with scripts/make-key.mjs.
|
|
6
|
+
//
|
|
7
|
+
// SECURITY NOTE: the secret ships inside the published package, so a determined
|
|
8
|
+
// user could extract it and forge a key. That's acceptable for a trusted-tester
|
|
9
|
+
// beta. When we move to paid, validation moves server-side and the secret never
|
|
10
|
+
// reaches clients.
|
|
11
|
+
import * as crypto from "node:crypto";
|
|
12
|
+
const BETA_SECRET = "9f3c1a7e5b2d8064c1f4a9e7d3b60285f1c8a4e6d7902b3c5a1e0f7b";
|
|
13
|
+
function b64urlEncode(s) {
|
|
14
|
+
return Buffer.from(s, "utf8").toString("base64")
|
|
15
|
+
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
16
|
+
}
|
|
17
|
+
function b64urlDecode(s) {
|
|
18
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
19
|
+
while (s.length % 4)
|
|
20
|
+
s += "=";
|
|
21
|
+
return Buffer.from(s, "base64").toString("utf8");
|
|
22
|
+
}
|
|
23
|
+
function sign(payloadB64) {
|
|
24
|
+
return crypto.createHmac("sha256", BETA_SECRET).update(payloadB64).digest("hex").slice(0, 12);
|
|
25
|
+
}
|
|
26
|
+
/** Build a signed beta key valid for `days` days from now. */
|
|
27
|
+
export function buildBetaKey(name, days) {
|
|
28
|
+
const exp = Date.now() + Math.max(1, days) * 24 * 60 * 60 * 1000;
|
|
29
|
+
const payload = { n: name, exp, v: 1 };
|
|
30
|
+
const p = b64urlEncode(JSON.stringify(payload));
|
|
31
|
+
return "SZ-BETA-" + p + "." + sign(p);
|
|
32
|
+
}
|
|
33
|
+
/** Verify a beta key: signature must match and it must not be expired. */
|
|
34
|
+
export function verifyBetaKey(key) {
|
|
35
|
+
const k = (key || "").trim();
|
|
36
|
+
if (!k.startsWith("SZ-BETA-"))
|
|
37
|
+
return { ok: false, reason: "invalid" };
|
|
38
|
+
const rest = k.slice("SZ-BETA-".length);
|
|
39
|
+
const dot = rest.lastIndexOf(".");
|
|
40
|
+
if (dot < 0)
|
|
41
|
+
return { ok: false, reason: "invalid" };
|
|
42
|
+
const p = rest.slice(0, dot);
|
|
43
|
+
const sig = rest.slice(dot + 1);
|
|
44
|
+
if (sign(p) !== sig)
|
|
45
|
+
return { ok: false, reason: "invalid" };
|
|
46
|
+
let payload;
|
|
47
|
+
try {
|
|
48
|
+
payload = JSON.parse(b64urlDecode(p));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { ok: false, reason: "invalid" };
|
|
52
|
+
}
|
|
53
|
+
if (typeof payload.exp !== "number")
|
|
54
|
+
return { ok: false, reason: "invalid" };
|
|
55
|
+
if (Date.now() > payload.exp)
|
|
56
|
+
return { ok: false, reason: "expired", payload };
|
|
57
|
+
return { ok: true, payload };
|
|
58
|
+
}
|
|
59
|
+
/** Human-readable expiry date for a key (or null if unreadable). */
|
|
60
|
+
export function betaKeyExpiry(key) {
|
|
61
|
+
const c = verifyBetaKey(key);
|
|
62
|
+
if (c.payload && typeof c.payload.exp === "number")
|
|
63
|
+
return new Date(c.payload.exp);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -21,8 +21,9 @@ export const Errors = {
|
|
|
21
21
|
noPageName: () => fail("NO_PAGE_NAME", "What should I name this page in WordPress?", "Tell me the page title."),
|
|
22
22
|
noSiteUrl: () => fail("NO_SITE_URL", "I don't know which WordPress site to push to. Please tell me the site URL (e.g. https://example.com).", "Share the site URL."),
|
|
23
23
|
noConnectionKey: (siteUrl) => fail("NO_CONNECTION_KEY", `I don't have a saved SiteZen connection key for ${siteUrl}. Please share the connection key — you can find it in WordPress under SiteZen → Connection.`, "Share the connection key for that site."),
|
|
24
|
-
noLicenseKey: () => fail("NO_LICENSE_KEY", "No SiteZen license key is set.
|
|
25
|
-
invalidLicense: () => fail("INVALID_LICENSE", "Your SiteZen license key isn't recognised. Check it for typos, or
|
|
24
|
+
noLicenseKey: () => fail("NO_LICENSE_KEY", "No SiteZen license key is set. Reach out for a beta key, then paste it into your claude_desktop_config.json as SITEZEN_LICENSE_KEY and restart Claude Desktop.", "Set SITEZEN_LICENSE_KEY in your Claude Desktop config, then restart."),
|
|
25
|
+
invalidLicense: () => fail("INVALID_LICENSE", "Your SiteZen license key isn't recognised. Check it for typos, or reach out to hello@sitezenai.com for a fresh beta key.", "Update SITEZEN_LICENSE_KEY in the config and restart Claude Desktop."),
|
|
26
|
+
betaExpired: () => fail("BETA_EXPIRED", "Your beta access is over. If you'd like to keep using SiteZen, you can upgrade to a paid plan by reaching out to us at hello@sitezenai.com.", "Tell the user their beta access has ended and to contact hello@sitezenai.com to upgrade. Do not attempt the conversion."),
|
|
26
27
|
limitReachedConversions: (plan, upgradeUrl) => fail("LIMIT_REACHED_CONVERSIONS", `You've used all the conversions in your ${plan} plan. Upgrade at ${upgradeUrl} — your existing converted pages stay live.`, "Upgrade the plan to keep converting."),
|
|
27
28
|
limitReachedSites: (plan, sitesAllowed, upgradeUrl) => fail("LIMIT_REACHED_SITES", `Your ${plan} plan allows ${sitesAllowed} site(s) and you're already at the limit. Disconnect a site you no longer use (disconnect_site), or upgrade at ${upgradeUrl}.`, "Disconnect an existing site or upgrade the plan."),
|
|
28
29
|
noFigmaToken: () => fail("NO_FIGMA_TOKEN", "No Figma access token is set. Generate one at https://www.figma.com/developers/api#access-tokens, then paste it into your claude_desktop_config.json as FIGMA_TOKEN and restart Claude Desktop.", "Set FIGMA_TOKEN in your Claude Desktop config, then restart."),
|
package/dist/license.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// cache automatically.
|
|
13
13
|
import * as crypto from "node:crypto";
|
|
14
14
|
import { readState, writeState } from "./state.js";
|
|
15
|
+
import { verifyBetaKey } from "./beta-keys.js";
|
|
15
16
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
16
|
-
const UPGRADE_URL = "https://
|
|
17
|
+
const UPGRADE_URL = "https://sitezenai.com/pricing";
|
|
17
18
|
function hashKey(key) {
|
|
18
19
|
return crypto.createHash("sha256").update(key.trim()).digest("hex");
|
|
19
20
|
}
|
|
@@ -51,54 +52,62 @@ async function callValidateEndpoint(key) {
|
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
function stubValidate(key) {
|
|
55
|
-
// Stub: any non-empty key → UNLIMITED plan while the real dashboard /
|
|
56
|
-
// LemonSqueezy licensing is still being built. This lets the product be
|
|
57
|
-
// tested end-to-end (multiple sites, unlimited conversions) without an
|
|
58
|
-
// artificial Free-plan wall. Replace by setting SITEZEN_LICENSE_API_URL in
|
|
59
|
-
// env — then the real validate endpoint governs sites/conversions.
|
|
60
|
-
return {
|
|
61
|
-
ok: true,
|
|
62
|
-
plan: "agency",
|
|
63
|
-
sites_allowed: -1, // -1 → unlimited sites
|
|
64
|
-
conversions_remaining: -1, // -1 → unlimited conversions
|
|
65
|
-
upgrade_url: UPGRADE_URL,
|
|
66
|
-
cached: false,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
55
|
/**
|
|
70
|
-
* Validate the user's license key.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
56
|
+
* Validate the user's license key.
|
|
57
|
+
* - `null` → no key set (caller shows Errors.noLicenseKey())
|
|
58
|
+
* - `{ ok: false, ... }` → key present but expired / unrecognised
|
|
59
|
+
* - `LicenseStatus` → valid, use the plan/quota
|
|
60
|
+
*
|
|
61
|
+
* Two modes:
|
|
62
|
+
* 1. If SITEZEN_LICENSE_API_URL is set, the real dashboard governs the key
|
|
63
|
+
* (cached for 1 hour). This is the future paid path.
|
|
64
|
+
* 2. Otherwise (today), validation is fully LOCAL against signed BETA keys
|
|
65
|
+
* generated by scripts/make-key.mjs — each carries its own expiry, no
|
|
66
|
+
* server needed. Random / edited / expired keys are rejected.
|
|
73
67
|
*/
|
|
74
68
|
export async function validateLicense(licenseKey) {
|
|
75
69
|
if (!licenseKey || !licenseKey.trim())
|
|
76
70
|
return null;
|
|
77
71
|
const key = licenseKey.trim();
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
// ── Paid path: a real license API is configured ──────────────────────
|
|
73
|
+
if (process.env.SITEZEN_LICENSE_API_URL) {
|
|
74
|
+
const state = readState();
|
|
75
|
+
if (state.license_cache && cacheIsFresh(state.license_cache, key)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
plan: state.license_cache.plan,
|
|
79
|
+
sites_allowed: state.license_cache.sites_allowed,
|
|
80
|
+
conversions_remaining: state.license_cache.conversions_remaining,
|
|
81
|
+
upgrade_url: UPGRADE_URL,
|
|
82
|
+
cached: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const fresh = await callValidateEndpoint(key);
|
|
86
|
+
if (!fresh)
|
|
87
|
+
return { ok: false, reason: "invalid" };
|
|
88
|
+
state.license_cache = {
|
|
89
|
+
key_hash: hashKey(key),
|
|
90
|
+
plan: fresh.plan,
|
|
91
|
+
sites_allowed: fresh.sites_allowed,
|
|
92
|
+
conversions_remaining: fresh.conversions_remaining,
|
|
93
|
+
cached_at: new Date().toISOString(),
|
|
88
94
|
};
|
|
95
|
+
writeState(state);
|
|
96
|
+
return fresh;
|
|
89
97
|
}
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
// ── Beta path: local signed-key validation (no server) ───────────────
|
|
99
|
+
const check = verifyBetaKey(key);
|
|
100
|
+
if (!check.ok)
|
|
101
|
+
return { ok: false, reason: check.reason };
|
|
102
|
+
// Valid beta key → unlimited use within its window; expiry is the only gate.
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
plan: "free",
|
|
106
|
+
sites_allowed: -1,
|
|
107
|
+
conversions_remaining: -1,
|
|
108
|
+
upgrade_url: UPGRADE_URL,
|
|
109
|
+
cached: false,
|
|
99
110
|
};
|
|
100
|
-
writeState(state);
|
|
101
|
-
return fresh;
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
104
113
|
* Decrement the cached conversion count after a successful conversion.
|
package/dist/tools-session.js
CHANGED
|
@@ -39,6 +39,8 @@ export function registerSessionTools(server) {
|
|
|
39
39
|
const license = await validateLicense(process.env.SITEZEN_LICENSE_KEY);
|
|
40
40
|
if (!license)
|
|
41
41
|
return failResult(Errors.noLicenseKey());
|
|
42
|
+
if (license.ok === false)
|
|
43
|
+
return failResult(license.reason === "expired" ? Errors.betaExpired() : Errors.invalidLicense());
|
|
42
44
|
const sitesUsed = state.sites.length;
|
|
43
45
|
const canAddMore = license.sites_allowed < 0 || sitesUsed < license.sites_allowed;
|
|
44
46
|
return ok({
|
|
@@ -71,6 +73,8 @@ export function registerSessionTools(server) {
|
|
|
71
73
|
const license = await validateLicense(process.env.SITEZEN_LICENSE_KEY);
|
|
72
74
|
if (!license)
|
|
73
75
|
return failResult(Errors.noLicenseKey());
|
|
76
|
+
if (license.ok === false)
|
|
77
|
+
return failResult(license.reason === "expired" ? Errors.betaExpired() : Errors.invalidLicense());
|
|
74
78
|
const state = readState();
|
|
75
79
|
const existing = findSite(state, site_url);
|
|
76
80
|
// Enforce site-count limit when adding a NEW site (re-saving the
|
|
@@ -118,6 +122,8 @@ export function registerSessionTools(server) {
|
|
|
118
122
|
const license = await validateLicense(process.env.SITEZEN_LICENSE_KEY);
|
|
119
123
|
if (!license)
|
|
120
124
|
return failResult(Errors.noLicenseKey());
|
|
125
|
+
if (license.ok === false)
|
|
126
|
+
return failResult(license.reason === "expired" ? Errors.betaExpired() : Errors.invalidLicense());
|
|
121
127
|
const state = readState();
|
|
122
128
|
return ok({
|
|
123
129
|
ok: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sitezen-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "SiteZen MCP server — lets Claude Desktop (or any MCP client) drive a SiteZen-enabled WordPress site directly. No Vercel, no platform API, no API-credit burn for the operator. The end user's Claude subscription pays for LLM tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|