@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.
- package/README.md +185 -108
- package/dist/bin/mcp-bridge.js +178 -13
- package/dist/index.cjs +679 -82
- package/dist/index.d.cts +524 -149
- package/dist/index.d.ts +524 -149
- package/dist/index.js +658 -83
- package/package.json +4 -7
- package/dist/bin/alter-identity.js +0 -2306
- package/dist/mcp-bridge.js +0 -166
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
592
|
-
|
|
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 };
|