@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/dist/index.d.ts CHANGED
@@ -437,7 +437,7 @@ interface MCPClientInfo {
437
437
  version: string;
438
438
  }
439
439
  interface MCPClientOptions {
440
- /** Streamable HTTP endpoint. Default: https://mcp.truealter.com */
440
+ /** Streamable HTTP endpoint. Default: https://mcp.truealter.com/api/v1/mcp (public discovery). Member bridge runtime sets api.truealter.com via env or explicit option. */
441
441
  endpoint?: string;
442
442
  /** Optional API key for the `X-ALTER-API-Key` header. */
443
443
  apiKey?: string;
@@ -474,7 +474,7 @@ interface MCPClientOptions {
474
474
  /**
475
475
  * Optional hook invoked once, lazily, before the first network call
476
476
  * (the first `initialize()` or any direct `rpc()`). Used by
477
- * {@link AlterClient} to run the D-MIN-VERSION-FLOOR-1 preflight on
477
+ * {@link AlterClient} to run the version-floor preflight on
478
478
  * first request: not on import, not in the constructor. A throw
479
479
  * from the hook propagates to the caller of the first `tools/call`
480
480
  * (or `initialize()`).
@@ -486,7 +486,7 @@ interface MCPSigningOptions {
486
486
  kid: string;
487
487
  /** ES256 P-256 private key: 32-byte scalar or PEM. */
488
488
  privateKey: Uint8Array | string;
489
- /** The caller's bound ~handle. */
489
+ /** The caller's bound handle. */
490
490
  handle: string;
491
491
  }
492
492
  interface MCPCallOptions {
@@ -545,7 +545,7 @@ declare class MCPClient {
545
545
  private initialised;
546
546
  constructor(opts?: MCPClientOptions);
547
547
  /**
548
- * Run the lazy preflight hook (D-MIN-VERSION-FLOOR-1) exactly once.
548
+ * Run the lazy version-floor preflight hook exactly once.
549
549
  * Idempotent and serialised: concurrent callers share the same
550
550
  * promise. Throws from the hook propagate to every concurrent caller.
551
551
  */
@@ -1212,7 +1212,8 @@ declare const TOOL_BLAST_RADIUS: Record<ToolName, "low" | "medium" | "high">;
1212
1212
  * This is the entry point most consumers will use. It bundles
1213
1213
  * {@link MCPClient}, {@link X402Client}, discovery, and provenance
1214
1214
  * verification into a single ergonomic surface that mirrors the 32
1215
- * tools exposed at https://mcp.truealter.com/api/v1/mcp.
1215
+ * tools exposed at https://mcp.truealter.com/api/v1/mcp (public/discovery)
1216
+ * and https://api.truealter.com/api/v1/mcp (member bearer-first runtime).
1216
1217
  *
1217
1218
  * Free tier methods require no authentication. Premium methods accept
1218
1219
  * an `X402Client` (or fall back to throwing {@link AlterPaymentRequired}
@@ -1237,8 +1238,13 @@ interface AlterClientOptions extends Omit<MCPClientOptions, 'x402'> {
1237
1238
  */
1238
1239
  skipDiscovery?: boolean;
1239
1240
  /**
1240
- * URL of the JWKS document used for provenance verification. Defaults
1241
- * to `https://api.truealter.com/.well-known/alter-keys.json`.
1241
+ * URL of the JWKS document used for provenance verification.
1242
+ *
1243
+ * When unset, the JWKS host is derived from the configured API surface
1244
+ * ({@link resolveJwksUrl}: `apiBase` > `ALTER_API` env > the `endpoint`
1245
+ * origin > the canonical `api.truealter.com`), so the trust anchor
1246
+ * tracks whatever surface the client is actually pointed at rather than
1247
+ * a single hardcoded host.
1242
1248
  *
1243
1249
  * When set, this URL is used verbatim for every `verifyProvenance`
1244
1250
  * call and *overrides* any `verify_at` hint on the server response:
@@ -1258,7 +1264,7 @@ interface AlterClientOptions extends Omit<MCPClientOptions, 'x402'> {
1258
1264
  */
1259
1265
  verifyAtAllowlist?: readonly string[];
1260
1266
  /**
1261
- * Skip the D-MIN-VERSION-FLOOR-1 client-side preflight (the lazy
1267
+ * Skip the client-side version-floor preflight (the lazy
1262
1268
  * `checkMinVersion()` invocation on first request). Deliberately named
1263
1269
  * to discourage casual use: the server-side floor gate (Phase 1
1264
1270
  * middleware) still rejects below-floor clients with HTTP 426
@@ -1402,7 +1408,15 @@ declare class AlterClient {
1402
1408
  valid: boolean;
1403
1409
  reason?: string;
1404
1410
  }[]>;
1405
- /** Fetch the published JWKS for ALTER's signing key (cached 5 min). */
1411
+ /**
1412
+ * Fetch the published JWKS for ALTER's signing key (cached 5 min).
1413
+ *
1414
+ * The JWKS URL is derived from the configured API surface
1415
+ * ({@link resolveJwksUrl}: explicit `jwksUrl` > `apiBase` > `ALTER_API` >
1416
+ * the configured `endpoint` origin > the canonical host) rather than a
1417
+ * single hardcoded host, so the trust anchor tracks the surface the
1418
+ * client is actually pointed at.
1419
+ */
1406
1420
  fetchPublicKeys(): Promise<unknown>;
1407
1421
  }
1408
1422
 
@@ -1477,9 +1491,9 @@ interface SignInvocationOptions {
1477
1491
  declare function signInvocation(toolName: string, toolArgs: Record<string, unknown>, options: SignInvocationOptions): string;
1478
1492
 
1479
1493
  /**
1480
- * floor-preflight: SDK-side D-MIN-VERSION-FLOOR-1 Phase 4 enforcement.
1494
+ * floor-preflight: SDK-side client version-floor enforcement.
1481
1495
  *
1482
- * Sister to `alter-cli`'s `src/lib/floor-preflight.ts` (Phase 2). This is
1496
+ * Sister to the CLI client's own floor-preflight. This is
1483
1497
  * the `@truealter/sdk` implementation: it fires lazily on the first
1484
1498
  * authenticated network call from {@link AlterClient}, hits the public
1485
1499
  * floor endpoint, verifies the Ed25519 signature against the published
@@ -1505,8 +1519,8 @@ declare function signInvocation(toolName: string, toolArgs: Record<string, unkno
1505
1519
  * corresponding public key (SPKI PEM) in {@link KNOWN_FLOOR_PUBLIC_KEYS},
1506
1520
  * keyed by `key_id`. No signing secret ships in the client: this is
1507
1521
  * asymmetric: the public key cannot forge signatures. MUST stay
1508
- * byte-compatible with `backend/app/core/floor_signing.py` and the
1509
- * canonical TypeScript client (`alter-cli` `src/lib/floor-preflight.ts`):
1522
+ * byte-compatible with the ALTER backend floor-signing implementation
1523
+ * and the canonical TypeScript CLI client:
1510
1524
  * - Algorithm: Ed25519 (RFC 8032). Deterministic: same key + payload
1511
1525
  * always yields the same signature.
1512
1526
  * - `key_id` = first 8 hex chars of SHA-256(raw 32-byte public key).
@@ -1534,8 +1548,8 @@ declare const CLIENT_CHANNEL = "npm";
1534
1548
  /**
1535
1549
  * Compute the 8-char `key_id` for an Ed25519 public key (SPKI PEM).
1536
1550
  *
1537
- * Matches the backend `key_id_for_public_key(pub)` in
1538
- * `backend/app/core/floor_signing.py`:
1551
+ * Matches the backend `key_id_for_public_key(pub)` in the ALTER
1552
+ * backend floor-signing implementation:
1539
1553
  * raw = pub.public_bytes(Encoding.Raw, PublicFormat.Raw) # 32 bytes
1540
1554
  * sha256(raw).hexdigest()[:8]
1541
1555
  *
@@ -1552,7 +1566,7 @@ declare function computeKeyId(publicKeyPem: string): string;
1552
1566
  * stripped via the `,`/`:` separators; non-ASCII passes through as raw UTF-8
1553
1567
  * (Node's `JSON.stringify` never `\uXXXX`-escapes non-ASCII, matching the
1554
1568
  * backend's `ensure_ascii=False`). The result is the byte-exact input to
1555
- * Ed25519 signing (backend) and verification (this SDK and alter-cli) on
1569
+ * Ed25519 signing (backend) and verification (this SDK and the CLI client) on
1556
1570
  * both sides.
1557
1571
  */
1558
1572
  declare function canonicalJson(obj: unknown): string;
@@ -1562,12 +1576,12 @@ declare function canonicalJson(obj: unknown): string;
1562
1576
  * `key_id` = first 8 hex chars of SHA-256(raw 32-byte Ed25519 public key).
1563
1577
  * Use `computeKeyId(spkiPem)` to re-derive and validate the map key.
1564
1578
  *
1565
- * Backend may rotate via D-MSG11 dual-key path: clients keep N keys and
1579
+ * Backend may rotate via a dual-key path: clients keep N keys and
1566
1580
  * select via `key_id`. Add new keys additively; remove old keys only after
1567
1581
  * all deployed clients have received the new key.
1568
1582
  *
1569
1583
  * Current keys (mirrors `KNOWN_FLOOR_PUBLIC_KEYS` in the canonical
1570
- * `alter-cli` client):
1584
+ * CLI client):
1571
1585
  * 8aa59e05: dev/non-prod key (local + e2e + staging). Safe to ship publicly;
1572
1586
  * this is the PUBLIC key only, never the private key.
1573
1587
  * 640f7d9a: prod key. Added 2026-06-05. key_id = first 8 hex of
@@ -1707,7 +1721,7 @@ declare function compareSemver(a: string, b: string): number;
1707
1721
  * canonical JSON of `{ floors, served_at }`: `signature` + `key_id` +
1708
1722
  * `cache_ttl_seconds` are excluded from the signed bytes.
1709
1723
  *
1710
- * MUST stay byte-compatible with the backend (`backend/app/core/floor_signing.py`):
1724
+ * MUST stay byte-compatible with the ALTER backend floor-signing implementation:
1711
1725
  * - Algorithm: Ed25519 (RFC 8032). Deterministic: same key + payload = same sig.
1712
1726
  * - Payload: `canonicalJson({floors, served_at})`: sorted-keys + compact.
1713
1727
  * - `key_id`: first 8 hex chars of SHA-256(raw 32-byte Ed25519 public key).
@@ -1715,7 +1729,7 @@ declare function compareSemver(a: string, b: string): number;
1715
1729
  *
1716
1730
  * The `keys` parameter maps `key_id -> SPKI PEM string`; defaults to
1717
1731
  * `KNOWN_FLOOR_PUBLIC_KEYS`. Returns false on unknown key_id (triggers refetch
1718
- * per D-MSG11 rotation path).
1732
+ * per the key-rotation path).
1719
1733
  */
1720
1734
  declare function verifyFloorSignature(doc: FloorDocument, keys?: Record<string, string>): boolean;
1721
1735
 
@@ -1747,8 +1761,8 @@ declare function generateGenericMcpConfig(opts?: GenerateMcpConfigOptions): Gene
1747
1761
  /**
1748
1762
  * Claude Code MCP config helper.
1749
1763
  *
1750
- * Claude Code reads MCP servers from `.mcp.json` (project) or
1751
- * `~/.claude/mcp.json` (user). The shape matches `mcpServers`.
1764
+ * Claude Code reads MCP servers from the project-level `.mcp.json` or
1765
+ * the user-level Claude Code config. The shape matches `mcpServers`.
1752
1766
  */
1753
1767
 
1754
1768
  declare function generateClaudeConfig(opts?: GenerateMcpConfigOptions): GenericMcpConfig;
@@ -1766,10 +1780,11 @@ declare function generateCursorConfig(opts?: GenerateMcpConfigOptions): GenericM
1766
1780
  * Claude Desktop MCP config helper.
1767
1781
  *
1768
1782
  * Claude Desktop speaks stdio only: it does not currently dial
1769
- * Streamable-HTTP MCP servers directly. The canonical bridge is our
1770
- * own `alter-mcp-bridge` binary, which is published alongside this CLI
1771
- * in the same npm package. Desktop hosts then spawn the bridge as a
1772
- * child process and read JSON-RPC over stdin/stdout.
1783
+ * Streamable-HTTP MCP servers directly. The canonical bridge is the
1784
+ * `alter` CLI's `mcp-bridge` subcommand. The SDK no longer publishes a
1785
+ * standalone bridge binary; the CLI launches the bridge by file path.
1786
+ * Desktop hosts spawn `alter mcp-bridge` as a child process and read
1787
+ * JSON-RPC over stdin/stdout.
1773
1788
  *
1774
1789
  * Config file path varies by platform and is resolved in
1775
1790
  * `src/wire/paths.ts`. This adapter only produces the config *shape*.
@@ -1790,9 +1805,9 @@ interface GenerateClaudeDesktopOptions {
1790
1805
  apiKey?: string;
1791
1806
  /** Identifier used by Claude Desktop for this server. Default: `alter`. */
1792
1807
  serverName?: string;
1793
- /** Override the bridge command (e.g. `npx alter-mcp-bridge`). Default: bare `alter-mcp-bridge`. */
1808
+ /** Override the bridge command (e.g. `npx`). Default: the `alter` CLI. */
1794
1809
  bridgeCommand?: string;
1795
- /** Extra args appended after the default bridge args. */
1810
+ /** Extra args appended after the default `mcp-bridge` subcommand arg. */
1796
1811
  extraArgs?: string[];
1797
1812
  }
1798
1813
  declare function generateClaudeDesktopConfig(opts?: GenerateClaudeDesktopOptions): ClaudeDesktopConfig;
@@ -1943,6 +1958,15 @@ interface WireOptions {
1943
1958
  only?: readonly ClientId[];
1944
1959
  /** Skip any client whose probe said "not installed" even if the caller passed it via `only`. */
1945
1960
  skipMissing?: boolean;
1961
+ /**
1962
+ * Absolute path to the `alter` CLI launcher entry (its `dist/index.js`).
1963
+ * When set, Claude Code is wired to run `node <launcherPath> mcp-bridge`,
1964
+ * which injects the ES256 signing credential (ALTER_SIGNING_KEY /
1965
+ * ALTER_SIGNING_KID) from the OS secure store before spawning the bridge,
1966
+ * so MCP tool calls are signed. When absent (standalone SDK use without the
1967
+ * CLI), wiring falls back to `node <bridge>`: anonymous / L0, no signing.
1968
+ */
1969
+ launcherPath?: string;
1946
1970
  }
1947
1971
  interface WireReport {
1948
1972
  state: WireState;
@@ -1980,13 +2004,13 @@ declare const TIER_PRICES: Record<string, number>;
1980
2004
  /** Publicly advertised tool counts. Mirrors canonical-facts.json public_advertised. */
1981
2005
  declare const ADVERTISED_TOOL_COUNTS: {
1982
2006
  /** Free (L0) tools visible to anonymous / agent-class callers. */
1983
- readonly free: 26;
2007
+ readonly free: 27;
1984
2008
  /** Premium (L1-L5) tools requiring x402 payment. */
1985
- readonly premium: 8;
2009
+ readonly premium: 9;
1986
2010
  /** Messaging tools (member-self-only; excluded from external advertisement). */
1987
2011
  readonly messaging: 0;
1988
2012
  /** Total publicly advertised (free + premium). */
1989
- readonly total: 34;
2013
+ readonly total: 36;
1990
2014
  };
1991
2015
  /** x402 settlement revenue split. Weaver = the data subject earning Identity Income. */
1992
2016
  declare const REVENUE_SPLIT: {
@@ -2164,8 +2188,8 @@ interface HomepageOutput {
2164
2188
  */
2165
2189
  type HomepageCallerVertical = "workplace" | "education" | "personal" | "civic" | "agent" | "unknown";
2166
2190
  /** Maximum sizes from the spec. SDK consumers can use these to validate
2167
- * input before sending. Mirrored from
2168
- * `docs/technical/alter-portfolio-manifest-v1.md` (forthcoming). */
2191
+ * input before sending. Mirrored from the ALTER portfolio manifest spec v1
2192
+ * (forthcoming). */
2169
2193
  declare const HOMEPAGE_LIMITS: {
2170
2194
  readonly whoami_max_chars: 240;
2171
2195
  readonly opener_max_chars: 280;
@@ -2178,8 +2202,7 @@ declare const HOMEPAGE_LIMITS: {
2178
2202
  * @truealter/sdk: theme pack types
2179
2203
  *
2180
2204
  * Wire-format types for ALTER theme packs and `themes.lock` composition
2181
- * manifests. The full specification lives in
2182
- * `docs/technical/alter-theme-pack-spec-v1.md`.
2205
+ * manifests. The full specification lives in the ALTER theme pack spec v1.
2183
2206
  *
2184
2207
  * These types describe the on-the-wire shape of theme manifests as they
2185
2208
  * are produced by `alter theme install`, persisted to `themes.lock`,
@@ -2205,7 +2228,7 @@ interface ThemeMeta {
2205
2228
  name: string;
2206
2229
  /** SemVer-shaped recommended; informational only. Resolution uses pack_id. */
2207
2230
  version: string;
2208
- /** MUST be a ~handle whose D-ID8 public key signs the pack. */
2231
+ /** MUST be a ~handle whose signing public key signs the pack. */
2209
2232
  author: string;
2210
2233
  /** ≤ 240 characters after NFC. */
2211
2234
  description: string;
@@ -2267,7 +2290,7 @@ interface ThemeManifestV1 {
2267
2290
  }
2268
2291
  /**
2269
2292
  * The `.sig` file accompanying every pack. Verification logic lives in
2270
- * `alter-cli/src/theme/sign.ts`; this is the wire-format type only.
2293
+ * the CLI client; this is the wire-format type only.
2271
2294
  */
2272
2295
  interface ThemeSignatureManifest {
2273
2296
  /** SHA-256 multihash of the canonical-form pack. */
@@ -2303,7 +2326,7 @@ interface ThemeLockEntry {
2303
2326
  */
2304
2327
  interface ThemesLockV1 {
2305
2328
  schema_version: 1;
2306
- /** e.g. "alter-cli/0.5.0", informational only. */
2329
+ /** Free-form producer tag, e.g. "@truealter/cli/1.0.0", informational only. */
2307
2330
  generated_by: string;
2308
2331
  /** RFC 3339 UTC timestamp at lockfile-write time. */
2309
2332
  generated_at: string;
@@ -2342,7 +2365,7 @@ interface ThemeShareOutput {
2342
2365
  message: string;
2343
2366
  };
2344
2367
  }
2345
- /** v1 schema constants. Mirror the spec at docs/technical/alter-theme-pack-spec-v1.md. */
2368
+ /** v1 schema constants. Mirror the ALTER theme pack spec v1. */
2346
2369
  declare const THEME_LIMITS: {
2347
2370
  readonly meta_name_pattern: RegExp;
2348
2371
  readonly meta_description_max_chars: 240;
package/dist/index.js CHANGED
@@ -15,8 +15,13 @@ var __defProp = Object.defineProperty;
15
15
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
16
16
  var __getOwnPropNames = Object.getOwnPropertyNames;
17
17
  var __hasOwnProp = Object.prototype.hasOwnProperty;
18
- var __esm = (fn, res) => function __init() {
19
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
18
+ var __esm = (fn, res, err) => function __init() {
19
+ if (err) throw err[0];
20
+ try {
21
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
22
+ } catch (e) {
23
+ throw err = [e], e;
24
+ }
20
25
  };
21
26
  var __export = (target, all) => {
22
27
  for (var name in all)
@@ -404,7 +409,7 @@ function ensureMcpPath(url) {
404
409
 
405
410
  // src/meta.ts
406
411
  var SDK_NAME = "@truealter/sdk";
407
- var SDK_VERSION = "0.5.3" ;
412
+ var SDK_VERSION = "0.5.8" ;
408
413
 
409
414
  // src/floor-preflight.ts
410
415
  var MIN_VERSION_ENDPOINT = "/v1/clients/min-version";
@@ -832,7 +837,7 @@ var MCPClient = class {
832
837
  this.preflightHook = opts.preflightHook;
833
838
  }
834
839
  /**
835
- * Run the lazy preflight hook (D-MIN-VERSION-FLOOR-1) exactly once.
840
+ * Run the lazy version-floor preflight hook exactly once.
836
841
  * Idempotent and serialised: concurrent callers share the same
837
842
  * promise. Throws from the hook propagate to every concurrent caller.
838
843
  */
@@ -954,7 +959,14 @@ var MCPClient = class {
954
959
  method: "POST",
955
960
  headers: this.buildHeaders(signatureHeader),
956
961
  body: JSON.stringify(payload),
957
- signal: controller.signal
962
+ signal: controller.signal,
963
+ // Prevent fetch from silently following 3xx redirects. When
964
+ // Cloudflare Access credentials are absent or expired the edge
965
+ // returns HTTP 302 → CF Access login page (text/html). Without
966
+ // this option undici follows the redirect, lands on a 200 HTML
967
+ // body, and resp.json() throws the opaque "invalid JSON body"
968
+ // error that was surfaced as "MCP <method>: invalid JSON body".
969
+ redirect: "manual"
958
970
  });
959
971
  } catch (err) {
960
972
  clearTimeout(timer);
@@ -971,6 +983,19 @@ var MCPClient = class {
971
983
  clearTimeout(timer);
972
984
  const sessionHeader = resp.headers.get("Mcp-Session-Id");
973
985
  if (sessionHeader) this.sessionId = sessionHeader;
986
+ if (resp.status >= 300 && resp.status < 400) {
987
+ const location = resp.headers.get("Location") ?? "";
988
+ const isAuthRedirect = location.includes("cloudflareaccess.com") || location.includes("/cdn-cgi/access/") || !location.startsWith("/") && !location.startsWith(new URL(this.endpoint).origin);
989
+ if (isAuthRedirect) {
990
+ throw new AlterAuthError(
991
+ `MCP ${method}: Cloudflare Access blocked the request (session expired or credentials missing). Run \`alter login\` to re-authenticate.`,
992
+ 302
993
+ );
994
+ }
995
+ throw new AlterNetworkError(
996
+ `MCP ${method}: unexpected redirect ${resp.status} to ${location || "(no Location)"}`
997
+ );
998
+ }
974
999
  if (resp.status === 401 || resp.status === 403) {
975
1000
  throw new AlterAuthError(`HTTP ${resp.status} on ${method}`, resp.status);
976
1001
  }
@@ -995,11 +1020,56 @@ var MCPClient = class {
995
1020
  const body2 = await safeText(resp);
996
1021
  throw new AlterError("NETWORK", `HTTP ${resp.status} on ${method}: ${body2.slice(0, 200)}`);
997
1022
  }
1023
+ const contentType = resp.headers.get("Content-Type") ?? "";
1024
+ const isHtml = contentType.includes("text/html");
1025
+ const isSse = contentType.includes("text/event-stream");
1026
+ if (isHtml || isSse) {
1027
+ if (isSse) {
1028
+ const rawText = await safeText(resp);
1029
+ const dataLine = rawText.split("\n").find((l) => l.startsWith("data:"));
1030
+ if (dataLine) {
1031
+ const jsonPart = dataLine.slice("data:".length).trim();
1032
+ try {
1033
+ const parsed = JSON.parse(jsonPart);
1034
+ if (parsed.error) {
1035
+ const code = parsed.error.code;
1036
+ const message = parsed.error.message ?? `MCP ${method} error`;
1037
+ throw new AlterToolError(this.guessToolName(payload), message, code);
1038
+ }
1039
+ return parsed.result;
1040
+ } catch (parseErr) {
1041
+ if (parseErr instanceof AlterError) throw parseErr;
1042
+ throw new AlterInvalidResponse(
1043
+ `MCP ${method}: could not parse SSE data frame as JSON`,
1044
+ parseErr
1045
+ );
1046
+ }
1047
+ }
1048
+ throw new AlterInvalidResponse(
1049
+ `MCP ${method}: received text/event-stream response with no data: frame`
1050
+ );
1051
+ }
1052
+ const excerpt = (await safeText(resp)).slice(0, 300);
1053
+ const looksLikeLoginPage = excerpt.toLowerCase().includes("cloudflareaccess") || excerpt.toLowerCase().includes("access denied") || excerpt.toLowerCase().includes("<title>");
1054
+ if (looksLikeLoginPage) {
1055
+ throw new AlterAuthError(
1056
+ `MCP ${method}: received an HTML login page instead of JSON (Content-Type: ${contentType}). Run \`alter login\` to re-authenticate.`,
1057
+ 200
1058
+ );
1059
+ }
1060
+ throw new AlterInvalidResponse(
1061
+ `MCP ${method}: unexpected Content-Type "${contentType}" (expected application/json). Body excerpt: ${excerpt.slice(0, 120)}`
1062
+ );
1063
+ }
998
1064
  let body;
999
1065
  try {
1000
1066
  body = await resp.json();
1001
1067
  } catch (err) {
1002
- throw new AlterInvalidResponse(`MCP ${method}: invalid JSON body`, err);
1068
+ const hint = contentType ? ` (Content-Type: ${contentType})` : "";
1069
+ throw new AlterInvalidResponse(
1070
+ `MCP ${method}: failed to parse JSON response${hint}. The server may have returned a non-JSON body. Run \`alter login\` if the session is expired.`,
1071
+ err
1072
+ );
1003
1073
  }
1004
1074
  if (body.error) {
1005
1075
  const code = body.error.code;
@@ -1020,7 +1090,7 @@ var MCPClient = class {
1020
1090
  const headers = {
1021
1091
  ...this.extraHeaders ?? {},
1022
1092
  "Content-Type": "application/json",
1023
- Accept: "application/json",
1093
+ Accept: "application/json, text/event-stream",
1024
1094
  "User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`,
1025
1095
  "X-Alter-Client-Id": "alter-identity",
1026
1096
  "X-Alter-Client-Version": SDK_VERSION,
@@ -1497,7 +1567,23 @@ function canonicalJson2(value) {
1497
1567
 
1498
1568
  // src/client.ts
1499
1569
  var DEFAULT_ENDPOINT = "https://mcp.truealter.com/api/v1/mcp";
1570
+ var MEMBER_BRIDGE_ENDPOINT = "https://api.truealter.com/api/v1/mcp";
1500
1571
  var DEFAULT_DOMAIN = "truealter.com";
1572
+ var CANONICAL_API_BASE = "https://api.truealter.com";
1573
+ var JWKS_WELL_KNOWN_PATH = "/.well-known/alter-keys.json";
1574
+ function resolveJwksUrl(opts = {}) {
1575
+ if (opts.jwksUrl) return opts.jwksUrl;
1576
+ const base = opts.apiBase ?? (typeof process !== "undefined" ? process.env?.ALTER_API : void 0) ?? originOf(opts.endpoint) ?? CANONICAL_API_BASE;
1577
+ return `${base.replace(/\/+$/, "")}${JWKS_WELL_KNOWN_PATH}`;
1578
+ }
1579
+ function originOf(url) {
1580
+ if (!url) return void 0;
1581
+ try {
1582
+ return new URL(url).origin;
1583
+ } catch {
1584
+ return void 0;
1585
+ }
1586
+ }
1501
1587
  var AlterClient = class {
1502
1588
  mcp;
1503
1589
  x402;
@@ -1656,7 +1742,7 @@ var AlterClient = class {
1656
1742
  // ── Alter-to-Alter Messaging ─────────────────────────────────────────
1657
1743
  // Wave 1: cross-handle direct messages between authenticated tilde
1658
1744
  // handles. Default closed: recipient must have granted the sender via
1659
- // alter_message_grant. Spec: docs/technical/Alter-to-Alter Messaging.md.
1745
+ // alter_message_grant. Spec: the ALTER Alter-to-Alter Messaging spec.
1660
1746
  /** Send a direct message to another tilde handle. */
1661
1747
  async messageSend(args) {
1662
1748
  return this.mcp.callTool("alter_message_send", args);
@@ -1695,6 +1781,10 @@ var AlterClient = class {
1695
1781
  if (!envelope) return { valid: false, reason: "no provenance envelope" };
1696
1782
  const inner = envelope.provenance ?? envelope;
1697
1783
  return verifyProvenance(inner, {
1784
+ // Pass an explicit jwksUrl only when the caller pinned one. Leaving it
1785
+ // undefined preserves the envelope `verify_at` (allowlist-gated)
1786
+ // resolution path inside verifyProvenance; the allowlist is the
1787
+ // trust-anchor gate there, not a bare hardcoded host.
1698
1788
  jwksUrl: this.options.jwksUrl,
1699
1789
  verifyAtAllowlist: this.options.verifyAtAllowlist
1700
1790
  });
@@ -1707,9 +1797,21 @@ var AlterClient = class {
1707
1797
  async verifyToolSignatures(tools, signatures) {
1708
1798
  return verifyToolSignatures(tools, signatures);
1709
1799
  }
1710
- /** Fetch the published JWKS for ALTER's signing key (cached 5 min). */
1800
+ /**
1801
+ * Fetch the published JWKS for ALTER's signing key (cached 5 min).
1802
+ *
1803
+ * The JWKS URL is derived from the configured API surface
1804
+ * ({@link resolveJwksUrl}: explicit `jwksUrl` > `apiBase` > `ALTER_API` >
1805
+ * the configured `endpoint` origin > the canonical host) rather than a
1806
+ * single hardcoded host, so the trust anchor tracks the surface the
1807
+ * client is actually pointed at.
1808
+ */
1711
1809
  async fetchPublicKeys() {
1712
- const url = this.options.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
1810
+ const url = resolveJwksUrl({
1811
+ jwksUrl: this.options.jwksUrl,
1812
+ apiBase: this.options.apiBase,
1813
+ endpoint: this.options.endpoint
1814
+ });
1713
1815
  return fetchPublicKeys(url);
1714
1816
  }
1715
1817
  };
@@ -1744,17 +1846,20 @@ function generateCursorConfig(opts = {}) {
1744
1846
  // src/adapters/claude-desktop.ts
1745
1847
  function generateClaudeDesktopConfig(opts = {}) {
1746
1848
  const serverName = opts.serverName ?? "alter";
1747
- const bridgeCommand = opts.bridgeCommand ?? "alter-mcp-bridge";
1849
+ const bridgeCommand = opts.bridgeCommand ?? "alter";
1748
1850
  const env2 = {};
1749
- env2.ALTER_MCP_ENDPOINT = opts.endpoint ?? DEFAULT_ENDPOINT;
1851
+ env2.ALTER_MCP_ENDPOINT = opts.endpoint ?? MEMBER_BRIDGE_ENDPOINT;
1750
1852
  if (opts.apiKey) env2.ALTER_API_KEY = opts.apiKey;
1751
1853
  const entry = {
1752
1854
  command: bridgeCommand,
1855
+ // The `mcp-bridge` subcommand is always the first arg now that the bridge
1856
+ // is invoked through the `alter` CLI, not a bare bridge binary.
1857
+ args: ["mcp-bridge"],
1753
1858
  env: env2,
1754
1859
  description: "ALTER Identity: psychometric identity field for AI agents"
1755
1860
  };
1756
1861
  if (opts.extraArgs && opts.extraArgs.length > 0) {
1757
- entry.args = [...opts.extraArgs];
1862
+ entry.args = [...entry.args, ...opts.extraArgs];
1758
1863
  }
1759
1864
  return { mcpServers: { [serverName]: entry } };
1760
1865
  }
@@ -2032,6 +2137,7 @@ function wire(opts = {}) {
2032
2137
  const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
2033
2138
  const apiKey = opts.apiKey;
2034
2139
  const cfAccess = opts.cfAccess ?? readCfAccessEnv();
2140
+ const launcherPath = opts.launcherPath;
2035
2141
  const probes = probeAll();
2036
2142
  const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
2037
2143
  const ts = TIMESTAMP();
@@ -2050,7 +2156,7 @@ function wire(opts = {}) {
2050
2156
  }
2051
2157
  try {
2052
2158
  if (id === "claude-code") {
2053
- targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
2159
+ targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess, launcherPath }));
2054
2160
  } else {
2055
2161
  targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
2056
2162
  }
@@ -2121,10 +2227,28 @@ function wireFileTarget(args) {
2121
2227
  postSha256: result.postSha256
2122
2228
  };
2123
2229
  }
2124
- function wireClaudeCode(args) {
2125
- const cmd = "claude";
2126
- const bridgePath = resolveBridgeScript();
2127
- const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
2230
+ function redactSecret(text, secret) {
2231
+ if (!secret) return text;
2232
+ return text.split(secret).join("***redacted***");
2233
+ }
2234
+ function buildClaudeCodeAddArgs(args) {
2235
+ if (args.subprocessArgv) {
2236
+ return [
2237
+ "mcp",
2238
+ "add",
2239
+ "--scope",
2240
+ "user",
2241
+ "alter",
2242
+ "--env",
2243
+ `ALTER_MCP_ENDPOINT=${MEMBER_BRIDGE_ENDPOINT}`,
2244
+ "--env",
2245
+ `ALTER_PUBLIC_MCP_ENDPOINT=${MEMBER_BRIDGE_ENDPOINT}`,
2246
+ ...args.apiKey ? ["--env", `ALTER_API_KEY=${args.apiKey}`] : [],
2247
+ "--",
2248
+ ...args.subprocessArgv
2249
+ ];
2250
+ }
2251
+ return [
2128
2252
  "mcp",
2129
2253
  "add",
2130
2254
  "--scope",
@@ -2135,12 +2259,28 @@ function wireClaudeCode(args) {
2135
2259
  args.endpoint,
2136
2260
  ...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
2137
2261
  ];
2138
- const full = `${cmd} ${argList.join(" ")}`;
2262
+ }
2263
+ function wireClaudeCode(args) {
2264
+ const cmd = "claude";
2265
+ const bridgePath = resolveBridgeScript();
2266
+ const launcher = args.launcherPath && existsSync(args.launcherPath) ? args.launcherPath : null;
2267
+ const subprocessArgv = launcher ? ["node", launcher, "mcp-bridge"] : bridgePath ? ["node", bridgePath] : null;
2268
+ const argList = buildClaudeCodeAddArgs({
2269
+ apiKey: args.apiKey,
2270
+ subprocessArgv,
2271
+ endpoint: args.endpoint
2272
+ });
2273
+ const full = redactSecret(`${cmd} ${argList.join(" ")}`, args.apiKey);
2139
2274
  const run = spawnSync(cmd, argList, {
2140
2275
  encoding: "utf8",
2141
2276
  shell: process.platform === "win32",
2142
2277
  timeout: 1e4,
2143
- env: bridgePath ? { ...process.env, ALTER_PUBLIC_MCP_ENDPOINT: args.endpoint, ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {} } : void 0
2278
+ env: subprocessArgv ? {
2279
+ ...process.env,
2280
+ ALTER_MCP_ENDPOINT: MEMBER_BRIDGE_ENDPOINT,
2281
+ ALTER_PUBLIC_MCP_ENDPOINT: MEMBER_BRIDGE_ENDPOINT,
2282
+ ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {}
2283
+ } : void 0
2144
2284
  });
2145
2285
  if (run.error) {
2146
2286
  return {
@@ -2173,6 +2313,10 @@ function wireClaudeCode(args) {
2173
2313
  }
2174
2314
  function resolveBridgeScript() {
2175
2315
  const here = dirname(fileURLToPath(import.meta.url));
2316
+ const distBinBridge = join(here, "bin", "mcp-bridge.js");
2317
+ if (existsSync(distBinBridge)) return distBinBridge;
2318
+ const nestedDistBinBridge = join(here, "..", "dist", "bin", "mcp-bridge.js");
2319
+ if (existsSync(nestedDistBinBridge)) return nestedDistBinBridge;
2176
2320
  const siblingBridge = join(here, "..", "dist", "mcp-bridge.js");
2177
2321
  if (existsSync(siblingBridge)) return siblingBridge;
2178
2322
  const srcBridge = join(here, "..", "mcp-bridge.js");
@@ -2462,13 +2606,13 @@ var TIER_PRICES = {
2462
2606
  };
2463
2607
  var ADVERTISED_TOOL_COUNTS = {
2464
2608
  /** Free (L0) tools visible to anonymous / agent-class callers. */
2465
- free: 26,
2609
+ free: 27,
2466
2610
  /** Premium (L1-L5) tools requiring x402 payment. */
2467
- premium: 8,
2611
+ premium: 9,
2468
2612
  /** Messaging tools (member-self-only; excluded from external advertisement). */
2469
2613
  messaging: 0,
2470
2614
  /** Total publicly advertised (free + premium). */
2471
- total: 34
2615
+ total: 36
2472
2616
  };
2473
2617
  var REVENUE_SPLIT = {
2474
2618
  weaver: 0.75,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@truealter/sdk",
3
- "version": "0.5.3",
4
- "description": "ALTER Identity SDK: query the continuous identity field from any JavaScript/TypeScript environment",
3
+ "version": "0.5.8",
4
+ "description": "~Alter Identity SDK: query the continuous identity field from any JavaScript/TypeScript environment",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/index.cjs",
@@ -14,10 +14,6 @@
14
14
  "require": "./dist/index.cjs"
15
15
  }
16
16
  },
17
- "bin": {
18
- "alter-identity": "./dist/bin/alter-identity.js",
19
- "alter-mcp-bridge": "./dist/bin/mcp-bridge.js"
20
- },
21
17
  "files": [
22
18
  "dist/",
23
19
  "README.md",
@@ -47,6 +43,7 @@
47
43
  "vitest": "^4.1.3"
48
44
  },
49
45
  "overrides": {
46
+ "esbuild": ">=0.28.1",
50
47
  "postcss": ">=8.5.10"
51
48
  },
52
49
  "keywords": [