@paybond/kit 0.1.1 → 0.2.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,8 +1,6 @@
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, and tenant-scoped ledger provenance reads.
4
-
5
- It does **not** currently expose a first-class Signal client or Signal analytics/reputation API surface. Signal remains a separate platform surface today.
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, tenant-scoped ledger provenance reads, and tenant-scoped Signal analytics and reputation reads.
6
4
 
7
5
  Install the public package with:
8
6
 
@@ -22,7 +20,7 @@ npm install @paybond/kit
22
20
 
23
21
  ## Tenant isolation
24
22
 
25
- Every session is bound to the tenant realm echoed by the gateway `POST /v1/auth/harbor-access` exchange.
23
+ Every session is bound to the tenant realm echoed by gateway-authenticated service-account introspection and Harbor access exchange flows.
26
24
 
27
25
  - Do not pass tenant ids by hand for normal SDK usage.
28
26
  - Construct one `Paybond` session per tenant/service account.
@@ -59,6 +57,8 @@ try {
59
57
 
60
58
  - `Paybond.open(...)` for gateway-authenticated, tenant-derived Harbor sessions
61
59
  - `HarborClient` for capability verification, intent creation, evidence submission, and ledger reads
60
+ - `GatewaySignalClient` and `ServiceAccountSignalSession` for tenant-scoped Signal reads
61
+ - `paybond.signal` on `Paybond` sessions opened from one service-account API key
62
62
  - `PaybondIntents` helpers for principal-signed intent creation and payee-signed evidence submission
63
63
  - Low-level signing helpers exported for advanced callers
64
64
 
@@ -66,8 +66,6 @@ try {
66
66
 
67
67
  ## What it does not include
68
68
 
69
- - No first-class `SignalClient`
70
- - No Signal reputation or analytics fetch API
71
69
  - No operator-tier settlement or console workflows
72
70
 
73
71
  ## Docs
package/dist/index.d.ts CHANGED
@@ -44,6 +44,19 @@ export declare class GatewayAuthError extends Error {
44
44
  bodyText?: string;
45
45
  });
46
46
  }
47
+ /**
48
+ * Structured HTTP failure from gateway Signal read routes.
49
+ */
50
+ export declare class SignalHttpError extends Error {
51
+ readonly statusCode: number;
52
+ readonly url: string;
53
+ readonly bodyText: string;
54
+ constructor(message: string, init: {
55
+ statusCode: number;
56
+ url: string;
57
+ bodyText: string;
58
+ });
59
+ }
47
60
  /**
48
61
  * Exchanges a `paybond_sk_` API key for short-lived Harbor JWTs and caches tenant realm from the
49
62
  * gateway response (no separate tenant env var for the default path).
@@ -191,6 +204,42 @@ export declare class HarborClient {
191
204
  */
192
205
  getLedgerMerkleLatest(): Promise<Record<string, unknown>>;
193
206
  }
207
+ type GatewaySignalClientOptions = {
208
+ staticGatewayBearerToken: string;
209
+ maxRetries?: number;
210
+ };
211
+ /**
212
+ * Tenant-bound reader for gateway Signal routes.
213
+ */
214
+ export declare class GatewaySignalClient {
215
+ private readonly base;
216
+ readonly tenantId: string;
217
+ private readonly bearerToken;
218
+ private readonly maxRetries;
219
+ constructor(gatewayBaseUrl: string, tenantId: string, options: GatewaySignalClientOptions);
220
+ private fetchGetWithRetries;
221
+ private assertTenant;
222
+ private scoreQuery;
223
+ getReputationReceipt(operatorDid: string, scoreVersion?: string): Promise<Record<string, unknown> | null>;
224
+ getPortfolioSummary(scoreVersion?: string): Promise<Record<string, unknown>>;
225
+ getOperatorExplanation(operatorDid: string, scoreVersion?: string): Promise<Record<string, unknown> | null>;
226
+ getOperatorReviewStatus(operatorDid: string, scoreVersion?: string): Promise<Record<string, unknown> | null>;
227
+ }
228
+ export type ServiceAccountSignalSessionInit = {
229
+ gatewayBaseUrl: string;
230
+ apiKey: string;
231
+ principalPath?: string;
232
+ maxRetries?: number;
233
+ };
234
+ /**
235
+ * Read-only tenant-bound Signal session for one service-account API key.
236
+ */
237
+ export declare class ServiceAccountSignalSession {
238
+ readonly signal: GatewaySignalClient;
239
+ private constructor();
240
+ static open(init: ServiceAccountSignalSessionInit): Promise<ServiceAccountSignalSession>;
241
+ aclose(): Promise<void>;
242
+ }
194
243
  /** Parameters for {@link PaybondIntents.create} (tenant is taken from the bound Harbor client). */
195
244
  export type PaybondCreateIntentParams = Omit<BuildSignedCreateIntentParams, "tenantId" | "intentId"> & {
196
245
  intentId?: string;
@@ -221,6 +270,7 @@ export declare class PaybondIntents {
221
270
  */
222
271
  export declare class Paybond {
223
272
  readonly harbor: HarborClient;
273
+ readonly signal: GatewaySignalClient;
224
274
  readonly intents: PaybondIntents;
225
275
  private readonly session;
226
276
  private constructor();
package/dist/index.js CHANGED
@@ -32,6 +32,21 @@ export class GatewayAuthError extends Error {
32
32
  this.bodyText = init?.bodyText;
33
33
  }
34
34
  }
35
+ /**
36
+ * Structured HTTP failure from gateway Signal read routes.
37
+ */
38
+ export class SignalHttpError extends Error {
39
+ statusCode;
40
+ url;
41
+ bodyText;
42
+ constructor(message, init) {
43
+ super(message);
44
+ this.name = "SignalHttpError";
45
+ this.statusCode = init.statusCode;
46
+ this.url = init.url;
47
+ this.bodyText = init.bodyText;
48
+ }
49
+ }
35
50
  function normalizeBase(url) {
36
51
  return url.trim().replace(/\/+$/, "");
37
52
  }
@@ -507,6 +522,226 @@ export class HarborClient {
507
522
  return body;
508
523
  }
509
524
  }
525
+ const DEFAULT_PRINCIPAL_PATH = "/v1/auth/principal";
526
+ function assertJSONObject(value) {
527
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
528
+ throw new Error("expected JSON object");
529
+ }
530
+ return value;
531
+ }
532
+ /**
533
+ * Tenant-bound reader for gateway Signal routes.
534
+ */
535
+ export class GatewaySignalClient {
536
+ base;
537
+ tenantId;
538
+ bearerToken;
539
+ maxRetries;
540
+ constructor(gatewayBaseUrl, tenantId, options) {
541
+ this.base = normalizeBase(gatewayBaseUrl) + "/";
542
+ this.tenantId = tenantId.trim();
543
+ this.bearerToken = options.staticGatewayBearerToken.trim();
544
+ this.maxRetries = Math.max(1, options.maxRetries ?? 3);
545
+ }
546
+ async fetchGetWithRetries(url) {
547
+ let lastErr;
548
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
549
+ let res;
550
+ try {
551
+ res = await fetch(url, {
552
+ method: "GET",
553
+ headers: {
554
+ accept: "application/json",
555
+ authorization: `Bearer ${this.bearerToken}`,
556
+ },
557
+ });
558
+ }
559
+ catch (e) {
560
+ lastErr = e;
561
+ if (attempt + 1 >= this.maxRetries)
562
+ throw e;
563
+ await new Promise((r) => setTimeout(r, backoffMs(attempt)));
564
+ continue;
565
+ }
566
+ if ([429, 500, 502, 503, 504].includes(res.status)) {
567
+ if (attempt + 1 >= this.maxRetries) {
568
+ return res;
569
+ }
570
+ const raSec = parseRetryAfterSeconds(res.headers.get("retry-after"));
571
+ const delayMs = raSec != null ? raSec * 1000 : backoffMs(attempt);
572
+ await new Promise((r) => setTimeout(r, delayMs));
573
+ continue;
574
+ }
575
+ return res;
576
+ }
577
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
578
+ }
579
+ assertTenant(body, url) {
580
+ const tid = String(body.tenant_id ?? "");
581
+ if (tid !== this.tenantId) {
582
+ throw new Error(`signal tenant mismatch: client=${this.tenantId} gateway=${tid} url=${url}`);
583
+ }
584
+ }
585
+ scoreQuery(scoreVersion) {
586
+ if (!scoreVersion || !scoreVersion.trim()) {
587
+ return "";
588
+ }
589
+ return `?score_version=${encodeURIComponent(scoreVersion.trim())}`;
590
+ }
591
+ async getReputationReceipt(operatorDid, scoreVersion) {
592
+ const enc = encodeURIComponent(operatorDid);
593
+ const url = `${this.base}reputation/${enc}${this.scoreQuery(scoreVersion)}`;
594
+ const res = await this.fetchGetWithRetries(url);
595
+ const text = await res.text();
596
+ if (res.status === 404) {
597
+ return null;
598
+ }
599
+ if (!res.ok) {
600
+ throw new SignalHttpError(`Signal receipt HTTP ${res.status}: ${text}`, {
601
+ statusCode: res.status,
602
+ url,
603
+ bodyText: text,
604
+ });
605
+ }
606
+ const body = assertJSONObject(JSON.parse(text));
607
+ const receipt = assertJSONObject(body.receipt);
608
+ const tenant = String(receipt.tenant_id ?? "");
609
+ const echoedOperator = String(receipt.operator_did ?? "");
610
+ if (tenant !== this.tenantId) {
611
+ throw new Error(`signal receipt tenant mismatch: client=${this.tenantId} gateway=${tenant}`);
612
+ }
613
+ if (echoedOperator !== operatorDid) {
614
+ throw new Error(`signal receipt operator mismatch: requested=${operatorDid} gateway=${echoedOperator}`);
615
+ }
616
+ return body;
617
+ }
618
+ async getPortfolioSummary(scoreVersion) {
619
+ const url = `${this.base}signal/v1/portfolio/summary${this.scoreQuery(scoreVersion)}`;
620
+ const res = await this.fetchGetWithRetries(url);
621
+ const text = await res.text();
622
+ if (!res.ok) {
623
+ throw new SignalHttpError(`Signal portfolio summary HTTP ${res.status}: ${text}`, {
624
+ statusCode: res.status,
625
+ url,
626
+ bodyText: text,
627
+ });
628
+ }
629
+ const body = assertJSONObject(JSON.parse(text));
630
+ this.assertTenant(body, url);
631
+ return body;
632
+ }
633
+ async getOperatorExplanation(operatorDid, scoreVersion) {
634
+ const enc = encodeURIComponent(operatorDid);
635
+ const url = `${this.base}signal/v1/operators/${enc}/explanation${this.scoreQuery(scoreVersion)}`;
636
+ const res = await this.fetchGetWithRetries(url);
637
+ const text = await res.text();
638
+ if (res.status === 404) {
639
+ return null;
640
+ }
641
+ if (!res.ok) {
642
+ throw new SignalHttpError(`Signal explanation HTTP ${res.status}: ${text}`, {
643
+ statusCode: res.status,
644
+ url,
645
+ bodyText: text,
646
+ });
647
+ }
648
+ const body = assertJSONObject(JSON.parse(text));
649
+ this.assertTenant(body, url);
650
+ if (String(body.operator_did ?? "") !== operatorDid) {
651
+ throw new Error(`signal explanation operator mismatch: requested=${operatorDid} gateway=${String(body.operator_did ?? "")}`);
652
+ }
653
+ return body;
654
+ }
655
+ async getOperatorReviewStatus(operatorDid, scoreVersion) {
656
+ const enc = encodeURIComponent(operatorDid);
657
+ const url = `${this.base}signal/v1/operators/${enc}/review-status${this.scoreQuery(scoreVersion)}`;
658
+ const res = await this.fetchGetWithRetries(url);
659
+ const text = await res.text();
660
+ if (res.status === 404) {
661
+ return null;
662
+ }
663
+ if (!res.ok) {
664
+ throw new SignalHttpError(`Signal review status HTTP ${res.status}: ${text}`, {
665
+ statusCode: res.status,
666
+ url,
667
+ bodyText: text,
668
+ });
669
+ }
670
+ const body = assertJSONObject(JSON.parse(text));
671
+ this.assertTenant(body, url);
672
+ if (String(body.operator_did ?? "") !== operatorDid) {
673
+ throw new Error(`signal review operator mismatch: requested=${operatorDid} gateway=${String(body.operator_did ?? "")}`);
674
+ }
675
+ return body;
676
+ }
677
+ }
678
+ async function resolveGatewayTenantId(gatewayBaseUrl, apiKey, principalPath, maxRetries) {
679
+ const base = normalizeBase(gatewayBaseUrl);
680
+ const path = principalPath.startsWith("/") ? principalPath : `/${principalPath}`;
681
+ const url = `${base}${path}`;
682
+ let lastErr;
683
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
684
+ let res;
685
+ try {
686
+ res = await fetch(url, {
687
+ method: "GET",
688
+ headers: {
689
+ accept: "application/json",
690
+ authorization: `Bearer ${apiKey.trim()}`,
691
+ },
692
+ });
693
+ }
694
+ catch (e) {
695
+ lastErr = e;
696
+ if (attempt + 1 >= maxRetries) {
697
+ throw e;
698
+ }
699
+ await new Promise((r) => setTimeout(r, backoffMs(attempt)));
700
+ continue;
701
+ }
702
+ const text = await res.text();
703
+ if (!res.ok) {
704
+ if ([429, 500, 502, 503, 504].includes(res.status) && attempt + 1 < maxRetries) {
705
+ const raSec = parseRetryAfterSeconds(res.headers.get("retry-after"));
706
+ const delayMs = raSec != null ? raSec * 1000 : backoffMs(attempt);
707
+ await new Promise((r) => setTimeout(r, delayMs));
708
+ continue;
709
+ }
710
+ throw new GatewayAuthError(`gateway principal HTTP ${res.status}`, {
711
+ statusCode: res.status,
712
+ bodyText: text,
713
+ });
714
+ }
715
+ const body = assertJSONObject(JSON.parse(text));
716
+ const tenant = String(body.tenant_id ?? "").trim();
717
+ if (!tenant) {
718
+ throw new GatewayAuthError("gateway principal JSON missing tenant_id", {
719
+ bodyText: text,
720
+ });
721
+ }
722
+ return tenant;
723
+ }
724
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
725
+ }
726
+ /**
727
+ * Read-only tenant-bound Signal session for one service-account API key.
728
+ */
729
+ export class ServiceAccountSignalSession {
730
+ signal;
731
+ constructor(signal) {
732
+ this.signal = signal;
733
+ }
734
+ static async open(init) {
735
+ const tenantId = await resolveGatewayTenantId(init.gatewayBaseUrl, init.apiKey, init.principalPath ?? DEFAULT_PRINCIPAL_PATH, Math.max(1, init.maxRetries ?? 3));
736
+ return new ServiceAccountSignalSession(new GatewaySignalClient(init.gatewayBaseUrl, tenantId, {
737
+ staticGatewayBearerToken: init.apiKey,
738
+ maxRetries: init.maxRetries ?? 3,
739
+ }));
740
+ }
741
+ async aclose() {
742
+ await Promise.resolve();
743
+ }
744
+ }
510
745
  /**
511
746
  * Ergonomic intent helpers: principal-signed intent create and payee-signed evidence.
512
747
  */
@@ -545,17 +780,23 @@ export class PaybondIntents {
545
780
  */
546
781
  export class Paybond {
547
782
  harbor;
783
+ signal;
548
784
  intents;
549
785
  session;
550
- constructor(session) {
786
+ constructor(session, signal) {
551
787
  this.session = session;
552
788
  this.harbor = session.harbor;
789
+ this.signal = signal;
553
790
  this.intents = new PaybondIntents(session.harbor);
554
791
  }
555
792
  /** Open a tenant-bound session via gateway `harbor-access` exchange. */
556
793
  static async open(init) {
557
794
  const session = await ServiceAccountHarborSession.open(init);
558
- return new Paybond(session);
795
+ const signal = new GatewaySignalClient(init.gatewayBaseUrl, session.harbor.tenantId, {
796
+ staticGatewayBearerToken: init.apiKey,
797
+ maxRetries: init.maxRetries ?? 3,
798
+ });
799
+ return new Paybond(session, signal);
559
800
  }
560
801
  async rotateHarborToken() {
561
802
  await this.session.rotateHarborToken();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paybond/kit",
3
- "version": "0.1.1",
3
+ "version": "0.2.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",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/nonameuserd/paybond-kit.git"
25
+ "url": "git+https://github.com/nonameuserd/paybond-kit.git"
26
26
  },
27
27
  "homepage": "https://github.com/nonameuserd/paybond-kit",
28
28
  "bugs": {