@hivemind-os/collective-core 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/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
- package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
- package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
- package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
- package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
- package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
- package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
- package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
- package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
- package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
- package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
- package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
- package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
- package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
- package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
- package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
- package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
- package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
- package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
- package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
- package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
- package/.turbo/turbo-build.log +14 -0
- package/dist/index.d.ts +1675 -0
- package/dist/index.js +8006 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/auth/device-flow.ts +108 -0
- package/src/auth/ed25519-provider.ts +43 -0
- package/src/auth/errors.ts +82 -0
- package/src/auth/evm-key.ts +55 -0
- package/src/auth/index.ts +8 -0
- package/src/auth/session-state.ts +25 -0
- package/src/auth/session-store.ts +510 -0
- package/src/auth/types.ts +81 -0
- package/src/auth/zklogin-provider.ts +902 -0
- package/src/blobstore/WALRUS_FINDINGS.md +284 -0
- package/src/blobstore/encrypted-store.ts +56 -0
- package/src/blobstore/fs-store.ts +91 -0
- package/src/blobstore/hybrid-store.ts +144 -0
- package/src/blobstore/index.ts +5 -0
- package/src/blobstore/interface.ts +33 -0
- package/src/blobstore/walrus-spike.ts +345 -0
- package/src/blobstore/walrus-store.ts +551 -0
- package/src/cache/agent-cache.ts +403 -0
- package/src/cache/index.ts +1 -0
- package/src/crypto/encryption.ts +152 -0
- package/src/crypto/index.ts +2 -0
- package/src/crypto/x25519.ts +41 -0
- package/src/dispute/client.ts +191 -0
- package/src/dispute/index.ts +1 -0
- package/src/events/index.ts +2 -0
- package/src/events/parser.ts +291 -0
- package/src/events/subscription.ts +131 -0
- package/src/evm/constants.ts +6 -0
- package/src/evm/index.ts +2 -0
- package/src/evm/wallet.ts +136 -0
- package/src/identity/did.ts +36 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keypair.ts +199 -0
- package/src/identity/signing.ts +28 -0
- package/src/index.ts +22 -0
- package/src/internal/parsing.ts +416 -0
- package/src/marketplace/client.ts +349 -0
- package/src/marketplace/index.ts +1 -0
- package/src/metering/hash-chain.ts +94 -0
- package/src/metering/index.ts +4 -0
- package/src/metering/meter.ts +80 -0
- package/src/metering/streaming.ts +196 -0
- package/src/metering/verification.ts +104 -0
- package/src/payment/index.ts +1 -0
- package/src/payment/rail-selector.ts +41 -0
- package/src/registry/client.ts +328 -0
- package/src/registry/index.ts +1 -0
- package/src/relay/consumer-client.ts +497 -0
- package/src/relay/index.ts +1 -0
- package/src/relay-registry/client.ts +295 -0
- package/src/relay-registry/discovery.ts +109 -0
- package/src/relay-registry/index.ts +2 -0
- package/src/reputation/anchor-client.ts +126 -0
- package/src/reputation/event-publisher.ts +67 -0
- package/src/reputation/index.ts +5 -0
- package/src/reputation/merkle.ts +79 -0
- package/src/reputation/score-calculator.ts +133 -0
- package/src/reputation/serialization.ts +37 -0
- package/src/reputation/store.ts +165 -0
- package/src/reputation/validation.ts +135 -0
- package/src/routing/circuit-breaker.ts +111 -0
- package/src/routing/fan-out.ts +266 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/performance.ts +244 -0
- package/src/routing/selector.ts +225 -0
- package/src/spending/index.ts +1 -0
- package/src/spending/policy.ts +271 -0
- package/src/staking/client.ts +319 -0
- package/src/staking/index.ts +1 -0
- package/src/sui/client.ts +214 -0
- package/src/sui/index.ts +2 -0
- package/src/sui/tx-helpers.ts +1070 -0
- package/src/task/client.ts +215 -0
- package/src/task/index.ts +1 -0
- package/src/x402/client.ts +295 -0
- package/src/x402/index.ts +1 -0
- package/tests/auth/device-flow.test.ts +62 -0
- package/tests/auth/ed25519-provider.test.ts +24 -0
- package/tests/auth/evm-key.test.ts +31 -0
- package/tests/auth/session-store.test.ts +201 -0
- package/tests/auth/zklogin-provider.test.ts +366 -0
- package/tests/blobstore/encrypted-store.test.ts +78 -0
- package/tests/blobstore.test.ts +91 -0
- package/tests/cache.test.ts +124 -0
- package/tests/crypto/encryption.test.ts +70 -0
- package/tests/crypto/x25519.test.ts +47 -0
- package/tests/dispute/client.test.ts +238 -0
- package/tests/events.test.ts +202 -0
- package/tests/evm/wallet.test.ts +101 -0
- package/tests/hybrid-store.test.ts +121 -0
- package/tests/identity.test.ts +161 -0
- package/tests/marketplace.test.ts +308 -0
- package/tests/metering/hash-chain.test.ts +32 -0
- package/tests/metering/meter.test.ts +23 -0
- package/tests/metering/streaming.test.ts +52 -0
- package/tests/metering/verification.test.ts +27 -0
- package/tests/payment/rail-selector.test.ts +95 -0
- package/tests/registry.test.ts +183 -0
- package/tests/relay-consumer-client.test.ts +119 -0
- package/tests/relay-registry/client.test.ts +261 -0
- package/tests/reputation/event-publisher.test.ts +70 -0
- package/tests/reputation/merkle.test.ts +44 -0
- package/tests/reputation/score-calculator.test.ts +104 -0
- package/tests/reputation/store.test.ts +94 -0
- package/tests/routing/circuit-breaker.test.ts +45 -0
- package/tests/routing/fan-out.test.ts +123 -0
- package/tests/routing/performance.test.ts +49 -0
- package/tests/routing/selector.test.ts +114 -0
- package/tests/spending.test.ts +133 -0
- package/tests/staking/client.test.ts +286 -0
- package/tests/sui-client.test.ts +85 -0
- package/tests/task.test.ts +249 -0
- package/tests/tx-helpers.test.ts +70 -0
- package/tests/walrus-spike.test.ts +100 -0
- package/tests/walrus-store.test.ts +196 -0
- package/tests/x402/client.test.ts +116 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { PaymentRail, type DID } from '@hivemind-os/collective-types';
|
|
4
|
+
|
|
5
|
+
import type { AuthProvider } from '../auth/types.js';
|
|
6
|
+
import { parseDID, verify } from '../identity/index.js';
|
|
7
|
+
import type { X402Client } from '../x402/client.js';
|
|
8
|
+
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
12
|
+
|
|
13
|
+
interface RelayChallenge {
|
|
14
|
+
rail?: PaymentRail;
|
|
15
|
+
paymentAddress?: string;
|
|
16
|
+
amount?: string;
|
|
17
|
+
currency?: string;
|
|
18
|
+
network?: string;
|
|
19
|
+
relayFee?: string;
|
|
20
|
+
expiresAt?: number;
|
|
21
|
+
nonce?: string;
|
|
22
|
+
asset?: string;
|
|
23
|
+
extra?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RelayErrorPayload {
|
|
27
|
+
error?: {
|
|
28
|
+
code?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
retryable?: boolean;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RelaySuiPaymentProof {
|
|
35
|
+
rail: PaymentRail.SUI_TRANSFER;
|
|
36
|
+
payerDid: DID;
|
|
37
|
+
payerAddress: string;
|
|
38
|
+
paymentAddress: string;
|
|
39
|
+
amount: string;
|
|
40
|
+
currency: string;
|
|
41
|
+
network: string;
|
|
42
|
+
nonce: string;
|
|
43
|
+
expiresAt: number;
|
|
44
|
+
signature: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RelayConsumerClient {
|
|
48
|
+
private readonly sessionId = randomUUID();
|
|
49
|
+
private sequence = 0;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly x402Client: X402Client | null,
|
|
53
|
+
private readonly identity: AuthProvider,
|
|
54
|
+
private readonly config: { relayUrl: string },
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
async executeSync(request: {
|
|
58
|
+
providerDid: string;
|
|
59
|
+
capability: string;
|
|
60
|
+
input: unknown;
|
|
61
|
+
paymentRail: PaymentRail;
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
result: unknown;
|
|
65
|
+
paymentReceipt?: string;
|
|
66
|
+
latencyMs: number;
|
|
67
|
+
taskId?: string;
|
|
68
|
+
providerDid?: string;
|
|
69
|
+
}> {
|
|
70
|
+
const startedAt = Date.now();
|
|
71
|
+
const response = await this.executeWithPayment(request, request.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
result: response.result,
|
|
75
|
+
paymentReceipt: response.paymentReceipt,
|
|
76
|
+
latencyMs: Date.now() - startedAt,
|
|
77
|
+
taskId: response.taskId,
|
|
78
|
+
providerDid: response.providerDid,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async executeSyncStreaming(request: {
|
|
83
|
+
providerDid: string;
|
|
84
|
+
capability: string;
|
|
85
|
+
input: unknown;
|
|
86
|
+
paymentRail: PaymentRail;
|
|
87
|
+
onChunk: (chunk: string) => void;
|
|
88
|
+
onProgress: (progress: number, message?: string) => void;
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
}): Promise<{ result: unknown; paymentReceipt?: string }> {
|
|
91
|
+
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
92
|
+
const initialResponse = await this.sendExecuteRequest({
|
|
93
|
+
providerDid: request.providerDid,
|
|
94
|
+
capability: request.capability,
|
|
95
|
+
input: request.input,
|
|
96
|
+
paymentRail: request.paymentRail,
|
|
97
|
+
stream: true,
|
|
98
|
+
timeoutMs,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const challenge = this.requirePaymentChallenge(initialResponse, request.paymentRail);
|
|
102
|
+
const paymentHeader = await this.createPaymentHeader(challenge, request.paymentRail);
|
|
103
|
+
const paidResponse = await this.sendExecuteRequest({
|
|
104
|
+
providerDid: request.providerDid,
|
|
105
|
+
capability: request.capability,
|
|
106
|
+
input: request.input,
|
|
107
|
+
paymentRail: request.paymentRail,
|
|
108
|
+
paymentSignature: paymentHeader,
|
|
109
|
+
paymentNonce: challenge.nonce,
|
|
110
|
+
stream: true,
|
|
111
|
+
timeoutMs,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!paidResponse.response.ok) {
|
|
115
|
+
throw await toRelayError(paidResponse.response);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const stream = paidResponse.response.body;
|
|
119
|
+
if (!stream) {
|
|
120
|
+
throw new Error('Relay streaming response did not include a body.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const reader = stream.getReader();
|
|
124
|
+
let buffer = '';
|
|
125
|
+
let result: unknown;
|
|
126
|
+
let paymentReceipt = paidResponse.response.headers.get('payment-response') ?? undefined;
|
|
127
|
+
|
|
128
|
+
while (true) {
|
|
129
|
+
const { value, done } = await reader.read();
|
|
130
|
+
if (done) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
buffer += decoder.decode(value, { stream: true });
|
|
135
|
+
while (buffer.includes('\n\n')) {
|
|
136
|
+
const separator = buffer.indexOf('\n\n');
|
|
137
|
+
const rawEvent = buffer.slice(0, separator);
|
|
138
|
+
buffer = buffer.slice(separator + 2);
|
|
139
|
+
const parsed = parseSseEvent(rawEvent);
|
|
140
|
+
if (!parsed) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const eventData = isRecord(parsed.data) ? parsed.data : {};
|
|
145
|
+
switch (parsed.event) {
|
|
146
|
+
case 'progress':
|
|
147
|
+
request.onProgress(Number(eventData.progress ?? 0), asOptionalString(eventData.message));
|
|
148
|
+
break;
|
|
149
|
+
case 'chunk':
|
|
150
|
+
request.onChunk(String(eventData.data ?? ''));
|
|
151
|
+
break;
|
|
152
|
+
case 'result':
|
|
153
|
+
result = eventData.result;
|
|
154
|
+
paymentReceipt = asOptionalString(eventData.paymentReceipt) ?? paymentReceipt;
|
|
155
|
+
break;
|
|
156
|
+
case 'error':
|
|
157
|
+
throw new Error(String(eventData.message ?? 'Relay streaming task failed.'));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result === undefined) {
|
|
163
|
+
throw new Error('Relay streaming task completed without a final result event.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { result, paymentReceipt };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async executeWithPayment(
|
|
170
|
+
request: {
|
|
171
|
+
providerDid: string;
|
|
172
|
+
capability: string;
|
|
173
|
+
input: unknown;
|
|
174
|
+
paymentRail: PaymentRail;
|
|
175
|
+
},
|
|
176
|
+
timeoutMs: number,
|
|
177
|
+
): Promise<{ result: unknown; paymentReceipt?: string; taskId?: string; providerDid?: string }> {
|
|
178
|
+
const initialResponse = await this.sendExecuteRequest({
|
|
179
|
+
providerDid: request.providerDid,
|
|
180
|
+
capability: request.capability,
|
|
181
|
+
input: request.input,
|
|
182
|
+
paymentRail: request.paymentRail,
|
|
183
|
+
timeoutMs,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (initialResponse.response.ok) {
|
|
187
|
+
return {
|
|
188
|
+
result: initialResponse.body,
|
|
189
|
+
paymentReceipt: initialResponse.response.headers.get('payment-response') ?? undefined,
|
|
190
|
+
taskId: initialResponse.response.headers.get('x-mesh-response-id') ?? undefined,
|
|
191
|
+
providerDid: initialResponse.response.headers.get('x-mesh-provider') ?? undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (initialResponse.response.status !== 402) {
|
|
196
|
+
throw await toRelayError(initialResponse.response);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const challenge = this.requirePaymentChallenge(initialResponse, request.paymentRail);
|
|
200
|
+
const signedPayment = await this.createPaymentHeader(challenge, request.paymentRail);
|
|
201
|
+
const paidResponse = await this.sendExecuteRequest({
|
|
202
|
+
providerDid: request.providerDid,
|
|
203
|
+
capability: request.capability,
|
|
204
|
+
input: request.input,
|
|
205
|
+
paymentRail: request.paymentRail,
|
|
206
|
+
paymentSignature: signedPayment,
|
|
207
|
+
paymentNonce: challenge.nonce,
|
|
208
|
+
timeoutMs,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!paidResponse.response.ok) {
|
|
212
|
+
throw await toRelayError(paidResponse.response);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
result: paidResponse.body,
|
|
217
|
+
paymentReceipt: paidResponse.response.headers.get('payment-response') ?? undefined,
|
|
218
|
+
taskId: paidResponse.response.headers.get('x-mesh-response-id') ?? undefined,
|
|
219
|
+
providerDid: paidResponse.response.headers.get('x-mesh-provider') ?? undefined,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async sendExecuteRequest(params: {
|
|
224
|
+
providerDid: string;
|
|
225
|
+
capability: string;
|
|
226
|
+
input: unknown;
|
|
227
|
+
paymentRail: PaymentRail;
|
|
228
|
+
paymentSignature?: string;
|
|
229
|
+
paymentNonce?: string;
|
|
230
|
+
stream?: boolean;
|
|
231
|
+
timeoutMs: number;
|
|
232
|
+
}): Promise<{ response: Response; body: unknown }> {
|
|
233
|
+
const controller = new AbortController();
|
|
234
|
+
const timeout = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const response = await fetch(buildExecuteUrl(this.config.relayUrl, params.providerDid, params.capability, params.stream), {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: {
|
|
240
|
+
accept: params.stream ? 'text/event-stream' : 'application/json',
|
|
241
|
+
'content-type': 'application/json',
|
|
242
|
+
'x-mesh-request-id': randomUUID(),
|
|
243
|
+
'x-mesh-requester': this.identity.getDID(),
|
|
244
|
+
'x-mesh-target-provider': params.providerDid,
|
|
245
|
+
'x-mesh-session-id': this.sessionId,
|
|
246
|
+
'x-mesh-sequence': String(this.nextSequence()),
|
|
247
|
+
'x-mesh-payment-rail': params.paymentRail,
|
|
248
|
+
...(params.paymentSignature ? { 'payment-signature': params.paymentSignature } : {}),
|
|
249
|
+
...(params.paymentNonce ? { 'x-mesh-payment-nonce': params.paymentNonce } : {}),
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify(params.input),
|
|
252
|
+
signal: controller.signal,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const body = params.stream && response.ok
|
|
256
|
+
? undefined
|
|
257
|
+
: await parseResponseBody(response.clone());
|
|
258
|
+
return { response, body };
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
261
|
+
throw new Error('Relay request timed out.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
throw error;
|
|
265
|
+
} finally {
|
|
266
|
+
clearTimeout(timeout);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private requirePaymentChallenge(
|
|
271
|
+
response: { response: Response; body: unknown },
|
|
272
|
+
paymentRail: PaymentRail,
|
|
273
|
+
): Required<RelayChallenge> {
|
|
274
|
+
const payload = isRecord(response.body) && isRecord(response.body.payment)
|
|
275
|
+
? (response.body.payment as RelayChallenge)
|
|
276
|
+
: isRecord(response.body) && isRecord(response.body.paymentRequest)
|
|
277
|
+
? (response.body.paymentRequest as RelayChallenge)
|
|
278
|
+
: ({} as RelayChallenge);
|
|
279
|
+
|
|
280
|
+
const challenge: RelayChallenge = {
|
|
281
|
+
rail: (payload.rail as PaymentRail | undefined) ?? paymentRail,
|
|
282
|
+
paymentAddress: asOptionalString(payload.paymentAddress),
|
|
283
|
+
amount: asOptionalString(payload.amount),
|
|
284
|
+
currency: asOptionalString(payload.currency),
|
|
285
|
+
network: asOptionalString(payload.network),
|
|
286
|
+
relayFee: asOptionalString(payload.relayFee),
|
|
287
|
+
expiresAt: typeof payload.expiresAt === 'number' ? payload.expiresAt : Number(payload.expiresAt),
|
|
288
|
+
nonce: asOptionalString(payload.nonce),
|
|
289
|
+
asset: asOptionalString(payload.asset),
|
|
290
|
+
extra: isRecord(payload.extra)
|
|
291
|
+
? Object.fromEntries(Object.entries(payload.extra).filter((entry): entry is [string, string] => typeof entry[1] === 'string'))
|
|
292
|
+
: {},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (
|
|
296
|
+
!challenge.rail ||
|
|
297
|
+
!challenge.paymentAddress ||
|
|
298
|
+
!challenge.amount ||
|
|
299
|
+
!challenge.currency ||
|
|
300
|
+
!challenge.network ||
|
|
301
|
+
!challenge.nonce ||
|
|
302
|
+
!Number.isFinite(challenge.expiresAt)
|
|
303
|
+
) {
|
|
304
|
+
throw new Error('Relay 402 response did not include a supported payment challenge.');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return challenge as Required<RelayChallenge>;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async createPaymentHeader(challenge: Required<RelayChallenge>, rail: PaymentRail): Promise<string> {
|
|
311
|
+
switch (rail) {
|
|
312
|
+
case PaymentRail.X402_BASE: {
|
|
313
|
+
if (!this.x402Client) {
|
|
314
|
+
throw new Error('x402 payment was requested but no x402 client is configured.');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const paymentRequest = this.x402Client.parse402Response(
|
|
318
|
+
challenge.extra['payment-required'] ? { 'payment-required': challenge.extra['payment-required'] } : {},
|
|
319
|
+
{ payment: challenge },
|
|
320
|
+
);
|
|
321
|
+
return this.x402Client.createPaymentHeader(paymentRequest);
|
|
322
|
+
}
|
|
323
|
+
case PaymentRail.SUI_TRANSFER:
|
|
324
|
+
case PaymentRail.SUI_ESCROW:
|
|
325
|
+
return encodeRelaySuiPaymentProof(
|
|
326
|
+
await createRelaySuiPaymentProof(this.identity, {
|
|
327
|
+
paymentAddress: challenge.paymentAddress,
|
|
328
|
+
amount: challenge.amount,
|
|
329
|
+
currency: challenge.currency,
|
|
330
|
+
network: challenge.network,
|
|
331
|
+
nonce: challenge.nonce,
|
|
332
|
+
expiresAt: challenge.expiresAt,
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
default:
|
|
336
|
+
throw new Error(`Unsupported relay payment rail: ${rail}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private nextSequence(): number {
|
|
341
|
+
this.sequence += 1;
|
|
342
|
+
return this.sequence;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function createRelaySuiPaymentProof(
|
|
347
|
+
identity: AuthProvider,
|
|
348
|
+
request: {
|
|
349
|
+
paymentAddress: string;
|
|
350
|
+
amount: string;
|
|
351
|
+
currency: string;
|
|
352
|
+
network: string;
|
|
353
|
+
nonce: string;
|
|
354
|
+
expiresAt: number;
|
|
355
|
+
},
|
|
356
|
+
): Promise<RelaySuiPaymentProof> {
|
|
357
|
+
const signer = identity.toSuiSigner();
|
|
358
|
+
const payerDid = identity.getDID() as DID;
|
|
359
|
+
const payerAddress = await identity.getAddress();
|
|
360
|
+
const payload = createRelaySuiPayload({
|
|
361
|
+
payerDid,
|
|
362
|
+
payerAddress,
|
|
363
|
+
paymentAddress: request.paymentAddress,
|
|
364
|
+
amount: request.amount,
|
|
365
|
+
currency: request.currency,
|
|
366
|
+
network: request.network,
|
|
367
|
+
nonce: request.nonce,
|
|
368
|
+
expiresAt: request.expiresAt,
|
|
369
|
+
});
|
|
370
|
+
const signature = Buffer.from(await signer.sign(encoder.encode(payload))).toString('hex');
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
rail: PaymentRail.SUI_TRANSFER,
|
|
374
|
+
payerDid,
|
|
375
|
+
payerAddress,
|
|
376
|
+
paymentAddress: request.paymentAddress,
|
|
377
|
+
amount: request.amount,
|
|
378
|
+
currency: request.currency,
|
|
379
|
+
network: request.network,
|
|
380
|
+
nonce: request.nonce,
|
|
381
|
+
expiresAt: request.expiresAt,
|
|
382
|
+
signature,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function encodeRelaySuiPaymentProof(proof: RelaySuiPaymentProof): string {
|
|
387
|
+
return Buffer.from(JSON.stringify(proof), 'utf8').toString('base64');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function decodeRelaySuiPaymentProof(header: string): RelaySuiPaymentProof {
|
|
391
|
+
return JSON.parse(Buffer.from(header, 'base64').toString('utf8')) as RelaySuiPaymentProof;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function verifyRelaySuiPaymentProof(proof: RelaySuiPaymentProof): boolean {
|
|
395
|
+
const signature = Buffer.from(proof.signature, 'hex');
|
|
396
|
+
const publicKey = parseDID(proof.payerDid).publicKey;
|
|
397
|
+
|
|
398
|
+
return verify(
|
|
399
|
+
encoder.encode(
|
|
400
|
+
createRelaySuiPayload({
|
|
401
|
+
payerDid: proof.payerDid,
|
|
402
|
+
payerAddress: proof.payerAddress,
|
|
403
|
+
paymentAddress: proof.paymentAddress,
|
|
404
|
+
amount: proof.amount,
|
|
405
|
+
currency: proof.currency,
|
|
406
|
+
network: proof.network,
|
|
407
|
+
nonce: proof.nonce,
|
|
408
|
+
expiresAt: proof.expiresAt,
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
signature,
|
|
412
|
+
publicKey,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createRelaySuiPayload(params: {
|
|
417
|
+
payerDid: DID;
|
|
418
|
+
payerAddress: string;
|
|
419
|
+
paymentAddress: string;
|
|
420
|
+
amount: string;
|
|
421
|
+
currency: string;
|
|
422
|
+
network: string;
|
|
423
|
+
nonce: string;
|
|
424
|
+
expiresAt: number;
|
|
425
|
+
}): string {
|
|
426
|
+
return [
|
|
427
|
+
'mesh-sui-payment',
|
|
428
|
+
params.payerDid,
|
|
429
|
+
params.payerAddress,
|
|
430
|
+
params.paymentAddress,
|
|
431
|
+
params.amount,
|
|
432
|
+
params.currency,
|
|
433
|
+
params.network,
|
|
434
|
+
params.nonce,
|
|
435
|
+
String(params.expiresAt),
|
|
436
|
+
].join('|');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function buildExecuteUrl(relayUrl: string, providerDid: string, capability: string, stream?: boolean): string {
|
|
440
|
+
const normalized = relayUrl.replace(/^wss:/i, 'https:').replace(/^ws:/i, 'http:').replace(/\/v1\/ws$/i, '');
|
|
441
|
+
const url = new URL(
|
|
442
|
+
`/mesh/providers/${encodeURIComponent(providerDid)}/capabilities/${encodeURIComponent(capability)}/execute`,
|
|
443
|
+
normalized.endsWith('/') ? normalized : `${normalized}/`,
|
|
444
|
+
);
|
|
445
|
+
if (stream) {
|
|
446
|
+
url.searchParams.set('stream', '1');
|
|
447
|
+
}
|
|
448
|
+
return url.toString();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function parseResponseBody(response: Response): Promise<unknown> {
|
|
452
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
453
|
+
if (contentType.includes('application/json')) {
|
|
454
|
+
return response.json();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return response.text();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function toRelayError(response: Response): Promise<Error> {
|
|
461
|
+
const body = (await parseResponseBody(response.clone())) as RelayErrorPayload | string;
|
|
462
|
+
if (typeof body === 'string') {
|
|
463
|
+
return new Error(body.trim().length > 0 ? body : `Relay request failed with status ${response.status}.`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const message = body.error?.message ?? `Relay request failed with status ${response.status}.`;
|
|
467
|
+
return new Error(message);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseSseEvent(rawEvent: string): { event: string; data: unknown } | null {
|
|
471
|
+
const lines = rawEvent
|
|
472
|
+
.split('\n')
|
|
473
|
+
.map((line) => line.trim())
|
|
474
|
+
.filter(Boolean);
|
|
475
|
+
const event = lines.find((line) => line.startsWith('event:'))?.slice('event:'.length).trim();
|
|
476
|
+
const dataLine = lines.find((line) => line.startsWith('data:'))?.slice('data:'.length).trim();
|
|
477
|
+
if (!event) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
return {
|
|
483
|
+
event,
|
|
484
|
+
data: dataLine ? JSON.parse(dataLine) : undefined,
|
|
485
|
+
};
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
492
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function asOptionalString(value: unknown): string | undefined {
|
|
496
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
497
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './consumer-client.js';
|