@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/README.md +61 -45
- package/dist/bin/alter-identity.js +164 -12
- package/dist/bin/mcp-bridge.js +18 -3
- package/dist/index.cjs +167 -13
- package/dist/index.d.cts +56 -19
- package/dist/index.d.ts +56 -19
- package/dist/index.js +167 -15
- package/dist/mcp-bridge.js +166 -0
- package/package.json +4 -1
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 {
|
|
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
|
|
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
|
|
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.
|
|
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",
|