@provenonce/beats-client 0.2.0 → 0.3.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 +50 -50
- package/index.d.ts +148 -96
- package/index.mjs +476 -178
- package/package.json +20 -20
package/README.md
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
# @provenonce/beats-client
|
|
2
|
-
|
|
3
|
-
Minimal client for the public Beats service.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
From npm (after publish):
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm i @provenonce/beats-client
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Local repo path (pre-publish):
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm i ./sdk/beats-client
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
```js
|
|
22
|
-
import { createBeatsClient } from '@provenonce/beats-client';
|
|
23
|
-
|
|
24
|
-
const beats = createBeatsClient();
|
|
25
|
-
|
|
26
|
-
const anchor = await beats.getAnchor();
|
|
27
|
-
const receipt = await beats.timestampHash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
28
|
-
const receiptValid = await beats.verifyReceipt(receipt);
|
|
29
|
-
const anchorValid = await beats.verifyAnchor(anchor);
|
|
30
|
-
const onChain = await beats.verifyOnChain(receipt.on_chain.tx_signature, { cluster: 'devnet' });
|
|
31
|
-
|
|
32
|
-
// Auto-verify anchor receipt:
|
|
33
|
-
const verifiedAnchor = await beats.getAnchor({ verify: true });
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Endpoints wrapped
|
|
37
|
-
|
|
38
|
-
- `GET /api/health`
|
|
39
|
-
- `GET /api/v1/beat/anchor`
|
|
40
|
-
- `GET /api/v1/beat/key`
|
|
41
|
-
- `POST /api/v1/beat/verify`
|
|
42
|
-
- `POST /api/v1/beat/timestamp`
|
|
43
|
-
|
|
44
|
-
## Helpers
|
|
45
|
-
|
|
46
|
-
- `verifyReceipt(response)` - offline Ed25519 verification of timestamp/anchor receipts.
|
|
47
|
-
- `verifyAnchor(anchorResponse)` - explicit offline verification helper for anchor responses.
|
|
48
|
-
- `verifyOnChain(txSignature, { cluster | rpcUrl })` - direct Solana RPC status check.
|
|
49
|
-
- `getAnchor({ verify: true })` - fetch anchor and verify attached receipt in one call.
|
|
50
|
-
|
|
1
|
+
# @provenonce/beats-client
|
|
2
|
+
|
|
3
|
+
Minimal client for the public Beats service.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From npm (after publish):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @provenonce/beats-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Local repo path (pre-publish):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i ./sdk/beats-client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { createBeatsClient } from '@provenonce/beats-client';
|
|
23
|
+
|
|
24
|
+
const beats = createBeatsClient();
|
|
25
|
+
|
|
26
|
+
const anchor = await beats.getAnchor();
|
|
27
|
+
const receipt = await beats.timestampHash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
28
|
+
const receiptValid = await beats.verifyReceipt(receipt);
|
|
29
|
+
const anchorValid = await beats.verifyAnchor(anchor);
|
|
30
|
+
const onChain = await beats.verifyOnChain(receipt.on_chain.tx_signature, { cluster: 'devnet' });
|
|
31
|
+
|
|
32
|
+
// Auto-verify anchor receipt:
|
|
33
|
+
const verifiedAnchor = await beats.getAnchor({ verify: true });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Endpoints wrapped
|
|
37
|
+
|
|
38
|
+
- `GET /api/health`
|
|
39
|
+
- `GET /api/v1/beat/anchor`
|
|
40
|
+
- `GET /api/v1/beat/key`
|
|
41
|
+
- `POST /api/v1/beat/verify`
|
|
42
|
+
- `POST /api/v1/beat/timestamp`
|
|
43
|
+
|
|
44
|
+
## Helpers
|
|
45
|
+
|
|
46
|
+
- `verifyReceipt(response)` - offline Ed25519 verification of timestamp/anchor receipts.
|
|
47
|
+
- `verifyAnchor(anchorResponse)` - explicit offline verification helper for anchor responses.
|
|
48
|
+
- `verifyOnChain(txSignature, { cluster | rpcUrl })` - direct Solana RPC status check.
|
|
49
|
+
- `getAnchor({ verify: true })` - fetch anchor and verify attached receipt in one call.
|
|
50
|
+
|
package/index.d.ts
CHANGED
|
@@ -1,96 +1,148 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
receipt
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
export interface ContinuityState {
|
|
2
|
+
beat_index: number;
|
|
3
|
+
hash: string;
|
|
4
|
+
agent_id: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface BeatsClientOptions {
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
/** Pin a known public key for receipt verification (B-1). Hex or base58. */
|
|
11
|
+
pinnedPublicKey?: string | null;
|
|
12
|
+
/** Request timeout in milliseconds (B-5). Default: 30000. */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
/** Callback when continuity state changes. Persist this for restart recovery. */
|
|
15
|
+
onStateChange?: (state: ContinuityState) => void;
|
|
16
|
+
/** Load persisted continuity state on startup. */
|
|
17
|
+
loadState?: () => ContinuityState | null;
|
|
18
|
+
/** Agent ID for scoping persisted state. */
|
|
19
|
+
agentId?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VerifyOnChainOptions {
|
|
23
|
+
rpcUrl?: string;
|
|
24
|
+
cluster?: 'devnet' | 'testnet' | 'mainnet-beta';
|
|
25
|
+
/** If provided, fetches the full transaction and verifies SPL Memo content matches (B-4). */
|
|
26
|
+
expectedPayload?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VerifyOnChainResult {
|
|
30
|
+
found: boolean;
|
|
31
|
+
confirmationStatus: string | null;
|
|
32
|
+
finalized: boolean;
|
|
33
|
+
slot: number | null;
|
|
34
|
+
/** True if memo content matches expectedPayload (B-4). Only present when expectedPayload is provided. */
|
|
35
|
+
memoVerified?: boolean;
|
|
36
|
+
/** Parsed memo data from the transaction (B-4). */
|
|
37
|
+
memoData?: Record<string, unknown>;
|
|
38
|
+
/** Reason if memoVerified is false. */
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface VerifyReceiptOptions {
|
|
43
|
+
/** Override key for this specific verification. Hex or base58. */
|
|
44
|
+
publicKey?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AnchorOptions {
|
|
48
|
+
/** Verify receipt signature against pinned/cached key (B-1). */
|
|
49
|
+
verify?: boolean;
|
|
50
|
+
/** Recompute anchor hash locally via SHA-256 chain (B-3, Node.js only). */
|
|
51
|
+
recompute?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface HealthResponse {
|
|
55
|
+
status?: string;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BeatAnchor {
|
|
60
|
+
beat_index: number;
|
|
61
|
+
hash: string;
|
|
62
|
+
prev_hash: string;
|
|
63
|
+
utc: number;
|
|
64
|
+
difficulty: number;
|
|
65
|
+
epoch: number;
|
|
66
|
+
tx_signature: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ReceiptEnvelope {
|
|
70
|
+
signature: string;
|
|
71
|
+
public_key: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AnchorResponse {
|
|
75
|
+
anchor: BeatAnchor | null;
|
|
76
|
+
on_chain: {
|
|
77
|
+
tx_signature: string | null;
|
|
78
|
+
explorer_url: string | null;
|
|
79
|
+
anchored?: boolean;
|
|
80
|
+
};
|
|
81
|
+
receipt?: ReceiptEnvelope;
|
|
82
|
+
anchor_interval_sec?: number;
|
|
83
|
+
next_anchor_at?: string;
|
|
84
|
+
_verified_receipt?: boolean;
|
|
85
|
+
_verified_hash?: boolean;
|
|
86
|
+
_note?: string;
|
|
87
|
+
_info?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface TimestampPayload {
|
|
91
|
+
hash: string;
|
|
92
|
+
anchor_index: number;
|
|
93
|
+
anchor_hash: string;
|
|
94
|
+
utc: number;
|
|
95
|
+
tx_signature: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface TimestampResponse {
|
|
99
|
+
timestamp: TimestampPayload;
|
|
100
|
+
on_chain: {
|
|
101
|
+
tx_signature: string;
|
|
102
|
+
explorer_url: string;
|
|
103
|
+
};
|
|
104
|
+
receipt: ReceiptEnvelope;
|
|
105
|
+
tier?: 'free' | 'pro';
|
|
106
|
+
_note?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface VerifyApiResponse {
|
|
110
|
+
ok?: boolean;
|
|
111
|
+
mode?: 'beat' | 'chain' | 'proof';
|
|
112
|
+
valid?: boolean;
|
|
113
|
+
error?: string;
|
|
114
|
+
reason?: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface BeatsClient {
|
|
119
|
+
getHealth(): Promise<HealthResponse>;
|
|
120
|
+
getAnchor(opts?: AnchorOptions): Promise<AnchorResponse>;
|
|
121
|
+
getKey(): Promise<{ public_key_base58: string; public_key_hex: string; algorithm: string; purpose?: string; _note?: string }>;
|
|
122
|
+
verify(payload: unknown): Promise<VerifyApiResponse>;
|
|
123
|
+
timestampHash(hash: string): Promise<TimestampResponse>;
|
|
124
|
+
|
|
125
|
+
/** Verify receipt signature against pinned/cached/fetched key (B-1). Never uses response-embedded key. */
|
|
126
|
+
verifyReceipt(response: AnchorResponse | TimestampResponse | { payload: Record<string, unknown>; signature: string; public_key: string }, opts?: VerifyReceiptOptions): Promise<boolean>;
|
|
127
|
+
verifyAnchor(anchorResponse: AnchorResponse): Promise<boolean>;
|
|
128
|
+
|
|
129
|
+
/** Recompute anchor hash from fields via SHA-256 chain (B-3, Node.js only). */
|
|
130
|
+
verifyAnchorHash(anchor: BeatAnchor): Promise<boolean>;
|
|
131
|
+
|
|
132
|
+
/** Verify on-chain tx status and optionally SPL Memo content (B-4). */
|
|
133
|
+
verifyOnChain(txSignature: string, opts?: VerifyOnChainOptions): Promise<VerifyOnChainResult>;
|
|
134
|
+
|
|
135
|
+
/** Set last known anchor for continuity tracking (B-2). Throws if chain is broken. */
|
|
136
|
+
setLastKnownAnchor(anchor: BeatAnchor): void;
|
|
137
|
+
/** Get last known anchor (B-2). Returns null if no anchor has been seen. */
|
|
138
|
+
getLastKnownAnchor(): BeatAnchor | null;
|
|
139
|
+
/** Re-establish chain continuity after a break. Requires explicit operator action. */
|
|
140
|
+
resync(anchor: BeatAnchor): void;
|
|
141
|
+
/** Returns true if chain continuity is broken and resync() is required. */
|
|
142
|
+
isBroken(): boolean;
|
|
143
|
+
|
|
144
|
+
/** Internal: resolve public key from cache or auto-fetch. */
|
|
145
|
+
_resolveKey(): Promise<string | null>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export declare function createBeatsClient(options?: BeatsClientOptions): BeatsClient;
|
package/index.mjs
CHANGED
|
@@ -1,178 +1,476 @@
|
|
|
1
|
-
const DEFAULT_BASE_URL = 'https://beats.provenonce.dev';
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
bytes
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://beats.provenonce.dev';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
4
|
+
const BASE58_MAP = Object.fromEntries([...BASE58_ALPHABET].map((ch, i) => [ch, i]));
|
|
5
|
+
const CLUSTER_RPC = {
|
|
6
|
+
devnet: 'https://api.devnet.solana.com',
|
|
7
|
+
testnet: 'https://api.testnet.solana.com',
|
|
8
|
+
'mainnet-beta': 'https://api.mainnet-beta.solana.com',
|
|
9
|
+
};
|
|
10
|
+
const HEX64 = /^[0-9a-f]{64}$/i;
|
|
11
|
+
const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
12
|
+
|
|
13
|
+
// ============ UTILITIES ============
|
|
14
|
+
|
|
15
|
+
function toCanonicalJson(value) {
|
|
16
|
+
const keys = Object.keys(value).sort();
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const key of keys) out[key] = value[key];
|
|
19
|
+
return JSON.stringify(out);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decodeBase58(str) {
|
|
23
|
+
if (!str || typeof str !== 'string') throw new Error('base58 value required');
|
|
24
|
+
let bytes = [0];
|
|
25
|
+
for (let i = 0; i < str.length; i++) {
|
|
26
|
+
const val = BASE58_MAP[str[i]];
|
|
27
|
+
if (val === undefined) throw new Error('invalid base58 character');
|
|
28
|
+
let carry = val;
|
|
29
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
30
|
+
const x = bytes[j] * 58 + carry;
|
|
31
|
+
bytes[j] = x & 0xff;
|
|
32
|
+
carry = x >> 8;
|
|
33
|
+
}
|
|
34
|
+
while (carry > 0) {
|
|
35
|
+
bytes.push(carry & 0xff);
|
|
36
|
+
carry >>= 8;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (let i = 0; i < str.length && str[i] === '1'; i++) bytes.push(0);
|
|
40
|
+
return new Uint8Array(bytes.reverse());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function decodePublicKeyBytes(publicKey) {
|
|
44
|
+
if (typeof publicKey !== 'string' || publicKey.length === 0) {
|
|
45
|
+
throw new Error('receipt public key is required');
|
|
46
|
+
}
|
|
47
|
+
if (HEX64.test(publicKey)) {
|
|
48
|
+
return Uint8Array.from(Buffer.from(publicKey, 'hex'));
|
|
49
|
+
}
|
|
50
|
+
return decodeBase58(publicKey);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============ CRYPTOGRAPHIC VERIFICATION ============
|
|
54
|
+
|
|
55
|
+
async function verifyEd25519(payload, signatureBase64, publicKey) {
|
|
56
|
+
const message = new TextEncoder().encode(toCanonicalJson(payload));
|
|
57
|
+
const signature = Uint8Array.from(Buffer.from(signatureBase64, 'base64'));
|
|
58
|
+
const keyBytes = decodePublicKeyBytes(publicKey);
|
|
59
|
+
|
|
60
|
+
const subtle = globalThis.crypto?.subtle;
|
|
61
|
+
if (subtle) {
|
|
62
|
+
try {
|
|
63
|
+
const key = await subtle.importKey('raw', keyBytes, { name: 'Ed25519' }, false, ['verify']);
|
|
64
|
+
return subtle.verify({ name: 'Ed25519' }, key, signature, message);
|
|
65
|
+
} catch {
|
|
66
|
+
// Ed25519 not supported in this subtle implementation, fall through to Node
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Node.js fallback
|
|
71
|
+
const { createPublicKey, verify } = await import('node:crypto');
|
|
72
|
+
const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex');
|
|
73
|
+
const pubDer = Buffer.concat([spkiPrefix, Buffer.from(keyBytes)]);
|
|
74
|
+
const key = createPublicKey({ key: pubDer, format: 'der', type: 'spki' });
|
|
75
|
+
return verify(null, Buffer.from(message), key, Buffer.from(signature));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Recompute an anchor's hash from its fields (B-3).
|
|
80
|
+
* Requires Node.js crypto — not available in browsers.
|
|
81
|
+
* Returns true if the recomputed hash matches anchor.hash.
|
|
82
|
+
*/
|
|
83
|
+
async function recomputeAnchorHash(anchor) {
|
|
84
|
+
if (!anchor || !HEX64.test(anchor.hash) || !HEX64.test(anchor.prev_hash)) return false;
|
|
85
|
+
if (!Number.isInteger(anchor.beat_index) || anchor.beat_index < 0) return false;
|
|
86
|
+
if (!Number.isInteger(anchor.difficulty) || anchor.difficulty <= 0) return false;
|
|
87
|
+
if (!Number.isInteger(anchor.utc) || anchor.utc < 0) return false;
|
|
88
|
+
if (!Number.isInteger(anchor.epoch) || anchor.epoch < 0) return false;
|
|
89
|
+
|
|
90
|
+
const { createHash } = await import('node:crypto');
|
|
91
|
+
const nonce = `anchor:${anchor.utc}:${anchor.epoch}`;
|
|
92
|
+
const seed = `${anchor.prev_hash}:${anchor.beat_index}:${nonce}`;
|
|
93
|
+
let current = createHash('sha256').update(seed, 'utf8').digest('hex');
|
|
94
|
+
for (let i = 0; i < anchor.difficulty; i++) {
|
|
95
|
+
current = createHash('sha256').update(current, 'utf8').digest('hex');
|
|
96
|
+
}
|
|
97
|
+
return current === anchor.hash;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============ ON-CHAIN VERIFICATION ============
|
|
101
|
+
|
|
102
|
+
async function verifyOnChainTx({
|
|
103
|
+
txSignature,
|
|
104
|
+
rpcUrl,
|
|
105
|
+
cluster = 'devnet',
|
|
106
|
+
fetchImpl,
|
|
107
|
+
}) {
|
|
108
|
+
if (typeof txSignature !== 'string' || txSignature.length === 0) {
|
|
109
|
+
throw new Error('txSignature is required');
|
|
110
|
+
}
|
|
111
|
+
const endpoint = rpcUrl || CLUSTER_RPC[cluster] || CLUSTER_RPC.devnet;
|
|
112
|
+
const res = await fetchImpl(endpoint, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'content-type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
jsonrpc: '2.0',
|
|
117
|
+
id: 1,
|
|
118
|
+
method: 'getSignatureStatuses',
|
|
119
|
+
params: [[txSignature], { searchTransactionHistory: true }],
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
const body = await res.json();
|
|
123
|
+
if (!res.ok || body?.error) {
|
|
124
|
+
throw new Error(`RPC verification failed for ${txSignature}`);
|
|
125
|
+
}
|
|
126
|
+
const status = body?.result?.value?.[0] || null;
|
|
127
|
+
return {
|
|
128
|
+
found: !!status,
|
|
129
|
+
confirmationStatus: status?.confirmationStatus || null,
|
|
130
|
+
finalized: status?.confirmationStatus === 'finalized',
|
|
131
|
+
slot: status?.slot ?? null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetch full transaction from Solana RPC and extract SPL Memo content (B-4).
|
|
137
|
+
* Returns parsed memo data or null if no memo found.
|
|
138
|
+
*/
|
|
139
|
+
async function fetchTransactionMemo({ txSignature, rpcUrl, cluster = 'devnet', fetchImpl }) {
|
|
140
|
+
const endpoint = rpcUrl || CLUSTER_RPC[cluster] || CLUSTER_RPC.devnet;
|
|
141
|
+
const res = await fetchImpl(endpoint, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { 'content-type': 'application/json' },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
jsonrpc: '2.0',
|
|
146
|
+
id: 2,
|
|
147
|
+
method: 'getTransaction',
|
|
148
|
+
params: [txSignature, { encoding: 'jsonParsed', commitment: 'finalized' }],
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
const body = await res.json();
|
|
152
|
+
if (!res.ok || body?.error || !body?.result) return null;
|
|
153
|
+
|
|
154
|
+
const instructions = body.result.transaction?.message?.instructions || [];
|
|
155
|
+
for (const ix of instructions) {
|
|
156
|
+
// SPL Memo can appear as programId or program field
|
|
157
|
+
if (ix.programId === MEMO_PROGRAM_ID || ix.program === 'spl-memo') {
|
|
158
|
+
const raw = ix.parsed || ix.data;
|
|
159
|
+
if (!raw) continue;
|
|
160
|
+
// Memo may have a Solana program prefix: "[program] JSON"
|
|
161
|
+
let memoStr = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
162
|
+
const bracketEnd = memoStr.indexOf('] ');
|
|
163
|
+
if (bracketEnd !== -1 && memoStr.startsWith('[')) {
|
|
164
|
+
memoStr = memoStr.slice(bracketEnd + 2);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(memoStr);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============ CLIENT FACTORY ============
|
|
177
|
+
|
|
178
|
+
export function createBeatsClient({
|
|
179
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
180
|
+
fetchImpl = globalThis.fetch,
|
|
181
|
+
pinnedPublicKey = null,
|
|
182
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
183
|
+
onStateChange = null,
|
|
184
|
+
loadState = null,
|
|
185
|
+
agentId = null,
|
|
186
|
+
} = {}) {
|
|
187
|
+
if (typeof fetchImpl !== 'function') {
|
|
188
|
+
throw new Error('fetch implementation is required');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// B-1: Key resolution state — pinned key has highest priority
|
|
192
|
+
let _cachedKey = pinnedPublicKey || null;
|
|
193
|
+
|
|
194
|
+
// B-2: Anchor continuity state
|
|
195
|
+
let _lastAnchor = null;
|
|
196
|
+
let _broken = false;
|
|
197
|
+
let _breakReason = null;
|
|
198
|
+
|
|
199
|
+
// Load persisted continuity state
|
|
200
|
+
if (typeof loadState === 'function') {
|
|
201
|
+
const saved = loadState();
|
|
202
|
+
if (saved && typeof saved.beat_index === 'number' && HEX64.test(saved.hash)) {
|
|
203
|
+
if (!agentId || saved.agent_id === agentId) {
|
|
204
|
+
_lastAnchor = { beat_index: saved.beat_index, hash: saved.hash };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _persistState(anchor) {
|
|
210
|
+
if (typeof onStateChange === 'function' && anchor) {
|
|
211
|
+
onStateChange({ beat_index: anchor.beat_index, hash: anchor.hash, agent_id: agentId || null });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Internal fetch with timeout (B-5)
|
|
216
|
+
const fetchWithTimeout = async (url, init = {}) => {
|
|
217
|
+
if (typeof AbortController !== 'undefined' && timeoutMs > 0) {
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
220
|
+
try {
|
|
221
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
222
|
+
} finally {
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return fetchImpl(url, init);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const request = async (path, init = {}) => {
|
|
230
|
+
const res = await fetchWithTimeout(`${baseUrl}${path}`, init);
|
|
231
|
+
const text = await res.text();
|
|
232
|
+
let data = null;
|
|
233
|
+
try {
|
|
234
|
+
data = text ? JSON.parse(text) : null;
|
|
235
|
+
} catch {
|
|
236
|
+
data = { raw: text };
|
|
237
|
+
}
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
const msg = data?.error || `HTTP ${res.status}`;
|
|
240
|
+
const err = new Error(msg);
|
|
241
|
+
err.status = res.status;
|
|
242
|
+
err.data = data;
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
return data;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
// ---- Health ----
|
|
250
|
+
getHealth() {
|
|
251
|
+
return request('/api/health');
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// ---- Anchor (with continuity tracking B-2) ----
|
|
255
|
+
async getAnchor(opts = {}) {
|
|
256
|
+
// Fail closed if chain is broken
|
|
257
|
+
if (_broken) {
|
|
258
|
+
const err = new Error(`Chain continuity broken: ${_breakReason}. Call resync() to re-establish.`);
|
|
259
|
+
err.code = 'CHAIN_BROKEN';
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const data = await request('/api/v1/beat/anchor');
|
|
264
|
+
const anchor = data?.anchor;
|
|
265
|
+
|
|
266
|
+
// B-2: Strict continuity validation against last known anchor
|
|
267
|
+
if (anchor && _lastAnchor) {
|
|
268
|
+
if (anchor.beat_index < _lastAnchor.beat_index) {
|
|
269
|
+
_broken = true;
|
|
270
|
+
_breakReason = `regression from ${_lastAnchor.beat_index} to ${anchor.beat_index}`;
|
|
271
|
+
const err = new Error(
|
|
272
|
+
`Anchor chain regression: server returned beat_index ${anchor.beat_index}, ` +
|
|
273
|
+
`but last known was ${_lastAnchor.beat_index}`
|
|
274
|
+
);
|
|
275
|
+
err.code = 'ANCHOR_REGRESSION';
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
// Same beat_index: must be same hash (idempotent re-fetch)
|
|
279
|
+
if (anchor.beat_index === _lastAnchor.beat_index) {
|
|
280
|
+
if (anchor.hash !== _lastAnchor.hash) {
|
|
281
|
+
_broken = true;
|
|
282
|
+
_breakReason = `fork at beat_index ${anchor.beat_index}: hash changed`;
|
|
283
|
+
const err = new Error(
|
|
284
|
+
`Anchor chain fork at beat_index ${anchor.beat_index}: ` +
|
|
285
|
+
`hash ${anchor.hash} differs from last known ${_lastAnchor.hash}`
|
|
286
|
+
);
|
|
287
|
+
err.code = 'ANCHOR_FORK';
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
} else if (anchor.beat_index === _lastAnchor.beat_index + 1) {
|
|
291
|
+
// Consecutive: prev_hash must link
|
|
292
|
+
if (anchor.prev_hash !== _lastAnchor.hash) {
|
|
293
|
+
_broken = true;
|
|
294
|
+
_breakReason = `prev_hash mismatch at beat_index ${anchor.beat_index}`;
|
|
295
|
+
const err = new Error(
|
|
296
|
+
`Anchor chain break at beat_index ${anchor.beat_index}: ` +
|
|
297
|
+
`prev_hash ${anchor.prev_hash} does not match last known hash ${_lastAnchor.hash}`
|
|
298
|
+
);
|
|
299
|
+
err.code = 'ANCHOR_CHAIN_BREAK';
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// Non-consecutive jump: fail closed
|
|
304
|
+
_broken = true;
|
|
305
|
+
_breakReason = `beat_index jumped from ${_lastAnchor.beat_index} to ${anchor.beat_index}`;
|
|
306
|
+
const err = new Error(
|
|
307
|
+
`Anchor chain jump: beat_index ${anchor.beat_index} is not consecutive ` +
|
|
308
|
+
`(expected ${_lastAnchor.beat_index + 1})`
|
|
309
|
+
);
|
|
310
|
+
err.code = 'ANCHOR_JUMP';
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// B-1: Receipt verification with pinned/cached key (never response key)
|
|
316
|
+
if (opts.verify === true) {
|
|
317
|
+
const ok = await this.verifyAnchor(data);
|
|
318
|
+
if (!ok) throw new Error('Anchor receipt verification failed');
|
|
319
|
+
data._verified_receipt = true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// B-3: Optional hash recomputation
|
|
323
|
+
if (opts.recompute === true && anchor) {
|
|
324
|
+
const hashValid = await recomputeAnchorHash(anchor);
|
|
325
|
+
if (!hashValid) throw new Error('Anchor hash recomputation failed — server returned invalid hash');
|
|
326
|
+
data._verified_hash = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Update continuity state + persist
|
|
330
|
+
if (anchor && (!_lastAnchor || anchor.beat_index > _lastAnchor.beat_index)) {
|
|
331
|
+
_lastAnchor = { ...anchor };
|
|
332
|
+
_persistState(_lastAnchor);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return data;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// ---- Key ----
|
|
339
|
+
getKey() {
|
|
340
|
+
return request('/api/v1/beat/key');
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// ---- Verify (server-side) ----
|
|
344
|
+
verify(payload) {
|
|
345
|
+
return request('/api/v1/beat/verify', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'content-type': 'application/json' },
|
|
348
|
+
body: JSON.stringify(payload),
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
// ---- Timestamp ----
|
|
353
|
+
timestampHash(hash) {
|
|
354
|
+
return request('/api/v1/beat/timestamp', {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: { 'content-type': 'application/json' },
|
|
357
|
+
body: JSON.stringify({ hash }),
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
// ---- Receipt Verification (B-1: key pinning) ----
|
|
362
|
+
async verifyReceipt(response, opts = {}) {
|
|
363
|
+
const payload = response?.timestamp || response?.anchor || response?.payload;
|
|
364
|
+
const signature = response?.receipt?.signature || response?.signature;
|
|
365
|
+
if (!payload || !signature) return false;
|
|
366
|
+
|
|
367
|
+
// B-1: Key resolution priority — param > pinned > cached > auto-fetch
|
|
368
|
+
// NEVER use response.receipt.public_key for verification
|
|
369
|
+
const key = opts.publicKey || _cachedKey || await this._resolveKey();
|
|
370
|
+
if (!key) return false;
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
return await verifyEd25519(payload, signature, key);
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
async verifyAnchor(anchorResponse) {
|
|
380
|
+
if (!anchorResponse?.anchor || !anchorResponse?.receipt) return false;
|
|
381
|
+
return this.verifyReceipt(anchorResponse);
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
// ---- Anchor Hash Recomputation (B-3) ----
|
|
385
|
+
verifyAnchorHash(anchor) {
|
|
386
|
+
return recomputeAnchorHash(anchor);
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
// ---- On-Chain Verification (B-4: memo content check) ----
|
|
390
|
+
async verifyOnChain(txSignature, opts = {}) {
|
|
391
|
+
const rpcUrl = opts.rpcUrl || CLUSTER_RPC[opts.cluster || 'devnet'];
|
|
392
|
+
const status = await verifyOnChainTx({
|
|
393
|
+
txSignature,
|
|
394
|
+
rpcUrl,
|
|
395
|
+
cluster: opts.cluster || 'devnet',
|
|
396
|
+
fetchImpl: fetchWithTimeout,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (!status.found || !status.finalized) return status;
|
|
400
|
+
|
|
401
|
+
// B-4: If expectedPayload provided, fetch tx and verify memo content
|
|
402
|
+
if (opts.expectedPayload) {
|
|
403
|
+
const memo = await fetchTransactionMemo({
|
|
404
|
+
txSignature,
|
|
405
|
+
rpcUrl,
|
|
406
|
+
cluster: opts.cluster || 'devnet',
|
|
407
|
+
fetchImpl: fetchWithTimeout,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (!memo) {
|
|
411
|
+
return { ...status, memoVerified: false, reason: 'No SPL Memo instruction found in transaction' };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const expected = opts.expectedPayload;
|
|
415
|
+
// Compare critical anchor fields
|
|
416
|
+
const memoMatch = (
|
|
417
|
+
(expected.hash === undefined || memo.hash === expected.hash) &&
|
|
418
|
+
(expected.beat_index === undefined || memo.beat_index === expected.beat_index) &&
|
|
419
|
+
(expected.prev_hash === undefined || memo.prev === expected.prev_hash) &&
|
|
420
|
+
(expected.utc === undefined || memo.utc === expected.utc) &&
|
|
421
|
+
(expected.difficulty === undefined || memo.difficulty === expected.difficulty) &&
|
|
422
|
+
(expected.epoch === undefined || memo.epoch === expected.epoch) &&
|
|
423
|
+
// Timestamp memo fields
|
|
424
|
+
(expected.anchor_index === undefined || memo.anchor_index === expected.anchor_index) &&
|
|
425
|
+
(expected.anchor_hash === undefined || memo.anchor_hash === expected.anchor_hash)
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
return { ...status, memoVerified: memoMatch, memoData: memo };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return status;
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
// ---- Continuity State (B-2) ----
|
|
435
|
+
setLastKnownAnchor(anchor) {
|
|
436
|
+
if (_broken) {
|
|
437
|
+
throw new Error('Chain is broken. Call resync() to re-establish continuity.');
|
|
438
|
+
}
|
|
439
|
+
if (anchor && typeof anchor.beat_index === 'number' && HEX64.test(anchor.hash)) {
|
|
440
|
+
_lastAnchor = { ...anchor };
|
|
441
|
+
_persistState(_lastAnchor);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
getLastKnownAnchor() {
|
|
446
|
+
return _lastAnchor ? { ..._lastAnchor } : null;
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
resync(anchor) {
|
|
450
|
+
if (!anchor || typeof anchor.beat_index !== 'number' || !HEX64.test(anchor.hash)) {
|
|
451
|
+
throw new Error('resync requires a valid anchor with beat_index and hash');
|
|
452
|
+
}
|
|
453
|
+
_broken = false;
|
|
454
|
+
_breakReason = null;
|
|
455
|
+
_lastAnchor = { ...anchor };
|
|
456
|
+
_persistState(_lastAnchor);
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
isBroken() {
|
|
460
|
+
return _broken;
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
// ---- Internal: Key Resolution (B-1) ----
|
|
464
|
+
async _resolveKey() {
|
|
465
|
+
if (_cachedKey) return _cachedKey;
|
|
466
|
+
try {
|
|
467
|
+
const keyData = await this.getKey();
|
|
468
|
+
// Prefer hex (more portable) over base58
|
|
469
|
+
_cachedKey = keyData.public_key_hex || keyData.public_key_base58;
|
|
470
|
+
return _cachedKey;
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@provenonce/beats-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Minimal client for the public Provenonce Beats service",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.mjs",
|
|
7
|
-
"types": "index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": "./index.mjs",
|
|
11
|
-
"types": "./index.d.ts"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"index.mjs",
|
|
16
|
-
"index.d.ts",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
19
|
-
"license": "MIT"
|
|
20
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@provenonce/beats-client",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Minimal client for the public Provenonce Beats service",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./index.mjs",
|
|
11
|
+
"types": "./index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.mjs",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|