@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.
@@ -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} \u2192 redirect rejected (discovery must not follow redirects; validate the server configuration)`
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} \u2192 HTTP ${resp.status}`);
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/x402.ts
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: "@truealter/sdk", version: "0.2.0" };
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 safe to call multiple times.
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(canonicalJson(tool.inputSchema));
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} \u2192 redirect rejected (allowlist enforces initial URL only)`
1368
+ `${url} -> redirect rejected (allowlist enforces initial URL only)`
924
1369
  );
925
1370
  }
926
- if (!resp.ok) throw new AlterNetworkError(`${url} \u2192 HTTP ${resp.status}`);
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} \u2192 JWKS too large: ${n} > ${JWKS_MAX_BYTES} bytes`
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} \u2192 JWKS too large: ${body.length} > ${JWKS_MAX_BYTES} bytes`
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 canonicalJson(value) {
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(canonicalJson).join(",")}]`;
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)}:${canonicalJson(obj[k])}`).join(",")}}`;
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
- this.mcp = new MCPClient({ ...options, endpoint, x402: options.x402 });
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 the first successful lookup is cached.
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 every method calls
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 confirms the connection, returns trust tier and tool counts. */
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. ~drew) to its canonical form and kind. No auth required. */
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 server resolves these via the member_id field
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 recipient must have granted the sender via
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 the latter is more convenient for ad-hoc verification.
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 \u2014 psychometric identity field for AI agents"
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 \u2014 psychometric identity field for AI agents"
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 both the new and legacy mounts.
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 high-signal names worth refusing.
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-identity wire\`.`
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 \u2014 move the config off the sync root, or run wire on the device you want to target.`
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 entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
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 argList = [
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 = [];