@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/README.md +103 -74
- package/dist/bin/alter-identity.js +177 -19
- package/dist/bin/mcp-bridge.js +47 -11
- package/dist/index.cjs +203 -20
- package/dist/index.d.cts +431 -30
- package/dist/index.d.ts +431 -30
- package/dist/index.js +199 -23
- package/dist/mcp-bridge.js +166 -0
- package/package.json +7 -4
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 {
|
|
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
|
|
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("@") ? {
|
|
1097
|
-
// ~handle — server resolves these via the
|
|
1098
|
-
{
|
|
1099
|
-
) : {
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
40
|
-
"@noble/ed25519": "
|
|
41
|
-
"@noble/hashes": "
|
|
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",
|