@truealter/sdk 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,11 +6,11 @@ var utils = require('@noble/hashes/utils');
6
6
  var crypto$1 = require('crypto');
7
7
  var ed25519 = require('@noble/ed25519');
8
8
  var sha512 = require('@noble/hashes/sha512');
9
- var child_process = require('child_process');
10
- var os = require('os');
9
+ var fs = require('fs');
11
10
  var path = require('path');
11
+ var os = require('os');
12
+ var child_process = require('child_process');
12
13
  var process$1 = require('process');
13
- var fs = require('fs');
14
14
 
15
15
  function _interopNamespace(e) {
16
16
  if (e && e.__esModule) return e;
@@ -36,6 +36,12 @@ var __defProp = Object.defineProperty;
36
36
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
37
37
  var __getOwnPropNames = Object.getOwnPropertyNames;
38
38
  var __hasOwnProp = Object.prototype.hasOwnProperty;
39
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
40
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
41
+ }) : x)(function(x) {
42
+ if (typeof require !== "undefined") return require.apply(this, arguments);
43
+ throw Error('Dynamic require of "' + x + '" is not supported');
44
+ });
39
45
  var __esm = (fn, res) => function __init() {
40
46
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
41
47
  };
@@ -450,11 +456,19 @@ var X402Client = class {
450
456
  maxPerQuery;
451
457
  networks;
452
458
  assets;
459
+ // undefined = allowlist check disabled (backward-compatible default).
460
+ // Non-null = active allowlist; reject any recipient not in the set.
461
+ recipientAllowlist;
453
462
  constructor(opts = {}) {
454
463
  this.signer = opts.signer;
455
464
  this.maxPerQuery = opts.maxPerQuery !== void 0 ? Number(opts.maxPerQuery) : void 0;
456
465
  this.networks = new Set(opts.networks ?? ["base", "base-sepolia"]);
457
466
  this.assets = new Set(opts.assets ?? ["USDC"]);
467
+ if (opts.recipientAllowlist !== void 0) {
468
+ this.recipientAllowlist = opts.recipientAllowlist.length === 0 ? void 0 : new Set(opts.recipientAllowlist.map((a) => a.toLowerCase()));
469
+ } else {
470
+ this.recipientAllowlist = void 0;
471
+ }
458
472
  }
459
473
  /**
460
474
  * Validate the envelope against this client's policy and, if a signer
@@ -480,6 +494,15 @@ var X402Client = class {
480
494
  );
481
495
  }
482
496
  }
497
+ if (this.recipientAllowlist !== void 0) {
498
+ const recipientNorm = (envelope.recipient ?? "").toLowerCase();
499
+ if (!recipientNorm || !this.recipientAllowlist.has(recipientNorm)) {
500
+ throw new AlterError(
501
+ "PAYMENT_REQUIRED",
502
+ `recipient "${envelope.recipient}" is not on the known-recipient allowlist`
503
+ );
504
+ }
505
+ }
483
506
  if (!this.signer) {
484
507
  throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
485
508
  }
@@ -952,9 +975,46 @@ async function verifyProvenance(envelope, opts = {}) {
952
975
  kid: header.kid
953
976
  };
954
977
  }
978
+ if (opts.expectedAud !== void 0 && opts.expectedAud !== "") {
979
+ const tokenAud = payload.aud;
980
+ const audList = tokenAud === void 0 ? [] : Array.isArray(tokenAud) ? tokenAud : [tokenAud];
981
+ if (!audList.includes(opts.expectedAud)) {
982
+ return {
983
+ valid: false,
984
+ reason: `aud mismatch: expected "${opts.expectedAud}", got ${JSON.stringify(tokenAud ?? null)}`,
985
+ payload,
986
+ kid: header.kid
987
+ };
988
+ }
989
+ }
955
990
  return { valid: true, payload, kid: header.kid };
956
991
  }
957
- async function verifyToolSignatures(tools, signatures) {
992
+ async function verifyToolSignatures(tools, signatures, opts = {}) {
993
+ const jwksUrl = opts.jwksUrl ?? "https://api.truealter.com/.well-known/alter-keys.json";
994
+ const fetchImpl = opts.fetch ?? fetch;
995
+ if (!jwksUrl.startsWith("https://")) {
996
+ return tools.map((t) => ({
997
+ tool: t.name,
998
+ valid: false,
999
+ reason: `jwksUrl must be https: got ${jwksUrl}`
1000
+ }));
1001
+ }
1002
+ const needsJwks = tools.some((t) => {
1003
+ const sig = signatures[t.name];
1004
+ return sig && sig.signature;
1005
+ });
1006
+ let jwks = null;
1007
+ if (needsJwks) {
1008
+ try {
1009
+ jwks = await fetchJwks(jwksUrl, fetchImpl);
1010
+ } catch (err) {
1011
+ return tools.map((t) => ({
1012
+ tool: t.name,
1013
+ valid: false,
1014
+ reason: `jwks fetch failed: ${err.message}`
1015
+ }));
1016
+ }
1017
+ }
958
1018
  const out = [];
959
1019
  for (const tool of tools) {
960
1020
  const sig = signatures[tool.name];
@@ -967,6 +1027,63 @@ async function verifyToolSignatures(tools, signatures) {
967
1027
  out.push({ tool: tool.name, valid: false, reason: "schema hash mismatch" });
968
1028
  continue;
969
1029
  }
1030
+ const jwsToken = sig.signature;
1031
+ if (!jwsToken) {
1032
+ out.push({ tool: tool.name, valid: true, warn_no_signature: true });
1033
+ continue;
1034
+ }
1035
+ const jwksDoc = jwks;
1036
+ let jHeader;
1037
+ let jPayloadRaw;
1038
+ let jSigBytes;
1039
+ try {
1040
+ const parts2 = jwsToken.split(".");
1041
+ if (parts2.length !== 3) throw new Error("JWS must have three segments");
1042
+ jHeader = JSON.parse(new TextDecoder().decode(base64urlDecode(parts2[0])));
1043
+ jPayloadRaw = new TextDecoder().decode(base64urlDecode(parts2[1]));
1044
+ jSigBytes = base64urlDecode(parts2[2]);
1045
+ } catch (err) {
1046
+ out.push({ tool: tool.name, valid: false, reason: `malformed tool JWS: ${err.message}` });
1047
+ continue;
1048
+ }
1049
+ if (jHeader.alg !== "ES256") {
1050
+ out.push({ tool: tool.name, valid: false, reason: `unsupported tool sig alg: ${jHeader.alg}` });
1051
+ continue;
1052
+ }
1053
+ if (jPayloadRaw !== sig.schema_hash) {
1054
+ out.push({ tool: tool.name, valid: false, reason: "tool JWS payload does not match schema_hash" });
1055
+ continue;
1056
+ }
1057
+ const jwk = jwksDoc.keys.find((k) => jHeader.kid ? k.kid === jHeader.kid : true);
1058
+ if (!jwk) {
1059
+ out.push({ tool: tool.name, valid: false, reason: `no JWK for kid=${jHeader.kid}` });
1060
+ continue;
1061
+ }
1062
+ let publicKey;
1063
+ try {
1064
+ publicKey = await importEs256JwkAsPublicKey(jwk);
1065
+ } catch (err) {
1066
+ out.push({ tool: tool.name, valid: false, reason: `jwk import: ${err.message}` });
1067
+ continue;
1068
+ }
1069
+ const parts = jwsToken.split(".");
1070
+ const signedInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
1071
+ let sigValid = false;
1072
+ try {
1073
+ sigValid = await crypto.subtle.verify(
1074
+ { name: "ECDSA", hash: "SHA-256" },
1075
+ publicKey,
1076
+ toArrayBuffer(jSigBytes),
1077
+ toArrayBuffer(signedInput)
1078
+ );
1079
+ } catch (err) {
1080
+ out.push({ tool: tool.name, valid: false, reason: `sig verify error: ${err.message}` });
1081
+ continue;
1082
+ }
1083
+ if (!sigValid) {
1084
+ out.push({ tool: tool.name, valid: false, reason: "tool signature mismatch" });
1085
+ continue;
1086
+ }
970
1087
  out.push({ tool: tool.name, valid: true });
971
1088
  }
972
1089
  return out;
@@ -1626,6 +1743,26 @@ function restoreFromBackup(path, backupPath) {
1626
1743
  // src/wire/index.ts
1627
1744
  var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
1628
1745
  var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
1746
+ function readCfAccessEnv() {
1747
+ const envPath = path.join(os.homedir(), ".config", "alter", "cf-access.env");
1748
+ try {
1749
+ const content = fs.readFileSync(envPath, "utf8");
1750
+ let clientId = "";
1751
+ let clientSecret = "";
1752
+ for (const line of content.split("\n")) {
1753
+ const trimmed = line.trim();
1754
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
1755
+ const eqIdx = trimmed.indexOf("=");
1756
+ const key = trimmed.slice(0, eqIdx).replace(/^export\s+/, "").trim();
1757
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
1758
+ if (key === "CF_ACCESS_CLIENT_ID") clientId = val;
1759
+ if (key === "CF_ACCESS_CLIENT_SECRET") clientSecret = val;
1760
+ }
1761
+ if (clientId && clientSecret) return { clientId, clientSecret };
1762
+ } catch {
1763
+ }
1764
+ return void 0;
1765
+ }
1629
1766
  function clientById(id) {
1630
1767
  const hit = ALL_CLIENTS.find((c) => c.id === id);
1631
1768
  if (!hit) throw new Error(`unknown client id: ${id}`);
@@ -1634,6 +1771,7 @@ function clientById(id) {
1634
1771
  function wire(opts = {}) {
1635
1772
  const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
1636
1773
  const apiKey = opts.apiKey;
1774
+ const cfAccess = opts.cfAccess ?? readCfAccessEnv();
1637
1775
  const probes = probeAll();
1638
1776
  const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
1639
1777
  const ts = TIMESTAMP();
@@ -1652,9 +1790,9 @@ function wire(opts = {}) {
1652
1790
  }
1653
1791
  try {
1654
1792
  if (id === "claude-code") {
1655
- targets.push(wireClaudeCode({ endpoint, apiKey }));
1793
+ targets.push(wireClaudeCode({ endpoint, apiKey, cfAccess }));
1656
1794
  } else {
1657
- targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
1795
+ targets.push(wireFileTarget({ id, endpoint, apiKey, cfAccess, timestamp: ts }));
1658
1796
  }
1659
1797
  } catch (err) {
1660
1798
  const message = err.message;
@@ -1688,7 +1826,12 @@ function wireFileTarget(args) {
1688
1826
  `refusing to wire ${client.label}: config path ${sync.resolvedPath} lives under ${sync.matchedPrefix}. Synced volumes propagate credentials across devices \u2014 move the config off the sync root, or run wire on the device you want to target.`
1689
1827
  );
1690
1828
  }
1691
- const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
1829
+ const cfHeaders = {};
1830
+ if (args.cfAccess) {
1831
+ cfHeaders["CF-Access-Client-Id"] = args.cfAccess.clientId;
1832
+ cfHeaders["CF-Access-Client-Secret"] = args.cfAccess.clientSecret;
1833
+ }
1834
+ const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey, headers: cfHeaders });
1692
1835
  const rootKey = client.rootKey;
1693
1836
  const serverName = "alter";
1694
1837
  const result = atomicJsonMerge({
@@ -1720,7 +1863,8 @@ function wireFileTarget(args) {
1720
1863
  }
1721
1864
  function wireClaudeCode(args) {
1722
1865
  const cmd = "claude";
1723
- const argList = [
1866
+ const bridgePath = resolveBridgeScript();
1867
+ const argList = bridgePath ? ["mcp", "add", "--scope", "user", "alter", "--", "node", bridgePath] : [
1724
1868
  "mcp",
1725
1869
  "add",
1726
1870
  "--scope",
@@ -1728,16 +1872,15 @@ function wireClaudeCode(args) {
1728
1872
  "--transport",
1729
1873
  "http",
1730
1874
  "alter",
1731
- args.endpoint
1875
+ args.endpoint,
1876
+ ...args.apiKey ? ["--header", `X-ALTER-API-Key:${args.apiKey}`] : []
1732
1877
  ];
1733
- if (args.apiKey) {
1734
- argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
1735
- }
1736
1878
  const full = `${cmd} ${argList.join(" ")}`;
1737
1879
  const run = child_process.spawnSync(cmd, argList, {
1738
1880
  encoding: "utf8",
1739
1881
  shell: process.platform === "win32",
1740
- timeout: 1e4
1882
+ timeout: 1e4,
1883
+ env: bridgePath ? { ...process.env, ALTER_PUBLIC_MCP_ENDPOINT: args.endpoint, ...args.apiKey ? { ALTER_API_KEY: args.apiKey } : {} } : void 0
1741
1884
  });
1742
1885
  if (run.error) {
1743
1886
  return {
@@ -1768,6 +1911,17 @@ function wireClaudeCode(args) {
1768
1911
  reason: `claude mcp add exited ${String(run.status)}`
1769
1912
  };
1770
1913
  }
1914
+ function resolveBridgeScript() {
1915
+ const { existsSync: existsSync4 } = __require("fs");
1916
+ const { join: join3, dirname: dirname3 } = __require("path");
1917
+ const siblingBridge = join3(dirname3(__filename), "..", "dist", "mcp-bridge.js");
1918
+ if (existsSync4(siblingBridge)) return siblingBridge;
1919
+ const srcBridge = join3(dirname3(__filename), "..", "mcp-bridge.js");
1920
+ if (existsSync4(srcBridge)) return srcBridge;
1921
+ const npmGlobalBridge = join3(dirname3(__filename), "mcp-bridge.js");
1922
+ if (existsSync4(npmGlobalBridge)) return npmGlobalBridge;
1923
+ return null;
1924
+ }
1771
1925
  function unwire() {
1772
1926
  const state = readWireState();
1773
1927
  const undone = [];
package/dist/index.d.cts CHANGED
@@ -130,6 +130,7 @@ interface ProvenanceEnvelope {
130
130
  }
131
131
  interface ProvenancePayload {
132
132
  iss: string;
133
+ aud?: string | string[];
133
134
  iat: number;
134
135
  exp: number;
135
136
  purpose: string;
@@ -196,6 +197,13 @@ interface VerifyProvenanceOptions {
196
197
  * recommended for production use.
197
198
  */
198
199
  expectedIss?: string;
200
+ /**
201
+ * Expected `aud` claim. When provided and non-empty, the token's `aud`
202
+ * claim must contain this value (RFC 7519 §4.1.3). Pass an empty string
203
+ * to disable the check. Defaults to no check (undefined) for backward
204
+ * compatibility — callers SHOULD supply this in production environments.
205
+ */
206
+ expectedAud?: string;
199
207
  }
200
208
  /**
201
209
  * Verify a provenance JWS token against ALTER's published JWKS.
@@ -216,7 +224,15 @@ declare function verifyProvenance(envelope: ProvenanceEnvelope | string, opts?:
216
224
  *
217
225
  * ALTER signs each tool's input schema at startup and exposes the
218
226
  * signatures via the MCP `tools/list` `_meta.signatures` map. This helper
219
- * checks that each tool's schema hash matches the signed value.
227
+ * checks that each tool's schema hash matches the signed value, AND
228
+ * cryptographically verifies the ES256 JWS signature carried in
229
+ * `sig.signature` / `sig.kid` against the JWKS published at `jwksUrl`.
230
+ *
231
+ * H-9 remediation: previously only the schema hash was verified; the
232
+ * `sig.signature` and `sig.kid` fields were carried but never checked,
233
+ * allowing a hostile MCP server to substitute a fake tool schema whose
234
+ * hash matched a maliciously crafted `schema_hash` without needing the
235
+ * signing key.
220
236
  */
221
237
  interface SignedToolDefinition {
222
238
  name: string;
@@ -230,10 +246,22 @@ interface ToolSignatureMap {
230
246
  kid?: string | null;
231
247
  };
232
248
  }
233
- declare function verifyToolSignatures(tools: SignedToolDefinition[], signatures: ToolSignatureMap): Promise<{
249
+ interface VerifyToolSignaturesOptions {
250
+ /**
251
+ * JWKS URL to verify ES256 signatures against. Must be https.
252
+ * Defaults to `https://api.truealter.com/.well-known/alter-keys.json`.
253
+ * When a tool's `signature` field is null/absent, the tool is marked
254
+ * valid-on-hash (legacy path) and a `warn_no_signature: true` flag is
255
+ * included in the result.
256
+ */
257
+ jwksUrl?: string;
258
+ fetch?: typeof fetch;
259
+ }
260
+ declare function verifyToolSignatures(tools: SignedToolDefinition[], signatures: ToolSignatureMap, opts?: VerifyToolSignaturesOptions): Promise<{
234
261
  tool: string;
235
262
  valid: boolean;
236
263
  reason?: string;
264
+ warn_no_signature?: boolean;
237
265
  }[]>;
238
266
  /**
239
267
  * Fetch the ALTER public key set. Cached in-process for five minutes.
@@ -369,12 +397,21 @@ interface X402ClientOptions {
369
397
  networks?: string[];
370
398
  /** Permitted assets. Defaults to `['USDC']`. */
371
399
  assets?: string[];
400
+ /**
401
+ * Known recipient addresses. Defaults to {@link DEFAULT_RECIPIENT_ALLOWLIST}.
402
+ * Passing a list here *replaces* the default — include the ALTER canonical
403
+ * address if you still want it accepted.
404
+ *
405
+ * An empty array disables the check entirely (not recommended for production).
406
+ */
407
+ recipientAllowlist?: string[];
372
408
  }
373
409
  declare class X402Client {
374
410
  private readonly signer?;
375
411
  private readonly maxPerQuery?;
376
412
  private readonly networks;
377
413
  private readonly assets;
414
+ private readonly recipientAllowlist;
378
415
  constructor(opts?: X402ClientOptions);
379
416
  /**
380
417
  * Validate the envelope against this client's policy and, if a signer
@@ -415,7 +452,7 @@ interface MCPClientOptions {
415
452
  /** Optional x402 client for automatic premium tool payment. */
416
453
  x402?: X402Client;
417
454
  /**
418
- * Q5c per-invocation signing. When present, every `tools/call` is
455
+ * ES256 per-invocation signing. When present, every `tools/call` is
419
456
  * ES256-signed and submitted with the `Mcp-Invocation-Signature`
420
457
  * header. The public half of `privateKey` MUST have been
421
458
  * registered via `POST /api/v1/agents/keys` against the same API
@@ -1595,11 +1632,17 @@ declare function sha256(bytes: string | Buffer): string;
1595
1632
  * deterministic ordering is worth the tiny blocking cost.
1596
1633
  */
1597
1634
 
1635
+ interface CfAccessCredentials {
1636
+ clientId: string;
1637
+ clientSecret: string;
1638
+ }
1598
1639
  interface WireOptions {
1599
1640
  /** Override the endpoint written into every client config. Defaults to DEFAULT_ENDPOINT. */
1600
1641
  endpoint?: string;
1601
1642
  /** Optional API key written into `headers['X-ALTER-API-Key']` for each target. */
1602
1643
  apiKey?: string;
1644
+ /** CF Access service token credentials. Auto-read from ~/.config/alter/cf-access.env when absent. */
1645
+ cfAccess?: CfAccessCredentials;
1603
1646
  /** Restrict to a subset of client ids. Default: every detected client. */
1604
1647
  only?: readonly ClientId[];
1605
1648
  /** Skip any client whose probe said "not installed" even if the caller passed it via `only`. */
@@ -1624,15 +1667,12 @@ declare function unwire(): UnwireReport;
1624
1667
  * @truealter/sdk — alter_homepage MCP tool types
1625
1668
  *
1626
1669
  * Wire-format types for the user-authored, externally-queryable identity
1627
- * homepage surface ratified (proposed) as D-CUST-PORTFOLIO-1 in
1628
- * alter-internal Session 54.
1670
+ * homepage surface.
1629
1671
  *
1630
- * Tool name note: ratified-as `alter_portfolio` in the proposed DR;
1631
- * shipping as `alter_homepage` because `alter_portfolio` is already
1632
- * taken by the verified-attestations tool in mcp-alter (different
1633
- * concept). Per the handover's explicit fallback. The DR text and
1634
- * companion docs may still say "portfolio" — the wire-format and
1635
- * tool-name-on-server are `homepage`.
1672
+ * Tool name note: the wire-format and server-side tool name are
1673
+ * `alter_homepage`. The name `alter_portfolio` is reserved by the
1674
+ * verified-attestations tool in `mcp-alter` (a different concept), so
1675
+ * homepage types ship under `homepage`.
1636
1676
  *
1637
1677
  * Wire-format rule: every field name matches the JSON Schema property
1638
1678
  * name exactly (snake_case). These are passed straight into JSON-RPC
@@ -1690,7 +1730,7 @@ interface HomepageManifest {
1690
1730
  */
1691
1731
  opener?: HomepageField<string>;
1692
1732
  /**
1693
- * Composed-glyph string (from typed primitives, D-CUST-1 M3). The
1733
+ * Composed-glyph string (from typed primitives). The
1694
1734
  * sigil is a string of renderer-recognised primitive references —
1695
1735
  * not raw glyph codes — so different consumers can render the same
1696
1736
  * sigil distinctly. Provenance is `declared` for user-composed,
@@ -1774,8 +1814,7 @@ interface HomepageOutput {
1774
1814
  type HomepageCallerVertical = "workplace" | "education" | "personal" | "civic" | "agent" | "unknown";
1775
1815
  /** Maximum sizes from the spec. SDK consumers can use these to validate
1776
1816
  * input before sending. Mirrored from
1777
- * `docs/technical/alter-portfolio-manifest-v1.md` (forthcoming) and
1778
- * the proposed-D-CUST-PORTFOLIO-1 DR. */
1817
+ * `docs/technical/alter-portfolio-manifest-v1.md` (forthcoming). */
1779
1818
  declare const HOMEPAGE_LIMITS: {
1780
1819
  readonly whoami_max_chars: 240;
1781
1820
  readonly opener_max_chars: 280;
@@ -1784,13 +1823,11 @@ declare const HOMEPAGE_LIMITS: {
1784
1823
  };
1785
1824
 
1786
1825
  /**
1787
- * @truealter/sdk — theme pack types (D-CUST-1 substrate, Wave 2)
1826
+ * @truealter/sdk — theme pack types
1788
1827
  *
1789
1828
  * Wire-format types for ALTER theme packs and `themes.lock` composition
1790
1829
  * manifests. The full specification lives in
1791
- * `docs/technical/alter-theme-pack-spec-v1.md`; the architecture spike
1792
- * (with threat model F1–F10) lives in
1793
- * `.repos/internal/02-Technical-Strategy/alter-theme-packs-architecture-spike.md`.
1830
+ * `docs/technical/alter-theme-pack-spec-v1.md`.
1794
1831
  *
1795
1832
  * These types describe the on-the-wire shape of theme manifests as they
1796
1833
  * are produced by `alter theme install`, persisted to `themes.lock`,
@@ -1930,7 +1967,7 @@ interface ThemesLockV1 {
1930
1967
  * Input arguments for the `theme_share` MCP tool. Sharing emits a 5:1
1931
1968
  * return event to the sharer (recognition credit + pack citation) and
1932
1969
  * to the recipient (discovery signal). Implementation lives in
1933
- * `mcp-alter` per D-RS15.
1970
+ * `mcp-alter`.
1934
1971
  */
1935
1972
  interface ThemeShareInput {
1936
1973
  /** Recipient ~handle. */
package/dist/index.d.ts CHANGED
@@ -130,6 +130,7 @@ interface ProvenanceEnvelope {
130
130
  }
131
131
  interface ProvenancePayload {
132
132
  iss: string;
133
+ aud?: string | string[];
133
134
  iat: number;
134
135
  exp: number;
135
136
  purpose: string;
@@ -196,6 +197,13 @@ interface VerifyProvenanceOptions {
196
197
  * recommended for production use.
197
198
  */
198
199
  expectedIss?: string;
200
+ /**
201
+ * Expected `aud` claim. When provided and non-empty, the token's `aud`
202
+ * claim must contain this value (RFC 7519 §4.1.3). Pass an empty string
203
+ * to disable the check. Defaults to no check (undefined) for backward
204
+ * compatibility — callers SHOULD supply this in production environments.
205
+ */
206
+ expectedAud?: string;
199
207
  }
200
208
  /**
201
209
  * Verify a provenance JWS token against ALTER's published JWKS.
@@ -216,7 +224,15 @@ declare function verifyProvenance(envelope: ProvenanceEnvelope | string, opts?:
216
224
  *
217
225
  * ALTER signs each tool's input schema at startup and exposes the
218
226
  * signatures via the MCP `tools/list` `_meta.signatures` map. This helper
219
- * checks that each tool's schema hash matches the signed value.
227
+ * checks that each tool's schema hash matches the signed value, AND
228
+ * cryptographically verifies the ES256 JWS signature carried in
229
+ * `sig.signature` / `sig.kid` against the JWKS published at `jwksUrl`.
230
+ *
231
+ * H-9 remediation: previously only the schema hash was verified; the
232
+ * `sig.signature` and `sig.kid` fields were carried but never checked,
233
+ * allowing a hostile MCP server to substitute a fake tool schema whose
234
+ * hash matched a maliciously crafted `schema_hash` without needing the
235
+ * signing key.
220
236
  */
221
237
  interface SignedToolDefinition {
222
238
  name: string;
@@ -230,10 +246,22 @@ interface ToolSignatureMap {
230
246
  kid?: string | null;
231
247
  };
232
248
  }
233
- declare function verifyToolSignatures(tools: SignedToolDefinition[], signatures: ToolSignatureMap): Promise<{
249
+ interface VerifyToolSignaturesOptions {
250
+ /**
251
+ * JWKS URL to verify ES256 signatures against. Must be https.
252
+ * Defaults to `https://api.truealter.com/.well-known/alter-keys.json`.
253
+ * When a tool's `signature` field is null/absent, the tool is marked
254
+ * valid-on-hash (legacy path) and a `warn_no_signature: true` flag is
255
+ * included in the result.
256
+ */
257
+ jwksUrl?: string;
258
+ fetch?: typeof fetch;
259
+ }
260
+ declare function verifyToolSignatures(tools: SignedToolDefinition[], signatures: ToolSignatureMap, opts?: VerifyToolSignaturesOptions): Promise<{
234
261
  tool: string;
235
262
  valid: boolean;
236
263
  reason?: string;
264
+ warn_no_signature?: boolean;
237
265
  }[]>;
238
266
  /**
239
267
  * Fetch the ALTER public key set. Cached in-process for five minutes.
@@ -369,12 +397,21 @@ interface X402ClientOptions {
369
397
  networks?: string[];
370
398
  /** Permitted assets. Defaults to `['USDC']`. */
371
399
  assets?: string[];
400
+ /**
401
+ * Known recipient addresses. Defaults to {@link DEFAULT_RECIPIENT_ALLOWLIST}.
402
+ * Passing a list here *replaces* the default — include the ALTER canonical
403
+ * address if you still want it accepted.
404
+ *
405
+ * An empty array disables the check entirely (not recommended for production).
406
+ */
407
+ recipientAllowlist?: string[];
372
408
  }
373
409
  declare class X402Client {
374
410
  private readonly signer?;
375
411
  private readonly maxPerQuery?;
376
412
  private readonly networks;
377
413
  private readonly assets;
414
+ private readonly recipientAllowlist;
378
415
  constructor(opts?: X402ClientOptions);
379
416
  /**
380
417
  * Validate the envelope against this client's policy and, if a signer
@@ -415,7 +452,7 @@ interface MCPClientOptions {
415
452
  /** Optional x402 client for automatic premium tool payment. */
416
453
  x402?: X402Client;
417
454
  /**
418
- * Q5c per-invocation signing. When present, every `tools/call` is
455
+ * ES256 per-invocation signing. When present, every `tools/call` is
419
456
  * ES256-signed and submitted with the `Mcp-Invocation-Signature`
420
457
  * header. The public half of `privateKey` MUST have been
421
458
  * registered via `POST /api/v1/agents/keys` against the same API
@@ -1595,11 +1632,17 @@ declare function sha256(bytes: string | Buffer): string;
1595
1632
  * deterministic ordering is worth the tiny blocking cost.
1596
1633
  */
1597
1634
 
1635
+ interface CfAccessCredentials {
1636
+ clientId: string;
1637
+ clientSecret: string;
1638
+ }
1598
1639
  interface WireOptions {
1599
1640
  /** Override the endpoint written into every client config. Defaults to DEFAULT_ENDPOINT. */
1600
1641
  endpoint?: string;
1601
1642
  /** Optional API key written into `headers['X-ALTER-API-Key']` for each target. */
1602
1643
  apiKey?: string;
1644
+ /** CF Access service token credentials. Auto-read from ~/.config/alter/cf-access.env when absent. */
1645
+ cfAccess?: CfAccessCredentials;
1603
1646
  /** Restrict to a subset of client ids. Default: every detected client. */
1604
1647
  only?: readonly ClientId[];
1605
1648
  /** Skip any client whose probe said "not installed" even if the caller passed it via `only`. */
@@ -1624,15 +1667,12 @@ declare function unwire(): UnwireReport;
1624
1667
  * @truealter/sdk — alter_homepage MCP tool types
1625
1668
  *
1626
1669
  * Wire-format types for the user-authored, externally-queryable identity
1627
- * homepage surface ratified (proposed) as D-CUST-PORTFOLIO-1 in
1628
- * alter-internal Session 54.
1670
+ * homepage surface.
1629
1671
  *
1630
- * Tool name note: ratified-as `alter_portfolio` in the proposed DR;
1631
- * shipping as `alter_homepage` because `alter_portfolio` is already
1632
- * taken by the verified-attestations tool in mcp-alter (different
1633
- * concept). Per the handover's explicit fallback. The DR text and
1634
- * companion docs may still say "portfolio" — the wire-format and
1635
- * tool-name-on-server are `homepage`.
1672
+ * Tool name note: the wire-format and server-side tool name are
1673
+ * `alter_homepage`. The name `alter_portfolio` is reserved by the
1674
+ * verified-attestations tool in `mcp-alter` (a different concept), so
1675
+ * homepage types ship under `homepage`.
1636
1676
  *
1637
1677
  * Wire-format rule: every field name matches the JSON Schema property
1638
1678
  * name exactly (snake_case). These are passed straight into JSON-RPC
@@ -1690,7 +1730,7 @@ interface HomepageManifest {
1690
1730
  */
1691
1731
  opener?: HomepageField<string>;
1692
1732
  /**
1693
- * Composed-glyph string (from typed primitives, D-CUST-1 M3). The
1733
+ * Composed-glyph string (from typed primitives). The
1694
1734
  * sigil is a string of renderer-recognised primitive references —
1695
1735
  * not raw glyph codes — so different consumers can render the same
1696
1736
  * sigil distinctly. Provenance is `declared` for user-composed,
@@ -1774,8 +1814,7 @@ interface HomepageOutput {
1774
1814
  type HomepageCallerVertical = "workplace" | "education" | "personal" | "civic" | "agent" | "unknown";
1775
1815
  /** Maximum sizes from the spec. SDK consumers can use these to validate
1776
1816
  * input before sending. Mirrored from
1777
- * `docs/technical/alter-portfolio-manifest-v1.md` (forthcoming) and
1778
- * the proposed-D-CUST-PORTFOLIO-1 DR. */
1817
+ * `docs/technical/alter-portfolio-manifest-v1.md` (forthcoming). */
1779
1818
  declare const HOMEPAGE_LIMITS: {
1780
1819
  readonly whoami_max_chars: 240;
1781
1820
  readonly opener_max_chars: 280;
@@ -1784,13 +1823,11 @@ declare const HOMEPAGE_LIMITS: {
1784
1823
  };
1785
1824
 
1786
1825
  /**
1787
- * @truealter/sdk — theme pack types (D-CUST-1 substrate, Wave 2)
1826
+ * @truealter/sdk — theme pack types
1788
1827
  *
1789
1828
  * Wire-format types for ALTER theme packs and `themes.lock` composition
1790
1829
  * manifests. The full specification lives in
1791
- * `docs/technical/alter-theme-pack-spec-v1.md`; the architecture spike
1792
- * (with threat model F1–F10) lives in
1793
- * `.repos/internal/02-Technical-Strategy/alter-theme-packs-architecture-spike.md`.
1830
+ * `docs/technical/alter-theme-pack-spec-v1.md`.
1794
1831
  *
1795
1832
  * These types describe the on-the-wire shape of theme manifests as they
1796
1833
  * are produced by `alter theme install`, persisted to `themes.lock`,
@@ -1930,7 +1967,7 @@ interface ThemesLockV1 {
1930
1967
  * Input arguments for the `theme_share` MCP tool. Sharing emits a 5:1
1931
1968
  * return event to the sharer (recognition credit + pack citation) and
1932
1969
  * to the recipient (discovery signal). Implementation lives in
1933
- * `mcp-alter` per D-RS15.
1970
+ * `mcp-alter`.
1934
1971
  */
1935
1972
  interface ThemeShareInput {
1936
1973
  /** Recipient ~handle. */