@truealter/sdk 0.0.1 → 0.2.0

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.
@@ -0,0 +1,1330 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join, dirname } from 'path';
5
+ import { env, stderr, exit, argv, stdout } from 'process';
6
+ import * as ed25519 from '@noble/ed25519';
7
+ import { sha512 } from '@noble/hashes/sha512';
8
+ import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
9
+
10
+ // src/errors.ts
11
+ var AlterError = class extends Error {
12
+ code;
13
+ cause;
14
+ constructor(code, message, cause) {
15
+ super(message);
16
+ this.name = "AlterError";
17
+ this.code = code;
18
+ this.cause = cause;
19
+ Object.setPrototypeOf(this, new.target.prototype);
20
+ }
21
+ };
22
+ var AlterNetworkError = class extends AlterError {
23
+ constructor(message, cause) {
24
+ super("NETWORK", message, cause);
25
+ this.name = "AlterNetworkError";
26
+ Object.setPrototypeOf(this, new.target.prototype);
27
+ }
28
+ };
29
+ var AlterTimeoutError = class extends AlterError {
30
+ constructor(message, cause) {
31
+ super("TIMEOUT", message, cause);
32
+ this.name = "AlterTimeoutError";
33
+ Object.setPrototypeOf(this, new.target.prototype);
34
+ }
35
+ };
36
+ var AlterAuthError = class extends AlterError {
37
+ status;
38
+ constructor(message, status = 401) {
39
+ super("AUTH", message);
40
+ this.name = "AlterAuthError";
41
+ this.status = status;
42
+ Object.setPrototypeOf(this, new.target.prototype);
43
+ }
44
+ };
45
+ var AlterPaymentRequired = class extends AlterError {
46
+ envelope;
47
+ tool;
48
+ constructor(tool, envelope) {
49
+ super("PAYMENT_REQUIRED", `x402 payment required for tool "${tool}"`);
50
+ this.name = "AlterPaymentRequired";
51
+ this.tool = tool;
52
+ this.envelope = envelope;
53
+ Object.setPrototypeOf(this, new.target.prototype);
54
+ }
55
+ };
56
+ var AlterRateLimited = class extends AlterError {
57
+ retryAfter;
58
+ constructor(message, retryAfter = 60) {
59
+ super("RATE_LIMITED", message);
60
+ this.name = "AlterRateLimited";
61
+ this.retryAfter = retryAfter;
62
+ Object.setPrototypeOf(this, new.target.prototype);
63
+ }
64
+ };
65
+ var AlterToolError = class extends AlterError {
66
+ tool;
67
+ rpcCode;
68
+ constructor(tool, message, rpcCode) {
69
+ super("TOOL_ERROR", message);
70
+ this.name = "AlterToolError";
71
+ this.tool = tool;
72
+ this.rpcCode = rpcCode;
73
+ Object.setPrototypeOf(this, new.target.prototype);
74
+ }
75
+ };
76
+ var AlterProvenanceError = class extends AlterError {
77
+ constructor(message, cause) {
78
+ super("PROVENANCE", message, cause);
79
+ this.name = "AlterProvenanceError";
80
+ Object.setPrototypeOf(this, new.target.prototype);
81
+ }
82
+ };
83
+ var AlterDiscoveryError = class extends AlterError {
84
+ constructor(message, cause) {
85
+ super("DISCOVERY", message, cause);
86
+ this.name = "AlterDiscoveryError";
87
+ Object.setPrototypeOf(this, new.target.prototype);
88
+ }
89
+ };
90
+ var AlterInvalidResponse = class extends AlterError {
91
+ constructor(message, cause) {
92
+ super("INVALID_RESPONSE", message, cause);
93
+ this.name = "AlterInvalidResponse";
94
+ Object.setPrototypeOf(this, new.target.prototype);
95
+ }
96
+ };
97
+
98
+ // src/discovery.ts
99
+ var _cache = /* @__PURE__ */ new Map();
100
+ async function discover(domain, opts = {}) {
101
+ const { cache = true, skipDns = false, timeoutMs = 5e3, fetch: fetchImpl = fetch } = opts;
102
+ const host = normaliseDomain(domain);
103
+ if (cache && _cache.has(host)) return _cache.get(host);
104
+ const errors = [];
105
+ if (!skipDns) {
106
+ try {
107
+ const dnsHit = await tryDns(host);
108
+ if (dnsHit) {
109
+ const result = { url: dnsHit, transport: "streamable-http", source: "dns" };
110
+ if (cache) _cache.set(host, result);
111
+ return result;
112
+ }
113
+ } catch (err) {
114
+ errors.push(`dns: ${err.message}`);
115
+ }
116
+ }
117
+ try {
118
+ const result = await tryWellKnown(host, "mcp.json", timeoutMs, fetchImpl);
119
+ if (result) {
120
+ if (cache) _cache.set(host, result);
121
+ return result;
122
+ }
123
+ } catch (err) {
124
+ errors.push(`mcp.json: ${err.message}`);
125
+ }
126
+ try {
127
+ const result = await tryWellKnown(host, "alter.json", timeoutMs, fetchImpl);
128
+ if (result) {
129
+ if (cache) _cache.set(host, result);
130
+ return result;
131
+ }
132
+ } catch (err) {
133
+ errors.push(`alter.json: ${err.message}`);
134
+ }
135
+ throw new AlterDiscoveryError(
136
+ `No MCP discovery hit for ${host}: ${errors.join("; ") || "all sources empty"}`
137
+ );
138
+ }
139
+ function normaliseDomain(input) {
140
+ let host = input.trim().toLowerCase();
141
+ host = host.replace(/^https?:\/\//, "");
142
+ host = host.split("/")[0];
143
+ host = host.split(":")[0];
144
+ if (!host) throw new AlterDiscoveryError(`Empty domain: "${input}"`);
145
+ return host;
146
+ }
147
+ async function tryDns(host) {
148
+ let resolveTxt;
149
+ try {
150
+ const dns = await import('dns/promises');
151
+ resolveTxt = dns.resolveTxt.bind(dns);
152
+ } catch {
153
+ return null;
154
+ }
155
+ let records;
156
+ try {
157
+ records = await resolveTxt(`_mcp.${host}`);
158
+ } catch (err) {
159
+ const code = err.code;
160
+ if (code === "ENOTFOUND" || code === "ENODATA") return null;
161
+ throw err;
162
+ }
163
+ for (const chunks of records) {
164
+ const joined = chunks.join("");
165
+ const parsed = parseDnsTxt(joined);
166
+ if (parsed.mcp) return parsed.mcp;
167
+ }
168
+ return null;
169
+ }
170
+ function parseDnsTxt(record) {
171
+ const out = {};
172
+ for (const part of record.split(/[;\s]+/)) {
173
+ const [k, ...rest] = part.split("=");
174
+ if (!k || rest.length === 0) continue;
175
+ out[k.toLowerCase()] = rest.join("=");
176
+ }
177
+ return out;
178
+ }
179
+ async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
180
+ const url = `https://${host}/.well-known/${file}`;
181
+ const controller = new AbortController();
182
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
183
+ let resp;
184
+ try {
185
+ resp = await fetchImpl(url, {
186
+ method: "GET",
187
+ headers: { Accept: "application/json" },
188
+ signal: controller.signal,
189
+ redirect: "manual"
190
+ });
191
+ } catch (err) {
192
+ throw new AlterNetworkError(`fetch ${url}: ${err.message}`, err);
193
+ } finally {
194
+ clearTimeout(timer);
195
+ }
196
+ if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
197
+ throw new AlterNetworkError(
198
+ `${url} \u2192 redirect rejected (discovery must not follow redirects; validate the server configuration)`
199
+ );
200
+ }
201
+ if (resp.status === 404) return null;
202
+ if (!resp.ok) throw new AlterNetworkError(`${url} \u2192 HTTP ${resp.status}`);
203
+ const doc = await resp.json();
204
+ if (file === "mcp.json") {
205
+ const remotes = doc.remotes || [];
206
+ const remote = remotes.find((r) => r.transportType === "streamable-http" || r.transportType === "http");
207
+ const url2 = remote?.url || doc.url;
208
+ if (!url2) return null;
209
+ return { url: url2, transport: "streamable-http", source: "mcp.json", raw: doc };
210
+ }
211
+ const mcpHost = doc.mcp;
212
+ if (!mcpHost) return null;
213
+ return {
214
+ url: ensureMcpPath(mcpHost),
215
+ transport: "streamable-http",
216
+ source: "alter.json",
217
+ publicKey: doc.pk,
218
+ x402Contract: doc.x402,
219
+ capability: doc.cap,
220
+ raw: doc
221
+ };
222
+ }
223
+ function ensureMcpPath(url) {
224
+ try {
225
+ const u = new URL(url);
226
+ if (u.pathname === "" || u.pathname === "/") u.pathname = "/api/v1/mcp";
227
+ return u.toString().replace(/\/$/, "");
228
+ } catch {
229
+ return url;
230
+ }
231
+ }
232
+
233
+ // src/x402.ts
234
+ var X402Client = class {
235
+ signer;
236
+ maxPerQuery;
237
+ networks;
238
+ assets;
239
+ constructor(opts = {}) {
240
+ this.signer = opts.signer;
241
+ this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
242
+ this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
243
+ this.assets = new Set(opts.assets ?? ["USDC"]);
244
+ }
245
+ /**
246
+ * Validate the envelope against this client's policy and, if a signer
247
+ * is configured, settle it. Returns the settlement reference that
248
+ * should be replayed in the next request's `_payment` field.
249
+ */
250
+ async authorise(envelope) {
251
+ if (envelope.scheme !== "x402") {
252
+ throw new AlterError("PAYMENT_REQUIRED", `unsupported payment scheme: ${envelope.scheme}`);
253
+ }
254
+ if (!this.networks.has(envelope.network)) {
255
+ throw new AlterError("PAYMENT_REQUIRED", `network ${envelope.network} not permitted by client policy`);
256
+ }
257
+ if (!this.assets.has(envelope.asset)) {
258
+ throw new AlterError("PAYMENT_REQUIRED", `asset ${envelope.asset} not permitted by client policy`);
259
+ }
260
+ if (this.maxPerQuery !== void 0 && Number(envelope.amount) > this.maxPerQuery) {
261
+ throw new AlterError(
262
+ "PAYMENT_REQUIRED",
263
+ `quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
264
+ );
265
+ }
266
+ if (!this.signer) {
267
+ throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
268
+ }
269
+ return this.signer.settle(envelope);
270
+ }
271
+ /**
272
+ * Build the `_payment` argument that gets attached to retried tool calls.
273
+ * Mirrors the shape the ALTER server expects.
274
+ */
275
+ static buildPaymentArg(settlement) {
276
+ return {
277
+ scheme: "x402",
278
+ network: settlement.network,
279
+ asset: settlement.asset,
280
+ amount: settlement.amount,
281
+ reference: settlement.reference
282
+ };
283
+ }
284
+ };
285
+ function parsePaymentHeader(header) {
286
+ try {
287
+ const parsed = JSON.parse(header);
288
+ if (parsed && typeof parsed === "object") return parsed;
289
+ } catch {
290
+ }
291
+ const out = {};
292
+ for (const part of header.split(/[,;]/)) {
293
+ const [k, ...rest] = part.trim().split("=");
294
+ if (!k) continue;
295
+ out[k] = rest.join("=").replace(/^"|"$/g, "");
296
+ }
297
+ if (!out.scheme && !out.network && !out.amount) return null;
298
+ return {
299
+ scheme: "x402",
300
+ network: out.network || "base",
301
+ asset: out.asset || "USDC",
302
+ amount: out.amount || "0",
303
+ recipient: out.recipient || "",
304
+ resource: out.resource || "",
305
+ expires_at: out.expires_at,
306
+ nonce: out.nonce
307
+ };
308
+ }
309
+
310
+ // src/mcp.ts
311
+ var MCP_PROTOCOL_VERSION = "2025-03-26";
312
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
313
+ var MCPClient = class {
314
+ endpoint;
315
+ sessionId = null;
316
+ apiKey;
317
+ fetchImpl;
318
+ timeoutMs;
319
+ maxRetries;
320
+ clientInfo;
321
+ x402;
322
+ requestCounter = 0;
323
+ initialised = false;
324
+ constructor(opts = {}) {
325
+ this.endpoint = (opts.endpoint ?? "https://mcp.truealter.com/api/v1/mcp").replace(/\/+$/, "");
326
+ this.apiKey = opts.apiKey;
327
+ this.fetchImpl = opts.fetch ?? fetch;
328
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
329
+ this.maxRetries = opts.maxRetries ?? 2;
330
+ this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
331
+ this.x402 = opts.x402;
332
+ }
333
+ /**
334
+ * Send the MCP `initialize` handshake and capture the resulting session
335
+ * id. Idempotent — safe to call multiple times.
336
+ */
337
+ async initialize() {
338
+ if (this.initialised) return null;
339
+ const result = await this.rpc("initialize", {
340
+ protocolVersion: MCP_PROTOCOL_VERSION,
341
+ capabilities: {},
342
+ clientInfo: this.clientInfo
343
+ });
344
+ this.initialised = true;
345
+ return result;
346
+ }
347
+ /** List available tools. */
348
+ async listTools() {
349
+ if (!this.initialised) await this.initialize();
350
+ return await this.rpc("tools/list", {});
351
+ }
352
+ /** Invoke a tool by name. */
353
+ async callTool(name, args = {}, opts = {}) {
354
+ if (!this.initialised) await this.initialize();
355
+ return this.callToolInternal(name, args, opts);
356
+ }
357
+ /** Close the session and release any held resources. */
358
+ async closeSession() {
359
+ if (!this.sessionId) return;
360
+ try {
361
+ await this.fetchImpl(this.endpoint, {
362
+ method: "DELETE",
363
+ headers: this.buildHeaders()
364
+ });
365
+ } catch {
366
+ }
367
+ this.sessionId = null;
368
+ this.initialised = false;
369
+ }
370
+ // ── Internals ────────────────────────────────────────────────────────
371
+ async callToolInternal(name, args, opts) {
372
+ try {
373
+ const raw = await this.rpc("tools/call", { name, arguments: args });
374
+ const result = this.shapeToolResult(raw);
375
+ if (result.isError) {
376
+ const text = result.content?.[0]?.text ?? `tool ${name} returned an error`;
377
+ throw new AlterToolError(name, text);
378
+ }
379
+ return result;
380
+ } catch (err) {
381
+ if (err instanceof AlterPaymentRequired && !opts.noPaymentRetry) {
382
+ const x402 = opts.x402 ?? this.x402;
383
+ if (!x402) throw err;
384
+ const settlement = await x402.authorise(err.envelope);
385
+ const retryArgs = { ...args, _payment: X402Client.buildPaymentArg(settlement) };
386
+ return this.callToolInternal(name, retryArgs, { ...opts, noPaymentRetry: true });
387
+ }
388
+ throw err;
389
+ }
390
+ }
391
+ shapeToolResult(raw) {
392
+ if (!raw || !Array.isArray(raw.content)) return raw;
393
+ if (raw.data === void 0) {
394
+ const first = raw.content[0];
395
+ if (first && first.type === "json" && "data" in first) {
396
+ raw.data = first.data;
397
+ } else if (first && first.type === "text" && first.text) {
398
+ try {
399
+ raw.data = JSON.parse(first.text);
400
+ } catch {
401
+ }
402
+ }
403
+ }
404
+ return raw;
405
+ }
406
+ /**
407
+ * Send a JSON-RPC 2.0 request and return the result. Errors are
408
+ * normalised into the typed {@link AlterError} hierarchy.
409
+ */
410
+ async rpc(method, params) {
411
+ const id = ++this.requestCounter;
412
+ const payload = {
413
+ jsonrpc: "2.0",
414
+ id,
415
+ method
416
+ };
417
+ if (params !== void 0) payload.params = params;
418
+ let attempt = 0;
419
+ let lastErr = null;
420
+ while (attempt <= this.maxRetries) {
421
+ attempt += 1;
422
+ const controller = new AbortController();
423
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
424
+ let resp;
425
+ try {
426
+ resp = await this.fetchImpl(this.endpoint, {
427
+ method: "POST",
428
+ headers: this.buildHeaders(),
429
+ body: JSON.stringify(payload),
430
+ signal: controller.signal
431
+ });
432
+ } catch (err) {
433
+ clearTimeout(timer);
434
+ const isAbort = err?.name === "AbortError";
435
+ if (isAbort) {
436
+ lastErr = new AlterTimeoutError(`MCP ${method} timed out after ${this.timeoutMs}ms`, err);
437
+ } else {
438
+ lastErr = new AlterNetworkError(`MCP ${method}: ${err.message}`, err);
439
+ }
440
+ if (attempt > this.maxRetries) throw lastErr;
441
+ await this.backoff(attempt);
442
+ continue;
443
+ }
444
+ clearTimeout(timer);
445
+ const sessionHeader = resp.headers.get("Mcp-Session-Id");
446
+ if (sessionHeader) this.sessionId = sessionHeader;
447
+ if (resp.status === 401 || resp.status === 403) {
448
+ throw new AlterAuthError(`HTTP ${resp.status} on ${method}`, resp.status);
449
+ }
450
+ if (resp.status === 402) {
451
+ const envelope = await this.extractPaymentEnvelope(resp);
452
+ throw new AlterPaymentRequired(this.guessToolName(payload), envelope);
453
+ }
454
+ if (resp.status === 429) {
455
+ const retryAfter = Number(resp.headers.get("Retry-After") ?? 60);
456
+ if (attempt > this.maxRetries) {
457
+ throw new AlterRateLimited(`HTTP 429 on ${method}`, retryAfter);
458
+ }
459
+ await this.backoff(attempt, retryAfter * 1e3);
460
+ continue;
461
+ }
462
+ if (RETRYABLE_STATUSES.has(resp.status) && attempt <= this.maxRetries) {
463
+ await this.backoff(attempt);
464
+ continue;
465
+ }
466
+ if (!resp.ok) {
467
+ const body2 = await safeText(resp);
468
+ throw new AlterError("NETWORK", `HTTP ${resp.status} on ${method}: ${body2.slice(0, 200)}`);
469
+ }
470
+ let body;
471
+ try {
472
+ body = await resp.json();
473
+ } catch (err) {
474
+ throw new AlterInvalidResponse(`MCP ${method}: invalid JSON body`, err);
475
+ }
476
+ if (body.error) {
477
+ const code = body.error.code;
478
+ const message = body.error.message ?? `MCP ${method} error`;
479
+ if (code === -32001 || code === 402) {
480
+ const data = body.error.data;
481
+ if (data?.envelope) {
482
+ throw new AlterPaymentRequired(this.guessToolName(payload), data.envelope);
483
+ }
484
+ }
485
+ throw new AlterToolError(this.guessToolName(payload), message, code);
486
+ }
487
+ return body.result;
488
+ }
489
+ throw lastErr ?? new AlterNetworkError(`MCP ${method}: exhausted retries`);
490
+ }
491
+ buildHeaders() {
492
+ const headers = {
493
+ "Content-Type": "application/json",
494
+ Accept: "application/json",
495
+ "User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`
496
+ };
497
+ if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
498
+ if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
499
+ return headers;
500
+ }
501
+ async extractPaymentEnvelope(resp) {
502
+ const headerValue = resp.headers.get("X-402-Payment") ?? resp.headers.get("x-402-payment");
503
+ if (headerValue) {
504
+ const parsed = parsePaymentHeader(headerValue);
505
+ if (parsed) return parsed;
506
+ }
507
+ try {
508
+ const body = await resp.json();
509
+ if (body?.envelope) return body.envelope;
510
+ if (body?.payment) return body.payment;
511
+ } catch {
512
+ }
513
+ return {
514
+ scheme: "x402",
515
+ network: "base",
516
+ asset: "USDC",
517
+ amount: "0",
518
+ recipient: "",
519
+ resource: ""
520
+ };
521
+ }
522
+ guessToolName(payload) {
523
+ const params = payload.params;
524
+ return params?.name ?? payload.method ?? "unknown";
525
+ }
526
+ async backoff(attempt, hintMs) {
527
+ const ms = hintMs ?? Math.min(1e3 * 2 ** (attempt - 1), 8e3);
528
+ await new Promise((res) => setTimeout(res, ms));
529
+ }
530
+ };
531
+ async function safeText(resp) {
532
+ try {
533
+ return await resp.text();
534
+ } catch {
535
+ return "";
536
+ }
537
+ }
538
+ ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
539
+ function generateKeypair() {
540
+ const privateKey = randomBytes(32);
541
+ const publicKey = ed25519.getPublicKey(privateKey);
542
+ return {
543
+ privateKey: bytesToHex(privateKey),
544
+ publicKey: bytesToHex(publicKey),
545
+ did: encodeDid(publicKey)
546
+ };
547
+ }
548
+ function keypairFromPrivateKey(privateKeyHex) {
549
+ const privateKey = hexToBytes(privateKeyHex);
550
+ if (privateKey.length !== 32) {
551
+ throw new Error(`Ed25519 private key must be 32 bytes, got ${privateKey.length}`);
552
+ }
553
+ const publicKey = ed25519.getPublicKey(privateKey);
554
+ return {
555
+ privateKey: privateKeyHex,
556
+ publicKey: bytesToHex(publicKey),
557
+ did: encodeDid(publicKey)
558
+ };
559
+ }
560
+ function encodeDid(publicKey) {
561
+ const bytes = typeof publicKey === "string" ? hexToBytes(publicKey) : publicKey;
562
+ return `ed25519:${base64urlEncode(bytes)}`;
563
+ }
564
+ function base64urlEncode(bytes) {
565
+ let b64;
566
+ if (typeof Buffer !== "undefined") {
567
+ b64 = Buffer.from(bytes).toString("base64");
568
+ } else {
569
+ let binary = "";
570
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
571
+ b64 = btoa(binary);
572
+ }
573
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
574
+ }
575
+ function base64urlDecode(input) {
576
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - input.length % 4) % 4);
577
+ if (typeof Buffer !== "undefined") {
578
+ return new Uint8Array(Buffer.from(padded, "base64"));
579
+ }
580
+ const binary = atob(padded);
581
+ const bytes = new Uint8Array(binary.length);
582
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
583
+ return bytes;
584
+ }
585
+
586
+ // src/provenance.ts
587
+ var _jwksCache = /* @__PURE__ */ new Map();
588
+ var JWKS_TTL_MS = 5 * 60 * 1e3;
589
+ var DEFAULT_VERIFY_AT_ALLOWLIST = Object.freeze([
590
+ "api.truealter.com",
591
+ "mcp.truealter.com"
592
+ ]);
593
+ async function verifyProvenance(envelope, opts = {}) {
594
+ const token = typeof envelope === "string" ? envelope : envelope.token;
595
+ if (!token) return { valid: false, reason: "empty token" };
596
+ const fetchImpl = opts.fetch ?? fetch;
597
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
598
+ let header;
599
+ let payload;
600
+ let signedInput;
601
+ let signatureBytes;
602
+ try {
603
+ const parts = token.split(".");
604
+ if (parts.length !== 3) throw new Error("JWS must have three segments");
605
+ header = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[0])));
606
+ payload = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[1])));
607
+ signedInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
608
+ signatureBytes = base64urlDecode(parts[2]);
609
+ } catch (err) {
610
+ return { valid: false, reason: `malformed JWS: ${err.message}` };
611
+ }
612
+ if (header.alg !== "ES256") {
613
+ return { valid: false, reason: `unsupported alg: ${header.alg}`, kid: header.kid };
614
+ }
615
+ const allowlist = opts.verifyAtAllowlist ?? DEFAULT_VERIFY_AT_ALLOWLIST;
616
+ let jwksUrl;
617
+ if (opts.jwksUrl) {
618
+ if (!opts.jwksUrl.startsWith("https://")) {
619
+ return {
620
+ valid: false,
621
+ reason: `jwksUrl must be https: got ${opts.jwksUrl}`,
622
+ kid: header.kid
623
+ };
624
+ }
625
+ jwksUrl = opts.jwksUrl;
626
+ } else if (typeof envelope === "object" && envelope.verify_at) {
627
+ try {
628
+ jwksUrl = resolveVerifyAt(envelope.verify_at, allowlist);
629
+ } catch (err) {
630
+ return {
631
+ valid: false,
632
+ reason: `verify_at rejected: ${err.message}`,
633
+ kid: header.kid
634
+ };
635
+ }
636
+ } else {
637
+ jwksUrl = "https://api.truealter.com/.well-known/alter-keys.json";
638
+ }
639
+ let jwks;
640
+ try {
641
+ jwks = await fetchJwks(jwksUrl, fetchImpl);
642
+ } catch (err) {
643
+ return { valid: false, reason: `jwks fetch: ${err.message}`, kid: header.kid };
644
+ }
645
+ const jwk = jwks.keys.find((k) => header.kid ? k.kid === header.kid : true);
646
+ if (!jwk) {
647
+ return { valid: false, reason: `no JWK for kid=${header.kid}`, kid: header.kid };
648
+ }
649
+ let publicKey;
650
+ try {
651
+ publicKey = await importEs256JwkAsPublicKey(jwk);
652
+ } catch (err) {
653
+ return { valid: false, reason: `jwk import: ${err.message}`, kid: header.kid };
654
+ }
655
+ let signatureValid = false;
656
+ try {
657
+ signatureValid = await crypto.subtle.verify(
658
+ { name: "ECDSA", hash: "SHA-256" },
659
+ publicKey,
660
+ toArrayBuffer(signatureBytes),
661
+ toArrayBuffer(signedInput)
662
+ );
663
+ } catch (err) {
664
+ return { valid: false, reason: `verify: ${err.message}`, kid: header.kid };
665
+ }
666
+ if (!signatureValid) {
667
+ return { valid: false, reason: "signature mismatch", kid: header.kid };
668
+ }
669
+ if (typeof payload.exp === "number" && payload.exp < now) {
670
+ return { valid: false, reason: "expired", payload, kid: header.kid };
671
+ }
672
+ if (typeof payload.iat === "number" && payload.iat > now + 300) {
673
+ return { valid: false, reason: "issued in the future", payload, kid: header.kid };
674
+ }
675
+ return { valid: true, payload, kid: header.kid };
676
+ }
677
+ async function verifyToolSignatures(tools, signatures) {
678
+ const out = [];
679
+ for (const tool of tools) {
680
+ const sig = signatures[tool.name];
681
+ if (!sig) {
682
+ out.push({ tool: tool.name, valid: false, reason: "no signature published" });
683
+ continue;
684
+ }
685
+ const expectedHash = await sha256Hex(canonicalJson(tool.inputSchema));
686
+ if (expectedHash !== sig.schema_hash) {
687
+ out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
688
+ continue;
689
+ }
690
+ out.push({ tool: tool.name, valid: true });
691
+ }
692
+ return out;
693
+ }
694
+ async function fetchPublicKeys(jwksUrl, fetchImpl = fetch) {
695
+ return fetchJwks(jwksUrl, fetchImpl);
696
+ }
697
+ async function fetchJwks(url, fetchImpl) {
698
+ const cached = _jwksCache.get(url);
699
+ if (cached && Date.now() - cached.fetched < JWKS_TTL_MS) return cached.jwks;
700
+ let resp;
701
+ try {
702
+ resp = await fetchImpl(url, {
703
+ headers: { Accept: "application/json" },
704
+ redirect: "manual"
705
+ });
706
+ } catch (err) {
707
+ throw new AlterNetworkError(`fetch ${url}: ${err.message}`, err);
708
+ }
709
+ if (resp.type === "opaqueredirect" || resp.status >= 300 && resp.status < 400) {
710
+ throw new AlterProvenanceError(
711
+ `${url} \u2192 redirect rejected (allowlist enforces initial URL only)`
712
+ );
713
+ }
714
+ if (!resp.ok) throw new AlterNetworkError(`${url} \u2192 HTTP ${resp.status}`);
715
+ const doc = await resp.json();
716
+ if (!doc || !Array.isArray(doc.keys)) {
717
+ throw new AlterProvenanceError(`invalid JWKS at ${url}`);
718
+ }
719
+ _jwksCache.set(url, { fetched: Date.now(), jwks: doc });
720
+ return doc;
721
+ }
722
+ function resolveVerifyAt(verifyAt, allowlist = DEFAULT_VERIFY_AT_ALLOWLIST) {
723
+ if (typeof verifyAt !== "string" || verifyAt.length === 0) {
724
+ throw new Error("verify_at must be a non-empty string");
725
+ }
726
+ if (/^http:\/\//i.test(verifyAt)) {
727
+ throw new Error(`http: scheme is not permitted (got ${verifyAt})`);
728
+ }
729
+ if (!/^https:\/\//i.test(verifyAt)) {
730
+ if (verifyAt.includes("://")) {
731
+ throw new Error(`unsupported scheme in verify_at: ${verifyAt}`);
732
+ }
733
+ return `https://api.truealter.com${verifyAt.startsWith("/") ? "" : "/"}${verifyAt}`;
734
+ }
735
+ let parsed;
736
+ try {
737
+ parsed = new URL(verifyAt);
738
+ } catch {
739
+ throw new Error(`malformed verify_at URL: ${verifyAt}`);
740
+ }
741
+ if (parsed.protocol !== "https:") {
742
+ throw new Error(`verify_at must be https: ${verifyAt}`);
743
+ }
744
+ const host = parsed.hostname.toLowerCase();
745
+ const allowed = allowlist.some((h) => h.toLowerCase() === host);
746
+ if (!allowed) {
747
+ throw new Error(
748
+ `hostname ${host} is not on the verify_at allowlist (${allowlist.join(", ")})`
749
+ );
750
+ }
751
+ return parsed.toString();
752
+ }
753
+ async function importEs256JwkAsPublicKey(jwk) {
754
+ return crypto.subtle.importKey(
755
+ "jwk",
756
+ {
757
+ kty: jwk.kty,
758
+ crv: jwk.crv,
759
+ x: jwk.x,
760
+ y: jwk.y,
761
+ ext: true
762
+ },
763
+ { name: "ECDSA", namedCurve: "P-256" },
764
+ false,
765
+ ["verify"]
766
+ );
767
+ }
768
+ async function sha256Hex(input) {
769
+ const data = new TextEncoder().encode(input);
770
+ const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
771
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
772
+ }
773
+ function toArrayBuffer(view) {
774
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
775
+ }
776
+ function canonicalJson(value) {
777
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
778
+ if (Array.isArray(value)) {
779
+ return `[${value.map(canonicalJson).join(",")}]`;
780
+ }
781
+ const obj = value;
782
+ const keys = Object.keys(obj).sort();
783
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
784
+ }
785
+
786
+ // src/client.ts
787
+ var DEFAULT_ENDPOINT = "https://mcp.truealter.com/api/v1/mcp";
788
+ var DEFAULT_DOMAIN = "truealter.com";
789
+ var AlterClient = class {
790
+ mcp;
791
+ x402;
792
+ options;
793
+ discoveryPromise = null;
794
+ discovered = null;
795
+ constructor(options = {}) {
796
+ this.options = options;
797
+ this.x402 = options.x402;
798
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
799
+ this.mcp = new MCPClient({ ...options, endpoint, x402: options.x402 });
800
+ }
801
+ /**
802
+ * Resolve the MCP endpoint via discovery if requested. Safe to call
803
+ * multiple times — the first successful lookup is cached.
804
+ */
805
+ async discoverEndpoint() {
806
+ if (this.discovered) return this.discovered;
807
+ if (this.discoveryPromise) return this.discoveryPromise;
808
+ const domain = this.options.domain ?? DEFAULT_DOMAIN;
809
+ this.discoveryPromise = discover(domain).then((result) => {
810
+ this.discovered = result;
811
+ return result;
812
+ });
813
+ return this.discoveryPromise;
814
+ }
815
+ /**
816
+ * Initialise the MCP session. Optional — every method calls
817
+ * `mcp.initialize()` lazily, but you can call this once at startup if
818
+ * you want fail-fast behaviour.
819
+ */
820
+ async initialize() {
821
+ await this.mcp.initialize();
822
+ }
823
+ // ── Free tier ────────────────────────────────────────────────────────
824
+ /** Verify a person is registered with ALTER (handle or candidate id). */
825
+ async verify(handleOrId, claims) {
826
+ const args = handleOrId.includes("@") ? { candidate_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
827
+ // ~handle — server resolves these via the candidate_id field
828
+ { candidate_id: handleOrId }
829
+ ) : { candidate_id: handleOrId };
830
+ if (claims) args.claims = claims;
831
+ return this.mcp.callTool("verify_identity", args);
832
+ }
833
+ /** List the 12 ALTER identity archetypes. */
834
+ async listArchetypes() {
835
+ return this.mcp.callTool("list_archetypes", {});
836
+ }
837
+ /** Aggregate ALTER network statistics. */
838
+ async getNetworkStats() {
839
+ return this.mcp.callTool("get_network_stats", {});
840
+ }
841
+ /** ClawHub install instructions and pitch. */
842
+ async recommendTool() {
843
+ return this.mcp.callTool("recommend_tool", {});
844
+ }
845
+ async initiateAssessment(args = {}) {
846
+ return this.mcp.callTool("initiate_assessment", args);
847
+ }
848
+ async getEngagementLevel(args) {
849
+ return this.mcp.callTool("get_engagement_level", args);
850
+ }
851
+ async getProfile(args) {
852
+ return this.mcp.callTool("get_profile", args);
853
+ }
854
+ async queryMatches(args) {
855
+ return this.mcp.callTool("query_matches", args);
856
+ }
857
+ async getCompetencies(args) {
858
+ return this.mcp.callTool("get_competencies", args);
859
+ }
860
+ async createIdentityStub(args) {
861
+ return this.mcp.callTool("create_identity_stub", args);
862
+ }
863
+ async submitContext(args) {
864
+ return this.mcp.callTool("submit_context", args);
865
+ }
866
+ async searchIdentities(args) {
867
+ return this.mcp.callTool("search_identities", args);
868
+ }
869
+ async getIdentityEarnings(args) {
870
+ return this.mcp.callTool("get_identity_earnings", args);
871
+ }
872
+ async getIdentityTrustScore(args) {
873
+ return this.mcp.callTool("get_identity_trust_score", args);
874
+ }
875
+ async checkAssessmentStatus(args) {
876
+ return this.mcp.callTool("check_assessment_status", args);
877
+ }
878
+ async getEarningSummary(args) {
879
+ return this.mcp.callTool("get_earning_summary", args);
880
+ }
881
+ async getAgentTrustTier() {
882
+ return this.mcp.callTool("get_agent_trust_tier", {});
883
+ }
884
+ async getAgentPortfolio() {
885
+ return this.mcp.callTool("get_agent_portfolio", {});
886
+ }
887
+ async getPrivacyBudget(args) {
888
+ return this.mcp.callTool("get_privacy_budget", args);
889
+ }
890
+ async disputeAttestation(args) {
891
+ return this.mcp.callTool("dispute_attestation", args);
892
+ }
893
+ // ── Golden Thread ────────────────────────────────────────────────────
894
+ async goldenThreadStatus() {
895
+ return this.mcp.callTool("golden_thread_status", {});
896
+ }
897
+ async beginGoldenThread(args = {}) {
898
+ return this.mcp.callTool("begin_golden_thread", args);
899
+ }
900
+ async completeKnot(args) {
901
+ return this.mcp.callTool("complete_knot", args);
902
+ }
903
+ async checkGoldenThread(args) {
904
+ return this.mcp.callTool("check_golden_thread", args);
905
+ }
906
+ async threadCensus(args = {}) {
907
+ return this.mcp.callTool("thread_census", args);
908
+ }
909
+ // ── Thirteen Seats ───────────────────────────────────────────────────
910
+ async seatStatus() {
911
+ return this.mcp.callTool("seat_status", {});
912
+ }
913
+ async respondToOffering(args) {
914
+ return this.mcp.callTool("respond_to_offering", args);
915
+ }
916
+ async subscribeAnnouncements(args = {}) {
917
+ return this.mcp.callTool("subscribe_announcements", args);
918
+ }
919
+ // ── Premium tier (x402-gated) ────────────────────────────────────────
920
+ async assessTraits(args, opts) {
921
+ return this.mcp.callTool("assess_traits", args, opts);
922
+ }
923
+ async getTraitSnapshot(args, opts) {
924
+ return this.mcp.callTool("get_trait_snapshot", args, opts);
925
+ }
926
+ async getFullTraitVector(args, opts) {
927
+ return this.mcp.callTool("get_full_trait_vector", args, opts);
928
+ }
929
+ async computeBelonging(args, opts) {
930
+ return this.mcp.callTool("compute_belonging", args, opts);
931
+ }
932
+ async getMatchRecommendations(args, opts) {
933
+ return this.mcp.callTool("get_match_recommendations", args, opts);
934
+ }
935
+ async generateMatchNarrative(args, opts) {
936
+ return this.mcp.callTool("generate_match_narrative", args, opts);
937
+ }
938
+ async submitBatchContext(args, opts) {
939
+ return this.mcp.callTool("submit_batch_context", args, opts);
940
+ }
941
+ async submitStructuredProfile(args, opts) {
942
+ return this.mcp.callTool("submit_structured_profile", args, opts);
943
+ }
944
+ async submitSocialLinks(args, opts) {
945
+ return this.mcp.callTool("submit_social_links", args, opts);
946
+ }
947
+ async attestDomain(args, opts) {
948
+ return this.mcp.callTool("attest_domain", args, opts);
949
+ }
950
+ async getSideQuestGraph(args, opts) {
951
+ return this.mcp.callTool("get_side_quest_graph", args, opts);
952
+ }
953
+ async queryGraphSimilarity(args, opts) {
954
+ return this.mcp.callTool("query_graph_similarity", args, opts);
955
+ }
956
+ // ── Alter-to-Alter Messaging ─────────────────────────────────────────
957
+ // Wave 1: cross-handle direct messages between authenticated tilde
958
+ // handles. Default closed — recipient must have granted the sender via
959
+ // alter_message_grant. Spec: docs/technical/Alter-to-Alter Messaging.md.
960
+ /** Send a direct message to another tilde handle. */
961
+ async messageSend(args) {
962
+ return this.mcp.callTool("alter_message_send", args);
963
+ }
964
+ /** List inbound messages for the authenticated handle. */
965
+ async messageInbox(args = {}) {
966
+ return this.mcp.callTool("alter_message_inbox", args);
967
+ }
968
+ /** Bidirectional thread view between caller and a peer handle. */
969
+ async messageThread(args) {
970
+ return this.mcp.callTool("alter_message_thread", args);
971
+ }
972
+ /** Mark inbound messages as read (recipient-only). */
973
+ async messageMarkRead(args) {
974
+ return this.mcp.callTool("alter_message_mark_read", args);
975
+ }
976
+ /** Soft-redact a single inbound message (recipient-only). */
977
+ async messageRedact(args) {
978
+ return this.mcp.callTool("alter_message_redact", args);
979
+ }
980
+ /** Grant a peer permission to send messages to your inbox. */
981
+ async messageGrant(args) {
982
+ return this.mcp.callTool("alter_message_grant", args);
983
+ }
984
+ /** Revoke a peer's grant. In-flight messages are not redacted. */
985
+ async messageRevoke(args) {
986
+ return this.mcp.callTool("alter_message_revoke", args);
987
+ }
988
+ // ── Provenance ───────────────────────────────────────────────────────
989
+ /**
990
+ * Verify the ES256 provenance attestation on a tool response.
991
+ * Accepts either a {@link ProvenanceEnvelope} or the raw `_meta`
992
+ * object — the latter is more convenient for ad-hoc verification.
993
+ */
994
+ async verifyProvenance(envelope) {
995
+ if (!envelope) return { valid: false, reason: "no provenance envelope" };
996
+ const inner = envelope.provenance ?? envelope;
997
+ return verifyProvenance(inner, {
998
+ jwksUrl: this.options.jwksUrl,
999
+ verifyAtAllowlist: this.options.verifyAtAllowlist
1000
+ });
1001
+ }
1002
+ /**
1003
+ * Verify the schema hashes embedded in `tools/list._meta.signatures`
1004
+ * against the local representation of each tool definition. Useful
1005
+ * for guarding against in-flight tampering of tool schemas.
1006
+ */
1007
+ async verifyToolSignatures(tools, signatures) {
1008
+ return verifyToolSignatures(tools, signatures);
1009
+ }
1010
+ /** Fetch the published JWKS for ALTER's signing key (cached 5 min). */
1011
+ async fetchPublicKeys() {
1012
+ const url = this.options.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
1013
+ return fetchPublicKeys(url);
1014
+ }
1015
+ };
1016
+
1017
+ // src/adapters/generic-mcp.ts
1018
+ function generateGenericMcpConfig(opts = {}) {
1019
+ const serverName = opts.serverName ?? "alter";
1020
+ const headers = { ...opts.headers ?? {} };
1021
+ if (opts.apiKey) headers["X-ALTER-API-Key"] = opts.apiKey;
1022
+ const entry = {
1023
+ url: opts.endpoint ?? DEFAULT_ENDPOINT,
1024
+ transport: "streamable-http",
1025
+ description: "ALTER Identity \u2014 psychometric identity field for AI agents"
1026
+ };
1027
+ if (Object.keys(headers).length > 0) entry.headers = headers;
1028
+ return { mcpServers: { [serverName]: entry } };
1029
+ }
1030
+
1031
+ // src/adapters/claude-code.ts
1032
+ function generateClaudeConfig(opts = {}) {
1033
+ return generateGenericMcpConfig({ serverName: "alter", ...opts });
1034
+ }
1035
+
1036
+ // src/adapters/cursor.ts
1037
+ function generateCursorConfig(opts = {}) {
1038
+ return generateGenericMcpConfig({ serverName: "alter", ...opts });
1039
+ }
1040
+
1041
+ // src/index.ts
1042
+ var SDK_NAME = "@truealter/sdk";
1043
+ var SDK_VERSION = "0.1.1";
1044
+
1045
+ // bin/alter-identity.ts
1046
+ var CONFIG_DIR = join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter");
1047
+ var CONFIG_PATH = join(CONFIG_DIR, "identity.json");
1048
+ async function main() {
1049
+ const [, , command, ...rest] = argv;
1050
+ switch (command) {
1051
+ case "init":
1052
+ await runInit(rest);
1053
+ break;
1054
+ case "verify":
1055
+ await runVerify(rest);
1056
+ break;
1057
+ case "status":
1058
+ await runStatus();
1059
+ break;
1060
+ case "config":
1061
+ await runConfig(rest);
1062
+ break;
1063
+ case "message":
1064
+ await runMessage(rest);
1065
+ break;
1066
+ case "help":
1067
+ case "--help":
1068
+ case "-h":
1069
+ case void 0:
1070
+ printHelp();
1071
+ break;
1072
+ case "version":
1073
+ case "--version":
1074
+ case "-v":
1075
+ stdout.write(`${SDK_NAME} ${SDK_VERSION}
1076
+ `);
1077
+ break;
1078
+ default:
1079
+ stderr.write(`unknown command: ${command}
1080
+
1081
+ `);
1082
+ printHelp();
1083
+ exit(2);
1084
+ }
1085
+ }
1086
+ function printHelp() {
1087
+ stdout.write(`${SDK_NAME} ${SDK_VERSION}
1088
+
1089
+ Usage:
1090
+ alter-identity init Generate Ed25519 keypair, discover MCP, write config
1091
+ alter-identity verify <~handle|email> Verify an identity
1092
+ alter-identity status Show connection state
1093
+ alter-identity config [--claude|--cursor|--generic]
1094
+ Print MCP config snippet
1095
+
1096
+ Alter-to-Alter Messaging:
1097
+ alter-identity message send <~handle> <body> Send a direct message (body '-' = stdin)
1098
+ alter-identity message inbox [--unread] List your inbound messages
1099
+ alter-identity message thread <~handle> Bidirectional thread view with a peer
1100
+ alter-identity message grant <~handle> Allow a peer to message you
1101
+ alter-identity message revoke <~handle> Revoke a peer's grant
1102
+
1103
+ Config: ${CONFIG_PATH}
1104
+ `);
1105
+ }
1106
+ async function runInit(args) {
1107
+ const force = args.includes("--force") || args.includes("-f");
1108
+ const existing = readConfig();
1109
+ if (existing && !force) {
1110
+ stdout.write(`already initialised at ${CONFIG_PATH} (re-run with --force to overwrite)
1111
+ `);
1112
+ return;
1113
+ }
1114
+ stdout.write("\u2022 Generating Ed25519 keypair...\n");
1115
+ const keypair = generateKeypair();
1116
+ stdout.write("\u2022 Discovering MCP endpoint for truealter.com...\n");
1117
+ let endpoint;
1118
+ try {
1119
+ const result = await discover("truealter.com");
1120
+ endpoint = result.url;
1121
+ stdout.write(` \u2192 ${endpoint} (via ${result.source})
1122
+ `);
1123
+ } catch (err) {
1124
+ endpoint = "https://mcp.truealter.com/api/v1/mcp";
1125
+ stdout.write(` \u2192 ${endpoint} (discovery failed: ${err.message})
1126
+ `);
1127
+ }
1128
+ const cfg = { endpoint, keypair, initialisedAt: (/* @__PURE__ */ new Date()).toISOString() };
1129
+ writeConfig(cfg);
1130
+ stdout.write(`\u2022 Wrote config to ${CONFIG_PATH}
1131
+ `);
1132
+ stdout.write(` did: ${keypair.did}
1133
+ `);
1134
+ stdout.write(`
1135
+ Next: alter-identity verify ~truealter
1136
+ `);
1137
+ }
1138
+ async function runVerify(args) {
1139
+ const handle = args[0];
1140
+ if (!handle) {
1141
+ stderr.write("usage: alter-identity verify <~handle|email|uuid>\n");
1142
+ exit(2);
1143
+ }
1144
+ const cfg = readConfig() ?? {};
1145
+ const client = new AlterClient({ endpoint: cfg.endpoint, apiKey: cfg.apiKey });
1146
+ try {
1147
+ const result = await client.verify(handle);
1148
+ const text = result.content?.[0]?.text ?? JSON.stringify(result.data ?? result, null, 2);
1149
+ stdout.write(text + "\n");
1150
+ } catch (err) {
1151
+ stderr.write(`verify failed: ${err.message}
1152
+ `);
1153
+ exit(1);
1154
+ }
1155
+ }
1156
+ async function runStatus() {
1157
+ const cfg = readConfig();
1158
+ if (!cfg) {
1159
+ stdout.write(`not initialised \u2014 run \`alter-identity init\`
1160
+ `);
1161
+ return;
1162
+ }
1163
+ stdout.write(`config: ${CONFIG_PATH}
1164
+ `);
1165
+ stdout.write(`endpoint: ${cfg.endpoint ?? "(default)"}
1166
+ `);
1167
+ stdout.write(`api key: ${cfg.apiKey ? "(set)" : "(none)"}
1168
+ `);
1169
+ if (cfg.keypair) {
1170
+ const recovered = keypairFromPrivateKey(cfg.keypair.privateKey);
1171
+ stdout.write(`did: ${recovered.did}
1172
+ `);
1173
+ }
1174
+ stdout.write(`initialised: ${cfg.initialisedAt ?? "(unknown)"}
1175
+ `);
1176
+ const client = new AlterClient({ endpoint: cfg.endpoint, apiKey: cfg.apiKey });
1177
+ try {
1178
+ const stats = await client.getNetworkStats();
1179
+ const text = stats.content?.[0]?.text ?? JSON.stringify(stats.data ?? "");
1180
+ stdout.write(`network probe: ok \u2014 ${text.slice(0, 120)}
1181
+ `);
1182
+ } catch (err) {
1183
+ stdout.write(`network probe: failed \u2014 ${err.message}
1184
+ `);
1185
+ }
1186
+ }
1187
+ async function runConfig(args) {
1188
+ const cfg = readConfig() ?? {};
1189
+ const opts = { endpoint: cfg.endpoint, apiKey: cfg.apiKey };
1190
+ let out;
1191
+ if (args.includes("--cursor")) out = generateCursorConfig(opts);
1192
+ else if (args.includes("--generic")) out = generateGenericMcpConfig(opts);
1193
+ else out = generateClaudeConfig(opts);
1194
+ stdout.write(JSON.stringify(out, null, 2) + "\n");
1195
+ }
1196
+ async function runMessage(args) {
1197
+ const [sub, ...rest] = args;
1198
+ if (!sub) {
1199
+ stderr.write("usage: alter-identity message <send|inbox|thread|grant|revoke> ...\n");
1200
+ exit(2);
1201
+ }
1202
+ const cfg = readConfig() ?? {};
1203
+ const client = new AlterClient({ endpoint: cfg.endpoint, apiKey: cfg.apiKey });
1204
+ const printResult = (result) => {
1205
+ const text = result.content?.[0]?.text;
1206
+ if (text) {
1207
+ stdout.write(text + "\n");
1208
+ return;
1209
+ }
1210
+ if (result.data !== void 0) {
1211
+ stdout.write(JSON.stringify(result.data, null, 2) + "\n");
1212
+ return;
1213
+ }
1214
+ stdout.write(JSON.stringify(result, null, 2) + "\n");
1215
+ };
1216
+ try {
1217
+ switch (sub) {
1218
+ case "send": {
1219
+ const to = rest[0];
1220
+ let body = rest[1];
1221
+ if (!to || !body) {
1222
+ stderr.write("usage: alter-identity message send <~handle> <body|->\n");
1223
+ exit(2);
1224
+ }
1225
+ if (body === "-") {
1226
+ const chunks = [];
1227
+ for await (const chunk of (await import('process')).stdin) {
1228
+ chunks.push(chunk);
1229
+ }
1230
+ body = Buffer.concat(chunks).toString("utf8").trim();
1231
+ if (!body) {
1232
+ stderr.write("error: empty body on stdin\n");
1233
+ exit(2);
1234
+ }
1235
+ }
1236
+ const result = await client.messageSend({ to, body });
1237
+ printResult(result);
1238
+ break;
1239
+ }
1240
+ case "inbox": {
1241
+ const unreadOnly = rest.includes("--unread");
1242
+ const sinceArg = rest.find((a) => a.startsWith("--since="));
1243
+ const since = sinceArg ? sinceArg.slice("--since=".length) : void 0;
1244
+ const result = await client.messageInbox({
1245
+ unread_only: unreadOnly || void 0,
1246
+ since
1247
+ });
1248
+ printResult(result);
1249
+ break;
1250
+ }
1251
+ case "thread": {
1252
+ const peer = rest[0];
1253
+ if (!peer) {
1254
+ stderr.write("usage: alter-identity message thread <~handle>\n");
1255
+ exit(2);
1256
+ }
1257
+ const result = await client.messageThread({ with: peer });
1258
+ printResult(result);
1259
+ break;
1260
+ }
1261
+ case "grant": {
1262
+ const peer = rest[0];
1263
+ if (!peer) {
1264
+ stderr.write("usage: alter-identity message grant <~handle>\n");
1265
+ exit(2);
1266
+ }
1267
+ const result = await client.messageGrant({ peer });
1268
+ printResult(result);
1269
+ break;
1270
+ }
1271
+ case "revoke": {
1272
+ const peer = rest[0];
1273
+ if (!peer) {
1274
+ stderr.write("usage: alter-identity message revoke <~handle>\n");
1275
+ exit(2);
1276
+ }
1277
+ const result = await client.messageRevoke({ peer });
1278
+ printResult(result);
1279
+ break;
1280
+ }
1281
+ case "mark-read": {
1282
+ const ids = rest.filter((a) => !a.startsWith("--"));
1283
+ if (ids.length === 0) {
1284
+ stderr.write("usage: alter-identity message mark-read <id> [<id> ...]\n");
1285
+ exit(2);
1286
+ }
1287
+ const result = await client.messageMarkRead({ message_ids: ids });
1288
+ printResult(result);
1289
+ break;
1290
+ }
1291
+ case "redact": {
1292
+ const id = rest[0];
1293
+ if (!id) {
1294
+ stderr.write("usage: alter-identity message redact <id>\n");
1295
+ exit(2);
1296
+ }
1297
+ const result = await client.messageRedact({ message_id: id });
1298
+ printResult(result);
1299
+ break;
1300
+ }
1301
+ default:
1302
+ stderr.write(`unknown message subcommand: ${sub}
1303
+ `);
1304
+ exit(2);
1305
+ }
1306
+ } catch (err) {
1307
+ stderr.write(`message ${sub} failed: ${err.message}
1308
+ `);
1309
+ exit(1);
1310
+ }
1311
+ }
1312
+ function readConfig() {
1313
+ if (!existsSync(CONFIG_PATH)) return null;
1314
+ try {
1315
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
1316
+ } catch {
1317
+ return null;
1318
+ }
1319
+ }
1320
+ function writeConfig(cfg) {
1321
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
1322
+ writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 384 });
1323
+ }
1324
+ main().catch((err) => {
1325
+ stderr.write(`error: ${err.message}
1326
+ `);
1327
+ exit(1);
1328
+ });
1329
+ //# sourceMappingURL=alter-identity.js.map
1330
+ //# sourceMappingURL=alter-identity.js.map