@truealter/sdk 0.5.1 → 0.5.8

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.
@@ -2,16 +2,25 @@
2
2
  import { p256 } from '@noble/curves/p256';
3
3
  import { sha256 } from '@noble/hashes/sha256';
4
4
  import { randomBytes } from '@noble/hashes/utils';
5
+ import * as crypto from 'crypto';
5
6
  import { createPrivateKey } from 'crypto';
6
7
  import { createInterface } from 'readline';
7
8
  import { env, stderr, exit, stdin, stdout } from 'process';
9
+ import * as fs from 'fs';
10
+ import * as nodePath from 'path';
11
+ import * as os from 'os';
8
12
 
9
13
  var __defProp = Object.defineProperty;
10
14
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
15
  var __getOwnPropNames = Object.getOwnPropertyNames;
12
16
  var __hasOwnProp = Object.prototype.hasOwnProperty;
13
- var __esm = (fn, res) => function __init() {
14
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ var __esm = (fn, res, err) => function __init() {
18
+ if (err) throw err[0];
19
+ try {
20
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
+ } catch (e) {
22
+ throw err = [e], e;
23
+ }
15
24
  };
16
25
  var __export = (target, all) => {
17
26
  for (var name in all)
@@ -214,6 +223,10 @@ var AlterInvalidResponse = class extends AlterError {
214
223
  Object.setPrototypeOf(this, new.target.prototype);
215
224
  }
216
225
  };
226
+
227
+ // src/meta.ts
228
+ var SDK_NAME = "@truealter/sdk";
229
+ var SDK_VERSION = "0.5.8" ;
217
230
  var X402Client = class {
218
231
  signer;
219
232
  maxPerQuery;
@@ -324,6 +337,9 @@ var MCPClient = class {
324
337
  x402;
325
338
  signing;
326
339
  extraHeaders;
340
+ preflightHook;
341
+ preflightPromise = null;
342
+ preflightDone = false;
327
343
  requestCounter = 0;
328
344
  initialised = false;
329
345
  constructor(opts = {}) {
@@ -332,17 +348,43 @@ var MCPClient = class {
332
348
  this.fetchImpl = opts.fetch ?? fetch;
333
349
  this.timeoutMs = opts.timeoutMs ?? 3e4;
334
350
  this.maxRetries = opts.maxRetries ?? 2;
335
- this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
351
+ this.clientInfo = opts.clientInfo ?? { name: SDK_NAME, version: SDK_VERSION };
336
352
  this.x402 = opts.x402;
337
353
  this.signing = opts.signing;
338
354
  this.extraHeaders = opts.extraHeaders;
355
+ this.preflightHook = opts.preflightHook;
356
+ }
357
+ /**
358
+ * Run the lazy version-floor preflight hook exactly once.
359
+ * Idempotent and serialised: concurrent callers share the same
360
+ * promise. Throws from the hook propagate to every concurrent caller.
361
+ */
362
+ async runPreflight() {
363
+ if (this.preflightDone) return;
364
+ if (!this.preflightHook) {
365
+ this.preflightDone = true;
366
+ return;
367
+ }
368
+ if (!this.preflightPromise) {
369
+ this.preflightPromise = this.preflightHook().then(
370
+ () => {
371
+ this.preflightDone = true;
372
+ },
373
+ (err) => {
374
+ this.preflightPromise = null;
375
+ throw err;
376
+ }
377
+ );
378
+ }
379
+ await this.preflightPromise;
339
380
  }
340
381
  /**
341
382
  * Send the MCP `initialize` handshake and capture the resulting session
342
- * id. Idempotent safe to call multiple times.
383
+ * id. Idempotent: safe to call multiple times.
343
384
  */
344
385
  async initialize() {
345
386
  if (this.initialised) return null;
387
+ await this.runPreflight();
346
388
  const result = await this.rpc("initialize", {
347
389
  protocolVersion: MCP_PROTOCOL_VERSION,
348
390
  capabilities: {},
@@ -435,7 +477,14 @@ var MCPClient = class {
435
477
  method: "POST",
436
478
  headers: this.buildHeaders(signatureHeader),
437
479
  body: JSON.stringify(payload),
438
- signal: controller.signal
480
+ signal: controller.signal,
481
+ // Prevent fetch from silently following 3xx redirects. When
482
+ // Cloudflare Access credentials are absent or expired the edge
483
+ // returns HTTP 302 → CF Access login page (text/html). Without
484
+ // this option undici follows the redirect, lands on a 200 HTML
485
+ // body, and resp.json() throws the opaque "invalid JSON body"
486
+ // error that was surfaced as "MCP <method>: invalid JSON body".
487
+ redirect: "manual"
439
488
  });
440
489
  } catch (err) {
441
490
  clearTimeout(timer);
@@ -452,6 +501,19 @@ var MCPClient = class {
452
501
  clearTimeout(timer);
453
502
  const sessionHeader = resp.headers.get("Mcp-Session-Id");
454
503
  if (sessionHeader) this.sessionId = sessionHeader;
504
+ if (resp.status >= 300 && resp.status < 400) {
505
+ const location = resp.headers.get("Location") ?? "";
506
+ const isAuthRedirect = location.includes("cloudflareaccess.com") || location.includes("/cdn-cgi/access/") || !location.startsWith("/") && !location.startsWith(new URL(this.endpoint).origin);
507
+ if (isAuthRedirect) {
508
+ throw new AlterAuthError(
509
+ `MCP ${method}: Cloudflare Access blocked the request (session expired or credentials missing). Run \`alter login\` to re-authenticate.`,
510
+ 302
511
+ );
512
+ }
513
+ throw new AlterNetworkError(
514
+ `MCP ${method}: unexpected redirect ${resp.status} to ${location || "(no Location)"}`
515
+ );
516
+ }
455
517
  if (resp.status === 401 || resp.status === 403) {
456
518
  throw new AlterAuthError(`HTTP ${resp.status} on ${method}`, resp.status);
457
519
  }
@@ -476,11 +538,56 @@ var MCPClient = class {
476
538
  const body2 = await safeText(resp);
477
539
  throw new AlterError("NETWORK", `HTTP ${resp.status} on ${method}: ${body2.slice(0, 200)}`);
478
540
  }
541
+ const contentType = resp.headers.get("Content-Type") ?? "";
542
+ const isHtml = contentType.includes("text/html");
543
+ const isSse = contentType.includes("text/event-stream");
544
+ if (isHtml || isSse) {
545
+ if (isSse) {
546
+ const rawText = await safeText(resp);
547
+ const dataLine = rawText.split("\n").find((l) => l.startsWith("data:"));
548
+ if (dataLine) {
549
+ const jsonPart = dataLine.slice("data:".length).trim();
550
+ try {
551
+ const parsed = JSON.parse(jsonPart);
552
+ if (parsed.error) {
553
+ const code = parsed.error.code;
554
+ const message = parsed.error.message ?? `MCP ${method} error`;
555
+ throw new AlterToolError(this.guessToolName(payload), message, code);
556
+ }
557
+ return parsed.result;
558
+ } catch (parseErr) {
559
+ if (parseErr instanceof AlterError) throw parseErr;
560
+ throw new AlterInvalidResponse(
561
+ `MCP ${method}: could not parse SSE data frame as JSON`,
562
+ parseErr
563
+ );
564
+ }
565
+ }
566
+ throw new AlterInvalidResponse(
567
+ `MCP ${method}: received text/event-stream response with no data: frame`
568
+ );
569
+ }
570
+ const excerpt = (await safeText(resp)).slice(0, 300);
571
+ const looksLikeLoginPage = excerpt.toLowerCase().includes("cloudflareaccess") || excerpt.toLowerCase().includes("access denied") || excerpt.toLowerCase().includes("<title>");
572
+ if (looksLikeLoginPage) {
573
+ throw new AlterAuthError(
574
+ `MCP ${method}: received an HTML login page instead of JSON (Content-Type: ${contentType}). Run \`alter login\` to re-authenticate.`,
575
+ 200
576
+ );
577
+ }
578
+ throw new AlterInvalidResponse(
579
+ `MCP ${method}: unexpected Content-Type "${contentType}" (expected application/json). Body excerpt: ${excerpt.slice(0, 120)}`
580
+ );
581
+ }
479
582
  let body;
480
583
  try {
481
584
  body = await resp.json();
482
585
  } catch (err) {
483
- throw new AlterInvalidResponse(`MCP ${method}: invalid JSON body`, err);
586
+ const hint = contentType ? ` (Content-Type: ${contentType})` : "";
587
+ throw new AlterInvalidResponse(
588
+ `MCP ${method}: failed to parse JSON response${hint}. The server may have returned a non-JSON body. Run \`alter login\` if the session is expired.`,
589
+ err
590
+ );
484
591
  }
485
592
  if (body.error) {
486
593
  const code = body.error.code;
@@ -501,8 +608,11 @@ var MCPClient = class {
501
608
  const headers = {
502
609
  ...this.extraHeaders ?? {},
503
610
  "Content-Type": "application/json",
504
- Accept: "application/json",
505
- "User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`
611
+ Accept: "application/json, text/event-stream",
612
+ "User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`,
613
+ "X-Alter-Client-Id": "alter-identity",
614
+ "X-Alter-Client-Version": SDK_VERSION,
615
+ "X-Alter-Client-Channel": "npm"
506
616
  };
507
617
  if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
508
618
  if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
@@ -566,7 +676,7 @@ async function safeText(resp) {
566
676
  }
567
677
 
568
678
  // bin/mcp-bridge.ts
569
- var ENDPOINT = env.ALTER_MCP_ENDPOINT ?? "https://mcp.truealter.com/api/v1/mcp";
679
+ var ENDPOINT = env.ALTER_MCP_ENDPOINT ?? "https://api.truealter.com/api/v1/mcp";
570
680
  var API_KEY = env.ALTER_API_KEY ?? void 0;
571
681
  function buildExtraHeaders() {
572
682
  const headers = {};
@@ -588,14 +698,67 @@ function buildExtraHeaders() {
588
698
  return Object.keys(headers).length ? headers : void 0;
589
699
  }
590
700
  var EXTRA_HEADERS = buildExtraHeaders();
591
- console.warn(
592
- "This bridge is a dev/demo surface. Authenticated MCP tools require ES256 per-invocation signing; for production, import `@truealter/sdk` directly. Bridge signing lands in Wave-2."
593
- );
701
+ var xdgConfig = env.XDG_CONFIG_HOME ?? nodePath.join(os.homedir(), ".config");
702
+ function readSession() {
703
+ const sessionFile = env.ALTER_SESSION_FILE ?? nodePath.join(xdgConfig, "alter", "session.json");
704
+ try {
705
+ const raw = fs.readFileSync(sessionFile, "utf8");
706
+ const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
707
+ return JSON.parse(clean);
708
+ } catch {
709
+ return {};
710
+ }
711
+ }
712
+ function resolveSigningOptions(session) {
713
+ const envPem = env.ALTER_SIGNING_KEY;
714
+ if (envPem) {
715
+ const envKid = env.ALTER_SIGNING_KID ?? session.signing_kid;
716
+ if (envKid) {
717
+ try {
718
+ crypto.createPrivateKey(envPem);
719
+ return { kid: envKid, privateKey: envPem, handle: session.handle ?? "" };
720
+ } catch (e) {
721
+ stderr.write(`bridge: ALTER_SIGNING_KEY parse error: ${e.message}
722
+ `);
723
+ }
724
+ }
725
+ }
726
+ const kid = session.signing_kid;
727
+ if (!kid) return null;
728
+ const candidates = [];
729
+ if (env.ALTER_SIGNING_KEY_FILE) candidates.push(env.ALTER_SIGNING_KEY_FILE);
730
+ candidates.push(nodePath.join(xdgConfig, "alter", "signing-keys", `${kid}.pem`));
731
+ candidates.push(nodePath.join(xdgConfig, "alter", "signing-key.pem"));
732
+ for (const p of candidates) {
733
+ try {
734
+ const pem = fs.readFileSync(p, "utf8");
735
+ crypto.createPrivateKey(pem);
736
+ return { kid, privateKey: pem, handle: session.handle ?? "" };
737
+ } catch {
738
+ }
739
+ }
740
+ return null;
741
+ }
742
+ var _session = readSession();
743
+ var _signingOpts = resolveSigningOptions(_session);
744
+ if (API_KEY && !_signingOpts) {
745
+ stderr.write(
746
+ `bridge: no signing key for kid ${_session.signing_kid ?? "(unset)"}: run 'alter login' to provision one
747
+ `
748
+ );
749
+ }
594
750
  var client = new MCPClient({
595
751
  endpoint: ENDPOINT,
596
752
  apiKey: API_KEY,
597
753
  clientInfo: { name: "@truealter/sdk-mcp-bridge", version: "0.2.0" },
598
- extraHeaders: EXTRA_HEADERS
754
+ extraHeaders: EXTRA_HEADERS,
755
+ ..._signingOpts ? {
756
+ signing: {
757
+ kid: _signingOpts.kid,
758
+ privateKey: _signingOpts.privateKey,
759
+ handle: _signingOpts.handle
760
+ }
761
+ } : {}
599
762
  });
600
763
  function send(response) {
601
764
  stdout.write(JSON.stringify(response) + "\n");
@@ -694,3 +857,5 @@ main().catch((err) => {
694
857
  `);
695
858
  exit(1);
696
859
  });
860
+
861
+ export { resolveSigningOptions };