@truealter/sdk 0.2.4 → 0.5.0
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 +50 -36
- package/dist/bin/alter-identity.js +841 -31
- package/dist/bin/mcp-bridge.js +201 -9
- package/dist/index.cjs +785 -24
- package/dist/index.d.cts +677 -41
- package/dist/index.d.ts +677 -41
- package/dist/index.js +712 -28
- package/package.json +4 -3
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -1,7 +1,146 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { p256 } from '@noble/curves/p256';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
4
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
5
|
+
import { createPrivateKey } from 'crypto';
|
|
2
6
|
import { createInterface } from 'readline';
|
|
3
7
|
import { env, stderr, exit, stdin, stdout } from 'process';
|
|
4
8
|
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
13
|
+
var __esm = (fn, res) => function __init() {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/signing.ts
|
|
31
|
+
var signing_exports = {};
|
|
32
|
+
__export(signing_exports, {
|
|
33
|
+
canonicalArgsSha256: () => canonicalArgsSha256,
|
|
34
|
+
canonicalStringify: () => canonicalStringify,
|
|
35
|
+
loadPrivateKey: () => loadPrivateKey,
|
|
36
|
+
signInvocation: () => signInvocation
|
|
37
|
+
});
|
|
38
|
+
function canonicalStringify(value) {
|
|
39
|
+
return stringifyInner(value);
|
|
40
|
+
}
|
|
41
|
+
function stringifyInner(value) {
|
|
42
|
+
if (value === null) return "null";
|
|
43
|
+
if (value === void 0) {
|
|
44
|
+
throw new TypeError("canonicalStringify: undefined is not representable in JSON");
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
47
|
+
if (typeof value === "number") {
|
|
48
|
+
if (!Number.isFinite(value)) {
|
|
49
|
+
throw new TypeError("canonicalStringify: non-finite numbers are not representable");
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "string") return encodeString(value);
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return "[" + value.map((v) => stringifyInner(v)).join(",") + "]";
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === "object") {
|
|
58
|
+
const obj = value;
|
|
59
|
+
const keys = Object.keys(obj).sort();
|
|
60
|
+
return "{" + keys.map((k) => encodeString(k) + ":" + stringifyInner(obj[k])).join(",") + "}";
|
|
61
|
+
}
|
|
62
|
+
throw new TypeError(`canonicalStringify: unsupported type ${typeof value}`);
|
|
63
|
+
}
|
|
64
|
+
function encodeString(s) {
|
|
65
|
+
return JSON.stringify(s);
|
|
66
|
+
}
|
|
67
|
+
function canonicalArgsSha256(toolArgs) {
|
|
68
|
+
const canonical = canonicalStringify(toolArgs ?? {});
|
|
69
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
70
|
+
const digest = sha256(bytes);
|
|
71
|
+
return bytesToHex(digest);
|
|
72
|
+
}
|
|
73
|
+
function bytesToHex(bytes) {
|
|
74
|
+
let out = "";
|
|
75
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
76
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function base64urlEncode(bytes) {
|
|
81
|
+
const raw = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
|
|
82
|
+
if (typeof Buffer !== "undefined") {
|
|
83
|
+
return Buffer.from(raw).toString("base64url");
|
|
84
|
+
}
|
|
85
|
+
let binary = "";
|
|
86
|
+
for (let i = 0; i < raw.length; i++) binary += String.fromCharCode(raw[i]);
|
|
87
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
88
|
+
}
|
|
89
|
+
function loadPrivateKey(key) {
|
|
90
|
+
if (key instanceof Uint8Array) {
|
|
91
|
+
if (key.length !== 32) {
|
|
92
|
+
throw new TypeError("ES256 raw private key must be 32 bytes.");
|
|
93
|
+
}
|
|
94
|
+
return key;
|
|
95
|
+
}
|
|
96
|
+
if (typeof key === "string" && key.includes("-----BEGIN")) {
|
|
97
|
+
const keyObj = createPrivateKey({ key, format: "pem" });
|
|
98
|
+
const jwk = keyObj.export({ format: "jwk" });
|
|
99
|
+
if (jwk.crv !== "P-256" || !jwk.d) {
|
|
100
|
+
throw new TypeError("PEM is not a P-256 private key.");
|
|
101
|
+
}
|
|
102
|
+
return base64urlDecodeToBytes(jwk.d);
|
|
103
|
+
}
|
|
104
|
+
throw new TypeError("loadPrivateKey: expected Uint8Array(32) or PEM string.");
|
|
105
|
+
}
|
|
106
|
+
function base64urlDecodeToBytes(s) {
|
|
107
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
108
|
+
const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
109
|
+
if (typeof Buffer !== "undefined") {
|
|
110
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
111
|
+
}
|
|
112
|
+
const binary = atob(b64);
|
|
113
|
+
const out = new Uint8Array(binary.length);
|
|
114
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function signInvocation(toolName, toolArgs, options) {
|
|
118
|
+
const { kid, privateKey, handle: handle2 } = options;
|
|
119
|
+
const nonce = options.nonce ?? base64urlEncode(randomBytes(24));
|
|
120
|
+
const iat = options.iatSeconds ?? Math.floor(Date.now() / 1e3);
|
|
121
|
+
const claims = {
|
|
122
|
+
tool: toolName,
|
|
123
|
+
args_sha256: canonicalArgsSha256(toolArgs ?? {}),
|
|
124
|
+
nonce,
|
|
125
|
+
iat,
|
|
126
|
+
iss: handle2
|
|
127
|
+
};
|
|
128
|
+
const headerB64 = base64urlEncode(JSON.stringify({ alg: "ES256", kid }));
|
|
129
|
+
const payloadB64 = base64urlEncode(JSON.stringify(claims));
|
|
130
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
131
|
+
const signingBytes = new TextEncoder().encode(signingInput);
|
|
132
|
+
const dBytes = loadPrivateKey(privateKey);
|
|
133
|
+
const digest = sha256(signingBytes);
|
|
134
|
+
const sig = p256.sign(digest, dBytes, { prehash: false });
|
|
135
|
+
const sigBytes = sig.toCompactRawBytes();
|
|
136
|
+
const sigB64 = base64urlEncode(sigBytes);
|
|
137
|
+
return `${signingInput}.${sigB64}`;
|
|
138
|
+
}
|
|
139
|
+
var init_signing = __esm({
|
|
140
|
+
"src/signing.ts"() {
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
5
144
|
// src/errors.ts
|
|
6
145
|
var AlterError = class extends Error {
|
|
7
146
|
code;
|
|
@@ -103,11 +242,14 @@ var X402Client = class {
|
|
|
103
242
|
if (!this.assets.has(envelope.asset)) {
|
|
104
243
|
throw new AlterError("PAYMENT_REQUIRED", `asset ${envelope.asset} not permitted by client policy`);
|
|
105
244
|
}
|
|
106
|
-
if (this.maxPerQuery !== void 0
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
245
|
+
if (this.maxPerQuery !== void 0) {
|
|
246
|
+
const amt = Number(envelope.amount);
|
|
247
|
+
if (!Number.isFinite(amt) || amt < 0 || amt > this.maxPerQuery) {
|
|
248
|
+
throw new AlterError(
|
|
249
|
+
"PAYMENT_REQUIRED",
|
|
250
|
+
`quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
111
253
|
}
|
|
112
254
|
if (!this.signer) {
|
|
113
255
|
throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
|
|
@@ -165,6 +307,8 @@ var MCPClient = class {
|
|
|
165
307
|
maxRetries;
|
|
166
308
|
clientInfo;
|
|
167
309
|
x402;
|
|
310
|
+
signing;
|
|
311
|
+
extraHeaders;
|
|
168
312
|
requestCounter = 0;
|
|
169
313
|
initialised = false;
|
|
170
314
|
constructor(opts = {}) {
|
|
@@ -175,6 +319,8 @@ var MCPClient = class {
|
|
|
175
319
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
176
320
|
this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
|
|
177
321
|
this.x402 = opts.x402;
|
|
322
|
+
this.signing = opts.signing;
|
|
323
|
+
this.extraHeaders = opts.extraHeaders;
|
|
178
324
|
}
|
|
179
325
|
/**
|
|
180
326
|
* Send the MCP `initialize` handshake and capture the resulting session
|
|
@@ -261,6 +407,7 @@ var MCPClient = class {
|
|
|
261
407
|
method
|
|
262
408
|
};
|
|
263
409
|
if (params !== void 0) payload.params = params;
|
|
410
|
+
const signatureHeader = this.buildSignatureHeader(method, params);
|
|
264
411
|
let attempt = 0;
|
|
265
412
|
let lastErr = null;
|
|
266
413
|
while (attempt <= this.maxRetries) {
|
|
@@ -271,7 +418,7 @@ var MCPClient = class {
|
|
|
271
418
|
try {
|
|
272
419
|
resp = await this.fetchImpl(this.endpoint, {
|
|
273
420
|
method: "POST",
|
|
274
|
-
headers: this.buildHeaders(),
|
|
421
|
+
headers: this.buildHeaders(signatureHeader),
|
|
275
422
|
body: JSON.stringify(payload),
|
|
276
423
|
signal: controller.signal
|
|
277
424
|
});
|
|
@@ -298,7 +445,8 @@ var MCPClient = class {
|
|
|
298
445
|
throw new AlterPaymentRequired(this.guessToolName(payload), envelope);
|
|
299
446
|
}
|
|
300
447
|
if (resp.status === 429) {
|
|
301
|
-
const
|
|
448
|
+
const rawRetryAfter = Number(resp.headers.get("Retry-After") ?? 60);
|
|
449
|
+
const retryAfter = Number.isFinite(rawRetryAfter) && rawRetryAfter >= 0 ? Math.min(rawRetryAfter, 300) : 60;
|
|
302
450
|
if (attempt > this.maxRetries) {
|
|
303
451
|
throw new AlterRateLimited(`HTTP 429 on ${method}`, retryAfter);
|
|
304
452
|
}
|
|
@@ -334,16 +482,36 @@ var MCPClient = class {
|
|
|
334
482
|
}
|
|
335
483
|
throw lastErr ?? new AlterNetworkError(`MCP ${method}: exhausted retries`);
|
|
336
484
|
}
|
|
337
|
-
buildHeaders() {
|
|
485
|
+
buildHeaders(extra) {
|
|
338
486
|
const headers = {
|
|
487
|
+
...this.extraHeaders ?? {},
|
|
339
488
|
"Content-Type": "application/json",
|
|
340
489
|
Accept: "application/json",
|
|
341
490
|
"User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`
|
|
342
491
|
};
|
|
343
492
|
if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
|
|
344
493
|
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
494
|
+
if (extra) Object.assign(headers, extra);
|
|
345
495
|
return headers;
|
|
346
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Produce the `Mcp-Invocation-Signature` header for a `tools/call`
|
|
499
|
+
* payload, when signing is configured. Returns `undefined` when no
|
|
500
|
+
* signing key is attached or the method is not `tools/call`.
|
|
501
|
+
*/
|
|
502
|
+
buildSignatureHeader(method, params) {
|
|
503
|
+
if (!this.signing) return void 0;
|
|
504
|
+
if (method !== "tools/call") return void 0;
|
|
505
|
+
const p = params;
|
|
506
|
+
if (!p?.name) return void 0;
|
|
507
|
+
const { signInvocation: signInvocation2 } = (init_signing(), __toCommonJS(signing_exports));
|
|
508
|
+
const headerValue = signInvocation2(p.name, p.arguments ?? {}, {
|
|
509
|
+
kid: this.signing.kid,
|
|
510
|
+
privateKey: this.signing.privateKey,
|
|
511
|
+
handle: this.signing.handle
|
|
512
|
+
});
|
|
513
|
+
return { "Mcp-Invocation-Signature": headerValue };
|
|
514
|
+
}
|
|
347
515
|
async extractPaymentEnvelope(resp) {
|
|
348
516
|
const headerValue = resp.headers.get("X-402-Payment") ?? resp.headers.get("x-402-payment");
|
|
349
517
|
if (headerValue) {
|
|
@@ -385,10 +553,34 @@ async function safeText(resp) {
|
|
|
385
553
|
// bin/mcp-bridge.ts
|
|
386
554
|
var ENDPOINT = env.ALTER_MCP_ENDPOINT ?? "https://mcp.truealter.com/api/v1/mcp";
|
|
387
555
|
var API_KEY = env.ALTER_API_KEY ?? void 0;
|
|
556
|
+
function buildExtraHeaders() {
|
|
557
|
+
const headers = {};
|
|
558
|
+
if (env.CF_ACCESS_CLIENT_ID && env.CF_ACCESS_CLIENT_SECRET) {
|
|
559
|
+
headers["CF-Access-Client-Id"] = env.CF_ACCESS_CLIENT_ID;
|
|
560
|
+
headers["CF-Access-Client-Secret"] = env.CF_ACCESS_CLIENT_SECRET;
|
|
561
|
+
}
|
|
562
|
+
if (env.ALTER_BRIDGE_HEADERS) {
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(env.ALTER_BRIDGE_HEADERS);
|
|
565
|
+
Object.assign(headers, parsed);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
stderr.write(
|
|
568
|
+
`[alter-bridge] warning: ALTER_BRIDGE_HEADERS is not valid JSON; ignored (${err.message})
|
|
569
|
+
`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return Object.keys(headers).length ? headers : void 0;
|
|
574
|
+
}
|
|
575
|
+
var EXTRA_HEADERS = buildExtraHeaders();
|
|
576
|
+
console.warn(
|
|
577
|
+
"This bridge is a dev/demo surface. Authenticated MCP tools require Q5c signing; for production, import `@truealter/sdk` directly. Bridge signing lands in Wave-2."
|
|
578
|
+
);
|
|
388
579
|
var client = new MCPClient({
|
|
389
580
|
endpoint: ENDPOINT,
|
|
390
581
|
apiKey: API_KEY,
|
|
391
|
-
clientInfo: { name: "@truealter/sdk-mcp-bridge", version: "0.2.0" }
|
|
582
|
+
clientInfo: { name: "@truealter/sdk-mcp-bridge", version: "0.2.0" },
|
|
583
|
+
extraHeaders: EXTRA_HEADERS
|
|
392
584
|
});
|
|
393
585
|
function send(response) {
|
|
394
586
|
stdout.write(JSON.stringify(response) + "\n");
|