@truealter/sdk 0.2.2 → 0.4.1
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 +23 -25
- package/dist/bin/alter-identity.js +881 -53
- package/dist/bin/mcp-bridge.js +179 -10
- package/dist/index.cjs +815 -81
- package/dist/index.d.cts +333 -143
- package/dist/index.d.ts +333 -143
- package/dist/index.js +747 -85
- package/package.json +11 -5
- package/dist/bin/alter-identity.js.map +0 -1
- package/dist/bin/mcp-bridge.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -1,11 +1,158 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { p256 } from '@noble/curves/p256';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
4
|
+
import { hexToBytes, bytesToHex as bytesToHex$1, randomBytes } from '@noble/hashes/utils';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, renameSync, copyFileSync } from 'fs';
|
|
6
|
+
import { homedir, platform } from 'os';
|
|
7
|
+
import { join, dirname, resolve } from 'path';
|
|
8
|
+
import { env, stderr, exit, argv, stdout, stdin } from 'process';
|
|
9
|
+
import { createInterface } from 'readline';
|
|
6
10
|
import * as ed25519 from '@noble/ed25519';
|
|
7
11
|
import { sha512 } from '@noble/hashes/sha512';
|
|
8
|
-
import {
|
|
12
|
+
import { spawnSync } from 'child_process';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
17
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
18
|
+
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
|
+
var __esm = (fn, res) => function __init() {
|
|
26
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
27
|
+
};
|
|
28
|
+
var __export = (target, all) => {
|
|
29
|
+
for (var name in all)
|
|
30
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
31
|
+
};
|
|
32
|
+
var __copyProps = (to, from, except, desc) => {
|
|
33
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
34
|
+
for (let key of __getOwnPropNames(from))
|
|
35
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
36
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
37
|
+
}
|
|
38
|
+
return to;
|
|
39
|
+
};
|
|
40
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
41
|
+
|
|
42
|
+
// src/signing.ts
|
|
43
|
+
var signing_exports = {};
|
|
44
|
+
__export(signing_exports, {
|
|
45
|
+
canonicalArgsSha256: () => canonicalArgsSha256,
|
|
46
|
+
canonicalStringify: () => canonicalStringify,
|
|
47
|
+
loadPrivateKey: () => loadPrivateKey,
|
|
48
|
+
signInvocation: () => signInvocation
|
|
49
|
+
});
|
|
50
|
+
function canonicalStringify(value) {
|
|
51
|
+
return stringifyInner(value);
|
|
52
|
+
}
|
|
53
|
+
function stringifyInner(value) {
|
|
54
|
+
if (value === null) return "null";
|
|
55
|
+
if (value === void 0) {
|
|
56
|
+
throw new TypeError("canonicalStringify: undefined is not representable in JSON");
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
59
|
+
if (typeof value === "number") {
|
|
60
|
+
if (!Number.isFinite(value)) {
|
|
61
|
+
throw new TypeError("canonicalStringify: non-finite numbers are not representable");
|
|
62
|
+
}
|
|
63
|
+
return JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "string") return encodeString(value);
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return "[" + value.map((v) => stringifyInner(v)).join(",") + "]";
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "object") {
|
|
70
|
+
const obj = value;
|
|
71
|
+
const keys = Object.keys(obj).sort();
|
|
72
|
+
return "{" + keys.map((k) => encodeString(k) + ":" + stringifyInner(obj[k])).join(",") + "}";
|
|
73
|
+
}
|
|
74
|
+
throw new TypeError(`canonicalStringify: unsupported type ${typeof value}`);
|
|
75
|
+
}
|
|
76
|
+
function encodeString(s) {
|
|
77
|
+
return JSON.stringify(s);
|
|
78
|
+
}
|
|
79
|
+
function canonicalArgsSha256(toolArgs) {
|
|
80
|
+
const canonical = canonicalStringify(toolArgs ?? {});
|
|
81
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
82
|
+
const digest = sha256(bytes);
|
|
83
|
+
return bytesToHex(digest);
|
|
84
|
+
}
|
|
85
|
+
function bytesToHex(bytes) {
|
|
86
|
+
let out = "";
|
|
87
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
88
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function base64urlEncode(bytes) {
|
|
93
|
+
const raw = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
|
|
94
|
+
if (typeof Buffer !== "undefined") {
|
|
95
|
+
return Buffer.from(raw).toString("base64url");
|
|
96
|
+
}
|
|
97
|
+
let binary = "";
|
|
98
|
+
for (let i = 0; i < raw.length; i++) binary += String.fromCharCode(raw[i]);
|
|
99
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
100
|
+
}
|
|
101
|
+
function loadPrivateKey(key) {
|
|
102
|
+
if (key instanceof Uint8Array) {
|
|
103
|
+
if (key.length !== 32) {
|
|
104
|
+
throw new TypeError("ES256 raw private key must be 32 bytes.");
|
|
105
|
+
}
|
|
106
|
+
return key;
|
|
107
|
+
}
|
|
108
|
+
if (typeof key === "string" && key.includes("-----BEGIN")) {
|
|
109
|
+
const nodeCrypto = __require("crypto");
|
|
110
|
+
const keyObj = nodeCrypto.createPrivateKey({ key, format: "pem" });
|
|
111
|
+
const jwk = keyObj.export({ format: "jwk" });
|
|
112
|
+
if (jwk.crv !== "P-256" || !jwk.d) {
|
|
113
|
+
throw new TypeError("PEM is not a P-256 private key.");
|
|
114
|
+
}
|
|
115
|
+
return base64urlDecodeToBytes(jwk.d);
|
|
116
|
+
}
|
|
117
|
+
throw new TypeError("loadPrivateKey: expected Uint8Array(32) or PEM string.");
|
|
118
|
+
}
|
|
119
|
+
function base64urlDecodeToBytes(s) {
|
|
120
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
121
|
+
const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
122
|
+
if (typeof Buffer !== "undefined") {
|
|
123
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
124
|
+
}
|
|
125
|
+
const binary = atob(b64);
|
|
126
|
+
const out = new Uint8Array(binary.length);
|
|
127
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function signInvocation(toolName, toolArgs, options) {
|
|
131
|
+
const { kid, privateKey, handle } = options;
|
|
132
|
+
const nonce = options.nonce ?? base64urlEncode(randomBytes(24));
|
|
133
|
+
const iat = options.iatSeconds ?? Math.floor(Date.now() / 1e3);
|
|
134
|
+
const claims = {
|
|
135
|
+
tool: toolName,
|
|
136
|
+
args_sha256: canonicalArgsSha256(toolArgs ?? {}),
|
|
137
|
+
nonce,
|
|
138
|
+
iat,
|
|
139
|
+
iss: handle
|
|
140
|
+
};
|
|
141
|
+
const headerB64 = base64urlEncode(JSON.stringify({ alg: "ES256", kid }));
|
|
142
|
+
const payloadB64 = base64urlEncode(JSON.stringify(claims));
|
|
143
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
144
|
+
const signingBytes = new TextEncoder().encode(signingInput);
|
|
145
|
+
const dBytes = loadPrivateKey(privateKey);
|
|
146
|
+
const digest = sha256(signingBytes);
|
|
147
|
+
const sig = p256.sign(digest, dBytes, { prehash: false });
|
|
148
|
+
const sigBytes = sig.toCompactRawBytes();
|
|
149
|
+
const sigB64 = base64urlEncode(sigBytes);
|
|
150
|
+
return `${signingInput}.${sigB64}`;
|
|
151
|
+
}
|
|
152
|
+
var init_signing = __esm({
|
|
153
|
+
"src/signing.ts"() {
|
|
154
|
+
}
|
|
155
|
+
});
|
|
9
156
|
|
|
10
157
|
// src/errors.ts
|
|
11
158
|
var AlterError = class extends Error {
|
|
@@ -106,7 +253,12 @@ async function discover(domain, opts = {}) {
|
|
|
106
253
|
try {
|
|
107
254
|
const dnsHit = await tryDns(host);
|
|
108
255
|
if (dnsHit) {
|
|
109
|
-
const
|
|
256
|
+
const parsed = validateDiscoveredUrl(dnsHit, "dns");
|
|
257
|
+
const result = {
|
|
258
|
+
url: parsed.toString().replace(/\/$/, ""),
|
|
259
|
+
transport: "streamable-http",
|
|
260
|
+
source: "dns"
|
|
261
|
+
};
|
|
110
262
|
if (cache) _cache.set(host, result);
|
|
111
263
|
return result;
|
|
112
264
|
}
|
|
@@ -144,6 +296,28 @@ function normaliseDomain(input) {
|
|
|
144
296
|
if (!host) throw new AlterDiscoveryError(`Empty domain: "${input}"`);
|
|
145
297
|
return host;
|
|
146
298
|
}
|
|
299
|
+
function validateDiscoveredUrl(url, source) {
|
|
300
|
+
let parsed;
|
|
301
|
+
try {
|
|
302
|
+
parsed = new URL(url);
|
|
303
|
+
} catch {
|
|
304
|
+
throw new AlterDiscoveryError(`${source}: malformed URL ${url}`);
|
|
305
|
+
}
|
|
306
|
+
if (parsed.protocol !== "https:") {
|
|
307
|
+
throw new AlterDiscoveryError(
|
|
308
|
+
`${source}: non-https MCP endpoint rejected (got ${parsed.protocol}//${parsed.hostname})`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (parsed.username || parsed.password) {
|
|
312
|
+
throw new AlterDiscoveryError(
|
|
313
|
+
`${source}: MCP endpoint must not contain userinfo (user:pass@host)`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (!parsed.hostname) {
|
|
317
|
+
throw new AlterDiscoveryError(`${source}: MCP endpoint missing hostname`);
|
|
318
|
+
}
|
|
319
|
+
return parsed;
|
|
320
|
+
}
|
|
147
321
|
async function tryDns(host) {
|
|
148
322
|
let resolveTxt;
|
|
149
323
|
try {
|
|
@@ -204,14 +378,17 @@ async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
|
|
|
204
378
|
if (file === "mcp.json") {
|
|
205
379
|
const remotes = doc.remotes || [];
|
|
206
380
|
const remote = remotes.find((r) => r.transportType === "streamable-http" || r.transportType === "http");
|
|
207
|
-
const
|
|
208
|
-
if (!
|
|
209
|
-
|
|
381
|
+
const rawUrl = remote?.url || doc.url;
|
|
382
|
+
if (!rawUrl) return null;
|
|
383
|
+
const parsed = validateDiscoveredUrl(rawUrl, "mcp.json");
|
|
384
|
+
return { url: parsed.toString().replace(/\/$/, ""), transport: "streamable-http", source: "mcp.json", raw: doc };
|
|
210
385
|
}
|
|
211
386
|
const mcpHost = doc.mcp;
|
|
212
387
|
if (!mcpHost) return null;
|
|
388
|
+
const normalised = ensureMcpPath(mcpHost);
|
|
389
|
+
validateDiscoveredUrl(normalised, "alter.json");
|
|
213
390
|
return {
|
|
214
|
-
url:
|
|
391
|
+
url: normalised,
|
|
215
392
|
transport: "streamable-http",
|
|
216
393
|
source: "alter.json",
|
|
217
394
|
publicKey: doc.pk,
|
|
@@ -257,11 +434,14 @@ var X402Client = class {
|
|
|
257
434
|
if (!this.assets.has(envelope.asset)) {
|
|
258
435
|
throw new AlterError("PAYMENT_REQUIRED", `asset ${envelope.asset} not permitted by client policy`);
|
|
259
436
|
}
|
|
260
|
-
if (this.maxPerQuery !== void 0
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
437
|
+
if (this.maxPerQuery !== void 0) {
|
|
438
|
+
const amt = Number(envelope.amount);
|
|
439
|
+
if (!Number.isFinite(amt) || amt < 0 || amt > this.maxPerQuery) {
|
|
440
|
+
throw new AlterError(
|
|
441
|
+
"PAYMENT_REQUIRED",
|
|
442
|
+
`quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
265
445
|
}
|
|
266
446
|
if (!this.signer) {
|
|
267
447
|
throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
|
|
@@ -319,6 +499,7 @@ var MCPClient = class {
|
|
|
319
499
|
maxRetries;
|
|
320
500
|
clientInfo;
|
|
321
501
|
x402;
|
|
502
|
+
signing;
|
|
322
503
|
requestCounter = 0;
|
|
323
504
|
initialised = false;
|
|
324
505
|
constructor(opts = {}) {
|
|
@@ -329,6 +510,7 @@ var MCPClient = class {
|
|
|
329
510
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
330
511
|
this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
|
|
331
512
|
this.x402 = opts.x402;
|
|
513
|
+
this.signing = opts.signing;
|
|
332
514
|
}
|
|
333
515
|
/**
|
|
334
516
|
* Send the MCP `initialize` handshake and capture the resulting session
|
|
@@ -415,6 +597,7 @@ var MCPClient = class {
|
|
|
415
597
|
method
|
|
416
598
|
};
|
|
417
599
|
if (params !== void 0) payload.params = params;
|
|
600
|
+
const signatureHeader = this.buildSignatureHeader(method, params);
|
|
418
601
|
let attempt = 0;
|
|
419
602
|
let lastErr = null;
|
|
420
603
|
while (attempt <= this.maxRetries) {
|
|
@@ -425,7 +608,7 @@ var MCPClient = class {
|
|
|
425
608
|
try {
|
|
426
609
|
resp = await this.fetchImpl(this.endpoint, {
|
|
427
610
|
method: "POST",
|
|
428
|
-
headers: this.buildHeaders(),
|
|
611
|
+
headers: this.buildHeaders(signatureHeader),
|
|
429
612
|
body: JSON.stringify(payload),
|
|
430
613
|
signal: controller.signal
|
|
431
614
|
});
|
|
@@ -452,7 +635,8 @@ var MCPClient = class {
|
|
|
452
635
|
throw new AlterPaymentRequired(this.guessToolName(payload), envelope);
|
|
453
636
|
}
|
|
454
637
|
if (resp.status === 429) {
|
|
455
|
-
const
|
|
638
|
+
const rawRetryAfter = Number(resp.headers.get("Retry-After") ?? 60);
|
|
639
|
+
const retryAfter = Number.isFinite(rawRetryAfter) && rawRetryAfter >= 0 ? Math.min(rawRetryAfter, 300) : 60;
|
|
456
640
|
if (attempt > this.maxRetries) {
|
|
457
641
|
throw new AlterRateLimited(`HTTP 429 on ${method}`, retryAfter);
|
|
458
642
|
}
|
|
@@ -488,7 +672,7 @@ var MCPClient = class {
|
|
|
488
672
|
}
|
|
489
673
|
throw lastErr ?? new AlterNetworkError(`MCP ${method}: exhausted retries`);
|
|
490
674
|
}
|
|
491
|
-
buildHeaders() {
|
|
675
|
+
buildHeaders(extra) {
|
|
492
676
|
const headers = {
|
|
493
677
|
"Content-Type": "application/json",
|
|
494
678
|
Accept: "application/json",
|
|
@@ -496,8 +680,27 @@ var MCPClient = class {
|
|
|
496
680
|
};
|
|
497
681
|
if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
|
|
498
682
|
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
683
|
+
if (extra) Object.assign(headers, extra);
|
|
499
684
|
return headers;
|
|
500
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Produce the `Mcp-Invocation-Signature` header for a `tools/call`
|
|
688
|
+
* payload, when signing is configured. Returns `undefined` when no
|
|
689
|
+
* signing key is attached or the method is not `tools/call`.
|
|
690
|
+
*/
|
|
691
|
+
buildSignatureHeader(method, params) {
|
|
692
|
+
if (!this.signing) return void 0;
|
|
693
|
+
if (method !== "tools/call") return void 0;
|
|
694
|
+
const p = params;
|
|
695
|
+
if (!p?.name) return void 0;
|
|
696
|
+
const { signInvocation: signInvocation2 } = (init_signing(), __toCommonJS(signing_exports));
|
|
697
|
+
const headerValue = signInvocation2(p.name, p.arguments ?? {}, {
|
|
698
|
+
kid: this.signing.kid,
|
|
699
|
+
privateKey: this.signing.privateKey,
|
|
700
|
+
handle: this.signing.handle
|
|
701
|
+
});
|
|
702
|
+
return { "Mcp-Invocation-Signature": headerValue };
|
|
703
|
+
}
|
|
501
704
|
async extractPaymentEnvelope(resp) {
|
|
502
705
|
const headerValue = resp.headers.get("X-402-Payment") ?? resp.headers.get("x-402-payment");
|
|
503
706
|
if (headerValue) {
|
|
@@ -540,8 +743,8 @@ function generateKeypair() {
|
|
|
540
743
|
const privateKey = randomBytes(32);
|
|
541
744
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
542
745
|
return {
|
|
543
|
-
privateKey: bytesToHex(privateKey),
|
|
544
|
-
publicKey: bytesToHex(publicKey),
|
|
746
|
+
privateKey: bytesToHex$1(privateKey),
|
|
747
|
+
publicKey: bytesToHex$1(publicKey),
|
|
545
748
|
did: encodeDid(publicKey)
|
|
546
749
|
};
|
|
547
750
|
}
|
|
@@ -553,15 +756,15 @@ function keypairFromPrivateKey(privateKeyHex) {
|
|
|
553
756
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
554
757
|
return {
|
|
555
758
|
privateKey: privateKeyHex,
|
|
556
|
-
publicKey: bytesToHex(publicKey),
|
|
759
|
+
publicKey: bytesToHex$1(publicKey),
|
|
557
760
|
did: encodeDid(publicKey)
|
|
558
761
|
};
|
|
559
762
|
}
|
|
560
763
|
function encodeDid(publicKey) {
|
|
561
764
|
const bytes = typeof publicKey === "string" ? hexToBytes(publicKey) : publicKey;
|
|
562
|
-
return `ed25519:${
|
|
765
|
+
return `ed25519:${base64urlEncode2(bytes)}`;
|
|
563
766
|
}
|
|
564
|
-
function
|
|
767
|
+
function base64urlEncode2(bytes) {
|
|
565
768
|
let b64;
|
|
566
769
|
if (typeof Buffer !== "undefined") {
|
|
567
770
|
b64 = Buffer.from(bytes).toString("base64");
|
|
@@ -586,6 +789,8 @@ function base64urlDecode(input) {
|
|
|
586
789
|
// src/provenance.ts
|
|
587
790
|
var _jwksCache = /* @__PURE__ */ new Map();
|
|
588
791
|
var JWKS_TTL_MS = 5 * 60 * 1e3;
|
|
792
|
+
var JWKS_MAX_BYTES = 64 * 1024;
|
|
793
|
+
var JWKS_CACHE_MAX_ENTRIES = 32;
|
|
589
794
|
var DEFAULT_VERIFY_AT_ALLOWLIST = Object.freeze([
|
|
590
795
|
"api.truealter.com",
|
|
591
796
|
"mcp.truealter.com"
|
|
@@ -695,7 +900,8 @@ async function fetchPublicKeys(jwksUrl, fetchImpl = fetch) {
|
|
|
695
900
|
return fetchJwks(jwksUrl, fetchImpl);
|
|
696
901
|
}
|
|
697
902
|
async function fetchJwks(url, fetchImpl) {
|
|
698
|
-
const
|
|
903
|
+
const cacheKey = jwksCacheKey(url);
|
|
904
|
+
const cached = _jwksCache.get(cacheKey);
|
|
699
905
|
if (cached && Date.now() - cached.fetched < JWKS_TTL_MS) return cached.jwks;
|
|
700
906
|
let resp;
|
|
701
907
|
try {
|
|
@@ -712,13 +918,45 @@ async function fetchJwks(url, fetchImpl) {
|
|
|
712
918
|
);
|
|
713
919
|
}
|
|
714
920
|
if (!resp.ok) throw new AlterNetworkError(`${url} \u2192 HTTP ${resp.status}`);
|
|
715
|
-
const
|
|
921
|
+
const contentLength = resp.headers.get("content-length");
|
|
922
|
+
if (contentLength !== null) {
|
|
923
|
+
const n = Number.parseInt(contentLength, 10);
|
|
924
|
+
if (Number.isFinite(n) && n > JWKS_MAX_BYTES) {
|
|
925
|
+
throw new AlterProvenanceError(
|
|
926
|
+
`${url} \u2192 JWKS too large: ${n} > ${JWKS_MAX_BYTES} bytes`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const body = await resp.text();
|
|
931
|
+
if (body.length > JWKS_MAX_BYTES) {
|
|
932
|
+
throw new AlterProvenanceError(
|
|
933
|
+
`${url} \u2192 JWKS too large: ${body.length} > ${JWKS_MAX_BYTES} bytes`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
let doc;
|
|
937
|
+
try {
|
|
938
|
+
doc = JSON.parse(body);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
throw new AlterProvenanceError(`invalid JWKS at ${url}: ${err.message}`);
|
|
941
|
+
}
|
|
716
942
|
if (!doc || !Array.isArray(doc.keys)) {
|
|
717
943
|
throw new AlterProvenanceError(`invalid JWKS at ${url}`);
|
|
718
944
|
}
|
|
719
|
-
_jwksCache.
|
|
945
|
+
if (_jwksCache.size >= JWKS_CACHE_MAX_ENTRIES && !_jwksCache.has(cacheKey)) {
|
|
946
|
+
const oldest = _jwksCache.keys().next().value;
|
|
947
|
+
if (oldest !== void 0) _jwksCache.delete(oldest);
|
|
948
|
+
}
|
|
949
|
+
_jwksCache.set(cacheKey, { fetched: Date.now(), jwks: doc });
|
|
720
950
|
return doc;
|
|
721
951
|
}
|
|
952
|
+
function jwksCacheKey(url) {
|
|
953
|
+
try {
|
|
954
|
+
const parsed = new URL(url);
|
|
955
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
956
|
+
} catch {
|
|
957
|
+
return url;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
722
960
|
function resolveVerifyAt(verifyAt, allowlist = DEFAULT_VERIFY_AT_ALLOWLIST) {
|
|
723
961
|
if (typeof verifyAt !== "string" || verifyAt.length === 0) {
|
|
724
962
|
throw new Error("verify_at must be a non-empty string");
|
|
@@ -741,6 +979,9 @@ function resolveVerifyAt(verifyAt, allowlist = DEFAULT_VERIFY_AT_ALLOWLIST) {
|
|
|
741
979
|
if (parsed.protocol !== "https:") {
|
|
742
980
|
throw new Error(`verify_at must be https: ${verifyAt}`);
|
|
743
981
|
}
|
|
982
|
+
if (parsed.username || parsed.password) {
|
|
983
|
+
throw new Error(`verify_at must not contain userinfo: ${verifyAt}`);
|
|
984
|
+
}
|
|
744
985
|
const host = parsed.hostname.toLowerCase();
|
|
745
986
|
const allowed = allowlist.some((h) => h.toLowerCase() === host);
|
|
746
987
|
if (!allowed) {
|
|
@@ -821,6 +1062,15 @@ var AlterClient = class {
|
|
|
821
1062
|
await this.mcp.initialize();
|
|
822
1063
|
}
|
|
823
1064
|
// ── Free tier ────────────────────────────────────────────────────────
|
|
1065
|
+
/** First handshake — confirms the connection, returns trust tier and tool counts. */
|
|
1066
|
+
async helloAgent() {
|
|
1067
|
+
return this.mcp.callTool("hello_agent", {});
|
|
1068
|
+
}
|
|
1069
|
+
/** Resolve a ~handle (e.g. ~drew) to its canonical form and kind. No auth required. */
|
|
1070
|
+
async resolveHandle(args) {
|
|
1071
|
+
const payload = typeof args === "string" ? { query: args } : args;
|
|
1072
|
+
return this.mcp.callTool("alter_resolve_handle", payload);
|
|
1073
|
+
}
|
|
824
1074
|
/** Verify a person is registered with ALTER (handle or id). */
|
|
825
1075
|
async verify(handleOrId, claims) {
|
|
826
1076
|
const args = handleOrId.includes("@") ? { candidate_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
|
|
@@ -857,12 +1107,6 @@ var AlterClient = class {
|
|
|
857
1107
|
async getCompetencies(args) {
|
|
858
1108
|
return this.mcp.callTool("get_competencies", args);
|
|
859
1109
|
}
|
|
860
|
-
async createIdentityStub(args) {
|
|
861
|
-
return this.mcp.callTool("create_identity_stub", args);
|
|
862
|
-
}
|
|
863
|
-
async submitContext(args) {
|
|
864
|
-
return this.mcp.callTool("submit_context", args);
|
|
865
|
-
}
|
|
866
1110
|
async searchIdentities(args) {
|
|
867
1111
|
return this.mcp.callTool("search_identities", args);
|
|
868
1112
|
}
|
|
@@ -887,9 +1131,6 @@ var AlterClient = class {
|
|
|
887
1131
|
async getPrivacyBudget(args) {
|
|
888
1132
|
return this.mcp.callTool("get_privacy_budget", args);
|
|
889
1133
|
}
|
|
890
|
-
async disputeAttestation(args) {
|
|
891
|
-
return this.mcp.callTool("dispute_attestation", args);
|
|
892
|
-
}
|
|
893
1134
|
// ── Golden Thread ────────────────────────────────────────────────────
|
|
894
1135
|
async goldenThreadStatus() {
|
|
895
1136
|
return this.mcp.callTool("golden_thread_status", {});
|
|
@@ -925,18 +1166,6 @@ var AlterClient = class {
|
|
|
925
1166
|
async generateMatchNarrative(args, opts) {
|
|
926
1167
|
return this.mcp.callTool("generate_match_narrative", args, opts);
|
|
927
1168
|
}
|
|
928
|
-
async submitBatchContext(args, opts) {
|
|
929
|
-
return this.mcp.callTool("submit_batch_context", args, opts);
|
|
930
|
-
}
|
|
931
|
-
async submitStructuredProfile(args, opts) {
|
|
932
|
-
return this.mcp.callTool("submit_structured_profile", args, opts);
|
|
933
|
-
}
|
|
934
|
-
async submitSocialLinks(args, opts) {
|
|
935
|
-
return this.mcp.callTool("submit_social_links", args, opts);
|
|
936
|
-
}
|
|
937
|
-
async attestDomain(args, opts) {
|
|
938
|
-
return this.mcp.callTool("attest_domain", args, opts);
|
|
939
|
-
}
|
|
940
1169
|
async getSideQuestGraph(args, opts) {
|
|
941
1170
|
return this.mcp.callTool("get_side_quest_graph", args, opts);
|
|
942
1171
|
}
|
|
@@ -1028,9 +1257,460 @@ function generateCursorConfig(opts = {}) {
|
|
|
1028
1257
|
return generateGenericMcpConfig({ serverName: "alter", ...opts });
|
|
1029
1258
|
}
|
|
1030
1259
|
|
|
1031
|
-
// src/
|
|
1260
|
+
// src/adapters/claude-desktop.ts
|
|
1261
|
+
function generateClaudeDesktopConfig(opts = {}) {
|
|
1262
|
+
const serverName = opts.serverName ?? "alter";
|
|
1263
|
+
const bridgeCommand = opts.bridgeCommand ?? "alter-mcp-bridge";
|
|
1264
|
+
const env3 = {};
|
|
1265
|
+
env3.ALTER_MCP_ENDPOINT = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
1266
|
+
if (opts.apiKey) env3.ALTER_API_KEY = opts.apiKey;
|
|
1267
|
+
const entry = {
|
|
1268
|
+
command: bridgeCommand,
|
|
1269
|
+
env: env3,
|
|
1270
|
+
description: "ALTER Identity \u2014 psychometric identity field for AI agents"
|
|
1271
|
+
};
|
|
1272
|
+
if (opts.extraArgs && opts.extraArgs.length > 0) {
|
|
1273
|
+
entry.args = [...opts.extraArgs];
|
|
1274
|
+
}
|
|
1275
|
+
return { mcpServers: { [serverName]: entry } };
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/meta.ts
|
|
1032
1279
|
var SDK_NAME = "@truealter/sdk";
|
|
1033
|
-
var SDK_VERSION = "0.
|
|
1280
|
+
var SDK_VERSION = "0.3.0";
|
|
1281
|
+
var HOME = homedir();
|
|
1282
|
+
var PLAT = platform();
|
|
1283
|
+
function appData() {
|
|
1284
|
+
return env.APPDATA ?? join(HOME, "AppData", "Roaming");
|
|
1285
|
+
}
|
|
1286
|
+
function xdgConfig() {
|
|
1287
|
+
return env.XDG_CONFIG_HOME ?? join(HOME, ".config");
|
|
1288
|
+
}
|
|
1289
|
+
function macAppSupport() {
|
|
1290
|
+
return join(HOME, "Library", "Application Support");
|
|
1291
|
+
}
|
|
1292
|
+
function claudeDesktopConfigPath() {
|
|
1293
|
+
if (PLAT === "darwin") return join(macAppSupport(), "Claude", "claude_desktop_config.json");
|
|
1294
|
+
if (PLAT === "win32") return join(appData(), "Claude", "claude_desktop_config.json");
|
|
1295
|
+
return join(xdgConfig(), "Claude", "claude_desktop_config.json");
|
|
1296
|
+
}
|
|
1297
|
+
function claudeDesktopDir() {
|
|
1298
|
+
if (PLAT === "darwin") return join(macAppSupport(), "Claude");
|
|
1299
|
+
if (PLAT === "win32") return join(appData(), "Claude");
|
|
1300
|
+
return join(xdgConfig(), "Claude");
|
|
1301
|
+
}
|
|
1302
|
+
function vscodeConfigPath() {
|
|
1303
|
+
if (PLAT === "darwin") return join(macAppSupport(), "Code", "User", "mcp.json");
|
|
1304
|
+
if (PLAT === "win32") return join(appData(), "Code", "User", "mcp.json");
|
|
1305
|
+
return join(xdgConfig(), "Code", "User", "mcp.json");
|
|
1306
|
+
}
|
|
1307
|
+
function vscodeDir() {
|
|
1308
|
+
if (PLAT === "darwin") return join(macAppSupport(), "Code", "User");
|
|
1309
|
+
if (PLAT === "win32") return join(appData(), "Code", "User");
|
|
1310
|
+
return join(xdgConfig(), "Code", "User");
|
|
1311
|
+
}
|
|
1312
|
+
var cursorDir = join(HOME, ".cursor");
|
|
1313
|
+
var cursorConfigPath = join(cursorDir, "mcp.json");
|
|
1314
|
+
var claudeCodeProbeDir = join(HOME, ".claude");
|
|
1315
|
+
var CLAUDE_CODE = {
|
|
1316
|
+
id: "claude-code",
|
|
1317
|
+
label: "Claude Code",
|
|
1318
|
+
configPath: null,
|
|
1319
|
+
probeDir: claudeCodeProbeDir,
|
|
1320
|
+
rootKey: "mcpServers"
|
|
1321
|
+
};
|
|
1322
|
+
var CURSOR = {
|
|
1323
|
+
id: "cursor",
|
|
1324
|
+
label: "Cursor",
|
|
1325
|
+
configPath: cursorConfigPath,
|
|
1326
|
+
probeDir: cursorDir,
|
|
1327
|
+
rootKey: "mcpServers"
|
|
1328
|
+
};
|
|
1329
|
+
var CLAUDE_DESKTOP = {
|
|
1330
|
+
id: "claude-desktop",
|
|
1331
|
+
label: "Claude Desktop",
|
|
1332
|
+
configPath: claudeDesktopConfigPath(),
|
|
1333
|
+
probeDir: claudeDesktopDir(),
|
|
1334
|
+
rootKey: "mcpServers"
|
|
1335
|
+
};
|
|
1336
|
+
var VSCODE = {
|
|
1337
|
+
id: "vscode",
|
|
1338
|
+
label: "VS Code",
|
|
1339
|
+
configPath: vscodeConfigPath(),
|
|
1340
|
+
probeDir: vscodeDir(),
|
|
1341
|
+
// VS Code's user-scoped mcp.json uses `servers`, not `mcpServers`.
|
|
1342
|
+
rootKey: "servers"
|
|
1343
|
+
};
|
|
1344
|
+
var ALL_CLIENTS = [CLAUDE_CODE, CURSOR, CLAUDE_DESKTOP, VSCODE];
|
|
1345
|
+
function alterConfigDir() {
|
|
1346
|
+
return join(xdgConfig(), "alter");
|
|
1347
|
+
}
|
|
1348
|
+
function wireStatePath() {
|
|
1349
|
+
return join(alterConfigDir(), "wire-state.json");
|
|
1350
|
+
}
|
|
1351
|
+
function probeClaudeCode() {
|
|
1352
|
+
try {
|
|
1353
|
+
const result = spawnSync("claude", ["--version"], {
|
|
1354
|
+
encoding: "utf8",
|
|
1355
|
+
shell: process.platform === "win32",
|
|
1356
|
+
timeout: 5e3
|
|
1357
|
+
});
|
|
1358
|
+
if (result.error) {
|
|
1359
|
+
return {
|
|
1360
|
+
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1361
|
+
installed: false,
|
|
1362
|
+
reason: `claude binary not on PATH (${result.error.message})`
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (result.status === 0) {
|
|
1366
|
+
return {
|
|
1367
|
+
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1368
|
+
installed: true,
|
|
1369
|
+
version: result.stdout.trim() || void 0,
|
|
1370
|
+
reason: "claude --version returned 0"
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1375
|
+
installed: false,
|
|
1376
|
+
reason: `claude --version exited ${String(result.status)}`
|
|
1377
|
+
};
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
return {
|
|
1380
|
+
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1381
|
+
installed: false,
|
|
1382
|
+
reason: err.message
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function probeByDir(id) {
|
|
1387
|
+
const client = ALL_CLIENTS.find((c) => c.id === id);
|
|
1388
|
+
if (!client) throw new Error(`unknown client id: ${id}`);
|
|
1389
|
+
const installed = existsSync(client.probeDir);
|
|
1390
|
+
return {
|
|
1391
|
+
client,
|
|
1392
|
+
installed,
|
|
1393
|
+
reason: installed ? `found ${client.probeDir}` : `no directory at ${client.probeDir}`
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function probeAll() {
|
|
1397
|
+
return [
|
|
1398
|
+
probeClaudeCode(),
|
|
1399
|
+
probeByDir("cursor"),
|
|
1400
|
+
probeByDir("claude-desktop"),
|
|
1401
|
+
probeByDir("vscode")
|
|
1402
|
+
];
|
|
1403
|
+
}
|
|
1404
|
+
var SYNC_PREFIXES = [
|
|
1405
|
+
// iCloud Drive — both the new and legacy mounts.
|
|
1406
|
+
"Library/Mobile Documents/com~apple~CloudDocs",
|
|
1407
|
+
"iCloud Drive",
|
|
1408
|
+
// OneDrive variants Microsoft ships across editions.
|
|
1409
|
+
"OneDrive",
|
|
1410
|
+
"OneDrive - ",
|
|
1411
|
+
// Dropbox standard + enterprise mounts.
|
|
1412
|
+
"Dropbox",
|
|
1413
|
+
"Dropbox (",
|
|
1414
|
+
// Google Drive (ALTER does not integrate with Google; still refuse).
|
|
1415
|
+
"Google Drive",
|
|
1416
|
+
"GoogleDrive",
|
|
1417
|
+
"CloudStorage/GoogleDrive",
|
|
1418
|
+
// Box, pCloud, Sync.com, MEGA — high-signal names worth refusing.
|
|
1419
|
+
"Box Sync",
|
|
1420
|
+
"pCloud Drive",
|
|
1421
|
+
"Sync.com",
|
|
1422
|
+
"MEGAsync"
|
|
1423
|
+
];
|
|
1424
|
+
function detectSyncedVolume(path) {
|
|
1425
|
+
const absolute = resolve(path);
|
|
1426
|
+
const normalised = platform() === "win32" ? absolute.replace(/\\/g, "/") : absolute;
|
|
1427
|
+
for (const prefix of SYNC_PREFIXES) {
|
|
1428
|
+
if (normalised.includes(`/${prefix}/`) || normalised.includes(`/${prefix}`)) {
|
|
1429
|
+
return { refused: true, matchedPrefix: prefix, resolvedPath: absolute };
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
var WIRE_STATE_VERSION = 1;
|
|
1435
|
+
function readWireState() {
|
|
1436
|
+
const path = wireStatePath();
|
|
1437
|
+
if (!existsSync(path)) return null;
|
|
1438
|
+
try {
|
|
1439
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1440
|
+
if (parsed.version !== WIRE_STATE_VERSION) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`wire-state.json version ${String(parsed.version)} is not supported by this SDK (expected ${WIRE_STATE_VERSION})`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
return parsed;
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
throw new Error(`failed to parse wire-state.json: ${err.message}`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function writeWireState(state) {
|
|
1451
|
+
const path = wireStatePath();
|
|
1452
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
1453
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
1454
|
+
}
|
|
1455
|
+
function sha2562(bytes) {
|
|
1456
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1457
|
+
}
|
|
1458
|
+
function atomicJsonMerge(opts) {
|
|
1459
|
+
const { path, timestamp, merge, idempotent = true } = opts;
|
|
1460
|
+
const tmpPath = `${path}.alter-tmp-${timestamp}`;
|
|
1461
|
+
const backupPath = `${path}.alter-backup-${timestamp}`;
|
|
1462
|
+
let existed = false;
|
|
1463
|
+
let preBytes = null;
|
|
1464
|
+
let parsed = {};
|
|
1465
|
+
if (existsSync(path)) {
|
|
1466
|
+
existed = true;
|
|
1467
|
+
preBytes = readFileSync(path, "utf8");
|
|
1468
|
+
if (preBytes.trim().length > 0) {
|
|
1469
|
+
try {
|
|
1470
|
+
parsed = JSON.parse(preBytes);
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
throw new Error(
|
|
1473
|
+
`refusing to wire ${path}: existing file is not valid JSON (${err.message}). Hand-fix the file, then re-run \`alter-identity wire\`.`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
|
|
1477
|
+
throw new Error(`refusing to wire ${path}: existing JSON root is not an object`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const merged = merge(parsed);
|
|
1482
|
+
const serialised = JSON.stringify(merged, null, 2) + "\n";
|
|
1483
|
+
if (idempotent && preBytes !== null && preBytes === serialised) {
|
|
1484
|
+
return {
|
|
1485
|
+
path,
|
|
1486
|
+
backupPath: null,
|
|
1487
|
+
preSha256: sha2562(preBytes),
|
|
1488
|
+
postSha256: sha2562(preBytes),
|
|
1489
|
+
noop: true
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1493
|
+
writeFileSync(tmpPath, serialised, { mode: 384 });
|
|
1494
|
+
try {
|
|
1495
|
+
if (existed) copyFileSync(path, backupPath);
|
|
1496
|
+
renameSync(tmpPath, path);
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
try {
|
|
1499
|
+
unlinkSync(tmpPath);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
throw err;
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
path,
|
|
1506
|
+
backupPath: existed ? backupPath : null,
|
|
1507
|
+
preSha256: preBytes === null ? null : sha2562(preBytes),
|
|
1508
|
+
postSha256: sha2562(serialised),
|
|
1509
|
+
noop: false
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function restoreFromBackup(path, backupPath) {
|
|
1513
|
+
if (backupPath === null) {
|
|
1514
|
+
if (existsSync(path)) unlinkSync(path);
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (!existsSync(backupPath)) {
|
|
1518
|
+
throw new Error(`cannot restore ${path}: backup missing at ${backupPath}`);
|
|
1519
|
+
}
|
|
1520
|
+
renameSync(backupPath, path);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/wire/index.ts
|
|
1524
|
+
var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
|
|
1525
|
+
var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1526
|
+
function clientById(id) {
|
|
1527
|
+
const hit = ALL_CLIENTS.find((c) => c.id === id);
|
|
1528
|
+
if (!hit) throw new Error(`unknown client id: ${id}`);
|
|
1529
|
+
return hit;
|
|
1530
|
+
}
|
|
1531
|
+
function wire(opts = {}) {
|
|
1532
|
+
const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
1533
|
+
const apiKey = opts.apiKey;
|
|
1534
|
+
const probes = probeAll();
|
|
1535
|
+
const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
|
|
1536
|
+
const ts = TIMESTAMP();
|
|
1537
|
+
const targets = [];
|
|
1538
|
+
for (const id of selection) {
|
|
1539
|
+
const probe = id === "claude-code" ? probeClaudeCode() : probeByDir(id);
|
|
1540
|
+
if (!probe.installed && opts.skipMissing !== false) {
|
|
1541
|
+
targets.push({
|
|
1542
|
+
client: id,
|
|
1543
|
+
method: id === "claude-code" ? "cli" : "file",
|
|
1544
|
+
status: "skipped",
|
|
1545
|
+
...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
|
|
1546
|
+
reason: probe.reason
|
|
1547
|
+
});
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
try {
|
|
1551
|
+
if (id === "claude-code") {
|
|
1552
|
+
targets.push(wireClaudeCode({ endpoint, apiKey }));
|
|
1553
|
+
} else {
|
|
1554
|
+
targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
|
|
1555
|
+
}
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
const message = err.message;
|
|
1558
|
+
targets.push({
|
|
1559
|
+
client: id,
|
|
1560
|
+
method: id === "claude-code" ? "cli" : "file",
|
|
1561
|
+
status: "failed",
|
|
1562
|
+
...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
|
|
1563
|
+
reason: message
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
const state = {
|
|
1568
|
+
version: 1,
|
|
1569
|
+
sdkVersion: SDK_VERSION,
|
|
1570
|
+
writtenAt: ISO_NOW(),
|
|
1571
|
+
endpoint,
|
|
1572
|
+
targets
|
|
1573
|
+
};
|
|
1574
|
+
writeWireState(state);
|
|
1575
|
+
return { state, probes };
|
|
1576
|
+
}
|
|
1577
|
+
function wireFileTarget(args) {
|
|
1578
|
+
const client = clientById(args.id);
|
|
1579
|
+
if (!client.configPath) {
|
|
1580
|
+
throw new Error(`client ${client.id} has no file-based config path`);
|
|
1581
|
+
}
|
|
1582
|
+
const sync = detectSyncedVolume(client.configPath);
|
|
1583
|
+
if (sync) {
|
|
1584
|
+
throw new Error(
|
|
1585
|
+
`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.`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
|
|
1589
|
+
const rootKey = client.rootKey;
|
|
1590
|
+
const serverName = "alter";
|
|
1591
|
+
const result = atomicJsonMerge({
|
|
1592
|
+
path: client.configPath,
|
|
1593
|
+
timestamp: args.timestamp,
|
|
1594
|
+
merge: (existing) => {
|
|
1595
|
+
const bucket = existing[rootKey] ?? {};
|
|
1596
|
+
const source = entry.mcpServers.alter;
|
|
1597
|
+
return {
|
|
1598
|
+
...existing,
|
|
1599
|
+
[rootKey]: {
|
|
1600
|
+
...bucket,
|
|
1601
|
+
[serverName]: source
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
return {
|
|
1607
|
+
client: args.id,
|
|
1608
|
+
method: "file",
|
|
1609
|
+
status: result.noop ? "already-wired" : "written",
|
|
1610
|
+
path: result.path,
|
|
1611
|
+
backupPath: result.backupPath,
|
|
1612
|
+
rootKey,
|
|
1613
|
+
serverName,
|
|
1614
|
+
preSha256: result.preSha256,
|
|
1615
|
+
postSha256: result.postSha256
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function wireClaudeCode(args) {
|
|
1619
|
+
const cmd = "claude";
|
|
1620
|
+
const argList = [
|
|
1621
|
+
"mcp",
|
|
1622
|
+
"add",
|
|
1623
|
+
"--scope",
|
|
1624
|
+
"user",
|
|
1625
|
+
"--transport",
|
|
1626
|
+
"http",
|
|
1627
|
+
"alter",
|
|
1628
|
+
args.endpoint
|
|
1629
|
+
];
|
|
1630
|
+
if (args.apiKey) {
|
|
1631
|
+
argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
|
|
1632
|
+
}
|
|
1633
|
+
const full = `${cmd} ${argList.join(" ")}`;
|
|
1634
|
+
const run = spawnSync(cmd, argList, {
|
|
1635
|
+
encoding: "utf8",
|
|
1636
|
+
shell: process.platform === "win32",
|
|
1637
|
+
timeout: 1e4
|
|
1638
|
+
});
|
|
1639
|
+
if (run.error) {
|
|
1640
|
+
return {
|
|
1641
|
+
client: "claude-code",
|
|
1642
|
+
method: "cli",
|
|
1643
|
+
status: "failed",
|
|
1644
|
+
command: full,
|
|
1645
|
+
stdout: run.stdout,
|
|
1646
|
+
stderr: run.stderr,
|
|
1647
|
+
reason: run.error.message
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
const stderr2 = (run.stderr ?? "").toLowerCase();
|
|
1651
|
+
const alreadyExists = stderr2.includes("already exists") || stderr2.includes("already configured");
|
|
1652
|
+
if (run.status === 0) {
|
|
1653
|
+
return { client: "claude-code", method: "cli", status: "written", command: full, stdout: run.stdout, stderr: run.stderr };
|
|
1654
|
+
}
|
|
1655
|
+
if (alreadyExists) {
|
|
1656
|
+
return { client: "claude-code", method: "cli", status: "already-wired", command: full, stdout: run.stdout, stderr: run.stderr };
|
|
1657
|
+
}
|
|
1658
|
+
return {
|
|
1659
|
+
client: "claude-code",
|
|
1660
|
+
method: "cli",
|
|
1661
|
+
status: "failed",
|
|
1662
|
+
command: full,
|
|
1663
|
+
stdout: run.stdout,
|
|
1664
|
+
stderr: run.stderr,
|
|
1665
|
+
reason: `claude mcp add exited ${String(run.status)}`
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function unwire() {
|
|
1669
|
+
const state = readWireState();
|
|
1670
|
+
const undone = [];
|
|
1671
|
+
if (!state || state.targets.length === 0) {
|
|
1672
|
+
return { state, undone };
|
|
1673
|
+
}
|
|
1674
|
+
for (const target of state.targets) {
|
|
1675
|
+
try {
|
|
1676
|
+
if (target.method === "file") {
|
|
1677
|
+
if (target.status === "written") {
|
|
1678
|
+
restoreFromBackup(target.path, target.backupPath);
|
|
1679
|
+
undone.push({ client: target.client, action: target.backupPath ? "restored" : "removed" });
|
|
1680
|
+
} else {
|
|
1681
|
+
undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
|
|
1682
|
+
}
|
|
1683
|
+
} else if (target.method === "cli") {
|
|
1684
|
+
if (target.status === "written") {
|
|
1685
|
+
const run = spawnSync("claude", ["mcp", "remove", "--scope", "user", "alter"], {
|
|
1686
|
+
encoding: "utf8",
|
|
1687
|
+
shell: process.platform === "win32",
|
|
1688
|
+
timeout: 1e4
|
|
1689
|
+
});
|
|
1690
|
+
if (run.error) {
|
|
1691
|
+
undone.push({ client: target.client, action: "failed", reason: run.error.message });
|
|
1692
|
+
} else if (run.status === 0) {
|
|
1693
|
+
undone.push({ client: target.client, action: "cli-removed" });
|
|
1694
|
+
} else {
|
|
1695
|
+
undone.push({ client: target.client, action: "failed", reason: `claude mcp remove exited ${String(run.status)}` });
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
undone.push({ client: target.client, action: "failed", reason: err.message });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
writeWireState({
|
|
1706
|
+
version: 1,
|
|
1707
|
+
sdkVersion: state.sdkVersion,
|
|
1708
|
+
writtenAt: ISO_NOW(),
|
|
1709
|
+
endpoint: state.endpoint,
|
|
1710
|
+
targets: []
|
|
1711
|
+
});
|
|
1712
|
+
return { state, undone };
|
|
1713
|
+
}
|
|
1034
1714
|
|
|
1035
1715
|
// bin/alter-identity.ts
|
|
1036
1716
|
var CONFIG_DIR = join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter");
|
|
@@ -1050,6 +1730,12 @@ async function main() {
|
|
|
1050
1730
|
case "config":
|
|
1051
1731
|
await runConfig(rest);
|
|
1052
1732
|
break;
|
|
1733
|
+
case "wire":
|
|
1734
|
+
await runWire(rest);
|
|
1735
|
+
break;
|
|
1736
|
+
case "unwire":
|
|
1737
|
+
await runUnwire();
|
|
1738
|
+
break;
|
|
1053
1739
|
case "message":
|
|
1054
1740
|
await runMessage(rest);
|
|
1055
1741
|
break;
|
|
@@ -1077,11 +1763,15 @@ function printHelp() {
|
|
|
1077
1763
|
stdout.write(`${SDK_NAME} ${SDK_VERSION}
|
|
1078
1764
|
|
|
1079
1765
|
Usage:
|
|
1080
|
-
alter-identity init
|
|
1766
|
+
alter-identity init [--wire|--no-wire] [--yes]
|
|
1767
|
+
Generate keypair, discover MCP, optionally wire detected AI clients
|
|
1081
1768
|
alter-identity verify <~handle|email> Verify an identity
|
|
1082
1769
|
alter-identity status Show connection state
|
|
1083
|
-
alter-identity config [--claude|--cursor|--generic]
|
|
1770
|
+
alter-identity config [--claude|--cursor|--claude-desktop|--generic]
|
|
1084
1771
|
Print MCP config snippet
|
|
1772
|
+
alter-identity wire [--only=<ids>] [--yes]
|
|
1773
|
+
Merge ALTER into detected AI clients (Claude Code, Cursor, Claude Desktop)
|
|
1774
|
+
alter-identity unwire Restore every target from its backup sibling
|
|
1085
1775
|
|
|
1086
1776
|
Alter-to-Alter Messaging:
|
|
1087
1777
|
alter-identity message send <~handle> <body> Send a direct message (body '-' = stdin)
|
|
@@ -1095,6 +1785,13 @@ Config: ${CONFIG_PATH}
|
|
|
1095
1785
|
}
|
|
1096
1786
|
async function runInit(args) {
|
|
1097
1787
|
const force = args.includes("--force") || args.includes("-f");
|
|
1788
|
+
const wireFlag = args.includes("--wire");
|
|
1789
|
+
const noWireFlag = args.includes("--no-wire");
|
|
1790
|
+
const yesFlag = args.includes("--yes") || args.includes("-y");
|
|
1791
|
+
if (wireFlag && noWireFlag) {
|
|
1792
|
+
stderr.write("error: --wire and --no-wire are mutually exclusive\n");
|
|
1793
|
+
exit(2);
|
|
1794
|
+
}
|
|
1098
1795
|
const existing = readConfig();
|
|
1099
1796
|
if (existing && !force) {
|
|
1100
1797
|
stdout.write(`already initialised at ${CONFIG_PATH} (re-run with --force to overwrite)
|
|
@@ -1121,6 +1818,28 @@ async function runInit(args) {
|
|
|
1121
1818
|
`);
|
|
1122
1819
|
stdout.write(` did: ${keypair.did}
|
|
1123
1820
|
`);
|
|
1821
|
+
let shouldWire = false;
|
|
1822
|
+
if (noWireFlag) {
|
|
1823
|
+
shouldWire = false;
|
|
1824
|
+
} else if (wireFlag || yesFlag) {
|
|
1825
|
+
shouldWire = true;
|
|
1826
|
+
} else if (stdin.isTTY) {
|
|
1827
|
+
const probes = probeAll();
|
|
1828
|
+
const found = probes.filter((p) => p.installed).map((p) => p.client.label);
|
|
1829
|
+
if (found.length === 0) {
|
|
1830
|
+
stdout.write("\nNo MCP-aware clients detected on this machine \u2014 skipping wire.\n");
|
|
1831
|
+
} else {
|
|
1832
|
+
stdout.write(`
|
|
1833
|
+
Detected MCP-aware clients: ${found.join(", ")}
|
|
1834
|
+
`);
|
|
1835
|
+
shouldWire = await confirm("Wire detected AI clients to ALTER?", true);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (shouldWire) {
|
|
1839
|
+
stdout.write("\n\u2022 Wiring detected AI clients...\n");
|
|
1840
|
+
const report = wire({ endpoint });
|
|
1841
|
+
printWireReport(report);
|
|
1842
|
+
}
|
|
1124
1843
|
stdout.write(`
|
|
1125
1844
|
Next: alter-identity verify ~truealter
|
|
1126
1845
|
`);
|
|
@@ -1179,10 +1898,121 @@ async function runConfig(args) {
|
|
|
1179
1898
|
const opts = { endpoint: cfg.endpoint, apiKey: cfg.apiKey };
|
|
1180
1899
|
let out;
|
|
1181
1900
|
if (args.includes("--cursor")) out = generateCursorConfig(opts);
|
|
1901
|
+
else if (args.includes("--claude-desktop")) out = generateClaudeDesktopConfig(opts);
|
|
1182
1902
|
else if (args.includes("--generic")) out = generateGenericMcpConfig(opts);
|
|
1183
1903
|
else out = generateClaudeConfig(opts);
|
|
1184
1904
|
stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
1185
1905
|
}
|
|
1906
|
+
async function runWire(args) {
|
|
1907
|
+
const yesFlag = args.includes("--yes") || args.includes("-y");
|
|
1908
|
+
const onlyArg = args.find((a) => a.startsWith("--only="));
|
|
1909
|
+
const only = onlyArg ? onlyArg.slice("--only=".length).split(",").filter(Boolean) : void 0;
|
|
1910
|
+
const cfg = readConfig() ?? {};
|
|
1911
|
+
if (!cfg.endpoint) {
|
|
1912
|
+
stderr.write("error: no endpoint \u2014 run `alter-identity init` first\n");
|
|
1913
|
+
exit(2);
|
|
1914
|
+
}
|
|
1915
|
+
if (!yesFlag && stdin.isTTY) {
|
|
1916
|
+
const probes = probeAll();
|
|
1917
|
+
const found = probes.filter((p) => p.installed).map((p) => p.client.label);
|
|
1918
|
+
if (found.length === 0) {
|
|
1919
|
+
stdout.write("No MCP-aware clients detected on this machine. Nothing to do.\n");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
stdout.write(`Detected: ${found.join(", ")}
|
|
1923
|
+
`);
|
|
1924
|
+
const proceed = await confirm("Wire these clients to ALTER?", true);
|
|
1925
|
+
if (!proceed) {
|
|
1926
|
+
stdout.write("aborted.\n");
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const report = wire({ endpoint: cfg.endpoint, apiKey: cfg.apiKey, only });
|
|
1931
|
+
printWireReport(report);
|
|
1932
|
+
}
|
|
1933
|
+
async function runUnwire() {
|
|
1934
|
+
const report = unwire();
|
|
1935
|
+
printUnwireReport(report);
|
|
1936
|
+
}
|
|
1937
|
+
function printWireReport(report) {
|
|
1938
|
+
for (const target of report.state.targets) {
|
|
1939
|
+
const tag = `[${target.client}]`;
|
|
1940
|
+
switch (target.status) {
|
|
1941
|
+
case "written":
|
|
1942
|
+
if (target.method === "file") {
|
|
1943
|
+
stdout.write(` \u2713 ${tag} wrote ${target.path} (backup: ${target.backupPath ?? "(none \u2014 created new file)"})
|
|
1944
|
+
`);
|
|
1945
|
+
} else {
|
|
1946
|
+
stdout.write(` \u2713 ${tag} registered via \`${target.command}\`
|
|
1947
|
+
`);
|
|
1948
|
+
}
|
|
1949
|
+
break;
|
|
1950
|
+
case "already-wired":
|
|
1951
|
+
stdout.write(` \xB7 ${tag} already wired \u2014 no change
|
|
1952
|
+
`);
|
|
1953
|
+
break;
|
|
1954
|
+
case "skipped":
|
|
1955
|
+
stdout.write(` - ${tag} skipped (${target.reason ?? "not installed"})
|
|
1956
|
+
`);
|
|
1957
|
+
break;
|
|
1958
|
+
case "failed":
|
|
1959
|
+
stderr.write(` \u2717 ${tag} failed: ${target.reason ?? "unknown"}
|
|
1960
|
+
`);
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
stdout.write(`
|
|
1965
|
+
wire-state \u2192 ${join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter", "wire-state.json")}
|
|
1966
|
+
`);
|
|
1967
|
+
stdout.write("run `alter-identity unwire` to reverse.\n");
|
|
1968
|
+
}
|
|
1969
|
+
function printUnwireReport(report) {
|
|
1970
|
+
if (!report.state) {
|
|
1971
|
+
stdout.write("nothing to unwire \u2014 no wire-state.json found\n");
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
if (report.state.targets.length === 0) {
|
|
1975
|
+
stdout.write("wire-state.json is empty \u2014 nothing to unwire\n");
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
for (const entry of report.undone) {
|
|
1979
|
+
const tag = `[${entry.client}]`;
|
|
1980
|
+
switch (entry.action) {
|
|
1981
|
+
case "restored":
|
|
1982
|
+
stdout.write(` \u2713 ${tag} restored from backup
|
|
1983
|
+
`);
|
|
1984
|
+
break;
|
|
1985
|
+
case "removed":
|
|
1986
|
+
stdout.write(` \u2713 ${tag} removed (file was created by wire)
|
|
1987
|
+
`);
|
|
1988
|
+
break;
|
|
1989
|
+
case "cli-removed":
|
|
1990
|
+
stdout.write(` \u2713 ${tag} removed via \`claude mcp remove\`
|
|
1991
|
+
`);
|
|
1992
|
+
break;
|
|
1993
|
+
case "skipped":
|
|
1994
|
+
stdout.write(` \xB7 ${tag} skipped (${entry.reason ?? ""})
|
|
1995
|
+
`);
|
|
1996
|
+
break;
|
|
1997
|
+
case "failed":
|
|
1998
|
+
stderr.write(` \u2717 ${tag} failed: ${entry.reason ?? ""}
|
|
1999
|
+
`);
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async function confirm(question, defaultYes) {
|
|
2005
|
+
if (!stdin.isTTY) return false;
|
|
2006
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
2007
|
+
const suffix = " [Y/n] " ;
|
|
2008
|
+
const answer = await new Promise((resolve2) => {
|
|
2009
|
+
rl.question(question + suffix, (ans) => resolve2(ans));
|
|
2010
|
+
});
|
|
2011
|
+
rl.close();
|
|
2012
|
+
const trimmed = answer.trim().toLowerCase();
|
|
2013
|
+
if (!trimmed) return defaultYes;
|
|
2014
|
+
return trimmed === "y" || trimmed === "yes";
|
|
2015
|
+
}
|
|
1186
2016
|
async function runMessage(args) {
|
|
1187
2017
|
const [sub, ...rest] = args;
|
|
1188
2018
|
if (!sub) {
|
|
@@ -1316,5 +2146,3 @@ main().catch((err) => {
|
|
|
1316
2146
|
`);
|
|
1317
2147
|
exit(1);
|
|
1318
2148
|
});
|
|
1319
|
-
//# sourceMappingURL=alter-identity.js.map
|
|
1320
|
-
//# sourceMappingURL=alter-identity.js.map
|