@truealter/sdk 0.5.1 → 0.5.3
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 +115 -50
- package/dist/bin/alter-identity.js +383 -48
- package/dist/bin/mcp-bridge.js +40 -4
- package/dist/index.cjs +517 -64
- package/dist/index.d.cts +480 -128
- package/dist/index.d.ts +480 -128
- package/dist/index.js +496 -65
- package/package.json +3 -3
- package/dist/mcp-bridge.js +0 -166
|
@@ -2,26 +2,21 @@
|
|
|
2
2
|
import { p256 } from '@noble/curves/p256';
|
|
3
3
|
import { sha256 } from '@noble/hashes/sha256';
|
|
4
4
|
import { hexToBytes, bytesToHex as bytesToHex$1, randomBytes } from '@noble/hashes/utils';
|
|
5
|
-
import { createHash, createPrivateKey } from 'crypto';
|
|
6
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, renameSync, copyFileSync } from 'fs';
|
|
5
|
+
import { createPublicKey, verify, createHash, createPrivateKey } from 'crypto';
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, renameSync, statSync, chmodSync, copyFileSync } from 'fs';
|
|
7
7
|
import { homedir, platform } from 'os';
|
|
8
8
|
import { join, dirname, resolve } from 'path';
|
|
9
9
|
import { env, stderr, exit, argv, stdout, stdin } from 'process';
|
|
10
10
|
import { createInterface } from 'readline';
|
|
11
11
|
import * as ed25519 from '@noble/ed25519';
|
|
12
12
|
import { sha512 } from '@noble/hashes/sha512';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
13
14
|
import { spawnSync } from 'child_process';
|
|
14
15
|
|
|
15
16
|
var __defProp = Object.defineProperty;
|
|
16
17
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
17
18
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
18
19
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
19
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
20
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
21
|
-
}) : x)(function(x) {
|
|
22
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
23
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
24
|
-
});
|
|
25
20
|
var __esm = (fn, res) => function __init() {
|
|
26
21
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
27
22
|
};
|
|
@@ -368,11 +363,11 @@ async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
|
|
|
368
363
|
}
|
|
369
364
|
if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
|
|
370
365
|
throw new AlterNetworkError(
|
|
371
|
-
`${url}
|
|
366
|
+
`${url} -> redirect rejected (discovery must not follow redirects; validate the server configuration)`
|
|
372
367
|
);
|
|
373
368
|
}
|
|
374
369
|
if (resp.status === 404) return null;
|
|
375
|
-
if (!resp.ok) throw new AlterNetworkError(`${url}
|
|
370
|
+
if (!resp.ok) throw new AlterNetworkError(`${url} -> HTTP ${resp.status}`);
|
|
376
371
|
const doc = await resp.json();
|
|
377
372
|
if (file === "mcp.json") {
|
|
378
373
|
const remotes = doc.remotes || [];
|
|
@@ -405,6 +400,309 @@ function ensureMcpPath(url) {
|
|
|
405
400
|
return url;
|
|
406
401
|
}
|
|
407
402
|
}
|
|
403
|
+
|
|
404
|
+
// src/meta.ts
|
|
405
|
+
var SDK_NAME = "@truealter/sdk";
|
|
406
|
+
var SDK_VERSION = "0.5.3" ;
|
|
407
|
+
|
|
408
|
+
// src/floor-preflight.ts
|
|
409
|
+
var MIN_VERSION_ENDPOINT = "/v1/clients/min-version";
|
|
410
|
+
var CLIENT_ID = "alter-identity";
|
|
411
|
+
var CLIENT_CHANNEL = "npm";
|
|
412
|
+
var IN_MEMORY_TTL_DEFAULT_MS = 60 * 60 * 1e3;
|
|
413
|
+
var IN_MEMORY_TTL_MIN_MS = 60 * 1e3;
|
|
414
|
+
var IN_MEMORY_TTL_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
415
|
+
var DISK_FRESH_MS = 24 * 60 * 60 * 1e3;
|
|
416
|
+
var DISK_WARN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
417
|
+
var FETCH_TIMEOUT_MS = 4e3;
|
|
418
|
+
function computeKeyId(publicKeyPem) {
|
|
419
|
+
if (!publicKeyPem) return "00000000";
|
|
420
|
+
const pub = createPublicKey({ key: publicKeyPem, format: "pem" });
|
|
421
|
+
const jwk = pub.export({ format: "jwk" });
|
|
422
|
+
const rawBytes = Buffer.from(jwk.x, "base64url");
|
|
423
|
+
return createHash("sha256").update(rawBytes).digest("hex").slice(0, 8);
|
|
424
|
+
}
|
|
425
|
+
function canonicalJson(obj) {
|
|
426
|
+
return JSON.stringify(sortKeysDeep(obj));
|
|
427
|
+
}
|
|
428
|
+
function sortKeysDeep(value) {
|
|
429
|
+
if (Array.isArray(value)) {
|
|
430
|
+
return value.map(sortKeysDeep);
|
|
431
|
+
}
|
|
432
|
+
if (value !== null && typeof value === "object") {
|
|
433
|
+
const obj = value;
|
|
434
|
+
const sorted = {};
|
|
435
|
+
for (const k of Object.keys(obj).sort()) {
|
|
436
|
+
sorted[k] = sortKeysDeep(obj[k]);
|
|
437
|
+
}
|
|
438
|
+
return sorted;
|
|
439
|
+
}
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
var KNOWN_FLOOR_PUBLIC_KEYS = {
|
|
443
|
+
"8aa59e05": `-----BEGIN PUBLIC KEY-----
|
|
444
|
+
MCowBQYDK2VwAyEAgqw28dlniOuiTE1f4BxCPSEgMLaPtHsO8wN5RWEwEhE=
|
|
445
|
+
-----END PUBLIC KEY-----`,
|
|
446
|
+
"640f7d9a": `-----BEGIN PUBLIC KEY-----
|
|
447
|
+
MCowBQYDK2VwAyEARzvAWayDwHvZRfOZizGZe+/a7PF082WGhyMS3tx06H4=
|
|
448
|
+
-----END PUBLIC KEY-----`
|
|
449
|
+
};
|
|
450
|
+
var BelowFloorError = class extends Error {
|
|
451
|
+
name = "BelowFloorError";
|
|
452
|
+
code = "client_below_floor";
|
|
453
|
+
client_version;
|
|
454
|
+
min_version;
|
|
455
|
+
upgrade_cmd;
|
|
456
|
+
channel;
|
|
457
|
+
envelope;
|
|
458
|
+
constructor(envelope) {
|
|
459
|
+
super(envelope.error.message);
|
|
460
|
+
this.envelope = envelope;
|
|
461
|
+
this.client_version = envelope.error.client_version;
|
|
462
|
+
this.min_version = envelope.error.min_version;
|
|
463
|
+
this.upgrade_cmd = envelope.error.upgrade_cmd;
|
|
464
|
+
this.channel = envelope.error.channel;
|
|
465
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var memCache = null;
|
|
469
|
+
async function checkMinVersion(opts = {}) {
|
|
470
|
+
const apiBase = opts.apiBase ?? defaultApiBase();
|
|
471
|
+
const clientVersion = opts.clientVersion ?? SDK_VERSION;
|
|
472
|
+
const clientId = opts.clientId ?? CLIENT_ID;
|
|
473
|
+
const channel = opts.channel ?? CLIENT_CHANNEL;
|
|
474
|
+
const knownKeys = opts.knownFloorPublicKeys ?? KNOWN_FLOOR_PUBLIC_KEYS;
|
|
475
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
476
|
+
const now = opts.now ?? Date.now;
|
|
477
|
+
const cachePath = opts.diskCachePath === void 0 ? defaultDiskCachePath() : opts.diskCachePath;
|
|
478
|
+
const mem = readInMemoryCache(now);
|
|
479
|
+
if (mem) {
|
|
480
|
+
return compareAndPermit(mem, {
|
|
481
|
+
clientVersion,
|
|
482
|
+
clientId,
|
|
483
|
+
channel,
|
|
484
|
+
diagnostic: "mem-cache-hit"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const disk = cachePath ? readDiskCache(cachePath, knownKeys) : null;
|
|
488
|
+
const diskAgeMs = disk ? now() - disk.fetched_at_ms : Number.POSITIVE_INFINITY;
|
|
489
|
+
let fetched = null;
|
|
490
|
+
let fetchError = null;
|
|
491
|
+
if (!disk || diskAgeMs > IN_MEMORY_TTL_DEFAULT_MS) {
|
|
492
|
+
try {
|
|
493
|
+
fetched = await fetchFloorDoc(apiBase, fetchImpl, knownKeys);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
fetchError = err.message ?? "fetch-error";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (fetched) {
|
|
499
|
+
populateMemCache(fetched, now());
|
|
500
|
+
if (cachePath) writeDiskCache(cachePath, fetched, now());
|
|
501
|
+
return compareAndPermit(fetched, {
|
|
502
|
+
clientVersion,
|
|
503
|
+
clientId,
|
|
504
|
+
channel,
|
|
505
|
+
diagnostic: "fetched"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (disk) {
|
|
509
|
+
populateMemCache(disk.doc, disk.fetched_at_ms);
|
|
510
|
+
if (diskAgeMs > DISK_WARN_MS) {
|
|
511
|
+
return compareAndPermit(disk.doc, {
|
|
512
|
+
clientVersion,
|
|
513
|
+
clientId,
|
|
514
|
+
channel,
|
|
515
|
+
diagnostic: "below-floor-offline-stale-or-permit",
|
|
516
|
+
warn: `floor cache is >7d old and backend unreachable (${fetchError ?? "no refresh attempted"}); permitting if above floor`
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (diskAgeMs > DISK_FRESH_MS) {
|
|
520
|
+
return compareAndPermit(disk.doc, {
|
|
521
|
+
clientVersion,
|
|
522
|
+
clientId,
|
|
523
|
+
channel,
|
|
524
|
+
diagnostic: "warn-stale-permit",
|
|
525
|
+
warn: `floor cache is ${Math.round(diskAgeMs / (60 * 60 * 1e3))}h old; refresh recommended`
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return compareAndPermit(disk.doc, {
|
|
529
|
+
clientVersion,
|
|
530
|
+
clientId,
|
|
531
|
+
channel,
|
|
532
|
+
diagnostic: "disk-cache-hit"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
ok: true,
|
|
537
|
+
floor: null,
|
|
538
|
+
diagnostic: "no-cache-no-fetch-permit",
|
|
539
|
+
warn: `floor preflight skipped: backend unreachable (${fetchError ?? "unknown"})`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function compareAndPermit(doc, ctx) {
|
|
543
|
+
const floor = lookupFloor(doc, ctx.clientId, ctx.channel);
|
|
544
|
+
if (!floor) {
|
|
545
|
+
return { ok: true, floor: null, diagnostic: `${ctx.diagnostic}+no-floor`, warn: ctx.warn };
|
|
546
|
+
}
|
|
547
|
+
if (compareSemver(ctx.clientVersion, floor.min_version) >= 0) {
|
|
548
|
+
return { ok: true, floor, diagnostic: ctx.diagnostic, warn: ctx.warn };
|
|
549
|
+
}
|
|
550
|
+
const envelope = {
|
|
551
|
+
error: {
|
|
552
|
+
code: "client_below_floor",
|
|
553
|
+
message: `Your ${ctx.clientId} is too old. Upgrade required.`,
|
|
554
|
+
client_version: ctx.clientVersion,
|
|
555
|
+
min_version: floor.min_version,
|
|
556
|
+
upgrade_cmd: floor.upgrade_cmd,
|
|
557
|
+
channel: ctx.channel
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
throw new BelowFloorError(envelope);
|
|
561
|
+
}
|
|
562
|
+
function lookupFloor(doc, clientId, channel) {
|
|
563
|
+
const entry = doc.floors[clientId];
|
|
564
|
+
if (!entry) return null;
|
|
565
|
+
if (isChannelFloor(entry)) return entry;
|
|
566
|
+
const exact = entry[channel];
|
|
567
|
+
if (exact) return exact;
|
|
568
|
+
const fallback = entry["unknown"];
|
|
569
|
+
if (fallback) return fallback;
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
function isChannelFloor(v) {
|
|
573
|
+
return typeof v.min_version === "string" && typeof v.upgrade_cmd === "string";
|
|
574
|
+
}
|
|
575
|
+
function compareSemver(a, b) {
|
|
576
|
+
const [aMaj, aMin, aPat, aPre] = parseSemver(a);
|
|
577
|
+
const [bMaj, bMin, bPat, bPre] = parseSemver(b);
|
|
578
|
+
if (aMaj !== bMaj) return aMaj - bMaj;
|
|
579
|
+
if (aMin !== bMin) return aMin - bMin;
|
|
580
|
+
if (aPat !== bPat) return aPat - bPat;
|
|
581
|
+
if (aPre && !bPre) return -1;
|
|
582
|
+
if (!aPre && bPre) return 1;
|
|
583
|
+
if (aPre && bPre) return aPre.localeCompare(bPre);
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
function parseSemver(v) {
|
|
587
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);
|
|
588
|
+
if (!m) return [0, 0, 0, null];
|
|
589
|
+
return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] ?? null];
|
|
590
|
+
}
|
|
591
|
+
function verifyFloorSignature(doc, keys = KNOWN_FLOOR_PUBLIC_KEYS) {
|
|
592
|
+
const pem = keys[doc.key_id];
|
|
593
|
+
if (!pem) return false;
|
|
594
|
+
if (computeKeyId(pem) !== doc.key_id) return false;
|
|
595
|
+
try {
|
|
596
|
+
const pubKeyObject = createPublicKey({ key: pem, format: "pem" });
|
|
597
|
+
const canonical = canonicalJson({
|
|
598
|
+
floors: doc.floors,
|
|
599
|
+
served_at: doc.served_at
|
|
600
|
+
});
|
|
601
|
+
return verify(
|
|
602
|
+
null,
|
|
603
|
+
Buffer.from(canonical, "utf-8"),
|
|
604
|
+
pubKeyObject,
|
|
605
|
+
Buffer.from(doc.signature, "hex")
|
|
606
|
+
);
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function fetchFloorDoc(apiBase, fetchImpl, knownKeys) {
|
|
612
|
+
const url = `${apiBase.replace(/\/+$/, "")}${MIN_VERSION_ENDPOINT}`;
|
|
613
|
+
let response;
|
|
614
|
+
try {
|
|
615
|
+
response = await fetchImpl(url, {
|
|
616
|
+
headers: {
|
|
617
|
+
accept: "application/json",
|
|
618
|
+
"X-Alter-Client-Id": CLIENT_ID,
|
|
619
|
+
"X-Alter-Client-Version": SDK_VERSION,
|
|
620
|
+
"X-Alter-Client-Channel": CLIENT_CHANNEL,
|
|
621
|
+
"User-Agent": `${SDK_NAME}/${SDK_VERSION}`
|
|
622
|
+
},
|
|
623
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
624
|
+
});
|
|
625
|
+
} catch (err) {
|
|
626
|
+
throw new Error(`network: ${err.message ?? String(err)}`);
|
|
627
|
+
}
|
|
628
|
+
if (!response.ok) throw new Error(`http-${response.status}`);
|
|
629
|
+
const body = await response.json();
|
|
630
|
+
if (!body || !body.floors || !body.signature || !body.key_id) {
|
|
631
|
+
throw new Error("malformed-floor-doc");
|
|
632
|
+
}
|
|
633
|
+
if (!verifyFloorSignature(body, knownKeys)) {
|
|
634
|
+
throw new Error("signature-invalid");
|
|
635
|
+
}
|
|
636
|
+
return body;
|
|
637
|
+
}
|
|
638
|
+
function readInMemoryCache(now) {
|
|
639
|
+
if (!memCache) return null;
|
|
640
|
+
if (now() - memCache.fetched_at_ms > memCache.ttl_ms) {
|
|
641
|
+
memCache = null;
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
return memCache.doc;
|
|
645
|
+
}
|
|
646
|
+
function populateMemCache(doc, fetched_at_ms) {
|
|
647
|
+
const ttlSec = doc.cache_ttl_seconds ?? 3600;
|
|
648
|
+
const ttlMs = Math.min(
|
|
649
|
+
Math.max(ttlSec * 1e3, IN_MEMORY_TTL_MIN_MS),
|
|
650
|
+
IN_MEMORY_TTL_MAX_MS
|
|
651
|
+
);
|
|
652
|
+
memCache = { doc, fetched_at_ms, ttl_ms: ttlMs };
|
|
653
|
+
}
|
|
654
|
+
function defaultDiskCachePath() {
|
|
655
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
656
|
+
return join(xdg, "alter", "floor-cache.json");
|
|
657
|
+
}
|
|
658
|
+
function readDiskCache(path, knownKeys) {
|
|
659
|
+
if (process.platform !== "win32") {
|
|
660
|
+
let st;
|
|
661
|
+
try {
|
|
662
|
+
st = statSync(path);
|
|
663
|
+
} catch {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
const euid = typeof process.geteuid === "function" ? process.geteuid() : st.uid;
|
|
667
|
+
if (st.uid !== euid) return null;
|
|
668
|
+
if ((st.mode & 511) !== 384) return null;
|
|
669
|
+
}
|
|
670
|
+
let raw;
|
|
671
|
+
try {
|
|
672
|
+
raw = readFileSync(path, "utf-8");
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
let parsed;
|
|
677
|
+
try {
|
|
678
|
+
parsed = JSON.parse(raw);
|
|
679
|
+
} catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
if (!parsed.doc || typeof parsed.fetched_at_ms !== "number") return null;
|
|
683
|
+
if (!verifyFloorSignature(parsed.doc, knownKeys)) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
return parsed;
|
|
687
|
+
}
|
|
688
|
+
function writeDiskCache(path, doc, now_ms) {
|
|
689
|
+
const entry = { doc, fetched_at_ms: now_ms };
|
|
690
|
+
const payload = JSON.stringify(entry);
|
|
691
|
+
try {
|
|
692
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
693
|
+
const tmp = `${path}.tmp`;
|
|
694
|
+
writeFileSync(tmp, payload, { mode: 384 });
|
|
695
|
+
try {
|
|
696
|
+
chmodSync(tmp, 384);
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
renameSync(tmp, path);
|
|
700
|
+
} catch {
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function defaultApiBase() {
|
|
704
|
+
return process.env.ALTER_API ?? "https://api.truealter.com";
|
|
705
|
+
}
|
|
408
706
|
var X402Client = class {
|
|
409
707
|
signer;
|
|
410
708
|
maxPerQuery;
|
|
@@ -515,6 +813,9 @@ var MCPClient = class {
|
|
|
515
813
|
x402;
|
|
516
814
|
signing;
|
|
517
815
|
extraHeaders;
|
|
816
|
+
preflightHook;
|
|
817
|
+
preflightPromise = null;
|
|
818
|
+
preflightDone = false;
|
|
518
819
|
requestCounter = 0;
|
|
519
820
|
initialised = false;
|
|
520
821
|
constructor(opts = {}) {
|
|
@@ -523,17 +824,43 @@ var MCPClient = class {
|
|
|
523
824
|
this.fetchImpl = opts.fetch ?? fetch;
|
|
524
825
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
525
826
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
526
|
-
this.clientInfo = opts.clientInfo ?? { name:
|
|
827
|
+
this.clientInfo = opts.clientInfo ?? { name: SDK_NAME, version: SDK_VERSION };
|
|
527
828
|
this.x402 = opts.x402;
|
|
528
829
|
this.signing = opts.signing;
|
|
529
830
|
this.extraHeaders = opts.extraHeaders;
|
|
831
|
+
this.preflightHook = opts.preflightHook;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Run the lazy preflight hook (D-MIN-VERSION-FLOOR-1) exactly once.
|
|
835
|
+
* Idempotent and serialised: concurrent callers share the same
|
|
836
|
+
* promise. Throws from the hook propagate to every concurrent caller.
|
|
837
|
+
*/
|
|
838
|
+
async runPreflight() {
|
|
839
|
+
if (this.preflightDone) return;
|
|
840
|
+
if (!this.preflightHook) {
|
|
841
|
+
this.preflightDone = true;
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (!this.preflightPromise) {
|
|
845
|
+
this.preflightPromise = this.preflightHook().then(
|
|
846
|
+
() => {
|
|
847
|
+
this.preflightDone = true;
|
|
848
|
+
},
|
|
849
|
+
(err) => {
|
|
850
|
+
this.preflightPromise = null;
|
|
851
|
+
throw err;
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
await this.preflightPromise;
|
|
530
856
|
}
|
|
531
857
|
/**
|
|
532
858
|
* Send the MCP `initialize` handshake and capture the resulting session
|
|
533
|
-
* id. Idempotent
|
|
859
|
+
* id. Idempotent: safe to call multiple times.
|
|
534
860
|
*/
|
|
535
861
|
async initialize() {
|
|
536
862
|
if (this.initialised) return null;
|
|
863
|
+
await this.runPreflight();
|
|
537
864
|
const result = await this.rpc("initialize", {
|
|
538
865
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
539
866
|
capabilities: {},
|
|
@@ -693,7 +1020,10 @@ var MCPClient = class {
|
|
|
693
1020
|
...this.extraHeaders ?? {},
|
|
694
1021
|
"Content-Type": "application/json",
|
|
695
1022
|
Accept: "application/json",
|
|
696
|
-
"User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}
|
|
1023
|
+
"User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`,
|
|
1024
|
+
"X-Alter-Client-Id": "alter-identity",
|
|
1025
|
+
"X-Alter-Client-Version": SDK_VERSION,
|
|
1026
|
+
"X-Alter-Client-Channel": "npm"
|
|
697
1027
|
};
|
|
698
1028
|
if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
|
|
699
1029
|
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
@@ -951,7 +1281,7 @@ async function verifyToolSignatures(tools, signatures, opts = {}) {
|
|
|
951
1281
|
out.push({ tool: tool.name, valid: false, reason: "no signature published" });
|
|
952
1282
|
continue;
|
|
953
1283
|
}
|
|
954
|
-
const expectedHash = await sha256Hex(
|
|
1284
|
+
const expectedHash = await sha256Hex(canonicalJson2(tool.inputSchema));
|
|
955
1285
|
if (expectedHash !== sig.schema_hash) {
|
|
956
1286
|
out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
|
|
957
1287
|
continue;
|
|
@@ -1035,23 +1365,23 @@ async function fetchJwks(url, fetchImpl) {
|
|
|
1035
1365
|
}
|
|
1036
1366
|
if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
|
|
1037
1367
|
throw new AlterProvenanceError(
|
|
1038
|
-
`${url}
|
|
1368
|
+
`${url} -> redirect rejected (allowlist enforces initial URL only)`
|
|
1039
1369
|
);
|
|
1040
1370
|
}
|
|
1041
|
-
if (!resp.ok) throw new AlterNetworkError(`${url}
|
|
1371
|
+
if (!resp.ok) throw new AlterNetworkError(`${url} -> HTTP ${resp.status}`);
|
|
1042
1372
|
const contentLength = resp.headers.get("content-length");
|
|
1043
1373
|
if (contentLength !== null) {
|
|
1044
1374
|
const n = Number.parseInt(contentLength, 10);
|
|
1045
1375
|
if (Number.isFinite(n) && n > JWKS_MAX_BYTES) {
|
|
1046
1376
|
throw new AlterProvenanceError(
|
|
1047
|
-
`${url}
|
|
1377
|
+
`${url} -> JWKS too large: ${n} > ${JWKS_MAX_BYTES} bytes`
|
|
1048
1378
|
);
|
|
1049
1379
|
}
|
|
1050
1380
|
}
|
|
1051
1381
|
const body = await resp.text();
|
|
1052
1382
|
if (body.length > JWKS_MAX_BYTES) {
|
|
1053
1383
|
throw new AlterProvenanceError(
|
|
1054
|
-
`${url}
|
|
1384
|
+
`${url} -> JWKS too large: ${body.length} > ${JWKS_MAX_BYTES} bytes`
|
|
1055
1385
|
);
|
|
1056
1386
|
}
|
|
1057
1387
|
let doc;
|
|
@@ -1135,14 +1465,14 @@ async function sha256Hex(input) {
|
|
|
1135
1465
|
function toArrayBuffer(view) {
|
|
1136
1466
|
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
1137
1467
|
}
|
|
1138
|
-
function
|
|
1468
|
+
function canonicalJson2(value) {
|
|
1139
1469
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1140
1470
|
if (Array.isArray(value)) {
|
|
1141
|
-
return `[${value.map(
|
|
1471
|
+
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
1142
1472
|
}
|
|
1143
1473
|
const obj = value;
|
|
1144
1474
|
const keys = Object.keys(obj).sort();
|
|
1145
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${
|
|
1475
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson2(obj[k])}`).join(",")}}`;
|
|
1146
1476
|
}
|
|
1147
1477
|
|
|
1148
1478
|
// src/client.ts
|
|
@@ -1158,11 +1488,21 @@ var AlterClient = class {
|
|
|
1158
1488
|
this.options = options;
|
|
1159
1489
|
this.x402 = options.x402;
|
|
1160
1490
|
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
1161
|
-
|
|
1491
|
+
const preflightHook = options.unsafe_skipVersionCheck ? void 0 : () => checkMinVersion({
|
|
1492
|
+
apiBase: options.apiBase,
|
|
1493
|
+
knownFloorPublicKeys: options.knownFloorPublicKeys,
|
|
1494
|
+
fetchImpl: options.fetch
|
|
1495
|
+
}).then(() => void 0);
|
|
1496
|
+
this.mcp = new MCPClient({
|
|
1497
|
+
...options,
|
|
1498
|
+
endpoint,
|
|
1499
|
+
x402: options.x402,
|
|
1500
|
+
preflightHook
|
|
1501
|
+
});
|
|
1162
1502
|
}
|
|
1163
1503
|
/**
|
|
1164
1504
|
* Resolve the MCP endpoint via discovery if requested. Safe to call
|
|
1165
|
-
* multiple times
|
|
1505
|
+
* multiple times: the first successful lookup is cached.
|
|
1166
1506
|
*/
|
|
1167
1507
|
async discoverEndpoint() {
|
|
1168
1508
|
if (this.discovered) return this.discovered;
|
|
@@ -1175,7 +1515,7 @@ var AlterClient = class {
|
|
|
1175
1515
|
return this.discoveryPromise;
|
|
1176
1516
|
}
|
|
1177
1517
|
/**
|
|
1178
|
-
* Initialise the MCP session. Optional
|
|
1518
|
+
* Initialise the MCP session. Optional: every method calls
|
|
1179
1519
|
* `mcp.initialize()` lazily, but you can call this once at startup if
|
|
1180
1520
|
* you want fail-fast behaviour.
|
|
1181
1521
|
*/
|
|
@@ -1183,11 +1523,11 @@ var AlterClient = class {
|
|
|
1183
1523
|
await this.mcp.initialize();
|
|
1184
1524
|
}
|
|
1185
1525
|
// ── Free tier ────────────────────────────────────────────────────────
|
|
1186
|
-
/** First handshake
|
|
1526
|
+
/** First handshake: confirms the connection, returns trust tier and tool counts. */
|
|
1187
1527
|
async helloAgent() {
|
|
1188
1528
|
return this.mcp.callTool("hello_agent", {});
|
|
1189
1529
|
}
|
|
1190
|
-
/** Resolve a ~handle (e.g. ~
|
|
1530
|
+
/** Resolve a ~handle (e.g. ~example) to its canonical form and kind. No auth required. */
|
|
1191
1531
|
async resolveHandle(args) {
|
|
1192
1532
|
const payload = typeof args === "string" ? { query: args } : args;
|
|
1193
1533
|
return this.mcp.callTool("alter_resolve_handle", payload);
|
|
@@ -1195,7 +1535,7 @@ var AlterClient = class {
|
|
|
1195
1535
|
/** Verify a person is registered with ALTER (handle or id). */
|
|
1196
1536
|
async verify(handleOrId, claims) {
|
|
1197
1537
|
const args = handleOrId.includes("@") ? { member_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
|
|
1198
|
-
// ~handle
|
|
1538
|
+
// ~handle: server resolves these via the member_id field
|
|
1199
1539
|
{ member_id: handleOrId }
|
|
1200
1540
|
) : { member_id: handleOrId };
|
|
1201
1541
|
if (claims) args.claims = claims;
|
|
@@ -1295,7 +1635,7 @@ var AlterClient = class {
|
|
|
1295
1635
|
}
|
|
1296
1636
|
// ── Alter-to-Alter Messaging ─────────────────────────────────────────
|
|
1297
1637
|
// Wave 1: cross-handle direct messages between authenticated tilde
|
|
1298
|
-
// handles. Default closed
|
|
1638
|
+
// handles. Default closed: recipient must have granted the sender via
|
|
1299
1639
|
// alter_message_grant. Spec: docs/technical/Alter-to-Alter Messaging.md.
|
|
1300
1640
|
/** Send a direct message to another tilde handle. */
|
|
1301
1641
|
async messageSend(args) {
|
|
@@ -1329,7 +1669,7 @@ var AlterClient = class {
|
|
|
1329
1669
|
/**
|
|
1330
1670
|
* Verify the ES256 provenance attestation on a tool response.
|
|
1331
1671
|
* Accepts either a {@link ProvenanceEnvelope} or the raw `_meta`
|
|
1332
|
-
* object
|
|
1672
|
+
* object: the latter is more convenient for ad-hoc verification.
|
|
1333
1673
|
*/
|
|
1334
1674
|
async verifyProvenance(envelope) {
|
|
1335
1675
|
if (!envelope) return { valid: false, reason: "no provenance envelope" };
|
|
@@ -1362,7 +1702,7 @@ function generateGenericMcpConfig(opts = {}) {
|
|
|
1362
1702
|
const entry = {
|
|
1363
1703
|
url: opts.endpoint ?? DEFAULT_ENDPOINT,
|
|
1364
1704
|
transport: "streamable-http",
|
|
1365
|
-
description: "ALTER Identity
|
|
1705
|
+
description: "ALTER Identity: psychometric identity field for AI agents"
|
|
1366
1706
|
};
|
|
1367
1707
|
if (Object.keys(headers).length > 0) entry.headers = headers;
|
|
1368
1708
|
return { mcpServers: { [serverName]: entry } };
|
|
@@ -1388,17 +1728,13 @@ function generateClaudeDesktopConfig(opts = {}) {
|
|
|
1388
1728
|
const entry = {
|
|
1389
1729
|
command: bridgeCommand,
|
|
1390
1730
|
env: env3,
|
|
1391
|
-
description: "ALTER Identity
|
|
1731
|
+
description: "ALTER Identity: psychometric identity field for AI agents"
|
|
1392
1732
|
};
|
|
1393
1733
|
if (opts.extraArgs && opts.extraArgs.length > 0) {
|
|
1394
1734
|
entry.args = [...opts.extraArgs];
|
|
1395
1735
|
}
|
|
1396
1736
|
return { mcpServers: { [serverName]: entry } };
|
|
1397
1737
|
}
|
|
1398
|
-
|
|
1399
|
-
// src/meta.ts
|
|
1400
|
-
var SDK_NAME = "@truealter/sdk";
|
|
1401
|
-
var SDK_VERSION = "0.3.0";
|
|
1402
1738
|
var HOME = homedir();
|
|
1403
1739
|
var PLAT = platform();
|
|
1404
1740
|
function appData() {
|
|
@@ -1523,7 +1859,7 @@ function probeAll() {
|
|
|
1523
1859
|
];
|
|
1524
1860
|
}
|
|
1525
1861
|
var SYNC_PREFIXES = [
|
|
1526
|
-
// iCloud Drive
|
|
1862
|
+
// iCloud Drive: both the new and legacy mounts.
|
|
1527
1863
|
"Library/Mobile Documents/com~apple~CloudDocs",
|
|
1528
1864
|
"iCloud Drive",
|
|
1529
1865
|
// OneDrive variants Microsoft ships across editions.
|
|
@@ -1536,7 +1872,7 @@ var SYNC_PREFIXES = [
|
|
|
1536
1872
|
"Google Drive",
|
|
1537
1873
|
"GoogleDrive",
|
|
1538
1874
|
"CloudStorage/GoogleDrive",
|
|
1539
|
-
// Box, pCloud, Sync.com, MEGA
|
|
1875
|
+
// Box, pCloud, Sync.com, MEGA: high-signal names worth refusing.
|
|
1540
1876
|
"Box Sync",
|
|
1541
1877
|
"pCloud Drive",
|
|
1542
1878
|
"Sync.com",
|
|
@@ -1588,10 +1924,10 @@ function atomicJsonMerge(opts) {
|
|
|
1588
1924
|
preBytes = readFileSync(path, "utf8");
|
|
1589
1925
|
if (preBytes.trim().length > 0) {
|
|
1590
1926
|
try {
|
|
1591
|
-
parsed = JSON.parse(preBytes);
|
|
1927
|
+
parsed = JSON.parse(preBytes.replace(/^\uFEFF/, ""));
|
|
1592
1928
|
} catch (err) {
|
|
1593
1929
|
throw new Error(
|
|
1594
|
-
`refusing to wire ${path}: existing file is not valid JSON (${err.message}). Hand-fix the file, then re-run \`alter
|
|
1930
|
+
`refusing to wire ${path}: existing file is not valid JSON (${err.message}). Hand-fix the file, then re-run \`alter wire\`.`
|
|
1595
1931
|
);
|
|
1596
1932
|
}
|
|
1597
1933
|
if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
|
|
@@ -1724,7 +2060,7 @@ function wireFileTarget(args) {
|
|
|
1724
2060
|
const sync = detectSyncedVolume(client.configPath);
|
|
1725
2061
|
if (sync) {
|
|
1726
2062
|
throw new Error(
|
|
1727
|
-
`refusing to wire ${client.label}: config path ${sync.resolvedPath} lives under ${sync.matchedPrefix}. Synced volumes propagate credentials across devices
|
|
2063
|
+
`refusing to wire ${client.label}: config path ${sync.resolvedPath} lives under ${sync.matchedPrefix}. Synced volumes propagate credentials across devices: move the config off the sync root, or run wire on the device you want to target.`
|
|
1728
2064
|
);
|
|
1729
2065
|
}
|
|
1730
2066
|
const cfHeaders = {};
|
|
@@ -1813,14 +2149,13 @@ function wireClaudeCode(args) {
|
|
|
1813
2149
|
};
|
|
1814
2150
|
}
|
|
1815
2151
|
function resolveBridgeScript() {
|
|
1816
|
-
const
|
|
1817
|
-
const
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
if (existsSync5(npmGlobalBridge)) return npmGlobalBridge;
|
|
2152
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2153
|
+
const siblingBridge = join(here, "..", "dist", "mcp-bridge.js");
|
|
2154
|
+
if (existsSync(siblingBridge)) return siblingBridge;
|
|
2155
|
+
const srcBridge = join(here, "..", "mcp-bridge.js");
|
|
2156
|
+
if (existsSync(srcBridge)) return srcBridge;
|
|
2157
|
+
const npmGlobalBridge = join(here, "mcp-bridge.js");
|
|
2158
|
+
if (existsSync(npmGlobalBridge)) return npmGlobalBridge;
|
|
1824
2159
|
return null;
|
|
1825
2160
|
}
|
|
1826
2161
|
function unwire() {
|