@truealter/sdk 0.5.0 → 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.js CHANGED
@@ -4,16 +4,22 @@ import { randomBytes, bytesToHex as bytesToHex$1, hexToBytes } from '@noble/hash
4
4
  import { createPrivateKey, createHash } from 'crypto';
5
5
  import * as ed25519 from '@noble/ed25519';
6
6
  import { sha512 } from '@noble/hashes/sha512';
7
- import { spawnSync } from 'child_process';
8
- import { homedir, platform } from 'os';
7
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, renameSync, unlinkSync } from 'fs';
9
8
  import { join, resolve, dirname } from 'path';
9
+ import { homedir, platform } from 'os';
10
+ import { spawnSync } from 'child_process';
10
11
  import { env } from 'process';
11
- import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, renameSync, unlinkSync } from 'fs';
12
12
 
13
13
  var __defProp = Object.defineProperty;
14
14
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
15
15
  var __getOwnPropNames = Object.getOwnPropertyNames;
16
16
  var __hasOwnProp = Object.prototype.hasOwnProperty;
17
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
18
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
19
+ }) : x)(function(x) {
20
+ if (typeof require !== "undefined") return require.apply(this, arguments);
21
+ throw Error('Dynamic require of "' + x + '" is not supported');
22
+ });
17
23
  var __esm = (fn, res) => function __init() {
18
24
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
25
  };
@@ -400,18 +406,24 @@ function ensureMcpPath(url) {
400
406
  return url;
401
407
  }
402
408
  }
403
-
404
- // src/x402.ts
405
409
  var X402Client = class {
406
410
  signer;
407
411
  maxPerQuery;
408
412
  networks;
409
413
  assets;
414
+ // undefined = allowlist check disabled (backward-compatible default).
415
+ // Non-null = active allowlist; reject any recipient not in the set.
416
+ recipientAllowlist;
410
417
  constructor(opts = {}) {
411
418
  this.signer = opts.signer;
412
419
  this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
413
420
  this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
414
421
  this.assets = new Set(opts.assets ?? ["USDC"]);
422
+ if (opts.recipientAllowlist !== void 0) {
423
+ this.recipientAllowlist = opts.recipientAllowlist.length === 0 ? void 0 : new Set(opts.recipientAllowlist.map((a) => a.toLowerCase()));
424
+ } else {
425
+ this.recipientAllowlist = void 0;
426
+ }
415
427
  }
416
428
  /**
417
429
  * Validate the envelope against this client's policy and, if a signer
@@ -437,6 +449,15 @@ var X402Client = class {
437
449
  );
438
450
  }
439
451
  }
452
+ if (this.recipientAllowlist !== void 0) {
453
+ const recipientNorm = (envelope.recipient ?? "").toLowerCase();
454
+ if (!recipientNorm || !this.recipientAllowlist.has(recipientNorm)) {
455
+ throw new AlterError(
456
+ "PAYMENT_REQUIRED",
457
+ `recipient "${envelope.recipient}" is not on the known-recipient allowlist`
458
+ );
459
+ }
460
+ }
440
461
  if (!this.signer) {
441
462
  throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
442
463
  }
@@ -903,9 +924,46 @@ async function verifyProvenance(envelope, opts = {}) {
903
924
  kid: header.kid
904
925
  };
905
926
  }
927
+ if (opts.expectedAud !== void 0 && opts.expectedAud !== "") {
928
+ const tokenAud = payload.aud;
929
+ const audList = tokenAud === void 0 ? [] : Array.isArray(tokenAud) ? tokenAud : [tokenAud];
930
+ if (!audList.includes(opts.expectedAud)) {
931
+ return {
932
+ valid: false,
933
+ reason: `aud mismatch: expected "${opts.expectedAud}", got ${JSON.stringify(tokenAud ?? null)}`,
934
+ payload,
935
+ kid: header.kid
936
+ };
937
+ }
938
+ }
906
939
  return { valid: true, payload, kid: header.kid };
907
940
  }
908
- async function verifyToolSignatures(tools, signatures) {
941
+ async function verifyToolSignatures(tools, signatures, opts = {}) {
942
+ const jwksUrl = opts.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
943
+ const fetchImpl = opts.fetch ?? fetch;
944
+ if (!jwksUrl.startsWith("https://")) {
945
+ return tools.map((t) => ({
946
+ tool: t.name,
947
+ valid: false,
948
+ reason: `jwksUrl must be https: got ${jwksUrl}`
949
+ }));
950
+ }
951
+ const needsJwks = tools.some((t) => {
952
+ const sig = signatures[t.name];
953
+ return sig && sig.signature;
954
+ });
955
+ let jwks = null;
956
+ if (needsJwks) {
957
+ try {
958
+ jwks = await fetchJwks(jwksUrl, fetchImpl);
959
+ } catch (err) {
960
+ return tools.map((t) => ({
961
+ tool: t.name,
962
+ valid: false,
963
+ reason: `jwks fetch failed: ${err.message}`
964
+ }));
965
+ }
966
+ }
909
967
  const out = [];
910
968
  for (const tool of tools) {
911
969
  const sig = signatures[tool.name];
@@ -918,6 +976,63 @@ async function verifyToolSignatures(tools, signatures) {
918
976
  out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
919
977
  continue;
920
978
  }
979
+ const jwsToken = sig.signature;
980
+ if (!jwsToken) {
981
+ out.push({ tool: tool.name, valid: true, warn_no_signature: true });
982
+ continue;
983
+ }
984
+ const jwksDoc = jwks;
985
+ let jHeader;
986
+ let jPayloadRaw;
987
+ let jSigBytes;
988
+ try {
989
+ const parts2 = jwsToken.split(".");
990
+ if (parts2.length !== 3) throw new Error("JWS must have three segments");
991
+ jHeader = JSON.parse(new TextDecoder().decode(base64urlDecode(parts2[0])));
992
+ jPayloadRaw = new TextDecoder().decode(base64urlDecode(parts2[1]));
993
+ jSigBytes = base64urlDecode(parts2[2]);
994
+ } catch (err) {
995
+ out.push({ tool: tool.name, valid: false, reason: `malformed tool JWS: ${err.message}` });
996
+ continue;
997
+ }
998
+ if (jHeader.alg !== "ES256") {
999
+ out.push({ tool: tool.name, valid: false, reason: `unsupported tool sig alg: ${jHeader.alg}` });
1000
+ continue;
1001
+ }
1002
+ if (jPayloadRaw !== sig.schema_hash) {
1003
+ out.push({ tool: tool.name, valid: false, reason: "tool JWS payload does not match schema_hash" });
1004
+ continue;
1005
+ }
1006
+ const jwk = jwksDoc.keys.find((k) => jHeader.kid ? k.kid === jHeader.kid : true);
1007
+ if (!jwk) {
1008
+ out.push({ tool: tool.name, valid: false, reason: `no JWK for kid=${jHeader.kid}` });
1009
+ continue;
1010
+ }
1011
+ let publicKey;
1012
+ try {
1013
+ publicKey = await importEs256JwkAsPublicKey(jwk);
1014
+ } catch (err) {
1015
+ out.push({ tool: tool.name, valid: false, reason: `jwk import: ${err.message}` });
1016
+ continue;
1017
+ }
1018
+ const parts = jwsToken.split(".");
1019
+ const signedInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
1020
+ let sigValid = false;
1021
+ try {
1022
+ sigValid = await crypto.subtle.verify(
1023
+ { name: "ECDSA", hash: "SHA-256" },
1024
+ publicKey,
1025
+ toArrayBuffer(jSigBytes),
1026
+ toArrayBuffer(signedInput)
1027
+ );
1028
+ } catch (err) {
1029
+ out.push({ tool: tool.name, valid: false, reason: `sig verify error: ${err.message}` });
1030
+ continue;
1031
+ }
1032
+ if (!sigValid) {
1033
+ out.push({ tool: tool.name, valid: false, reason: "tool signature mismatch" });
1034
+ continue;
1035
+ }
921
1036
  out.push({ tool: tool.name, valid: true });
922
1037
  }
923
1038
  return out;
@@ -1552,6 +1667,26 @@ function restoreFromBackup(path, backupPath) {
1552
1667
  // src/wire/index.ts
1553
1668
  var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
1554
1669
  var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
1670
+ function readCfAccessEnv() {
1671
+ const envPath = join(homedir(), ".config", "alter", "cf-access.env");
1672
+ try {
1673
+ const content = readFileSync(envPath, "utf8");
1674
+ let clientId = "";
1675
+ let clientSecret = "";
1676
+ for (const line of content.split("\n")) {
1677
+ const trimmed = line.trim();
1678
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
1679
+ const eqIdx = trimmed.indexOf("=");
1680
+ const key = trimmed.slice(0, eqIdx).replace(/^export\s+/, "").trim();
1681
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
1682
+ if (key === "CF_ACCESS_CLIENT_ID") clientId = val;
1683
+ if (key === "CF_ACCESS_CLIENT_SECRET") clientSecret = val;
1684
+ }
1685
+ if (clientId && clientSecret) return { clientId, clientSecret };
1686
+ } catch {
1687
+ }
1688
+ return void 0;
1689
+ }
1555
1690
  function clientById(id) {
1556
1691
  const hit = ALL_CLIENTS.find((c) => c.id === id);
1557
1692
  if (!hit) throw new Error(`unknown client id: ${id}`);
@@ -1560,6 +1695,7 @@ function clientById(id) {
1560
1695
  function wire(opts = {}) {
1561
1696
  const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
1562
1697
  const apiKey = opts.apiKey;
1698
+ const cfAccess = opts.cfAccess ?? readCfAccessEnv();
1563
1699
  const probes = probeAll();
1564
1700
  const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
1565
1701
  const ts = TIMESTAMP();
@@ -1578,9 +1714,9 @@ function wire(opts = {}) {
1578
1714
  }
1579
1715
  try {
1580
1716
  if (id === "claude-code") {
1581
- targets.push(wireClaudeCode({ endpoint, apiKey }));
1717
+ targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
1582
1718
  } else {
1583
- targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
1719
+ targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
1584
1720
  }
1585
1721
  } catch (err) {
1586
1722
  const message = err.message;
@@ -1614,7 +1750,12 @@ function wireFileTarget(args) {
1614
1750
  `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.`
1615
1751
  );
1616
1752
  }
1617
- const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
1753
+ const cfHeaders = {};
1754
+ if (args.cfAccess) {
1755
+ cfHeaders["CF-Access-Client-Id"] = args.cfAccess.clientId;
1756
+ cfHeaders["CF-Access-Client-Secret"] = args.cfAccess.clientSecret;
1757
+ }
1758
+ const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey, headers: cfHeaders });
1618
1759
  const rootKey = client.rootKey;
1619
1760
  const serverName = "alter";
1620
1761
  const result = atomicJsonMerge({
@@ -1646,7 +1787,8 @@ function wireFileTarget(args) {
1646
1787
  }
1647
1788
  function wireClaudeCode(args) {
1648
1789
  const cmd = "claude";
1649
- const argList = [
1790
+ const bridgePath = resolveBridgeScript();
1791
+ const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
1650
1792
  "mcp",
1651
1793
  "add",
1652
1794
  "--scope",
@@ -1654,16 +1796,15 @@ function wireClaudeCode(args) {
1654
1796
  "--transport",
1655
1797
  "http",
1656
1798
  "alter",
1657
- args.endpoint
1799
+ args.endpoint,
1800
+ ...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
1658
1801
  ];
1659
- if (args.apiKey) {
1660
- argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
1661
- }
1662
1802
  const full = `${cmd} ${argList.join(" ")}`;
1663
1803
  const run = spawnSync(cmd, argList, {
1664
1804
  encoding: "utf8",
1665
1805
  shell: process.platform === "win32",
1666
- timeout: 1e4
1806
+ timeout: 1e4,
1807
+ env: bridgePath ? { ...process.env, ALTER_PUBLIC_MCP_ENDPOINT: args.endpoint, ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {} } : void 0
1667
1808
  });
1668
1809
  if (run.error) {
1669
1810
  return {
@@ -1694,6 +1835,17 @@ function wireClaudeCode(args) {
1694
1835
  reason: `claude mcp add exited ${String(run.status)}`
1695
1836
  };
1696
1837
  }
1838
+ function resolveBridgeScript() {
1839
+ const { existsSync: existsSync4 } = __require("fs");
1840
+ const { join: join3, dirname: dirname3 } = __require("path");
1841
+ const siblingBridge = join3(dirname3(__filename), "..", "dist", "mcp-bridge.js");
1842
+ if (existsSync4(siblingBridge)) return siblingBridge;
1843
+ const srcBridge = join3(dirname3(__filename), "..", "mcp-bridge.js");
1844
+ if (existsSync4(srcBridge)) return srcBridge;
1845
+ const npmGlobalBridge = join3(dirname3(__filename), "mcp-bridge.js");
1846
+ if (existsSync4(npmGlobalBridge)) return npmGlobalBridge;
1847
+ return null;
1848
+ }
1697
1849
  function unwire() {
1698
1850
  const state = readWireState();
1699
1851
  const undone = [];
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import * as https from 'https';
6
+ import * as http from 'http';
7
+ import * as crypto from 'crypto';
8
+ import * as readline from 'readline';
9
+
10
+ var DEFAULT_ENDPOINT = "https://mcp.truealter.com/api/v1/mcp";
11
+ var BRIDGE_VERSION = "0.4.0";
12
+ var xdgConfig = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
13
+ var SESSION_FILE = process.env.ALTER_SESSION_FILE ?? path.join(xdgConfig, "alter", "session.json");
14
+ var CF_ENV_FILE = process.env.ALTER_CF_ACCESS_ENV ?? path.join(xdgConfig, "alter", "cf-access.env");
15
+ var ENDPOINT = process.env.ALTER_PUBLIC_MCP_ENDPOINT ?? DEFAULT_ENDPOINT;
16
+ function readSession() {
17
+ try {
18
+ const raw = fs.readFileSync(SESSION_FILE, "utf8");
19
+ const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
20
+ return JSON.parse(clean);
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+ function readCfAccess() {
26
+ try {
27
+ const raw = fs.readFileSync(CF_ENV_FILE, "utf8");
28
+ const idMatch = raw.match(/CF_ACCESS_(?:MCP_ORG_ALTER_)?CLIENT_ID=['"](.*?)['"]/);
29
+ const secretMatch = raw.match(/CF_ACCESS_(?:MCP_ORG_ALTER_)?CLIENT_SECRET=['"](.*?)['"]/);
30
+ return {
31
+ id: idMatch?.[1] ?? "",
32
+ secret: secretMatch?.[1] ?? ""
33
+ };
34
+ } catch {
35
+ return { id: "", secret: "" };
36
+ }
37
+ }
38
+ function versionHash() {
39
+ const digest = crypto.createHash("sha256").update(`@truealter/sdk-bridge@${BRIDGE_VERSION}`).digest("hex").slice(0, 32);
40
+ return `sha256:${digest}`;
41
+ }
42
+ function b64url(buf) {
43
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
44
+ }
45
+ function canonicalJson(v) {
46
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
47
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
48
+ const obj = v;
49
+ const keys = Object.keys(obj).sort();
50
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
51
+ }
52
+ function loadSigningKey(session) {
53
+ const kid = session.signing_kid;
54
+ if (!kid) return null;
55
+ const candidates = [];
56
+ if (process.env.ALTER_SIGNING_KEY_FILE) candidates.push(process.env.ALTER_SIGNING_KEY_FILE);
57
+ candidates.push(path.join(xdgConfig, "alter", "signing-keys", `${kid}.pem`));
58
+ candidates.push(path.join(xdgConfig, "alter", "signing-key.pem"));
59
+ for (const p of candidates) {
60
+ try {
61
+ const pem = fs.readFileSync(p, "utf8");
62
+ return { kid, key: crypto.createPrivateKey(pem) };
63
+ } catch {
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ function signInvocation(signer, toolName, toolArgs, handle) {
69
+ const header = b64url(Buffer.from(JSON.stringify({ alg: "ES256", kid: signer.kid })));
70
+ const claims = {
71
+ tool: toolName,
72
+ args_sha256: crypto.createHash("sha256").update(canonicalJson(toolArgs ?? {}), "utf8").digest("hex"),
73
+ nonce: b64url(crypto.randomBytes(24)),
74
+ iat: Math.floor(Date.now() / 1e3),
75
+ iss: handle
76
+ };
77
+ const payload = b64url(Buffer.from(JSON.stringify(claims)));
78
+ const signingInput = `${header}.${payload}`;
79
+ const sig = crypto.createSign("SHA256").update(signingInput).sign({ key: signer.key, dsaEncoding: "ieee-p1363" });
80
+ return `${signingInput}.${b64url(sig)}`;
81
+ }
82
+ function proxyRequest(body, msg) {
83
+ return new Promise((resolve, reject) => {
84
+ const session = readSession();
85
+ const cfAccess = readCfAccess();
86
+ const apiKey = process.env.ALTER_API_KEY ?? session.member_api_key ?? "";
87
+ const url = new URL(ENDPOINT);
88
+ const isHttps = url.protocol === "https:";
89
+ const transport = isHttps ? https : http;
90
+ const headers = {
91
+ "Content-Type": "application/json",
92
+ "Content-Length": String(Buffer.byteLength(body)),
93
+ "X-Agent-Version-Hash": versionHash()
94
+ };
95
+ if (apiKey) headers["X-ALTER-API-Key"] = apiKey;
96
+ if (cfAccess.id) headers["CF-Access-Client-Id"] = cfAccess.id;
97
+ if (cfAccess.secret) headers["CF-Access-Client-Secret"] = cfAccess.secret;
98
+ if (apiKey && msg.method === "tools/call" && msg.params?.name) {
99
+ const signer = loadSigningKey(session);
100
+ if (signer) {
101
+ try {
102
+ headers["Mcp-Invocation-Signature"] = signInvocation(
103
+ signer,
104
+ msg.params.name,
105
+ msg.params.arguments ?? {},
106
+ session.handle ?? ""
107
+ );
108
+ } catch (e) {
109
+ process.stderr.write(`bridge sign error: ${e.message}
110
+ `);
111
+ }
112
+ } else {
113
+ process.stderr.write(
114
+ `bridge: no signing key for kid ${session.signing_kid ?? "(unset)"} \u2014 run 'alter login' to provision one
115
+ `
116
+ );
117
+ }
118
+ }
119
+ const req = transport.request(
120
+ {
121
+ hostname: url.hostname,
122
+ port: url.port || (isHttps ? 443 : 80),
123
+ path: url.pathname,
124
+ method: "POST",
125
+ headers
126
+ },
127
+ (res) => {
128
+ let data = "";
129
+ res.on("data", (chunk) => data += chunk.toString());
130
+ res.on("end", () => resolve(data));
131
+ }
132
+ );
133
+ req.on("error", (err) => reject(err));
134
+ req.write(body);
135
+ req.end();
136
+ });
137
+ }
138
+ async function main() {
139
+ const rl = readline.createInterface({ input: process.stdin });
140
+ for await (const line of rl) {
141
+ const trimmed = line.trim();
142
+ if (!trimmed) continue;
143
+ let msg;
144
+ try {
145
+ msg = JSON.parse(trimmed);
146
+ } catch {
147
+ continue;
148
+ }
149
+ try {
150
+ const response = await proxyRequest(trimmed, msg);
151
+ process.stdout.write(response + "\n");
152
+ } catch (err) {
153
+ const errResponse = JSON.stringify({
154
+ jsonrpc: "2.0",
155
+ id: msg.id ?? null,
156
+ error: { code: -32e3, message: err.message }
157
+ });
158
+ process.stdout.write(errResponse + "\n");
159
+ }
160
+ }
161
+ }
162
+ main().catch((err) => {
163
+ process.stderr.write(`bridge fatal: ${err.message}
164
+ `);
165
+ process.exit(1);
166
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truealter/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "ALTER Identity SDK — query the continuous identity field from any JavaScript/TypeScript environment",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -46,6 +46,9 @@
46
46
  "typescript": "^5.4.5",
47
47
  "vitest": "^4.1.3"
48
48
  },
49
+ "overrides": {
50
+ "postcss": ">=8.5.10"
51
+ },
49
52
  "keywords": [
50
53
  "alter",
51
54
  "identity",