@truealter/sdk 0.4.1 → 0.5.1

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/index.cjs CHANGED
@@ -3,14 +3,14 @@
3
3
  var p256 = require('@noble/curves/p256');
4
4
  var sha256 = require('@noble/hashes/sha256');
5
5
  var utils = require('@noble/hashes/utils');
6
+ var crypto$1 = require('crypto');
6
7
  var ed25519 = require('@noble/ed25519');
7
8
  var sha512 = require('@noble/hashes/sha512');
8
- var child_process = require('child_process');
9
- var os = require('os');
9
+ var fs = require('fs');
10
10
  var path = require('path');
11
+ var os = require('os');
12
+ var child_process = require('child_process');
11
13
  var process$1 = require('process');
12
- var fs = require('fs');
13
- var crypto$1 = require('crypto');
14
14
 
15
15
  function _interopNamespace(e) {
16
16
  if (e && e.__esModule) return e;
@@ -132,8 +132,7 @@ function loadPrivateKey(key) {
132
132
  return key;
133
133
  }
134
134
  if (typeof key === "string" && key.includes("-----BEGIN")) {
135
- const nodeCrypto = __require("crypto");
136
- const keyObj = nodeCrypto.createPrivateKey({ key, format: "pem" });
135
+ const keyObj = crypto$1.createPrivateKey({ key, format: "pem" });
137
136
  const jwk = keyObj.export({ format: "jwk" });
138
137
  if (jwk.crv !== "P-256" || !jwk.d) {
139
138
  throw new TypeError("PEM is not a P-256 private key.");
@@ -457,11 +456,19 @@ var X402Client = class {
457
456
  maxPerQuery;
458
457
  networks;
459
458
  assets;
459
+ // undefined = allowlist check disabled (backward-compatible default).
460
+ // Non-null = active allowlist; reject any recipient not in the set.
461
+ recipientAllowlist;
460
462
  constructor(opts = {}) {
461
463
  this.signer = opts.signer;
462
464
  this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
463
465
  this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
464
466
  this.assets = new Set(opts.assets ?? ["USDC"]);
467
+ if (opts.recipientAllowlist !== void 0) {
468
+ this.recipientAllowlist = opts.recipientAllowlist.length === 0 ? void 0 : new Set(opts.recipientAllowlist.map((a) => a.toLowerCase()));
469
+ } else {
470
+ this.recipientAllowlist = void 0;
471
+ }
465
472
  }
466
473
  /**
467
474
  * Validate the envelope against this client's policy and, if a signer
@@ -487,6 +494,15 @@ var X402Client = class {
487
494
  );
488
495
  }
489
496
  }
497
+ if (this.recipientAllowlist !== void 0) {
498
+ const recipientNorm = (envelope.recipient ?? "").toLowerCase();
499
+ if (!recipientNorm || !this.recipientAllowlist.has(recipientNorm)) {
500
+ throw new AlterError(
501
+ "PAYMENT_REQUIRED",
502
+ `recipient "${envelope.recipient}" is not on the known-recipient allowlist`
503
+ );
504
+ }
505
+ }
490
506
  if (!this.signer) {
491
507
  throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
492
508
  }
@@ -544,6 +560,7 @@ var MCPClient = class {
544
560
  clientInfo;
545
561
  x402;
546
562
  signing;
563
+ extraHeaders;
547
564
  requestCounter = 0;
548
565
  initialised = false;
549
566
  constructor(opts = {}) {
@@ -555,6 +572,7 @@ var MCPClient = class {
555
572
  this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
556
573
  this.x402 = opts.x402;
557
574
  this.signing = opts.signing;
575
+ this.extraHeaders = opts.extraHeaders;
558
576
  }
559
577
  /**
560
578
  * Send the MCP `initialize` handshake and capture the resulting session
@@ -718,6 +736,7 @@ var MCPClient = class {
718
736
  }
719
737
  buildHeaders(extra) {
720
738
  const headers = {
739
+ ...this.extraHeaders ?? {},
721
740
  "Content-Type": "application/json",
722
741
  Accept: "application/json",
723
742
  "User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`
@@ -864,6 +883,7 @@ var DEFAULT_VERIFY_AT_ALLOWLIST = Object.freeze([
864
883
  "api.truealter.com",
865
884
  "mcp.truealter.com"
866
885
  ]);
886
+ var ALTER_PLATFORM_ISS = "did:alter:platform";
867
887
  async function verifyProvenance(envelope, opts = {}) {
868
888
  const token = typeof envelope === "string" ? envelope : envelope.token;
869
889
  if (!token) return { valid: false, reason: "empty token" };
@@ -946,9 +966,55 @@ async function verifyProvenance(envelope, opts = {}) {
946
966
  if (typeof payload.iat === "number" && payload.iat > now + 300) {
947
967
  return { valid: false, reason: "issued in the future", payload, kid: header.kid };
948
968
  }
969
+ const expectedIss = opts.expectedIss !== void 0 ? opts.expectedIss : ALTER_PLATFORM_ISS;
970
+ if (expectedIss !== "" && payload.iss !== expectedIss) {
971
+ return {
972
+ valid: false,
973
+ reason: `iss mismatch: expected "${expectedIss}", got "${payload.iss}"`,
974
+ payload,
975
+ kid: header.kid
976
+ };
977
+ }
978
+ if (opts.expectedAud !== void 0 && opts.expectedAud !== "") {
979
+ const tokenAud = payload.aud;
980
+ const audList = tokenAud === void 0 ? [] : Array.isArray(tokenAud) ? tokenAud : [tokenAud];
981
+ if (!audList.includes(opts.expectedAud)) {
982
+ return {
983
+ valid: false,
984
+ reason: `aud mismatch: expected "${opts.expectedAud}", got ${JSON.stringify(tokenAud ?? null)}`,
985
+ payload,
986
+ kid: header.kid
987
+ };
988
+ }
989
+ }
949
990
  return { valid: true, payload, kid: header.kid };
950
991
  }
951
- async function verifyToolSignatures(tools, signatures) {
992
+ async function verifyToolSignatures(tools, signatures, opts = {}) {
993
+ const jwksUrl = opts.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
994
+ const fetchImpl = opts.fetch ?? fetch;
995
+ if (!jwksUrl.startsWith("https://")) {
996
+ return tools.map((t) => ({
997
+ tool: t.name,
998
+ valid: false,
999
+ reason: `jwksUrl must be https: got ${jwksUrl}`
1000
+ }));
1001
+ }
1002
+ const needsJwks = tools.some((t) => {
1003
+ const sig = signatures[t.name];
1004
+ return sig && sig.signature;
1005
+ });
1006
+ let jwks = null;
1007
+ if (needsJwks) {
1008
+ try {
1009
+ jwks = await fetchJwks(jwksUrl, fetchImpl);
1010
+ } catch (err) {
1011
+ return tools.map((t) => ({
1012
+ tool: t.name,
1013
+ valid: false,
1014
+ reason: `jwks fetch failed: ${err.message}`
1015
+ }));
1016
+ }
1017
+ }
952
1018
  const out = [];
953
1019
  for (const tool of tools) {
954
1020
  const sig = signatures[tool.name];
@@ -961,6 +1027,63 @@ async function verifyToolSignatures(tools, signatures) {
961
1027
  out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
962
1028
  continue;
963
1029
  }
1030
+ const jwsToken = sig.signature;
1031
+ if (!jwsToken) {
1032
+ out.push({ tool: tool.name, valid: true, warn_no_signature: true });
1033
+ continue;
1034
+ }
1035
+ const jwksDoc = jwks;
1036
+ let jHeader;
1037
+ let jPayloadRaw;
1038
+ let jSigBytes;
1039
+ try {
1040
+ const parts2 = jwsToken.split(".");
1041
+ if (parts2.length !== 3) throw new Error("JWS must have three segments");
1042
+ jHeader = JSON.parse(new TextDecoder().decode(base64urlDecode(parts2[0])));
1043
+ jPayloadRaw = new TextDecoder().decode(base64urlDecode(parts2[1]));
1044
+ jSigBytes = base64urlDecode(parts2[2]);
1045
+ } catch (err) {
1046
+ out.push({ tool: tool.name, valid: false, reason: `malformed tool JWS: ${err.message}` });
1047
+ continue;
1048
+ }
1049
+ if (jHeader.alg !== "ES256") {
1050
+ out.push({ tool: tool.name, valid: false, reason: `unsupported tool sig alg: ${jHeader.alg}` });
1051
+ continue;
1052
+ }
1053
+ if (jPayloadRaw !== sig.schema_hash) {
1054
+ out.push({ tool: tool.name, valid: false, reason: "tool JWS payload does not match schema_hash" });
1055
+ continue;
1056
+ }
1057
+ const jwk = jwksDoc.keys.find((k) => jHeader.kid ? k.kid === jHeader.kid : true);
1058
+ if (!jwk) {
1059
+ out.push({ tool: tool.name, valid: false, reason: `no JWK for kid=${jHeader.kid}` });
1060
+ continue;
1061
+ }
1062
+ let publicKey;
1063
+ try {
1064
+ publicKey = await importEs256JwkAsPublicKey(jwk);
1065
+ } catch (err) {
1066
+ out.push({ tool: tool.name, valid: false, reason: `jwk import: ${err.message}` });
1067
+ continue;
1068
+ }
1069
+ const parts = jwsToken.split(".");
1070
+ const signedInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
1071
+ let sigValid = false;
1072
+ try {
1073
+ sigValid = await crypto.subtle.verify(
1074
+ { name: "ECDSA", hash: "SHA-256" },
1075
+ publicKey,
1076
+ toArrayBuffer(jSigBytes),
1077
+ toArrayBuffer(signedInput)
1078
+ );
1079
+ } catch (err) {
1080
+ out.push({ tool: tool.name, valid: false, reason: `sig verify error: ${err.message}` });
1081
+ continue;
1082
+ }
1083
+ if (!sigValid) {
1084
+ out.push({ tool: tool.name, valid: false, reason: "tool signature mismatch" });
1085
+ continue;
1086
+ }
964
1087
  out.push({ tool: tool.name, valid: true });
965
1088
  }
966
1089
  return out;
@@ -1142,10 +1265,10 @@ var AlterClient = class {
1142
1265
  }
1143
1266
  /** Verify a person is registered with ALTER (handle or id). */
1144
1267
  async verify(handleOrId, claims) {
1145
- const args = handleOrId.includes("@") ? { candidate_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
1146
- // ~handle — server resolves these via the candidate_id field
1147
- { candidate_id: handleOrId }
1148
- ) : { candidate_id: handleOrId };
1268
+ const args = handleOrId.includes("@") ? { member_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
1269
+ // ~handle — server resolves these via the member_id field
1270
+ { member_id: handleOrId }
1271
+ ) : { member_id: handleOrId };
1149
1272
  if (claims) args.claims = claims;
1150
1273
  return this.mcp.callTool("verify_identity", args);
1151
1274
  }
@@ -1620,6 +1743,26 @@ function restoreFromBackup(path, backupPath) {
1620
1743
  // src/wire/index.ts
1621
1744
  var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
1622
1745
  var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
1746
+ function readCfAccessEnv() {
1747
+ const envPath = path.join(os.homedir(), ".config", "alter", "cf-access.env");
1748
+ try {
1749
+ const content = fs.readFileSync(envPath, "utf8");
1750
+ let clientId = "";
1751
+ let clientSecret = "";
1752
+ for (const line of content.split("\n")) {
1753
+ const trimmed = line.trim();
1754
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
1755
+ const eqIdx = trimmed.indexOf("=");
1756
+ const key = trimmed.slice(0, eqIdx).replace(/^export\s+/, "").trim();
1757
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
1758
+ if (key === "CF_ACCESS_CLIENT_ID") clientId = val;
1759
+ if (key === "CF_ACCESS_CLIENT_SECRET") clientSecret = val;
1760
+ }
1761
+ if (clientId && clientSecret) return { clientId, clientSecret };
1762
+ } catch {
1763
+ }
1764
+ return void 0;
1765
+ }
1623
1766
  function clientById(id) {
1624
1767
  const hit = ALL_CLIENTS.find((c) => c.id === id);
1625
1768
  if (!hit) throw new Error(`unknown client id: ${id}`);
@@ -1628,6 +1771,7 @@ function clientById(id) {
1628
1771
  function wire(opts = {}) {
1629
1772
  const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
1630
1773
  const apiKey = opts.apiKey;
1774
+ const cfAccess = opts.cfAccess ?? readCfAccessEnv();
1631
1775
  const probes = probeAll();
1632
1776
  const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
1633
1777
  const ts = TIMESTAMP();
@@ -1646,9 +1790,9 @@ function wire(opts = {}) {
1646
1790
  }
1647
1791
  try {
1648
1792
  if (id === "claude-code") {
1649
- targets.push(wireClaudeCode({ endpoint, apiKey }));
1793
+ targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
1650
1794
  } else {
1651
- targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
1795
+ targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
1652
1796
  }
1653
1797
  } catch (err) {
1654
1798
  const message = err.message;
@@ -1682,7 +1826,12 @@ function wireFileTarget(args) {
1682
1826
  `refusing to wire ${client.label}: config path ${sync.resolvedPath} lives under ${sync.matchedPrefix}. Synced volumes propagate credentials across devices \u2014 move the config off the sync root, or run wire on the device you want to target.`
1683
1827
  );
1684
1828
  }
1685
- const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
1829
+ const cfHeaders = {};
1830
+ if (args.cfAccess) {
1831
+ cfHeaders["CF-Access-Client-Id"] = args.cfAccess.clientId;
1832
+ cfHeaders["CF-Access-Client-Secret"] = args.cfAccess.clientSecret;
1833
+ }
1834
+ const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey, headers: cfHeaders });
1686
1835
  const rootKey = client.rootKey;
1687
1836
  const serverName = "alter";
1688
1837
  const result = atomicJsonMerge({
@@ -1714,7 +1863,8 @@ function wireFileTarget(args) {
1714
1863
  }
1715
1864
  function wireClaudeCode(args) {
1716
1865
  const cmd = "claude";
1717
- const argList = [
1866
+ const bridgePath = resolveBridgeScript();
1867
+ const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
1718
1868
  "mcp",
1719
1869
  "add",
1720
1870
  "--scope",
@@ -1722,16 +1872,15 @@ function wireClaudeCode(args) {
1722
1872
  "--transport",
1723
1873
  "http",
1724
1874
  "alter",
1725
- args.endpoint
1875
+ args.endpoint,
1876
+ ...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
1726
1877
  ];
1727
- if (args.apiKey) {
1728
- argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
1729
- }
1730
1878
  const full = `${cmd} ${argList.join(" ")}`;
1731
1879
  const run = child_process.spawnSync(cmd, argList, {
1732
1880
  encoding: "utf8",
1733
1881
  shell: process.platform === "win32",
1734
- timeout: 1e4
1882
+ timeout: 1e4,
1883
+ env: bridgePath ? { ...process.env, ALTER_PUBLIC_MCP_ENDPOINT: args.endpoint, ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {} } : void 0
1735
1884
  });
1736
1885
  if (run.error) {
1737
1886
  return {
@@ -1762,6 +1911,17 @@ function wireClaudeCode(args) {
1762
1911
  reason: `claude mcp add exited ${String(run.status)}`
1763
1912
  };
1764
1913
  }
1914
+ function resolveBridgeScript() {
1915
+ const { existsSync: existsSync4 } = __require("fs");
1916
+ const { join: join3, dirname: dirname3 } = __require("path");
1917
+ const siblingBridge = join3(dirname3(__filename), "..", "dist", "mcp-bridge.js");
1918
+ if (existsSync4(siblingBridge)) return siblingBridge;
1919
+ const srcBridge = join3(dirname3(__filename), "..", "mcp-bridge.js");
1920
+ if (existsSync4(srcBridge)) return srcBridge;
1921
+ const npmGlobalBridge = join3(dirname3(__filename), "mcp-bridge.js");
1922
+ if (existsSync4(npmGlobalBridge)) return npmGlobalBridge;
1923
+ return null;
1924
+ }
1765
1925
  function unwire() {
1766
1926
  const state = readWireState();
1767
1927
  const undone = [];
@@ -1965,6 +2125,26 @@ var TOOL_BLAST_RADIUS = {
1965
2125
  query_graph_similarity: "high"
1966
2126
  };
1967
2127
 
2128
+ // src/homepage.ts
2129
+ init_cjs_shims();
2130
+ var HOMEPAGE_LIMITS = {
2131
+ whoami_max_chars: 240,
2132
+ opener_max_chars: 280,
2133
+ pronouns_max_chars: 32,
2134
+ attunement_glyph_max_chars: 16
2135
+ };
2136
+
2137
+ // src/themes.ts
2138
+ init_cjs_shims();
2139
+ var THEME_LIMITS = {
2140
+ meta_name_pattern: /^[a-z][a-z0-9-]{0,63}$/,
2141
+ meta_description_max_chars: 240,
2142
+ opener_library_max_entries: 32,
2143
+ opener_entry_max_chars: 240,
2144
+ share_note_max_chars: 280
2145
+ };
2146
+ var OSC8_ALLOWED_SCHEMES = ["https:", "mailto:"];
2147
+
1968
2148
  exports.ALL_CLIENTS = ALL_CLIENTS;
1969
2149
  exports.AlterAuthError = AlterAuthError;
1970
2150
  exports.AlterClient = AlterClient;
@@ -1984,11 +2164,14 @@ exports.DEFAULT_DOMAIN = DEFAULT_DOMAIN;
1984
2164
  exports.DEFAULT_ENDPOINT = DEFAULT_ENDPOINT;
1985
2165
  exports.DEFAULT_VERIFY_AT_ALLOWLIST = DEFAULT_VERIFY_AT_ALLOWLIST;
1986
2166
  exports.FREE_TOOL_NAMES = FREE_TOOL_NAMES;
2167
+ exports.HOMEPAGE_LIMITS = HOMEPAGE_LIMITS;
1987
2168
  exports.MCPClient = MCPClient;
1988
2169
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
2170
+ exports.OSC8_ALLOWED_SCHEMES = OSC8_ALLOWED_SCHEMES;
1989
2171
  exports.PREMIUM_TOOL_NAMES = PREMIUM_TOOL_NAMES;
1990
2172
  exports.SDK_NAME = SDK_NAME;
1991
2173
  exports.SDK_VERSION = SDK_VERSION;
2174
+ exports.THEME_LIMITS = THEME_LIMITS;
1992
2175
  exports.TOOL_BLAST_RADIUS = TOOL_BLAST_RADIUS;
1993
2176
  exports.TOOL_COSTS = TOOL_COSTS;
1994
2177
  exports.TOOL_TIERS = TOOL_TIERS;