@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 +4 -6
- package/dist/index.d.ts +50 -0
- package/dist/index.js +243 -2
- package/package.json +2 -2
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|