@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.
@@ -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 && Number(envelope.amount) > this.maxPerQuery) {
107
- throw new AlterError(
108
- "PAYMENT_REQUIRED",
109
- `quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
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 retryAfter = Number(resp.headers.get("Retry-After") ?? 60);
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");