@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.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { p256 } from '@noble/curves/p256';
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
3
  import { randomBytes, bytesToHex as bytesToHex$1, hexToBytes } from '@noble/hashes/utils';
4
+ import { createPrivateKey, createHash } from 'crypto';
4
5
  import * as ed25519 from '@noble/ed25519';
5
6
  import { sha512 } from '@noble/hashes/sha512';
6
- import { spawnSync } from 'child_process';
7
- import { homedir, platform } from 'os';
7
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, renameSync, unlinkSync } from 'fs';
8
8
  import { join, resolve, dirname } from 'path';
9
+ import { homedir, platform } from 'os';
10
+ import { spawnSync } from 'child_process';
9
11
  import { env } from 'process';
10
- import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, renameSync, unlinkSync } from 'fs';
11
- import { createHash } from 'crypto';
12
12
 
13
13
  var __defProp = Object.defineProperty;
14
14
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -104,8 +104,7 @@ function loadPrivateKey(key) {
104
104
  return key;
105
105
  }
106
106
  if (typeof key === "string" && key.includes("-----BEGIN")) {
107
- const nodeCrypto = __require("crypto");
108
- const keyObj = nodeCrypto.createPrivateKey({ key, format: "pem" });
107
+ const keyObj = createPrivateKey({ key, format: "pem" });
109
108
  const jwk = keyObj.export({ format: "jwk" });
110
109
  if (jwk.crv !== "P-256" || !jwk.d) {
111
110
  throw new TypeError("PEM is not a P-256 private key.");
@@ -407,18 +406,24 @@ function ensureMcpPath(url) {
407
406
  return url;
408
407
  }
409
408
  }
410
-
411
- // src/x402.ts
412
409
  var X402Client = class {
413
410
  signer;
414
411
  maxPerQuery;
415
412
  networks;
416
413
  assets;
414
+ // undefined = allowlist check disabled (backward-compatible default).
415
+ // Non-null = active allowlist; reject any recipient not in the set.
416
+ recipientAllowlist;
417
417
  constructor(opts = {}) {
418
418
  this.signer = opts.signer;
419
419
  this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
420
420
  this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
421
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
+ }
422
427
  }
423
428
  /**
424
429
  * Validate the envelope against this client's policy and, if a signer
@@ -444,6 +449,15 @@ var X402Client = class {
444
449
  );
445
450
  }
446
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
+ }
447
461
  if (!this.signer) {
448
462
  throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
449
463
  }
@@ -501,6 +515,7 @@ var MCPClient = class {
501
515
  clientInfo;
502
516
  x402;
503
517
  signing;
518
+ extraHeaders;
504
519
  requestCounter = 0;
505
520
  initialised = false;
506
521
  constructor(opts = {}) {
@@ -512,6 +527,7 @@ var MCPClient = class {
512
527
  this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
513
528
  this.x402 = opts.x402;
514
529
  this.signing = opts.signing;
530
+ this.extraHeaders = opts.extraHeaders;
515
531
  }
516
532
  /**
517
533
  * Send the MCP `initialize` handshake and capture the resulting session
@@ -675,6 +691,7 @@ var MCPClient = class {
675
691
  }
676
692
  buildHeaders(extra) {
677
693
  const headers = {
694
+ ...this.extraHeaders ?? {},
678
695
  "Content-Type": "application/json",
679
696
  Accept: "application/json",
680
697
  "User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`
@@ -815,6 +832,7 @@ var DEFAULT_VERIFY_AT_ALLOWLIST = Object.freeze([
815
832
  "api.truealter.com",
816
833
  "mcp.truealter.com"
817
834
  ]);
835
+ var ALTER_PLATFORM_ISS = "did:alter:platform";
818
836
  async function verifyProvenance(envelope, opts = {}) {
819
837
  const token = typeof envelope === "string" ? envelope : envelope.token;
820
838
  if (!token) return { valid: false, reason: "empty token" };
@@ -897,9 +915,55 @@ async function verifyProvenance(envelope, opts = {}) {
897
915
  if (typeof payload.iat === "number" && payload.iat > now + 300) {
898
916
  return { valid: false, reason: "issued in the future", payload, kid: header.kid };
899
917
  }
918
+ const expectedIss = opts.expectedIss !== void 0 ? opts.expectedIss : ALTER_PLATFORM_ISS;
919
+ if (expectedIss !== "" && payload.iss !== expectedIss) {
920
+ return {
921
+ valid: false,
922
+ reason: `iss mismatch: expected "${expectedIss}", got "${payload.iss}"`,
923
+ payload,
924
+ kid: header.kid
925
+ };
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
+ }
900
939
  return { valid: true, payload, kid: header.kid };
901
940
  }
902
- 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
+ }
903
967
  const out = [];
904
968
  for (const tool of tools) {
905
969
  const sig = signatures[tool.name];
@@ -912,6 +976,63 @@ async function verifyToolSignatures(tools, signatures) {
912
976
  out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
913
977
  continue;
914
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
+ }
915
1036
  out.push({ tool: tool.name, valid: true });
916
1037
  }
917
1038
  return out;
@@ -1093,10 +1214,10 @@ var AlterClient = class {
1093
1214
  }
1094
1215
  /** Verify a person is registered with ALTER (handle or id). */
1095
1216
  async verify(handleOrId, claims) {
1096
- const args = handleOrId.includes("@") ? { candidate_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
1097
- // ~handle — server resolves these via the candidate_id field
1098
- { candidate_id: handleOrId }
1099
- ) : { candidate_id: handleOrId };
1217
+ const args = handleOrId.includes("@") ? { member_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
1218
+ // ~handle — server resolves these via the member_id field
1219
+ { member_id: handleOrId }
1220
+ ) : { member_id: handleOrId };
1100
1221
  if (claims) args.claims = claims;
1101
1222
  return this.mcp.callTool("verify_identity", args);
1102
1223
  }
@@ -1546,6 +1667,26 @@ function restoreFromBackup(path, backupPath) {
1546
1667
  // src/wire/index.ts
1547
1668
  var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
1548
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
+ }
1549
1690
  function clientById(id) {
1550
1691
  const hit = ALL_CLIENTS.find((c) => c.id === id);
1551
1692
  if (!hit) throw new Error(`unknown client id: ${id}`);
@@ -1554,6 +1695,7 @@ function clientById(id) {
1554
1695
  function wire(opts = {}) {
1555
1696
  const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
1556
1697
  const apiKey = opts.apiKey;
1698
+ const cfAccess = opts.cfAccess ?? readCfAccessEnv();
1557
1699
  const probes = probeAll();
1558
1700
  const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
1559
1701
  const ts = TIMESTAMP();
@@ -1572,9 +1714,9 @@ function wire(opts = {}) {
1572
1714
  }
1573
1715
  try {
1574
1716
  if (id === "claude-code") {
1575
- targets.push(wireClaudeCode({ endpoint, apiKey }));
1717
+ targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
1576
1718
  } else {
1577
- targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
1719
+ targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
1578
1720
  }
1579
1721
  } catch (err) {
1580
1722
  const message = err.message;
@@ -1608,7 +1750,12 @@ function wireFileTarget(args) {
1608
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.`
1609
1751
  );
1610
1752
  }
1611
- 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 });
1612
1759
  const rootKey = client.rootKey;
1613
1760
  const serverName = "alter";
1614
1761
  const result = atomicJsonMerge({
@@ -1640,7 +1787,8 @@ function wireFileTarget(args) {
1640
1787
  }
1641
1788
  function wireClaudeCode(args) {
1642
1789
  const cmd = "claude";
1643
- const argList = [
1790
+ const bridgePath = resolveBridgeScript();
1791
+ const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
1644
1792
  "mcp",
1645
1793
  "add",
1646
1794
  "--scope",
@@ -1648,16 +1796,15 @@ function wireClaudeCode(args) {
1648
1796
  "--transport",
1649
1797
  "http",
1650
1798
  "alter",
1651
- args.endpoint
1799
+ args.endpoint,
1800
+ ...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
1652
1801
  ];
1653
- if (args.apiKey) {
1654
- argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
1655
- }
1656
1802
  const full = `${cmd} ${argList.join(" ")}`;
1657
1803
  const run = spawnSync(cmd, argList, {
1658
1804
  encoding: "utf8",
1659
1805
  shell: process.platform === "win32",
1660
- 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
1661
1808
  });
1662
1809
  if (run.error) {
1663
1810
  return {
@@ -1688,6 +1835,17 @@ function wireClaudeCode(args) {
1688
1835
  reason: `claude mcp add exited ${String(run.status)}`
1689
1836
  };
1690
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
+ }
1691
1849
  function unwire() {
1692
1850
  const state = readWireState();
1693
1851
  const undone = [];
@@ -1890,4 +2048,22 @@ var TOOL_BLAST_RADIUS = {
1890
2048
  query_graph_similarity: "high"
1891
2049
  };
1892
2050
 
1893
- export { ALL_CLIENTS, AlterAuthError, AlterClient, AlterDiscoveryError, AlterError, AlterInvalidResponse, AlterNetworkError, AlterPaymentRequired, AlterProvenanceError, AlterRateLimited, AlterTimeoutError, AlterToolError, CLAUDE_CODE, CLAUDE_DESKTOP, CURSOR, DEFAULT_DOMAIN, DEFAULT_ENDPOINT, DEFAULT_VERIFY_AT_ALLOWLIST, FREE_TOOL_NAMES, MCPClient, MCP_PROTOCOL_VERSION, PREMIUM_TOOL_NAMES, SDK_NAME, SDK_VERSION, TOOL_BLAST_RADIUS, TOOL_COSTS, TOOL_TIERS, VSCODE, X402Client, base64urlDecode, base64urlEncode2 as base64urlEncode, canonicalArgsSha256, canonicalStringify, clearDiscoveryCache, decodeDid, detectSyncedVolume, discover, encodeDid, fetchPublicKeys, generateClaudeConfig, generateClaudeDesktopConfig, generateCursorConfig, generateGenericMcpConfig, generateKeypair, keypairFromPrivateKey, loadPrivateKey, parsePaymentHeader, probeAll, probeByDir, probeClaudeCode, readWireState, resolveVerifyAt, sha2562 as sha256, sign, signInvocation, unwire, verify, verifyProvenance, verifyToolSignatures, wire, writeWireState };
2051
+ // src/homepage.ts
2052
+ var HOMEPAGE_LIMITS = {
2053
+ whoami_max_chars: 240,
2054
+ opener_max_chars: 280,
2055
+ pronouns_max_chars: 32,
2056
+ attunement_glyph_max_chars: 16
2057
+ };
2058
+
2059
+ // src/themes.ts
2060
+ var THEME_LIMITS = {
2061
+ meta_name_pattern: /^[a-z][a-z0-9-]{0,63}$/,
2062
+ meta_description_max_chars: 240,
2063
+ opener_library_max_entries: 32,
2064
+ opener_entry_max_chars: 240,
2065
+ share_note_max_chars: 280
2066
+ };
2067
+ var OSC8_ALLOWED_SCHEMES = ["https:", "mailto:"];
2068
+
2069
+ export { ALL_CLIENTS, AlterAuthError, AlterClient, AlterDiscoveryError, AlterError, AlterInvalidResponse, AlterNetworkError, AlterPaymentRequired, AlterProvenanceError, AlterRateLimited, AlterTimeoutError, AlterToolError, CLAUDE_CODE, CLAUDE_DESKTOP, CURSOR, DEFAULT_DOMAIN, DEFAULT_ENDPOINT, DEFAULT_VERIFY_AT_ALLOWLIST, FREE_TOOL_NAMES, HOMEPAGE_LIMITS, MCPClient, MCP_PROTOCOL_VERSION, OSC8_ALLOWED_SCHEMES, PREMIUM_TOOL_NAMES, SDK_NAME, SDK_VERSION, THEME_LIMITS, TOOL_BLAST_RADIUS, TOOL_COSTS, TOOL_TIERS, VSCODE, X402Client, base64urlDecode, base64urlEncode2 as base64urlEncode, canonicalArgsSha256, canonicalStringify, clearDiscoveryCache, decodeDid, detectSyncedVolume, discover, encodeDid, fetchPublicKeys, generateClaudeConfig, generateClaudeDesktopConfig, generateCursorConfig, generateGenericMcpConfig, generateKeypair, keypairFromPrivateKey, loadPrivateKey, parsePaymentHeader, probeAll, probeByDir, probeClaudeCode, readWireState, resolveVerifyAt, sha2562 as sha256, sign, signInvocation, unwire, verify, verifyProvenance, verifyToolSignatures, wire, writeWireState };
@@ -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.4.1",
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",
@@ -36,9 +36,9 @@
36
36
  "prepublishOnly": "npm run build"
37
37
  },
38
38
  "dependencies": {
39
- "@noble/curves": "^1.6.0",
40
- "@noble/ed25519": "^2.1.0",
41
- "@noble/hashes": "^1.4.0"
39
+ "@noble/curves": "1.9.7",
40
+ "@noble/ed25519": "2.3.0",
41
+ "@noble/hashes": "1.8.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^20.11.0",
@@ -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",