@elitedcs/ghl-mcp 3.53.0 → 3.53.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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.53.2 — Fixes a serious 3.53.1 regression: upgrade immediately
4
+
5
+ 3.53.1 resolved the machine identity randomly when nothing was passed in, and
6
+ that is exactly how attestation verification asks for it. Cached attestations
7
+ failed to verify, the tool dropped to bootstrap, and each restart would have
8
+ claimed another install slot until the member hit their cap.
9
+
10
+ - No-argument identity resolution is deterministic again: stored `device_id`
11
+ first, then the fingerprint inside an existing attestation (so an upgrade
12
+ keeps the slot it already holds), then the legacy scheme as a last resort.
13
+ - Two tests pin the determinism so this cannot ship again.
14
+
15
+ If you ran 3.53.1, upgrade. If your install count looks inflated, write to
16
+ support@ghlcommand.com and we will reset it.
17
+
18
+ ## 3.53.1 — Laptops stop eating install slots; "cancelled" stops lying
19
+
20
+ Reported by Carlos Boyd, who hit his install cap with two physical machines.
21
+
22
+ - **Device identity is stable now.** The fingerprint was derived from the
23
+ machine hostname, and a Mac's `.local` hostname changes with the network
24
+ (new Wi-Fi, DHCP, a name collision) — so the same laptop could present a new
25
+ identity and claim another install slot. Identity is now a random id
26
+ persisted in the config dir, so it survives reinstalls and renames. On
27
+ upgrade it ADOPTS the fingerprint from the existing cached attestation, so no
28
+ current install loses the slot it already holds. `legacyDeviceFingerprint()`
29
+ is kept for reference.
30
+ - **Install-limit is no longer reported as "cancelled".** Both conditions come
31
+ back from the gate as 403, and the client labelled both `cancelled`, so a
32
+ full machine list read as a billing failure — two contradictory lines in the
33
+ same block. `install_limit` is its own reason, and the gate prints the
34
+ server's own sentence instead of a reason code beside it.
35
+
3
36
  ## 3.52.1 — Docs: positioning vs. HighLevel's official Anthropic MCP
4
37
 
5
38
  No behavior changes; README only.
@@ -48,7 +48,11 @@ var CredentialsSchema = import_zod.z.object({
48
48
  // hand-crafted-credentials.json bypass that existed through v3.23.0).
49
49
  // Optional in the schema only so files written by earlier versions still
50
50
  // parse — index.ts's gate forces a re-validate when the field is missing.
51
- signed_attestation: import_zod.z.string().optional()
51
+ signed_attestation: import_zod.z.string().optional(),
52
+ /** Stable machine id (2026-07-28). Replaces the hostname-derived
53
+ * fingerprint, which changed with the network on laptops and silently ate
54
+ * install slots. Written once and reused forever. */
55
+ device_id: import_zod.z.string().optional()
52
56
  });
53
57
  function appDataDir() {
54
58
  const override = process.env.GHL_MCP_CONFIG_DIR?.trim();
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
41
41
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
42
42
  mod
43
43
  ));
44
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
44
45
 
45
46
  // src/retry.ts
46
47
  function computeRetryDelay(retryAfterHeader, attempt, baseMs) {
@@ -214,6 +215,16 @@ ${errorBody}`
214
215
  });
215
216
 
216
217
  // src/credentials-store.ts
218
+ var credentials_store_exports = {};
219
+ __export(credentials_store_exports, {
220
+ appDataDir: () => appDataDir,
221
+ credentialsPath: () => credentialsPath,
222
+ ensureAppDataDir: () => ensureAppDataDir,
223
+ foldCredentialsIntoEnv: () => foldCredentialsIntoEnv,
224
+ readCredentials: () => readCredentials,
225
+ tokenRegistryPath: () => tokenRegistryPath,
226
+ writeCredentials: () => writeCredentials
227
+ });
217
228
  function appDataDir() {
218
229
  const override = process.env.GHL_MCP_CONFIG_DIR?.trim();
219
230
  if (override) {
@@ -331,7 +342,11 @@ var init_credentials_store = __esm({
331
342
  // hand-crafted-credentials.json bypass that existed through v3.23.0).
332
343
  // Optional in the schema only so files written by earlier versions still
333
344
  // parse — index.ts's gate forces a re-validate when the field is missing.
334
- signed_attestation: import_zod.z.string().optional()
345
+ signed_attestation: import_zod.z.string().optional(),
346
+ /** Stable machine id (2026-07-28). Replaces the hostname-derived
347
+ * fingerprint, which changed with the network on laptops and silently ate
348
+ * install slots. Written once and reused forever. */
349
+ device_id: import_zod.z.string().optional()
335
350
  });
336
351
  }
337
352
  });
@@ -962,10 +977,184 @@ var init_interactive_capture = __esm({
962
977
  }
963
978
  });
964
979
 
980
+ // src/attestation.ts
981
+ var attestation_exports = {};
982
+ __export(attestation_exports, {
983
+ ATTESTATION_GRACE_DAYS: () => ATTESTATION_GRACE_DAYS,
984
+ ATTESTATION_TTL_DAYS: () => ATTESTATION_TTL_DAYS,
985
+ ATTESTATION_VERSION: () => ATTESTATION_VERSION,
986
+ deviceFingerprint: () => deviceFingerprint,
987
+ legacyDeviceFingerprint: () => legacyDeviceFingerprint,
988
+ parseAttestationPayload: () => parseAttestationPayload,
989
+ planFromPayload: () => planFromPayload,
990
+ shouldRenew: () => shouldRenew,
991
+ tierFromPayload: () => tierFromPayload,
992
+ verifyAttestation: () => verifyAttestation
993
+ });
994
+ function tierFromPayload(payload) {
995
+ return payload?.tier === "free" ? "free" : "full";
996
+ }
997
+ function planFromPayload(payload) {
998
+ return payload?.plan === "command-os" ? "command-os" : "mcp";
999
+ }
1000
+ function legacyDeviceFingerprint() {
1001
+ const os5 = require("node:os");
1002
+ const crypto4 = require("node:crypto");
1003
+ const raw = `${os5.hostname()}:${os5.userInfo().username}:${os5.platform()}:${os5.arch()}`;
1004
+ return crypto4.createHash("sha256").update(raw).digest("hex").slice(0, 16);
1005
+ }
1006
+ function deviceFingerprint(opts) {
1007
+ if (opts?.stored) return opts.stored;
1008
+ if (opts?.cached) {
1009
+ opts.persist?.(opts.cached);
1010
+ return opts.cached;
1011
+ }
1012
+ const crypto4 = require("node:crypto");
1013
+ if (!opts) {
1014
+ try {
1015
+ const store = (init_credentials_store(), __toCommonJS(credentials_store_exports));
1016
+ const creds = store.readCredentials();
1017
+ if (creds?.device_id) return creds.device_id;
1018
+ const adopted = creds?.signed_attestation ? parseAttestationPayload(creds.signed_attestation)?.device_fingerprint : void 0;
1019
+ const id = adopted || crypto4.randomBytes(8).toString("hex");
1020
+ if (creds) {
1021
+ try {
1022
+ store.writeCredentials({ ...creds, device_id: id });
1023
+ } catch {
1024
+ }
1025
+ }
1026
+ return id;
1027
+ } catch {
1028
+ return legacyDeviceFingerprint();
1029
+ }
1030
+ }
1031
+ const fresh = crypto4.randomBytes(8).toString("hex");
1032
+ opts.persist?.(fresh);
1033
+ return fresh;
1034
+ }
1035
+ function base64urlDecode(s) {
1036
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
1037
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
1038
+ return new Uint8Array(Buffer.from(padded, "base64"));
1039
+ }
1040
+ async function getPublicKey() {
1041
+ if (cachedPublicKey) return cachedPublicKey;
1042
+ const raw = Buffer.from(ATTESTATION_PUBLIC_KEY_B64, "base64");
1043
+ cachedPublicKey = await import_node_crypto.webcrypto.subtle.importKey(
1044
+ "raw",
1045
+ raw,
1046
+ { name: "Ed25519" },
1047
+ false,
1048
+ ["verify"]
1049
+ );
1050
+ return cachedPublicKey;
1051
+ }
1052
+ function parseAttestationPayload(token) {
1053
+ try {
1054
+ const dot = token.indexOf(".");
1055
+ if (dot <= 0 || dot === token.length - 1) return null;
1056
+ const payloadJson = new TextDecoder().decode(base64urlDecode(token.slice(0, dot)));
1057
+ const payload = JSON.parse(payloadJson);
1058
+ if (typeof payload.v !== "number" || typeof payload.email !== "string" || typeof payload.license_key !== "string" || typeof payload.device_fingerprint !== "string" || typeof payload.installs_max !== "number" || typeof payload.issued_at !== "string" || typeof payload.expires_at !== "string") {
1059
+ return null;
1060
+ }
1061
+ return payload;
1062
+ } catch {
1063
+ return null;
1064
+ }
1065
+ }
1066
+ async function verifyAttestation(token, bindings, now = /* @__PURE__ */ new Date()) {
1067
+ if (typeof token !== "string" || !token) return { ok: false, reason: "malformed" };
1068
+ const dot = token.indexOf(".");
1069
+ if (dot <= 0 || dot === token.length - 1) return { ok: false, reason: "malformed" };
1070
+ let payloadBytes;
1071
+ let signatureBytes;
1072
+ try {
1073
+ payloadBytes = base64urlDecode(token.slice(0, dot));
1074
+ signatureBytes = base64urlDecode(token.slice(dot + 1));
1075
+ } catch {
1076
+ return { ok: false, reason: "malformed" };
1077
+ }
1078
+ let payload;
1079
+ try {
1080
+ const parsed = JSON.parse(new TextDecoder().decode(payloadBytes));
1081
+ if (typeof parsed.v !== "number") return { ok: false, reason: "malformed" };
1082
+ if (parsed.v !== ATTESTATION_VERSION) return { ok: false, reason: "unsupported-version" };
1083
+ payload = parsed;
1084
+ } catch {
1085
+ return { ok: false, reason: "malformed" };
1086
+ }
1087
+ let signatureValid;
1088
+ try {
1089
+ const key = await getPublicKey();
1090
+ signatureValid = await import_node_crypto.webcrypto.subtle.verify({ name: "Ed25519" }, key, signatureBytes, payloadBytes);
1091
+ } catch {
1092
+ return { ok: false, reason: "bad-signature" };
1093
+ }
1094
+ if (!signatureValid) return { ok: false, reason: "bad-signature" };
1095
+ if (payload.email.toLowerCase() !== bindings.email.toLowerCase()) {
1096
+ return { ok: false, reason: "wrong-email" };
1097
+ }
1098
+ if (payload.license_key !== bindings.license_key) {
1099
+ return { ok: false, reason: "wrong-license" };
1100
+ }
1101
+ const fingerprint = bindings.expectedFingerprint ?? deviceFingerprint();
1102
+ if (payload.device_fingerprint !== fingerprint) {
1103
+ return { ok: false, reason: "wrong-device" };
1104
+ }
1105
+ const expiry = Date.parse(payload.expires_at);
1106
+ if (!Number.isFinite(expiry)) return { ok: false, reason: "malformed" };
1107
+ const graceDeadline = expiry + ATTESTATION_GRACE_DAYS * 24 * 60 * 60 * 1e3;
1108
+ if (now.getTime() <= expiry) {
1109
+ return { ok: true, payload, reason: "valid" };
1110
+ }
1111
+ if (now.getTime() <= graceDeadline) {
1112
+ return { ok: true, payload, reason: "in-grace" };
1113
+ }
1114
+ return { ok: false, reason: "expired" };
1115
+ }
1116
+ function shouldRenew(payload, now = /* @__PURE__ */ new Date()) {
1117
+ const expiry = Date.parse(payload.expires_at);
1118
+ if (!Number.isFinite(expiry)) return true;
1119
+ const remainingMs = expiry - now.getTime();
1120
+ return remainingMs < 7 * 24 * 60 * 60 * 1e3;
1121
+ }
1122
+ var import_node_crypto, ATTESTATION_PUBLIC_KEY_B64, ATTESTATION_VERSION, ATTESTATION_TTL_DAYS, ATTESTATION_GRACE_DAYS, cachedPublicKey;
1123
+ var init_attestation = __esm({
1124
+ "src/attestation.ts"() {
1125
+ "use strict";
1126
+ import_node_crypto = require("node:crypto");
1127
+ ATTESTATION_PUBLIC_KEY_B64 = "8KJo8V3Z7ngeToIQfOXpKdlr/EA34hXJVEIpW0A7wqs=";
1128
+ ATTESTATION_VERSION = 1;
1129
+ ATTESTATION_TTL_DAYS = 30;
1130
+ ATTESTATION_GRACE_DAYS = 14;
1131
+ cachedPublicKey = null;
1132
+ }
1133
+ });
1134
+
965
1135
  // src/setup-tool.ts
966
- function deviceFingerprint() {
967
- const raw = `${os2.hostname()}:${os2.userInfo().username}:${os2.platform()}:${os2.arch()}`;
968
- return crypto2.createHash("sha256").update(raw).digest("hex").slice(0, 16);
1136
+ function deviceFingerprint2() {
1137
+ try {
1138
+ const store = (init_credentials_store(), __toCommonJS(credentials_store_exports));
1139
+ const creds = store.readCredentials();
1140
+ if (creds?.device_id) return creds.device_id;
1141
+ let adopted;
1142
+ if (creds?.signed_attestation) {
1143
+ const att = (init_attestation(), __toCommonJS(attestation_exports));
1144
+ adopted = att.parseAttestationPayload(creds.signed_attestation)?.device_fingerprint;
1145
+ }
1146
+ const id = adopted || crypto2.randomBytes(8).toString("hex");
1147
+ if (creds) {
1148
+ try {
1149
+ store.writeCredentials({ ...creds, device_id: id });
1150
+ } catch {
1151
+ }
1152
+ }
1153
+ return id;
1154
+ } catch {
1155
+ const raw = `${os2.userInfo().username}:${os2.platform()}:${os2.arch()}`;
1156
+ return crypto2.createHash("sha256").update(raw).digest("hex").slice(0, 16);
1157
+ }
969
1158
  }
970
1159
  async function validateLicense(email, licenseKey) {
971
1160
  let res;
@@ -976,7 +1165,7 @@ async function validateLicense(email, licenseKey) {
976
1165
  body: JSON.stringify({
977
1166
  email: email.trim(),
978
1167
  license_key: licenseKey.trim(),
979
- device_fingerprint: deviceFingerprint()
1168
+ device_fingerprint: deviceFingerprint2()
980
1169
  }),
981
1170
  signal: AbortSignal.timeout(1e4)
982
1171
  });
@@ -997,11 +1186,12 @@ async function validateLicense(email, licenseKey) {
997
1186
  };
998
1187
  }
999
1188
  const errorText = String(data.error || data.message || "").toLowerCase();
1000
- const looksCancelled = res.status === 403 || errorText.includes("cancel") || errorText.includes("install limit");
1189
+ const isInstallLimit = errorText.includes("install limit") || errorText.includes("installs");
1190
+ const looksCancelled = !isInstallLimit && (res.status === 403 || errorText.includes("cancel"));
1001
1191
  return {
1002
1192
  ok: false,
1003
1193
  error: data.error || data.message || `License validation failed (HTTP ${res.status})`,
1004
- reason: looksCancelled ? "cancelled" : "unauthorized"
1194
+ reason: isInstallLimit ? "install_limit" : looksCancelled ? "cancelled" : "unauthorized"
1005
1195
  };
1006
1196
  }
1007
1197
  async function validateGhl(apiKey2, locationId2) {
@@ -1907,118 +2097,12 @@ var init_question_set = __esm({
1907
2097
  }
1908
2098
  });
1909
2099
 
1910
- // src/attestation.ts
1911
- function tierFromPayload(payload) {
1912
- return payload?.tier === "free" ? "free" : "full";
1913
- }
1914
- function planFromPayload(payload) {
1915
- return payload?.plan === "command-os" ? "command-os" : "mcp";
1916
- }
1917
- function deviceFingerprint2() {
1918
- const os5 = require("node:os");
1919
- const crypto4 = require("node:crypto");
1920
- const raw = `${os5.hostname()}:${os5.userInfo().username}:${os5.platform()}:${os5.arch()}`;
1921
- return crypto4.createHash("sha256").update(raw).digest("hex").slice(0, 16);
1922
- }
1923
- function base64urlDecode(s) {
1924
- const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
1925
- const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
1926
- return new Uint8Array(Buffer.from(padded, "base64"));
1927
- }
1928
- async function getPublicKey() {
1929
- if (cachedPublicKey) return cachedPublicKey;
1930
- const raw = Buffer.from(ATTESTATION_PUBLIC_KEY_B64, "base64");
1931
- cachedPublicKey = await import_node_crypto.webcrypto.subtle.importKey(
1932
- "raw",
1933
- raw,
1934
- { name: "Ed25519" },
1935
- false,
1936
- ["verify"]
1937
- );
1938
- return cachedPublicKey;
1939
- }
1940
- function parseAttestationPayload(token) {
1941
- try {
1942
- const dot = token.indexOf(".");
1943
- if (dot <= 0 || dot === token.length - 1) return null;
1944
- const payloadJson = new TextDecoder().decode(base64urlDecode(token.slice(0, dot)));
1945
- const payload = JSON.parse(payloadJson);
1946
- if (typeof payload.v !== "number" || typeof payload.email !== "string" || typeof payload.license_key !== "string" || typeof payload.device_fingerprint !== "string" || typeof payload.installs_max !== "number" || typeof payload.issued_at !== "string" || typeof payload.expires_at !== "string") {
1947
- return null;
1948
- }
1949
- return payload;
1950
- } catch {
1951
- return null;
1952
- }
1953
- }
1954
- async function verifyAttestation(token, bindings, now = /* @__PURE__ */ new Date()) {
1955
- if (typeof token !== "string" || !token) return { ok: false, reason: "malformed" };
1956
- const dot = token.indexOf(".");
1957
- if (dot <= 0 || dot === token.length - 1) return { ok: false, reason: "malformed" };
1958
- let payloadBytes;
1959
- let signatureBytes;
1960
- try {
1961
- payloadBytes = base64urlDecode(token.slice(0, dot));
1962
- signatureBytes = base64urlDecode(token.slice(dot + 1));
1963
- } catch {
1964
- return { ok: false, reason: "malformed" };
1965
- }
1966
- let payload;
1967
- try {
1968
- const parsed = JSON.parse(new TextDecoder().decode(payloadBytes));
1969
- if (typeof parsed.v !== "number") return { ok: false, reason: "malformed" };
1970
- if (parsed.v !== ATTESTATION_VERSION) return { ok: false, reason: "unsupported-version" };
1971
- payload = parsed;
1972
- } catch {
1973
- return { ok: false, reason: "malformed" };
1974
- }
1975
- let signatureValid;
1976
- try {
1977
- const key = await getPublicKey();
1978
- signatureValid = await import_node_crypto.webcrypto.subtle.verify({ name: "Ed25519" }, key, signatureBytes, payloadBytes);
1979
- } catch {
1980
- return { ok: false, reason: "bad-signature" };
1981
- }
1982
- if (!signatureValid) return { ok: false, reason: "bad-signature" };
1983
- if (payload.email.toLowerCase() !== bindings.email.toLowerCase()) {
1984
- return { ok: false, reason: "wrong-email" };
1985
- }
1986
- if (payload.license_key !== bindings.license_key) {
1987
- return { ok: false, reason: "wrong-license" };
1988
- }
1989
- const fingerprint = bindings.expectedFingerprint ?? deviceFingerprint2();
1990
- if (payload.device_fingerprint !== fingerprint) {
1991
- return { ok: false, reason: "wrong-device" };
1992
- }
1993
- const expiry = Date.parse(payload.expires_at);
1994
- if (!Number.isFinite(expiry)) return { ok: false, reason: "malformed" };
1995
- const graceDeadline = expiry + ATTESTATION_GRACE_DAYS * 24 * 60 * 60 * 1e3;
1996
- if (now.getTime() <= expiry) {
1997
- return { ok: true, payload, reason: "valid" };
1998
- }
1999
- if (now.getTime() <= graceDeadline) {
2000
- return { ok: true, payload, reason: "in-grace" };
2001
- }
2002
- return { ok: false, reason: "expired" };
2003
- }
2004
- var import_node_crypto, ATTESTATION_PUBLIC_KEY_B64, ATTESTATION_VERSION, ATTESTATION_GRACE_DAYS, cachedPublicKey;
2005
- var init_attestation = __esm({
2006
- "src/attestation.ts"() {
2007
- "use strict";
2008
- import_node_crypto = require("node:crypto");
2009
- ATTESTATION_PUBLIC_KEY_B64 = "8KJo8V3Z7ngeToIQfOXpKdlr/EA34hXJVEIpW0A7wqs=";
2010
- ATTESTATION_VERSION = 1;
2011
- ATTESTATION_GRACE_DAYS = 14;
2012
- cachedPublicKey = null;
2013
- }
2014
- });
2015
-
2016
2100
  // package.json
2017
2101
  var require_package = __commonJS({
2018
2102
  "package.json"(exports2, module2) {
2019
2103
  module2.exports = {
2020
2104
  name: "@elitedcs/ghl-mcp",
2021
- version: "3.53.0",
2105
+ version: "3.53.2",
2022
2106
  mcpName: "io.github.drjerryrelth/ghl-command",
2023
2107
  description: "GoHighLevel MCP Server for Claude. 232 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
2024
2108
  main: "dist/index.js",
@@ -16986,10 +17070,11 @@ async function renewAttestation(creds) {
16986
17070
  }
16987
17071
  return "renewed";
16988
17072
  }
16989
- if (lic.reason === "cancelled" || lic.reason === "unauthorized") {
17073
+ if (lic.reason === "cancelled" || lic.reason === "unauthorized" || lic.reason === "install_limit") {
16990
17074
  try {
16991
17075
  writeCredentials({ ...creds, signed_attestation: void 0 });
16992
- process.stderr.write(`[ghl-mcp] Server rejected license (${lic.reason}). Dropping to bootstrap mode.
17076
+ process.stderr.write(lic.reason === "install_limit" ? `[ghl-mcp] ${lic.error}
17077
+ ` : `[ghl-mcp] ${lic.error} Dropping to bootstrap mode.
16993
17078
  `);
16994
17079
  } catch {
16995
17080
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.53.0",
3
+ "version": "3.53.2",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
- "description": "GoHighLevel MCP Server for Claude. 232 tools full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
5
+ "description": "GoHighLevel MCP Server for Claude. 232 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "ghl-mcp": "dist/index.js"