@truealter/sdk 0.5.3 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -80
- package/dist/bin/mcp-bridge.js +141 -12
- package/dist/index.cjs +167 -23
- package/dist/index.d.cts +62 -39
- package/dist/index.d.ts +62 -39
- package/dist/index.js +167 -23
- package/package.json +3 -6
- package/dist/bin/alter-identity.js +0 -2641
|
@@ -1,2641 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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 { createPublicKey, verify, createHash, createPrivateKey } from 'crypto';
|
|
6
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, renameSync, statSync, chmodSync, copyFileSync } from 'fs';
|
|
7
|
-
import { homedir, platform } from 'os';
|
|
8
|
-
import { join, dirname, resolve } from 'path';
|
|
9
|
-
import { env, stderr, exit, argv, stdout, stdin } from 'process';
|
|
10
|
-
import { createInterface } from 'readline';
|
|
11
|
-
import * as ed25519 from '@noble/ed25519';
|
|
12
|
-
import { sha512 } from '@noble/hashes/sha512';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
import { spawnSync } from 'child_process';
|
|
15
|
-
|
|
16
|
-
var __defProp = Object.defineProperty;
|
|
17
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
18
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
19
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
20
|
-
var __esm = (fn, res) => function __init() {
|
|
21
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
22
|
-
};
|
|
23
|
-
var __export = (target, all) => {
|
|
24
|
-
for (var name in all)
|
|
25
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
|
-
};
|
|
27
|
-
var __copyProps = (to, from, except, desc) => {
|
|
28
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
29
|
-
for (let key of __getOwnPropNames(from))
|
|
30
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
31
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
32
|
-
}
|
|
33
|
-
return to;
|
|
34
|
-
};
|
|
35
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
|
-
|
|
37
|
-
// src/signing.ts
|
|
38
|
-
var signing_exports = {};
|
|
39
|
-
__export(signing_exports, {
|
|
40
|
-
canonicalArgsSha256: () => canonicalArgsSha256,
|
|
41
|
-
canonicalStringify: () => canonicalStringify,
|
|
42
|
-
loadPrivateKey: () => loadPrivateKey,
|
|
43
|
-
signInvocation: () => signInvocation
|
|
44
|
-
});
|
|
45
|
-
function canonicalStringify(value) {
|
|
46
|
-
return stringifyInner(value);
|
|
47
|
-
}
|
|
48
|
-
function stringifyInner(value) {
|
|
49
|
-
if (value === null) return "null";
|
|
50
|
-
if (value === void 0) {
|
|
51
|
-
throw new TypeError("canonicalStringify: undefined is not representable in JSON");
|
|
52
|
-
}
|
|
53
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
54
|
-
if (typeof value === "number") {
|
|
55
|
-
if (!Number.isFinite(value)) {
|
|
56
|
-
throw new TypeError("canonicalStringify: non-finite numbers are not representable");
|
|
57
|
-
}
|
|
58
|
-
return JSON.stringify(value);
|
|
59
|
-
}
|
|
60
|
-
if (typeof value === "string") return encodeString(value);
|
|
61
|
-
if (Array.isArray(value)) {
|
|
62
|
-
return "[" + value.map((v) => stringifyInner(v)).join(",") + "]";
|
|
63
|
-
}
|
|
64
|
-
if (typeof value === "object") {
|
|
65
|
-
const obj = value;
|
|
66
|
-
const keys = Object.keys(obj).sort();
|
|
67
|
-
return "{" + keys.map((k) => encodeString(k) + ":" + stringifyInner(obj[k])).join(",") + "}";
|
|
68
|
-
}
|
|
69
|
-
throw new TypeError(`canonicalStringify: unsupported type ${typeof value}`);
|
|
70
|
-
}
|
|
71
|
-
function encodeString(s) {
|
|
72
|
-
return JSON.stringify(s);
|
|
73
|
-
}
|
|
74
|
-
function canonicalArgsSha256(toolArgs) {
|
|
75
|
-
const canonical = canonicalStringify(toolArgs ?? {});
|
|
76
|
-
const bytes = new TextEncoder().encode(canonical);
|
|
77
|
-
const digest = sha256(bytes);
|
|
78
|
-
return bytesToHex(digest);
|
|
79
|
-
}
|
|
80
|
-
function bytesToHex(bytes) {
|
|
81
|
-
let out = "";
|
|
82
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
83
|
-
out += bytes[i].toString(16).padStart(2, "0");
|
|
84
|
-
}
|
|
85
|
-
return out;
|
|
86
|
-
}
|
|
87
|
-
function base64urlEncode(bytes) {
|
|
88
|
-
const raw = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
|
|
89
|
-
if (typeof Buffer !== "undefined") {
|
|
90
|
-
return Buffer.from(raw).toString("base64url");
|
|
91
|
-
}
|
|
92
|
-
let binary = "";
|
|
93
|
-
for (let i = 0; i < raw.length; i++) binary += String.fromCharCode(raw[i]);
|
|
94
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
95
|
-
}
|
|
96
|
-
function loadPrivateKey(key) {
|
|
97
|
-
if (key instanceof Uint8Array) {
|
|
98
|
-
if (key.length !== 32) {
|
|
99
|
-
throw new TypeError("ES256 raw private key must be 32 bytes.");
|
|
100
|
-
}
|
|
101
|
-
return key;
|
|
102
|
-
}
|
|
103
|
-
if (typeof key === "string" && key.includes("-----BEGIN")) {
|
|
104
|
-
const keyObj = createPrivateKey({ key, format: "pem" });
|
|
105
|
-
const jwk = keyObj.export({ format: "jwk" });
|
|
106
|
-
if (jwk.crv !== "P-256" || !jwk.d) {
|
|
107
|
-
throw new TypeError("PEM is not a P-256 private key.");
|
|
108
|
-
}
|
|
109
|
-
return base64urlDecodeToBytes(jwk.d);
|
|
110
|
-
}
|
|
111
|
-
throw new TypeError("loadPrivateKey: expected Uint8Array(32) or PEM string.");
|
|
112
|
-
}
|
|
113
|
-
function base64urlDecodeToBytes(s) {
|
|
114
|
-
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
115
|
-
const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
116
|
-
if (typeof Buffer !== "undefined") {
|
|
117
|
-
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
118
|
-
}
|
|
119
|
-
const binary = atob(b64);
|
|
120
|
-
const out = new Uint8Array(binary.length);
|
|
121
|
-
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
function signInvocation(toolName, toolArgs, options) {
|
|
125
|
-
const { kid, privateKey, handle } = options;
|
|
126
|
-
const nonce = options.nonce ?? base64urlEncode(randomBytes(24));
|
|
127
|
-
const iat = options.iatSeconds ?? Math.floor(Date.now() / 1e3);
|
|
128
|
-
const claims = {
|
|
129
|
-
tool: toolName,
|
|
130
|
-
args_sha256: canonicalArgsSha256(toolArgs ?? {}),
|
|
131
|
-
nonce,
|
|
132
|
-
iat,
|
|
133
|
-
iss: handle
|
|
134
|
-
};
|
|
135
|
-
const headerB64 = base64urlEncode(JSON.stringify({ alg: "ES256", kid }));
|
|
136
|
-
const payloadB64 = base64urlEncode(JSON.stringify(claims));
|
|
137
|
-
const signingInput = `${headerB64}.${payloadB64}`;
|
|
138
|
-
const signingBytes = new TextEncoder().encode(signingInput);
|
|
139
|
-
const dBytes = loadPrivateKey(privateKey);
|
|
140
|
-
const digest = sha256(signingBytes);
|
|
141
|
-
const sig = p256.sign(digest, dBytes, { prehash: false });
|
|
142
|
-
const sigBytes = sig.toCompactRawBytes();
|
|
143
|
-
const sigB64 = base64urlEncode(sigBytes);
|
|
144
|
-
return `${signingInput}.${sigB64}`;
|
|
145
|
-
}
|
|
146
|
-
var init_signing = __esm({
|
|
147
|
-
"src/signing.ts"() {
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// src/errors.ts
|
|
152
|
-
var AlterError = class extends Error {
|
|
153
|
-
code;
|
|
154
|
-
cause;
|
|
155
|
-
constructor(code, message, cause) {
|
|
156
|
-
super(message);
|
|
157
|
-
this.name = "AlterError";
|
|
158
|
-
this.code = code;
|
|
159
|
-
this.cause = cause;
|
|
160
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
var AlterNetworkError = class extends AlterError {
|
|
164
|
-
constructor(message, cause) {
|
|
165
|
-
super("NETWORK", message, cause);
|
|
166
|
-
this.name = "AlterNetworkError";
|
|
167
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
var AlterTimeoutError = class extends AlterError {
|
|
171
|
-
constructor(message, cause) {
|
|
172
|
-
super("TIMEOUT", message, cause);
|
|
173
|
-
this.name = "AlterTimeoutError";
|
|
174
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
var AlterAuthError = class extends AlterError {
|
|
178
|
-
status;
|
|
179
|
-
constructor(message, status = 401) {
|
|
180
|
-
super("AUTH", message);
|
|
181
|
-
this.name = "AlterAuthError";
|
|
182
|
-
this.status = status;
|
|
183
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
var AlterPaymentRequired = class extends AlterError {
|
|
187
|
-
envelope;
|
|
188
|
-
tool;
|
|
189
|
-
constructor(tool, envelope) {
|
|
190
|
-
super("PAYMENT_REQUIRED", `x402 payment required for tool "${tool}"`);
|
|
191
|
-
this.name = "AlterPaymentRequired";
|
|
192
|
-
this.tool = tool;
|
|
193
|
-
this.envelope = envelope;
|
|
194
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
var AlterRateLimited = class extends AlterError {
|
|
198
|
-
retryAfter;
|
|
199
|
-
constructor(message, retryAfter = 60) {
|
|
200
|
-
super("RATE_LIMITED", message);
|
|
201
|
-
this.name = "AlterRateLimited";
|
|
202
|
-
this.retryAfter = retryAfter;
|
|
203
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
var AlterToolError = class extends AlterError {
|
|
207
|
-
tool;
|
|
208
|
-
rpcCode;
|
|
209
|
-
constructor(tool, message, rpcCode) {
|
|
210
|
-
super("TOOL_ERROR", message);
|
|
211
|
-
this.name = "AlterToolError";
|
|
212
|
-
this.tool = tool;
|
|
213
|
-
this.rpcCode = rpcCode;
|
|
214
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
var AlterProvenanceError = class extends AlterError {
|
|
218
|
-
constructor(message, cause) {
|
|
219
|
-
super("PROVENANCE", message, cause);
|
|
220
|
-
this.name = "AlterProvenanceError";
|
|
221
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
var AlterDiscoveryError = class extends AlterError {
|
|
225
|
-
constructor(message, cause) {
|
|
226
|
-
super("DISCOVERY", message, cause);
|
|
227
|
-
this.name = "AlterDiscoveryError";
|
|
228
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
var AlterInvalidResponse = class extends AlterError {
|
|
232
|
-
constructor(message, cause) {
|
|
233
|
-
super("INVALID_RESPONSE", message, cause);
|
|
234
|
-
this.name = "AlterInvalidResponse";
|
|
235
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
// src/discovery.ts
|
|
240
|
-
var _cache = /* @__PURE__ */ new Map();
|
|
241
|
-
async function discover(domain, opts = {}) {
|
|
242
|
-
const { cache = true, skipDns = false, timeoutMs = 5e3, fetch: fetchImpl = fetch } = opts;
|
|
243
|
-
const host = normaliseDomain(domain);
|
|
244
|
-
if (cache && _cache.has(host)) return _cache.get(host);
|
|
245
|
-
const errors = [];
|
|
246
|
-
if (!skipDns) {
|
|
247
|
-
try {
|
|
248
|
-
const dnsHit = await tryDns(host);
|
|
249
|
-
if (dnsHit) {
|
|
250
|
-
const parsed = validateDiscoveredUrl(dnsHit, "dns");
|
|
251
|
-
const result = {
|
|
252
|
-
url: parsed.toString().replace(/\/$/, ""),
|
|
253
|
-
transport: "streamable-http",
|
|
254
|
-
source: "dns"
|
|
255
|
-
};
|
|
256
|
-
if (cache) _cache.set(host, result);
|
|
257
|
-
return result;
|
|
258
|
-
}
|
|
259
|
-
} catch (err) {
|
|
260
|
-
errors.push(`dns: ${err.message}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
try {
|
|
264
|
-
const result = await tryWellKnown(host, "mcp.json", timeoutMs, fetchImpl);
|
|
265
|
-
if (result) {
|
|
266
|
-
if (cache) _cache.set(host, result);
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
} catch (err) {
|
|
270
|
-
errors.push(`mcp.json: ${err.message}`);
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
const result = await tryWellKnown(host, "alter.json", timeoutMs, fetchImpl);
|
|
274
|
-
if (result) {
|
|
275
|
-
if (cache) _cache.set(host, result);
|
|
276
|
-
return result;
|
|
277
|
-
}
|
|
278
|
-
} catch (err) {
|
|
279
|
-
errors.push(`alter.json: ${err.message}`);
|
|
280
|
-
}
|
|
281
|
-
throw new AlterDiscoveryError(
|
|
282
|
-
`No MCP discovery hit for ${host}: ${errors.join("; ") || "all sources empty"}`
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
function normaliseDomain(input) {
|
|
286
|
-
let host = input.trim().toLowerCase();
|
|
287
|
-
host = host.replace(/^https?:\/\//, "");
|
|
288
|
-
host = host.split("/")[0];
|
|
289
|
-
host = host.split(":")[0];
|
|
290
|
-
if (!host) throw new AlterDiscoveryError(`Empty domain: "${input}"`);
|
|
291
|
-
return host;
|
|
292
|
-
}
|
|
293
|
-
function validateDiscoveredUrl(url, source) {
|
|
294
|
-
let parsed;
|
|
295
|
-
try {
|
|
296
|
-
parsed = new URL(url);
|
|
297
|
-
} catch {
|
|
298
|
-
throw new AlterDiscoveryError(`${source}: malformed URL ${url}`);
|
|
299
|
-
}
|
|
300
|
-
if (parsed.protocol !== "https:") {
|
|
301
|
-
throw new AlterDiscoveryError(
|
|
302
|
-
`${source}: non-https MCP endpoint rejected (got ${parsed.protocol}//${parsed.hostname})`
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
if (parsed.username || parsed.password) {
|
|
306
|
-
throw new AlterDiscoveryError(
|
|
307
|
-
`${source}: MCP endpoint must not contain userinfo (user:pass@host)`
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
if (!parsed.hostname) {
|
|
311
|
-
throw new AlterDiscoveryError(`${source}: MCP endpoint missing hostname`);
|
|
312
|
-
}
|
|
313
|
-
return parsed;
|
|
314
|
-
}
|
|
315
|
-
async function tryDns(host) {
|
|
316
|
-
let resolveTxt;
|
|
317
|
-
try {
|
|
318
|
-
const dns = await import('dns/promises');
|
|
319
|
-
resolveTxt = dns.resolveTxt.bind(dns);
|
|
320
|
-
} catch {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
let records;
|
|
324
|
-
try {
|
|
325
|
-
records = await resolveTxt(`_mcp.${host}`);
|
|
326
|
-
} catch (err) {
|
|
327
|
-
const code = err.code;
|
|
328
|
-
if (code === "ENOTFOUND" || code === "ENODATA") return null;
|
|
329
|
-
throw err;
|
|
330
|
-
}
|
|
331
|
-
for (const chunks of records) {
|
|
332
|
-
const joined = chunks.join("");
|
|
333
|
-
const parsed = parseDnsTxt(joined);
|
|
334
|
-
if (parsed.mcp) return parsed.mcp;
|
|
335
|
-
}
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
function parseDnsTxt(record) {
|
|
339
|
-
const out = {};
|
|
340
|
-
for (const part of record.split(/[;\s]+/)) {
|
|
341
|
-
const [k, ...rest] = part.split("=");
|
|
342
|
-
if (!k || rest.length === 0) continue;
|
|
343
|
-
out[k.toLowerCase()] = rest.join("=");
|
|
344
|
-
}
|
|
345
|
-
return out;
|
|
346
|
-
}
|
|
347
|
-
async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
|
|
348
|
-
const url = `https://${host}/.well-known/${file}`;
|
|
349
|
-
const controller = new AbortController();
|
|
350
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
351
|
-
let resp;
|
|
352
|
-
try {
|
|
353
|
-
resp = await fetchImpl(url, {
|
|
354
|
-
method: "GET",
|
|
355
|
-
headers: { Accept: "application/json" },
|
|
356
|
-
signal: controller.signal,
|
|
357
|
-
redirect: "manual"
|
|
358
|
-
});
|
|
359
|
-
} catch (err) {
|
|
360
|
-
throw new AlterNetworkError(`fetch ${url}: ${err.message}`, err);
|
|
361
|
-
} finally {
|
|
362
|
-
clearTimeout(timer);
|
|
363
|
-
}
|
|
364
|
-
if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
|
|
365
|
-
throw new AlterNetworkError(
|
|
366
|
-
`${url} -> redirect rejected (discovery must not follow redirects; validate the server configuration)`
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
if (resp.status === 404) return null;
|
|
370
|
-
if (!resp.ok) throw new AlterNetworkError(`${url} -> HTTP ${resp.status}`);
|
|
371
|
-
const doc = await resp.json();
|
|
372
|
-
if (file === "mcp.json") {
|
|
373
|
-
const remotes = doc.remotes || [];
|
|
374
|
-
const remote = remotes.find((r) => r.transportType === "streamable-http" || r.transportType === "http");
|
|
375
|
-
const rawUrl = remote?.url || doc.url;
|
|
376
|
-
if (!rawUrl) return null;
|
|
377
|
-
const parsed = validateDiscoveredUrl(rawUrl, "mcp.json");
|
|
378
|
-
return { url: parsed.toString().replace(/\/$/, ""), transport: "streamable-http", source: "mcp.json", raw: doc };
|
|
379
|
-
}
|
|
380
|
-
const mcpHost = doc.mcp;
|
|
381
|
-
if (!mcpHost) return null;
|
|
382
|
-
const normalised = ensureMcpPath(mcpHost);
|
|
383
|
-
validateDiscoveredUrl(normalised, "alter.json");
|
|
384
|
-
return {
|
|
385
|
-
url: normalised,
|
|
386
|
-
transport: "streamable-http",
|
|
387
|
-
source: "alter.json",
|
|
388
|
-
publicKey: doc.pk,
|
|
389
|
-
x402Contract: doc.x402,
|
|
390
|
-
capability: doc.cap,
|
|
391
|
-
raw: doc
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
function ensureMcpPath(url) {
|
|
395
|
-
try {
|
|
396
|
-
const u = new URL(url);
|
|
397
|
-
if (u.pathname === "" || u.pathname === "/") u.pathname = "/api/v1/mcp";
|
|
398
|
-
return u.toString().replace(/\/$/, "");
|
|
399
|
-
} catch {
|
|
400
|
-
return url;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// src/meta.ts
|
|
405
|
-
var SDK_NAME = "@truealter/sdk";
|
|
406
|
-
var SDK_VERSION = "0.5.3" ;
|
|
407
|
-
|
|
408
|
-
// src/floor-preflight.ts
|
|
409
|
-
var MIN_VERSION_ENDPOINT = "/v1/clients/min-version";
|
|
410
|
-
var CLIENT_ID = "alter-identity";
|
|
411
|
-
var CLIENT_CHANNEL = "npm";
|
|
412
|
-
var IN_MEMORY_TTL_DEFAULT_MS = 60 * 60 * 1e3;
|
|
413
|
-
var IN_MEMORY_TTL_MIN_MS = 60 * 1e3;
|
|
414
|
-
var IN_MEMORY_TTL_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
415
|
-
var DISK_FRESH_MS = 24 * 60 * 60 * 1e3;
|
|
416
|
-
var DISK_WARN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
417
|
-
var FETCH_TIMEOUT_MS = 4e3;
|
|
418
|
-
function computeKeyId(publicKeyPem) {
|
|
419
|
-
if (!publicKeyPem) return "00000000";
|
|
420
|
-
const pub = createPublicKey({ key: publicKeyPem, format: "pem" });
|
|
421
|
-
const jwk = pub.export({ format: "jwk" });
|
|
422
|
-
const rawBytes = Buffer.from(jwk.x, "base64url");
|
|
423
|
-
return createHash("sha256").update(rawBytes).digest("hex").slice(0, 8);
|
|
424
|
-
}
|
|
425
|
-
function canonicalJson(obj) {
|
|
426
|
-
return JSON.stringify(sortKeysDeep(obj));
|
|
427
|
-
}
|
|
428
|
-
function sortKeysDeep(value) {
|
|
429
|
-
if (Array.isArray(value)) {
|
|
430
|
-
return value.map(sortKeysDeep);
|
|
431
|
-
}
|
|
432
|
-
if (value !== null && typeof value === "object") {
|
|
433
|
-
const obj = value;
|
|
434
|
-
const sorted = {};
|
|
435
|
-
for (const k of Object.keys(obj).sort()) {
|
|
436
|
-
sorted[k] = sortKeysDeep(obj[k]);
|
|
437
|
-
}
|
|
438
|
-
return sorted;
|
|
439
|
-
}
|
|
440
|
-
return value;
|
|
441
|
-
}
|
|
442
|
-
var KNOWN_FLOOR_PUBLIC_KEYS = {
|
|
443
|
-
"8aa59e05": `-----BEGIN PUBLIC KEY-----
|
|
444
|
-
MCowBQYDK2VwAyEAgqw28dlniOuiTE1f4BxCPSEgMLaPtHsO8wN5RWEwEhE=
|
|
445
|
-
-----END PUBLIC KEY-----`,
|
|
446
|
-
"640f7d9a": `-----BEGIN PUBLIC KEY-----
|
|
447
|
-
MCowBQYDK2VwAyEARzvAWayDwHvZRfOZizGZe+/a7PF082WGhyMS3tx06H4=
|
|
448
|
-
-----END PUBLIC KEY-----`
|
|
449
|
-
};
|
|
450
|
-
var BelowFloorError = class extends Error {
|
|
451
|
-
name = "BelowFloorError";
|
|
452
|
-
code = "client_below_floor";
|
|
453
|
-
client_version;
|
|
454
|
-
min_version;
|
|
455
|
-
upgrade_cmd;
|
|
456
|
-
channel;
|
|
457
|
-
envelope;
|
|
458
|
-
constructor(envelope) {
|
|
459
|
-
super(envelope.error.message);
|
|
460
|
-
this.envelope = envelope;
|
|
461
|
-
this.client_version = envelope.error.client_version;
|
|
462
|
-
this.min_version = envelope.error.min_version;
|
|
463
|
-
this.upgrade_cmd = envelope.error.upgrade_cmd;
|
|
464
|
-
this.channel = envelope.error.channel;
|
|
465
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
var memCache = null;
|
|
469
|
-
async function checkMinVersion(opts = {}) {
|
|
470
|
-
const apiBase = opts.apiBase ?? defaultApiBase();
|
|
471
|
-
const clientVersion = opts.clientVersion ?? SDK_VERSION;
|
|
472
|
-
const clientId = opts.clientId ?? CLIENT_ID;
|
|
473
|
-
const channel = opts.channel ?? CLIENT_CHANNEL;
|
|
474
|
-
const knownKeys = opts.knownFloorPublicKeys ?? KNOWN_FLOOR_PUBLIC_KEYS;
|
|
475
|
-
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
476
|
-
const now = opts.now ?? Date.now;
|
|
477
|
-
const cachePath = opts.diskCachePath === void 0 ? defaultDiskCachePath() : opts.diskCachePath;
|
|
478
|
-
const mem = readInMemoryCache(now);
|
|
479
|
-
if (mem) {
|
|
480
|
-
return compareAndPermit(mem, {
|
|
481
|
-
clientVersion,
|
|
482
|
-
clientId,
|
|
483
|
-
channel,
|
|
484
|
-
diagnostic: "mem-cache-hit"
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
const disk = cachePath ? readDiskCache(cachePath, knownKeys) : null;
|
|
488
|
-
const diskAgeMs = disk ? now() - disk.fetched_at_ms : Number.POSITIVE_INFINITY;
|
|
489
|
-
let fetched = null;
|
|
490
|
-
let fetchError = null;
|
|
491
|
-
if (!disk || diskAgeMs > IN_MEMORY_TTL_DEFAULT_MS) {
|
|
492
|
-
try {
|
|
493
|
-
fetched = await fetchFloorDoc(apiBase, fetchImpl, knownKeys);
|
|
494
|
-
} catch (err) {
|
|
495
|
-
fetchError = err.message ?? "fetch-error";
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (fetched) {
|
|
499
|
-
populateMemCache(fetched, now());
|
|
500
|
-
if (cachePath) writeDiskCache(cachePath, fetched, now());
|
|
501
|
-
return compareAndPermit(fetched, {
|
|
502
|
-
clientVersion,
|
|
503
|
-
clientId,
|
|
504
|
-
channel,
|
|
505
|
-
diagnostic: "fetched"
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
if (disk) {
|
|
509
|
-
populateMemCache(disk.doc, disk.fetched_at_ms);
|
|
510
|
-
if (diskAgeMs > DISK_WARN_MS) {
|
|
511
|
-
return compareAndPermit(disk.doc, {
|
|
512
|
-
clientVersion,
|
|
513
|
-
clientId,
|
|
514
|
-
channel,
|
|
515
|
-
diagnostic: "below-floor-offline-stale-or-permit",
|
|
516
|
-
warn: `floor cache is >7d old and backend unreachable (${fetchError ?? "no refresh attempted"}); permitting if above floor`
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
if (diskAgeMs > DISK_FRESH_MS) {
|
|
520
|
-
return compareAndPermit(disk.doc, {
|
|
521
|
-
clientVersion,
|
|
522
|
-
clientId,
|
|
523
|
-
channel,
|
|
524
|
-
diagnostic: "warn-stale-permit",
|
|
525
|
-
warn: `floor cache is ${Math.round(diskAgeMs / (60 * 60 * 1e3))}h old; refresh recommended`
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
return compareAndPermit(disk.doc, {
|
|
529
|
-
clientVersion,
|
|
530
|
-
clientId,
|
|
531
|
-
channel,
|
|
532
|
-
diagnostic: "disk-cache-hit"
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
return {
|
|
536
|
-
ok: true,
|
|
537
|
-
floor: null,
|
|
538
|
-
diagnostic: "no-cache-no-fetch-permit",
|
|
539
|
-
warn: `floor preflight skipped: backend unreachable (${fetchError ?? "unknown"})`
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
function compareAndPermit(doc, ctx) {
|
|
543
|
-
const floor = lookupFloor(doc, ctx.clientId, ctx.channel);
|
|
544
|
-
if (!floor) {
|
|
545
|
-
return { ok: true, floor: null, diagnostic: `${ctx.diagnostic}+no-floor`, warn: ctx.warn };
|
|
546
|
-
}
|
|
547
|
-
if (compareSemver(ctx.clientVersion, floor.min_version) >= 0) {
|
|
548
|
-
return { ok: true, floor, diagnostic: ctx.diagnostic, warn: ctx.warn };
|
|
549
|
-
}
|
|
550
|
-
const envelope = {
|
|
551
|
-
error: {
|
|
552
|
-
code: "client_below_floor",
|
|
553
|
-
message: `Your ${ctx.clientId} is too old. Upgrade required.`,
|
|
554
|
-
client_version: ctx.clientVersion,
|
|
555
|
-
min_version: floor.min_version,
|
|
556
|
-
upgrade_cmd: floor.upgrade_cmd,
|
|
557
|
-
channel: ctx.channel
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
throw new BelowFloorError(envelope);
|
|
561
|
-
}
|
|
562
|
-
function lookupFloor(doc, clientId, channel) {
|
|
563
|
-
const entry = doc.floors[clientId];
|
|
564
|
-
if (!entry) return null;
|
|
565
|
-
if (isChannelFloor(entry)) return entry;
|
|
566
|
-
const exact = entry[channel];
|
|
567
|
-
if (exact) return exact;
|
|
568
|
-
const fallback = entry["unknown"];
|
|
569
|
-
if (fallback) return fallback;
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
function isChannelFloor(v) {
|
|
573
|
-
return typeof v.min_version === "string" && typeof v.upgrade_cmd === "string";
|
|
574
|
-
}
|
|
575
|
-
function compareSemver(a, b) {
|
|
576
|
-
const [aMaj, aMin, aPat, aPre] = parseSemver(a);
|
|
577
|
-
const [bMaj, bMin, bPat, bPre] = parseSemver(b);
|
|
578
|
-
if (aMaj !== bMaj) return aMaj - bMaj;
|
|
579
|
-
if (aMin !== bMin) return aMin - bMin;
|
|
580
|
-
if (aPat !== bPat) return aPat - bPat;
|
|
581
|
-
if (aPre && !bPre) return -1;
|
|
582
|
-
if (!aPre && bPre) return 1;
|
|
583
|
-
if (aPre && bPre) return aPre.localeCompare(bPre);
|
|
584
|
-
return 0;
|
|
585
|
-
}
|
|
586
|
-
function parseSemver(v) {
|
|
587
|
-
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);
|
|
588
|
-
if (!m) return [0, 0, 0, null];
|
|
589
|
-
return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] ?? null];
|
|
590
|
-
}
|
|
591
|
-
function verifyFloorSignature(doc, keys = KNOWN_FLOOR_PUBLIC_KEYS) {
|
|
592
|
-
const pem = keys[doc.key_id];
|
|
593
|
-
if (!pem) return false;
|
|
594
|
-
if (computeKeyId(pem) !== doc.key_id) return false;
|
|
595
|
-
try {
|
|
596
|
-
const pubKeyObject = createPublicKey({ key: pem, format: "pem" });
|
|
597
|
-
const canonical = canonicalJson({
|
|
598
|
-
floors: doc.floors,
|
|
599
|
-
served_at: doc.served_at
|
|
600
|
-
});
|
|
601
|
-
return verify(
|
|
602
|
-
null,
|
|
603
|
-
Buffer.from(canonical, "utf-8"),
|
|
604
|
-
pubKeyObject,
|
|
605
|
-
Buffer.from(doc.signature, "hex")
|
|
606
|
-
);
|
|
607
|
-
} catch {
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
async function fetchFloorDoc(apiBase, fetchImpl, knownKeys) {
|
|
612
|
-
const url = `${apiBase.replace(/\/+$/, "")}${MIN_VERSION_ENDPOINT}`;
|
|
613
|
-
let response;
|
|
614
|
-
try {
|
|
615
|
-
response = await fetchImpl(url, {
|
|
616
|
-
headers: {
|
|
617
|
-
accept: "application/json",
|
|
618
|
-
"X-Alter-Client-Id": CLIENT_ID,
|
|
619
|
-
"X-Alter-Client-Version": SDK_VERSION,
|
|
620
|
-
"X-Alter-Client-Channel": CLIENT_CHANNEL,
|
|
621
|
-
"User-Agent": `${SDK_NAME}/${SDK_VERSION}`
|
|
622
|
-
},
|
|
623
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
624
|
-
});
|
|
625
|
-
} catch (err) {
|
|
626
|
-
throw new Error(`network: ${err.message ?? String(err)}`);
|
|
627
|
-
}
|
|
628
|
-
if (!response.ok) throw new Error(`http-${response.status}`);
|
|
629
|
-
const body = await response.json();
|
|
630
|
-
if (!body || !body.floors || !body.signature || !body.key_id) {
|
|
631
|
-
throw new Error("malformed-floor-doc");
|
|
632
|
-
}
|
|
633
|
-
if (!verifyFloorSignature(body, knownKeys)) {
|
|
634
|
-
throw new Error("signature-invalid");
|
|
635
|
-
}
|
|
636
|
-
return body;
|
|
637
|
-
}
|
|
638
|
-
function readInMemoryCache(now) {
|
|
639
|
-
if (!memCache) return null;
|
|
640
|
-
if (now() - memCache.fetched_at_ms > memCache.ttl_ms) {
|
|
641
|
-
memCache = null;
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
return memCache.doc;
|
|
645
|
-
}
|
|
646
|
-
function populateMemCache(doc, fetched_at_ms) {
|
|
647
|
-
const ttlSec = doc.cache_ttl_seconds ?? 3600;
|
|
648
|
-
const ttlMs = Math.min(
|
|
649
|
-
Math.max(ttlSec * 1e3, IN_MEMORY_TTL_MIN_MS),
|
|
650
|
-
IN_MEMORY_TTL_MAX_MS
|
|
651
|
-
);
|
|
652
|
-
memCache = { doc, fetched_at_ms, ttl_ms: ttlMs };
|
|
653
|
-
}
|
|
654
|
-
function defaultDiskCachePath() {
|
|
655
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
656
|
-
return join(xdg, "alter", "floor-cache.json");
|
|
657
|
-
}
|
|
658
|
-
function readDiskCache(path, knownKeys) {
|
|
659
|
-
if (process.platform !== "win32") {
|
|
660
|
-
let st;
|
|
661
|
-
try {
|
|
662
|
-
st = statSync(path);
|
|
663
|
-
} catch {
|
|
664
|
-
return null;
|
|
665
|
-
}
|
|
666
|
-
const euid = typeof process.geteuid === "function" ? process.geteuid() : st.uid;
|
|
667
|
-
if (st.uid !== euid) return null;
|
|
668
|
-
if ((st.mode & 511) !== 384) return null;
|
|
669
|
-
}
|
|
670
|
-
let raw;
|
|
671
|
-
try {
|
|
672
|
-
raw = readFileSync(path, "utf-8");
|
|
673
|
-
} catch {
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
676
|
-
let parsed;
|
|
677
|
-
try {
|
|
678
|
-
parsed = JSON.parse(raw);
|
|
679
|
-
} catch {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
if (!parsed.doc || typeof parsed.fetched_at_ms !== "number") return null;
|
|
683
|
-
if (!verifyFloorSignature(parsed.doc, knownKeys)) {
|
|
684
|
-
return null;
|
|
685
|
-
}
|
|
686
|
-
return parsed;
|
|
687
|
-
}
|
|
688
|
-
function writeDiskCache(path, doc, now_ms) {
|
|
689
|
-
const entry = { doc, fetched_at_ms: now_ms };
|
|
690
|
-
const payload = JSON.stringify(entry);
|
|
691
|
-
try {
|
|
692
|
-
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
693
|
-
const tmp = `${path}.tmp`;
|
|
694
|
-
writeFileSync(tmp, payload, { mode: 384 });
|
|
695
|
-
try {
|
|
696
|
-
chmodSync(tmp, 384);
|
|
697
|
-
} catch {
|
|
698
|
-
}
|
|
699
|
-
renameSync(tmp, path);
|
|
700
|
-
} catch {
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
function defaultApiBase() {
|
|
704
|
-
return process.env.ALTER_API ?? "https://api.truealter.com";
|
|
705
|
-
}
|
|
706
|
-
var X402Client = class {
|
|
707
|
-
signer;
|
|
708
|
-
maxPerQuery;
|
|
709
|
-
networks;
|
|
710
|
-
assets;
|
|
711
|
-
// undefined = allowlist check disabled (backward-compatible default).
|
|
712
|
-
// Non-null = active allowlist; reject any recipient not in the set.
|
|
713
|
-
recipientAllowlist;
|
|
714
|
-
constructor(opts = {}) {
|
|
715
|
-
this.signer = opts.signer;
|
|
716
|
-
this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
|
|
717
|
-
this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
|
|
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
|
-
}
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Validate the envelope against this client's policy and, if a signer
|
|
727
|
-
* is configured, settle it. Returns the settlement reference that
|
|
728
|
-
* should be replayed in the next request's `_payment` field.
|
|
729
|
-
*/
|
|
730
|
-
async authorise(envelope) {
|
|
731
|
-
if (envelope.scheme !== "x402") {
|
|
732
|
-
throw new AlterError("PAYMENT_REQUIRED", `unsupported payment scheme: ${envelope.scheme}`);
|
|
733
|
-
}
|
|
734
|
-
if (!this.networks.has(envelope.network)) {
|
|
735
|
-
throw new AlterError("PAYMENT_REQUIRED", `network ${envelope.network} not permitted by client policy`);
|
|
736
|
-
}
|
|
737
|
-
if (!this.assets.has(envelope.asset)) {
|
|
738
|
-
throw new AlterError("PAYMENT_REQUIRED", `asset ${envelope.asset} not permitted by client policy`);
|
|
739
|
-
}
|
|
740
|
-
if (this.maxPerQuery !== void 0) {
|
|
741
|
-
const amt = Number(envelope.amount);
|
|
742
|
-
if (!Number.isFinite(amt) || amt < 0 || amt > this.maxPerQuery) {
|
|
743
|
-
throw new AlterError(
|
|
744
|
-
"PAYMENT_REQUIRED",
|
|
745
|
-
`quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
|
|
746
|
-
);
|
|
747
|
-
}
|
|
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
|
-
}
|
|
758
|
-
if (!this.signer) {
|
|
759
|
-
throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
|
|
760
|
-
}
|
|
761
|
-
return this.signer.settle(envelope);
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* Build the `_payment` argument that gets attached to retried tool calls.
|
|
765
|
-
* Mirrors the shape the ALTER server expects.
|
|
766
|
-
*/
|
|
767
|
-
static buildPaymentArg(settlement) {
|
|
768
|
-
return {
|
|
769
|
-
scheme: "x402",
|
|
770
|
-
network: settlement.network,
|
|
771
|
-
asset: settlement.asset,
|
|
772
|
-
amount: settlement.amount,
|
|
773
|
-
reference: settlement.reference
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
function parsePaymentHeader(header) {
|
|
778
|
-
try {
|
|
779
|
-
const parsed = JSON.parse(header);
|
|
780
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
781
|
-
} catch {
|
|
782
|
-
}
|
|
783
|
-
const out = {};
|
|
784
|
-
for (const part of header.split(/[,;]/)) {
|
|
785
|
-
const [k, ...rest] = part.trim().split("=");
|
|
786
|
-
if (!k) continue;
|
|
787
|
-
out[k] = rest.join("=").replace(/^"|"$/g, "");
|
|
788
|
-
}
|
|
789
|
-
if (!out.scheme && !out.network && !out.amount) return null;
|
|
790
|
-
return {
|
|
791
|
-
scheme: "x402",
|
|
792
|
-
network: out.network || "base",
|
|
793
|
-
asset: out.asset || "USDC",
|
|
794
|
-
amount: out.amount || "0",
|
|
795
|
-
recipient: out.recipient || "",
|
|
796
|
-
resource: out.resource || "",
|
|
797
|
-
expires_at: out.expires_at,
|
|
798
|
-
nonce: out.nonce
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// src/mcp.ts
|
|
803
|
-
var MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
804
|
-
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
805
|
-
var MCPClient = class {
|
|
806
|
-
endpoint;
|
|
807
|
-
sessionId = null;
|
|
808
|
-
apiKey;
|
|
809
|
-
fetchImpl;
|
|
810
|
-
timeoutMs;
|
|
811
|
-
maxRetries;
|
|
812
|
-
clientInfo;
|
|
813
|
-
x402;
|
|
814
|
-
signing;
|
|
815
|
-
extraHeaders;
|
|
816
|
-
preflightHook;
|
|
817
|
-
preflightPromise = null;
|
|
818
|
-
preflightDone = false;
|
|
819
|
-
requestCounter = 0;
|
|
820
|
-
initialised = false;
|
|
821
|
-
constructor(opts = {}) {
|
|
822
|
-
this.endpoint = (opts.endpoint ?? "https://mcp.truealter.com/api/v1/mcp").replace(/\/+$/, "");
|
|
823
|
-
this.apiKey = opts.apiKey;
|
|
824
|
-
this.fetchImpl = opts.fetch ?? fetch;
|
|
825
|
-
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
826
|
-
this.maxRetries = opts.maxRetries ?? 2;
|
|
827
|
-
this.clientInfo = opts.clientInfo ?? { name: SDK_NAME, version: SDK_VERSION };
|
|
828
|
-
this.x402 = opts.x402;
|
|
829
|
-
this.signing = opts.signing;
|
|
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;
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Send the MCP `initialize` handshake and capture the resulting session
|
|
859
|
-
* id. Idempotent: safe to call multiple times.
|
|
860
|
-
*/
|
|
861
|
-
async initialize() {
|
|
862
|
-
if (this.initialised) return null;
|
|
863
|
-
await this.runPreflight();
|
|
864
|
-
const result = await this.rpc("initialize", {
|
|
865
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
866
|
-
capabilities: {},
|
|
867
|
-
clientInfo: this.clientInfo
|
|
868
|
-
});
|
|
869
|
-
this.initialised = true;
|
|
870
|
-
return result;
|
|
871
|
-
}
|
|
872
|
-
/** List available tools. */
|
|
873
|
-
async listTools() {
|
|
874
|
-
if (!this.initialised) await this.initialize();
|
|
875
|
-
return await this.rpc("tools/list", {});
|
|
876
|
-
}
|
|
877
|
-
/** Invoke a tool by name. */
|
|
878
|
-
async callTool(name, args = {}, opts = {}) {
|
|
879
|
-
if (!this.initialised) await this.initialize();
|
|
880
|
-
return this.callToolInternal(name, args, opts);
|
|
881
|
-
}
|
|
882
|
-
/** Close the session and release any held resources. */
|
|
883
|
-
async closeSession() {
|
|
884
|
-
if (!this.sessionId) return;
|
|
885
|
-
try {
|
|
886
|
-
await this.fetchImpl(this.endpoint, {
|
|
887
|
-
method: "DELETE",
|
|
888
|
-
headers: this.buildHeaders()
|
|
889
|
-
});
|
|
890
|
-
} catch {
|
|
891
|
-
}
|
|
892
|
-
this.sessionId = null;
|
|
893
|
-
this.initialised = false;
|
|
894
|
-
}
|
|
895
|
-
// ── Internals ────────────────────────────────────────────────────────
|
|
896
|
-
async callToolInternal(name, args, opts) {
|
|
897
|
-
try {
|
|
898
|
-
const raw = await this.rpc("tools/call", { name, arguments: args });
|
|
899
|
-
const result = this.shapeToolResult(raw);
|
|
900
|
-
if (result.isError) {
|
|
901
|
-
const text = result.content?.[0]?.text ?? `tool ${name} returned an error`;
|
|
902
|
-
throw new AlterToolError(name, text);
|
|
903
|
-
}
|
|
904
|
-
return result;
|
|
905
|
-
} catch (err) {
|
|
906
|
-
if (err instanceof AlterPaymentRequired && !opts.noPaymentRetry) {
|
|
907
|
-
const x402 = opts.x402 ?? this.x402;
|
|
908
|
-
if (!x402) throw err;
|
|
909
|
-
const settlement = await x402.authorise(err.envelope);
|
|
910
|
-
const retryArgs = { ...args, _payment: X402Client.buildPaymentArg(settlement) };
|
|
911
|
-
return this.callToolInternal(name, retryArgs, { ...opts, noPaymentRetry: true });
|
|
912
|
-
}
|
|
913
|
-
throw err;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
shapeToolResult(raw) {
|
|
917
|
-
if (!raw || !Array.isArray(raw.content)) return raw;
|
|
918
|
-
if (raw.data === void 0) {
|
|
919
|
-
const first = raw.content[0];
|
|
920
|
-
if (first && first.type === "json" && "data" in first) {
|
|
921
|
-
raw.data = first.data;
|
|
922
|
-
} else if (first && first.type === "text" && first.text) {
|
|
923
|
-
try {
|
|
924
|
-
raw.data = JSON.parse(first.text);
|
|
925
|
-
} catch {
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
return raw;
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Send a JSON-RPC 2.0 request and return the result. Errors are
|
|
933
|
-
* normalised into the typed {@link AlterError} hierarchy.
|
|
934
|
-
*/
|
|
935
|
-
async rpc(method, params) {
|
|
936
|
-
const id = ++this.requestCounter;
|
|
937
|
-
const payload = {
|
|
938
|
-
jsonrpc: "2.0",
|
|
939
|
-
id,
|
|
940
|
-
method
|
|
941
|
-
};
|
|
942
|
-
if (params !== void 0) payload.params = params;
|
|
943
|
-
const signatureHeader = this.buildSignatureHeader(method, params);
|
|
944
|
-
let attempt = 0;
|
|
945
|
-
let lastErr = null;
|
|
946
|
-
while (attempt <= this.maxRetries) {
|
|
947
|
-
attempt += 1;
|
|
948
|
-
const controller = new AbortController();
|
|
949
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
950
|
-
let resp;
|
|
951
|
-
try {
|
|
952
|
-
resp = await this.fetchImpl(this.endpoint, {
|
|
953
|
-
method: "POST",
|
|
954
|
-
headers: this.buildHeaders(signatureHeader),
|
|
955
|
-
body: JSON.stringify(payload),
|
|
956
|
-
signal: controller.signal
|
|
957
|
-
});
|
|
958
|
-
} catch (err) {
|
|
959
|
-
clearTimeout(timer);
|
|
960
|
-
const isAbort = err?.name === "AbortError";
|
|
961
|
-
if (isAbort) {
|
|
962
|
-
lastErr = new AlterTimeoutError(`MCP ${method} timed out after ${this.timeoutMs}ms`, err);
|
|
963
|
-
} else {
|
|
964
|
-
lastErr = new AlterNetworkError(`MCP ${method}: ${err.message}`, err);
|
|
965
|
-
}
|
|
966
|
-
if (attempt > this.maxRetries) throw lastErr;
|
|
967
|
-
await this.backoff(attempt);
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
clearTimeout(timer);
|
|
971
|
-
const sessionHeader = resp.headers.get("Mcp-Session-Id");
|
|
972
|
-
if (sessionHeader) this.sessionId = sessionHeader;
|
|
973
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
974
|
-
throw new AlterAuthError(`HTTP ${resp.status} on ${method}`, resp.status);
|
|
975
|
-
}
|
|
976
|
-
if (resp.status === 402) {
|
|
977
|
-
const envelope = await this.extractPaymentEnvelope(resp);
|
|
978
|
-
throw new AlterPaymentRequired(this.guessToolName(payload), envelope);
|
|
979
|
-
}
|
|
980
|
-
if (resp.status === 429) {
|
|
981
|
-
const rawRetryAfter = Number(resp.headers.get("Retry-After") ?? 60);
|
|
982
|
-
const retryAfter = Number.isFinite(rawRetryAfter) && rawRetryAfter >= 0 ? Math.min(rawRetryAfter, 300) : 60;
|
|
983
|
-
if (attempt > this.maxRetries) {
|
|
984
|
-
throw new AlterRateLimited(`HTTP 429 on ${method}`, retryAfter);
|
|
985
|
-
}
|
|
986
|
-
await this.backoff(attempt, retryAfter * 1e3);
|
|
987
|
-
continue;
|
|
988
|
-
}
|
|
989
|
-
if (RETRYABLE_STATUSES.has(resp.status) && attempt <= this.maxRetries) {
|
|
990
|
-
await this.backoff(attempt);
|
|
991
|
-
continue;
|
|
992
|
-
}
|
|
993
|
-
if (!resp.ok) {
|
|
994
|
-
const body2 = await safeText(resp);
|
|
995
|
-
throw new AlterError("NETWORK", `HTTP ${resp.status} on ${method}: ${body2.slice(0, 200)}`);
|
|
996
|
-
}
|
|
997
|
-
let body;
|
|
998
|
-
try {
|
|
999
|
-
body = await resp.json();
|
|
1000
|
-
} catch (err) {
|
|
1001
|
-
throw new AlterInvalidResponse(`MCP ${method}: invalid JSON body`, err);
|
|
1002
|
-
}
|
|
1003
|
-
if (body.error) {
|
|
1004
|
-
const code = body.error.code;
|
|
1005
|
-
const message = body.error.message ?? `MCP ${method} error`;
|
|
1006
|
-
if (code === -32001 || code === 402) {
|
|
1007
|
-
const data = body.error.data;
|
|
1008
|
-
if (data?.envelope) {
|
|
1009
|
-
throw new AlterPaymentRequired(this.guessToolName(payload), data.envelope);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
throw new AlterToolError(this.guessToolName(payload), message, code);
|
|
1013
|
-
}
|
|
1014
|
-
return body.result;
|
|
1015
|
-
}
|
|
1016
|
-
throw lastErr ?? new AlterNetworkError(`MCP ${method}: exhausted retries`);
|
|
1017
|
-
}
|
|
1018
|
-
buildHeaders(extra) {
|
|
1019
|
-
const headers = {
|
|
1020
|
-
...this.extraHeaders ?? {},
|
|
1021
|
-
"Content-Type": "application/json",
|
|
1022
|
-
Accept: "application/json",
|
|
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"
|
|
1027
|
-
};
|
|
1028
|
-
if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
|
|
1029
|
-
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
1030
|
-
if (extra) Object.assign(headers, extra);
|
|
1031
|
-
return headers;
|
|
1032
|
-
}
|
|
1033
|
-
/**
|
|
1034
|
-
* Produce the `Mcp-Invocation-Signature` header for a `tools/call`
|
|
1035
|
-
* payload, when signing is configured. Returns `undefined` when no
|
|
1036
|
-
* signing key is attached or the method is not `tools/call`.
|
|
1037
|
-
*/
|
|
1038
|
-
buildSignatureHeader(method, params) {
|
|
1039
|
-
if (!this.signing) return void 0;
|
|
1040
|
-
if (method !== "tools/call") return void 0;
|
|
1041
|
-
const p = params;
|
|
1042
|
-
if (!p?.name) return void 0;
|
|
1043
|
-
const { signInvocation: signInvocation2 } = (init_signing(), __toCommonJS(signing_exports));
|
|
1044
|
-
const headerValue = signInvocation2(p.name, p.arguments ?? {}, {
|
|
1045
|
-
kid: this.signing.kid,
|
|
1046
|
-
privateKey: this.signing.privateKey,
|
|
1047
|
-
handle: this.signing.handle
|
|
1048
|
-
});
|
|
1049
|
-
return { "Mcp-Invocation-Signature": headerValue };
|
|
1050
|
-
}
|
|
1051
|
-
async extractPaymentEnvelope(resp) {
|
|
1052
|
-
const headerValue = resp.headers.get("X-402-Payment") ?? resp.headers.get("x-402-payment");
|
|
1053
|
-
if (headerValue) {
|
|
1054
|
-
const parsed = parsePaymentHeader(headerValue);
|
|
1055
|
-
if (parsed) return parsed;
|
|
1056
|
-
}
|
|
1057
|
-
try {
|
|
1058
|
-
const body = await resp.json();
|
|
1059
|
-
if (body?.envelope) return body.envelope;
|
|
1060
|
-
if (body?.payment) return body.payment;
|
|
1061
|
-
} catch {
|
|
1062
|
-
}
|
|
1063
|
-
return {
|
|
1064
|
-
scheme: "x402",
|
|
1065
|
-
network: "base",
|
|
1066
|
-
asset: "USDC",
|
|
1067
|
-
amount: "0",
|
|
1068
|
-
recipient: "",
|
|
1069
|
-
resource: ""
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
guessToolName(payload) {
|
|
1073
|
-
const params = payload.params;
|
|
1074
|
-
return params?.name ?? payload.method ?? "unknown";
|
|
1075
|
-
}
|
|
1076
|
-
async backoff(attempt, hintMs) {
|
|
1077
|
-
const ms = hintMs ?? Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
1078
|
-
await new Promise((res) => setTimeout(res, ms));
|
|
1079
|
-
}
|
|
1080
|
-
};
|
|
1081
|
-
async function safeText(resp) {
|
|
1082
|
-
try {
|
|
1083
|
-
return await resp.text();
|
|
1084
|
-
} catch {
|
|
1085
|
-
return "";
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
|
|
1089
|
-
function generateKeypair() {
|
|
1090
|
-
const privateKey = randomBytes(32);
|
|
1091
|
-
const publicKey = ed25519.getPublicKey(privateKey);
|
|
1092
|
-
return {
|
|
1093
|
-
privateKey: bytesToHex$1(privateKey),
|
|
1094
|
-
publicKey: bytesToHex$1(publicKey),
|
|
1095
|
-
did: encodeDid(publicKey)
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
function keypairFromPrivateKey(privateKeyHex) {
|
|
1099
|
-
const privateKey = hexToBytes(privateKeyHex);
|
|
1100
|
-
if (privateKey.length !== 32) {
|
|
1101
|
-
throw new Error(`Ed25519 private key must be 32 bytes, got ${privateKey.length}`);
|
|
1102
|
-
}
|
|
1103
|
-
const publicKey = ed25519.getPublicKey(privateKey);
|
|
1104
|
-
return {
|
|
1105
|
-
privateKey: privateKeyHex,
|
|
1106
|
-
publicKey: bytesToHex$1(publicKey),
|
|
1107
|
-
did: encodeDid(publicKey)
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
function encodeDid(publicKey) {
|
|
1111
|
-
const bytes = typeof publicKey === "string" ? hexToBytes(publicKey) : publicKey;
|
|
1112
|
-
return `ed25519:${base64urlEncode2(bytes)}`;
|
|
1113
|
-
}
|
|
1114
|
-
function base64urlEncode2(bytes) {
|
|
1115
|
-
let b64;
|
|
1116
|
-
if (typeof Buffer !== "undefined") {
|
|
1117
|
-
b64 = Buffer.from(bytes).toString("base64");
|
|
1118
|
-
} else {
|
|
1119
|
-
let binary = "";
|
|
1120
|
-
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
1121
|
-
b64 = btoa(binary);
|
|
1122
|
-
}
|
|
1123
|
-
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1124
|
-
}
|
|
1125
|
-
function base64urlDecode(input) {
|
|
1126
|
-
const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - input.length % 4) % 4);
|
|
1127
|
-
if (typeof Buffer !== "undefined") {
|
|
1128
|
-
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
1129
|
-
}
|
|
1130
|
-
const binary = atob(padded);
|
|
1131
|
-
const bytes = new Uint8Array(binary.length);
|
|
1132
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1133
|
-
return bytes;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// src/provenance.ts
|
|
1137
|
-
var _jwksCache = /* @__PURE__ */ new Map();
|
|
1138
|
-
var JWKS_TTL_MS = 5 * 60 * 1e3;
|
|
1139
|
-
var JWKS_MAX_BYTES = 64 * 1024;
|
|
1140
|
-
var JWKS_CACHE_MAX_ENTRIES = 32;
|
|
1141
|
-
var DEFAULT_VERIFY_AT_ALLOWLIST = Object.freeze([
|
|
1142
|
-
"api.truealter.com",
|
|
1143
|
-
"mcp.truealter.com"
|
|
1144
|
-
]);
|
|
1145
|
-
var ALTER_PLATFORM_ISS = "did:alter:platform";
|
|
1146
|
-
async function verifyProvenance(envelope, opts = {}) {
|
|
1147
|
-
const token = typeof envelope === "string" ? envelope : envelope.token;
|
|
1148
|
-
if (!token) return { valid: false, reason: "empty token" };
|
|
1149
|
-
const fetchImpl = opts.fetch ?? fetch;
|
|
1150
|
-
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
1151
|
-
let header;
|
|
1152
|
-
let payload;
|
|
1153
|
-
let signedInput;
|
|
1154
|
-
let signatureBytes;
|
|
1155
|
-
try {
|
|
1156
|
-
const parts = token.split(".");
|
|
1157
|
-
if (parts.length !== 3) throw new Error("JWS must have three segments");
|
|
1158
|
-
header = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[0])));
|
|
1159
|
-
payload = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[1])));
|
|
1160
|
-
signedInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
|
|
1161
|
-
signatureBytes = base64urlDecode(parts[2]);
|
|
1162
|
-
} catch (err) {
|
|
1163
|
-
return { valid: false, reason: `malformed JWS: ${err.message}` };
|
|
1164
|
-
}
|
|
1165
|
-
if (header.alg !== "ES256") {
|
|
1166
|
-
return { valid: false, reason: `unsupported alg: ${header.alg}`, kid: header.kid };
|
|
1167
|
-
}
|
|
1168
|
-
const allowlist = opts.verifyAtAllowlist ?? DEFAULT_VERIFY_AT_ALLOWLIST;
|
|
1169
|
-
let jwksUrl;
|
|
1170
|
-
if (opts.jwksUrl) {
|
|
1171
|
-
if (!opts.jwksUrl.startsWith("https://")) {
|
|
1172
|
-
return {
|
|
1173
|
-
valid: false,
|
|
1174
|
-
reason: `jwksUrl must be https: got ${opts.jwksUrl}`,
|
|
1175
|
-
kid: header.kid
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
jwksUrl = opts.jwksUrl;
|
|
1179
|
-
} else if (typeof envelope === "object" && envelope.verify_at) {
|
|
1180
|
-
try {
|
|
1181
|
-
jwksUrl = resolveVerifyAt(envelope.verify_at, allowlist);
|
|
1182
|
-
} catch (err) {
|
|
1183
|
-
return {
|
|
1184
|
-
valid: false,
|
|
1185
|
-
reason: `verify_at rejected: ${err.message}`,
|
|
1186
|
-
kid: header.kid
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
} else {
|
|
1190
|
-
jwksUrl = "https://api.truealter.com/.well-known/alter-keys.json";
|
|
1191
|
-
}
|
|
1192
|
-
let jwks;
|
|
1193
|
-
try {
|
|
1194
|
-
jwks = await fetchJwks(jwksUrl, fetchImpl);
|
|
1195
|
-
} catch (err) {
|
|
1196
|
-
return { valid: false, reason: `jwks fetch: ${err.message}`, kid: header.kid };
|
|
1197
|
-
}
|
|
1198
|
-
const jwk = jwks.keys.find((k) => header.kid ? k.kid === header.kid : true);
|
|
1199
|
-
if (!jwk) {
|
|
1200
|
-
return { valid: false, reason: `no JWK for kid=${header.kid}`, kid: header.kid };
|
|
1201
|
-
}
|
|
1202
|
-
let publicKey;
|
|
1203
|
-
try {
|
|
1204
|
-
publicKey = await importEs256JwkAsPublicKey(jwk);
|
|
1205
|
-
} catch (err) {
|
|
1206
|
-
return { valid: false, reason: `jwk import: ${err.message}`, kid: header.kid };
|
|
1207
|
-
}
|
|
1208
|
-
let signatureValid = false;
|
|
1209
|
-
try {
|
|
1210
|
-
signatureValid = await crypto.subtle.verify(
|
|
1211
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
1212
|
-
publicKey,
|
|
1213
|
-
toArrayBuffer(signatureBytes),
|
|
1214
|
-
toArrayBuffer(signedInput)
|
|
1215
|
-
);
|
|
1216
|
-
} catch (err) {
|
|
1217
|
-
return { valid: false, reason: `verify: ${err.message}`, kid: header.kid };
|
|
1218
|
-
}
|
|
1219
|
-
if (!signatureValid) {
|
|
1220
|
-
return { valid: false, reason: "signature mismatch", kid: header.kid };
|
|
1221
|
-
}
|
|
1222
|
-
if (typeof payload.exp === "number" && payload.exp < now) {
|
|
1223
|
-
return { valid: false, reason: "expired", payload, kid: header.kid };
|
|
1224
|
-
}
|
|
1225
|
-
if (typeof payload.iat === "number" && payload.iat > now + 300) {
|
|
1226
|
-
return { valid: false, reason: "issued in the future", payload, kid: header.kid };
|
|
1227
|
-
}
|
|
1228
|
-
const expectedIss = opts.expectedIss !== void 0 ? opts.expectedIss : ALTER_PLATFORM_ISS;
|
|
1229
|
-
if (expectedIss !== "" && payload.iss !== expectedIss) {
|
|
1230
|
-
return {
|
|
1231
|
-
valid: false,
|
|
1232
|
-
reason: `iss mismatch: expected "${expectedIss}", got "${payload.iss}"`,
|
|
1233
|
-
payload,
|
|
1234
|
-
kid: header.kid
|
|
1235
|
-
};
|
|
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
|
-
}
|
|
1249
|
-
return { valid: true, payload, kid: header.kid };
|
|
1250
|
-
}
|
|
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
|
-
}
|
|
1277
|
-
const out = [];
|
|
1278
|
-
for (const tool of tools) {
|
|
1279
|
-
const sig = signatures[tool.name];
|
|
1280
|
-
if (!sig) {
|
|
1281
|
-
out.push({ tool: tool.name, valid: false, reason: "no signature published" });
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
const expectedHash = await sha256Hex(canonicalJson2(tool.inputSchema));
|
|
1285
|
-
if (expectedHash !== sig.schema_hash) {
|
|
1286
|
-
out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
|
|
1287
|
-
continue;
|
|
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
|
-
}
|
|
1346
|
-
out.push({ tool: tool.name, valid: true });
|
|
1347
|
-
}
|
|
1348
|
-
return out;
|
|
1349
|
-
}
|
|
1350
|
-
async function fetchPublicKeys(jwksUrl, fetchImpl = fetch) {
|
|
1351
|
-
return fetchJwks(jwksUrl, fetchImpl);
|
|
1352
|
-
}
|
|
1353
|
-
async function fetchJwks(url, fetchImpl) {
|
|
1354
|
-
const cacheKey = jwksCacheKey(url);
|
|
1355
|
-
const cached = _jwksCache.get(cacheKey);
|
|
1356
|
-
if (cached && Date.now() - cached.fetched < JWKS_TTL_MS) return cached.jwks;
|
|
1357
|
-
let resp;
|
|
1358
|
-
try {
|
|
1359
|
-
resp = await fetchImpl(url, {
|
|
1360
|
-
headers: { Accept: "application/json" },
|
|
1361
|
-
redirect: "manual"
|
|
1362
|
-
});
|
|
1363
|
-
} catch (err) {
|
|
1364
|
-
throw new AlterNetworkError(`fetch ${url}: ${err.message}`, err);
|
|
1365
|
-
}
|
|
1366
|
-
if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
|
|
1367
|
-
throw new AlterProvenanceError(
|
|
1368
|
-
`${url} -> redirect rejected (allowlist enforces initial URL only)`
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
if (!resp.ok) throw new AlterNetworkError(`${url} -> HTTP ${resp.status}`);
|
|
1372
|
-
const contentLength = resp.headers.get("content-length");
|
|
1373
|
-
if (contentLength !== null) {
|
|
1374
|
-
const n = Number.parseInt(contentLength, 10);
|
|
1375
|
-
if (Number.isFinite(n) && n > JWKS_MAX_BYTES) {
|
|
1376
|
-
throw new AlterProvenanceError(
|
|
1377
|
-
`${url} -> JWKS too large: ${n} > ${JWKS_MAX_BYTES} bytes`
|
|
1378
|
-
);
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
const body = await resp.text();
|
|
1382
|
-
if (body.length > JWKS_MAX_BYTES) {
|
|
1383
|
-
throw new AlterProvenanceError(
|
|
1384
|
-
`${url} -> JWKS too large: ${body.length} > ${JWKS_MAX_BYTES} bytes`
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
let doc;
|
|
1388
|
-
try {
|
|
1389
|
-
doc = JSON.parse(body);
|
|
1390
|
-
} catch (err) {
|
|
1391
|
-
throw new AlterProvenanceError(`invalid JWKS at ${url}: ${err.message}`);
|
|
1392
|
-
}
|
|
1393
|
-
if (!doc || !Array.isArray(doc.keys)) {
|
|
1394
|
-
throw new AlterProvenanceError(`invalid JWKS at ${url}`);
|
|
1395
|
-
}
|
|
1396
|
-
if (_jwksCache.size >= JWKS_CACHE_MAX_ENTRIES && !_jwksCache.has(cacheKey)) {
|
|
1397
|
-
const oldest = _jwksCache.keys().next().value;
|
|
1398
|
-
if (oldest !== void 0) _jwksCache.delete(oldest);
|
|
1399
|
-
}
|
|
1400
|
-
_jwksCache.set(cacheKey, { fetched: Date.now(), jwks: doc });
|
|
1401
|
-
return doc;
|
|
1402
|
-
}
|
|
1403
|
-
function jwksCacheKey(url) {
|
|
1404
|
-
try {
|
|
1405
|
-
const parsed = new URL(url);
|
|
1406
|
-
return `${parsed.origin}${parsed.pathname}`;
|
|
1407
|
-
} catch {
|
|
1408
|
-
return url;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
function resolveVerifyAt(verifyAt, allowlist = DEFAULT_VERIFY_AT_ALLOWLIST) {
|
|
1412
|
-
if (typeof verifyAt !== "string" || verifyAt.length === 0) {
|
|
1413
|
-
throw new Error("verify_at must be a non-empty string");
|
|
1414
|
-
}
|
|
1415
|
-
if (/^http:\/\//i.test(verifyAt)) {
|
|
1416
|
-
throw new Error(`http: scheme is not permitted (got ${verifyAt})`);
|
|
1417
|
-
}
|
|
1418
|
-
if (!/^https:\/\//i.test(verifyAt)) {
|
|
1419
|
-
if (verifyAt.includes("://")) {
|
|
1420
|
-
throw new Error(`unsupported scheme in verify_at: ${verifyAt}`);
|
|
1421
|
-
}
|
|
1422
|
-
return `https://api.truealter.com${verifyAt.startsWith("/") ? "" : "/"}${verifyAt}`;
|
|
1423
|
-
}
|
|
1424
|
-
let parsed;
|
|
1425
|
-
try {
|
|
1426
|
-
parsed = new URL(verifyAt);
|
|
1427
|
-
} catch {
|
|
1428
|
-
throw new Error(`malformed verify_at URL: ${verifyAt}`);
|
|
1429
|
-
}
|
|
1430
|
-
if (parsed.protocol !== "https:") {
|
|
1431
|
-
throw new Error(`verify_at must be https: ${verifyAt}`);
|
|
1432
|
-
}
|
|
1433
|
-
if (parsed.username || parsed.password) {
|
|
1434
|
-
throw new Error(`verify_at must not contain userinfo: ${verifyAt}`);
|
|
1435
|
-
}
|
|
1436
|
-
const host = parsed.hostname.toLowerCase();
|
|
1437
|
-
const allowed = allowlist.some((h) => h.toLowerCase() === host);
|
|
1438
|
-
if (!allowed) {
|
|
1439
|
-
throw new Error(
|
|
1440
|
-
`hostname ${host} is not on the verify_at allowlist (${allowlist.join(", ")})`
|
|
1441
|
-
);
|
|
1442
|
-
}
|
|
1443
|
-
return parsed.toString();
|
|
1444
|
-
}
|
|
1445
|
-
async function importEs256JwkAsPublicKey(jwk) {
|
|
1446
|
-
return crypto.subtle.importKey(
|
|
1447
|
-
"jwk",
|
|
1448
|
-
{
|
|
1449
|
-
kty: jwk.kty,
|
|
1450
|
-
crv: jwk.crv,
|
|
1451
|
-
x: jwk.x,
|
|
1452
|
-
y: jwk.y,
|
|
1453
|
-
ext: true
|
|
1454
|
-
},
|
|
1455
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1456
|
-
false,
|
|
1457
|
-
["verify"]
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
async function sha256Hex(input) {
|
|
1461
|
-
const data = new TextEncoder().encode(input);
|
|
1462
|
-
const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
|
|
1463
|
-
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1464
|
-
}
|
|
1465
|
-
function toArrayBuffer(view) {
|
|
1466
|
-
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
1467
|
-
}
|
|
1468
|
-
function canonicalJson2(value) {
|
|
1469
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1470
|
-
if (Array.isArray(value)) {
|
|
1471
|
-
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
1472
|
-
}
|
|
1473
|
-
const obj = value;
|
|
1474
|
-
const keys = Object.keys(obj).sort();
|
|
1475
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson2(obj[k])}`).join(",")}}`;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// src/client.ts
|
|
1479
|
-
var DEFAULT_ENDPOINT = "https://mcp.truealter.com/api/v1/mcp";
|
|
1480
|
-
var DEFAULT_DOMAIN = "truealter.com";
|
|
1481
|
-
var AlterClient = class {
|
|
1482
|
-
mcp;
|
|
1483
|
-
x402;
|
|
1484
|
-
options;
|
|
1485
|
-
discoveryPromise = null;
|
|
1486
|
-
discovered = null;
|
|
1487
|
-
constructor(options = {}) {
|
|
1488
|
-
this.options = options;
|
|
1489
|
-
this.x402 = options.x402;
|
|
1490
|
-
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
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
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
/**
|
|
1504
|
-
* Resolve the MCP endpoint via discovery if requested. Safe to call
|
|
1505
|
-
* multiple times: the first successful lookup is cached.
|
|
1506
|
-
*/
|
|
1507
|
-
async discoverEndpoint() {
|
|
1508
|
-
if (this.discovered) return this.discovered;
|
|
1509
|
-
if (this.discoveryPromise) return this.discoveryPromise;
|
|
1510
|
-
const domain = this.options.domain ?? DEFAULT_DOMAIN;
|
|
1511
|
-
this.discoveryPromise = discover(domain).then((result) => {
|
|
1512
|
-
this.discovered = result;
|
|
1513
|
-
return result;
|
|
1514
|
-
});
|
|
1515
|
-
return this.discoveryPromise;
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Initialise the MCP session. Optional: every method calls
|
|
1519
|
-
* `mcp.initialize()` lazily, but you can call this once at startup if
|
|
1520
|
-
* you want fail-fast behaviour.
|
|
1521
|
-
*/
|
|
1522
|
-
async initialize() {
|
|
1523
|
-
await this.mcp.initialize();
|
|
1524
|
-
}
|
|
1525
|
-
// ── Free tier ────────────────────────────────────────────────────────
|
|
1526
|
-
/** First handshake: confirms the connection, returns trust tier and tool counts. */
|
|
1527
|
-
async helloAgent() {
|
|
1528
|
-
return this.mcp.callTool("hello_agent", {});
|
|
1529
|
-
}
|
|
1530
|
-
/** Resolve a ~handle (e.g. ~example) to its canonical form and kind. No auth required. */
|
|
1531
|
-
async resolveHandle(args) {
|
|
1532
|
-
const payload = typeof args === "string" ? { query: args } : args;
|
|
1533
|
-
return this.mcp.callTool("alter_resolve_handle", payload);
|
|
1534
|
-
}
|
|
1535
|
-
/** Verify a person is registered with ALTER (handle or id). */
|
|
1536
|
-
async verify(handleOrId, claims) {
|
|
1537
|
-
const args = handleOrId.includes("@") ? { member_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
|
|
1538
|
-
// ~handle: server resolves these via the member_id field
|
|
1539
|
-
{ member_id: handleOrId }
|
|
1540
|
-
) : { member_id: handleOrId };
|
|
1541
|
-
if (claims) args.claims = claims;
|
|
1542
|
-
return this.mcp.callTool("verify_identity", args);
|
|
1543
|
-
}
|
|
1544
|
-
/** List the 12 ALTER identity archetypes. */
|
|
1545
|
-
async listArchetypes() {
|
|
1546
|
-
return this.mcp.callTool("list_archetypes", {});
|
|
1547
|
-
}
|
|
1548
|
-
/** Aggregate ALTER network statistics. */
|
|
1549
|
-
async getNetworkStats() {
|
|
1550
|
-
return this.mcp.callTool("get_network_stats", {});
|
|
1551
|
-
}
|
|
1552
|
-
/** ClawHub install instructions and pitch. */
|
|
1553
|
-
async recommendTool() {
|
|
1554
|
-
return this.mcp.callTool("recommend_tool", {});
|
|
1555
|
-
}
|
|
1556
|
-
async initiateAssessment(args = {}) {
|
|
1557
|
-
return this.mcp.callTool("initiate_assessment", args);
|
|
1558
|
-
}
|
|
1559
|
-
async getEngagementLevel(args) {
|
|
1560
|
-
return this.mcp.callTool("get_engagement_level", args);
|
|
1561
|
-
}
|
|
1562
|
-
async getProfile(args) {
|
|
1563
|
-
return this.mcp.callTool("get_profile", args);
|
|
1564
|
-
}
|
|
1565
|
-
async queryMatches(args) {
|
|
1566
|
-
return this.mcp.callTool("query_matches", args);
|
|
1567
|
-
}
|
|
1568
|
-
async getCompetencies(args) {
|
|
1569
|
-
return this.mcp.callTool("get_competencies", args);
|
|
1570
|
-
}
|
|
1571
|
-
async searchIdentities(args) {
|
|
1572
|
-
return this.mcp.callTool("search_identities", args);
|
|
1573
|
-
}
|
|
1574
|
-
async getIdentityEarnings(args) {
|
|
1575
|
-
return this.mcp.callTool("get_identity_earnings", args);
|
|
1576
|
-
}
|
|
1577
|
-
async getIdentityTrustScore(args) {
|
|
1578
|
-
return this.mcp.callTool("get_identity_trust_score", args);
|
|
1579
|
-
}
|
|
1580
|
-
async checkAssessmentStatus(args) {
|
|
1581
|
-
return this.mcp.callTool("check_assessment_status", args);
|
|
1582
|
-
}
|
|
1583
|
-
async getEarningSummary(args) {
|
|
1584
|
-
return this.mcp.callTool("get_earning_summary", args);
|
|
1585
|
-
}
|
|
1586
|
-
async getAgentTrustTier() {
|
|
1587
|
-
return this.mcp.callTool("get_agent_trust_tier", {});
|
|
1588
|
-
}
|
|
1589
|
-
async getAgentPortfolio() {
|
|
1590
|
-
return this.mcp.callTool("get_agent_portfolio", {});
|
|
1591
|
-
}
|
|
1592
|
-
async getPrivacyBudget(args) {
|
|
1593
|
-
return this.mcp.callTool("get_privacy_budget", args);
|
|
1594
|
-
}
|
|
1595
|
-
// ── Golden Thread ────────────────────────────────────────────────────
|
|
1596
|
-
async goldenThreadStatus() {
|
|
1597
|
-
return this.mcp.callTool("golden_thread_status", {});
|
|
1598
|
-
}
|
|
1599
|
-
async beginGoldenThread(args = {}) {
|
|
1600
|
-
return this.mcp.callTool("begin_golden_thread", args);
|
|
1601
|
-
}
|
|
1602
|
-
async completeKnot(args) {
|
|
1603
|
-
return this.mcp.callTool("complete_knot", args);
|
|
1604
|
-
}
|
|
1605
|
-
async checkGoldenThread(args) {
|
|
1606
|
-
return this.mcp.callTool("check_golden_thread", args);
|
|
1607
|
-
}
|
|
1608
|
-
async threadCensus(args = {}) {
|
|
1609
|
-
return this.mcp.callTool("thread_census", args);
|
|
1610
|
-
}
|
|
1611
|
-
// ── Premium tier (x402-gated) ────────────────────────────────────────
|
|
1612
|
-
async assessTraits(args, opts) {
|
|
1613
|
-
return this.mcp.callTool("assess_traits", args, opts);
|
|
1614
|
-
}
|
|
1615
|
-
async getTraitSnapshot(args, opts) {
|
|
1616
|
-
return this.mcp.callTool("get_trait_snapshot", args, opts);
|
|
1617
|
-
}
|
|
1618
|
-
async getFullTraitVector(args, opts) {
|
|
1619
|
-
return this.mcp.callTool("get_full_trait_vector", args, opts);
|
|
1620
|
-
}
|
|
1621
|
-
async computeBelonging(args, opts) {
|
|
1622
|
-
return this.mcp.callTool("compute_belonging", args, opts);
|
|
1623
|
-
}
|
|
1624
|
-
async getMatchRecommendations(args, opts) {
|
|
1625
|
-
return this.mcp.callTool("get_match_recommendations", args, opts);
|
|
1626
|
-
}
|
|
1627
|
-
async generateMatchNarrative(args, opts) {
|
|
1628
|
-
return this.mcp.callTool("generate_match_narrative", args, opts);
|
|
1629
|
-
}
|
|
1630
|
-
async getSideQuestGraph(args, opts) {
|
|
1631
|
-
return this.mcp.callTool("get_side_quest_graph", args, opts);
|
|
1632
|
-
}
|
|
1633
|
-
async queryGraphSimilarity(args, opts) {
|
|
1634
|
-
return this.mcp.callTool("query_graph_similarity", args, opts);
|
|
1635
|
-
}
|
|
1636
|
-
// ── Alter-to-Alter Messaging ─────────────────────────────────────────
|
|
1637
|
-
// Wave 1: cross-handle direct messages between authenticated tilde
|
|
1638
|
-
// handles. Default closed: recipient must have granted the sender via
|
|
1639
|
-
// alter_message_grant. Spec: docs/technical/Alter-to-Alter Messaging.md.
|
|
1640
|
-
/** Send a direct message to another tilde handle. */
|
|
1641
|
-
async messageSend(args) {
|
|
1642
|
-
return this.mcp.callTool("alter_message_send", args);
|
|
1643
|
-
}
|
|
1644
|
-
/** List inbound messages for the authenticated handle. */
|
|
1645
|
-
async messageInbox(args = {}) {
|
|
1646
|
-
return this.mcp.callTool("alter_message_inbox", args);
|
|
1647
|
-
}
|
|
1648
|
-
/** Bidirectional thread view between caller and a peer handle. */
|
|
1649
|
-
async messageThread(args) {
|
|
1650
|
-
return this.mcp.callTool("alter_message_thread", args);
|
|
1651
|
-
}
|
|
1652
|
-
/** Mark inbound messages as read (recipient-only). */
|
|
1653
|
-
async messageMarkRead(args) {
|
|
1654
|
-
return this.mcp.callTool("alter_message_mark_read", args);
|
|
1655
|
-
}
|
|
1656
|
-
/** Soft-redact a single inbound message (recipient-only). */
|
|
1657
|
-
async messageRedact(args) {
|
|
1658
|
-
return this.mcp.callTool("alter_message_redact", args);
|
|
1659
|
-
}
|
|
1660
|
-
/** Grant a peer permission to send messages to your inbox. */
|
|
1661
|
-
async messageGrant(args) {
|
|
1662
|
-
return this.mcp.callTool("alter_message_grant", args);
|
|
1663
|
-
}
|
|
1664
|
-
/** Revoke a peer's grant. In-flight messages are not redacted. */
|
|
1665
|
-
async messageRevoke(args) {
|
|
1666
|
-
return this.mcp.callTool("alter_message_revoke", args);
|
|
1667
|
-
}
|
|
1668
|
-
// ── Provenance ───────────────────────────────────────────────────────
|
|
1669
|
-
/**
|
|
1670
|
-
* Verify the ES256 provenance attestation on a tool response.
|
|
1671
|
-
* Accepts either a {@link ProvenanceEnvelope} or the raw `_meta`
|
|
1672
|
-
* object: the latter is more convenient for ad-hoc verification.
|
|
1673
|
-
*/
|
|
1674
|
-
async verifyProvenance(envelope) {
|
|
1675
|
-
if (!envelope) return { valid: false, reason: "no provenance envelope" };
|
|
1676
|
-
const inner = envelope.provenance ?? envelope;
|
|
1677
|
-
return verifyProvenance(inner, {
|
|
1678
|
-
jwksUrl: this.options.jwksUrl,
|
|
1679
|
-
verifyAtAllowlist: this.options.verifyAtAllowlist
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
/**
|
|
1683
|
-
* Verify the schema hashes embedded in `tools/list._meta.signatures`
|
|
1684
|
-
* against the local representation of each tool definition. Useful
|
|
1685
|
-
* for guarding against in-flight tampering of tool schemas.
|
|
1686
|
-
*/
|
|
1687
|
-
async verifyToolSignatures(tools, signatures) {
|
|
1688
|
-
return verifyToolSignatures(tools, signatures);
|
|
1689
|
-
}
|
|
1690
|
-
/** Fetch the published JWKS for ALTER's signing key (cached 5 min). */
|
|
1691
|
-
async fetchPublicKeys() {
|
|
1692
|
-
const url = this.options.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
|
|
1693
|
-
return fetchPublicKeys(url);
|
|
1694
|
-
}
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
// src/adapters/generic-mcp.ts
|
|
1698
|
-
function generateGenericMcpConfig(opts = {}) {
|
|
1699
|
-
const serverName = opts.serverName ?? "alter";
|
|
1700
|
-
const headers = { ...opts.headers ?? {} };
|
|
1701
|
-
if (opts.apiKey) headers["X-ALTER-API-Key"] = opts.apiKey;
|
|
1702
|
-
const entry = {
|
|
1703
|
-
url: opts.endpoint ?? DEFAULT_ENDPOINT,
|
|
1704
|
-
transport: "streamable-http",
|
|
1705
|
-
description: "ALTER Identity: psychometric identity field for AI agents"
|
|
1706
|
-
};
|
|
1707
|
-
if (Object.keys(headers).length > 0) entry.headers = headers;
|
|
1708
|
-
return { mcpServers: { [serverName]: entry } };
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
// src/adapters/claude-code.ts
|
|
1712
|
-
function generateClaudeConfig(opts = {}) {
|
|
1713
|
-
return generateGenericMcpConfig({ serverName: "alter", ...opts });
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
// src/adapters/cursor.ts
|
|
1717
|
-
function generateCursorConfig(opts = {}) {
|
|
1718
|
-
return generateGenericMcpConfig({ serverName: "alter", ...opts });
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
// src/adapters/claude-desktop.ts
|
|
1722
|
-
function generateClaudeDesktopConfig(opts = {}) {
|
|
1723
|
-
const serverName = opts.serverName ?? "alter";
|
|
1724
|
-
const bridgeCommand = opts.bridgeCommand ?? "alter-mcp-bridge";
|
|
1725
|
-
const env3 = {};
|
|
1726
|
-
env3.ALTER_MCP_ENDPOINT = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
1727
|
-
if (opts.apiKey) env3.ALTER_API_KEY = opts.apiKey;
|
|
1728
|
-
const entry = {
|
|
1729
|
-
command: bridgeCommand,
|
|
1730
|
-
env: env3,
|
|
1731
|
-
description: "ALTER Identity: psychometric identity field for AI agents"
|
|
1732
|
-
};
|
|
1733
|
-
if (opts.extraArgs && opts.extraArgs.length > 0) {
|
|
1734
|
-
entry.args = [...opts.extraArgs];
|
|
1735
|
-
}
|
|
1736
|
-
return { mcpServers: { [serverName]: entry } };
|
|
1737
|
-
}
|
|
1738
|
-
var HOME = homedir();
|
|
1739
|
-
var PLAT = platform();
|
|
1740
|
-
function appData() {
|
|
1741
|
-
return env.APPDATA ?? join(HOME, "AppData", "Roaming");
|
|
1742
|
-
}
|
|
1743
|
-
function xdgConfig() {
|
|
1744
|
-
return env.XDG_CONFIG_HOME ?? join(HOME, ".config");
|
|
1745
|
-
}
|
|
1746
|
-
function macAppSupport() {
|
|
1747
|
-
return join(HOME, "Library", "Application Support");
|
|
1748
|
-
}
|
|
1749
|
-
function claudeDesktopConfigPath() {
|
|
1750
|
-
if (PLAT === "darwin") return join(macAppSupport(), "Claude", "claude_desktop_config.json");
|
|
1751
|
-
if (PLAT === "win32") return join(appData(), "Claude", "claude_desktop_config.json");
|
|
1752
|
-
return join(xdgConfig(), "Claude", "claude_desktop_config.json");
|
|
1753
|
-
}
|
|
1754
|
-
function claudeDesktopDir() {
|
|
1755
|
-
if (PLAT === "darwin") return join(macAppSupport(), "Claude");
|
|
1756
|
-
if (PLAT === "win32") return join(appData(), "Claude");
|
|
1757
|
-
return join(xdgConfig(), "Claude");
|
|
1758
|
-
}
|
|
1759
|
-
function vscodeConfigPath() {
|
|
1760
|
-
if (PLAT === "darwin") return join(macAppSupport(), "Code", "User", "mcp.json");
|
|
1761
|
-
if (PLAT === "win32") return join(appData(), "Code", "User", "mcp.json");
|
|
1762
|
-
return join(xdgConfig(), "Code", "User", "mcp.json");
|
|
1763
|
-
}
|
|
1764
|
-
function vscodeDir() {
|
|
1765
|
-
if (PLAT === "darwin") return join(macAppSupport(), "Code", "User");
|
|
1766
|
-
if (PLAT === "win32") return join(appData(), "Code", "User");
|
|
1767
|
-
return join(xdgConfig(), "Code", "User");
|
|
1768
|
-
}
|
|
1769
|
-
var cursorDir = join(HOME, ".cursor");
|
|
1770
|
-
var cursorConfigPath = join(cursorDir, "mcp.json");
|
|
1771
|
-
var claudeCodeProbeDir = join(HOME, ".claude");
|
|
1772
|
-
var CLAUDE_CODE = {
|
|
1773
|
-
id: "claude-code",
|
|
1774
|
-
label: "Claude Code",
|
|
1775
|
-
configPath: null,
|
|
1776
|
-
probeDir: claudeCodeProbeDir,
|
|
1777
|
-
rootKey: "mcpServers"
|
|
1778
|
-
};
|
|
1779
|
-
var CURSOR = {
|
|
1780
|
-
id: "cursor",
|
|
1781
|
-
label: "Cursor",
|
|
1782
|
-
configPath: cursorConfigPath,
|
|
1783
|
-
probeDir: cursorDir,
|
|
1784
|
-
rootKey: "mcpServers"
|
|
1785
|
-
};
|
|
1786
|
-
var CLAUDE_DESKTOP = {
|
|
1787
|
-
id: "claude-desktop",
|
|
1788
|
-
label: "Claude Desktop",
|
|
1789
|
-
configPath: claudeDesktopConfigPath(),
|
|
1790
|
-
probeDir: claudeDesktopDir(),
|
|
1791
|
-
rootKey: "mcpServers"
|
|
1792
|
-
};
|
|
1793
|
-
var VSCODE = {
|
|
1794
|
-
id: "vscode",
|
|
1795
|
-
label: "VS Code",
|
|
1796
|
-
configPath: vscodeConfigPath(),
|
|
1797
|
-
probeDir: vscodeDir(),
|
|
1798
|
-
// VS Code's user-scoped mcp.json uses `servers`, not `mcpServers`.
|
|
1799
|
-
rootKey: "servers"
|
|
1800
|
-
};
|
|
1801
|
-
var ALL_CLIENTS = [CLAUDE_CODE, CURSOR, CLAUDE_DESKTOP, VSCODE];
|
|
1802
|
-
function alterConfigDir() {
|
|
1803
|
-
return join(xdgConfig(), "alter");
|
|
1804
|
-
}
|
|
1805
|
-
function wireStatePath() {
|
|
1806
|
-
return join(alterConfigDir(), "wire-state.json");
|
|
1807
|
-
}
|
|
1808
|
-
function probeClaudeCode() {
|
|
1809
|
-
try {
|
|
1810
|
-
const result = spawnSync("claude", ["--version"], {
|
|
1811
|
-
encoding: "utf8",
|
|
1812
|
-
shell: process.platform === "win32",
|
|
1813
|
-
timeout: 5e3
|
|
1814
|
-
});
|
|
1815
|
-
if (result.error) {
|
|
1816
|
-
return {
|
|
1817
|
-
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1818
|
-
installed: false,
|
|
1819
|
-
reason: `claude binary not on PATH (${result.error.message})`
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
if (result.status === 0) {
|
|
1823
|
-
return {
|
|
1824
|
-
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1825
|
-
installed: true,
|
|
1826
|
-
version: result.stdout.trim() || void 0,
|
|
1827
|
-
reason: "claude --version returned 0"
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
return {
|
|
1831
|
-
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1832
|
-
installed: false,
|
|
1833
|
-
reason: `claude --version exited ${String(result.status)}`
|
|
1834
|
-
};
|
|
1835
|
-
} catch (err) {
|
|
1836
|
-
return {
|
|
1837
|
-
client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
|
|
1838
|
-
installed: false,
|
|
1839
|
-
reason: err.message
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
function probeByDir(id) {
|
|
1844
|
-
const client = ALL_CLIENTS.find((c) => c.id === id);
|
|
1845
|
-
if (!client) throw new Error(`unknown client id: ${id}`);
|
|
1846
|
-
const installed = existsSync(client.probeDir);
|
|
1847
|
-
return {
|
|
1848
|
-
client,
|
|
1849
|
-
installed,
|
|
1850
|
-
reason: installed ? `found ${client.probeDir}` : `no directory at ${client.probeDir}`
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
function probeAll() {
|
|
1854
|
-
return [
|
|
1855
|
-
probeClaudeCode(),
|
|
1856
|
-
probeByDir("cursor"),
|
|
1857
|
-
probeByDir("claude-desktop"),
|
|
1858
|
-
probeByDir("vscode")
|
|
1859
|
-
];
|
|
1860
|
-
}
|
|
1861
|
-
var SYNC_PREFIXES = [
|
|
1862
|
-
// iCloud Drive: both the new and legacy mounts.
|
|
1863
|
-
"Library/Mobile Documents/com~apple~CloudDocs",
|
|
1864
|
-
"iCloud Drive",
|
|
1865
|
-
// OneDrive variants Microsoft ships across editions.
|
|
1866
|
-
"OneDrive",
|
|
1867
|
-
"OneDrive - ",
|
|
1868
|
-
// Dropbox standard + enterprise mounts.
|
|
1869
|
-
"Dropbox",
|
|
1870
|
-
"Dropbox (",
|
|
1871
|
-
// Google Drive (ALTER does not integrate with Google; still refuse).
|
|
1872
|
-
"Google Drive",
|
|
1873
|
-
"GoogleDrive",
|
|
1874
|
-
"CloudStorage/GoogleDrive",
|
|
1875
|
-
// Box, pCloud, Sync.com, MEGA: high-signal names worth refusing.
|
|
1876
|
-
"Box Sync",
|
|
1877
|
-
"pCloud Drive",
|
|
1878
|
-
"Sync.com",
|
|
1879
|
-
"MEGAsync"
|
|
1880
|
-
];
|
|
1881
|
-
function detectSyncedVolume(path) {
|
|
1882
|
-
const absolute = resolve(path);
|
|
1883
|
-
const normalised = platform() === "win32" ? absolute.replace(/\\/g, "/") : absolute;
|
|
1884
|
-
for (const prefix of SYNC_PREFIXES) {
|
|
1885
|
-
if (normalised.includes(`/${prefix}/`) || normalised.includes(`/${prefix}`)) {
|
|
1886
|
-
return { refused: true, matchedPrefix: prefix, resolvedPath: absolute };
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
return null;
|
|
1890
|
-
}
|
|
1891
|
-
var WIRE_STATE_VERSION = 1;
|
|
1892
|
-
function readWireState() {
|
|
1893
|
-
const path = wireStatePath();
|
|
1894
|
-
if (!existsSync(path)) return null;
|
|
1895
|
-
try {
|
|
1896
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1897
|
-
if (parsed.version !== WIRE_STATE_VERSION) {
|
|
1898
|
-
throw new Error(
|
|
1899
|
-
`wire-state.json version ${String(parsed.version)} is not supported by this SDK (expected ${WIRE_STATE_VERSION})`
|
|
1900
|
-
);
|
|
1901
|
-
}
|
|
1902
|
-
return parsed;
|
|
1903
|
-
} catch (err) {
|
|
1904
|
-
throw new Error(`failed to parse wire-state.json: ${err.message}`);
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
function writeWireState(state) {
|
|
1908
|
-
const path = wireStatePath();
|
|
1909
|
-
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
1910
|
-
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
1911
|
-
}
|
|
1912
|
-
function sha2562(bytes) {
|
|
1913
|
-
return createHash("sha256").update(bytes).digest("hex");
|
|
1914
|
-
}
|
|
1915
|
-
function atomicJsonMerge(opts) {
|
|
1916
|
-
const { path, timestamp, merge, idempotent = true } = opts;
|
|
1917
|
-
const tmpPath = `${path}.alter-tmp-${timestamp}`;
|
|
1918
|
-
const backupPath = `${path}.alter-backup-${timestamp}`;
|
|
1919
|
-
let existed = false;
|
|
1920
|
-
let preBytes = null;
|
|
1921
|
-
let parsed = {};
|
|
1922
|
-
if (existsSync(path)) {
|
|
1923
|
-
existed = true;
|
|
1924
|
-
preBytes = readFileSync(path, "utf8");
|
|
1925
|
-
if (preBytes.trim().length > 0) {
|
|
1926
|
-
try {
|
|
1927
|
-
parsed = JSON.parse(preBytes.replace(/^\uFEFF/, ""));
|
|
1928
|
-
} catch (err) {
|
|
1929
|
-
throw new Error(
|
|
1930
|
-
`refusing to wire ${path}: existing file is not valid JSON (${err.message}). Hand-fix the file, then re-run \`alter wire\`.`
|
|
1931
|
-
);
|
|
1932
|
-
}
|
|
1933
|
-
if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
|
|
1934
|
-
throw new Error(`refusing to wire ${path}: existing JSON root is not an object`);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
const merged = merge(parsed);
|
|
1939
|
-
const serialised = JSON.stringify(merged, null, 2) + "\n";
|
|
1940
|
-
if (idempotent && preBytes !== null && preBytes === serialised) {
|
|
1941
|
-
return {
|
|
1942
|
-
path,
|
|
1943
|
-
backupPath: null,
|
|
1944
|
-
preSha256: sha2562(preBytes),
|
|
1945
|
-
postSha256: sha2562(preBytes),
|
|
1946
|
-
noop: true
|
|
1947
|
-
};
|
|
1948
|
-
}
|
|
1949
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1950
|
-
writeFileSync(tmpPath, serialised, { mode: 384 });
|
|
1951
|
-
try {
|
|
1952
|
-
if (existed) copyFileSync(path, backupPath);
|
|
1953
|
-
renameSync(tmpPath, path);
|
|
1954
|
-
} catch (err) {
|
|
1955
|
-
try {
|
|
1956
|
-
unlinkSync(tmpPath);
|
|
1957
|
-
} catch {
|
|
1958
|
-
}
|
|
1959
|
-
throw err;
|
|
1960
|
-
}
|
|
1961
|
-
return {
|
|
1962
|
-
path,
|
|
1963
|
-
backupPath: existed ? backupPath : null,
|
|
1964
|
-
preSha256: preBytes === null ? null : sha2562(preBytes),
|
|
1965
|
-
postSha256: sha2562(serialised),
|
|
1966
|
-
noop: false
|
|
1967
|
-
};
|
|
1968
|
-
}
|
|
1969
|
-
function restoreFromBackup(path, backupPath) {
|
|
1970
|
-
if (backupPath === null) {
|
|
1971
|
-
if (existsSync(path)) unlinkSync(path);
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
if (!existsSync(backupPath)) {
|
|
1975
|
-
throw new Error(`cannot restore ${path}: backup missing at ${backupPath}`);
|
|
1976
|
-
}
|
|
1977
|
-
renameSync(backupPath, path);
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
// src/wire/index.ts
|
|
1981
|
-
var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
|
|
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
|
-
}
|
|
2003
|
-
function clientById(id) {
|
|
2004
|
-
const hit = ALL_CLIENTS.find((c) => c.id === id);
|
|
2005
|
-
if (!hit) throw new Error(`unknown client id: ${id}`);
|
|
2006
|
-
return hit;
|
|
2007
|
-
}
|
|
2008
|
-
function wire(opts = {}) {
|
|
2009
|
-
const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
2010
|
-
const apiKey = opts.apiKey;
|
|
2011
|
-
const cfAccess = opts.cfAccess ?? readCfAccessEnv();
|
|
2012
|
-
const probes = probeAll();
|
|
2013
|
-
const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
|
|
2014
|
-
const ts = TIMESTAMP();
|
|
2015
|
-
const targets = [];
|
|
2016
|
-
for (const id of selection) {
|
|
2017
|
-
const probe = id === "claude-code" ? probeClaudeCode() : probeByDir(id);
|
|
2018
|
-
if (!probe.installed && opts.skipMissing !== false) {
|
|
2019
|
-
targets.push({
|
|
2020
|
-
client: id,
|
|
2021
|
-
method: id === "claude-code" ? "cli" : "file",
|
|
2022
|
-
status: "skipped",
|
|
2023
|
-
...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
|
|
2024
|
-
reason: probe.reason
|
|
2025
|
-
});
|
|
2026
|
-
continue;
|
|
2027
|
-
}
|
|
2028
|
-
try {
|
|
2029
|
-
if (id === "claude-code") {
|
|
2030
|
-
targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
|
|
2031
|
-
} else {
|
|
2032
|
-
targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
|
|
2033
|
-
}
|
|
2034
|
-
} catch (err) {
|
|
2035
|
-
const message = err.message;
|
|
2036
|
-
targets.push({
|
|
2037
|
-
client: id,
|
|
2038
|
-
method: id === "claude-code" ? "cli" : "file",
|
|
2039
|
-
status: "failed",
|
|
2040
|
-
...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
|
|
2041
|
-
reason: message
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
const state = {
|
|
2046
|
-
version: 1,
|
|
2047
|
-
sdkVersion: SDK_VERSION,
|
|
2048
|
-
writtenAt: ISO_NOW(),
|
|
2049
|
-
endpoint,
|
|
2050
|
-
targets
|
|
2051
|
-
};
|
|
2052
|
-
writeWireState(state);
|
|
2053
|
-
return { state, probes };
|
|
2054
|
-
}
|
|
2055
|
-
function wireFileTarget(args) {
|
|
2056
|
-
const client = clientById(args.id);
|
|
2057
|
-
if (!client.configPath) {
|
|
2058
|
-
throw new Error(`client ${client.id} has no file-based config path`);
|
|
2059
|
-
}
|
|
2060
|
-
const sync = detectSyncedVolume(client.configPath);
|
|
2061
|
-
if (sync) {
|
|
2062
|
-
throw new Error(
|
|
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.`
|
|
2064
|
-
);
|
|
2065
|
-
}
|
|
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 });
|
|
2072
|
-
const rootKey = client.rootKey;
|
|
2073
|
-
const serverName = "alter";
|
|
2074
|
-
const result = atomicJsonMerge({
|
|
2075
|
-
path: client.configPath,
|
|
2076
|
-
timestamp: args.timestamp,
|
|
2077
|
-
merge: (existing) => {
|
|
2078
|
-
const bucket = existing[rootKey] ?? {};
|
|
2079
|
-
const source = entry.mcpServers.alter;
|
|
2080
|
-
return {
|
|
2081
|
-
...existing,
|
|
2082
|
-
[rootKey]: {
|
|
2083
|
-
...bucket,
|
|
2084
|
-
[serverName]: source
|
|
2085
|
-
}
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
});
|
|
2089
|
-
return {
|
|
2090
|
-
client: args.id,
|
|
2091
|
-
method: "file",
|
|
2092
|
-
status: result.noop ? "already-wired" : "written",
|
|
2093
|
-
path: result.path,
|
|
2094
|
-
backupPath: result.backupPath,
|
|
2095
|
-
rootKey,
|
|
2096
|
-
serverName,
|
|
2097
|
-
preSha256: result.preSha256,
|
|
2098
|
-
postSha256: result.postSha256
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
function wireClaudeCode(args) {
|
|
2102
|
-
const cmd = "claude";
|
|
2103
|
-
const bridgePath = resolveBridgeScript();
|
|
2104
|
-
const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
|
|
2105
|
-
"mcp",
|
|
2106
|
-
"add",
|
|
2107
|
-
"--scope",
|
|
2108
|
-
"user",
|
|
2109
|
-
"--transport",
|
|
2110
|
-
"http",
|
|
2111
|
-
"alter",
|
|
2112
|
-
args.endpoint,
|
|
2113
|
-
...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
|
|
2114
|
-
];
|
|
2115
|
-
const full = `${cmd} ${argList.join(" ")}`;
|
|
2116
|
-
const run = spawnSync(cmd, argList, {
|
|
2117
|
-
encoding: "utf8",
|
|
2118
|
-
shell: process.platform === "win32",
|
|
2119
|
-
timeout: 1e4,
|
|
2120
|
-
env: bridgePath ? { ...process.env, ALTER_PUBLIC_MCP_ENDPOINT: args.endpoint, ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {} } : void 0
|
|
2121
|
-
});
|
|
2122
|
-
if (run.error) {
|
|
2123
|
-
return {
|
|
2124
|
-
client: "claude-code",
|
|
2125
|
-
method: "cli",
|
|
2126
|
-
status: "failed",
|
|
2127
|
-
command: full,
|
|
2128
|
-
stdout: run.stdout,
|
|
2129
|
-
stderr: run.stderr,
|
|
2130
|
-
reason: run.error.message
|
|
2131
|
-
};
|
|
2132
|
-
}
|
|
2133
|
-
const stderr2 = (run.stderr ?? "").toLowerCase();
|
|
2134
|
-
const alreadyExists = stderr2.includes("already exists") || stderr2.includes("already configured");
|
|
2135
|
-
if (run.status === 0) {
|
|
2136
|
-
return { client: "claude-code", method: "cli", status: "written", command: full, stdout: run.stdout, stderr: run.stderr };
|
|
2137
|
-
}
|
|
2138
|
-
if (alreadyExists) {
|
|
2139
|
-
return { client: "claude-code", method: "cli", status: "already-wired", command: full, stdout: run.stdout, stderr: run.stderr };
|
|
2140
|
-
}
|
|
2141
|
-
return {
|
|
2142
|
-
client: "claude-code",
|
|
2143
|
-
method: "cli",
|
|
2144
|
-
status: "failed",
|
|
2145
|
-
command: full,
|
|
2146
|
-
stdout: run.stdout,
|
|
2147
|
-
stderr: run.stderr,
|
|
2148
|
-
reason: `claude mcp add exited ${String(run.status)}`
|
|
2149
|
-
};
|
|
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
|
-
}
|
|
2161
|
-
function unwire() {
|
|
2162
|
-
const state = readWireState();
|
|
2163
|
-
const undone = [];
|
|
2164
|
-
if (!state || state.targets.length === 0) {
|
|
2165
|
-
return { state, undone };
|
|
2166
|
-
}
|
|
2167
|
-
for (const target of state.targets) {
|
|
2168
|
-
try {
|
|
2169
|
-
if (target.method === "file") {
|
|
2170
|
-
if (target.status === "written") {
|
|
2171
|
-
restoreFromBackup(target.path, target.backupPath);
|
|
2172
|
-
undone.push({ client: target.client, action: target.backupPath ? "restored" : "removed" });
|
|
2173
|
-
} else {
|
|
2174
|
-
undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
|
|
2175
|
-
}
|
|
2176
|
-
} else if (target.method === "cli") {
|
|
2177
|
-
if (target.status === "written") {
|
|
2178
|
-
const run = spawnSync("claude", ["mcp", "remove", "--scope", "user", "alter"], {
|
|
2179
|
-
encoding: "utf8",
|
|
2180
|
-
shell: process.platform === "win32",
|
|
2181
|
-
timeout: 1e4
|
|
2182
|
-
});
|
|
2183
|
-
if (run.error) {
|
|
2184
|
-
undone.push({ client: target.client, action: "failed", reason: run.error.message });
|
|
2185
|
-
} else if (run.status === 0) {
|
|
2186
|
-
undone.push({ client: target.client, action: "cli-removed" });
|
|
2187
|
-
} else {
|
|
2188
|
-
undone.push({ client: target.client, action: "failed", reason: `claude mcp remove exited ${String(run.status)}` });
|
|
2189
|
-
}
|
|
2190
|
-
} else {
|
|
2191
|
-
undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
} catch (err) {
|
|
2195
|
-
undone.push({ client: target.client, action: "failed", reason: err.message });
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
writeWireState({
|
|
2199
|
-
version: 1,
|
|
2200
|
-
sdkVersion: state.sdkVersion,
|
|
2201
|
-
writtenAt: ISO_NOW(),
|
|
2202
|
-
endpoint: state.endpoint,
|
|
2203
|
-
targets: []
|
|
2204
|
-
});
|
|
2205
|
-
return { state, undone };
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
// bin/alter-identity.ts
|
|
2209
|
-
var CONFIG_DIR = join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter");
|
|
2210
|
-
var CONFIG_PATH = join(CONFIG_DIR, "identity.json");
|
|
2211
|
-
async function main() {
|
|
2212
|
-
const [, , command, ...rest] = argv;
|
|
2213
|
-
switch (command) {
|
|
2214
|
-
case "init":
|
|
2215
|
-
await runInit(rest);
|
|
2216
|
-
break;
|
|
2217
|
-
case "verify":
|
|
2218
|
-
await runVerify(rest);
|
|
2219
|
-
break;
|
|
2220
|
-
case "status":
|
|
2221
|
-
await runStatus();
|
|
2222
|
-
break;
|
|
2223
|
-
case "config":
|
|
2224
|
-
await runConfig(rest);
|
|
2225
|
-
break;
|
|
2226
|
-
case "wire":
|
|
2227
|
-
await runWire(rest);
|
|
2228
|
-
break;
|
|
2229
|
-
case "unwire":
|
|
2230
|
-
await runUnwire();
|
|
2231
|
-
break;
|
|
2232
|
-
case "message":
|
|
2233
|
-
await runMessage(rest);
|
|
2234
|
-
break;
|
|
2235
|
-
case "help":
|
|
2236
|
-
case "--help":
|
|
2237
|
-
case "-h":
|
|
2238
|
-
case void 0:
|
|
2239
|
-
printHelp();
|
|
2240
|
-
break;
|
|
2241
|
-
case "version":
|
|
2242
|
-
case "--version":
|
|
2243
|
-
case "-v":
|
|
2244
|
-
stdout.write(`${SDK_NAME} ${SDK_VERSION}
|
|
2245
|
-
`);
|
|
2246
|
-
break;
|
|
2247
|
-
default:
|
|
2248
|
-
stderr.write(`unknown command: ${command}
|
|
2249
|
-
|
|
2250
|
-
`);
|
|
2251
|
-
printHelp();
|
|
2252
|
-
exit(2);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
function printHelp() {
|
|
2256
|
-
stdout.write(`${SDK_NAME} ${SDK_VERSION}
|
|
2257
|
-
|
|
2258
|
-
Usage:
|
|
2259
|
-
alter-identity init [--wire|--no-wire] [--yes]
|
|
2260
|
-
Generate keypair, discover MCP, optionally wire detected AI clients
|
|
2261
|
-
alter-identity verify <~handle|email> Verify an identity
|
|
2262
|
-
alter-identity status Show connection state
|
|
2263
|
-
alter-identity config [--claude|--cursor|--claude-desktop|--generic]
|
|
2264
|
-
Print MCP config snippet
|
|
2265
|
-
alter-identity wire [--only=<ids>] [--yes]
|
|
2266
|
-
Merge ALTER into detected AI clients (Claude Code, Cursor, Claude Desktop)
|
|
2267
|
-
alter-identity unwire Restore every target from its backup sibling
|
|
2268
|
-
|
|
2269
|
-
Alter-to-Alter Messaging:
|
|
2270
|
-
alter-identity message send <~handle> <body> Send a direct message (body '-' = stdin)
|
|
2271
|
-
alter-identity message inbox [--unread] List your inbound messages
|
|
2272
|
-
alter-identity message thread <~handle> Bidirectional thread view with a peer
|
|
2273
|
-
alter-identity message grant <~handle> Allow a peer to message you
|
|
2274
|
-
alter-identity message revoke <~handle> Revoke a peer's grant
|
|
2275
|
-
|
|
2276
|
-
Config: ${CONFIG_PATH}
|
|
2277
|
-
`);
|
|
2278
|
-
}
|
|
2279
|
-
async function runInit(args) {
|
|
2280
|
-
const force = args.includes("--force") || args.includes("-f");
|
|
2281
|
-
const wireFlag = args.includes("--wire");
|
|
2282
|
-
const noWireFlag = args.includes("--no-wire");
|
|
2283
|
-
const yesFlag = args.includes("--yes") || args.includes("-y");
|
|
2284
|
-
if (wireFlag && noWireFlag) {
|
|
2285
|
-
stderr.write("error: --wire and --no-wire are mutually exclusive\n");
|
|
2286
|
-
exit(2);
|
|
2287
|
-
}
|
|
2288
|
-
const existing = readConfig();
|
|
2289
|
-
if (existing && !force) {
|
|
2290
|
-
stdout.write(`already initialised at ${CONFIG_PATH} (re-run with --force to overwrite)
|
|
2291
|
-
`);
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
stdout.write("\u2022 Generating Ed25519 keypair...\n");
|
|
2295
|
-
const keypair = generateKeypair();
|
|
2296
|
-
stdout.write("\u2022 Discovering MCP endpoint for truealter.com...\n");
|
|
2297
|
-
let endpoint;
|
|
2298
|
-
try {
|
|
2299
|
-
const result = await discover("truealter.com");
|
|
2300
|
-
endpoint = result.url;
|
|
2301
|
-
stdout.write(` \u2192 ${endpoint} (via ${result.source})
|
|
2302
|
-
`);
|
|
2303
|
-
} catch (err) {
|
|
2304
|
-
endpoint = "https://mcp.truealter.com/api/v1/mcp";
|
|
2305
|
-
stdout.write(` \u2192 ${endpoint} (discovery failed: ${err.message})
|
|
2306
|
-
`);
|
|
2307
|
-
}
|
|
2308
|
-
const cfg = { endpoint, keypair, initialisedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2309
|
-
writeConfig(cfg);
|
|
2310
|
-
stdout.write(`\u2022 Wrote config to ${CONFIG_PATH}
|
|
2311
|
-
`);
|
|
2312
|
-
stdout.write(` did: ${keypair.did}
|
|
2313
|
-
`);
|
|
2314
|
-
let shouldWire = false;
|
|
2315
|
-
if (noWireFlag) {
|
|
2316
|
-
shouldWire = false;
|
|
2317
|
-
} else if (wireFlag || yesFlag) {
|
|
2318
|
-
shouldWire = true;
|
|
2319
|
-
} else if (stdin.isTTY) {
|
|
2320
|
-
const probes = probeAll();
|
|
2321
|
-
const found = probes.filter((p) => p.installed).map((p) => p.client.label);
|
|
2322
|
-
if (found.length === 0) {
|
|
2323
|
-
stdout.write("\nNo MCP-aware clients detected on this machine \u2014 skipping wire.\n");
|
|
2324
|
-
} else {
|
|
2325
|
-
stdout.write(`
|
|
2326
|
-
Detected MCP-aware clients: ${found.join(", ")}
|
|
2327
|
-
`);
|
|
2328
|
-
shouldWire = await confirm("Wire detected AI clients to ALTER?", true);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
if (shouldWire) {
|
|
2332
|
-
stdout.write("\n\u2022 Wiring detected AI clients...\n");
|
|
2333
|
-
const report = wire({ endpoint });
|
|
2334
|
-
printWireReport(report);
|
|
2335
|
-
}
|
|
2336
|
-
stdout.write(`
|
|
2337
|
-
Next: alter-identity verify ~truealter
|
|
2338
|
-
`);
|
|
2339
|
-
}
|
|
2340
|
-
async function runVerify(args) {
|
|
2341
|
-
const handle = args[0];
|
|
2342
|
-
if (!handle) {
|
|
2343
|
-
stderr.write("usage: alter-identity verify <~handle|email|uuid>\n");
|
|
2344
|
-
exit(2);
|
|
2345
|
-
}
|
|
2346
|
-
const cfg = readConfig() ?? {};
|
|
2347
|
-
const client = new AlterClient({ endpoint: cfg.endpoint, apiKey: cfg.apiKey });
|
|
2348
|
-
try {
|
|
2349
|
-
const result = await client.verify(handle);
|
|
2350
|
-
const text = result.content?.[0]?.text ?? JSON.stringify(result.data ?? result, null, 2);
|
|
2351
|
-
stdout.write(text + "\n");
|
|
2352
|
-
} catch (err) {
|
|
2353
|
-
stderr.write(`verify failed: ${err.message}
|
|
2354
|
-
`);
|
|
2355
|
-
exit(1);
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
async function runStatus() {
|
|
2359
|
-
const cfg = readConfig();
|
|
2360
|
-
if (!cfg) {
|
|
2361
|
-
stdout.write(`not initialised \u2014 run \`alter-identity init\`
|
|
2362
|
-
`);
|
|
2363
|
-
return;
|
|
2364
|
-
}
|
|
2365
|
-
stdout.write(`config: ${CONFIG_PATH}
|
|
2366
|
-
`);
|
|
2367
|
-
stdout.write(`endpoint: ${cfg.endpoint ?? "(default)"}
|
|
2368
|
-
`);
|
|
2369
|
-
stdout.write(`api key: ${cfg.apiKey ? "(set)" : "(none)"}
|
|
2370
|
-
`);
|
|
2371
|
-
if (cfg.keypair) {
|
|
2372
|
-
const recovered = keypairFromPrivateKey(cfg.keypair.privateKey);
|
|
2373
|
-
stdout.write(`did: ${recovered.did}
|
|
2374
|
-
`);
|
|
2375
|
-
}
|
|
2376
|
-
stdout.write(`initialised: ${cfg.initialisedAt ?? "(unknown)"}
|
|
2377
|
-
`);
|
|
2378
|
-
const client = new AlterClient({ endpoint: cfg.endpoint, apiKey: cfg.apiKey });
|
|
2379
|
-
try {
|
|
2380
|
-
const stats = await client.getNetworkStats();
|
|
2381
|
-
const text = stats.content?.[0]?.text ?? JSON.stringify(stats.data ?? "");
|
|
2382
|
-
stdout.write(`network probe: ok \u2014 ${text.slice(0, 120)}
|
|
2383
|
-
`);
|
|
2384
|
-
} catch (err) {
|
|
2385
|
-
stdout.write(`network probe: failed \u2014 ${err.message}
|
|
2386
|
-
`);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
async function runConfig(args) {
|
|
2390
|
-
const cfg = readConfig() ?? {};
|
|
2391
|
-
const opts = { endpoint: cfg.endpoint, apiKey: cfg.apiKey };
|
|
2392
|
-
let out;
|
|
2393
|
-
if (args.includes("--cursor")) out = generateCursorConfig(opts);
|
|
2394
|
-
else if (args.includes("--claude-desktop")) out = generateClaudeDesktopConfig(opts);
|
|
2395
|
-
else if (args.includes("--generic")) out = generateGenericMcpConfig(opts);
|
|
2396
|
-
else out = generateClaudeConfig(opts);
|
|
2397
|
-
stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
2398
|
-
}
|
|
2399
|
-
async function runWire(args) {
|
|
2400
|
-
const yesFlag = args.includes("--yes") || args.includes("-y");
|
|
2401
|
-
const onlyArg = args.find((a) => a.startsWith("--only="));
|
|
2402
|
-
const only = onlyArg ? onlyArg.slice("--only=".length).split(",").filter(Boolean) : void 0;
|
|
2403
|
-
const cfg = readConfig() ?? {};
|
|
2404
|
-
if (!cfg.endpoint) {
|
|
2405
|
-
stderr.write("error: no endpoint \u2014 run `alter-identity init` first\n");
|
|
2406
|
-
exit(2);
|
|
2407
|
-
}
|
|
2408
|
-
if (!yesFlag && stdin.isTTY) {
|
|
2409
|
-
const probes = probeAll();
|
|
2410
|
-
const found = probes.filter((p) => p.installed).map((p) => p.client.label);
|
|
2411
|
-
if (found.length === 0) {
|
|
2412
|
-
stdout.write("No MCP-aware clients detected on this machine. Nothing to do.\n");
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
stdout.write(`Detected: ${found.join(", ")}
|
|
2416
|
-
`);
|
|
2417
|
-
const proceed = await confirm("Wire these clients to ALTER?", true);
|
|
2418
|
-
if (!proceed) {
|
|
2419
|
-
stdout.write("aborted.\n");
|
|
2420
|
-
return;
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
const report = wire({ endpoint: cfg.endpoint, apiKey: cfg.apiKey, only });
|
|
2424
|
-
printWireReport(report);
|
|
2425
|
-
}
|
|
2426
|
-
async function runUnwire() {
|
|
2427
|
-
const report = unwire();
|
|
2428
|
-
printUnwireReport(report);
|
|
2429
|
-
}
|
|
2430
|
-
function printWireReport(report) {
|
|
2431
|
-
for (const target of report.state.targets) {
|
|
2432
|
-
const tag = `[${target.client}]`;
|
|
2433
|
-
switch (target.status) {
|
|
2434
|
-
case "written":
|
|
2435
|
-
if (target.method === "file") {
|
|
2436
|
-
stdout.write(` \u2713 ${tag} wrote ${target.path} (backup: ${target.backupPath ?? "(none \u2014 created new file)"})
|
|
2437
|
-
`);
|
|
2438
|
-
} else {
|
|
2439
|
-
stdout.write(` \u2713 ${tag} registered via \`${target.command}\`
|
|
2440
|
-
`);
|
|
2441
|
-
}
|
|
2442
|
-
break;
|
|
2443
|
-
case "already-wired":
|
|
2444
|
-
stdout.write(` \xB7 ${tag} already wired \u2014 no change
|
|
2445
|
-
`);
|
|
2446
|
-
break;
|
|
2447
|
-
case "skipped":
|
|
2448
|
-
stdout.write(` - ${tag} skipped (${target.reason ?? "not installed"})
|
|
2449
|
-
`);
|
|
2450
|
-
break;
|
|
2451
|
-
case "failed":
|
|
2452
|
-
stderr.write(` \u2717 ${tag} failed: ${target.reason ?? "unknown"}
|
|
2453
|
-
`);
|
|
2454
|
-
break;
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
stdout.write(`
|
|
2458
|
-
wire-state \u2192 ${join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter", "wire-state.json")}
|
|
2459
|
-
`);
|
|
2460
|
-
stdout.write("run `alter-identity unwire` to reverse.\n");
|
|
2461
|
-
}
|
|
2462
|
-
function printUnwireReport(report) {
|
|
2463
|
-
if (!report.state) {
|
|
2464
|
-
stdout.write("nothing to unwire \u2014 no wire-state.json found\n");
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
|
-
if (report.state.targets.length === 0) {
|
|
2468
|
-
stdout.write("wire-state.json is empty \u2014 nothing to unwire\n");
|
|
2469
|
-
return;
|
|
2470
|
-
}
|
|
2471
|
-
for (const entry of report.undone) {
|
|
2472
|
-
const tag = `[${entry.client}]`;
|
|
2473
|
-
switch (entry.action) {
|
|
2474
|
-
case "restored":
|
|
2475
|
-
stdout.write(` \u2713 ${tag} restored from backup
|
|
2476
|
-
`);
|
|
2477
|
-
break;
|
|
2478
|
-
case "removed":
|
|
2479
|
-
stdout.write(` \u2713 ${tag} removed (file was created by wire)
|
|
2480
|
-
`);
|
|
2481
|
-
break;
|
|
2482
|
-
case "cli-removed":
|
|
2483
|
-
stdout.write(` \u2713 ${tag} removed via \`claude mcp remove\`
|
|
2484
|
-
`);
|
|
2485
|
-
break;
|
|
2486
|
-
case "skipped":
|
|
2487
|
-
stdout.write(` \xB7 ${tag} skipped (${entry.reason ?? ""})
|
|
2488
|
-
`);
|
|
2489
|
-
break;
|
|
2490
|
-
case "failed":
|
|
2491
|
-
stderr.write(` \u2717 ${tag} failed: ${entry.reason ?? ""}
|
|
2492
|
-
`);
|
|
2493
|
-
break;
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
async function confirm(question, defaultYes) {
|
|
2498
|
-
if (!stdin.isTTY) return false;
|
|
2499
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
2500
|
-
const suffix = " [Y/n] " ;
|
|
2501
|
-
const answer = await new Promise((resolve2) => {
|
|
2502
|
-
rl.question(question + suffix, (ans) => resolve2(ans));
|
|
2503
|
-
});
|
|
2504
|
-
rl.close();
|
|
2505
|
-
const trimmed = answer.trim().toLowerCase();
|
|
2506
|
-
if (!trimmed) return defaultYes;
|
|
2507
|
-
return trimmed === "y" || trimmed === "yes";
|
|
2508
|
-
}
|
|
2509
|
-
async function runMessage(args) {
|
|
2510
|
-
const [sub, ...rest] = args;
|
|
2511
|
-
if (!sub) {
|
|
2512
|
-
stderr.write("usage: alter-identity message <send|inbox|thread|grant|revoke> ...\n");
|
|
2513
|
-
exit(2);
|
|
2514
|
-
}
|
|
2515
|
-
const cfg = readConfig() ?? {};
|
|
2516
|
-
const client = new AlterClient({ endpoint: cfg.endpoint, apiKey: cfg.apiKey });
|
|
2517
|
-
const printResult = (result) => {
|
|
2518
|
-
const text = result.content?.[0]?.text;
|
|
2519
|
-
if (text) {
|
|
2520
|
-
stdout.write(text + "\n");
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
if (result.data !== void 0) {
|
|
2524
|
-
stdout.write(JSON.stringify(result.data, null, 2) + "\n");
|
|
2525
|
-
return;
|
|
2526
|
-
}
|
|
2527
|
-
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2528
|
-
};
|
|
2529
|
-
try {
|
|
2530
|
-
switch (sub) {
|
|
2531
|
-
case "send": {
|
|
2532
|
-
const to = rest[0];
|
|
2533
|
-
let body = rest[1];
|
|
2534
|
-
if (!to || !body) {
|
|
2535
|
-
stderr.write("usage: alter-identity message send <~handle> <body|->\n");
|
|
2536
|
-
exit(2);
|
|
2537
|
-
}
|
|
2538
|
-
if (body === "-") {
|
|
2539
|
-
const chunks = [];
|
|
2540
|
-
for await (const chunk of (await import('process')).stdin) {
|
|
2541
|
-
chunks.push(chunk);
|
|
2542
|
-
}
|
|
2543
|
-
body = Buffer.concat(chunks).toString("utf8").trim();
|
|
2544
|
-
if (!body) {
|
|
2545
|
-
stderr.write("error: empty body on stdin\n");
|
|
2546
|
-
exit(2);
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
const result = await client.messageSend({ to, body });
|
|
2550
|
-
printResult(result);
|
|
2551
|
-
break;
|
|
2552
|
-
}
|
|
2553
|
-
case "inbox": {
|
|
2554
|
-
const unreadOnly = rest.includes("--unread");
|
|
2555
|
-
const sinceArg = rest.find((a) => a.startsWith("--since="));
|
|
2556
|
-
const since = sinceArg ? sinceArg.slice("--since=".length) : void 0;
|
|
2557
|
-
const result = await client.messageInbox({
|
|
2558
|
-
unread_only: unreadOnly || void 0,
|
|
2559
|
-
since
|
|
2560
|
-
});
|
|
2561
|
-
printResult(result);
|
|
2562
|
-
break;
|
|
2563
|
-
}
|
|
2564
|
-
case "thread": {
|
|
2565
|
-
const peer = rest[0];
|
|
2566
|
-
if (!peer) {
|
|
2567
|
-
stderr.write("usage: alter-identity message thread <~handle>\n");
|
|
2568
|
-
exit(2);
|
|
2569
|
-
}
|
|
2570
|
-
const result = await client.messageThread({ with: peer });
|
|
2571
|
-
printResult(result);
|
|
2572
|
-
break;
|
|
2573
|
-
}
|
|
2574
|
-
case "grant": {
|
|
2575
|
-
const peer = rest[0];
|
|
2576
|
-
if (!peer) {
|
|
2577
|
-
stderr.write("usage: alter-identity message grant <~handle>\n");
|
|
2578
|
-
exit(2);
|
|
2579
|
-
}
|
|
2580
|
-
const result = await client.messageGrant({ peer });
|
|
2581
|
-
printResult(result);
|
|
2582
|
-
break;
|
|
2583
|
-
}
|
|
2584
|
-
case "revoke": {
|
|
2585
|
-
const peer = rest[0];
|
|
2586
|
-
if (!peer) {
|
|
2587
|
-
stderr.write("usage: alter-identity message revoke <~handle>\n");
|
|
2588
|
-
exit(2);
|
|
2589
|
-
}
|
|
2590
|
-
const result = await client.messageRevoke({ peer });
|
|
2591
|
-
printResult(result);
|
|
2592
|
-
break;
|
|
2593
|
-
}
|
|
2594
|
-
case "mark-read": {
|
|
2595
|
-
const ids = rest.filter((a) => !a.startsWith("--"));
|
|
2596
|
-
if (ids.length === 0) {
|
|
2597
|
-
stderr.write("usage: alter-identity message mark-read <id> [<id> ...]\n");
|
|
2598
|
-
exit(2);
|
|
2599
|
-
}
|
|
2600
|
-
const result = await client.messageMarkRead({ message_ids: ids });
|
|
2601
|
-
printResult(result);
|
|
2602
|
-
break;
|
|
2603
|
-
}
|
|
2604
|
-
case "redact": {
|
|
2605
|
-
const id = rest[0];
|
|
2606
|
-
if (!id) {
|
|
2607
|
-
stderr.write("usage: alter-identity message redact <id>\n");
|
|
2608
|
-
exit(2);
|
|
2609
|
-
}
|
|
2610
|
-
const result = await client.messageRedact({ message_id: id });
|
|
2611
|
-
printResult(result);
|
|
2612
|
-
break;
|
|
2613
|
-
}
|
|
2614
|
-
default:
|
|
2615
|
-
stderr.write(`unknown message subcommand: ${sub}
|
|
2616
|
-
`);
|
|
2617
|
-
exit(2);
|
|
2618
|
-
}
|
|
2619
|
-
} catch (err) {
|
|
2620
|
-
stderr.write(`message ${sub} failed: ${err.message}
|
|
2621
|
-
`);
|
|
2622
|
-
exit(1);
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
function readConfig() {
|
|
2626
|
-
if (!existsSync(CONFIG_PATH)) return null;
|
|
2627
|
-
try {
|
|
2628
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
2629
|
-
} catch {
|
|
2630
|
-
return null;
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
function writeConfig(cfg) {
|
|
2634
|
-
mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
2635
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
2636
|
-
}
|
|
2637
|
-
main().catch((err) => {
|
|
2638
|
-
stderr.write(`error: ${err.message}
|
|
2639
|
-
`);
|
|
2640
|
-
exit(1);
|
|
2641
|
-
});
|