@fidacy/mcp 0.1.5 → 0.1.6

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/assess.js CHANGED
@@ -1,139 +1,120 @@
1
- // Self-contained client for the LIVE Fidacy engine POST /v1/assess.
2
- //
3
- // This mirrors the request/response shape of the OSS @fidacy/sdk client EXACTLY
4
- // but does NOT depend on it: the MCP package stays self-contained so a single
5
- // install ships both the signed verdict layer (this) and the payment firewall.
6
- //
7
- // The returned signed proof is `riskPayloadJws` + `signingKeyId`, verifiable by
8
- // anyone via @fidacy/verify against {engineUrl}/.well-known/jwks.json.
9
- //
10
- // Secret safety: the API key, request body, and any secret are NEVER logged,
11
- // echoed, or attached to an error. Errors carry only stable type/status.
12
- /**
13
- * Typed error from the engine. `message` is STATIC and never contains the key or
14
- * request body. `type` is the stable engine error code (or a client code), and
15
- * `status` is the HTTP status (0 for network/timeout).
16
- */
17
- export class AssessError extends Error {
18
- type;
19
- status;
20
- details;
21
- rejection_reasons;
22
- constructor(opts) {
23
- super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
24
- this.name = "AssessError";
25
- this.type = opts.type;
26
- this.status = opts.status;
27
- this.details = opts.details;
28
- this.rejection_reasons = opts.rejection_reasons;
29
- }
30
- }
31
- const DEFAULT_TIMEOUT = 10_000;
32
- const DEFAULT_RETRIES = 2;
33
- const defaultBackoff = (n) => Math.min(200 * 2 ** n, 2_000);
1
+ // src/assess.ts
2
+ var AssessError = class extends Error {
3
+ type;
4
+ status;
5
+ details;
6
+ rejection_reasons;
7
+ constructor(opts) {
8
+ super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
9
+ this.name = "AssessError";
10
+ this.type = opts.type;
11
+ this.status = opts.status;
12
+ this.details = opts.details;
13
+ this.rejection_reasons = opts.rejection_reasons;
14
+ }
15
+ };
16
+ var DEFAULT_TIMEOUT = 1e4;
17
+ var DEFAULT_RETRIES = 2;
18
+ var defaultBackoff = (n) => Math.min(200 * 2 ** n, 2e3);
34
19
  function isRecord(v) {
35
- return typeof v === "object" && v !== null;
20
+ return typeof v === "object" && v !== null;
36
21
  }
37
22
  function sleep(ms) {
38
- return new Promise((resolve) => setTimeout(resolve, ms));
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
24
  }
40
- /**
41
- * Call the live engine's POST /v1/assess and return the signed trust verdict.
42
- *
43
- * Retries ONLY when an idempotency key was supplied, on 5xx/429/network/timeout,
44
- * with small exponential backoff and a per-request AbortController timeout. This
45
- * mirrors the @fidacy/sdk request() behavior. Self-contained; no sdk import.
46
- */
47
- export async function assessAction(params, cfg) {
48
- const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
49
- if (typeof fetchImpl !== "function") {
50
- throw new AssessError({ type: "config_error", status: 0 });
51
- }
52
- const baseUrl = cfg.engineUrl.replace(/\/+$/, "");
53
- const url = `${baseUrl}/v1/assess`;
54
- const timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT;
55
- const maxRetries = cfg.maxRetries ?? DEFAULT_RETRIES;
56
- const backoffMs = cfg.backoffMs ?? defaultBackoff;
57
- // Build the body with snake_case wire keys, omitting unset keys entirely.
58
- const body = {
59
- kind: params.kind ?? "ap2_payment",
60
- mandate: params.mandate,
61
- };
62
- if (params.mandateType !== undefined)
63
- body.mandateType = params.mandateType;
64
- if (params.a2a !== undefined)
65
- body.a2a = params.a2a;
66
- if (params.spendingMandate !== undefined)
67
- body.spending_mandate = params.spendingMandate;
68
- if (params.idempotencyKey !== undefined)
69
- body.idempotency_key = params.idempotencyKey;
70
- const payload = JSON.stringify(body);
71
- const headers = {
72
- "content-type": "application/json",
73
- authorization: `Bearer ${cfg.apiKey}`,
74
- };
75
- if (params.a2a !== undefined)
76
- headers["A2A-Version"] = "0.1.0";
77
- // Retry is only safe when the caller supplied an idempotency key.
78
- const canRetryTransient = params.idempotencyKey !== undefined;
79
- let attempt = 0;
80
- for (;;) {
81
- try {
82
- return await postOnce(fetchImpl, url, headers, payload, timeoutMs);
83
- }
84
- catch (err) {
85
- const isTransient = err instanceof AssessError &&
86
- (err.status === 0 || err.status === 429 || err.status >= 500);
87
- const canRetry = isTransient && canRetryTransient && attempt < maxRetries;
88
- if (!canRetry)
89
- throw err;
90
- await sleep(backoffMs(attempt));
91
- attempt += 1;
92
- }
25
+ function requireSafeBaseUrl(raw) {
26
+ const trimmed = String(raw ?? "").replace(/\/+$/, "");
27
+ let u;
28
+ try {
29
+ u = new URL(trimmed);
30
+ } catch {
31
+ throw new AssessError({ type: "config_error", status: 0 });
32
+ }
33
+ const isLocalHttp = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
34
+ if (u.protocol !== "https:" && !isLocalHttp) {
35
+ throw new AssessError({ type: "config_error", status: 0 });
36
+ }
37
+ return trimmed;
38
+ }
39
+ async function assessAction(params, cfg) {
40
+ const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
41
+ if (typeof fetchImpl !== "function") {
42
+ throw new AssessError({ type: "config_error", status: 0 });
43
+ }
44
+ const baseUrl = requireSafeBaseUrl(cfg.engineUrl);
45
+ const url = `${baseUrl}/v1/assess`;
46
+ const timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT;
47
+ const maxRetries = cfg.maxRetries ?? DEFAULT_RETRIES;
48
+ const backoffMs = cfg.backoffMs ?? defaultBackoff;
49
+ const body = {
50
+ kind: params.kind ?? "ap2_payment",
51
+ mandate: params.mandate
52
+ };
53
+ if (params.mandateType !== void 0) body.mandateType = params.mandateType;
54
+ if (params.a2a !== void 0) body.a2a = params.a2a;
55
+ if (params.spendingMandate !== void 0) body.spending_mandate = params.spendingMandate;
56
+ if (params.idempotencyKey !== void 0) body.idempotency_key = params.idempotencyKey;
57
+ const payload = JSON.stringify(body);
58
+ if (payload.length > 256 * 1024) {
59
+ throw new AssessError({ type: "config_error", status: 0 });
60
+ }
61
+ const headers = {
62
+ "content-type": "application/json",
63
+ authorization: `Bearer ${cfg.apiKey}`
64
+ };
65
+ if (params.a2a !== void 0) headers["A2A-Version"] = "0.1.0";
66
+ const canRetryTransient = params.idempotencyKey !== void 0;
67
+ let attempt = 0;
68
+ for (; ; ) {
69
+ try {
70
+ return await postOnce(fetchImpl, url, headers, payload, timeoutMs);
71
+ } catch (err) {
72
+ const isTransient = err instanceof AssessError && (err.status === 0 || err.status === 429 || err.status >= 500);
73
+ const canRetry = isTransient && canRetryTransient && attempt < maxRetries;
74
+ if (!canRetry) throw err;
75
+ await sleep(backoffMs(attempt));
76
+ attempt += 1;
93
77
  }
78
+ }
94
79
  }
95
80
  async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
96
- const controller = new AbortController();
97
- const timer = setTimeout(() => controller.abort(), timeoutMs);
98
- let res;
81
+ const controller = new AbortController();
82
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
83
+ let res;
84
+ try {
85
+ res = await fetchImpl(url, {
86
+ method: "POST",
87
+ headers,
88
+ body: payload,
89
+ signal: controller.signal
90
+ });
91
+ } catch (err) {
92
+ const aborted = controller.signal.aborted;
93
+ throw new AssessError({ type: aborted ? "timeout" : "network_error", status: 0 });
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ const text = await res.text();
98
+ let parsed = void 0;
99
+ if (text) {
99
100
  try {
100
- res = await fetchImpl(url, {
101
- method: "POST",
102
- headers,
103
- body: payload,
104
- signal: controller.signal,
105
- });
106
- }
107
- catch (err) {
108
- const aborted = controller.signal.aborted;
109
- // Never attach the key/body/err message that could leak a secret.
110
- throw new AssessError({ type: aborted ? "timeout" : "network_error", status: 0 });
111
- }
112
- finally {
113
- clearTimeout(timer);
114
- }
115
- const text = await res.text();
116
- let parsed = undefined;
117
- if (text) {
118
- try {
119
- parsed = JSON.parse(text);
120
- }
121
- catch {
122
- // non-JSON response: handled below by status
123
- }
124
- }
125
- if (!res.ok) {
126
- const type = (isRecord(parsed) && typeof parsed.error === "string" && parsed.error) ||
127
- `http_${res.status}`;
128
- const details = isRecord(parsed) ? parsed.details : undefined;
129
- const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons)
130
- ? parsed.rejection_reasons
131
- : undefined;
132
- throw new AssessError({ type, status: res.status, details, rejection_reasons });
133
- }
134
- if (!isRecord(parsed)) {
135
- throw new AssessError({ type: "invalid_response", status: res.status });
101
+ parsed = JSON.parse(text);
102
+ } catch {
136
103
  }
137
- return parsed;
104
+ }
105
+ if (!res.ok) {
106
+ const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
107
+ const details = isRecord(parsed) ? parsed.details : void 0;
108
+ const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
109
+ throw new AssessError({ type, status: res.status, details, rejection_reasons });
110
+ }
111
+ if (!isRecord(parsed)) {
112
+ throw new AssessError({ type: "invalid_response", status: res.status });
113
+ }
114
+ return parsed;
138
115
  }
139
- //# sourceMappingURL=assess.js.map
116
+ export {
117
+ AssessError,
118
+ assessAction,
119
+ requireSafeBaseUrl
120
+ };