@paybond/kit 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # `@paybond/kit`
2
2
 
3
- Paybond Kit for TypeScript provides a tenant-bound Harbor client, gateway-backed service-account sessions, capability verification, canonical signing helpers for intent creation and evidence submission, x402 / USDC-on-Base intent funding helpers, tenant-scoped ledger provenance reads, and tenant-scoped Signal analytics and reputation reads.
3
+ Paybond Kit for TypeScript is the npm package for tenant-bound Paybond integrations. It opens gateway-authenticated Harbor sessions, verifies capability tokens, signs intent and evidence payloads, funds x402 / USDC-on-Base intents, and reads tenant-scoped Signal, fraud, ledger, protocol, and A2A data.
4
4
 
5
- Install the public package with:
5
+ ## Install
6
6
 
7
7
  ```bash
8
8
  npm install @paybond/kit
9
9
  ```
10
10
 
11
+ `@paybond/kit` is an ESM-only package for modern Node.js runtimes. Use `import` from a Node ESM / `NodeNext` project or a compatible bundler.
12
+
11
13
  ## Open source
12
14
 
13
15
  `@paybond/kit` is distributed as open-source software under the Apache 2.0 license. The published npm package includes the full license text in `LICENSE`.
@@ -17,6 +19,23 @@ npm install @paybond/kit
17
19
  - Node.js 22+
18
20
  - A `paybond_sk_...` service-account API key
19
21
  - Reachable Gateway and Harbor base URLs
22
+ - For capability verification: a funded intent id and a capability token minted for that intent
23
+ - For intent creation or evidence submission: 32-byte Ed25519 signing seeds owned by your application
24
+
25
+ Minimal environment for the quick start:
26
+
27
+ ```bash
28
+ export PAYBOND_GATEWAY_URL="https://gateway.example.com"
29
+ export PAYBOND_HARBOR_URL="https://harbor.example.com"
30
+ export PAYBOND_API_KEY="paybond_sk_..."
31
+ ```
32
+
33
+ Optional, if you want the quick start to verify a capability:
34
+
35
+ ```bash
36
+ export PAYBOND_INTENT_ID="00000000-0000-0000-0000-000000000000"
37
+ export PAYBOND_CAPABILITY="base64-biscuit-token"
38
+ ```
20
39
 
21
40
  ## Tenant isolation
22
41
 
@@ -31,22 +50,37 @@ Every session is bound to the tenant realm echoed by gateway-authenticated servi
31
50
  ```ts
32
51
  import { Paybond } from "@paybond/kit";
33
52
 
53
+ function requiredEnv(name: string): string {
54
+ const value = process.env[name];
55
+ if (!value) {
56
+ throw new Error(`missing ${name}`);
57
+ }
58
+ return value;
59
+ }
60
+
34
61
  const paybond = await Paybond.open({
35
- gatewayBaseUrl: "https://gateway.example.com",
36
- apiKey: process.env.PAYBOND_API_KEY!,
37
- harborBaseUrl: "https://harbor.example.com",
62
+ gatewayBaseUrl: requiredEnv("PAYBOND_GATEWAY_URL"),
63
+ apiKey: requiredEnv("PAYBOND_API_KEY"),
64
+ harborBaseUrl: requiredEnv("PAYBOND_HARBOR_URL"),
38
65
  });
39
66
 
40
67
  try {
41
- const verified = await paybond.harbor.verifyCapability({
42
- intentId: process.env.PAYBOND_INTENT_ID!,
43
- token: process.env.PAYBOND_CAPABILITY!,
44
- operation: "payments.capture",
45
- requestedSpendCents: 18_700,
46
- });
47
-
48
- if (!verified.allow) {
49
- throw new Error(`verify denied: ${verified.code ?? "deny"} ${verified.message ?? ""}`);
68
+ console.log("tenant realm:", paybond.harbor.tenantId);
69
+
70
+ const intentId = process.env.PAYBOND_INTENT_ID;
71
+ const capability = process.env.PAYBOND_CAPABILITY;
72
+ if (intentId && capability) {
73
+ const verified = await paybond.harbor.verifyCapability({
74
+ intentId,
75
+ token: capability,
76
+ operation: "payments.capture",
77
+ requestedSpendCents: 18_700,
78
+ });
79
+
80
+ if (!verified.allow) {
81
+ throw new Error(`verify denied: ${verified.code ?? "deny"} ${verified.message ?? ""}`);
82
+ }
83
+ console.log("capability verified:", verified.auditId);
50
84
  }
51
85
  } finally {
52
86
  await paybond.aclose();
@@ -55,18 +89,29 @@ try {
55
89
 
56
90
  ## What the package includes
57
91
 
92
+ Core SDK:
93
+
58
94
  - `Paybond.open(...)` for gateway-authenticated, tenant-derived Harbor sessions
59
95
  - `HarborClient` for capability verification, intent creation, x402 funding, evidence submission, and ledger reads
60
- - Protocol-v2 helpers for mandate verification, replay-safe recognition proof verification, receipt reads, and A2A discovery
61
- - `GatewaySignalClient` and `ServiceAccountSignalSession` for tenant-scoped Signal reads and signed portfolio artifacts
62
- - `paybond.signal` on `Paybond` sessions opened from one service-account API key
96
+ - `paybond.signal` and `paybond.fraud` on `Paybond` sessions opened from one service-account API key
63
97
  - `PaybondIntents` helpers for principal-signed intent creation, x402 funding, and payee-signed evidence submission
98
+
99
+ Gateway and trust helpers:
100
+
101
+ - `GatewaySignalClient` and `ServiceAccountSignalSession` for tenant-scoped Signal reads and signed portfolio artifacts
102
+ - `GatewayFraudClient` and `ServiceAccountFraudSession` for tenant-scoped fraud assessments, review queues, review events, metrics, and release-gate config
103
+ - Protocol-v2 helpers for mandate verification, replay-safe recognition proof verification, receipt reads, and A2A discovery
64
104
  - `paybond-mcp-server` for tenant-bound MCP tool exposure to any MCP-compatible host
65
- - Low-level signing helpers exported for advanced callers
105
+
106
+ Agent-facing surfaces are model-provider agnostic. Paybond verifies tool operations and tenant scope, not whether a tool call came from OpenAI, Anthropic, Gemini, a local model, or another runtime.
107
+
108
+ Advanced exports:
109
+
110
+ - Low-level signing helpers for callers that need to pre-build signed request bodies or evidence payloads
66
111
 
67
112
  `allowedTools` values are your own tool or operation names, not a Paybond-owned catalog. Harbor enforces string matching against whatever names you chose when creating the intent.
68
113
 
69
- `settlementRail` on intent creation is only a rail request. Stripe destinations and x402 receive addresses stay tenant-owned server-side config and are never supplied by the SDK caller.
114
+ `settlementRail` on intent creation is a principal-signed rail request. Stripe destinations and x402 receive addresses stay tenant-owned server-side config and are never supplied by the SDK caller.
70
115
 
71
116
  The protocol-v2 surface is trust-first: signed mandates, recognition proofs, and receipts work across supported settlement adapters instead of treating any single rail as the product boundary.
72
117
 
@@ -75,21 +120,21 @@ Gateway-backed protocol helpers throw `ProtocolHttpError` with parsed `errorCode
75
120
  ## What it does not include
76
121
 
77
122
  - No operator-tier settlement or console workflows
123
+ - No model-provider-specific TypeScript agent wrapper; use the documented app-side wrapper pattern with `PaybondCapabilityBinding`
78
124
  - No model-provider-specific MCP wrapper; the MCP server is host-agnostic and works with any MCP-compatible runtime
79
125
 
80
126
  ## Docs
81
127
 
82
- - Long-form docs: `docs/kit/`
83
- - MCP server guide: `docs/kit/mcp-server.md`
84
- - Agents SDK tutorial: `docs/kit/openai-agents.md`
85
- - TypeScript quickstart: `docs/kit/quickstart-typescript.md`
86
- - TypeScript SDK reference: `docs/kit/sdk-reference-typescript.md`
87
- - Example app: `examples/paybond-kit-typescript/`
88
- - OpenAI Agents example: `examples/paybond-kit-openai-agents-typescript/`
128
+ - Long-form docs: https://paybond.ai/docs/kit
129
+ - TypeScript quickstart: https://paybond.ai/docs/kit/quickstart-typescript
130
+ - TypeScript SDK reference: https://paybond.ai/docs/kit/sdk-reference-typescript
131
+ - MCP server guide: https://paybond.ai/docs/kit/mcp-server
132
+ - Agent runtime tutorial: https://paybond.ai/docs/kit/agent-runtime-tutorial
133
+ - TypeScript example projects: https://paybond.ai/docs/kit/examples-typescript
89
134
 
90
135
  ## Release verification
91
136
 
92
- From `kit/ts`:
137
+ For maintainers working from a source checkout, release verification lives in this package directory:
93
138
 
94
139
  ```bash
95
140
  npm run verify:release
package/dist/index.d.ts CHANGED
@@ -199,6 +199,198 @@ export type SignalPortfolioSummary = {
199
199
  total_receipted_volume_cents: number;
200
200
  operators_under_review: number;
201
201
  };
202
+ export type SignalFraudSeverity = "elevated" | "high" | "critical";
203
+ export type SignalReviewState = "none" | "open" | "in_review" | "closed" | "all";
204
+ export type SignalFraudSignal = {
205
+ code: string;
206
+ severity: SignalFraudSeverity | string;
207
+ category: string;
208
+ window: string;
209
+ evidence_count: number;
210
+ summary: string;
211
+ affects_score: false;
212
+ signal_source?: string;
213
+ first_seen_at?: string;
214
+ last_seen_at?: string;
215
+ evidence_binding_strength?: string;
216
+ provider_event_refs?: string[];
217
+ intent_refs?: string[];
218
+ };
219
+ export type SignalFraudAssessment = {
220
+ fraud_signal_version: string;
221
+ level: "none" | SignalFraudSeverity | string;
222
+ highest_severity: "none" | SignalFraudSeverity | string;
223
+ review_priority: "normal" | "elevated" | "high" | "urgent" | string;
224
+ signal_count: number;
225
+ severe_signal_count: number;
226
+ summary: string;
227
+ };
228
+ export type SignalFraudReleaseGateMode = "review_only" | "critical_hold";
229
+ export type SignalFraudReleaseGateConfig = {
230
+ mode: SignalFraudReleaseGateMode | string;
231
+ };
232
+ export type SignalFraudSignalFamilyReliability = {
233
+ signal_family: string;
234
+ reliable: boolean;
235
+ stale: boolean;
236
+ sparse: boolean;
237
+ reviewed_count: number;
238
+ labeled_outcome_count: number;
239
+ review_precision_bps: number;
240
+ min_signal_family_labeled_outcome_count: number;
241
+ last_labeled_at?: string;
242
+ reasons: string[];
243
+ summary: string;
244
+ };
245
+ export type SignalFraudReleaseGateMetricsReliability = {
246
+ reliable: boolean;
247
+ stale: boolean;
248
+ sparse: boolean;
249
+ reviewed_count: number;
250
+ labeled_outcome_count: number;
251
+ review_precision_bps: number;
252
+ min_reviewed_count: number;
253
+ min_labeled_outcome_count: number;
254
+ min_signal_family_labeled_outcome_count: number;
255
+ min_review_precision_bps: number;
256
+ last_labeled_at?: string;
257
+ signal_families?: SignalFraudSignalFamilyReliability[];
258
+ reasons: string[];
259
+ summary: string;
260
+ };
261
+ export type SignalFraudReleaseGateDecision = {
262
+ mode: SignalFraudReleaseGateMode | string;
263
+ enforcement_enabled: boolean;
264
+ metrics_reliable: boolean;
265
+ release_allowed: boolean;
266
+ hold_required: boolean;
267
+ critical_signal_count: number;
268
+ critical_signal_codes: string[];
269
+ blocking_signal_codes?: string[];
270
+ blocking_evidence_refs?: string[];
271
+ reliability_reasons?: string[];
272
+ reasons: string[];
273
+ summary: string;
274
+ };
275
+ export type SignalFraudAssessmentResponse = {
276
+ schema_version: number;
277
+ tenant_id: string;
278
+ operator_did: string;
279
+ score_model_version: string;
280
+ review_state: string;
281
+ review_outcome: string;
282
+ review_reasons: string[];
283
+ fraud_signals: SignalFraudSignal[];
284
+ fraud_assessment: SignalFraudAssessment;
285
+ release_gate?: SignalFraudReleaseGateDecision;
286
+ [key: string]: unknown;
287
+ };
288
+ export type SignalFraudReviewQueueItem = {
289
+ operator_did: string;
290
+ review_state: string;
291
+ review_outcome: string;
292
+ review_reasons: string[];
293
+ anomaly_flagged: boolean;
294
+ opened_at: string;
295
+ reviewed_at: string;
296
+ updated_at: string;
297
+ last_receipt_message_digest_hex: string;
298
+ fraud_signals: SignalFraudSignal[];
299
+ fraud_assessment: SignalFraudAssessment;
300
+ release_gate?: SignalFraudReleaseGateDecision;
301
+ [key: string]: unknown;
302
+ };
303
+ export type SignalFraudReviewQueueResponse = {
304
+ schema_version: number;
305
+ tenant_id: string;
306
+ score_model_version: string;
307
+ items: SignalFraudReviewQueueItem[];
308
+ };
309
+ export type SignalFraudMetricsWindow = "24h" | "7d" | "30d";
310
+ export type SignalFraudMetricsResponse = {
311
+ schema_version: number;
312
+ tenant_id: string;
313
+ score_model_version: string;
314
+ fraud_signal_version: string;
315
+ window: SignalFraudMetricsWindow | string;
316
+ window_started_at: string;
317
+ window_ended_at: string;
318
+ generated_at: string;
319
+ flagged_operator_count: number;
320
+ critical_signal_count: number;
321
+ high_signal_count: number;
322
+ elevated_signal_count: number;
323
+ review_open_count: number;
324
+ review_load_count: number;
325
+ reviewed_count: number;
326
+ labeled_outcome_count: number;
327
+ confirmed_risk_count: number;
328
+ false_positive_count: number;
329
+ needs_more_evidence_count: number;
330
+ review_precision_bps: number;
331
+ false_positive_rate_bps: number;
332
+ confirmed_risk_rate_bps: number;
333
+ labeled_coverage_bps: number;
334
+ median_time_to_review_seconds: number;
335
+ refund_burst_count: number;
336
+ dispute_cluster_count: number;
337
+ replay_appeal_abuse_count: number;
338
+ critical_signal_hold_candidate_count: number;
339
+ provider_signal_count: number;
340
+ stale_label_gap_seconds: number;
341
+ stale_signal_family_label_gap_count: number;
342
+ backtest_summary: string;
343
+ release_gate_config?: SignalFraudReleaseGateConfig;
344
+ release_gate_metrics_reliability?: SignalFraudReleaseGateMetricsReliability;
345
+ };
346
+ export type SignalFraudReleaseGateConfigResponse = {
347
+ schema_version: number;
348
+ tenant_id: string;
349
+ score_model_version: string;
350
+ fraud_signal_version: string;
351
+ generated_at: string;
352
+ config: SignalFraudReleaseGateConfig;
353
+ metrics_reliability: SignalFraudReleaseGateMetricsReliability;
354
+ };
355
+ export type SignalFraudReviewEventType = "review_open_requested" | "appeal_requested" | "replay_requested" | "review_outcome_recorded" | SignalFraudReviewOutcome;
356
+ export type SignalFraudReviewOutcome = "confirmed_risk" | "false_positive" | "needs_more_evidence";
357
+ export type SignalFraudReviewEventInput = {
358
+ eventType: SignalFraudReviewEventType | string;
359
+ reviewOutcome?: SignalFraudReviewOutcome | string;
360
+ review_outcome?: SignalFraudReviewOutcome | string;
361
+ signalCode?: string;
362
+ signal_code?: string;
363
+ intentId?: string;
364
+ intent_id?: string;
365
+ providerEventId?: string;
366
+ provider_event_id?: string;
367
+ summary: string;
368
+ };
369
+ export type SignalFraudReviewEventResponse = {
370
+ schema_version: number;
371
+ tenant_id: string;
372
+ operator_did: string;
373
+ score_model_version: string;
374
+ requested_event_type: string;
375
+ recorded_event_type: string;
376
+ review_outcome?: string;
377
+ signal_code?: string;
378
+ intent_id?: string;
379
+ provider_event_id?: string;
380
+ accepted: boolean;
381
+ next_eligible_at?: string;
382
+ [key: string]: unknown;
383
+ };
384
+ export type ListFraudReviewQueueOptions = {
385
+ state?: SignalReviewState | string;
386
+ severity?: SignalFraudSeverity | string;
387
+ limit?: number;
388
+ scoreVersion?: string;
389
+ };
390
+ export type GetFraudMetricsOptions = {
391
+ window?: SignalFraudMetricsWindow | string;
392
+ scoreVersion?: string;
393
+ };
202
394
  export type A2AAgentCard = {
203
395
  name: string;
204
396
  description: string;
@@ -616,6 +808,10 @@ type GatewaySignalClientOptions = {
616
808
  staticGatewayBearerToken: string;
617
809
  maxRetries?: number;
618
810
  };
811
+ type GatewayFraudClientOptions = {
812
+ staticGatewayBearerToken: string;
813
+ maxRetries?: number;
814
+ };
619
815
  /**
620
816
  * Tenant-bound reader for gateway Signal routes.
621
817
  */
@@ -634,6 +830,34 @@ export declare class GatewaySignalClient {
634
830
  getOperatorExplanation(operatorDid: string, scoreVersion?: string): Promise<Record<string, unknown> | null>;
635
831
  getOperatorReviewStatus(operatorDid: string, scoreVersion?: string): Promise<Record<string, unknown> | null>;
636
832
  }
833
+ /**
834
+ * Tenant-bound client for gateway fraud review and metrics routes.
835
+ */
836
+ export declare class GatewayFraudClient {
837
+ private readonly base;
838
+ readonly tenantId: string;
839
+ private readonly bearerToken;
840
+ private readonly maxRetries;
841
+ constructor(gatewayBaseUrl: string, tenantId: string, options: GatewayFraudClientOptions);
842
+ private fetchGetWithRetries;
843
+ private fetchPostJSON;
844
+ private fetchPutJSON;
845
+ private assertTenant;
846
+ private assertOperator;
847
+ private query;
848
+ private normalizedSeverity;
849
+ private normalizedWindow;
850
+ private normalizedReleaseGateMode;
851
+ private normalizedReviewEventType;
852
+ private normalizedReviewOutcome;
853
+ private optionalReviewContext;
854
+ getFraudAssessment(operatorDid: string, scoreVersion?: string): Promise<SignalFraudAssessmentResponse | null>;
855
+ listFraudReviewQueue(options?: ListFraudReviewQueueOptions): Promise<SignalFraudReviewQueueResponse>;
856
+ getFraudMetrics(options?: GetFraudMetricsOptions): Promise<SignalFraudMetricsResponse>;
857
+ getFraudReleaseGateConfig(scoreVersion?: string): Promise<SignalFraudReleaseGateConfigResponse>;
858
+ setFraudReleaseGateMode(mode: SignalFraudReleaseGateMode | string): Promise<SignalFraudReleaseGateConfigResponse>;
859
+ recordFraudReviewEvent(operatorDid: string, event: SignalFraudReviewEventInput, scoreVersion?: string): Promise<SignalFraudReviewEventResponse>;
860
+ }
637
861
  type GatewayA2AClientOptions = {
638
862
  staticGatewayBearerToken?: string;
639
863
  maxRetries?: number;
@@ -701,6 +925,7 @@ export type ServiceAccountSignalSessionInit = {
701
925
  principalPath?: string;
702
926
  maxRetries?: number;
703
927
  };
928
+ export type ServiceAccountFraudSessionInit = ServiceAccountSignalSessionInit;
704
929
  /**
705
930
  * Read-only tenant-bound Signal session for one service-account API key.
706
931
  */
@@ -710,10 +935,19 @@ export declare class ServiceAccountSignalSession {
710
935
  static open(init: ServiceAccountSignalSessionInit): Promise<ServiceAccountSignalSession>;
711
936
  aclose(): Promise<void>;
712
937
  }
938
+ /**
939
+ * Tenant-bound fraud review and metrics session for one service-account API key.
940
+ */
941
+ export declare class ServiceAccountFraudSession {
942
+ readonly fraud: GatewayFraudClient;
943
+ private constructor();
944
+ static open(init: ServiceAccountFraudSessionInit): Promise<ServiceAccountFraudSession>;
945
+ aclose(): Promise<void>;
946
+ }
713
947
  /**
714
948
  * Parameters for {@link PaybondIntents.create} (tenant is taken from the bound Harbor client).
715
- * `settlementRail`, when set, requests one allowed rail; Harbor still resolves the destination
716
- * from tenant-owned settlement config.
949
+ * `settlementRail` is principal-signed and requests one allowed rail; Harbor still resolves the
950
+ * destination from tenant-owned settlement config.
717
951
  */
718
952
  export type PaybondCreateIntentParams = Omit<BuildSignedCreateIntentParams, "tenantId" | "intentId"> & {
719
953
  intentId?: string;
@@ -728,7 +962,7 @@ export declare class PaybondIntents {
728
962
  constructor(harbor: HarborClient);
729
963
  /**
730
964
  * Build a principal-signed `POST /intents` body and submit it. `principalSigningSeed` must be
731
- * 32 bytes. `settlementRail` only requests an allowed rail; destinations stay server-owned.
965
+ * 32 bytes. `settlementRail` is signed as the requested rail; destinations stay server-owned.
732
966
  */
733
967
  create(params: PaybondCreateIntentParams & {
734
968
  idempotencyKey?: string;
@@ -754,6 +988,7 @@ export declare class PaybondIntents {
754
988
  export declare class Paybond {
755
989
  readonly harbor: HarborClient;
756
990
  readonly signal: GatewaySignalClient;
991
+ readonly fraud: GatewayFraudClient;
757
992
  readonly a2a: GatewayA2AClient;
758
993
  readonly protocol: GatewayProtocolClient;
759
994
  readonly intents: PaybondIntents;
package/dist/index.js CHANGED
@@ -659,6 +659,16 @@ export class HarborClient {
659
659
  }
660
660
  const DEFAULT_PRINCIPAL_PATH = "/v1/auth/principal";
661
661
  const SETTLEMENT_RAIL_VALUES = new Set(["stripe_connect", "x402_usdc_base"]);
662
+ const FRAUD_REVIEW_EVENT_TYPES = new Set([
663
+ "review_open_requested",
664
+ "appeal_requested",
665
+ "replay_requested",
666
+ "review_outcome_recorded",
667
+ "confirmed_risk",
668
+ "false_positive",
669
+ "needs_more_evidence",
670
+ ]);
671
+ const FRAUD_REVIEW_OUTCOMES = new Set(["confirmed_risk", "false_positive", "needs_more_evidence"]);
662
672
  function assertJSONObject(value) {
663
673
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
664
674
  throw new Error("expected JSON object");
@@ -884,6 +894,286 @@ export class GatewaySignalClient {
884
894
  return body;
885
895
  }
886
896
  }
897
+ /**
898
+ * Tenant-bound client for gateway fraud review and metrics routes.
899
+ */
900
+ export class GatewayFraudClient {
901
+ base;
902
+ tenantId;
903
+ bearerToken;
904
+ maxRetries;
905
+ constructor(gatewayBaseUrl, tenantId, options) {
906
+ this.base = normalizeBase(gatewayBaseUrl) + "/";
907
+ this.tenantId = tenantId.trim();
908
+ this.bearerToken = options.staticGatewayBearerToken.trim();
909
+ this.maxRetries = Math.max(1, options.maxRetries ?? 3);
910
+ }
911
+ async fetchGetWithRetries(url) {
912
+ let lastErr;
913
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
914
+ let res;
915
+ try {
916
+ res = await fetch(url, {
917
+ method: "GET",
918
+ headers: {
919
+ accept: "application/json",
920
+ authorization: `Bearer ${this.bearerToken}`,
921
+ },
922
+ });
923
+ }
924
+ catch (e) {
925
+ lastErr = e;
926
+ if (attempt + 1 >= this.maxRetries)
927
+ throw e;
928
+ await new Promise((r) => setTimeout(r, backoffMs(attempt)));
929
+ continue;
930
+ }
931
+ if ([429, 500, 502, 503, 504].includes(res.status)) {
932
+ if (attempt + 1 >= this.maxRetries) {
933
+ return res;
934
+ }
935
+ const raSec = parseRetryAfterSeconds(res.headers.get("retry-after"));
936
+ const delayMs = raSec != null ? raSec * 1000 : backoffMs(attempt);
937
+ await new Promise((r) => setTimeout(r, delayMs));
938
+ continue;
939
+ }
940
+ return res;
941
+ }
942
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
943
+ }
944
+ async fetchPostJSON(url, payload) {
945
+ return fetch(url, {
946
+ method: "POST",
947
+ headers: {
948
+ accept: "application/json",
949
+ authorization: `Bearer ${this.bearerToken}`,
950
+ "content-type": "application/json",
951
+ },
952
+ body: JSON.stringify(payload),
953
+ });
954
+ }
955
+ async fetchPutJSON(url, payload) {
956
+ return fetch(url, {
957
+ method: "PUT",
958
+ headers: {
959
+ accept: "application/json",
960
+ authorization: `Bearer ${this.bearerToken}`,
961
+ "content-type": "application/json",
962
+ },
963
+ body: JSON.stringify(payload),
964
+ });
965
+ }
966
+ assertTenant(body, url) {
967
+ const tid = String(body.tenant_id ?? "");
968
+ if (tid !== this.tenantId) {
969
+ throw new Error(`fraud tenant mismatch: client=${this.tenantId} gateway=${tid} url=${url}`);
970
+ }
971
+ }
972
+ assertOperator(body, operatorDid, label) {
973
+ const echoedOperator = String(body.operator_did ?? "");
974
+ if (echoedOperator !== operatorDid) {
975
+ throw new Error(`fraud ${label} operator mismatch: requested=${operatorDid} gateway=${echoedOperator}`);
976
+ }
977
+ }
978
+ query(params) {
979
+ const qs = new URLSearchParams();
980
+ for (const [key, value] of Object.entries(params)) {
981
+ if (typeof value === "number") {
982
+ qs.set(key, String(value));
983
+ continue;
984
+ }
985
+ if (typeof value === "string" && value.trim()) {
986
+ qs.set(key, value.trim());
987
+ }
988
+ }
989
+ const raw = qs.toString();
990
+ return raw ? `?${raw}` : "";
991
+ }
992
+ normalizedSeverity(severity) {
993
+ const normalized = severity?.trim();
994
+ if (!normalized) {
995
+ return undefined;
996
+ }
997
+ if (!["elevated", "high", "critical"].includes(normalized)) {
998
+ throw new Error("fraud severity must be one of elevated, high, or critical");
999
+ }
1000
+ return normalized;
1001
+ }
1002
+ normalizedWindow(window) {
1003
+ const normalized = window?.trim();
1004
+ if (!normalized) {
1005
+ return undefined;
1006
+ }
1007
+ if (!["24h", "7d", "30d"].includes(normalized)) {
1008
+ throw new Error("fraud metrics window must be one of 24h, 7d, or 30d");
1009
+ }
1010
+ return normalized;
1011
+ }
1012
+ normalizedReleaseGateMode(mode) {
1013
+ const normalized = mode.trim();
1014
+ if (!["review_only", "critical_hold"].includes(normalized)) {
1015
+ throw new Error("fraud release gate mode must be one of review_only or critical_hold");
1016
+ }
1017
+ return normalized;
1018
+ }
1019
+ normalizedReviewEventType(eventType) {
1020
+ const normalized = eventType.trim();
1021
+ if (!FRAUD_REVIEW_EVENT_TYPES.has(normalized)) {
1022
+ throw new Error("fraud review eventType must be one of review_open_requested, appeal_requested, replay_requested, review_outcome_recorded, confirmed_risk, false_positive, or needs_more_evidence");
1023
+ }
1024
+ return normalized;
1025
+ }
1026
+ normalizedReviewOutcome(outcome) {
1027
+ const normalized = outcome.trim();
1028
+ if (!FRAUD_REVIEW_OUTCOMES.has(normalized)) {
1029
+ throw new Error("fraud review outcome must be one of confirmed_risk, false_positive, or needs_more_evidence");
1030
+ }
1031
+ return normalized;
1032
+ }
1033
+ optionalReviewContext(value) {
1034
+ const normalized = value?.trim();
1035
+ return normalized || undefined;
1036
+ }
1037
+ async getFraudAssessment(operatorDid, scoreVersion) {
1038
+ const enc = encodeURIComponent(operatorDid);
1039
+ const url = `${this.base}signal/v1/operators/${enc}/review-status${this.query({
1040
+ score_version: scoreVersion,
1041
+ })}`;
1042
+ const res = await this.fetchGetWithRetries(url);
1043
+ const text = await res.text();
1044
+ if (res.status === 404) {
1045
+ return null;
1046
+ }
1047
+ if (!res.ok) {
1048
+ throw new SignalHttpError(`Fraud assessment HTTP ${res.status}: ${text}`, {
1049
+ statusCode: res.status,
1050
+ url,
1051
+ bodyText: text,
1052
+ });
1053
+ }
1054
+ const body = assertJSONObject(JSON.parse(text));
1055
+ this.assertTenant(body, url);
1056
+ this.assertOperator(body, operatorDid, "assessment");
1057
+ return body;
1058
+ }
1059
+ async listFraudReviewQueue(options = {}) {
1060
+ let rawLimit;
1061
+ if (options.limit !== undefined) {
1062
+ if (!Number.isFinite(options.limit)) {
1063
+ throw new Error("fraud review queue limit must be a finite number");
1064
+ }
1065
+ rawLimit = Math.max(1, Math.min(Math.floor(options.limit), 500));
1066
+ }
1067
+ const url = `${this.base}signal/v1/review-queue${this.query({
1068
+ state: options.state,
1069
+ fraud_severity: this.normalizedSeverity(options.severity),
1070
+ limit: rawLimit,
1071
+ score_version: options.scoreVersion,
1072
+ })}`;
1073
+ const res = await this.fetchGetWithRetries(url);
1074
+ const text = await res.text();
1075
+ if (!res.ok) {
1076
+ throw new SignalHttpError(`Fraud review queue HTTP ${res.status}: ${text}`, {
1077
+ statusCode: res.status,
1078
+ url,
1079
+ bodyText: text,
1080
+ });
1081
+ }
1082
+ const body = assertJSONObject(JSON.parse(text));
1083
+ this.assertTenant(body, url);
1084
+ return body;
1085
+ }
1086
+ async getFraudMetrics(options = {}) {
1087
+ const url = `${this.base}signal/v1/fraud/metrics${this.query({
1088
+ window: this.normalizedWindow(options.window),
1089
+ score_version: options.scoreVersion,
1090
+ })}`;
1091
+ const res = await this.fetchGetWithRetries(url);
1092
+ const text = await res.text();
1093
+ if (!res.ok) {
1094
+ throw new SignalHttpError(`Fraud metrics HTTP ${res.status}: ${text}`, {
1095
+ statusCode: res.status,
1096
+ url,
1097
+ bodyText: text,
1098
+ });
1099
+ }
1100
+ const body = assertJSONObject(JSON.parse(text));
1101
+ this.assertTenant(body, url);
1102
+ return body;
1103
+ }
1104
+ async getFraudReleaseGateConfig(scoreVersion) {
1105
+ const url = `${this.base}signal/v1/fraud/release-gate${this.query({
1106
+ score_version: scoreVersion,
1107
+ })}`;
1108
+ const res = await this.fetchGetWithRetries(url);
1109
+ const text = await res.text();
1110
+ if (!res.ok) {
1111
+ throw new SignalHttpError(`Fraud release gate HTTP ${res.status}: ${text}`, {
1112
+ statusCode: res.status,
1113
+ url,
1114
+ bodyText: text,
1115
+ });
1116
+ }
1117
+ const body = assertJSONObject(JSON.parse(text));
1118
+ this.assertTenant(body, url);
1119
+ return body;
1120
+ }
1121
+ async setFraudReleaseGateMode(mode) {
1122
+ const normalized = this.normalizedReleaseGateMode(mode);
1123
+ const url = `${this.base}signal/v1/fraud/release-gate`;
1124
+ const res = await this.fetchPutJSON(url, { mode: normalized });
1125
+ const text = await res.text();
1126
+ if (!res.ok) {
1127
+ throw new SignalHttpError(`Fraud release gate update HTTP ${res.status}: ${text}`, {
1128
+ statusCode: res.status,
1129
+ url,
1130
+ bodyText: text,
1131
+ });
1132
+ }
1133
+ const body = assertJSONObject(JSON.parse(text));
1134
+ this.assertTenant(body, url);
1135
+ return body;
1136
+ }
1137
+ async recordFraudReviewEvent(operatorDid, event, scoreVersion) {
1138
+ let eventType = this.normalizedReviewEventType(event.eventType);
1139
+ let reviewOutcome = event.reviewOutcome ?? event.review_outcome;
1140
+ if (FRAUD_REVIEW_OUTCOMES.has(eventType)) {
1141
+ reviewOutcome = eventType;
1142
+ eventType = "review_outcome_recorded";
1143
+ }
1144
+ const normalizedOutcome = reviewOutcome === undefined ? undefined : this.normalizedReviewOutcome(reviewOutcome);
1145
+ if (eventType === "review_outcome_recorded" && normalizedOutcome === undefined) {
1146
+ throw new Error("fraud review outcome must be one of confirmed_risk, false_positive, or needs_more_evidence");
1147
+ }
1148
+ const signalCode = this.optionalReviewContext(event.signalCode ?? event.signal_code);
1149
+ const intentId = this.optionalReviewContext(event.intentId ?? event.intent_id);
1150
+ const providerEventId = this.optionalReviewContext(event.providerEventId ?? event.provider_event_id);
1151
+ const enc = encodeURIComponent(operatorDid);
1152
+ const url = `${this.base}signal/v1/operators/${enc}/review-events${this.query({
1153
+ score_version: scoreVersion,
1154
+ })}`;
1155
+ const res = await this.fetchPostJSON(url, {
1156
+ event_type: eventType,
1157
+ ...(normalizedOutcome ? { review_outcome: normalizedOutcome } : {}),
1158
+ ...(signalCode ? { signal_code: signalCode } : {}),
1159
+ ...(intentId ? { intent_id: intentId } : {}),
1160
+ ...(providerEventId ? { provider_event_id: providerEventId } : {}),
1161
+ summary: event.summary,
1162
+ });
1163
+ const text = await res.text();
1164
+ if (!res.ok && res.status !== 429) {
1165
+ throw new SignalHttpError(`Fraud review event HTTP ${res.status}: ${text}`, {
1166
+ statusCode: res.status,
1167
+ url,
1168
+ bodyText: text,
1169
+ });
1170
+ }
1171
+ const body = assertJSONObject(JSON.parse(text));
1172
+ this.assertTenant(body, url);
1173
+ this.assertOperator(body, operatorDid, "review event");
1174
+ return body;
1175
+ }
1176
+ }
887
1177
  /**
888
1178
  * Public or optionally authenticated reader for the gateway's A2A discovery surface.
889
1179
  */
@@ -1218,6 +1508,25 @@ export class ServiceAccountSignalSession {
1218
1508
  await Promise.resolve();
1219
1509
  }
1220
1510
  }
1511
+ /**
1512
+ * Tenant-bound fraud review and metrics session for one service-account API key.
1513
+ */
1514
+ export class ServiceAccountFraudSession {
1515
+ fraud;
1516
+ constructor(fraud) {
1517
+ this.fraud = fraud;
1518
+ }
1519
+ static async open(init) {
1520
+ const tenantId = await resolveGatewayTenantId(init.gatewayBaseUrl, init.apiKey, init.principalPath ?? DEFAULT_PRINCIPAL_PATH, Math.max(1, init.maxRetries ?? 3));
1521
+ return new ServiceAccountFraudSession(new GatewayFraudClient(init.gatewayBaseUrl, tenantId, {
1522
+ staticGatewayBearerToken: init.apiKey,
1523
+ maxRetries: init.maxRetries ?? 3,
1524
+ }));
1525
+ }
1526
+ async aclose() {
1527
+ await Promise.resolve();
1528
+ }
1529
+ }
1221
1530
  /**
1222
1531
  * Ergonomic intent helpers: principal-signed intent create, x402 funding, and payee-signed evidence.
1223
1532
  */
@@ -1228,7 +1537,7 @@ export class PaybondIntents {
1228
1537
  }
1229
1538
  /**
1230
1539
  * Build a principal-signed `POST /intents` body and submit it. `principalSigningSeed` must be
1231
- * 32 bytes. `settlementRail` only requests an allowed rail; destinations stay server-owned.
1540
+ * 32 bytes. `settlementRail` is signed as the requested rail; destinations stay server-owned.
1232
1541
  */
1233
1542
  async create(params) {
1234
1543
  const { idempotencyKey, intentId: maybeIntentId, ...fields } = params;
@@ -1267,14 +1576,16 @@ export class PaybondIntents {
1267
1576
  export class Paybond {
1268
1577
  harbor;
1269
1578
  signal;
1579
+ fraud;
1270
1580
  a2a;
1271
1581
  protocol;
1272
1582
  intents;
1273
1583
  session;
1274
- constructor(session, signal, a2a, protocol) {
1584
+ constructor(session, signal, fraud, a2a, protocol) {
1275
1585
  this.session = session;
1276
1586
  this.harbor = session.harbor;
1277
1587
  this.signal = signal;
1588
+ this.fraud = fraud;
1278
1589
  this.a2a = a2a;
1279
1590
  this.protocol = protocol;
1280
1591
  this.intents = new PaybondIntents(session.harbor);
@@ -1286,6 +1597,10 @@ export class Paybond {
1286
1597
  staticGatewayBearerToken: init.apiKey,
1287
1598
  maxRetries: init.maxRetries ?? 3,
1288
1599
  });
1600
+ const fraud = new GatewayFraudClient(init.gatewayBaseUrl, session.harbor.tenantId, {
1601
+ staticGatewayBearerToken: init.apiKey,
1602
+ maxRetries: init.maxRetries ?? 3,
1603
+ });
1289
1604
  const a2a = new GatewayA2AClient(init.gatewayBaseUrl, {
1290
1605
  staticGatewayBearerToken: init.apiKey,
1291
1606
  maxRetries: init.maxRetries ?? 3,
@@ -1294,7 +1609,7 @@ export class Paybond {
1294
1609
  staticGatewayBearerToken: init.apiKey,
1295
1610
  maxRetries: init.maxRetries ?? 3,
1296
1611
  });
1297
- return new Paybond(session, signal, a2a, protocol);
1612
+ return new Paybond(session, signal, fraud, a2a, protocol);
1298
1613
  }
1299
1614
  async rotateHarborToken() {
1300
1615
  await this.session.rotateHarborToken();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { GatewayAuthError, GatewaySignalClient, HarborHttpError, ServiceAccountHarborSession, SignalHttpError, } from "./index.js";
2
+ import { GatewayAuthError, GatewayFraudClient, GatewaySignalClient, HarborHttpError, ServiceAccountHarborSession, SignalHttpError, } from "./index.js";
3
3
  const SERVER_NAME = "Paybond MCP";
4
- const SERVER_VERSION = "0.4.0";
4
+ const SERVER_VERSION = "0.6.0";
5
5
  const MCP_PROTOCOL_VERSION = "2025-11-25";
6
6
  const DEFAULT_HARBOR_ACCESS_PATH = "/v1/auth/harbor-access";
7
7
  const DEFAULT_PRINCIPAL_PATH = "/v1/auth/principal";
@@ -114,6 +114,7 @@ class PaybondMCPRuntime {
114
114
  gateway;
115
115
  principalValue = null;
116
116
  signalValue = null;
117
+ fraudValue = null;
117
118
  harborValue = null;
118
119
  constructor(settings) {
119
120
  this.settings = {
@@ -150,6 +151,13 @@ class PaybondMCPRuntime {
150
151
  }))();
151
152
  return this.signalValue;
152
153
  }
154
+ async fraud() {
155
+ this.fraudValue ??= (async () => new GatewayFraudClient(this.settings.gatewayBaseUrl, await this.tenantId(), {
156
+ staticGatewayBearerToken: this.settings.apiKey,
157
+ maxRetries: this.settings.maxRetries,
158
+ }))();
159
+ return this.fraudValue;
160
+ }
153
161
  async harbor() {
154
162
  if (!this.settings.harborBaseUrl) {
155
163
  throw new Error("PAYBOND_HARBOR_URL is required for direct Harbor mutation tools");
@@ -529,6 +537,27 @@ export class PaybondMCPServer {
529
537
  }),
530
538
  call: async (args) => (await this.runtime.signal()).getSignedPortfolioArtifact(optionalString(args.score_version)),
531
539
  },
540
+ {
541
+ name: "paybond_get_fraud_assessment",
542
+ description: "Fetch the read-only fraud assessment for one tenant-scoped operator DID.",
543
+ inputSchema: objectSchema({
544
+ operator_did: { type: "string" },
545
+ score_version: { type: "string" },
546
+ }, ["operator_did"]),
547
+ call: async (args) => (await this.runtime.fraud()).getFraudAssessment(stringArg(args.operator_did, "operator_did"), optionalString(args.score_version)),
548
+ },
549
+ {
550
+ name: "paybond_get_fraud_metrics",
551
+ description: "Fetch tenant-scoped read-only fraud backtesting and monitoring metrics for a supported active window.",
552
+ inputSchema: objectSchema({
553
+ window: { type: "string", enum: ["24h", "7d", "30d"] },
554
+ score_version: { type: "string" },
555
+ }),
556
+ call: async (args) => (await this.runtime.fraud()).getFraudMetrics({
557
+ window: optionalString(args.window),
558
+ scoreVersion: optionalString(args.score_version),
559
+ }),
560
+ },
532
561
  {
533
562
  name: "paybond_get_a2a_agent_card",
534
563
  description: "Fetch the published Paybond A2A discovery card for protocol-trust delegation.",
@@ -17,6 +17,7 @@ export declare function intentCreationSignBytesRaw(input: {
17
17
  predicate: Record<string, unknown>;
18
18
  predicateRef: string;
19
19
  allowedTools: string[];
20
+ settlementRail: SettlementRail;
20
21
  }): Uint8Array;
21
22
  export type BuildSignedCreateIntentParams = {
22
23
  tenantId: string;
@@ -32,11 +33,11 @@ export type BuildSignedCreateIntentParams = {
32
33
  deadlineRfc3339: string;
33
34
  allowedTools: string[];
34
35
  predicateRef?: string;
35
- /** Optional rail request for the new intent. Harbor resolves the destination server-side. */
36
- settlementRail?: SettlementRail;
36
+ /** Rail request for the new intent. Harbor resolves the destination server-side. */
37
+ settlementRail: SettlementRail;
37
38
  };
38
39
  /**
39
40
  * Build a Harbor `POST /intents` JSON body with principal Ed25519 detached signature.
40
- * `settlementRail` only requests an allowed rail; destinations remain tenant-owned server-side.
41
+ * `settlementRail` is included in the signature; destinations remain tenant-owned server-side.
41
42
  */
42
43
  export declare function buildSignedCreateIntentBody(params: BuildSignedCreateIntentParams): Record<string, unknown>;
@@ -49,15 +49,15 @@ function allowedToolsDigest(tools) {
49
49
  .filter((v, i, a) => a.indexOf(v) === i);
50
50
  return jsonValueDigest(sorted);
51
51
  }
52
- /** Bincode payload for principal intent creation (wire format revision byte `2`). */
52
+ /** Bincode payload for principal intent creation (wire format revision byte `4`). */
53
53
  function encodeIntentCreationSign(input) {
54
- const version = new Uint8Array([2]);
54
+ const version = new Uint8Array([4]);
55
55
  const intentBytes = parseUuid(input.intentId);
56
56
  if (intentBytes.length !== 16) {
57
57
  throw new Error("intentId must be a UUID string");
58
58
  }
59
59
  // Serde `Uuid` + bincode uses a u64 length prefix (16) before the raw 16 bytes (matches Rust).
60
- return concatBytes(version, encodeBincodeString(input.tenantId), encodeU64(16), intentBytes, encodeBincodeString(input.principalDid), encodeBincodeString(input.payeeDid), encodeI64(input.amountCents), encodeBincodeString(input.currency), encodeBincodeString(input.deadlineRfc3339), input.budgetDigest, input.evidenceSchemaDigest, input.predicateDslDigest, encodeBincodeString(input.predicateRef), input.allowedToolsDigest);
60
+ return concatBytes(version, encodeBincodeString(input.tenantId), encodeU64(16), intentBytes, encodeBincodeString(input.principalDid), encodeBincodeString(input.payeeDid), encodeI64(input.amountCents), encodeBincodeString(input.currency), encodeBincodeString(input.deadlineRfc3339), input.budgetDigest, input.evidenceSchemaDigest, input.predicateDslDigest, encodeBincodeString(input.predicateRef), input.allowedToolsDigest, encodeBincodeString(input.settlementRail));
61
61
  }
62
62
  export function intentCreationSignBytesRaw(input) {
63
63
  const budgetDigest = jsonValueDigest(input.budget);
@@ -77,6 +77,7 @@ export function intentCreationSignBytesRaw(input) {
77
77
  predicateDslDigest,
78
78
  predicateRef: input.predicateRef,
79
79
  allowedToolsDigest: allowedDigest,
80
+ settlementRail: input.settlementRail,
80
81
  });
81
82
  }
82
83
  function bytesToBase64(bytes) {
@@ -87,7 +88,7 @@ function bytesToBase64(bytes) {
87
88
  }
88
89
  /**
89
90
  * Build a Harbor `POST /intents` JSON body with principal Ed25519 detached signature.
90
- * `settlementRail` only requests an allowed rail; destinations remain tenant-owned server-side.
91
+ * `settlementRail` is included in the signature; destinations remain tenant-owned server-side.
91
92
  */
92
93
  export function buildSignedCreateIntentBody(params) {
93
94
  if (params.principalSigningSeed.length !== 32) {
@@ -96,9 +97,7 @@ export function buildSignedCreateIntentBody(params) {
96
97
  if (params.allowedTools.length === 0) {
97
98
  throw new Error("allowedTools must be non-empty");
98
99
  }
99
- const settlementRail = params.settlementRail
100
- ? validateSettlementRail(params.settlementRail)
101
- : undefined;
100
+ const settlementRail = validateSettlementRail(params.settlementRail);
102
101
  const predicateRef = params.predicateRef ?? "";
103
102
  const msg = intentCreationSignBytesRaw({
104
103
  tenantId: params.tenantId,
@@ -113,6 +112,7 @@ export function buildSignedCreateIntentBody(params) {
113
112
  predicate: params.predicate,
114
113
  predicateRef,
115
114
  allowedTools: params.allowedTools,
115
+ settlementRail,
116
116
  });
117
117
  const sig = sign(msg, params.principalSigningSeed);
118
118
  const pub = getPublicKey(params.principalSigningSeed);
@@ -128,15 +128,13 @@ export function buildSignedCreateIntentBody(params) {
128
128
  evidence_schema: params.evidenceSchema,
129
129
  deadline: params.deadlineRfc3339,
130
130
  predicate_dsl: params.predicate,
131
- signing_version: 2,
131
+ settlement_rail: settlementRail,
132
+ signing_version: 4,
132
133
  policy_binding: null,
133
134
  allowed_tools: params.allowedTools,
134
135
  };
135
136
  if (predicateRef.trim() !== "") {
136
137
  body.predicate_ref = predicateRef;
137
138
  }
138
- if (settlementRail) {
139
- body.settlement_rail = settlementRail;
140
- }
141
139
  return body;
142
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paybond/kit",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Paybond Kit for TypeScript: tenant-bound Harbor sessions, capability verification, and signed intent/evidence flows.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -38,11 +38,20 @@
38
38
  "keywords": [
39
39
  "paybond",
40
40
  "harbor",
41
+ "kit",
41
42
  "agents",
42
- "typescript",
43
+ "agent-runtime",
44
+ "agent-payments",
43
45
  "escrow",
46
+ "intent-escrow",
44
47
  "capabilities",
45
- "mcp"
48
+ "capability-verification",
49
+ "tenant-bound",
50
+ "x402",
51
+ "usdc",
52
+ "mcp",
53
+ "reputation",
54
+ "typescript"
46
55
  ],
47
56
  "publishConfig": {
48
57
  "access": "public",