@xrpl-utilities/mcp 0.2.195 → 0.2.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,18 @@ sign and retry against whichever rail its wallet supports.
40
40
 
41
41
  Operators can set `MCP_BYPASS_KEY` on the server to enable an opt-in
42
42
  bypass for friendlies / demos. The caller passes the matching key as
43
- `_bypass_key` in the tool args. Rate-limited at the proxy layer.
43
+ `_bypass_key` in the tool args.
44
+
45
+ The hosted endpoint budgets failed `_bypass_key` attempts per caller
46
+ separately from, and far more tightly than, ordinary requests: a 60
47
+ requests-per-minute cap is a fair-use limit, not an access control on a
48
+ secret. The budget is spent per guess, not per HTTP request, so a batched
49
+ JSON-RPC body cannot outrun it, and only requests that actually carry a
50
+ `_bypass_key` are held off once it is exhausted - `tools/list`, free tools
51
+ and paid x402 calls keep working. Batches are capped at 20 messages. Use a key of at least 32 random bytes, and scope or rotate it
52
+ per service rather than sharing one portfolio-wide key. Bucket keying
53
+ depends on `TRUST_PROXY_HOPS` matching the real number of proxies in
54
+ front of the container - see `.env.example`.
44
55
 
45
56
  ### H-Seal receipt co-signing (optional)
46
57
 
@@ -51,7 +62,16 @@ provider attestation. The attestation rides on the tool result's
51
62
  `_meta.hSeal`, so a caller can anchor a tamper-evident, independently
52
63
  verifiable on-chain receipt of the interaction. When either var is unset the
53
64
  feature is inert and responses are unchanged. Never hardcode the key — env
54
- only. See `src/hSeal.ts` and the ops runbook
65
+ only.
66
+
67
+ When a backend co-signs its own output, the MCP also builds a 2-party
68
+ receipt (`_meta.hSealReceipt`). That signs with the operator key, so set
69
+ `HSEAL_ALLOWED_PROVIDERS` to the CAIP-10 identities of the XR-* backends
70
+ you are willing to vouch for. Without it any provider that proves it holds
71
+ the key for the identity it names is accepted. The `responseHash` is always
72
+ recomputed from the delivered body; `requestHash` stays provider-asserted
73
+ (the backends hash a synthetic request object the MCP cannot reproduce) and
74
+ the result says so via `requestHashBasis`. See `src/hSeal.ts` and the ops runbook
55
75
  [`docs/hseal-provider.md`](docs/hseal-provider.md) (current identity, the
56
76
  ed25519 curve gotcha, and how to rotate/recover the key).
57
77
 
@@ -106,11 +126,19 @@ useful as templates in any language.
106
126
  npm install
107
127
  npm run build
108
128
  node dist/index.js --transport http --port 8080
129
+ npm test # builds, then runs the node:test suite in tests/
109
130
  ```
110
131
 
111
132
  Point MCP Inspector at `http://localhost:8080/mcp` to walk through
112
133
  tool definitions interactively.
113
134
 
135
+ Set `STRICT_VALIDATE=1` to treat a manifest that could not be read as
136
+ drift instead of a warning. It is off by default because Railway restarts
137
+ `ON_FAILURE`: failing closed on boot would crash-loop the endpoint, and
138
+ take the other five healthy services' tools down with it, whenever one
139
+ backend is cold. Either way, a service whose manifest was not read logs
140
+ `NOT CHECKED` - "could not check" is never reported as "all clear".
141
+
114
142
  ## Releases
115
143
 
116
144
  Releases are cut by tag push. The `Release` workflow builds, validates
@@ -44,6 +44,22 @@ export interface DispatchOptions {
44
44
  * can distinguish MCP traffic from direct API users.
45
45
  */
46
46
  userAgent?: string;
47
+ /**
48
+ * Called when a caller presented a `_bypass_key` that did not match.
49
+ * The transport layer counts these per-IP: a 60/min request limit still
50
+ * permits thousands of guesses a day against the shared bypass key, and
51
+ * the 402-vs-200 divergence is a clean oracle, so failed attempts need
52
+ * their own much tighter budget.
53
+ */
54
+ onBypassFailure?: () => void;
55
+ /**
56
+ * True while this caller has already burned through the failed-`_bypass_key`
57
+ * budget. Checked here, at the point of the guess, not only at the start of
58
+ * the HTTP request: one batched JSON-RPC POST carries thousands of messages,
59
+ * so a per-request check let a single body spend thousands of guesses before
60
+ * the lockout could apply.
61
+ */
62
+ isBypassBlocked?: () => boolean;
47
63
  }
48
64
  /**
49
65
  * Run a single tool call. Throws on missing tool / network error /
package/dist/dispatch.js CHANGED
@@ -55,6 +55,10 @@ export async function dispatchTool(toolName, args, opts = {}) {
55
55
  }
56
56
  const { service, tool } = owner;
57
57
  const baseUrl = opts.baseUrlOverride?.(service.id) ?? service.baseUrl;
58
+ // The low-level MCP Server does NOT check arguments against the advertised
59
+ // inputSchema - only McpServer.registerTool does - so every declared
60
+ // pattern/enum/limit is advisory until we enforce it here.
61
+ args = validateArgs(toolName, tool.inputSchema, args);
58
62
  // Substitute path params (e.g. /domain/{domain_id}) from args.
59
63
  let path = tool.path;
60
64
  const consumedPathArgs = new Set();
@@ -62,8 +66,17 @@ export async function dispatchTool(toolName, args, opts = {}) {
62
66
  if (!(key in args)) {
63
67
  throw new Error(`tool ${toolName} requires path parameter ${key}, missing from args`);
64
68
  }
69
+ // Unconditional, schema-independent. "." / ".." / a slash survive
70
+ // encodeURIComponent's intent because the WHATWG URL parser normalizes the
71
+ // built URL afterwards: /domain/.. collapses to the service root, and the
72
+ // paid drill-down tool then returns the root banner as a success - which,
73
+ // with H-Seal on, gets co-signed as an answer to the tool that was called.
74
+ const raw = String(args[key]);
75
+ if (raw === "" || raw === "." || raw === ".." || /[/\\]/.test(raw)) {
76
+ throw new Error(`tool ${toolName}: invalid value for path parameter ${key}`);
77
+ }
65
78
  consumedPathArgs.add(key);
66
- return encodeURIComponent(String(args[key]));
79
+ return encodeURIComponent(raw);
67
80
  });
68
81
  // Strip args reserved for transport-level concerns (payment_signature,
69
82
  // _bypass_key) AND args already consumed by path params. What remains
@@ -95,10 +108,26 @@ export async function dispatchTool(toolName, args, opts = {}) {
95
108
  if (callerPaymentSig) {
96
109
  headers["PAYMENT-SIGNATURE"] = callerPaymentSig;
97
110
  }
98
- else if (callerBypassKey && opts.bypassKey && timingSafeStringEqual(callerBypassKey, opts.bypassKey)) {
99
- // Operator-issued bypass. Forward as the dev-bypass header that
100
- // the underlying services accept.
101
- headers["PAYMENT-SIGNATURE"] = callerBypassKey;
111
+ else if (callerBypassKey) {
112
+ // Check BEFORE comparing. Gating only the failure branch would leave the
113
+ // compare reachable, so a blocked caller who finally guesses right still
114
+ // gets granted - the budget would cap the noise, not the attack.
115
+ // Throwing rather than falling through to the 402 path also stops the
116
+ // upstream fan-out a batched body would otherwise amplify, and it reads
117
+ // the same for every key: the block is per-IP, so it leaks nothing.
118
+ if (opts.isBypassBlocked?.()) {
119
+ throw new Error("bypass key attempts exhausted; retry later");
120
+ }
121
+ if (opts.bypassKey && timingSafeStringEqual(callerBypassKey, opts.bypassKey)) {
122
+ // Operator-issued bypass. Forward as the dev-bypass header that
123
+ // the underlying services accept.
124
+ headers["PAYMENT-SIGNATURE"] = callerBypassKey;
125
+ }
126
+ else {
127
+ // A wrong key is a guess at the operator secret, not ordinary traffic.
128
+ // Report it so the transport can budget guesses separately from requests.
129
+ opts.onBypassFailure?.();
130
+ }
102
131
  }
103
132
  else {
104
133
  // No auth supplied. Fire the request anyway so the caller gets
@@ -168,4 +197,78 @@ export async function dispatchTool(toolName, args, opts = {}) {
168
197
  function stringArg(v) {
169
198
  return typeof v === "string" && v.length > 0 ? v : undefined;
170
199
  }
200
+ // Args the dispatcher consumes itself. They are never part of the API payload
201
+ // and are not declared on every schema, so the unknown-key drop must not eat
202
+ // them or the operator bypass stops working.
203
+ const RESERVED_ARGS = new Set(["payment_signature", "_bypass_key"]);
204
+ /**
205
+ * Enforce the tool's advertised inputSchema. Throws on a violation; returns the
206
+ * args to actually send, with unknown keys dropped when the schema is closed
207
+ * (today every tool sets additionalProperties: false, so an invented key would
208
+ * otherwise be forwarded verbatim into the upstream query string or body).
209
+ *
210
+ * Deliberately tolerant about numbers-as-strings: LLM clients routinely send
211
+ * "10" for an integer arg and the backends coerce it, so rejecting that would
212
+ * break working callers without closing anything.
213
+ */
214
+ function validateArgs(toolName, schema, args) {
215
+ const props = schema.properties ?? {};
216
+ const closed = schema.additionalProperties === false;
217
+ const out = {};
218
+ // `required` is deliberately NOT enforced here. A caller probing a paid tool
219
+ // with no args to get the real 402 challenge back is a supported discovery
220
+ // move (see the auth block below); turning that into a local error would
221
+ // break it.
222
+ for (const [key, value] of Object.entries(args)) {
223
+ const spec = props[key];
224
+ if (!spec) {
225
+ if (RESERVED_ARGS.has(key))
226
+ out[key] = value;
227
+ else if (!closed)
228
+ out[key] = value;
229
+ continue;
230
+ }
231
+ if (value === undefined || value === null) {
232
+ out[key] = value;
233
+ continue;
234
+ }
235
+ const problem = valueProblem(spec, value);
236
+ if (problem) {
237
+ throw new Error(`tool ${toolName}: invalid value for ${key}: ${problem}`);
238
+ }
239
+ out[key] = value;
240
+ }
241
+ return out;
242
+ }
243
+ function valueProblem(spec, value) {
244
+ if (spec.enum && !spec.enum.some((e) => e === value || String(e) === String(value))) {
245
+ return `must be one of [${spec.enum.map(String).join(", ")}]`;
246
+ }
247
+ if (spec.type === "number" || spec.type === "integer") {
248
+ const n = typeof value === "number" ? value : Number(String(value));
249
+ if (!Number.isFinite(n))
250
+ return `must be a ${spec.type}`;
251
+ if (spec.type === "integer" && !Number.isInteger(n))
252
+ return "must be an integer";
253
+ if (typeof spec.minimum === "number" && n < spec.minimum)
254
+ return `must be >= ${spec.minimum}`;
255
+ if (typeof spec.maximum === "number" && n > spec.maximum)
256
+ return `must be <= ${spec.maximum}`;
257
+ return null;
258
+ }
259
+ if (spec.type === "boolean") {
260
+ if (typeof value === "boolean")
261
+ return null;
262
+ return value === "true" || value === "false" ? null : "must be a boolean";
263
+ }
264
+ if (spec.type === "string") {
265
+ if (typeof value !== "string")
266
+ return "must be a string";
267
+ if (spec.pattern && !new RegExp(spec.pattern).test(value)) {
268
+ return `must match ${spec.pattern}`;
269
+ }
270
+ return null;
271
+ }
272
+ return null;
273
+ }
171
274
  //# sourceMappingURL=dispatch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
@@ -17,23 +17,52 @@
17
17
  * no new secret is required. Inert when unset. NB: the H-Seal API host is
18
18
  * h-seal.xr-utilities.ai (the .com host is a browser-only, Cloudflare-challenged
19
19
  * surface and returns 403 to server clients).
20
+ *
21
+ * NOTHING in the attestation may be taken on trust. The receipt carries a real
22
+ * signature from the operator's key, so a backend (or anything that can shape a
23
+ * proxied response) that hands us an invented (requestHash, responseHash,
24
+ * providerIdentity) triple would otherwise get the operator to vouch for
25
+ * arbitrary content on behalf of an arbitrary third party. Before signing:
26
+ *
27
+ * 1. responseHash is recomputed from the body we actually delivered. It is
28
+ * the one hash the MCP can corroborate on its own.
29
+ * 2. The provider's own ed25519 signature is verified, AND the public key on
30
+ * the wire must derive the r-address in providerIdentity - otherwise any
31
+ * self-signed blob naming a real service would pass.
32
+ * 3. providerIdentity is pinned to HSEAL_ALLOWED_PROVIDERS when that env var
33
+ * is set (comma-separated CAIP-10 identities).
34
+ *
35
+ * requestHash stays PROVIDER-ASSERTED: backends hash a synthetic
36
+ * {"tool": ...} object the MCP cannot reproduce, so a strict comparison would
37
+ * reject every live receipt. The result says so via `requestHashBasis` rather
38
+ * than implying the caller corroborated it.
20
39
  */
21
40
  export declare const hSealReceiptEnabled: boolean;
22
41
  export interface HSealReceiptResult {
23
- body: unknown;
42
+ /** The signed receipt. Absent whenever the MCP refused to sign, or H-Seal explicitly rejected. */
43
+ body?: unknown;
24
44
  verdict?: unknown;
25
45
  verifyError?: string;
46
+ /** Always "provider_asserted" — the MCP cannot reproduce the backend's requestHash preimage. */
47
+ requestHashBasis?: string;
26
48
  }
27
49
  /**
28
- * Build + verify a 2-party receipt from a backend provider attestation. Always
29
- * returns the signed `body` (valid + independently verifiable) when signing
30
- * succeeds; `verdict` is best-effort — if the H-Seal service is unreachable the
31
- * body still stands and `verifyError` records why. Returns undefined only when
32
- * H-Seal isn't configured or the attestation is malformed. Never throws.
50
+ * Build + verify a 2-party receipt from a backend provider attestation. Returns
51
+ * the signed `body` (valid + independently verifiable) when signing succeeds;
52
+ * `verdict` is best-effort — if the H-Seal service is unreachable the body
53
+ * still stands and `verifyError` records why, but an explicit H-Seal reject
54
+ * drops the body. A responseHash that does not match what we delivered also
55
+ * drops the body, and says so in `verifyError` rather than vanishing, as does
56
+ * every other refusal: a malformed attestation, a provider signature that does
57
+ * not authenticate, and an identity outside HSEAL_ALLOWED_PROVIDERS all return
58
+ * a body-less result with a reason. Only two cases return undefined - H-Seal
59
+ * not being configured at all, and our own signing throwing. Never throws.
33
60
  */
34
61
  export declare function buildReceipt(opts: {
35
62
  serviceEndpoint: string;
36
63
  attestation: unknown;
64
+ /** The body actually delivered to the caller (result minus the attestation envelope). */
65
+ responseBody: unknown;
37
66
  startedAt: number;
38
67
  completedAt: number;
39
68
  latencyMs: number;
@@ -17,40 +17,198 @@
17
17
  * no new secret is required. Inert when unset. NB: the H-Seal API host is
18
18
  * h-seal.xr-utilities.ai (the .com host is a browser-only, Cloudflare-challenged
19
19
  * surface and returns 403 to server clients).
20
+ *
21
+ * NOTHING in the attestation may be taken on trust. The receipt carries a real
22
+ * signature from the operator's key, so a backend (or anything that can shape a
23
+ * proxied response) that hands us an invented (requestHash, responseHash,
24
+ * providerIdentity) triple would otherwise get the operator to vouch for
25
+ * arbitrary content on behalf of an arbitrary third party. Before signing:
26
+ *
27
+ * 1. responseHash is recomputed from the body we actually delivered. It is
28
+ * the one hash the MCP can corroborate on its own.
29
+ * 2. The provider's own ed25519 signature is verified, AND the public key on
30
+ * the wire must derive the r-address in providerIdentity - otherwise any
31
+ * self-signed blob naming a real service would pass.
32
+ * 3. providerIdentity is pinned to HSEAL_ALLOWED_PROVIDERS when that env var
33
+ * is set (comma-separated CAIP-10 identities).
34
+ *
35
+ * requestHash stays PROVIDER-ASSERTED: backends hash a synthetic
36
+ * {"tool": ...} object the MCP cannot reproduce, so a strict comparison would
37
+ * reject every live receipt. The result says so via `requestHashBasis` rather
38
+ * than implying the caller corroborated it.
20
39
  */
21
- import { randomUUID } from "node:crypto";
22
- import { signReceipt, attachAttestation, HSealClient, ed25519Signer, } from "@xr-utilities/h-seal-provider";
40
+ import { createHash, createPublicKey, randomUUID, verify as verifySignature } from "node:crypto";
41
+ import { signReceipt, attachAttestation, hashCanonicalJson, sha256Hex, HSealClient, ed25519Signer, } from "@xr-utilities/h-seal-provider";
23
42
  const HSEAL_API = process.env["HSEAL_ENDPOINT"] ?? "https://h-seal.xr-utilities.ai";
24
43
  const RECEIPT_TOPIC = process.env["HSEAL_RECEIPT_TOPIC"] ?? "0.0.10500472";
25
44
  const callerIdentity = process.env["MCP_IDENTITY"] ?? process.env["PROVIDER_IDENTITY"];
26
45
  const callerKeyRaw = process.env["MCP_KEY_RAW"] ?? process.env["PROVIDER_KEY_RAW"];
46
+ const allowedProviders = (process.env["HSEAL_ALLOWED_PROVIDERS"] ?? "")
47
+ .split(",")
48
+ .map((s) => s.trim())
49
+ .filter(Boolean);
50
+ /** How the receipt's requestHash was established. See the module header. */
51
+ const REQUEST_HASH_BASIS = "provider_asserted";
27
52
  export const hSealReceiptEnabled = Boolean(callerIdentity && callerKeyRaw);
53
+ /**
54
+ * A well-formed attestation carries the provider's own signature. Accepting a
55
+ * three-string blob would let a "2-party" receipt be emitted with no second
56
+ * party at all, so providerSignature / scheme / issuedAt are required here.
57
+ */
28
58
  function looksLikeAttestation(a) {
29
- return (typeof a === "object" && a !== null &&
30
- typeof a["requestHash"] === "string" &&
31
- typeof a["responseHash"] === "string" &&
32
- typeof a["providerIdentity"] === "string");
59
+ if (typeof a !== "object" || a === null)
60
+ return false;
61
+ const r = a;
62
+ return (typeof r["requestHash"] === "string" &&
63
+ typeof r["responseHash"] === "string" &&
64
+ typeof r["providerIdentity"] === "string" &&
65
+ typeof r["providerSignature"] === "string" &&
66
+ typeof r["providerSignatureScheme"] === "string" &&
67
+ typeof r["providerIssuedAt"] === "number");
68
+ }
69
+ const XRPL_BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
70
+ // SPKI DER header for a raw ed25519 public key: 12 fixed bytes + the 32-byte key.
71
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
72
+ function base58Xrpl(buf) {
73
+ let n = BigInt("0x" + (buf.toString("hex") || "0"));
74
+ let out = "";
75
+ while (n > 0n) {
76
+ out = XRPL_BASE58[Number(n % 58n)] + out;
77
+ n /= 58n;
78
+ }
79
+ for (const b of buf) {
80
+ if (b !== 0)
81
+ break;
82
+ out = XRPL_BASE58[0] + out;
83
+ }
84
+ return out;
85
+ }
86
+ /** Classic r-address for a 33-byte XRPL public key (0xED prefix + raw ed25519). */
87
+ function xrplAddressFromPubKey(pubKey) {
88
+ const accountId = createHash("ripemd160")
89
+ .update(createHash("sha256").update(pubKey).digest())
90
+ .digest();
91
+ const payload = Buffer.concat([Buffer.from([0x00]), accountId]);
92
+ const checksum = createHash("sha256")
93
+ .update(createHash("sha256").update(payload).digest())
94
+ .digest()
95
+ .subarray(0, 4);
96
+ return base58Xrpl(Buffer.concat([payload, checksum]));
97
+ }
98
+ /**
99
+ * Authenticate the provider. Returns null when the attestation really was
100
+ * signed by the account it names, otherwise the reason it wasn't. The preimage
101
+ * mirrors the SDK's signProviderAttestation exactly: the sha256 of the
102
+ * canonical JSON of {kind: "provider_attest", payload}, signed raw for XRPL.
103
+ */
104
+ function providerSignatureProblem(att) {
105
+ const parts = att.providerIdentity.split(":");
106
+ const namespace = parts[0];
107
+ const address = parts.slice(2).join(":");
108
+ if (namespace !== "xrpl" || !address) {
109
+ return `providerIdentity ${att.providerIdentity} is not an xrpl CAIP-10 identity; cannot authenticate it`;
110
+ }
111
+ if (att.providerSignatureScheme !== "ed25519") {
112
+ return `providerSignatureScheme ${att.providerSignatureScheme} is not ed25519`;
113
+ }
114
+ const wire = /^ed([0-9a-fA-F]{64}):([0-9a-fA-F]{128})$/.exec(att.providerSignature);
115
+ if (!wire) {
116
+ return "providerSignature is not the ed<pubkey_hex>:<sig_hex> wire form";
117
+ }
118
+ const pubKeyRaw = Buffer.from(wire[1], "hex");
119
+ const signature = Buffer.from(wire[2], "hex");
120
+ // Bind the key to the identity. Without this a self-signed blob naming any
121
+ // r-address verifies against its own key and passes.
122
+ const derived = xrplAddressFromPubKey(Buffer.concat([Buffer.from([0xed]), pubKeyRaw]));
123
+ if (derived !== address) {
124
+ return `providerSignature public key derives ${derived}, not the ${address} in providerIdentity`;
125
+ }
126
+ const digest = hashCanonicalJson({
127
+ kind: "provider_attest",
128
+ payload: {
129
+ providerIdentity: att.providerIdentity,
130
+ requestHash: att.requestHash,
131
+ responseHash: att.responseHash,
132
+ providerIssuedAt: att.providerIssuedAt,
133
+ },
134
+ });
135
+ const pubKey = createPublicKey({
136
+ key: Buffer.concat([ED25519_SPKI_PREFIX, pubKeyRaw]),
137
+ format: "der",
138
+ type: "spki",
139
+ });
140
+ if (!verifySignature(null, digest, pubKey, signature)) {
141
+ return "providerSignature does not verify over the attested payload";
142
+ }
143
+ return null;
33
144
  }
145
+ // One line per unpinned provider, not one per call: the operator needs to see
146
+ // it once to populate HSEAL_ALLOWED_PROVIDERS, not on every tool call.
147
+ const warnedUnpinned = new Set();
34
148
  /**
35
- * Build + verify a 2-party receipt from a backend provider attestation. Always
36
- * returns the signed `body` (valid + independently verifiable) when signing
37
- * succeeds; `verdict` is best-effort — if the H-Seal service is unreachable the
38
- * body still stands and `verifyError` records why. Returns undefined only when
39
- * H-Seal isn't configured or the attestation is malformed. Never throws.
149
+ * Build + verify a 2-party receipt from a backend provider attestation. Returns
150
+ * the signed `body` (valid + independently verifiable) when signing succeeds;
151
+ * `verdict` is best-effort — if the H-Seal service is unreachable the body
152
+ * still stands and `verifyError` records why, but an explicit H-Seal reject
153
+ * drops the body. A responseHash that does not match what we delivered also
154
+ * drops the body, and says so in `verifyError` rather than vanishing, as does
155
+ * every other refusal: a malformed attestation, a provider signature that does
156
+ * not authenticate, and an identity outside HSEAL_ALLOWED_PROVIDERS all return
157
+ * a body-less result with a reason. Only two cases return undefined - H-Seal
158
+ * not being configured at all, and our own signing throwing. Never throws.
40
159
  */
41
160
  export async function buildReceipt(opts) {
161
+ // The ONLY branch that may stay silent: H-Seal genuinely is not configured,
162
+ // so the absence of _meta.hSealReceipt is the truthful answer.
42
163
  if (!hSealReceiptEnabled)
43
164
  return undefined;
44
- if (!looksLikeAttestation(opts.attestation))
45
- return undefined;
165
+ if (!looksLikeAttestation(opts.attestation)) {
166
+ // The server only calls us when the backend put SOMETHING in the
167
+ // attestation slot, so a shape we cannot parse is producer drift, not
168
+ // "no attestation". Do not echo the blob back to the caller.
169
+ console.error("[hSealReceipt] attestation is missing required fields (see looksLikeAttestation); refusing to sign");
170
+ return { verifyError: "provider attestation is malformed", requestHashBasis: REQUEST_HASH_BASIS };
171
+ }
46
172
  const att = opts.attestation;
173
+ // Corroborate the one hash we can compute ourselves. The backends hash the
174
+ // canonical JSON of the response minus the attestation envelope, which is
175
+ // exactly what the server hands us here. A mismatch means the receipt would
176
+ // bind our signature to bytes we never served.
177
+ if (sha256Hex(opts.responseBody) !== att.responseHash) {
178
+ // Refusing is right, but returning nothing made the refusal invisible: the
179
+ // flagship paid tool would just stop carrying _meta.hSealReceipt with no
180
+ // signal past one log line. Name the provider so an operator can tell which
181
+ // backend drifted (a producer that attests before its response model
182
+ // appends footer fields lands here on EVERY call).
183
+ console.error(`[hSealReceipt] responseHash does not match the delivered body for ${att.providerIdentity}; refusing to sign`);
184
+ return { verifyError: "responseHash does not match the delivered body", requestHashBasis: REQUEST_HASH_BASIS };
185
+ }
186
+ const problem = providerSignatureProblem(att);
187
+ if (problem) {
188
+ console.error(`[hSealReceipt] provider attestation rejected: ${problem}; refusing to sign`);
189
+ // `problem` names an UNAUTHENTICATED identity/key, so it stays in the log.
190
+ // The caller gets the fact of the refusal, which is the part that must not
191
+ // be indistinguishable from the feature being switched off.
192
+ return { verifyError: "provider attestation failed authentication", requestHashBasis: REQUEST_HASH_BASIS };
193
+ }
194
+ if (allowedProviders.length > 0) {
195
+ if (!allowedProviders.includes(att.providerIdentity)) {
196
+ console.error(`[hSealReceipt] providerIdentity ${att.providerIdentity} is not in HSEAL_ALLOWED_PROVIDERS; refusing to sign`);
197
+ return { verifyError: "provider is not in HSEAL_ALLOWED_PROVIDERS", requestHashBasis: REQUEST_HASH_BASIS };
198
+ }
199
+ }
200
+ else if (!warnedUnpinned.has(att.providerIdentity)) {
201
+ warnedUnpinned.add(att.providerIdentity);
202
+ console.error(`[hSealReceipt] HSEAL_ALLOWED_PROVIDERS is unset; co-signing for self-attested provider ${att.providerIdentity}. ` +
203
+ `Set it to the known XR-* identities to pin who this key will vouch for.`);
204
+ }
47
205
  try {
48
206
  const signed = await signReceipt({
49
207
  receipt: {
50
208
  taskId: randomUUID(),
51
209
  serviceEndpoint: opts.serviceEndpoint,
52
- requestHash: att.requestHash, // MUST equal the attestation's
53
- responseHash: att.responseHash, // MUST equal the attestation's
210
+ requestHash: att.requestHash, // provider-asserted, see REQUEST_HASH_BASIS
211
+ responseHash: att.responseHash, // corroborated against the delivered body above
54
212
  resultStatus: "success",
55
213
  startedAt: opts.startedAt,
56
214
  completedAt: opts.completedAt,
@@ -65,10 +223,19 @@ export async function buildReceipt(opts) {
65
223
  const body = attachAttestation(signed.body, att);
66
224
  try {
67
225
  const verdict = await new HSealClient({ endpoint: HSEAL_API }).verify(body);
68
- return { body, verdict };
226
+ if (verdict.ok === false) {
227
+ // An explicit reject is the one verdict that must drop the body:
228
+ // handing the caller a receipt H-Seal has already refused presents it
229
+ // as provenance it does not carry. An unreachable H-Seal is different
230
+ // — the body verifies independently, so it still rides out below.
231
+ const reason = verdict.reason ?? "no reason given";
232
+ console.error(`[hSealReceipt] H-Seal rejected the receipt: ${reason}`);
233
+ return { verifyError: `H-Seal rejected the receipt: ${reason}`, requestHashBasis: REQUEST_HASH_BASIS };
234
+ }
235
+ return { body, verdict, requestHashBasis: REQUEST_HASH_BASIS };
69
236
  }
70
237
  catch (err) {
71
- return { body, verifyError: err.message };
238
+ return { body, verifyError: err.message, requestHashBasis: REQUEST_HASH_BASIS };
72
239
  }
73
240
  }
74
241
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"hSealReceipt.js","sourceRoot":"","sources":["../src/hSealReceipt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,aAAa,GAEd,MAAM,+BAA+B,CAAC;AAEvC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,gCAAgC,CAAC;AACpF,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,cAAc,CAAC;AAC3E,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACvF,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;AAEnF,MAAM,CAAC,MAAM,mBAAmB,GAAY,OAAO,CAAC,cAAc,IAAI,YAAY,CAAC,CAAC;AAQpF,SAAS,oBAAoB,CAAC,CAAU;IACtC,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;QACnC,OAAQ,CAA6B,CAAC,aAAa,CAAC,KAAK,QAAQ;QACjE,OAAQ,CAA6B,CAAC,cAAc,CAAC,KAAK,QAAQ;QAClE,OAAQ,CAA6B,CAAC,kBAAkB,CAAC,KAAK,QAAQ,CACvE,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAMlC;IACC,IAAI,CAAC,mBAAmB;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC;YAC/B,OAAO,EAAE;gBACP,MAAM,EAAE,UAAU,EAAE;gBACpB,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,WAAW,EAAE,GAAG,CAAC,WAAW,EAAI,+BAA+B;gBAC/D,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,+BAA+B;gBAC/D,YAAY,EAAE,SAAS;gBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,cAAc,EAAE,cAAwB,EAAI,kBAAkB;gBAC9D,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;gBACtC,cAAc,EAAE,aAAa;aAC9B;YACD,MAAM,EAAE,aAAa,CAAC,YAAsB,CAAC;YAC7C,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAI,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC5E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,EAAE,IAAI,EAAE,WAAW,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC;QACvD,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,+BAAgC,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACvE,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"hSealReceipt.js","sourceRoot":"","sources":["../src/hSealReceipt.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -39,9 +39,17 @@ async function main() {
39
39
  // on stderr (Claude Desktop captures stdout for the JSON-RPC stream).
40
40
  // On HTTP we log normally.
41
41
  if (!skipValidate) {
42
- const results = await validateAllServices({ strict: false });
42
+ // Opt-in, default off: railway.json restarts ON_FAILURE, so failing closed
43
+ // by default would crash-loop the endpoint whenever one backend is cold.
44
+ const strict = process.env["STRICT_VALIDATE"] === "1";
45
+ const results = await validateAllServices({ strict });
43
46
  const log = transport === "stdio" ? console.error : console.log;
44
47
  for (const r of results) {
48
+ if (!r.checked) {
49
+ // Always say it, strict or not: a boot that verified nothing must be
50
+ // impossible to mistake for a clean one.
51
+ log(`[validate] ${r.service}: NOT CHECKED (no manifest)`);
52
+ }
45
53
  if (r.errors.length) {
46
54
  log(`[validate] ${r.service}: ERRORS`);
47
55
  r.errors.forEach((e) => log(` ${e}`));
@@ -52,7 +60,8 @@ async function main() {
52
60
  }
53
61
  }
54
62
  const anyError = results.some((r) => r.errors.length);
55
- if (anyError && process.env["MCP_FAIL_ON_DRIFT"] === "1") {
63
+ const anyUnchecked = results.some((r) => !r.checked);
64
+ if ((anyError || (strict && anyUnchecked)) && process.env["MCP_FAIL_ON_DRIFT"] === "1") {
56
65
  process.exit(2);
57
66
  }
58
67
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}