@hivemind-os/collective-relay 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/dist/index.js ADDED
@@ -0,0 +1,1859 @@
1
+ // src/index.ts
2
+ import { pathToFileURL } from "url";
3
+
4
+ // src/config.ts
5
+ import { homedir } from "os";
6
+ import { join, resolve } from "path";
7
+ function getDefaultRelayConfig(baseDir = resolve(homedir(), ".hivemind-os/collective", "relay")) {
8
+ return {
9
+ host: "0.0.0.0",
10
+ port: 8080,
11
+ identity: {
12
+ keyPath: join(baseDir, "identity.key")
13
+ },
14
+ fees: {
15
+ basePercentage: 5,
16
+ minimumMist: 1000n
17
+ },
18
+ limits: {
19
+ maxConnections: 1e3,
20
+ maxRequestsPerSecond: 100,
21
+ taskTimeoutMs: 3e4,
22
+ heartbeatIntervalMs: 1e4,
23
+ heartbeatTimeoutMs: 3e4,
24
+ authNonceTtlMs: 5 * 6e4
25
+ },
26
+ cors: {
27
+ allowedOrigins: []
28
+ }
29
+ };
30
+ }
31
+ function loadRelayConfig(overrides = {}) {
32
+ const dataDir = process.env.COLLECTIVE_RELAY_DATA_DIR ? resolve(process.env.COLLECTIVE_RELAY_DATA_DIR) : void 0;
33
+ const defaults = getDefaultRelayConfig(dataDir);
34
+ const config = {
35
+ host: process.env.COLLECTIVE_RELAY_HOST ?? overrides.host ?? defaults.host,
36
+ port: readNumber(process.env.COLLECTIVE_RELAY_PORT) ?? overrides.port ?? defaults.port,
37
+ identity: {
38
+ keyPath: resolve(overrides.identity?.keyPath ?? process.env.COLLECTIVE_RELAY_KEY_PATH ?? defaults.identity.keyPath)
39
+ },
40
+ fees: {
41
+ basePercentage: readNumber(process.env.COLLECTIVE_RELAY_FEE_PERCENT) ?? overrides.fees?.basePercentage ?? defaults.fees.basePercentage,
42
+ minimumMist: readBigInt(process.env.COLLECTIVE_RELAY_MINIMUM_MIST) ?? overrides.fees?.minimumMist ?? defaults.fees.minimumMist
43
+ },
44
+ limits: {
45
+ maxConnections: readNumber(process.env.COLLECTIVE_RELAY_MAX_CONNECTIONS) ?? overrides.limits?.maxConnections ?? defaults.limits.maxConnections,
46
+ maxRequestsPerSecond: readNumber(process.env.COLLECTIVE_RELAY_MAX_RPS) ?? overrides.limits?.maxRequestsPerSecond ?? defaults.limits.maxRequestsPerSecond,
47
+ taskTimeoutMs: readNumber(process.env.COLLECTIVE_RELAY_TASK_TIMEOUT_MS) ?? overrides.limits?.taskTimeoutMs ?? defaults.limits.taskTimeoutMs,
48
+ heartbeatIntervalMs: readNumber(process.env.COLLECTIVE_RELAY_HEARTBEAT_INTERVAL_MS) ?? overrides.limits?.heartbeatIntervalMs ?? defaults.limits.heartbeatIntervalMs,
49
+ heartbeatTimeoutMs: readNumber(process.env.COLLECTIVE_RELAY_HEARTBEAT_TIMEOUT_MS) ?? overrides.limits?.heartbeatTimeoutMs ?? defaults.limits.heartbeatTimeoutMs,
50
+ authNonceTtlMs: readNumber(process.env.COLLECTIVE_RELAY_AUTH_NONCE_TTL_MS) ?? overrides.limits?.authNonceTtlMs ?? defaults.limits.authNonceTtlMs
51
+ },
52
+ cors: {
53
+ allowedOrigins: overrides.cors?.allowedOrigins ?? readStringList(process.env.COLLECTIVE_RELAY_ALLOWED_ORIGINS) ?? defaults.cors?.allowedOrigins ?? []
54
+ },
55
+ sui: overrides.sui || process.env.COLLECTIVE_RELAY_SUI_RPC_URL || process.env.COLLECTIVE_RELAY_SUI_PACKAGE_ID ? {
56
+ rpcUrl: overrides.sui?.rpcUrl ?? process.env.COLLECTIVE_RELAY_SUI_RPC_URL ?? "",
57
+ packageId: overrides.sui?.packageId ?? process.env.COLLECTIVE_RELAY_SUI_PACKAGE_ID ?? ""
58
+ } : void 0,
59
+ relayRegistry: overrides.relayRegistry || process.env.COLLECTIVE_RELAY_REGISTRY_ENABLED || process.env.COLLECTIVE_RELAY_REGISTRY_STAKE_ID || process.env.COLLECTIVE_RELAY_REGISTRY_RELAY_ID ? {
60
+ enabled: readBoolean(process.env.COLLECTIVE_RELAY_REGISTRY_ENABLED) ?? overrides.relayRegistry?.enabled ?? true,
61
+ relayId: overrides.relayRegistry?.relayId ?? process.env.COLLECTIVE_RELAY_REGISTRY_RELAY_ID,
62
+ stakePositionId: overrides.relayRegistry?.stakePositionId ?? process.env.COLLECTIVE_RELAY_REGISTRY_STAKE_ID,
63
+ endpoint: overrides.relayRegistry?.endpoint ?? process.env.COLLECTIVE_RELAY_REGISTRY_ENDPOINT,
64
+ capabilities: overrides.relayRegistry?.capabilities ?? readStringList(process.env.COLLECTIVE_RELAY_REGISTRY_CAPABILITIES) ?? [],
65
+ region: overrides.relayRegistry?.region ?? process.env.COLLECTIVE_RELAY_REGISTRY_REGION,
66
+ routingFeeBps: readNumber(process.env.COLLECTIVE_RELAY_REGISTRY_FEE_BPS) ?? overrides.relayRegistry?.routingFeeBps,
67
+ heartbeatIntervalMs: readNumber(process.env.COLLECTIVE_RELAY_REGISTRY_HEARTBEAT_INTERVAL_MS) ?? overrides.relayRegistry?.heartbeatIntervalMs ?? defaults.limits.heartbeatIntervalMs
68
+ } : void 0
69
+ };
70
+ validateRelayConfig(config);
71
+ return config;
72
+ }
73
+ function validateRelayConfig(config) {
74
+ if (!config.host) {
75
+ throw new Error("Relay host is required.");
76
+ }
77
+ if (!Number.isInteger(config.port) || config.port <= 0) {
78
+ throw new Error("Relay port must be a positive integer.");
79
+ }
80
+ if (!config.identity.keyPath) {
81
+ throw new Error("Relay identity keyPath is required.");
82
+ }
83
+ if (config.fees.basePercentage < 0) {
84
+ throw new Error("Relay base fee percentage must be non-negative.");
85
+ }
86
+ if (config.fees.minimumMist < 0n) {
87
+ throw new Error("Relay minimum fee must be non-negative.");
88
+ }
89
+ for (const [name, value] of Object.entries(config.limits)) {
90
+ if (!Number.isInteger(value) || value <= 0) {
91
+ throw new Error(`Relay limit ${name} must be a positive integer.`);
92
+ }
93
+ }
94
+ if (config.cors && config.cors.allowedOrigins.some((origin) => origin.trim().length === 0)) {
95
+ throw new Error("Relay CORS allowed origins must be non-empty strings.");
96
+ }
97
+ if (!config.relayRegistry || config.relayRegistry.enabled === false) {
98
+ return;
99
+ }
100
+ if (!config.sui?.rpcUrl || !config.sui.packageId) {
101
+ throw new Error("Relay registry integration requires sui.rpcUrl and sui.packageId.");
102
+ }
103
+ if (config.relayRegistry.stakePositionId && !/^0x[0-9a-f]+$/i.test(config.relayRegistry.stakePositionId)) {
104
+ throw new Error("Relay registry stakePositionId must be a 0x-prefixed object id.");
105
+ }
106
+ if (config.relayRegistry.relayId && !/^0x[0-9a-f]+$/i.test(config.relayRegistry.relayId)) {
107
+ throw new Error("Relay registry relayId must be a 0x-prefixed object id.");
108
+ }
109
+ if (config.relayRegistry.routingFeeBps !== void 0) {
110
+ if (!Number.isInteger(config.relayRegistry.routingFeeBps) || config.relayRegistry.routingFeeBps < 0 || config.relayRegistry.routingFeeBps > 1e4) {
111
+ throw new Error("Relay registry routingFeeBps must be an integer between 0 and 10000.");
112
+ }
113
+ }
114
+ if (!Number.isInteger(config.relayRegistry.heartbeatIntervalMs) || config.relayRegistry.heartbeatIntervalMs <= 0) {
115
+ throw new Error("Relay registry heartbeat interval must be a positive integer.");
116
+ }
117
+ }
118
+ function readNumber(value) {
119
+ if (!value) {
120
+ return void 0;
121
+ }
122
+ const parsed = Number(value);
123
+ return Number.isFinite(parsed) ? parsed : void 0;
124
+ }
125
+ function readBigInt(value) {
126
+ if (!value) {
127
+ return void 0;
128
+ }
129
+ return /^\d+$/.test(value.trim()) ? BigInt(value.trim()) : void 0;
130
+ }
131
+ function readBoolean(value) {
132
+ if (!value) {
133
+ return void 0;
134
+ }
135
+ const normalized = value.trim().toLowerCase();
136
+ if (normalized === "true" || normalized === "1" || normalized === "yes") {
137
+ return true;
138
+ }
139
+ if (normalized === "false" || normalized === "0" || normalized === "no") {
140
+ return false;
141
+ }
142
+ return void 0;
143
+ }
144
+ function readStringList(value) {
145
+ if (!value) {
146
+ return void 0;
147
+ }
148
+ const parsed = value.split(",").map((entry) => entry.trim()).filter(Boolean);
149
+ return parsed.length > 0 ? parsed : void 0;
150
+ }
151
+
152
+ // src/server/http-server.ts
153
+ import cors from "@fastify/cors";
154
+ import rateLimit from "@fastify/rate-limit";
155
+ import websocket from "@fastify/websocket";
156
+ import Fastify from "fastify";
157
+ import WebSocket from "ws";
158
+
159
+ // src/health/monitor.ts
160
+ var HealthMonitor = class {
161
+ constructor(options = {}) {
162
+ this.options = options;
163
+ this.now = options.now ?? (() => Date.now());
164
+ this.startedAt = this.now();
165
+ }
166
+ options;
167
+ now;
168
+ startedAt;
169
+ activeRequests = 0;
170
+ connectedProviders = 0;
171
+ totalRequestsServed = 0;
172
+ totalLatencyMs = 0;
173
+ setConnectedProviders(count) {
174
+ this.connectedProviders = count;
175
+ }
176
+ beginRequest() {
177
+ const startedAt = this.now();
178
+ let finished = false;
179
+ this.activeRequests += 1;
180
+ return {
181
+ finish: () => {
182
+ if (finished) {
183
+ return;
184
+ }
185
+ finished = true;
186
+ this.activeRequests -= 1;
187
+ this.totalRequestsServed += 1;
188
+ this.totalLatencyMs += this.now() - startedAt;
189
+ }
190
+ };
191
+ }
192
+ recordRequest(latencyMs) {
193
+ this.totalRequestsServed += 1;
194
+ this.totalLatencyMs += latencyMs;
195
+ }
196
+ getStatus() {
197
+ const connectedProviders = this.options.getConnectedProviders?.() ?? this.connectedProviders;
198
+ let status = "healthy";
199
+ if (connectedProviders === 0 && this.activeRequests > 0) {
200
+ status = "unhealthy";
201
+ } else if (connectedProviders === 0) {
202
+ status = "degraded";
203
+ }
204
+ return {
205
+ status,
206
+ uptime: this.now() - this.startedAt,
207
+ connectedProviders,
208
+ activeRequests: this.activeRequests,
209
+ totalRequestsServed: this.totalRequestsServed,
210
+ averageLatencyMs: this.totalRequestsServed === 0 ? 0 : Math.round(this.totalLatencyMs / this.totalRequestsServed)
211
+ };
212
+ }
213
+ };
214
+
215
+ // src/identity/relay-identity.ts
216
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
217
+ import { dirname, resolve as resolve2 } from "path";
218
+ import { createDID, generateKeypair, keypairFromSecretKey, signString } from "@hivemind-os/collective-core";
219
+ var RelayIdentity = class _RelayIdentity {
220
+ constructor(keyPath, keypair) {
221
+ this.keyPath = keyPath;
222
+ this.keypair = keypair;
223
+ this.did = createDID(keypair.publicKey);
224
+ }
225
+ keyPath;
226
+ keypair;
227
+ did;
228
+ static load(keyPath) {
229
+ const resolvedKeyPath = resolve2(keyPath);
230
+ mkdirSync(dirname(resolvedKeyPath), { recursive: true, mode: 448 });
231
+ if (existsSync(resolvedKeyPath)) {
232
+ chmodSync(resolvedKeyPath, 384);
233
+ const secretKey = Uint8Array.from(Buffer.from(readFileSync(resolvedKeyPath, "utf8").trim(), "hex"));
234
+ return new _RelayIdentity(resolvedKeyPath, keypairFromSecretKey(secretKey));
235
+ }
236
+ const keypair = generateKeypair();
237
+ writeFileSync(resolvedKeyPath, Buffer.from(keypair.secretKey).toString("hex"), { mode: 384 });
238
+ chmodSync(resolvedKeyPath, 384);
239
+ return new _RelayIdentity(resolvedKeyPath, keypair);
240
+ }
241
+ signPayload(payload) {
242
+ return signString(payload, this.keypair.secretKey);
243
+ }
244
+ };
245
+
246
+ // src/payment/payment-gate.ts
247
+ import { randomUUID } from "crypto";
248
+ import {
249
+ decodeRelaySuiPaymentProof,
250
+ verifyRelaySuiPaymentProof,
251
+ USDC_ADDRESS
252
+ } from "@hivemind-os/collective-core";
253
+ import { PaymentRail } from "@hivemind-os/collective-types";
254
+ import {
255
+ decodePaymentSignatureHeader,
256
+ encodePaymentRequiredHeader,
257
+ encodePaymentResponseHeader
258
+ } from "@x402/core/http";
259
+ import { PERMIT2_ADDRESS, permit2WitnessTypes, x402ExactPermit2ProxyAddress } from "@x402/evm";
260
+ import { getAddress, verifyTypedData } from "viem";
261
+
262
+ // src/payment/fee-schedule.ts
263
+ function calculateRelayFee(basePrice, feeSchedule) {
264
+ const computed = basePrice * BigInt(feeSchedule.basePercentage) / 100n;
265
+ const relayFee = computed > feeSchedule.minimumMist ? computed : feeSchedule.minimumMist;
266
+ return {
267
+ relayFee,
268
+ totalPrice: basePrice + relayFee
269
+ };
270
+ }
271
+
272
+ // src/payment/payment-gate.ts
273
+ var DEFAULT_X402_NETWORK = "base-sepolia";
274
+ var DEFAULT_EVM_PAYMENT_ADDRESS = "0x0000000000000000000000000000000000000abc";
275
+ var PaymentGate = class {
276
+ constructor(options) {
277
+ this.options = options;
278
+ this.now = options.now ?? (() => Date.now());
279
+ this.nonceFactory = options.nonceFactory ?? (() => randomUUID());
280
+ this.basePriceResolver = options.basePriceResolver ?? (() => 0n);
281
+ this.defaultRail = options.defaultRail ?? PaymentRail.SUI_TRANSFER;
282
+ }
283
+ options;
284
+ activeChallenges = /* @__PURE__ */ new Map();
285
+ consumedProofs = /* @__PURE__ */ new Map();
286
+ now;
287
+ nonceFactory;
288
+ basePriceResolver;
289
+ defaultRail;
290
+ generate402Challenge(rail, capability, provider) {
291
+ const basePrice = this.basePriceResolver(capability, provider);
292
+ const fee = this.calculateFee(basePrice);
293
+ const nonce = this.nonceFactory();
294
+ const expiresAt = this.now() + (this.options.challengeTtlMs ?? 6e4);
295
+ const challenge = rail === PaymentRail.X402_BASE ? this.createX402Challenge(fee, nonce, expiresAt) : this.createSuiChallenge(rail, fee, nonce, expiresAt);
296
+ this.activeChallenges.set(challenge.nonce, challenge);
297
+ return challenge;
298
+ }
299
+ getChallenge(nonce) {
300
+ return this.activeChallenges.get(nonce) ?? null;
301
+ }
302
+ isChallengeExpired(challenge) {
303
+ return challenge.expiresAt <= this.now();
304
+ }
305
+ async verifyPayment(paymentHeader, challenge) {
306
+ this.pruneConsumedProofs();
307
+ if (!this.activeChallenges.has(challenge.nonce)) {
308
+ return {
309
+ accepted: false,
310
+ reason: "Unknown payment challenge."
311
+ };
312
+ }
313
+ if (this.isChallengeExpired(challenge)) {
314
+ this.activeChallenges.delete(challenge.nonce);
315
+ return {
316
+ accepted: false,
317
+ reason: "Payment challenge expired."
318
+ };
319
+ }
320
+ const replayMetadata = getPaymentReplayMetadata(paymentHeader, challenge);
321
+ if (replayMetadata && this.consumedProofs.has(replayMetadata.key)) {
322
+ return {
323
+ accepted: false,
324
+ reason: "Payment proof has already been used."
325
+ };
326
+ }
327
+ const verifier = this.options.verifyPaymentProof;
328
+ const verification = verifier ? await verifier(paymentHeader, challenge) : await this.verifyChallenge(paymentHeader, challenge);
329
+ if (verification.accepted) {
330
+ this.activeChallenges.delete(challenge.nonce);
331
+ if (replayMetadata) {
332
+ this.consumedProofs.set(replayMetadata.key, replayMetadata.expiresAt);
333
+ }
334
+ }
335
+ return verification;
336
+ }
337
+ calculateFee(basePrice) {
338
+ return calculateRelayFee(basePrice, this.options.feeSchedule);
339
+ }
340
+ pruneExpiredChallenges() {
341
+ this.pruneConsumedProofs();
342
+ let removed = 0;
343
+ for (const challenge of this.activeChallenges.values()) {
344
+ if (!this.isChallengeExpired(challenge)) {
345
+ continue;
346
+ }
347
+ this.activeChallenges.delete(challenge.nonce);
348
+ removed += 1;
349
+ }
350
+ return removed;
351
+ }
352
+ pruneConsumedProofs() {
353
+ const now = this.now();
354
+ for (const [replayKey, expiresAt] of this.consumedProofs.entries()) {
355
+ if (expiresAt > now) {
356
+ continue;
357
+ }
358
+ this.consumedProofs.delete(replayKey);
359
+ }
360
+ }
361
+ createSuiChallenge(rail, fee, nonce, expiresAt) {
362
+ return {
363
+ rail,
364
+ paymentAddress: this.options.paymentAddress ?? this.options.relayDid,
365
+ amount: fee.totalPrice.toString(),
366
+ currency: "MIST",
367
+ network: "sui",
368
+ relayFee: fee.relayFee.toString(),
369
+ expiresAt,
370
+ nonce
371
+ };
372
+ }
373
+ createX402Challenge(fee, nonce, expiresAt) {
374
+ const network = DEFAULT_X402_NETWORK;
375
+ const asset = USDC_ADDRESS[network];
376
+ const paymentAddress = this.options.evmPaymentAddress ?? DEFAULT_EVM_PAYMENT_ADDRESS;
377
+ const paymentRequiredHeader = encodePaymentRequiredHeader({
378
+ x402Version: 2,
379
+ resource: { url: "https://relay.hivemind-collective.local/execute" },
380
+ accepts: [
381
+ {
382
+ scheme: "exact",
383
+ network: toCaip2Network(network),
384
+ asset,
385
+ amount: fee.totalPrice.toString(),
386
+ payTo: paymentAddress,
387
+ maxTimeoutSeconds: Math.max(1, Math.ceil((expiresAt - this.now()) / 1e3)),
388
+ extra: {
389
+ assetTransferMethod: "permit2",
390
+ currency: "USDC",
391
+ nonce,
392
+ expiresAt: String(expiresAt)
393
+ }
394
+ }
395
+ ]
396
+ });
397
+ return {
398
+ rail: PaymentRail.X402_BASE,
399
+ paymentAddress,
400
+ amount: fee.totalPrice.toString(),
401
+ currency: "USDC",
402
+ network,
403
+ relayFee: fee.relayFee.toString(),
404
+ expiresAt,
405
+ nonce,
406
+ asset,
407
+ extra: {
408
+ assetTransferMethod: "permit2",
409
+ currency: "USDC",
410
+ nonce,
411
+ expiresAt: String(expiresAt),
412
+ "payment-required": paymentRequiredHeader
413
+ }
414
+ };
415
+ }
416
+ async verifyChallenge(paymentHeader, challenge) {
417
+ switch (challenge.rail) {
418
+ case PaymentRail.X402_BASE:
419
+ return verifyX402Payment(paymentHeader, challenge);
420
+ case PaymentRail.SUI_TRANSFER:
421
+ case PaymentRail.SUI_ESCROW:
422
+ return verifySuiPayment(paymentHeader, challenge);
423
+ default:
424
+ return {
425
+ accepted: false,
426
+ reason: `Unsupported payment rail: ${String(challenge.rail)}`
427
+ };
428
+ }
429
+ }
430
+ };
431
+ async function verifyX402Payment(paymentHeader, challenge) {
432
+ try {
433
+ const payment = decodePaymentSignatureHeader(paymentHeader);
434
+ const accepted = payment.accepted;
435
+ if (accepted.scheme !== "exact" || accepted.amount !== challenge.amount || normalizeNetwork(accepted.network) !== normalizeNetwork(toCaip2Network(challenge.network)) || getAddress(accepted.payTo) !== getAddress(challenge.paymentAddress) || challenge.asset && getAddress(accepted.asset) !== getAddress(challenge.asset)) {
436
+ return {
437
+ accepted: false,
438
+ reason: "x402 payment requirements did not match the relay challenge."
439
+ };
440
+ }
441
+ if (!hasPermit2Authorization(payment.payload)) {
442
+ return {
443
+ accepted: false,
444
+ reason: "Only Permit2 x402 payments are currently supported by the relay."
445
+ };
446
+ }
447
+ const authorization = payment.payload.permit2Authorization;
448
+ const verified = await verifyTypedData({
449
+ address: getAddress(authorization.from),
450
+ domain: {
451
+ name: "Permit2",
452
+ chainId: toChainId(challenge.network),
453
+ verifyingContract: PERMIT2_ADDRESS
454
+ },
455
+ types: permit2WitnessTypes,
456
+ primaryType: "PermitWitnessTransferFrom",
457
+ message: {
458
+ permitted: {
459
+ token: getAddress(authorization.permitted.token),
460
+ amount: BigInt(authorization.permitted.amount)
461
+ },
462
+ spender: getAddress(authorization.spender),
463
+ nonce: BigInt(authorization.nonce),
464
+ deadline: BigInt(authorization.deadline),
465
+ witness: {
466
+ to: getAddress(authorization.witness.to),
467
+ validAfter: BigInt(authorization.witness.validAfter)
468
+ }
469
+ },
470
+ signature: payment.payload.signature
471
+ });
472
+ const deadlineMs = deadlineToTimestampMs(authorization.deadline);
473
+ if (!verified || getAddress(authorization.witness.to) !== getAddress(challenge.paymentAddress) || getAddress(authorization.permitted.token) !== getAddress(challenge.asset ?? authorization.permitted.token) || authorization.permitted.amount !== challenge.amount || getAddress(authorization.spender) !== getAddress(x402ExactPermit2ProxyAddress) || deadlineMs === null || deadlineMs > challenge.expiresAt + 1e3) {
474
+ return {
475
+ accepted: false,
476
+ reason: "x402 payment signature verification failed."
477
+ };
478
+ }
479
+ return {
480
+ accepted: true,
481
+ payer: authorization.from,
482
+ settlementReference: encodePaymentResponseHeader({
483
+ success: true,
484
+ transaction: `x402-${challenge.nonce}`,
485
+ network: toCaip2Network(challenge.network),
486
+ amount: challenge.amount,
487
+ payer: authorization.from
488
+ }),
489
+ relayFee: challenge.relayFee,
490
+ totalPrice: challenge.amount
491
+ };
492
+ } catch (error) {
493
+ return {
494
+ accepted: false,
495
+ reason: error instanceof Error ? error.message : "x402 payment verification failed."
496
+ };
497
+ }
498
+ }
499
+ async function verifySuiPayment(paymentHeader, challenge) {
500
+ try {
501
+ const proof = decodeRelaySuiPaymentProof(paymentHeader);
502
+ if (proof.amount !== challenge.amount || proof.nonce !== challenge.nonce || proof.currency !== challenge.currency || proof.network !== challenge.network || proof.paymentAddress !== challenge.paymentAddress || proof.expiresAt !== challenge.expiresAt) {
503
+ return {
504
+ accepted: false,
505
+ reason: "Sui payment proof did not match the relay challenge."
506
+ };
507
+ }
508
+ if (!verifyRelaySuiPaymentProof(proof)) {
509
+ return {
510
+ accepted: false,
511
+ reason: "Sui payment proof signature verification failed."
512
+ };
513
+ }
514
+ return {
515
+ accepted: true,
516
+ payer: proof.payerAddress,
517
+ settlementReference: JSON.stringify({
518
+ rail: challenge.rail,
519
+ nonce: challenge.nonce,
520
+ payerDid: proof.payerDid,
521
+ payerAddress: proof.payerAddress
522
+ }),
523
+ relayFee: challenge.relayFee,
524
+ totalPrice: challenge.amount
525
+ };
526
+ } catch (error) {
527
+ return {
528
+ accepted: false,
529
+ reason: error instanceof Error ? error.message : "Sui payment proof verification failed."
530
+ };
531
+ }
532
+ }
533
+ function hasPermit2Authorization(value) {
534
+ if (!value || typeof value !== "object") {
535
+ return false;
536
+ }
537
+ const candidate = value;
538
+ const authorization = candidate.permit2Authorization;
539
+ return Boolean(
540
+ typeof candidate.signature === "string" && authorization && typeof authorization.from === "string" && typeof authorization.spender === "string" && typeof authorization.nonce === "string" && typeof authorization.deadline === "string" && typeof authorization.witness === "object" && typeof authorization.permitted === "object"
541
+ );
542
+ }
543
+ function getPaymentReplayMetadata(paymentHeader, challenge) {
544
+ try {
545
+ switch (challenge.rail) {
546
+ case PaymentRail.X402_BASE: {
547
+ const payment = decodePaymentSignatureHeader(paymentHeader);
548
+ if (!hasPermit2Authorization(payment.payload)) {
549
+ return null;
550
+ }
551
+ const expiresAt = deadlineToTimestampMs(payment.payload.permit2Authorization.deadline) ?? challenge.expiresAt;
552
+ return {
553
+ key: `x402:${payment.payload.signature}`,
554
+ expiresAt
555
+ };
556
+ }
557
+ case PaymentRail.SUI_TRANSFER:
558
+ case PaymentRail.SUI_ESCROW: {
559
+ const proof = decodeRelaySuiPaymentProof(paymentHeader);
560
+ return {
561
+ key: `sui:${proof.signature}`,
562
+ expiresAt: proof.expiresAt
563
+ };
564
+ }
565
+ default:
566
+ return null;
567
+ }
568
+ } catch {
569
+ return null;
570
+ }
571
+ }
572
+ function deadlineToTimestampMs(deadlineSeconds) {
573
+ if (!/^\d+$/.test(deadlineSeconds)) {
574
+ return null;
575
+ }
576
+ const deadlineMs = BigInt(deadlineSeconds) * 1000n;
577
+ if (deadlineMs > BigInt(Number.MAX_SAFE_INTEGER)) {
578
+ return null;
579
+ }
580
+ return Number(deadlineMs);
581
+ }
582
+ function normalizeNetwork(network) {
583
+ return network.trim().toLowerCase();
584
+ }
585
+ function toChainId(network) {
586
+ switch (normalizeNetwork(network)) {
587
+ case "base":
588
+ return 8453;
589
+ case "base-sepolia":
590
+ return 84532;
591
+ case "localhost":
592
+ return 31337;
593
+ default:
594
+ throw new Error(`Unsupported x402 network: ${network}`);
595
+ }
596
+ }
597
+ function toCaip2Network(network) {
598
+ return `eip155:${toChainId(network)}`;
599
+ }
600
+
601
+ // src/registry/relay-registry-service.ts
602
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
603
+ import { MeshSuiClient, RelayNodeStatus, RelayRegistryClient } from "@hivemind-os/collective-core";
604
+ var RelayRegistryService = class {
605
+ constructor(config, identity, client) {
606
+ this.config = config;
607
+ this.identity = identity;
608
+ this.enabled = Boolean(config.relayRegistry?.enabled && config.sui?.rpcUrl && config.sui.packageId && config.relayRegistry.stakePositionId);
609
+ this.signer = Ed25519Keypair.fromSecretKey(identity.keypair.secretKey);
610
+ this.client = client ?? (this.enabled ? new RelayRegistryClient(createRelaySuiClient(config), { packageId: config.sui?.packageId ?? "" }) : void 0);
611
+ this.state = {
612
+ enabled: this.enabled,
613
+ registered: false,
614
+ capabilities: config.relayRegistry?.capabilities ?? [],
615
+ region: config.relayRegistry?.region,
616
+ stakePositionId: config.relayRegistry?.stakePositionId,
617
+ endpoint: config.relayRegistry?.endpoint,
618
+ routingFeeBps: config.relayRegistry?.routingFeeBps
619
+ };
620
+ }
621
+ config;
622
+ identity;
623
+ enabled;
624
+ signer;
625
+ client;
626
+ heartbeatTimer;
627
+ state;
628
+ async start(serverAddress) {
629
+ if (!this.enabled || !this.client || !this.config.relayRegistry?.stakePositionId) {
630
+ return;
631
+ }
632
+ const endpoint = resolveRelayEndpoint(this.config, serverAddress);
633
+ this.state.endpoint = endpoint;
634
+ this.state.capabilities = this.config.relayRegistry.capabilities;
635
+ this.state.region = this.config.relayRegistry.region;
636
+ this.state.stakePositionId = this.config.relayRegistry.stakePositionId;
637
+ this.state.routingFeeBps = resolveRoutingFeeBps(this.config);
638
+ this.state.operator = this.signer.getPublicKey().toSuiAddress();
639
+ try {
640
+ const existing = await this.resolveExistingRelay(endpoint);
641
+ if (existing) {
642
+ this.applyRelay(existing);
643
+ } else {
644
+ const registered = await this.client.registerRelay({
645
+ endpoint,
646
+ stakeId: this.config.relayRegistry.stakePositionId,
647
+ capabilities: this.config.relayRegistry.capabilities,
648
+ region: this.config.relayRegistry.region ?? "global",
649
+ routingFeeBps: resolveRoutingFeeBps(this.config),
650
+ signer: this.signer
651
+ });
652
+ await this.refreshRelay(registered.relayId);
653
+ }
654
+ await this.sendHeartbeat();
655
+ this.heartbeatTimer = setInterval(() => {
656
+ void this.sendHeartbeat();
657
+ }, this.config.relayRegistry.heartbeatIntervalMs);
658
+ } catch (error) {
659
+ this.state.lastError = error instanceof Error ? error.message : String(error);
660
+ }
661
+ }
662
+ async stop() {
663
+ if (this.heartbeatTimer) {
664
+ clearInterval(this.heartbeatTimer);
665
+ this.heartbeatTimer = void 0;
666
+ }
667
+ }
668
+ async recordRouting(feeAmountMist) {
669
+ if (!this.state.relayId) {
670
+ return;
671
+ }
672
+ if (feeAmountMist < 0n) {
673
+ throw new Error("feeAmountMist must be non-negative.");
674
+ }
675
+ this.state.totalRouted = (this.state.totalRouted ?? 0) + 1;
676
+ this.state.totalFeesEarnedMist = (BigInt(this.state.totalFeesEarnedMist ?? "0") + feeAmountMist).toString();
677
+ this.state.lastError = void 0;
678
+ }
679
+ getInfo() {
680
+ return { ...this.state };
681
+ }
682
+ async sendHeartbeat() {
683
+ if (!this.client || !this.state.relayId) {
684
+ return;
685
+ }
686
+ try {
687
+ const result = await this.client.heartbeat({ relayId: this.state.relayId, signer: this.signer });
688
+ this.state.lastHeartbeat = result.lastHeartbeat;
689
+ this.state.lastError = void 0;
690
+ await this.refreshRelay(this.state.relayId);
691
+ } catch (error) {
692
+ this.state.lastError = error instanceof Error ? error.message : String(error);
693
+ }
694
+ }
695
+ async resolveExistingRelay(endpoint) {
696
+ if (!this.client || !this.config.relayRegistry) {
697
+ return null;
698
+ }
699
+ if (this.config.relayRegistry.relayId) {
700
+ return await this.client.getRelay(this.config.relayRegistry.relayId);
701
+ }
702
+ const operator = this.signer.getPublicKey().toSuiAddress();
703
+ const matches = await this.client.listRelays({
704
+ activeOnly: false,
705
+ operator,
706
+ stakePositionId: this.config.relayRegistry.stakePositionId,
707
+ endpoint
708
+ });
709
+ return matches.find((relay) => relay.status === RelayNodeStatus.ACTIVE) ?? matches[0] ?? null;
710
+ }
711
+ async refreshRelay(relayId) {
712
+ if (!this.client) {
713
+ return;
714
+ }
715
+ const relay = await this.client.getRelay(relayId);
716
+ if (relay) {
717
+ this.applyRelay(relay);
718
+ }
719
+ }
720
+ applyRelay(relay) {
721
+ this.state = {
722
+ ...this.state,
723
+ enabled: true,
724
+ registered: true,
725
+ relayId: relay.id,
726
+ operator: relay.operator,
727
+ endpoint: relay.endpoint,
728
+ stakePositionId: relay.stakePositionId,
729
+ capabilities: relay.capabilities,
730
+ region: relay.region,
731
+ status: relayStatusToText(relay.status),
732
+ routingFeeBps: relay.routingFeeBps,
733
+ lastHeartbeat: relay.lastHeartbeat,
734
+ totalRouted: relay.totalRouted,
735
+ totalFeesEarnedMist: relay.totalFeesEarnedMist.toString(),
736
+ lastError: void 0
737
+ };
738
+ }
739
+ };
740
+ function createRelaySuiClient(config) {
741
+ return new MeshSuiClient({
742
+ rpcUrl: config.sui?.rpcUrl ?? "",
743
+ faucetUrl: config.sui?.rpcUrl ?? "",
744
+ packageId: config.sui?.packageId ?? "",
745
+ registryId: config.sui?.packageId ?? ""
746
+ });
747
+ }
748
+ function resolveRelayEndpoint(config, serverAddress) {
749
+ if (config.relayRegistry?.endpoint) {
750
+ return config.relayRegistry.endpoint;
751
+ }
752
+ const url = new URL(serverAddress);
753
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
754
+ url.pathname = "/v1/ws";
755
+ url.search = "";
756
+ url.hash = "";
757
+ return url.toString();
758
+ }
759
+ function resolveRoutingFeeBps(config) {
760
+ return config.relayRegistry?.routingFeeBps ?? Math.max(0, Math.round(config.fees.basePercentage * 100));
761
+ }
762
+ function relayStatusToText(status) {
763
+ switch (status) {
764
+ case RelayNodeStatus.INACTIVE:
765
+ return "INACTIVE";
766
+ case RelayNodeStatus.SLASHED:
767
+ return "SLASHED";
768
+ default:
769
+ return "ACTIVE";
770
+ }
771
+ }
772
+
773
+ // src/routing/message-types.ts
774
+ function normalizeCapability(capability) {
775
+ return capability.trim().toLowerCase();
776
+ }
777
+ function createAuthPayload(message) {
778
+ const capabilities = [...new Set(message.capabilities.map(normalizeCapability))].sort().join(",");
779
+ return `mesh-relay-auth|${message.did}|${message.nonce}|${capabilities}`;
780
+ }
781
+ function serializeRelayMessage(message) {
782
+ return JSON.stringify(message);
783
+ }
784
+ function parseProviderMessage(payload) {
785
+ const raw = typeof payload === "string" ? payload : payload instanceof ArrayBuffer ? Buffer.from(payload).toString("utf8") : Array.isArray(payload) ? Buffer.concat(payload).toString("utf8") : payload.toString("utf8");
786
+ try {
787
+ const parsed = JSON.parse(raw);
788
+ if (!isRecord(parsed) || typeof parsed.type !== "string") {
789
+ return null;
790
+ }
791
+ switch (parsed.type) {
792
+ case "auth":
793
+ return typeof parsed.did === "string" && typeof parsed.nonce === "string" && typeof parsed.signature === "string" && Array.isArray(parsed.capabilities) && parsed.capabilities.every((entry) => typeof entry === "string") ? {
794
+ type: "auth",
795
+ did: parsed.did,
796
+ nonce: parsed.nonce,
797
+ signature: parsed.signature,
798
+ capabilities: parsed.capabilities
799
+ } : null;
800
+ case "heartbeat":
801
+ return typeof parsed.sessionId === "string" ? { type: "heartbeat", sessionId: parsed.sessionId } : null;
802
+ case "task_result":
803
+ return typeof parsed.sessionId === "string" && typeof parsed.taskId === "string" && isPositiveInteger(parsed.sequence) ? { type: "task_result", sessionId: parsed.sessionId, taskId: parsed.taskId, sequence: parsed.sequence, result: parsed.result } : null;
804
+ case "task_progress":
805
+ return typeof parsed.sessionId === "string" && typeof parsed.taskId === "string" && isPositiveInteger(parsed.sequence) && isValidProgress(parsed.progress) && (parsed.message === void 0 || typeof parsed.message === "string") ? {
806
+ type: "task_progress",
807
+ sessionId: parsed.sessionId,
808
+ taskId: parsed.taskId,
809
+ sequence: parsed.sequence,
810
+ progress: parsed.progress,
811
+ message: parsed.message
812
+ } : null;
813
+ case "task_chunk":
814
+ return typeof parsed.sessionId === "string" && typeof parsed.taskId === "string" && isPositiveInteger(parsed.sequence) && typeof parsed.data === "string" ? { type: "task_chunk", sessionId: parsed.sessionId, taskId: parsed.taskId, sequence: parsed.sequence, data: parsed.data } : null;
815
+ case "task_error":
816
+ return typeof parsed.sessionId === "string" && typeof parsed.taskId === "string" && isPositiveInteger(parsed.sequence) && isRecord(parsed.error) && typeof parsed.error.code === "string" && typeof parsed.error.message === "string" ? {
817
+ type: "task_error",
818
+ sessionId: parsed.sessionId,
819
+ taskId: parsed.taskId,
820
+ sequence: parsed.sequence,
821
+ error: {
822
+ code: parsed.error.code,
823
+ message: parsed.error.message
824
+ }
825
+ } : null;
826
+ default:
827
+ return null;
828
+ }
829
+ } catch {
830
+ return null;
831
+ }
832
+ }
833
+ function parseRelayMessage(payload) {
834
+ const raw = typeof payload === "string" ? payload : payload instanceof ArrayBuffer ? Buffer.from(payload).toString("utf8") : Array.isArray(payload) ? Buffer.concat(payload).toString("utf8") : payload.toString("utf8");
835
+ try {
836
+ const parsed = JSON.parse(raw);
837
+ if (!isRecord(parsed) || typeof parsed.type !== "string") {
838
+ return null;
839
+ }
840
+ switch (parsed.type) {
841
+ case "auth_ok":
842
+ return typeof parsed.sessionId === "string" && typeof parsed.relayDid === "string" ? { type: "auth_ok", sessionId: parsed.sessionId, relayDid: parsed.relayDid } : null;
843
+ case "auth_fail":
844
+ return typeof parsed.reason === "string" ? { type: "auth_fail", reason: parsed.reason } : null;
845
+ case "heartbeat_ack":
846
+ return { type: "heartbeat_ack" };
847
+ case "task_request":
848
+ return typeof parsed.sessionId === "string" && typeof parsed.taskId === "string" && typeof parsed.capability === "string" && typeof parsed.requesterDid === "string" && isPositiveInteger(parsed.sequence) ? {
849
+ type: "task_request",
850
+ sessionId: parsed.sessionId,
851
+ taskId: parsed.taskId,
852
+ capability: parsed.capability,
853
+ input: parsed.input,
854
+ requesterDid: parsed.requesterDid,
855
+ sequence: parsed.sequence
856
+ } : null;
857
+ default:
858
+ return null;
859
+ }
860
+ } catch {
861
+ return null;
862
+ }
863
+ }
864
+ function isRecord(value) {
865
+ return typeof value === "object" && value !== null && !Array.isArray(value);
866
+ }
867
+ function isPositiveInteger(value) {
868
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
869
+ }
870
+ function isValidProgress(value) {
871
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
872
+ }
873
+
874
+ // src/routing/router.ts
875
+ import { randomUUID as randomUUID3 } from "crypto";
876
+
877
+ // src/routing/session-manager.ts
878
+ import { randomUUID as randomUUID2 } from "crypto";
879
+ import { EventEmitter } from "events";
880
+ import { parseDID, verify } from "@hivemind-os/collective-core";
881
+ var encoder = new TextEncoder();
882
+ var SessionManager = class extends EventEmitter {
883
+ constructor(options) {
884
+ super();
885
+ this.options = options;
886
+ this.now = options.now ?? (() => Date.now());
887
+ }
888
+ options;
889
+ sessions = /* @__PURE__ */ new Map();
890
+ sessionsByDid = /* @__PURE__ */ new Map();
891
+ capabilityIndex = /* @__PURE__ */ new Map();
892
+ inboundSequences = /* @__PURE__ */ new Map();
893
+ capabilityCursor = /* @__PURE__ */ new Map();
894
+ recentAuthNonces = /* @__PURE__ */ new Map();
895
+ now;
896
+ registerSession(ws, authMessage) {
897
+ if (this.sessions.size >= this.options.maxConnections) {
898
+ throw new Error("Relay connection limit reached.");
899
+ }
900
+ this.pruneRecentAuthNonces();
901
+ if (this.getSessionByDid(authMessage.did)) {
902
+ throw new Error(`Provider ${authMessage.did} is already connected to the relay.`);
903
+ }
904
+ this.verifyAuthMessage(authMessage);
905
+ this.markAuthNonceUsed(authMessage);
906
+ const sessionId = randomUUID2();
907
+ const timestamp = this.now();
908
+ const normalizedCapabilities = [...new Set(authMessage.capabilities.map(normalizeCapability))];
909
+ const session = {
910
+ sessionId,
911
+ providerDid: authMessage.did,
912
+ ws,
913
+ capabilities: [...normalizedCapabilities],
914
+ normalizedCapabilities,
915
+ connectedAt: timestamp,
916
+ lastHeartbeat: timestamp,
917
+ sequenceCounter: 0
918
+ };
919
+ this.sessions.set(sessionId, session);
920
+ this.addDidIndex(session.providerDid, sessionId);
921
+ this.addCapabilities(sessionId, normalizedCapabilities);
922
+ const cleanup = () => {
923
+ this.removeSession(sessionId);
924
+ };
925
+ ws.once("close", cleanup);
926
+ ws.once("error", cleanup);
927
+ this.emit("session_registered", this.toProviderInfo(session));
928
+ return session;
929
+ }
930
+ findProvider(capability, preferredDid) {
931
+ const normalizedCapability = normalizeCapability(capability);
932
+ if (preferredDid) {
933
+ return this.getSessionByDid(preferredDid, normalizedCapability);
934
+ }
935
+ const ids = [...this.capabilityIndex.get(normalizedCapability) ?? []];
936
+ if (ids.length === 0) {
937
+ return null;
938
+ }
939
+ const cursor = this.capabilityCursor.get(normalizedCapability) ?? 0;
940
+ const sessionId = ids[cursor % ids.length];
941
+ this.capabilityCursor.set(normalizedCapability, cursor + 1);
942
+ return sessionId ? this.sessions.get(sessionId) ?? null : null;
943
+ }
944
+ removeSession(sessionId) {
945
+ const session = this.sessions.get(sessionId);
946
+ if (!session) {
947
+ return;
948
+ }
949
+ this.sessions.delete(sessionId);
950
+ this.inboundSequences.delete(sessionId);
951
+ this.removeDidIndex(session.providerDid, sessionId);
952
+ this.removeCapabilities(sessionId, session.normalizedCapabilities);
953
+ this.emit("session_removed", this.toProviderInfo(session));
954
+ }
955
+ disconnectSession(sessionId, code = 4001, reason = "Session expired") {
956
+ const session = this.sessions.get(sessionId);
957
+ if (!session) {
958
+ return;
959
+ }
960
+ try {
961
+ session.ws.close(code, reason);
962
+ } catch {
963
+ this.removeSession(sessionId);
964
+ }
965
+ }
966
+ disconnectAllSessions(code = 1012, reason = "Relay shutting down") {
967
+ for (const sessionId of [...this.sessions.keys()]) {
968
+ this.disconnectSession(sessionId, code, reason);
969
+ }
970
+ }
971
+ handleHeartbeat(sessionId) {
972
+ const session = this.sessions.get(sessionId);
973
+ if (!session) {
974
+ return;
975
+ }
976
+ session.lastHeartbeat = this.now();
977
+ }
978
+ sweepExpiredSessions() {
979
+ this.pruneRecentAuthNonces();
980
+ const now = this.now();
981
+ const expired = [...this.sessions.values()].filter((session) => now - session.lastHeartbeat > this.options.heartbeatTimeoutMs).map((session) => session.sessionId);
982
+ for (const sessionId of expired) {
983
+ this.disconnectSession(sessionId, 4002, "Heartbeat timeout");
984
+ }
985
+ return expired;
986
+ }
987
+ getConnectedProviders() {
988
+ return [...this.sessions.values()].map((session) => this.toProviderInfo(session));
989
+ }
990
+ getSession(sessionId) {
991
+ return this.sessions.get(sessionId) ?? null;
992
+ }
993
+ getSessionByDid(did, capability) {
994
+ const sessionIds = [...this.sessionsByDid.get(did) ?? []];
995
+ if (sessionIds.length === 0) {
996
+ return null;
997
+ }
998
+ const normalizedCapability = capability ? normalizeCapability(capability) : void 0;
999
+ for (const sessionId of sessionIds) {
1000
+ const session = this.sessions.get(sessionId);
1001
+ if (!session) {
1002
+ continue;
1003
+ }
1004
+ if (!normalizedCapability || session.normalizedCapabilities.includes(normalizedCapability)) {
1005
+ return session;
1006
+ }
1007
+ }
1008
+ return null;
1009
+ }
1010
+ getSessionCount() {
1011
+ return this.sessions.size;
1012
+ }
1013
+ nextSequence(sessionId) {
1014
+ const session = this.sessions.get(sessionId);
1015
+ if (!session) {
1016
+ throw new Error(`Unknown relay session: ${sessionId}`);
1017
+ }
1018
+ session.sequenceCounter += 1;
1019
+ return session.sequenceCounter;
1020
+ }
1021
+ validateIncomingSequence(sessionId, sequence) {
1022
+ const previous = this.inboundSequences.get(sessionId) ?? 0;
1023
+ if (!Number.isInteger(sequence) || sequence <= previous) {
1024
+ return false;
1025
+ }
1026
+ this.inboundSequences.set(sessionId, sequence);
1027
+ return true;
1028
+ }
1029
+ verifyAuthMessage(authMessage) {
1030
+ if (this.recentAuthNonces.has(this.getAuthNonceKey(authMessage))) {
1031
+ throw new Error("Authentication nonce has already been used.");
1032
+ }
1033
+ try {
1034
+ const payload = createAuthPayload(authMessage);
1035
+ const signature = decodeHex(authMessage.signature);
1036
+ const publicKey = parseDID(authMessage.did).publicKey;
1037
+ if (!verify(encoder.encode(payload), signature, publicKey)) {
1038
+ throw new Error("Invalid provider authentication signature.");
1039
+ }
1040
+ } catch (error) {
1041
+ if (error instanceof Error && error.message === "Authentication nonce has already been used.") {
1042
+ throw error;
1043
+ }
1044
+ throw new Error("Invalid provider authentication signature.");
1045
+ }
1046
+ }
1047
+ markAuthNonceUsed(authMessage) {
1048
+ this.recentAuthNonces.set(this.getAuthNonceKey(authMessage), this.now() + this.getAuthNonceTtlMs());
1049
+ }
1050
+ pruneRecentAuthNonces() {
1051
+ const now = this.now();
1052
+ for (const [nonceKey, expiresAt] of this.recentAuthNonces.entries()) {
1053
+ if (expiresAt > now) {
1054
+ continue;
1055
+ }
1056
+ this.recentAuthNonces.delete(nonceKey);
1057
+ }
1058
+ }
1059
+ getAuthNonceKey(authMessage) {
1060
+ return `${authMessage.did}:${authMessage.nonce}`;
1061
+ }
1062
+ getAuthNonceTtlMs() {
1063
+ return this.options.authNonceTtlMs ?? 5 * 6e4;
1064
+ }
1065
+ addDidIndex(did, sessionId) {
1066
+ const ids = this.sessionsByDid.get(did) ?? /* @__PURE__ */ new Set();
1067
+ ids.add(sessionId);
1068
+ this.sessionsByDid.set(did, ids);
1069
+ }
1070
+ removeDidIndex(did, sessionId) {
1071
+ const ids = this.sessionsByDid.get(did);
1072
+ if (!ids) {
1073
+ return;
1074
+ }
1075
+ ids.delete(sessionId);
1076
+ if (ids.size === 0) {
1077
+ this.sessionsByDid.delete(did);
1078
+ }
1079
+ }
1080
+ addCapabilities(sessionId, capabilities) {
1081
+ for (const capability of capabilities) {
1082
+ const sessions = this.capabilityIndex.get(capability) ?? /* @__PURE__ */ new Set();
1083
+ sessions.add(sessionId);
1084
+ this.capabilityIndex.set(capability, sessions);
1085
+ }
1086
+ }
1087
+ removeCapabilities(sessionId, capabilities) {
1088
+ for (const capability of capabilities) {
1089
+ const sessions = this.capabilityIndex.get(capability);
1090
+ if (!sessions) {
1091
+ continue;
1092
+ }
1093
+ sessions.delete(sessionId);
1094
+ if (sessions.size === 0) {
1095
+ this.capabilityIndex.delete(capability);
1096
+ this.capabilityCursor.delete(capability);
1097
+ }
1098
+ }
1099
+ }
1100
+ toProviderInfo(session) {
1101
+ return {
1102
+ sessionId: session.sessionId,
1103
+ providerDid: session.providerDid,
1104
+ capabilities: [...session.capabilities],
1105
+ connectedAt: session.connectedAt,
1106
+ lastHeartbeat: session.lastHeartbeat
1107
+ };
1108
+ }
1109
+ };
1110
+ function decodeHex(value) {
1111
+ if (!/^[0-9a-f]+$/i.test(value) || value.length % 2 !== 0) {
1112
+ throw new Error("Authentication signature must be a hex string.");
1113
+ }
1114
+ return Uint8Array.from(Buffer.from(value, "hex"));
1115
+ }
1116
+
1117
+ // src/routing/router.ts
1118
+ var RelayRouteError = class extends Error {
1119
+ constructor(code, message, statusCode, retryable = false) {
1120
+ super(message);
1121
+ this.code = code;
1122
+ this.statusCode = statusCode;
1123
+ this.retryable = retryable;
1124
+ this.name = "RelayRouteError";
1125
+ }
1126
+ code;
1127
+ statusCode;
1128
+ retryable;
1129
+ };
1130
+ var RelayRouter = class {
1131
+ constructor(options) {
1132
+ this.options = options;
1133
+ this.options.sessionManager.on("session_removed", (session) => {
1134
+ this.rejectSessionTasks(session.sessionId, new RelayRouteError("PROVIDER_DISCONNECTED", "Provider disconnected.", 503, true));
1135
+ });
1136
+ }
1137
+ options;
1138
+ pendingTasks = /* @__PURE__ */ new Map();
1139
+ async routeTask(request) {
1140
+ return this.dispatch(request);
1141
+ }
1142
+ async routeStreamingTask(request, onChunk) {
1143
+ return this.dispatch(request, onChunk);
1144
+ }
1145
+ async routeMulti(request, providerDids) {
1146
+ return await Promise.all(
1147
+ providerDids.map((providerDid, index) => this.dispatch({
1148
+ ...request,
1149
+ providerDid,
1150
+ taskId: request.taskId ? `${request.taskId}-${index + 1}` : void 0
1151
+ }))
1152
+ );
1153
+ }
1154
+ close() {
1155
+ for (const taskId of [...this.pendingTasks.keys()]) {
1156
+ this.completeTask(
1157
+ taskId,
1158
+ void 0,
1159
+ new RelayRouteError("RELAY_SHUTDOWN", "Relay is shutting down.", 503, true)
1160
+ );
1161
+ }
1162
+ }
1163
+ handleProviderMessage(sessionId, message) {
1164
+ if (message.sessionId !== sessionId) {
1165
+ throw new RelayRouteError("SESSION_MISMATCH", "Relay session id did not match the authenticated connection.", 409);
1166
+ }
1167
+ if (!this.options.sessionManager.validateIncomingSequence(sessionId, message.sequence)) {
1168
+ throw new RelayRouteError("REPLAY_DETECTED", "Out-of-order relay message rejected.", 409);
1169
+ }
1170
+ const session = this.options.sessionManager.getSession(sessionId);
1171
+ if (!session) {
1172
+ throw new RelayRouteError("SESSION_NOT_FOUND", "Relay session was not found.", 404);
1173
+ }
1174
+ const pending = this.pendingTasks.get(message.taskId);
1175
+ if (!pending || pending.sessionId !== sessionId) {
1176
+ return;
1177
+ }
1178
+ switch (message.type) {
1179
+ case "task_progress":
1180
+ this.emitTaskEvent(message.taskId, {
1181
+ type: "progress",
1182
+ taskId: message.taskId,
1183
+ sequence: message.sequence,
1184
+ progress: message.progress,
1185
+ message: message.message
1186
+ });
1187
+ break;
1188
+ case "task_chunk":
1189
+ this.emitTaskEvent(message.taskId, {
1190
+ type: "chunk",
1191
+ taskId: message.taskId,
1192
+ sequence: message.sequence,
1193
+ data: message.data
1194
+ });
1195
+ break;
1196
+ case "task_error":
1197
+ this.completeTask(
1198
+ message.taskId,
1199
+ void 0,
1200
+ new RelayRouteError(message.error.code, message.error.message, 502, true)
1201
+ );
1202
+ break;
1203
+ case "task_result":
1204
+ if (!this.emitTaskEvent(message.taskId, {
1205
+ type: "result",
1206
+ taskId: message.taskId,
1207
+ sequence: message.sequence,
1208
+ result: message.result
1209
+ })) {
1210
+ return;
1211
+ }
1212
+ this.completeTask(message.taskId, {
1213
+ taskId: message.taskId,
1214
+ providerDid: session.providerDid,
1215
+ sequence: message.sequence,
1216
+ result: message.result
1217
+ });
1218
+ break;
1219
+ }
1220
+ }
1221
+ async dispatch(request, onChunk) {
1222
+ const session = this.findSession(request.capability, request.providerDid);
1223
+ const taskId = request.taskId ?? randomUUID3();
1224
+ const sequence = this.options.sessionManager.nextSequence(session.sessionId);
1225
+ const relayMessage = {
1226
+ type: "task_request",
1227
+ sessionId: session.sessionId,
1228
+ taskId,
1229
+ capability: request.capability,
1230
+ input: request.input,
1231
+ requesterDid: request.requesterDid,
1232
+ sequence
1233
+ };
1234
+ const response = new Promise((resolve3, reject) => {
1235
+ const timeoutMs = request.timeoutMs ?? this.options.taskTimeoutMs;
1236
+ const timer = setTimeout(() => {
1237
+ this.pendingTasks.delete(taskId);
1238
+ reject(new RelayRouteError("TASK_TIMEOUT", `Task ${taskId} timed out.`, 504, true));
1239
+ }, timeoutMs);
1240
+ this.pendingTasks.set(taskId, {
1241
+ taskId,
1242
+ sessionId: session.sessionId,
1243
+ onChunk,
1244
+ resolve: resolve3,
1245
+ reject,
1246
+ timer
1247
+ });
1248
+ });
1249
+ try {
1250
+ await sendWebSocketMessage(session.ws, serializeRelayMessage(relayMessage));
1251
+ return await response;
1252
+ } catch (error) {
1253
+ this.completeTask(taskId, void 0, toRouteError(error));
1254
+ throw toRouteError(error);
1255
+ }
1256
+ }
1257
+ emitTaskEvent(taskId, event) {
1258
+ const pending = this.pendingTasks.get(taskId);
1259
+ if (!pending?.onChunk) {
1260
+ return true;
1261
+ }
1262
+ try {
1263
+ pending.onChunk(event);
1264
+ return true;
1265
+ } catch {
1266
+ this.completeTask(
1267
+ taskId,
1268
+ void 0,
1269
+ new RelayRouteError("STREAM_DELIVERY_FAILED", "Streaming consumer disconnected before relay delivery completed.", 499, true)
1270
+ );
1271
+ return false;
1272
+ }
1273
+ }
1274
+ completeTask(taskId, response, error) {
1275
+ const pending = this.pendingTasks.get(taskId);
1276
+ if (!pending) {
1277
+ return;
1278
+ }
1279
+ clearTimeout(pending.timer);
1280
+ this.pendingTasks.delete(taskId);
1281
+ if (error) {
1282
+ pending.reject(error);
1283
+ return;
1284
+ }
1285
+ if (!response) {
1286
+ pending.reject(new RelayRouteError("TASK_FAILED", `Task ${taskId} failed.`, 500));
1287
+ return;
1288
+ }
1289
+ pending.resolve(response);
1290
+ }
1291
+ rejectSessionTasks(sessionId, error) {
1292
+ for (const [taskId, pending] of this.pendingTasks.entries()) {
1293
+ if (pending.sessionId !== sessionId) {
1294
+ continue;
1295
+ }
1296
+ this.completeTask(taskId, void 0, error);
1297
+ }
1298
+ }
1299
+ findSession(capability, providerDid) {
1300
+ const session = this.options.sessionManager.findProvider(capability, providerDid);
1301
+ if (!session) {
1302
+ throw new RelayRouteError("PROVIDER_NOT_FOUND", `No provider is connected for capability ${capability}.`, 404, true);
1303
+ }
1304
+ return session;
1305
+ }
1306
+ };
1307
+ async function sendWebSocketMessage(ws, message) {
1308
+ if ("readyState" in ws && typeof ws.readyState === "number" && ws.readyState !== 1) {
1309
+ throw new RelayRouteError("PROVIDER_UNAVAILABLE", "Provider WebSocket is not open.", 503, true);
1310
+ }
1311
+ await new Promise((resolve3, reject) => {
1312
+ try {
1313
+ ws.send(message, (error) => {
1314
+ if (error) {
1315
+ reject(error);
1316
+ return;
1317
+ }
1318
+ resolve3();
1319
+ });
1320
+ } catch (error) {
1321
+ reject(error);
1322
+ }
1323
+ });
1324
+ }
1325
+ function toRouteError(error) {
1326
+ if (error instanceof RelayRouteError) {
1327
+ return error;
1328
+ }
1329
+ return new RelayRouteError("ROUTING_FAILED", error instanceof Error ? error.message : "Relay routing failed.", 502, true);
1330
+ }
1331
+
1332
+ // src/server/middleware.ts
1333
+ import { randomUUID as randomUUID4 } from "crypto";
1334
+ import { PaymentRail as PaymentRail2 } from "@hivemind-os/collective-types";
1335
+ import { isValidDID } from "@hivemind-os/collective-core";
1336
+ function registerMeshMiddleware(app, relayDid) {
1337
+ app.addHook("onRequest", async (request, reply) => {
1338
+ const requesterDid = request.headers["x-mesh-requester"];
1339
+ const targetProvider = request.headers["x-mesh-target-provider"];
1340
+ if (typeof requesterDid === "string" && requesterDid.length > 0 && !isValidDID(requesterDid)) {
1341
+ reply.code(400).send({
1342
+ error: {
1343
+ code: "INVALID_REQUESTER_DID",
1344
+ message: "X-Mesh-Requester must be a valid did:mesh identifier."
1345
+ }
1346
+ });
1347
+ return;
1348
+ }
1349
+ if (typeof targetProvider === "string" && targetProvider.length > 0 && !isValidDID(targetProvider)) {
1350
+ reply.code(400).send({
1351
+ error: {
1352
+ code: "INVALID_TARGET_PROVIDER_DID",
1353
+ message: "X-Mesh-Target-Provider must be a valid did:mesh identifier."
1354
+ }
1355
+ });
1356
+ return;
1357
+ }
1358
+ reply.header("X-Mesh-Relay", relayDid);
1359
+ });
1360
+ }
1361
+ function getMeshRequestContext(request) {
1362
+ return {
1363
+ requestId: readHeader(request, "x-mesh-request-id") ?? randomUUID4(),
1364
+ requesterDid: readHeader(request, "x-mesh-requester"),
1365
+ targetProviderDid: readHeader(request, "x-mesh-target-provider"),
1366
+ timestamp: readHeader(request, "x-mesh-timestamp"),
1367
+ signature: readHeader(request, "x-mesh-signature"),
1368
+ paymentSignature: readHeader(request, "payment-signature") ?? readHeader(request, "x-payment-signature"),
1369
+ paymentNonce: readHeader(request, "x-mesh-payment-nonce"),
1370
+ paymentRail: normalizePaymentRail(readHeader(request, "x-mesh-payment-rail")),
1371
+ sessionId: readHeader(request, "x-mesh-session-id"),
1372
+ sequence: readPositiveInteger(readHeader(request, "x-mesh-sequence"))
1373
+ };
1374
+ }
1375
+ function applyMeshResponseHeaders(reply, relayDid, requestId) {
1376
+ reply.header("X-Mesh-Relay", relayDid);
1377
+ reply.header("X-Mesh-Request-Id", requestId);
1378
+ }
1379
+ function readHeader(request, name) {
1380
+ const value = request.headers[name];
1381
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1382
+ }
1383
+ function readPositiveInteger(value) {
1384
+ if (!value || !/^\d+$/.test(value)) {
1385
+ return void 0;
1386
+ }
1387
+ return Number(value);
1388
+ }
1389
+ function normalizePaymentRail(value) {
1390
+ if (value === PaymentRail2.SUI_ESCROW || value === PaymentRail2.SUI_TRANSFER || value === PaymentRail2.X402_BASE) {
1391
+ return value;
1392
+ }
1393
+ return void 0;
1394
+ }
1395
+
1396
+ // src/server/routes.ts
1397
+ import { encodePaymentResponseHeader as encodePaymentResponseHeader2 } from "@x402/core/http";
1398
+ import { PaymentRail as PaymentRail3 } from "@hivemind-os/collective-types";
1399
+ var CONSUMER_SEQUENCE_TTL_MS = 60 * 6e4;
1400
+ async function registerRelayRoutes(app, deps) {
1401
+ const consumerSequences = /* @__PURE__ */ new Map();
1402
+ app.get("/health", async () => {
1403
+ const status = deps.healthMonitor.getStatus();
1404
+ return {
1405
+ status: status.status === "healthy" ? "ok" : status.status,
1406
+ relayStatus: status.status,
1407
+ uptime: status.uptime,
1408
+ connectedProviders: status.connectedProviders,
1409
+ activeRequests: status.activeRequests,
1410
+ totalRequestsServed: status.totalRequestsServed,
1411
+ averageLatencyMs: status.averageLatencyMs,
1412
+ relayRegistry: deps.relayRegistry?.getInfo() ?? { enabled: false, registered: false }
1413
+ };
1414
+ });
1415
+ app.get("/info", async () => ({
1416
+ relayDid: deps.relayDid,
1417
+ version: deps.version,
1418
+ feeSchedule: {
1419
+ basePercentage: deps.config.fees.basePercentage,
1420
+ minimumMist: deps.config.fees.minimumMist.toString()
1421
+ },
1422
+ capabilities: [...new Set(deps.sessionManager.getConnectedProviders().flatMap((provider) => provider.capabilities))].sort(),
1423
+ relayRegistry: deps.relayRegistry?.getInfo() ?? { enabled: false, registered: false }
1424
+ }));
1425
+ app.post("/mesh/providers/:providerDid/capabilities/:capability/execute", async (request, reply) => {
1426
+ const tracker = deps.healthMonitor.beginRequest();
1427
+ const context = getMeshRequestContext(request);
1428
+ applyMeshResponseHeaders(reply, deps.relayDid, context.requestId);
1429
+ try {
1430
+ const params = request.params;
1431
+ const sequenceError = validateConsumerSequence(consumerSequences, context, Date.now());
1432
+ if (sequenceError) {
1433
+ reply.code(sequenceError.statusCode).send(toError(sequenceError.code, sequenceError.message, context.requestId));
1434
+ return;
1435
+ }
1436
+ if (!context.requesterDid) {
1437
+ reply.code(400).send(toError("REQUESTER_REQUIRED", "X-Mesh-Requester header is required.", context.requestId));
1438
+ return;
1439
+ }
1440
+ const provider = deps.sessionManager.findProvider(params.capability, params.providerDid);
1441
+ if (!provider) {
1442
+ reply.code(404).send(
1443
+ toError("PROVIDER_NOT_FOUND", `Provider ${params.providerDid} is not connected to the relay.`, context.requestId)
1444
+ );
1445
+ return;
1446
+ }
1447
+ const paymentRail = context.paymentRail ?? defaultPaymentRailForProvider(provider, deps.paymentGate);
1448
+ if (!context.paymentSignature) {
1449
+ const challenge2 = deps.paymentGate.generate402Challenge(paymentRail, params.capability, provider);
1450
+ reply.code(402).header("PAYMENT-REQUIRED", challenge2.extra?.["payment-required"] ?? "").send({
1451
+ ...toError("PAYMENT_REQUIRED", "Payment is required before relay execution.", context.requestId),
1452
+ payment: challenge2
1453
+ });
1454
+ return;
1455
+ }
1456
+ const challenge = context.paymentNonce ? deps.paymentGate.getChallenge(context.paymentNonce) : null;
1457
+ if (!challenge) {
1458
+ reply.code(400).send(
1459
+ toError("PAYMENT_CHALLENGE_REQUIRED", "X-Mesh-Payment-Nonce must reference an active challenge.", context.requestId)
1460
+ );
1461
+ return;
1462
+ }
1463
+ const verification = await deps.paymentGate.verifyPayment(context.paymentSignature, challenge);
1464
+ if (!verification.accepted) {
1465
+ reply.code(402).send({
1466
+ ...toError("PAYMENT_REJECTED", verification.reason ?? "Payment verification failed.", context.requestId),
1467
+ payment: challenge
1468
+ });
1469
+ return;
1470
+ }
1471
+ if (!deps.sessionManager.getSession(provider.sessionId)) {
1472
+ reply.code(503).send(
1473
+ toError(
1474
+ "PROVIDER_UNAVAILABLE",
1475
+ `Provider ${params.providerDid} disconnected before the relay could start the task. No funds were settled.`,
1476
+ context.requestId,
1477
+ true
1478
+ )
1479
+ );
1480
+ return;
1481
+ }
1482
+ if (isStreamingRequest(request)) {
1483
+ reply.hijack();
1484
+ reply.raw.writeHead(200, {
1485
+ "content-type": "text/event-stream; charset=utf-8",
1486
+ "cache-control": "no-cache, no-transform",
1487
+ connection: "keep-alive",
1488
+ "x-mesh-provider": provider.providerDid,
1489
+ "x-mesh-request-id": context.requestId,
1490
+ "x-mesh-relay": deps.relayDid,
1491
+ "payment-response": verification.settlementReference ?? ""
1492
+ });
1493
+ try {
1494
+ const routed = await deps.router.routeStreamingTask(
1495
+ {
1496
+ requesterDid: context.requesterDid,
1497
+ providerDid: params.providerDid,
1498
+ capability: params.capability,
1499
+ input: request.body,
1500
+ timeoutMs: deps.config.limits.taskTimeoutMs
1501
+ },
1502
+ (event) => {
1503
+ if (event.type === "result") {
1504
+ return;
1505
+ }
1506
+ writeSse(reply.raw, event.type, event);
1507
+ }
1508
+ );
1509
+ await deps.relayRegistry?.recordRouting(BigInt(challenge.relayFee));
1510
+ writeSse(reply.raw, "result", {
1511
+ taskId: routed.taskId,
1512
+ providerDid: routed.providerDid,
1513
+ result: routed.result,
1514
+ paymentReceipt: verification.settlementReference,
1515
+ relayFeeMist: challenge.relayFee
1516
+ });
1517
+ } catch (error) {
1518
+ const routeError = error instanceof RelayRouteError ? error : new RelayRouteError("ROUTE_FAILED", "Relay routing failed.", 502, true);
1519
+ writeSse(reply.raw, "error", { code: routeError.code, message: routeError.message });
1520
+ } finally {
1521
+ reply.raw.end();
1522
+ }
1523
+ return;
1524
+ }
1525
+ const response = await deps.router.routeTask({
1526
+ requesterDid: context.requesterDid,
1527
+ providerDid: params.providerDid,
1528
+ capability: params.capability,
1529
+ input: request.body,
1530
+ timeoutMs: deps.config.limits.taskTimeoutMs
1531
+ });
1532
+ await deps.relayRegistry?.recordRouting(BigInt(challenge.relayFee));
1533
+ reply.code(200).header("X-Mesh-Provider", response.providerDid).header("X-Mesh-Response-Id", response.taskId).header("X-Mesh-Relay-Fee", challenge.relayFee).header(
1534
+ "PAYMENT-RESPONSE",
1535
+ verification.settlementReference ?? encodePaymentResponseHeader2({
1536
+ success: true,
1537
+ transaction: `relay-${response.taskId}`,
1538
+ network: toSettlementNetwork(challenge.network),
1539
+ amount: challenge.amount,
1540
+ payer: verification.payer
1541
+ })
1542
+ ).send(response.result);
1543
+ } catch (error) {
1544
+ const routeError = error instanceof RelayRouteError ? error : new RelayRouteError("ROUTE_FAILED", "Relay routing failed.", 502, true);
1545
+ reply.code(routeError.statusCode).send(toError(routeError.code, routeError.message, context.requestId, routeError.retryable));
1546
+ } finally {
1547
+ tracker.finish();
1548
+ }
1549
+ });
1550
+ }
1551
+ function validateConsumerSequence(sequences, context, now) {
1552
+ pruneConsumerSequences(sequences, now);
1553
+ if (!context.sessionId && context.sequence === void 0) {
1554
+ return null;
1555
+ }
1556
+ if (!context.sessionId || context.sequence === void 0) {
1557
+ return {
1558
+ code: "SEQUENCE_REQUIRED",
1559
+ message: "X-Mesh-Session-Id and X-Mesh-Sequence must be provided together.",
1560
+ statusCode: 400
1561
+ };
1562
+ }
1563
+ const previous = sequences.get(context.sessionId)?.sequence ?? 0;
1564
+ if (context.sequence <= previous) {
1565
+ return {
1566
+ code: "REPLAY_DETECTED",
1567
+ message: "Relay rejected a replayed or out-of-order consumer request.",
1568
+ statusCode: 409
1569
+ };
1570
+ }
1571
+ sequences.set(context.sessionId, { sequence: context.sequence, updatedAt: now });
1572
+ return null;
1573
+ }
1574
+ function pruneConsumerSequences(sequences, now) {
1575
+ for (const [sessionId, state] of sequences.entries()) {
1576
+ if (now - state.updatedAt <= CONSUMER_SEQUENCE_TTL_MS) {
1577
+ continue;
1578
+ }
1579
+ sequences.delete(sessionId);
1580
+ }
1581
+ }
1582
+ function defaultPaymentRailForProvider(provider, paymentGate) {
1583
+ return paymentGate.defaultRail ?? PaymentRail3.SUI_TRANSFER;
1584
+ }
1585
+ function isStreamingRequest(request) {
1586
+ const accept = typeof request.headers.accept === "string" ? request.headers.accept : "";
1587
+ if (accept.includes("text/event-stream")) {
1588
+ return true;
1589
+ }
1590
+ if (typeof request.query !== "object" || request.query === null) {
1591
+ return false;
1592
+ }
1593
+ const query = request.query;
1594
+ return query.stream === "1" || query.stream === "true";
1595
+ }
1596
+ function writeSse(stream, event, data) {
1597
+ stream.write(`event: ${event}
1598
+ `);
1599
+ stream.write(`data: ${JSON.stringify(data)}
1600
+
1601
+ `);
1602
+ }
1603
+ function toError(code, message, requestId, retryable = false) {
1604
+ return {
1605
+ error: {
1606
+ code,
1607
+ message,
1608
+ details: {},
1609
+ retryable,
1610
+ retryAfterMs: retryable ? 1e3 : null,
1611
+ requestId
1612
+ }
1613
+ };
1614
+ }
1615
+ function toSettlementNetwork(network) {
1616
+ switch (network) {
1617
+ case "base":
1618
+ return "eip155:8453";
1619
+ case "base-sepolia":
1620
+ return "eip155:84532";
1621
+ case "sui-mainnet":
1622
+ return "sui:mainnet";
1623
+ case "sui-testnet":
1624
+ return "sui:testnet";
1625
+ case "sui-devnet":
1626
+ return "sui:devnet";
1627
+ default:
1628
+ return network.includes(":") ? network : "sui:testnet";
1629
+ }
1630
+ }
1631
+
1632
+ // src/server/http-server.ts
1633
+ var RELAY_VERSION = "0.1.0";
1634
+ async function createRelayServer(config, options = {}) {
1635
+ const app = Fastify({
1636
+ logger: {
1637
+ level: "info"
1638
+ }
1639
+ });
1640
+ const identity = RelayIdentity.load(config.identity.keyPath);
1641
+ const sessionManager = new SessionManager({
1642
+ maxConnections: config.limits.maxConnections,
1643
+ heartbeatTimeoutMs: config.limits.heartbeatTimeoutMs,
1644
+ authNonceTtlMs: config.limits.authNonceTtlMs
1645
+ });
1646
+ const router = new RelayRouter({
1647
+ sessionManager,
1648
+ taskTimeoutMs: config.limits.taskTimeoutMs
1649
+ });
1650
+ const paymentGate = new PaymentGate({
1651
+ relayDid: identity.did,
1652
+ feeSchedule: config.fees
1653
+ });
1654
+ const healthMonitor = new HealthMonitor({
1655
+ getConnectedProviders: () => sessionManager.getConnectedProviders().length
1656
+ });
1657
+ const relayRegistry = options.relayRegistry ?? new RelayRegistryService(config, identity);
1658
+ await app.register(cors, {
1659
+ origin: (origin, callback) => {
1660
+ const allowedOrigins = config.cors?.allowedOrigins ?? [];
1661
+ if (!origin) {
1662
+ callback(null, false);
1663
+ return;
1664
+ }
1665
+ callback(null, allowedOrigins.includes(origin));
1666
+ }
1667
+ });
1668
+ app.register(rateLimit, {
1669
+ max: config.limits.maxRequestsPerSecond,
1670
+ timeWindow: "1 second"
1671
+ });
1672
+ await app.register(websocket);
1673
+ registerMeshMiddleware(app, identity.did);
1674
+ await registerRelayRoutes(app, {
1675
+ relayDid: identity.did,
1676
+ version: RELAY_VERSION,
1677
+ config,
1678
+ sessionManager,
1679
+ router,
1680
+ paymentGate,
1681
+ healthMonitor,
1682
+ relayRegistry
1683
+ });
1684
+ app.get("/v1/ws", { websocket: true }, (socket) => {
1685
+ attachProviderSocket({
1686
+ ws: normalizeWebSocket(socket),
1687
+ identity,
1688
+ sessionManager,
1689
+ router
1690
+ });
1691
+ });
1692
+ const heartbeatSweep = setInterval(() => {
1693
+ sessionManager.sweepExpiredSessions();
1694
+ paymentGate.pruneExpiredChallenges();
1695
+ }, Math.max(1e3, Math.floor(config.limits.heartbeatIntervalMs / 2)));
1696
+ app.addHook("onClose", async () => {
1697
+ clearInterval(heartbeatSweep);
1698
+ await relayRegistry.stop();
1699
+ router.close();
1700
+ sessionManager.disconnectAllSessions();
1701
+ });
1702
+ await app.ready();
1703
+ return {
1704
+ app,
1705
+ config,
1706
+ identity,
1707
+ sessionManager,
1708
+ router,
1709
+ paymentGate,
1710
+ healthMonitor,
1711
+ relayRegistry,
1712
+ start: async () => {
1713
+ const address = await app.listen({ host: config.host, port: config.port });
1714
+ const resolvedAddress = typeof address === "string" ? address : `http://${config.host}:${config.port}`;
1715
+ await relayRegistry.start(resolvedAddress);
1716
+ return resolvedAddress;
1717
+ }
1718
+ };
1719
+ }
1720
+ function attachProviderSocket(params) {
1721
+ let authenticatedSessionId;
1722
+ const authTimeout = setTimeout(() => {
1723
+ if (authenticatedSessionId) {
1724
+ return;
1725
+ }
1726
+ safeSend(params.ws, serializeRelayMessage({ type: "auth_fail", reason: "Authentication timeout." }));
1727
+ safeClose(params.ws, 4003, "Authentication timeout");
1728
+ }, 5e3);
1729
+ params.ws.on("message", (payload) => {
1730
+ const message = parseProviderMessage(payload);
1731
+ if (!message) {
1732
+ clearTimeout(authTimeout);
1733
+ safeSend(params.ws, serializeRelayMessage({ type: "auth_fail", reason: "Invalid relay message." }));
1734
+ safeClose(params.ws, 4004, "Invalid relay message");
1735
+ return;
1736
+ }
1737
+ if (!authenticatedSessionId) {
1738
+ if (message.type !== "auth") {
1739
+ clearTimeout(authTimeout);
1740
+ safeSend(params.ws, serializeRelayMessage({ type: "auth_fail", reason: "Authentication required." }));
1741
+ safeClose(params.ws, 4005, "Authentication required");
1742
+ return;
1743
+ }
1744
+ try {
1745
+ const session = params.sessionManager.registerSession(params.ws, message);
1746
+ authenticatedSessionId = session.sessionId;
1747
+ clearTimeout(authTimeout);
1748
+ safeSend(
1749
+ params.ws,
1750
+ serializeRelayMessage({
1751
+ type: "auth_ok",
1752
+ sessionId: session.sessionId,
1753
+ relayDid: params.identity.did
1754
+ })
1755
+ );
1756
+ } catch (error) {
1757
+ clearTimeout(authTimeout);
1758
+ safeSend(
1759
+ params.ws,
1760
+ serializeRelayMessage({
1761
+ type: "auth_fail",
1762
+ reason: error instanceof Error ? error.message : "Authentication failed."
1763
+ })
1764
+ );
1765
+ safeClose(params.ws, 4006, "Authentication failed");
1766
+ }
1767
+ return;
1768
+ }
1769
+ if (message.type === "auth") {
1770
+ safeSend(params.ws, serializeRelayMessage({ type: "auth_fail", reason: "Session already authenticated." }));
1771
+ return;
1772
+ }
1773
+ if (message.type === "heartbeat") {
1774
+ if (message.sessionId !== authenticatedSessionId) {
1775
+ safeClose(params.ws, 4007, "Session mismatch");
1776
+ return;
1777
+ }
1778
+ params.sessionManager.handleHeartbeat(authenticatedSessionId);
1779
+ safeSend(params.ws, serializeRelayMessage({ type: "heartbeat_ack" }));
1780
+ return;
1781
+ }
1782
+ try {
1783
+ params.router.handleProviderMessage(authenticatedSessionId, message);
1784
+ } catch (error) {
1785
+ safeClose(params.ws, 4008, error instanceof Error ? error.message : "Provider message rejected");
1786
+ }
1787
+ });
1788
+ const cleanup = () => {
1789
+ clearTimeout(authTimeout);
1790
+ if (authenticatedSessionId) {
1791
+ params.sessionManager.removeSession(authenticatedSessionId);
1792
+ }
1793
+ };
1794
+ params.ws.once("close", cleanup);
1795
+ params.ws.once("error", cleanup);
1796
+ }
1797
+ function normalizeWebSocket(socket) {
1798
+ if ("on" in socket && typeof socket.on === "function" && "send" in socket && typeof socket.send === "function" && "close" in socket && typeof socket.close === "function") {
1799
+ return socket;
1800
+ }
1801
+ if ("socket" in socket && socket.socket) {
1802
+ return socket.socket;
1803
+ }
1804
+ throw new Error("Fastify websocket route did not provide a WebSocket connection.");
1805
+ }
1806
+ function safeSend(ws, message) {
1807
+ if (ws.readyState !== WebSocket.OPEN) {
1808
+ return;
1809
+ }
1810
+ ws.send(message);
1811
+ }
1812
+ function safeClose(ws, code, reason) {
1813
+ if (ws.readyState === WebSocket.CLOSED) {
1814
+ return;
1815
+ }
1816
+ ws.close(code, reason);
1817
+ }
1818
+
1819
+ // src/index.ts
1820
+ async function startRelayServer() {
1821
+ const config = loadRelayConfig();
1822
+ const relay = await createRelayServer(config);
1823
+ const address = await relay.start();
1824
+ return { relay, address };
1825
+ }
1826
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1827
+ try {
1828
+ const { relay, address } = await startRelayServer();
1829
+ relay.app.log.info({ address, relayDid: relay.identity.did }, "Relay server started");
1830
+ } catch (error) {
1831
+ console.error(error);
1832
+ process.exitCode = 1;
1833
+ }
1834
+ }
1835
+ export {
1836
+ HealthMonitor,
1837
+ PaymentGate,
1838
+ RelayIdentity,
1839
+ RelayRegistryService,
1840
+ RelayRouteError,
1841
+ RelayRouter,
1842
+ SessionManager,
1843
+ applyMeshResponseHeaders,
1844
+ calculateRelayFee,
1845
+ createAuthPayload,
1846
+ createRelayServer,
1847
+ getDefaultRelayConfig,
1848
+ getMeshRequestContext,
1849
+ loadRelayConfig,
1850
+ normalizeCapability,
1851
+ parseProviderMessage,
1852
+ parseRelayMessage,
1853
+ registerMeshMiddleware,
1854
+ registerRelayRoutes,
1855
+ serializeRelayMessage,
1856
+ startRelayServer,
1857
+ validateRelayConfig
1858
+ };
1859
+ //# sourceMappingURL=index.js.map