@truealter/sdk 0.5.0 → 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 +161 -80
- package/dist/bin/alter-identity.js +532 -45
- package/dist/bin/mcp-bridge.js +56 -5
- package/dist/index.cjs +667 -60
- package/dist/index.d.cts +533 -144
- package/dist/index.d.ts +533 -144
- package/dist/index.js +645 -62
- package/package.json +6 -3
|
@@ -2,14 +2,15 @@
|
|
|
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;
|
|
@@ -362,11 +363,11 @@ async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
|
|
|
362
363
|
}
|
|
363
364
|
if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
|
|
364
365
|
throw new AlterNetworkError(
|
|
365
|
-
`${url}
|
|
366
|
+
`${url} -> redirect rejected (discovery must not follow redirects; validate the server configuration)`
|
|
366
367
|
);
|
|
367
368
|
}
|
|
368
369
|
if (resp.status === 404) return null;
|
|
369
|
-
if (!resp.ok) throw new AlterNetworkError(`${url}
|
|
370
|
+
if (!resp.ok) throw new AlterNetworkError(`${url} -> HTTP ${resp.status}`);
|
|
370
371
|
const doc = await resp.json();
|
|
371
372
|
if (file === "mcp.json") {
|
|
372
373
|
const remotes = doc.remotes || [];
|
|
@@ -400,17 +401,326 @@ function ensureMcpPath(url) {
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
403
|
-
// src/
|
|
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
|
+
}
|
|
404
706
|
var X402Client = class {
|
|
405
707
|
signer;
|
|
406
708
|
maxPerQuery;
|
|
407
709
|
networks;
|
|
408
710
|
assets;
|
|
711
|
+
// undefined = allowlist check disabled (backward-compatible default).
|
|
712
|
+
// Non-null = active allowlist; reject any recipient not in the set.
|
|
713
|
+
recipientAllowlist;
|
|
409
714
|
constructor(opts = {}) {
|
|
410
715
|
this.signer = opts.signer;
|
|
411
716
|
this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
|
|
412
717
|
this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
|
|
413
718
|
this.assets = new Set(opts.assets ?? ["USDC"]);
|
|
719
|
+
if (opts.recipientAllowlist !== void 0) {
|
|
720
|
+
this.recipientAllowlist = opts.recipientAllowlist.length === 0 ? void 0 : new Set(opts.recipientAllowlist.map((a) => a.toLowerCase()));
|
|
721
|
+
} else {
|
|
722
|
+
this.recipientAllowlist = void 0;
|
|
723
|
+
}
|
|
414
724
|
}
|
|
415
725
|
/**
|
|
416
726
|
* Validate the envelope against this client's policy and, if a signer
|
|
@@ -436,6 +746,15 @@ var X402Client = class {
|
|
|
436
746
|
);
|
|
437
747
|
}
|
|
438
748
|
}
|
|
749
|
+
if (this.recipientAllowlist !== void 0) {
|
|
750
|
+
const recipientNorm = (envelope.recipient ?? "").toLowerCase();
|
|
751
|
+
if (!recipientNorm || !this.recipientAllowlist.has(recipientNorm)) {
|
|
752
|
+
throw new AlterError(
|
|
753
|
+
"PAYMENT_REQUIRED",
|
|
754
|
+
`recipient "${envelope.recipient}" is not on the known-recipient allowlist`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
439
758
|
if (!this.signer) {
|
|
440
759
|
throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
|
|
441
760
|
}
|
|
@@ -494,6 +813,9 @@ var MCPClient = class {
|
|
|
494
813
|
x402;
|
|
495
814
|
signing;
|
|
496
815
|
extraHeaders;
|
|
816
|
+
preflightHook;
|
|
817
|
+
preflightPromise = null;
|
|
818
|
+
preflightDone = false;
|
|
497
819
|
requestCounter = 0;
|
|
498
820
|
initialised = false;
|
|
499
821
|
constructor(opts = {}) {
|
|
@@ -502,17 +824,43 @@ var MCPClient = class {
|
|
|
502
824
|
this.fetchImpl = opts.fetch ?? fetch;
|
|
503
825
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
504
826
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
505
|
-
this.clientInfo = opts.clientInfo ?? { name:
|
|
827
|
+
this.clientInfo = opts.clientInfo ?? { name: SDK_NAME, version: SDK_VERSION };
|
|
506
828
|
this.x402 = opts.x402;
|
|
507
829
|
this.signing = opts.signing;
|
|
508
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;
|
|
509
856
|
}
|
|
510
857
|
/**
|
|
511
858
|
* Send the MCP `initialize` handshake and capture the resulting session
|
|
512
|
-
* id. Idempotent
|
|
859
|
+
* id. Idempotent: safe to call multiple times.
|
|
513
860
|
*/
|
|
514
861
|
async initialize() {
|
|
515
862
|
if (this.initialised) return null;
|
|
863
|
+
await this.runPreflight();
|
|
516
864
|
const result = await this.rpc("initialize", {
|
|
517
865
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
518
866
|
capabilities: {},
|
|
@@ -672,7 +1020,10 @@ var MCPClient = class {
|
|
|
672
1020
|
...this.extraHeaders ?? {},
|
|
673
1021
|
"Content-Type": "application/json",
|
|
674
1022
|
Accept: "application/json",
|
|
675
|
-
"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"
|
|
676
1027
|
};
|
|
677
1028
|
if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
|
|
678
1029
|
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
@@ -883,9 +1234,46 @@ async function verifyProvenance(envelope, opts = {}) {
|
|
|
883
1234
|
kid: header.kid
|
|
884
1235
|
};
|
|
885
1236
|
}
|
|
1237
|
+
if (opts.expectedAud !== void 0 && opts.expectedAud !== "") {
|
|
1238
|
+
const tokenAud = payload.aud;
|
|
1239
|
+
const audList = tokenAud === void 0 ? [] : Array.isArray(tokenAud) ? tokenAud : [tokenAud];
|
|
1240
|
+
if (!audList.includes(opts.expectedAud)) {
|
|
1241
|
+
return {
|
|
1242
|
+
valid: false,
|
|
1243
|
+
reason: `aud mismatch: expected "${opts.expectedAud}", got ${JSON.stringify(tokenAud ?? null)}`,
|
|
1244
|
+
payload,
|
|
1245
|
+
kid: header.kid
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
886
1249
|
return { valid: true, payload, kid: header.kid };
|
|
887
1250
|
}
|
|
888
|
-
async function verifyToolSignatures(tools, signatures) {
|
|
1251
|
+
async function verifyToolSignatures(tools, signatures, opts = {}) {
|
|
1252
|
+
const jwksUrl = opts.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
|
|
1253
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
1254
|
+
if (!jwksUrl.startsWith("https://")) {
|
|
1255
|
+
return tools.map((t) => ({
|
|
1256
|
+
tool: t.name,
|
|
1257
|
+
valid: false,
|
|
1258
|
+
reason: `jwksUrl must be https: got ${jwksUrl}`
|
|
1259
|
+
}));
|
|
1260
|
+
}
|
|
1261
|
+
const needsJwks = tools.some((t) => {
|
|
1262
|
+
const sig = signatures[t.name];
|
|
1263
|
+
return sig && sig.signature;
|
|
1264
|
+
});
|
|
1265
|
+
let jwks = null;
|
|
1266
|
+
if (needsJwks) {
|
|
1267
|
+
try {
|
|
1268
|
+
jwks = await fetchJwks(jwksUrl, fetchImpl);
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
return tools.map((t) => ({
|
|
1271
|
+
tool: t.name,
|
|
1272
|
+
valid: false,
|
|
1273
|
+
reason: `jwks fetch failed: ${err.message}`
|
|
1274
|
+
}));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
889
1277
|
const out = [];
|
|
890
1278
|
for (const tool of tools) {
|
|
891
1279
|
const sig = signatures[tool.name];
|
|
@@ -893,11 +1281,68 @@ async function verifyToolSignatures(tools, signatures) {
|
|
|
893
1281
|
out.push({ tool: tool.name, valid: false, reason: "no signature published" });
|
|
894
1282
|
continue;
|
|
895
1283
|
}
|
|
896
|
-
const expectedHash = await sha256Hex(
|
|
1284
|
+
const expectedHash = await sha256Hex(canonicalJson2(tool.inputSchema));
|
|
897
1285
|
if (expectedHash !== sig.schema_hash) {
|
|
898
1286
|
out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
|
|
899
1287
|
continue;
|
|
900
1288
|
}
|
|
1289
|
+
const jwsToken = sig.signature;
|
|
1290
|
+
if (!jwsToken) {
|
|
1291
|
+
out.push({ tool: tool.name, valid: true, warn_no_signature: true });
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
const jwksDoc = jwks;
|
|
1295
|
+
let jHeader;
|
|
1296
|
+
let jPayloadRaw;
|
|
1297
|
+
let jSigBytes;
|
|
1298
|
+
try {
|
|
1299
|
+
const parts2 = jwsToken.split(".");
|
|
1300
|
+
if (parts2.length !== 3) throw new Error("JWS must have three segments");
|
|
1301
|
+
jHeader = JSON.parse(new TextDecoder().decode(base64urlDecode(parts2[0])));
|
|
1302
|
+
jPayloadRaw = new TextDecoder().decode(base64urlDecode(parts2[1]));
|
|
1303
|
+
jSigBytes = base64urlDecode(parts2[2]);
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
out.push({ tool: tool.name, valid: false, reason: `malformed tool JWS: ${err.message}` });
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (jHeader.alg !== "ES256") {
|
|
1309
|
+
out.push({ tool: tool.name, valid: false, reason: `unsupported tool sig alg: ${jHeader.alg}` });
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (jPayloadRaw !== sig.schema_hash) {
|
|
1313
|
+
out.push({ tool: tool.name, valid: false, reason: "tool JWS payload does not match schema_hash" });
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
const jwk = jwksDoc.keys.find((k) => jHeader.kid ? k.kid === jHeader.kid : true);
|
|
1317
|
+
if (!jwk) {
|
|
1318
|
+
out.push({ tool: tool.name, valid: false, reason: `no JWK for kid=${jHeader.kid}` });
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
let publicKey;
|
|
1322
|
+
try {
|
|
1323
|
+
publicKey = await importEs256JwkAsPublicKey(jwk);
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
out.push({ tool: tool.name, valid: false, reason: `jwk import: ${err.message}` });
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const parts = jwsToken.split(".");
|
|
1329
|
+
const signedInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
|
|
1330
|
+
let sigValid = false;
|
|
1331
|
+
try {
|
|
1332
|
+
sigValid = await crypto.subtle.verify(
|
|
1333
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
1334
|
+
publicKey,
|
|
1335
|
+
toArrayBuffer(jSigBytes),
|
|
1336
|
+
toArrayBuffer(signedInput)
|
|
1337
|
+
);
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
out.push({ tool: tool.name, valid: false, reason: `sig verify error: ${err.message}` });
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (!sigValid) {
|
|
1343
|
+
out.push({ tool: tool.name, valid: false, reason: "tool signature mismatch" });
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
901
1346
|
out.push({ tool: tool.name, valid: true });
|
|
902
1347
|
}
|
|
903
1348
|
return out;
|
|
@@ -920,23 +1365,23 @@ async function fetchJwks(url, fetchImpl) {
|
|
|
920
1365
|
}
|
|
921
1366
|
if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
|
|
922
1367
|
throw new AlterProvenanceError(
|
|
923
|
-
`${url}
|
|
1368
|
+
`${url} -> redirect rejected (allowlist enforces initial URL only)`
|
|
924
1369
|
);
|
|
925
1370
|
}
|
|
926
|
-
if (!resp.ok) throw new AlterNetworkError(`${url}
|
|
1371
|
+
if (!resp.ok) throw new AlterNetworkError(`${url} -> HTTP ${resp.status}`);
|
|
927
1372
|
const contentLength = resp.headers.get("content-length");
|
|
928
1373
|
if (contentLength !== null) {
|
|
929
1374
|
const n = Number.parseInt(contentLength, 10);
|
|
930
1375
|
if (Number.isFinite(n) && n > JWKS_MAX_BYTES) {
|
|
931
1376
|
throw new AlterProvenanceError(
|
|
932
|
-
`${url}
|
|
1377
|
+
`${url} -> JWKS too large: ${n} > ${JWKS_MAX_BYTES} bytes`
|
|
933
1378
|
);
|
|
934
1379
|
}
|
|
935
1380
|
}
|
|
936
1381
|
const body = await resp.text();
|
|
937
1382
|
if (body.length > JWKS_MAX_BYTES) {
|
|
938
1383
|
throw new AlterProvenanceError(
|
|
939
|
-
`${url}
|
|
1384
|
+
`${url} -> JWKS too large: ${body.length} > ${JWKS_MAX_BYTES} bytes`
|
|
940
1385
|
);
|
|
941
1386
|
}
|
|
942
1387
|
let doc;
|
|
@@ -1020,14 +1465,14 @@ async function sha256Hex(input) {
|
|
|
1020
1465
|
function toArrayBuffer(view) {
|
|
1021
1466
|
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
1022
1467
|
}
|
|
1023
|
-
function
|
|
1468
|
+
function canonicalJson2(value) {
|
|
1024
1469
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1025
1470
|
if (Array.isArray(value)) {
|
|
1026
|
-
return `[${value.map(
|
|
1471
|
+
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
1027
1472
|
}
|
|
1028
1473
|
const obj = value;
|
|
1029
1474
|
const keys = Object.keys(obj).sort();
|
|
1030
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${
|
|
1475
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson2(obj[k])}`).join(",")}}`;
|
|
1031
1476
|
}
|
|
1032
1477
|
|
|
1033
1478
|
// src/client.ts
|
|
@@ -1043,11 +1488,21 @@ var AlterClient = class {
|
|
|
1043
1488
|
this.options = options;
|
|
1044
1489
|
this.x402 = options.x402;
|
|
1045
1490
|
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
1046
|
-
|
|
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
|
+
});
|
|
1047
1502
|
}
|
|
1048
1503
|
/**
|
|
1049
1504
|
* Resolve the MCP endpoint via discovery if requested. Safe to call
|
|
1050
|
-
* multiple times
|
|
1505
|
+
* multiple times: the first successful lookup is cached.
|
|
1051
1506
|
*/
|
|
1052
1507
|
async discoverEndpoint() {
|
|
1053
1508
|
if (this.discovered) return this.discovered;
|
|
@@ -1060,7 +1515,7 @@ var AlterClient = class {
|
|
|
1060
1515
|
return this.discoveryPromise;
|
|
1061
1516
|
}
|
|
1062
1517
|
/**
|
|
1063
|
-
* Initialise the MCP session. Optional
|
|
1518
|
+
* Initialise the MCP session. Optional: every method calls
|
|
1064
1519
|
* `mcp.initialize()` lazily, but you can call this once at startup if
|
|
1065
1520
|
* you want fail-fast behaviour.
|
|
1066
1521
|
*/
|
|
@@ -1068,11 +1523,11 @@ var AlterClient = class {
|
|
|
1068
1523
|
await this.mcp.initialize();
|
|
1069
1524
|
}
|
|
1070
1525
|
// ── Free tier ────────────────────────────────────────────────────────
|
|
1071
|
-
/** First handshake
|
|
1526
|
+
/** First handshake: confirms the connection, returns trust tier and tool counts. */
|
|
1072
1527
|
async helloAgent() {
|
|
1073
1528
|
return this.mcp.callTool("hello_agent", {});
|
|
1074
1529
|
}
|
|
1075
|
-
/** Resolve a ~handle (e.g. ~
|
|
1530
|
+
/** Resolve a ~handle (e.g. ~example) to its canonical form and kind. No auth required. */
|
|
1076
1531
|
async resolveHandle(args) {
|
|
1077
1532
|
const payload = typeof args === "string" ? { query: args } : args;
|
|
1078
1533
|
return this.mcp.callTool("alter_resolve_handle", payload);
|
|
@@ -1080,7 +1535,7 @@ var AlterClient = class {
|
|
|
1080
1535
|
/** Verify a person is registered with ALTER (handle or id). */
|
|
1081
1536
|
async verify(handleOrId, claims) {
|
|
1082
1537
|
const args = handleOrId.includes("@") ? { member_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
|
|
1083
|
-
// ~handle
|
|
1538
|
+
// ~handle: server resolves these via the member_id field
|
|
1084
1539
|
{ member_id: handleOrId }
|
|
1085
1540
|
) : { member_id: handleOrId };
|
|
1086
1541
|
if (claims) args.claims = claims;
|
|
@@ -1180,7 +1635,7 @@ var AlterClient = class {
|
|
|
1180
1635
|
}
|
|
1181
1636
|
// ── Alter-to-Alter Messaging ─────────────────────────────────────────
|
|
1182
1637
|
// Wave 1: cross-handle direct messages between authenticated tilde
|
|
1183
|
-
// handles. Default closed
|
|
1638
|
+
// handles. Default closed: recipient must have granted the sender via
|
|
1184
1639
|
// alter_message_grant. Spec: docs/technical/Alter-to-Alter Messaging.md.
|
|
1185
1640
|
/** Send a direct message to another tilde handle. */
|
|
1186
1641
|
async messageSend(args) {
|
|
@@ -1214,7 +1669,7 @@ var AlterClient = class {
|
|
|
1214
1669
|
/**
|
|
1215
1670
|
* Verify the ES256 provenance attestation on a tool response.
|
|
1216
1671
|
* Accepts either a {@link ProvenanceEnvelope} or the raw `_meta`
|
|
1217
|
-
* object
|
|
1672
|
+
* object: the latter is more convenient for ad-hoc verification.
|
|
1218
1673
|
*/
|
|
1219
1674
|
async verifyProvenance(envelope) {
|
|
1220
1675
|
if (!envelope) return { valid: false, reason: "no provenance envelope" };
|
|
@@ -1247,7 +1702,7 @@ function generateGenericMcpConfig(opts = {}) {
|
|
|
1247
1702
|
const entry = {
|
|
1248
1703
|
url: opts.endpoint ?? DEFAULT_ENDPOINT,
|
|
1249
1704
|
transport: "streamable-http",
|
|
1250
|
-
description: "ALTER Identity
|
|
1705
|
+
description: "ALTER Identity: psychometric identity field for AI agents"
|
|
1251
1706
|
};
|
|
1252
1707
|
if (Object.keys(headers).length > 0) entry.headers = headers;
|
|
1253
1708
|
return { mcpServers: { [serverName]: entry } };
|
|
@@ -1273,17 +1728,13 @@ function generateClaudeDesktopConfig(opts = {}) {
|
|
|
1273
1728
|
const entry = {
|
|
1274
1729
|
command: bridgeCommand,
|
|
1275
1730
|
env: env3,
|
|
1276
|
-
description: "ALTER Identity
|
|
1731
|
+
description: "ALTER Identity: psychometric identity field for AI agents"
|
|
1277
1732
|
};
|
|
1278
1733
|
if (opts.extraArgs && opts.extraArgs.length > 0) {
|
|
1279
1734
|
entry.args = [...opts.extraArgs];
|
|
1280
1735
|
}
|
|
1281
1736
|
return { mcpServers: { [serverName]: entry } };
|
|
1282
1737
|
}
|
|
1283
|
-
|
|
1284
|
-
// src/meta.ts
|
|
1285
|
-
var SDK_NAME = "@truealter/sdk";
|
|
1286
|
-
var SDK_VERSION = "0.3.0";
|
|
1287
1738
|
var HOME = homedir();
|
|
1288
1739
|
var PLAT = platform();
|
|
1289
1740
|
function appData() {
|
|
@@ -1408,7 +1859,7 @@ function probeAll() {
|
|
|
1408
1859
|
];
|
|
1409
1860
|
}
|
|
1410
1861
|
var SYNC_PREFIXES = [
|
|
1411
|
-
// iCloud Drive
|
|
1862
|
+
// iCloud Drive: both the new and legacy mounts.
|
|
1412
1863
|
"Library/Mobile Documents/com~apple~CloudDocs",
|
|
1413
1864
|
"iCloud Drive",
|
|
1414
1865
|
// OneDrive variants Microsoft ships across editions.
|
|
@@ -1421,7 +1872,7 @@ var SYNC_PREFIXES = [
|
|
|
1421
1872
|
"Google Drive",
|
|
1422
1873
|
"GoogleDrive",
|
|
1423
1874
|
"CloudStorage/GoogleDrive",
|
|
1424
|
-
// Box, pCloud, Sync.com, MEGA
|
|
1875
|
+
// Box, pCloud, Sync.com, MEGA: high-signal names worth refusing.
|
|
1425
1876
|
"Box Sync",
|
|
1426
1877
|
"pCloud Drive",
|
|
1427
1878
|
"Sync.com",
|
|
@@ -1473,10 +1924,10 @@ function atomicJsonMerge(opts) {
|
|
|
1473
1924
|
preBytes = readFileSync(path, "utf8");
|
|
1474
1925
|
if (preBytes.trim().length > 0) {
|
|
1475
1926
|
try {
|
|
1476
|
-
parsed = JSON.parse(preBytes);
|
|
1927
|
+
parsed = JSON.parse(preBytes.replace(/^\uFEFF/, ""));
|
|
1477
1928
|
} catch (err) {
|
|
1478
1929
|
throw new Error(
|
|
1479
|
-
`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\`.`
|
|
1480
1931
|
);
|
|
1481
1932
|
}
|
|
1482
1933
|
if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
|
|
@@ -1529,6 +1980,26 @@ function restoreFromBackup(path, backupPath) {
|
|
|
1529
1980
|
// src/wire/index.ts
|
|
1530
1981
|
var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
|
|
1531
1982
|
var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1983
|
+
function readCfAccessEnv() {
|
|
1984
|
+
const envPath = join(homedir(), ".config", "alter", "cf-access.env");
|
|
1985
|
+
try {
|
|
1986
|
+
const content = readFileSync(envPath, "utf8");
|
|
1987
|
+
let clientId = "";
|
|
1988
|
+
let clientSecret = "";
|
|
1989
|
+
for (const line of content.split("\n")) {
|
|
1990
|
+
const trimmed = line.trim();
|
|
1991
|
+
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
1992
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1993
|
+
const key = trimmed.slice(0, eqIdx).replace(/^export\s+/, "").trim();
|
|
1994
|
+
const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
1995
|
+
if (key === "CF_ACCESS_CLIENT_ID") clientId = val;
|
|
1996
|
+
if (key === "CF_ACCESS_CLIENT_SECRET") clientSecret = val;
|
|
1997
|
+
}
|
|
1998
|
+
if (clientId && clientSecret) return { clientId, clientSecret };
|
|
1999
|
+
} catch {
|
|
2000
|
+
}
|
|
2001
|
+
return void 0;
|
|
2002
|
+
}
|
|
1532
2003
|
function clientById(id) {
|
|
1533
2004
|
const hit = ALL_CLIENTS.find((c) => c.id === id);
|
|
1534
2005
|
if (!hit) throw new Error(`unknown client id: ${id}`);
|
|
@@ -1537,6 +2008,7 @@ function clientById(id) {
|
|
|
1537
2008
|
function wire(opts = {}) {
|
|
1538
2009
|
const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
1539
2010
|
const apiKey = opts.apiKey;
|
|
2011
|
+
const cfAccess = opts.cfAccess ?? readCfAccessEnv();
|
|
1540
2012
|
const probes = probeAll();
|
|
1541
2013
|
const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
|
|
1542
2014
|
const ts = TIMESTAMP();
|
|
@@ -1555,9 +2027,9 @@ function wire(opts = {}) {
|
|
|
1555
2027
|
}
|
|
1556
2028
|
try {
|
|
1557
2029
|
if (id === "claude-code") {
|
|
1558
|
-
targets.push(wireClaudeCode({ endpoint, apiKey }));
|
|
2030
|
+
targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
|
|
1559
2031
|
} else {
|
|
1560
|
-
targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
|
|
2032
|
+
targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
|
|
1561
2033
|
}
|
|
1562
2034
|
} catch (err) {
|
|
1563
2035
|
const message = err.message;
|
|
@@ -1588,10 +2060,15 @@ function wireFileTarget(args) {
|
|
|
1588
2060
|
const sync = detectSyncedVolume(client.configPath);
|
|
1589
2061
|
if (sync) {
|
|
1590
2062
|
throw new Error(
|
|
1591
|
-
`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.`
|
|
1592
2064
|
);
|
|
1593
2065
|
}
|
|
1594
|
-
const
|
|
2066
|
+
const cfHeaders = {};
|
|
2067
|
+
if (args.cfAccess) {
|
|
2068
|
+
cfHeaders["CF-Access-Client-Id"] = args.cfAccess.clientId;
|
|
2069
|
+
cfHeaders["CF-Access-Client-Secret"] = args.cfAccess.clientSecret;
|
|
2070
|
+
}
|
|
2071
|
+
const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey, headers: cfHeaders });
|
|
1595
2072
|
const rootKey = client.rootKey;
|
|
1596
2073
|
const serverName = "alter";
|
|
1597
2074
|
const result = atomicJsonMerge({
|
|
@@ -1623,7 +2100,8 @@ function wireFileTarget(args) {
|
|
|
1623
2100
|
}
|
|
1624
2101
|
function wireClaudeCode(args) {
|
|
1625
2102
|
const cmd = "claude";
|
|
1626
|
-
const
|
|
2103
|
+
const bridgePath = resolveBridgeScript();
|
|
2104
|
+
const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
|
|
1627
2105
|
"mcp",
|
|
1628
2106
|
"add",
|
|
1629
2107
|
"--scope",
|
|
@@ -1631,16 +2109,15 @@ function wireClaudeCode(args) {
|
|
|
1631
2109
|
"--transport",
|
|
1632
2110
|
"http",
|
|
1633
2111
|
"alter",
|
|
1634
|
-
args.endpoint
|
|
2112
|
+
args.endpoint,
|
|
2113
|
+
...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
|
|
1635
2114
|
];
|
|
1636
|
-
if (args.apiKey) {
|
|
1637
|
-
argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
|
|
1638
|
-
}
|
|
1639
2115
|
const full = `${cmd} ${argList.join(" ")}`;
|
|
1640
2116
|
const run = spawnSync(cmd, argList, {
|
|
1641
2117
|
encoding: "utf8",
|
|
1642
2118
|
shell: process.platform === "win32",
|
|
1643
|
-
timeout: 1e4
|
|
2119
|
+
timeout: 1e4,
|
|
2120
|
+
env: bridgePath ? { ...process.env, ALTER_PUBLIC_MCP_ENDPOINT: args.endpoint, ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {} } : void 0
|
|
1644
2121
|
});
|
|
1645
2122
|
if (run.error) {
|
|
1646
2123
|
return {
|
|
@@ -1671,6 +2148,16 @@ function wireClaudeCode(args) {
|
|
|
1671
2148
|
reason: `claude mcp add exited ${String(run.status)}`
|
|
1672
2149
|
};
|
|
1673
2150
|
}
|
|
2151
|
+
function resolveBridgeScript() {
|
|
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;
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
1674
2161
|
function unwire() {
|
|
1675
2162
|
const state = readWireState();
|
|
1676
2163
|
const undone = [];
|