mycomesh-consumer 0.1.53 → 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.
@@ -1,4176 +0,0 @@
1
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
- import { spawn as defaultSpawn } from "node:child_process";
3
- import { createServer } from "node:http";
4
- import { rootCertificates } from "node:tls";
5
- import { homedir } from "node:os";
6
- import { join, dirname } from "node:path";
7
- import {
8
- chmodSync,
9
- existsSync,
10
- mkdirSync,
11
- readFileSync,
12
- unlinkSync,
13
- writeFileSync,
14
- } from "node:fs";
15
-
16
- import { secp256k1 } from "@noble/curves/secp256k1";
17
- import { ed25519 } from "@noble/curves/ed25519";
18
- import { keccak_256 } from "@noble/hashes/sha3.js";
19
- import { Agent, ProxyAgent } from "undici";
20
- import { RESERVED_AUTH_SCHEMA, RESERVED_SIGNED_SCHEMA, buildReservedAuthorization, verifyReservedAuthorization, verifyReservedReceipt, decodeCapacityChannel, reservedSettlementKey } from "./consumer-reserved.mjs";
21
- import { ConsumerSessions } from "./consumer-sessions.mjs";
22
- import { ConsumerHistoryLedger } from "./consumer-history.mjs";
23
- import { ConsumerRequestJournal, requestPayloadHash } from "./consumer-request-journal.mjs";
24
- import { ConsumerRelayDiscovery, parseDiscoveryConfig } from "./consumer-discovery.mjs";
25
-
26
- export const DEFAULT_BASE_URL = "http://127.0.0.1:8110/v1";
27
- export const DEFAULT_RELAY_URL = "https://bridge.mycomesh.xyz";
28
- export const DEFAULT_MAX_FEE_UNITS = 100000;
29
- // Public API model slug; `codex-standard-v1` is only the settlement channel.
30
- export const DEFAULT_MODEL = "gpt-5.5";
31
- export const DEFAULT_CHAIN_ID = 11155111;
32
- export const DEFAULT_SETTLEMENT = "0x6b543a0ff6fae02172c6f205759b1b9de8a6d218";
33
- export const DEFAULT_CHANNEL_HASH =
34
- "0xdedf8b58276b80863f354409c963cbaddf4ca7d5b866d528ff1386d74b339104";
35
- export const DEFAULT_PRICING_HASH =
36
- "0x365dfdf311ab90468009d2a665803ca4321c50ab9ed0809ac2c6dc4a73ac9734";
37
- export const DEFAULT_RELAY_PAYMENT_ADDRESS =
38
- "0x27bd63aef83554700042685c2862da6f6a9197e8";
39
- export const DEFAULT_RELAY_SIGNER_ADDRESS =
40
- "0x36390747ae29f5f8ae55ddd7daace89ad57644cf";
41
- export const DEFAULT_STABLECOIN = "0xeb487c6e778248e16361dc313e4223c20d4c23b5";
42
- export const DEFAULT_RPC_URLS = [
43
- "https://ethereum-sepolia-rpc.publicnode.com",
44
- "https://sepolia.drpc.org",
45
- "https://rpc.sepolia.ethpandaops.io",
46
- "https://sepolia.gateway.tenderly.co",
47
- ];
48
-
49
- const ZERO_ADDRESS = "0x" + "0".repeat(40);
50
- const ZERO_BYTES32 = "0x" + "0".repeat(64);
51
- const DYNAMIC_PROVIDER_AI_COMMITTEE = "dynamic_provider_ai_v1";
52
- const DYNAMIC_JURY_FORBIDDEN_FIELDS = Object.freeze([
53
- "adjudicators",
54
- "adjudicator_operators",
55
- "independence_attested",
56
- "jury_provider_evidence",
57
- ]);
58
- const DYNAMIC_JURY_RANDOMNESS = "future_blockhash_v1";
59
- const REPUTATION_HISTORY_FIELD = "reputation_history_import";
60
- const REPUTATION_HISTORY_SCHEMA = "mycomesh.v10.reputation-history-import.v1";
61
- const REPUTATION_HISTORY_FIELDS = Object.freeze([
62
- "schema", "source_network_id", "source_protocol_version", "source_chain_id",
63
- "source_genesis_hash", "source_settlement_contract", "source_runtime_code_hash",
64
- "source_deployment_block", "source_deployment_block_hash",
65
- "source_history_through_block", "source_history_through_block_hash",
66
- "confirmations", "artifact_sha256", "artifact_root",
67
- ]);
68
- const DYNAMIC_JURY_RANDOMNESS_HASH = `0x${bytesToHex(keccak_256(Buffer.from(DYNAMIC_JURY_RANDOMNESS, "utf8")))}`;
69
- const MAX_DYNAMIC_PROVIDERS = 64;
70
- const MAX_DYNAMIC_JURY_SIZE = 7;
71
- const DISPUTE_CONFIRMATIONS = 6n;
72
- const JURY_EVIDENCE_REFERENCE_SCHEMA = "mycomesh.v10.provider-jury-evidence-reference.v1";
73
- const AUTH_SCHEMA = "mycomesh.x402.myco-credit-v2";
74
- const SIGNED_SCHEMA = "mycomesh.settlement.v8.signed.v1";
75
- const V9_AUTH_SCHEMA = "mycomesh.x402.myco-credit-v3";
76
- const V9_SIGNED_SCHEMA = "mycomesh.settlement.v9.signed.v1";
77
- export const RESPONSE_PROOF_SCHEMA = "mycomesh.provider-response-proof.v1";
78
- function protocolVersion(value = 8) {
79
- if (value !== 8 && value !== 9 && value !== 10) throw new Error("unsupported settlement protocol version");
80
- return value;
81
- }
82
- function authorizationVersion(value) {
83
- if (value?.schema === AUTH_SCHEMA) return 8;
84
- if (value?.schema === V9_AUTH_SCHEMA) return 9;
85
- throw new Error("unsupported payment authorization schema");
86
- }
87
- const DOMAIN_TYPE =
88
- "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
89
- const AUTHORIZATION_TYPE =
90
- "PaymentAuthorization(bytes32 requestId,bytes32 requestHash,address key,address relay,address relaySigner,bytes32 channel,uint64 pricingVersion,bytes32 pricingHash,uint256 maxFee,uint64 issuedAt,uint64 deadline)";
91
- const RECEIPT_TYPE =
92
- "UsageReceipt(bytes32 authorizationHash,bytes32 responseHash,address provider,address providerSigner,address relay,address pool,uint256 inputTokens,uint256 outputTokens,uint256 actualFee)";
93
- const RETRYABLE_RELAY_STATUS = new Set([408, 429, 500, 502, 503, 504]);
94
- const MAX_AUTHORIZATION_TTL = 3600;
95
- const V9_MAX_AUTHORIZATION_TTL = 10800;
96
- const MAX_BODY_BYTES = 32 * 1024 * 1024;
97
- const AUTHORIZATION_CLOCK_SKEW_SECONDS = 300;
98
- const RESPONSES_REQUEST_OPTION_FIELDS = new Set([
99
- "background",
100
- "client_metadata",
101
- "context_management",
102
- "conversation",
103
- "include",
104
- "instructions",
105
- "metadata",
106
- "max_tool_calls",
107
- "moderation",
108
- "parallel_tool_calls",
109
- "previous_response_id",
110
- "prompt",
111
- "prompt_cache_key",
112
- "prompt_cache_options",
113
- "prompt_cache_retention",
114
- "reasoning",
115
- "safety_identifier",
116
- "service_tier",
117
- "store",
118
- "temperature",
119
- "text",
120
- "tool_choice",
121
- "tools",
122
- "top_logprobs",
123
- "top_p",
124
- "truncation",
125
- "user",
126
- ]);
127
- const RESPONSES_LOCAL_OPTION_FIELDS = new Set(["stream", "stream_options"]);
128
- const MAX_SHARE_MINUTES = 24 * 60;
129
- const TUNNEL_START_TIMEOUT_MS = 30_000;
130
- const TUNNEL_STOP_TIMEOUT_MS = 2_000;
131
- const RELAY_HEALTH_CACHE_MS = 5_000;
132
- const RELAY_HEALTH_RETRY_DELAY_MS = 250;
133
- // Readiness is a liveness probe for local tooling and the dashboard. Keep it
134
- // bounded even when an RPC provider stalls; inference keeps its own deadline.
135
- const READINESS_TIMEOUT_MS = 5_000;
136
- const WALLET_CHALLENGE_TTL_MS = 5 * 60_000;
137
-
138
- const DEFAULT_NETWORK = Object.freeze({
139
- protocol_version: 8,
140
- chain_id: DEFAULT_CHAIN_ID,
141
- network_name: "Sepolia testnet",
142
- settlement_contract: DEFAULT_SETTLEMENT,
143
- stablecoin: DEFAULT_STABLECOIN,
144
- stablecoin_symbol: "tUSDC",
145
- stablecoin_decimals: 6,
146
- rpc_urls: DEFAULT_RPC_URLS,
147
- explorer_url: "https://sepolia.etherscan.io",
148
- });
149
-
150
- function bytesToHex(value) {
151
- return Buffer.from(value).toString("hex");
152
- }
153
-
154
- function hexToBytes(value, label = "hex value") {
155
- const text = String(value || "");
156
- if (!/^0x[0-9a-fA-F]*$/.test(text) || text.length % 2 !== 0) {
157
- throw new Error(`${label} must be hexadecimal`);
158
- }
159
- return Uint8Array.from(Buffer.from(text.slice(2), "hex"));
160
- }
161
-
162
- function normalizeBytes32(value, label = "bytes32") {
163
- const text = String(value || "").toLowerCase();
164
- if (!/^0x[0-9a-f]{64}$/.test(text)) {
165
- throw new Error(`${label} must be a 32-byte 0x-prefixed hex value`);
166
- }
167
- return text;
168
- }
169
-
170
- export function normalizeAddress(value, label = "address") {
171
- const text = String(value || "").toLowerCase();
172
- if (!/^0x[0-9a-f]{40}$/.test(text)) {
173
- throw new Error(`${label} must be a 20-byte 0x-prefixed hex value`);
174
- }
175
- return text;
176
- }
177
-
178
- function nonzeroAddress(value, label) {
179
- const address = normalizeAddress(value, label);
180
- if (address === ZERO_ADDRESS) throw new Error(`${label} cannot be zero`);
181
- return address;
182
- }
183
-
184
- function positiveBigInt(value, label) {
185
- if (typeof value === "boolean" || value === undefined || value === null || value === "") {
186
- throw new Error(`${label} must be an integer`);
187
- }
188
- let parsed;
189
- try {
190
- parsed = BigInt(value);
191
- } catch {
192
- throw new Error(`${label} must be an integer`);
193
- }
194
- if (parsed <= 0n) throw new Error(`${label} must be positive`);
195
- return parsed;
196
- }
197
-
198
- function uintBigInt(value, label) {
199
- if (typeof value === "boolean" || value === undefined || value === null || value === "") {
200
- throw new Error(`${label} must be an integer`);
201
- }
202
- let parsed;
203
- try {
204
- parsed = BigInt(value);
205
- } catch {
206
- throw new Error(`${label} must be an integer`);
207
- }
208
- if (parsed < 0n) throw new Error(`${label} cannot be negative`);
209
- return parsed;
210
- }
211
-
212
- function jsonInteger(value) {
213
- const parsed = typeof value === "bigint" ? value : BigInt(value);
214
- return parsed <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(parsed) : parsed.toString();
215
- }
216
-
217
- function base64Url(value) {
218
- return Buffer.from(value).toString("base64url");
219
- }
220
-
221
- function decodeBase64Url(value) {
222
- return Uint8Array.from(Buffer.from(String(value), "base64url"));
223
- }
224
-
225
- function stableStringify(value) {
226
- if (value === null) return "null";
227
- if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
228
- if (typeof value === "number") {
229
- if (!Number.isFinite(value)) throw new Error("request must contain canonical JSON data");
230
- return JSON.stringify(value);
231
- }
232
- if (typeof value === "bigint") return JSON.stringify(value.toString());
233
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
234
- if (typeof value === "object") {
235
- return `{${Object.keys(value)
236
- .filter((key) => value[key] !== undefined)
237
- .sort()
238
- .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
239
- .join(",")}}`;
240
- }
241
- throw new Error("request must contain canonical JSON data");
242
- }
243
-
244
- function normalizeJuryRelayPublicKeys(value) {
245
- if (!Array.isArray(value) || value.length < 1 || value.length > 4) {
246
- throw new Error("dynamic V10 network requires 1 to 4 jury Relay public keys");
247
- }
248
- const keys = value.map((item) => {
249
- if (typeof item !== "string" || !/^[0-9a-f]{64}$/.test(item)) {
250
- throw new Error("jury Relay public keys must be lowercase 32-byte Ed25519 hex");
251
- }
252
- return item;
253
- });
254
- if (new Set(keys).size !== keys.length) throw new Error("jury Relay public keys must be unique");
255
- return keys;
256
- }
257
-
258
- function juryEvidenceReferenceAudience({ network, settlement_contract, reporter }) {
259
- return `${JURY_EVIDENCE_REFERENCE_SCHEMA}:${network}:${settlement_contract}:${reporter}`;
260
- }
261
-
262
- export function verifyJuryEvidenceReference(value, expected) {
263
- const fields = new Set(["schema", "network", "chain_id", "settlement_contract",
264
- "settlement_key", "request_id", "request_hash", "evidence_hash",
265
- "predicted_report_id", "reporter", "origin_relay_public_key", "signature"]);
266
- if (!value || typeof value !== "object" || Array.isArray(value)
267
- || Object.keys(value).length !== fields.size
268
- || Object.keys(value).some((key) => !fields.has(key))) {
269
- throw new Error("jury evidence reference has unknown or missing fields");
270
- }
271
- const chainId = Number(value.chain_id);
272
- if (!Number.isSafeInteger(chainId) || chainId <= 0 || chainId !== Number(expected.chainId)) {
273
- throw new Error("jury evidence reference chain mismatch");
274
- }
275
- const network = `eip155:${chainId}`;
276
- const settlementContract = normalizeAddress(value.settlement_contract);
277
- const settlementKey = normalizeBytes32(value.settlement_key, "jury settlement key");
278
- const requestId = normalizeBytes32(value.request_id, "jury request ID");
279
- const requestHash = normalizeBytes32(value.request_hash, "jury request hash");
280
- const evidenceHash = normalizeBytes32(value.evidence_hash, "jury evidence hash");
281
- const predictedReportId = normalizeBytes32(value.predicted_report_id, "jury report ID");
282
- const reporter = nonzeroAddress(value.reporter, "jury evidence reporter");
283
- const origin = String(value.origin_relay_public_key || "");
284
- const pins = normalizeJuryRelayPublicKeys(expected.juryRelayPublicKeys);
285
- if (value.schema !== JURY_EVIDENCE_REFERENCE_SCHEMA || value.network !== network
286
- || settlementContract !== normalizeAddress(expected.contract)
287
- || settlementKey !== normalizeBytes32(expected.settlementKey, "expected settlement key")
288
- || requestId !== normalizeBytes32(expected.requestId, "expected request ID")
289
- || requestHash !== normalizeBytes32(expected.requestHash, "expected request hash")
290
- || reporter !== nonzeroAddress(expected.reporter, "expected jury reporter")
291
- || !pins.includes(origin)) {
292
- throw new Error("jury evidence reference differs from the paid request or pinned origin");
293
- }
294
- const calculatedReportId = keccakHex(Buffer.concat([
295
- abiWord(settlementKey), abiWord(reporter), abiWord(evidenceHash),
296
- ]));
297
- if (predictedReportId !== calculatedReportId) throw new Error("jury evidence report ID is not canonical");
298
- const signature = value.signature;
299
- const signatureFields = new Set(["nonce", "public_key", "purpose", "timestamp", "audience", "signature"]);
300
- if (!signature || typeof signature !== "object" || Array.isArray(signature)
301
- || Object.keys(signature).length !== signatureFields.size
302
- || Object.keys(signature).some((key) => !signatureFields.has(key))
303
- || signature.public_key !== origin || signature.purpose !== JURY_EVIDENCE_REFERENCE_SCHEMA
304
- || signature.audience !== juryEvidenceReferenceAudience({ network, settlement_contract: settlementContract, reporter })
305
- || signature.nonce !== evidenceHash.slice(2, 34)
306
- || !Number.isSafeInteger(signature.timestamp) || signature.timestamp !== Number(expected.issuedAt)
307
- || !/^[0-9a-f]{128}$/.test(String(signature.signature || ""))) {
308
- throw new Error("jury evidence reference signature domain is invalid");
309
- }
310
- const unsigned = Object.fromEntries(Object.entries(value).filter(([key]) => key !== "signature"));
311
- const signatureMetadata = Object.fromEntries(Object.entries(signature).filter(([key]) => key !== "signature"));
312
- const message = Buffer.from(stableStringify({ document: unsigned, signature: signatureMetadata }), "utf8");
313
- let verified = false;
314
- try {
315
- verified = ed25519.verify(
316
- Buffer.from(signature.signature, "hex"), message, Buffer.from(origin, "hex"),
317
- );
318
- } catch {}
319
- if (!verified) throw new Error("jury evidence reference signature is invalid");
320
- return { ...unsigned, settlement_contract: settlementContract, settlement_key: settlementKey,
321
- request_id: requestId, request_hash: requestHash, evidence_hash: evidenceHash,
322
- predicted_report_id: predictedReportId, reporter, origin_relay_public_key: origin,
323
- signature: { ...signature } };
324
- }
325
-
326
- export function restoreJuryEvidenceReference(entry, network, expectedWallet) {
327
- if (!entry || typeof entry !== "object") throw new Error("jury evidence history entry is missing");
328
- const chainId = Number(entry.chain_id);
329
- const contract = normalizeAddress(entry.settlement_contract);
330
- const settlementKey = normalizeBytes32(entry.settlement_key, "jury settlement key");
331
- const requestId = normalizeBytes32(entry.request_id, "jury request ID");
332
- const requestHash = normalizeBytes32(entry.request_hash, "jury request hash");
333
- const evidenceHash = normalizeBytes32(entry.jury_evidence_hash, "jury evidence hash");
334
- const reportId = normalizeBytes32(entry.jury_report_id, "jury report ID");
335
- const reporter = nonzeroAddress(entry.jury_reporter, "jury evidence reporter");
336
- const origin = String(entry.jury_origin_relay_public_key || "");
337
- const timestamp = Number(entry.jury_reference_timestamp);
338
- if (!Number.isSafeInteger(timestamp) || timestamp <= 0) {
339
- throw new Error("jury evidence history timestamp is invalid");
340
- }
341
- if (expectedWallet && reporter !== normalizeAddress(expectedWallet)) {
342
- throw new Error("jury evidence reporter is not the authenticated wallet");
343
- }
344
- const schema = JURY_EVIDENCE_REFERENCE_SCHEMA;
345
- const value = {
346
- schema,
347
- network: `eip155:${chainId}`,
348
- chain_id: chainId,
349
- settlement_contract: contract,
350
- settlement_key: settlementKey,
351
- request_id: requestId,
352
- request_hash: requestHash,
353
- evidence_hash: evidenceHash,
354
- predicted_report_id: reportId,
355
- reporter,
356
- origin_relay_public_key: origin,
357
- signature: {
358
- nonce: evidenceHash.slice(2, 34),
359
- public_key: origin,
360
- purpose: schema,
361
- timestamp,
362
- audience: juryEvidenceReferenceAudience({
363
- network: `eip155:${chainId}`,
364
- settlement_contract: contract,
365
- reporter,
366
- }),
367
- signature: String(entry.jury_reference_signature || ""),
368
- },
369
- };
370
- return verifyJuryEvidenceReference(value, {
371
- chainId: Number(network.chain_id),
372
- contract: network.settlement_contract,
373
- settlementKey,
374
- requestId,
375
- requestHash,
376
- reporter,
377
- issuedAt: timestamp,
378
- juryRelayPublicKeys: network.jury_relay_public_keys,
379
- });
380
- }
381
-
382
- function decodeAbiTuple(value, words, label) {
383
- const text = String(value || "").toLowerCase();
384
- if (!new RegExp(`^0x[0-9a-f]{${words * 64}}$`).test(text)) {
385
- throw new Error(`${label} returned non-canonical ABI data`);
386
- }
387
- return text.slice(2).match(/.{64}/g);
388
- }
389
-
390
- function decodeTupleAddress(word, label, { nonzero = true } = {}) {
391
- if (!/^0{24}[0-9a-f]{40}$/.test(word)) throw new Error(`${label} returned a non-canonical address`);
392
- const value = `0x${word.slice(-40)}`;
393
- if (nonzero && value === ZERO_ADDRESS) throw new Error(`${label} returned the zero address`);
394
- return value;
395
- }
396
-
397
- function decodeV10Settlement(value) {
398
- const words = decodeAbiTuple(value, 20, "Settlement V10 settlementInfo");
399
- const status = BigInt(`0x${words[19]}`);
400
- const settledAt = BigInt(`0x${words[17]}`);
401
- const releaseAt = BigInt(`0x${words[18]}`);
402
- if (status > 7n || settledAt >= (1n << 64n) || releaseAt >= (1n << 64n)) {
403
- throw new Error("Settlement V10 record has out-of-range typed fields");
404
- }
405
- return {
406
- owner: decodeTupleAddress(words[0], "settlement owner"),
407
- key: decodeTupleAddress(words[1], "settlement key address"),
408
- provider: decodeTupleAddress(words[2], "settlement Provider"),
409
- provider_signer: decodeTupleAddress(words[3], "settlement Provider signer"),
410
- relay: decodeTupleAddress(words[4], "settlement Relay"),
411
- relay_signer: decodeTupleAddress(words[5], "settlement Relay signer"),
412
- pool: decodeTupleAddress(words[6], "settlement pool", { nonzero: false }),
413
- treasury: decodeTupleAddress(words[7], "settlement treasury"),
414
- request_id: `0x${words[8]}`,
415
- request_hash: `0x${words[9]}`,
416
- authorization_hash: `0x${words[10]}`,
417
- response_hash: `0x${words[11]}`,
418
- gross_fee: BigInt(`0x${words[12]}`),
419
- settled_at: Number(settledAt),
420
- release_at: Number(releaseAt),
421
- status: Number(status),
422
- };
423
- }
424
-
425
- function decodeV10Policy(value) {
426
- const words = decodeAbiTuple(value, 13, "Settlement V10 policy");
427
- const uint64Indexes = [0, 1, 2];
428
- const uint16Indexes = [4, 6];
429
- if (uint64Indexes.some((index) => BigInt(`0x${words[index]}`) >= (1n << 64n))
430
- || uint16Indexes.some((index) => BigInt(`0x${words[index]}`) >= (1n << 16n))) {
431
- throw new Error("Settlement V10 policy has out-of-range typed fields");
432
- }
433
- decodeTupleAddress(words[12], "bond penalty recipient");
434
- return { reporter_bond: BigInt(`0x${words[3]}`) };
435
- }
436
-
437
- function decodeV10Report(value) {
438
- const words = decodeAbiTuple(value, 3, "Settlement V10 report");
439
- const claimed = BigInt(`0x${words[2]}`);
440
- if (claimed !== 0n && claimed !== 1n) throw new Error("Settlement V10 report returned a non-canonical bool");
441
- return {
442
- reporter: decodeTupleAddress(words[0], "dispute reporter"),
443
- evidence_hash: `0x${words[1]}`,
444
- bond_claimed: claimed === 1n,
445
- };
446
- }
447
-
448
- function canonicalBlock(value, label = "canonical block") {
449
- if (!value || typeof value !== "object"
450
- || !/^0x[0-9a-f]{64}$/i.test(String(value.hash || ""))
451
- || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(String(value.number || ""))
452
- || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(String(value.timestamp || ""))) {
453
- throw new Error(`${label} is unavailable`);
454
- }
455
- const timestamp = BigInt(value.timestamp);
456
- if (timestamp >= (1n << 64n) || timestamp > BigInt(Number.MAX_SAFE_INTEGER)) {
457
- throw new Error(`${label} timestamp is invalid`);
458
- }
459
- return {
460
- hash: String(value.hash).toLowerCase(),
461
- number: String(value.number).toLowerCase(),
462
- timestamp: Number(timestamp),
463
- };
464
- }
465
-
466
- function abiWord(value) {
467
- if (typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value)) {
468
- return Buffer.concat([Buffer.alloc(12), Buffer.from(value.slice(2), "hex")]);
469
- }
470
- if (typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value)) {
471
- return Buffer.from(value.slice(2), "hex");
472
- }
473
- const parsed = uintBigInt(value, "ABI uint");
474
- const output = Buffer.alloc(32);
475
- let remaining = parsed;
476
- for (let index = 31; index >= 0; index -= 1) {
477
- output[index] = Number(remaining & 0xffn);
478
- remaining >>= 8n;
479
- }
480
- if (remaining !== 0n) throw new Error("ABI uint is too large");
481
- return output;
482
- }
483
-
484
- function decodeSingleAbiWord(value, label) {
485
- const text = String(value || "").toLowerCase();
486
- if (!/^0x[0-9a-f]{64}$/.test(text)) throw new Error(`${label} returned a non-canonical ABI word`);
487
- return text;
488
- }
489
-
490
- function decodeAbiAddress(value, label) {
491
- const word = decodeSingleAbiWord(value, label);
492
- if (!/^0x0{24}/.test(word)) throw new Error(`${label} returned a non-canonical address`);
493
- return nonzeroAddress(`0x${word.slice(-40)}`, label);
494
- }
495
-
496
- function decodeAbiUint(value, label, bits = 256) {
497
- const result = BigInt(decodeSingleAbiWord(value, label));
498
- if (result >= (1n << BigInt(bits))) throw new Error(`${label} exceeds uint${bits}`);
499
- return result;
500
- }
501
-
502
- function decodeAbiBool(value, label) {
503
- const result = decodeAbiUint(value, label, 8);
504
- if (result !== 0n && result !== 1n) throw new Error(`${label} returned a non-canonical bool`);
505
- return result === 1n;
506
- }
507
-
508
- function hashText(value) {
509
- return Uint8Array.from(keccak_256(Buffer.from(value, "utf8")));
510
- }
511
-
512
- function keccakHex(value) {
513
- return `0x${bytesToHex(keccak_256(value))}`;
514
- }
515
-
516
- function paymentPrivateKey(value) {
517
- const text = String(value || "").trim();
518
- let raw;
519
- if (text.startsWith("myco_sk_")) {
520
- raw = decodeBase64Url(text.slice("myco_sk_".length));
521
- } else {
522
- raw = hexToBytes(text.startsWith("0x") ? text : `0x${text}`, "payment key");
523
- }
524
- if (raw.length !== 32) throw new Error("payment key must be 32 bytes");
525
- const scalar = BigInt(`0x${bytesToHex(raw)}`);
526
- if (scalar <= 0n || scalar >= secp256k1.CURVE.n) {
527
- throw new Error("payment key is outside secp256k1 range");
528
- }
529
- return raw;
530
- }
531
-
532
- export function generatePaymentKey() {
533
- while (true) {
534
- const raw = randomBytes(32);
535
- try {
536
- paymentPrivateKey(raw.toString("hex"));
537
- return `myco_sk_${raw.toString("base64url")}`;
538
- } catch {
539
- // A uniformly random 32-byte scalar is almost always valid.
540
- }
541
- }
542
- }
543
-
544
- export function paymentKeyAddress(value) {
545
- const publicKey = secp256k1.getPublicKey(paymentPrivateKey(value), false).slice(1);
546
- return `0x${bytesToHex(keccak_256(publicKey).slice(-20))}`;
547
- }
548
-
549
- function signDigest(privateKeyValue, digest) {
550
- const signature = secp256k1.sign(digest, paymentPrivateKey(privateKeyValue), {
551
- lowS: true,
552
- prehash: false,
553
- });
554
- const raw = Buffer.concat([
555
- Buffer.from(signature.toCompactRawBytes()),
556
- Buffer.from([27 + signature.recovery]),
557
- ]);
558
- return `0x${raw.toString("hex")}`;
559
- }
560
-
561
- function recoverAddress(digest, signatureValue) {
562
- const raw = hexToBytes(signatureValue, "signature");
563
- if (raw.length !== 65) throw new Error("signature must be 65 bytes");
564
- const recovery = raw[64] >= 27 ? raw[64] - 27 : raw[64];
565
- if (recovery < 0 || recovery > 3) throw new Error("signature recovery id is invalid");
566
- const signature = secp256k1.Signature.fromCompact(raw.slice(0, 64)).addRecoveryBit(recovery);
567
- const publicKey = signature.recoverPublicKey(digest).toRawBytes(false).slice(1);
568
- return `0x${bytesToHex(keccak_256(publicKey).slice(-20))}`;
569
- }
570
-
571
- export function walletMessageDigest(message) {
572
- const body = Buffer.from(String(message), "utf8");
573
- const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${body.length}`, "utf8");
574
- return Uint8Array.from(keccak_256(Buffer.concat([prefix, body])));
575
- }
576
-
577
- export function createReceiptStatusQuery(privateKey, { chainId, contract, requestId, channelId, issuedAt = Math.floor(Date.now() / 1000) }) {
578
- const query = { chain_id: Number(chainId), settlement_contract: normalizeAddress(contract),
579
- key: paymentKeyAddress(privateKey), request_id: normalizeBytes32(requestId, "request_id"), issued_at: Number(issuedAt) };
580
- if (!Number.isSafeInteger(query.chain_id) || query.chain_id <= 0 || !Number.isSafeInteger(query.issued_at) || query.issued_at <= 0) {
581
- throw new Error("invalid receipt status query scope");
582
- }
583
- if (channelId !== undefined) query.channel_id = normalizeBytes32(channelId, "channel_id");
584
- const message = `MycoMesh receipt status v${channelId === undefined ? 1 : 2}\nchain_id:${query.chain_id}\nsettlement_contract:${query.settlement_contract}\nkey:${query.key}${channelId === undefined ? "" : `\nchannel_id:${query.channel_id}`}\nrequest_id:${query.request_id}\nissued_at:${query.issued_at}`;
585
- return { ...query, signature: signDigest(privateKey, walletMessageDigest(message)) };
586
- }
587
-
588
- function typedDigest(structHash, chainId, contract, version = 8) {
589
- const domain = keccak_256(
590
- Buffer.concat([
591
- Buffer.from(hashText(DOMAIN_TYPE)),
592
- Buffer.from(hashText("MycoMesh Settlement")),
593
- Buffer.from(hashText(String(protocolVersion(version)))),
594
- abiWord(chainId),
595
- abiWord(normalizeAddress(contract)),
596
- ]),
597
- );
598
- return Uint8Array.from(
599
- keccak_256(Buffer.concat([Buffer.from([0x19, 0x01]), Buffer.from(domain), Buffer.from(hexToBytes(structHash))])),
600
- );
601
- }
602
-
603
- function authorizationStructHash(authorization) {
604
- const encoded = Buffer.concat([
605
- Buffer.from(hashText(AUTHORIZATION_TYPE)),
606
- abiWord(authorization.request_id),
607
- abiWord(authorization.request_hash),
608
- abiWord(authorization.key),
609
- abiWord(authorization.relay),
610
- abiWord(authorization.relay_signer),
611
- abiWord(authorization.channel),
612
- abiWord(authorization.pricing_version),
613
- abiWord(authorization.pricing_hash),
614
- abiWord(authorization.max_fee),
615
- abiWord(authorization.issued_at),
616
- abiWord(authorization.deadline),
617
- ]);
618
- return keccakHex(encoded);
619
- }
620
-
621
- function receiptStructHash(receipt) {
622
- const encoded = Buffer.concat([
623
- Buffer.from(hashText(RECEIPT_TYPE)),
624
- abiWord(receipt.authorization_hash),
625
- abiWord(receipt.response_hash),
626
- abiWord(receipt.provider),
627
- abiWord(receipt.provider_signer),
628
- abiWord(receipt.relay),
629
- abiWord(receipt.pool || ZERO_ADDRESS),
630
- abiWord(receipt.input_tokens),
631
- abiWord(receipt.output_tokens),
632
- abiWord(receipt.actual_fee),
633
- ]);
634
- return keccakHex(encoded);
635
- }
636
-
637
- export function inferenceRequestHash({
638
- endpoint,
639
- model,
640
- input,
641
- messages,
642
- maxOutputTokens,
643
- options,
644
- }) {
645
- const normalizedEndpoint = String(endpoint || "").trim().toLowerCase();
646
- if (!["responses", "chat"].includes(normalizedEndpoint)) {
647
- throw new Error("inference request endpoint must be responses or chat");
648
- }
649
- const normalizedModel = String(model || "");
650
- if (!normalizedModel) throw new Error("inference request model is required");
651
- const outputLimit = positiveBigInt(maxOutputTokens, "max_output_tokens");
652
- const requestOptions = normalizeInferenceOptions(normalizedEndpoint, options);
653
- const envelope = {
654
- request_hash_version: requestOptions
655
- ? "mycomesh.inference.request.v3"
656
- : "mycomesh.inference.request.v2",
657
- endpoint: normalizedEndpoint,
658
- model: normalizedModel,
659
- [normalizedEndpoint === "chat" ? "messages" : "input"]:
660
- normalizedEndpoint === "chat"
661
- ? messages ?? [{ role: "user", content: String(input || "") }]
662
- : input ?? "",
663
- max_output_tokens: jsonInteger(outputLimit),
664
- };
665
- if (requestOptions) envelope.options = requestOptions;
666
- return `0x${createHash("sha256").update(stableStringify(envelope), "utf8").digest("hex")}`;
667
- }
668
-
669
- export function derivePromptCacheKey({ endpoint, model, input, messages, options }) {
670
- const explicit = options && typeof options === "object" ? options.prompt_cache_key : undefined;
671
- if (typeof explicit === "string" && explicit.trim()) return explicit.trim();
672
- const normalizedEndpoint = String(endpoint || "").trim().toLowerCase();
673
- if (!["responses", "chat"].includes(normalizedEndpoint)) return null;
674
- const seed = { model: String(model || "") };
675
- for (const field of ["reasoning", "tool_choice", "tools", "functions", "instructions"]) {
676
- const value = options && typeof options === "object" ? options[field] : undefined;
677
- if (value !== undefined && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0)) {
678
- seed[field] = value;
679
- }
680
- }
681
- const source = normalizedEndpoint === "chat" ? (messages ?? [{ role: "user", content: String(input || "") }]) : (input ?? "");
682
- let firstUser;
683
- const system = [];
684
- if (Array.isArray(source)) {
685
- for (const item of source) {
686
- if (!item || typeof item !== "object") continue;
687
- const role = String(item.role || "").trim().toLowerCase();
688
- if (role === "system" || role === "developer") system.push(item.content);
689
- if (firstUser === undefined && role === "user") firstUser = item.content;
690
- if (firstUser === undefined && item.type === "input_text") firstUser = item.text;
691
- }
692
- } else if (typeof source === "string" && source.trim()) {
693
- firstUser = source;
694
- }
695
- if (system.length) seed.system = system;
696
- if (firstUser === undefined || firstUser === null || firstUser === "" || (Array.isArray(firstUser) && !firstUser.length)) return null;
697
- seed.first_user = firstUser;
698
- return `myco_csp_${createHash("sha256").update(stableStringify(seed), "utf8").digest("hex")}`;
699
- }
700
-
701
- function normalizeInferenceOptions(endpoint, options) {
702
- if (options === undefined || options === null) return null;
703
- if (typeof options !== "object" || Array.isArray(options)) {
704
- throw new Error("inference request options must be a JSON object");
705
- }
706
- const requestOptions = Object.fromEntries(
707
- Object.entries(options).filter(([key]) => !RESPONSES_LOCAL_OPTION_FIELDS.has(key)),
708
- );
709
- if (endpoint !== "responses") {
710
- const unknown = Object.keys(requestOptions).filter((key) => !RESPONSES_REQUEST_OPTION_FIELDS.has(key)).sort();
711
- if (unknown.length) throw new Error(`unsupported Chat request options: ${unknown.join(", ")}`);
712
- return Object.keys(requestOptions).length ? requestOptions : null;
713
- }
714
- const allowed = new Set([...RESPONSES_REQUEST_OPTION_FIELDS, ...RESPONSES_LOCAL_OPTION_FIELDS]);
715
- const unknown = Object.keys(options).filter((key) => !allowed.has(key)).sort();
716
- if (unknown.length) throw new Error(`unsupported Responses request options: ${unknown.join(", ")}`);
717
- const normalized = {};
718
- for (const key of [...RESPONSES_REQUEST_OPTION_FIELDS].sort()) {
719
- if (Object.prototype.hasOwnProperty.call(requestOptions, key)) normalized[key] = requestOptions[key];
720
- }
721
- return Object.keys(normalized).length ? normalized : null;
722
- }
723
-
724
- export function buildAuthorization({
725
- protocolVersion: requestedVersion = 8,
726
- paymentKey,
727
- chainId,
728
- settlementContract,
729
- requestId,
730
- requestHash,
731
- relay,
732
- relaySigner,
733
- channelHash,
734
- pricingVersion,
735
- pricingHash,
736
- maxFee,
737
- issuedAt = Math.floor(Date.now() / 1000),
738
- deadline = issuedAt + 900,
739
- maxAuthorizationTtlSeconds = MAX_AUTHORIZATION_TTL,
740
- }) {
741
- const version = protocolVersion(requestedVersion);
742
- const key = paymentKeyAddress(paymentKey);
743
- const authorization = {
744
- request_id: normalizeBytes32(requestId, "request_id"),
745
- request_hash: normalizeBytes32(requestHash, "request_hash"),
746
- key,
747
- relay: nonzeroAddress(relay, "relay"),
748
- relay_signer: nonzeroAddress(relaySigner, "relay_signer"),
749
- channel: normalizeBytes32(channelHash, "channel"),
750
- pricing_version: jsonInteger(positiveBigInt(pricingVersion, "pricing_version")),
751
- pricing_hash: normalizeBytes32(pricingHash, "pricing_hash"),
752
- max_fee: jsonInteger(positiveBigInt(maxFee, "max_fee")),
753
- issued_at: jsonInteger(positiveBigInt(issuedAt, "issued_at")),
754
- deadline: jsonInteger(positiveBigInt(deadline, "deadline")),
755
- };
756
- if (authorization.request_id === `0x${"0".repeat(64)}`) throw new Error("V8 request_id cannot be zero");
757
- if (authorization.request_hash === `0x${"0".repeat(64)}`) throw new Error("V8 request_hash cannot be zero");
758
- if (authorization.channel === `0x${"0".repeat(64)}`) throw new Error("V8 channel cannot be zero");
759
- if (authorization.pricing_hash === `0x${"0".repeat(64)}`) throw new Error("V8 pricing_hash cannot be zero");
760
- const issued = BigInt(authorization.issued_at);
761
- const expires = BigInt(authorization.deadline);
762
- if (![3600, ...(version === 9 ? [V9_MAX_AUTHORIZATION_TTL] : [])].includes(maxAuthorizationTtlSeconds)
763
- || expires <= issued || expires - issued > BigInt(maxAuthorizationTtlSeconds)) {
764
- throw new Error(`V${version} authorization exceeds the verified contract lifetime`);
765
- }
766
- const authorizationHash = authorizationStructHash(authorization);
767
- const digest = typedDigest(authorizationHash, chainId, settlementContract, version);
768
- return {
769
- schema: version === 9 ? V9_AUTH_SCHEMA : AUTH_SCHEMA,
770
- chain_id: jsonInteger(positiveBigInt(chainId, "chain_id")),
771
- settlement_contract: nonzeroAddress(settlementContract, "settlement_contract"),
772
- authorization,
773
- authorization_hash: authorizationHash,
774
- authorization_digest: `0x${bytesToHex(digest)}`,
775
- key_signature: signDigest(paymentKey, digest),
776
- };
777
- }
778
-
779
- function verifyAuthorization(value, expected = {}) {
780
- if (value?.schema === RESERVED_AUTH_SCHEMA) return verifyReservedAuthorization(value, expected);
781
- const version = authorizationVersion(value);
782
- if (!value.authorization) throw new Error("payment authorization is missing");
783
- if (expected.protocolVersion !== undefined && version !== protocolVersion(expected.protocolVersion)) throw new Error("settlement protocol version mismatch");
784
- const authorization = value.authorization;
785
- const chainId = uintBigInt(value.chain_id, "chain_id");
786
- const contract = nonzeroAddress(value.settlement_contract, "settlement_contract");
787
- const normalized = {
788
- request_id: normalizeBytes32(authorization.request_id, "request_id"),
789
- request_hash: normalizeBytes32(authorization.request_hash, "request_hash"),
790
- key: nonzeroAddress(authorization.key, "key"),
791
- relay: nonzeroAddress(authorization.relay, "relay"),
792
- relay_signer: nonzeroAddress(authorization.relay_signer, "relay_signer"),
793
- channel: normalizeBytes32(authorization.channel, "channel"),
794
- pricing_version: jsonInteger(positiveBigInt(authorization.pricing_version, "pricing_version")),
795
- pricing_hash: normalizeBytes32(authorization.pricing_hash, "pricing_hash"),
796
- max_fee: jsonInteger(positiveBigInt(authorization.max_fee, "max_fee")),
797
- issued_at: jsonInteger(positiveBigInt(authorization.issued_at, "issued_at")),
798
- deadline: jsonInteger(positiveBigInt(authorization.deadline, "deadline")),
799
- };
800
- if (expected.chainId !== undefined && chainId !== BigInt(expected.chainId)) throw new Error("V8 chain_id mismatch");
801
- if (expected.contract && contract !== normalizeAddress(expected.contract)) throw new Error("V8 settlement_contract mismatch");
802
- if (expected.relay && normalized.relay !== normalizeAddress(expected.relay)) throw new Error("V8 relay mismatch");
803
- if (expected.relaySigner && normalized.relay_signer !== normalizeAddress(expected.relaySigner)) throw new Error("V8 relay_signer mismatch");
804
- if (expected.requestId && normalized.request_id !== normalizeBytes32(expected.requestId)) throw new Error("V8 request_id mismatch");
805
- if (expected.requestHash && normalized.request_hash !== normalizeBytes32(expected.requestHash)) throw new Error("V8 request_hash mismatch");
806
- const now = Math.floor(Date.now() / 1000);
807
- const issued = BigInt(normalized.issued_at);
808
- const deadline = BigInt(normalized.deadline);
809
- if (issued > BigInt(now + 30) || deadline < BigInt(now)) throw new Error("V8 payment authorization is outside its time window");
810
- if (deadline <= issued || deadline - issued > BigInt(version === 9 ? V9_MAX_AUTHORIZATION_TTL : MAX_AUTHORIZATION_TTL)) throw new Error(`V${version} payment authorization lifetime is invalid`);
811
- const structHash = authorizationStructHash(normalized);
812
- if (normalizeBytes32(value.authorization_hash, "authorization_hash") !== structHash) throw new Error("V8 authorization hash mismatch");
813
- const digest = typedDigest(structHash, chainId, contract, version);
814
- if (normalizeBytes32(value.authorization_digest, "authorization_digest") !== `0x${bytesToHex(digest)}`) throw new Error("V8 authorization digest mismatch");
815
- if (recoverAddress(digest, value.key_signature) !== normalized.key) throw new Error("V8 payment key signature mismatch");
816
- return { ...value, chain_id: jsonInteger(chainId), settlement_contract: contract, authorization: normalized };
817
- }
818
-
819
- function verifySignedReceipt(value, expected = {}) {
820
- if (value?.schema === RESERVED_SIGNED_SCHEMA) return verifyReservedReceipt(value, expected);
821
- const version = value?.schema === V9_SIGNED_SCHEMA ? 9 : value?.schema === SIGNED_SCHEMA ? 8 : null;
822
- if (version === null) throw new Error("unsupported signed receipt schema");
823
- if (expected.protocolVersion !== undefined && expected.protocolVersion !== version) throw new Error("settlement protocol version mismatch");
824
- const authorization = verifyAuthorization(value.authorization, { ...expected, protocolVersion: version });
825
- if (version === 9 && (uintBigInt(value.chain_id, "chain_id") !== BigInt(authorization.chain_id)
826
- || nonzeroAddress(value.settlement_contract, "settlement_contract") !== authorization.settlement_contract)) {
827
- throw new Error("V9 signed receipt deployment mismatch");
828
- }
829
- const receipt = value.receipt;
830
- if (!receipt || typeof receipt !== "object") throw new Error("V8 signed receipt is missing");
831
- const normalizedReceipt = {
832
- authorization_hash: normalizeBytes32(receipt.authorization_hash, "authorization_hash"),
833
- response_hash: normalizeBytes32(receipt.response_hash, "response_hash"),
834
- provider: nonzeroAddress(receipt.provider, "provider"),
835
- provider_signer: nonzeroAddress(receipt.provider_signer, "provider_signer"),
836
- relay: nonzeroAddress(receipt.relay, "relay"),
837
- pool: normalizeAddress(receipt.pool || ZERO_ADDRESS, "pool"),
838
- input_tokens: jsonInteger(uintBigInt(receipt.input_tokens, "input_tokens")),
839
- output_tokens: jsonInteger(uintBigInt(receipt.output_tokens, "output_tokens")),
840
- actual_fee: jsonInteger(positiveBigInt(receipt.actual_fee, "actual_fee")),
841
- };
842
- if (normalizedReceipt.authorization_hash !== authorization.authorization_hash) throw new Error("V8 signed receipt authorization mismatch");
843
- if (normalizedReceipt.relay !== authorization.authorization.relay) throw new Error("V8 signed receipt Relay payout mismatch");
844
- if (value.key_signature !== authorization.key_signature) throw new Error("V8 signed receipt payment signature mismatch");
845
- if (BigInt(normalizedReceipt.actual_fee) > BigInt(authorization.authorization.max_fee)) throw new Error("receipt exceeds authorized max fee");
846
- const digest = typedDigest(receiptStructHash(normalizedReceipt), authorization.chain_id, authorization.settlement_contract, version);
847
- if (recoverAddress(digest, value.provider_signature) !== normalizedReceipt.provider_signer) throw new Error("V8 Provider signature mismatch");
848
- if (recoverAddress(digest, value.relay_signature) !== authorization.authorization.relay_signer) throw new Error("V8 Relay signature mismatch");
849
- return { authorization, receipt: normalizedReceipt };
850
- }
851
-
852
- const responseControls = new WeakMap();
853
-
854
- function deadlineError() {
855
- const error = new Error("request deadline exceeded");
856
- error.name = "TimeoutError";
857
- error.statusCode = 504;
858
- return error;
859
- }
860
-
861
- function remainingMs(deadline) {
862
- if (!Number.isFinite(deadline)) return Infinity;
863
- const left = deadline - performance.now();
864
- if (left <= 0) throw deadlineError();
865
- return Math.max(1, Math.ceil(left));
866
- }
867
-
868
- async function withinDeadline(promise, deadline) {
869
- if (!Number.isFinite(deadline)) return promise;
870
- let timer;
871
- try {
872
- const milliseconds = remainingMs(deadline);
873
- return await Promise.race([promise, new Promise((_, reject) => {
874
- timer = setTimeout(() => reject(deadlineError()), milliseconds);
875
- timer.unref?.();
876
- })]);
877
- } finally { clearTimeout(timer); }
878
- }
879
-
880
- async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
881
- const controller = new AbortController();
882
- const timer = setTimeout(() => controller.abort(deadlineError()), timeoutMs);
883
- timer.unref?.();
884
- const external = options.signal;
885
- const abort = () => controller.abort(external.reason);
886
- external?.addEventListener("abort", abort, { once: true });
887
- if (external?.aborted) abort();
888
- const cleanup = () => { clearTimeout(timer); external?.removeEventListener("abort", abort); };
889
- try {
890
- const response = await fetch(url, { redirect: "error", ...options, signal: controller.signal });
891
- // Keep the same deadline alive until the complete body is consumed.
892
- responseControls.set(response, { controller, cleanup });
893
- return response;
894
- } catch (error) { cleanup(); throw error; }
895
- }
896
-
897
- async function readJsonResponse(response) {
898
- const control = responseControls.get(response);
899
- let text;
900
- try {
901
- const reader = response.body?.getReader();
902
- if (!reader) text = "";
903
- else {
904
- let size = 0;
905
- const chunks = [];
906
- try {
907
- while (true) {
908
- const { done, value } = await reader.read();
909
- if (done) break;
910
- size += value.byteLength;
911
- if (size > MAX_BODY_BYTES) {
912
- const error = new Error("Relay response body exceeds the size limit");
913
- control?.controller.abort(error);
914
- await reader.cancel().catch(() => {});
915
- throw error;
916
- }
917
- chunks.push(Buffer.from(value));
918
- }
919
- text = Buffer.concat(chunks, size).toString("utf8");
920
- } finally { reader.releaseLock(); }
921
- }
922
- } finally { control?.cleanup(); responseControls.delete(response); }
923
- try {
924
- return text ? JSON.parse(text) : {};
925
- } catch {
926
- return { error: { message: text.slice(0, 1000), type: "relay_error" } };
927
- }
928
- }
929
-
930
- function openaiError(message, type = "server_error", code = type) {
931
- return { error: { message: String(message), type, param: null, code } };
932
- }
933
-
934
- export function parseNetworkConfig(path, { allowControlledTest = false } = {}) {
935
- if (!path) return { ...DEFAULT_NETWORK };
936
- if (!existsSync(path)) throw new Error("Configured settlement network manifest is missing");
937
- try {
938
- const network = JSON.parse(readFileSync(path, "utf8"));
939
- const deploymentPath = network.deployment ? join(dirname(path), network.deployment) : path;
940
- if (network.deployment && !existsSync(deploymentPath)) throw new Error("referenced deployment manifest is missing");
941
- const deployment = network.deployment ? JSON.parse(readFileSync(deploymentPath, "utf8")) : network;
942
- if (network.relay_discovery !== undefined && ["network_id", "channel_id"].some((name) => deployment[name] !== network[name])) {
943
- throw new Error("Discovery manifest and deployment disagree");
944
- }
945
- const version = protocolVersion(deployment.protocol_version);
946
- if (version >= 9 && (deployment.eip712_name !== "MycoMesh Settlement" || deployment.eip712_version !== String(version))) throw new Error(`V${version} deployment domain is not explicit`);
947
- const dynamicProviderJury = version === 10 && deployment.committee_mode === DYNAMIC_PROVIDER_AI_COMMITTEE;
948
- if (dynamicProviderJury) {
949
- for (const [label, manifest] of [["deployment", deployment], ["network", network]]) {
950
- const forbidden = DYNAMIC_JURY_FORBIDDEN_FIELDS.filter((name) => Object.prototype.hasOwnProperty.call(manifest, name));
951
- if (forbidden.length) throw new Error(`V10 dynamic Provider jury ${label} manifest contains forbidden static committee fields: ${forbidden.join(", ")}`);
952
- }
953
- }
954
- if (dynamicProviderJury) validateDynamicV10Deployment(deployment, { allowControlledTest });
955
- else if (version >= 9) validateV9Deployment(deployment, { allowControlledTest });
956
- if (dynamicProviderJury) {
957
- if (network !== deployment && (
958
- !Object.prototype.hasOwnProperty.call(network, REPUTATION_HISTORY_FIELD)
959
- || stableStringify(network[REPUTATION_HISTORY_FIELD])
960
- !== stableStringify(deployment[REPUTATION_HISTORY_FIELD])
961
- )) throw new Error("network reputation history lineage differs from deployment");
962
- if (network !== deployment && [
963
- "deployment_block", "deployment_block_hash",
964
- "settlement_runtime_code_keccak256",
965
- ].some((name) => network[name] !== deployment[name])) {
966
- throw new Error("network Settlement deployment boundary differs from deployment");
967
- }
968
- } else if (Object.prototype.hasOwnProperty.call(deployment, REPUTATION_HISTORY_FIELD)
969
- || (network !== deployment && Object.prototype.hasOwnProperty.call(network, REPUTATION_HISTORY_FIELD))) {
970
- throw new Error("reputation_history_import requires a dynamic V10 deployment");
971
- }
972
- if (network !== deployment && deployment.jury_relay_public_keys !== undefined) {
973
- throw new Error("jury_relay_public_keys belongs only in the network manifest");
974
- }
975
- const juryRelayPublicKeys = dynamicProviderJury
976
- ? normalizeJuryRelayPublicKeys(network.jury_relay_public_keys)
977
- : [];
978
- if (!dynamicProviderJury && network.jury_relay_public_keys !== undefined) {
979
- throw new Error("jury Relay public keys require a dynamic V10 deployment");
980
- }
981
- if (version === 10 && (deployment.reservation_mode !== "provider_bound_channel" || String(deployment.chain_domain) !== "10" || deployment.max_authorization_ttl_seconds !== 10800
982
- || ![604800, 2592000].includes(deployment.max_channel_duration_seconds))) throw new Error("V10 requires explicit fixed-budget channel policy");
983
- const capacityIds = network.capacity_channel_ids ?? deployment.capacity_channel_ids ?? [];
984
- if (!Array.isArray(capacityIds) || capacityIds.length > 32 || new Set(capacityIds.map(id => normalizeBytes32(id))).size !== capacityIds.length) throw new Error("invalid capacity channels");
985
- if (network !== deployment && network.capacity_channel_ids !== undefined && deployment.capacity_channel_ids !== undefined
986
- && JSON.stringify([...network.capacity_channel_ids].sort()) !== JSON.stringify([...deployment.capacity_channel_ids].sort())) throw new Error("network and deployment capacity channels disagree");
987
- if (network.require_response_proof !== undefined && typeof network.require_response_proof !== "boolean") throw new Error("require_response_proof must be boolean");
988
- const tlsCaFile = resolveTlsCaFile(network.tls_ca_file ?? deployment.tls_ca_file, path, { allowControlledTest });
989
- if (network.relay_fallbacks !== undefined && (!Array.isArray(network.relay_fallbacks) || network.relay_fallbacks.length > 3)) throw new Error("relay_fallbacks must contain at most three pinned Relays");
990
- const relayEntries = [network.relay, ...(network.relay_fallbacks || [])].filter((entry) => entry !== undefined);
991
- const relayUrls = [...new Set(relayEntries.map((entry) => {
992
- if (!entry || typeof entry.public_url !== "string") throw new Error("Relay manifest entry requires public_url");
993
- const url = new URL(entry.public_url);
994
- if ((url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))
995
- || url.username || url.password || url.search || url.hash) throw new Error("Relay manifest URLs require HTTPS without credentials/query/fragment");
996
- return url.href.replace(/\/+$/, "");
997
- }))];
998
- const relayPins = {};
999
- for (const entry of relayEntries) {
1000
- if (entry.payment_address === undefined && entry.attestation_address === undefined) continue;
1001
- const url = new URL(entry.public_url).href.replace(/\/+$/, "");
1002
- const pin = { payment_address: nonzeroAddress(entry.payment_address, "Relay payment address"),
1003
- attestation_address: nonzeroAddress(entry.attestation_address, "Relay attestation address") };
1004
- if (relayPins[url] && (relayPins[url].payment_address !== pin.payment_address
1005
- || relayPins[url].attestation_address !== pin.attestation_address)) throw new Error("conflicting Relay identity pins");
1006
- relayPins[url] = pin;
1007
- }
1008
- const parsed = {
1009
- protocol_version: version,
1010
- max_authorization_ttl_seconds: version >= 9 ? (deployment.max_authorization_ttl_seconds ?? 3600) : 3600,
1011
- authorization_deadline_seconds: version >= 9 ? (deployment.authorization_deadline_seconds ?? 900) : 900,
1012
- max_channel_duration_seconds: version === 10 ? deployment.max_channel_duration_seconds : undefined,
1013
- require_response_proof: version >= 9 || network.require_response_proof === true,
1014
- capacity_channel_ids: capacityIds.map(id => normalizeBytes32(id, "capacity channel ID")),
1015
- reservation_mode: version === 10 ? "provider_bound_channel" : undefined,
1016
- committee_mode: deployment.committee_mode,
1017
- ...(dynamicProviderJury ? {} : { independence_attested: deployment.independence_attested }),
1018
- ...(dynamicProviderJury ? {
1019
- jury_registry: normalizeAddress(deployment.jury_registry),
1020
- jury_registry_governance: normalizeAddress(deployment.jury_registry_governance),
1021
- reputation_authority: normalizeAddress(deployment.reputation_authority),
1022
- minimum_provider_reputation: deployment.minimum_provider_reputation,
1023
- jury_size: deployment.jury_size,
1024
- adjudication_threshold: deployment.adjudication_threshold,
1025
- jury_selection_delay_blocks: deployment.jury_selection_delay_blocks,
1026
- jury_randomness: deployment.jury_randomness,
1027
- jury_decision_policy_hash: deployment.jury_decision_policy_hash,
1028
- genesis_hash: deployment.genesis_hash,
1029
- deployment_block: deployment.deployment_block,
1030
- deployment_block_hash: deployment.deployment_block_hash,
1031
- settlement_runtime_code_keccak256: deployment.settlement_runtime_code_keccak256,
1032
- reputation_history_import: deployment.reputation_history_import,
1033
- jury_relay_public_keys: juryRelayPublicKeys,
1034
- bond_penalty_recipient: normalizeAddress(deployment.policy.bond_penalty_recipient),
1035
- reporter_bond_units: String(deployment.policy.reporter_bond),
1036
- } : {}),
1037
- relay_urls: relayUrls,
1038
- relay_pins: relayPins,
1039
- tls_ca_file: tlsCaFile,
1040
- chain_id: Number(deployment.chain_id),
1041
- network_name: Number(deployment.chain_id) === DEFAULT_CHAIN_ID ? "Sepolia testnet" : "EVM network",
1042
- settlement_contract: normalizeAddress(deployment.settlement),
1043
- stablecoin: normalizeAddress(deployment.stablecoin),
1044
- stablecoin_symbol: "tUSDC",
1045
- stablecoin_decimals: 6,
1046
- rpc_urls: (network.settlement_rpc_urls || [network.settlement_rpc_url]).filter(Boolean),
1047
- explorer_url: Number(deployment.chain_id) === DEFAULT_CHAIN_ID ? "https://sepolia.etherscan.io" : "",
1048
- };
1049
- parsed.relay_discovery = parseDiscoveryConfig(network, parsed);
1050
- return parsed;
1051
- } catch (error) {
1052
- throw new Error(`Invalid configured settlement network: ${error.message}`);
1053
- }
1054
- }
1055
-
1056
- function validateV9CommonDeployment(value) {
1057
- const maxTtl = value.max_authorization_ttl_seconds ?? 3600;
1058
- const deadlineSeconds = value.authorization_deadline_seconds ?? 900;
1059
- if (![3600, 10800].includes(maxTtl) || !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds <= 0
1060
- || deadlineSeconds + AUTHORIZATION_CLOCK_SKEW_SECONDS > maxTtl) throw new Error("invalid V9 authorization timing policy");
1061
- const required = ["chain_id", "deployer", "stablecoin", "settlement", "treasury", "governance", "channel", "channel_hash", "pricing_version", "pricing_hash", "reward_token", "policy", "network_id", "channel_id", "backend_policy"];
1062
- if (required.some((name) => value[name] === undefined)) throw new Error("V9 deployment requires explicit network and dispute policy fields");
1063
- const uint = (item, name, bits = 256) => {
1064
- if (!(typeof item === "number" && Number.isSafeInteger(item)) && !(typeof item === "string" && /^[0-9]+$/.test(item))) throw new Error(`invalid V9 ${name}`);
1065
- const result = BigInt(item);
1066
- if (result < 0n || result >= (1n << BigInt(bits))) throw new Error(`invalid V9 ${name}`);
1067
- return result;
1068
- };
1069
- if (!Number.isSafeInteger(value.chain_id) || value.chain_id <= 0 || uint(value.pricing_version, "pricing_version", 64) === 0n) throw new Error("invalid V9 chain/pricing version");
1070
- for (const name of ["deployer", "stablecoin", "settlement", "treasury", "governance"]) nonzeroAddress(value[name], name);
1071
- for (const name of ["channel_hash", "pricing_hash"]) if (normalizeBytes32(value[name], name) === `0x${"0".repeat(64)}`) throw new Error(`zero V9 ${name}`);
1072
- for (const name of ["channel", "network_id", "channel_id", "backend_policy"]) if (typeof value[name] !== "string" || !value[name].trim()) throw new Error(`missing V9 ${name}`);
1073
- const fields = ["dispute_window", "arbitration_timeout", "consumer_withdrawal_delay", "reporter_bond", "slash_bps", "slash_cap", "reporter_bounty_bps", "stable_bounty_cap", "token_reward", "token_reward_cap", "token_minimum_exposure", "token_minimum_penalty", "bond_penalty_recipient"];
1074
- if (!value.policy || typeof value.policy !== "object" || Array.isArray(value.policy)
1075
- || Object.keys(value.policy).length !== fields.length || fields.some((name) => value.policy[name] === undefined)) throw new Error("V9 complete dispute policy required");
1076
- const policy = Object.fromEntries(fields.slice(0, -1).map((name) => [name, uint(value.policy[name], name)]));
1077
- const penalty = nonzeroAddress(value.policy.bond_penalty_recipient, "bond_penalty_recipient");
1078
- for (const name of fields.slice(0, 3)) if (policy[name] === 0n || policy[name] > 2592000n) throw new Error(`invalid V9 ${name}`);
1079
- if (policy.reporter_bond === 0n || policy.slash_bps === 0n || policy.slash_bps > 10000n || policy.slash_cap === 0n
1080
- || policy.reporter_bounty_bps === 0n || policy.reporter_bounty_bps >= 10000n || policy.stable_bounty_cap === 0n || policy.stable_bounty_cap > policy.slash_cap) throw new Error("invalid V9 slash/bounty policy");
1081
- const reward = normalizeAddress(value.reward_token);
1082
- if (reward === ZERO_ADDRESS) {
1083
- if (["token_reward", "token_reward_cap", "token_minimum_exposure", "token_minimum_penalty"].some((name) => policy[name] !== 0n)) throw new Error("V9 disabled reward policy allocates tokens");
1084
- } else if (reward === normalizeAddress(value.stablecoin) || policy.token_reward === 0n || policy.token_reward > policy.token_reward_cap
1085
- || policy.token_minimum_exposure === 0n || policy.token_minimum_penalty === 0n || policy.token_minimum_penalty > policy.slash_cap) throw new Error("invalid V9 token reward policy");
1086
- return { uint, penalty, reward };
1087
- }
1088
-
1089
- function validateV9Deployment(value, { allowControlledTest = false } = {}) {
1090
- const { uint, penalty, reward } = validateV9CommonDeployment(value);
1091
- const required = ["adjudicators", "adjudication_threshold", "adjudicator_operators", "independence_attested"];
1092
- if (required.some((name) => value[name] === undefined)) throw new Error("V9 deployment requires explicit independent committee fields");
1093
- if (!Array.isArray(value.adjudicators) || value.adjudicators.length < 2 || value.adjudicators.length > 16) throw new Error("V9 explicit independent committee required");
1094
- const judges = value.adjudicators.map((judge) => nonzeroAddress(judge, "adjudicator"));
1095
- const threshold = uint(value.adjudication_threshold, "adjudication_threshold", 16);
1096
- if (new Set(judges).size !== judges.length || threshold < 2n || threshold <= BigInt(Math.floor(judges.length / 2)) || threshold > BigInt(judges.length)) throw new Error("V9 invalid committee quorum");
1097
- const authorities = [value.settlement, value.governance, value.treasury].map((item) => normalizeAddress(item)).concat(penalty);
1098
- if (normalizeAddress(value.settlement) === penalty || normalizeAddress(value.settlement) === normalizeAddress(value.treasury) || judges.some((judge) => authorities.includes(judge))) throw new Error("V9 committee conflicts with settlement authorities");
1099
- const operators = value.adjudicator_operators;
1100
- const controlled = value.committee_mode === "controlled_test";
1101
- if (controlled && !(allowControlledTest === true && [11155111,31337,1337].includes(value.chain_id) && value.network_id.endsWith("-controlled-test") && value.independence_attested === false && reward === ZERO_ADDRESS)) throw new Error("controlled test requires explicit --controlled-test opt-in, test chain and disabled rewards");
1102
- if (value.committee_mode !== undefined && !["controlled_test", "independent_users"].includes(value.committee_mode)) throw new Error("unknown committee mode");
1103
- if (!operators || typeof operators !== "object" || Array.isArray(operators) || (!controlled && value.independence_attested !== true)) throw new Error("V9 independent operator attestation required");
1104
- const entries = Object.entries(operators).map(([judge, operator]) => [nonzeroAddress(judge, "adjudicator operator"), operator]);
1105
- if (entries.length !== judges.length || new Set(entries.map(([judge]) => judge)).size !== judges.length
1106
- || entries.some(([judge, operator]) => !judges.includes(judge) || typeof operator !== "string" || !operator.trim())
1107
- || new Set(entries.map(([, operator]) => operator.trim().toLowerCase())).size !== (controlled ? 1 : judges.length)) throw new Error("V9 distinct operator identities required");
1108
- }
1109
-
1110
- function validateDynamicV10Deployment(value, { allowControlledTest = false } = {}) {
1111
- validateV9CommonDeployment(value);
1112
- const required = ["jury_registry", "jury_registry_governance", "reputation_authority", "minimum_provider_reputation",
1113
- "jury_size", "adjudication_threshold", "jury_selection_delay_blocks", "jury_randomness",
1114
- "jury_decision_policy_hash", "genesis_hash", "deployment_block",
1115
- "deployment_block_hash", "settlement_runtime_code_keccak256",
1116
- REPUTATION_HISTORY_FIELD];
1117
- if (required.some((name) => value[name] === undefined)) throw new Error("V10 dynamic Provider jury requires an explicit registry and reputation policy");
1118
- const forbidden = DYNAMIC_JURY_FORBIDDEN_FIELDS.filter((name) => Object.prototype.hasOwnProperty.call(value, name));
1119
- if (forbidden.length) throw new Error(`V10 dynamic Provider jury deployment contains forbidden static committee fields: ${forbidden.join(", ")}`);
1120
- const registry = nonzeroAddress(value.jury_registry, "jury_registry");
1121
- const governance = nonzeroAddress(value.jury_registry_governance, "jury_registry_governance");
1122
- const authority = nonzeroAddress(value.reputation_authority, "reputation_authority");
1123
- const settlement = nonzeroAddress(value.settlement, "settlement");
1124
- if (governance !== normalizeAddress(value.governance)
1125
- || new Set([registry, governance, authority, settlement]).size !== 4) {
1126
- throw new Error("V10 dynamic Provider jury authority bindings are unsafe");
1127
- }
1128
- const integer = (item, name, { minimum = 0, maximum = Number.MAX_SAFE_INTEGER } = {}) => {
1129
- if (!Number.isSafeInteger(item) || item < minimum || item > maximum) throw new Error(`invalid V10 dynamic jury ${name}`);
1130
- return item;
1131
- };
1132
- integer(value.minimum_provider_reputation, "minimum_provider_reputation", { minimum: 1 });
1133
- const jurySize = integer(value.jury_size, "jury_size", { minimum: 3, maximum: MAX_DYNAMIC_JURY_SIZE });
1134
- const threshold = integer(value.adjudication_threshold, "adjudication_threshold", { minimum: 2, maximum: jurySize });
1135
- integer(value.jury_selection_delay_blocks, "jury_selection_delay_blocks", { minimum: 1, maximum: 64 });
1136
- if (threshold <= Math.floor(jurySize / 2)) throw new Error("V10 dynamic Provider jury requires a strict-majority threshold");
1137
- const decisionPolicyHash = normalizeBytes32(value.jury_decision_policy_hash, "jury_decision_policy_hash");
1138
- if (value.jury_decision_policy_hash !== decisionPolicyHash || decisionPolicyHash === ZERO_BYTES32) {
1139
- throw new Error("V10 dynamic Provider jury requires a canonical nonzero SHA-256 decision policy hash");
1140
- }
1141
- const deploymentBlockHash = normalizeBytes32(value.deployment_block_hash, "deployment_block_hash");
1142
- const settlementRuntimeHash = normalizeBytes32(
1143
- value.settlement_runtime_code_keccak256, "settlement_runtime_code_keccak256",
1144
- );
1145
- if (!Number.isSafeInteger(value.deployment_block) || value.deployment_block <= 0
1146
- || value.deployment_block_hash !== deploymentBlockHash
1147
- || deploymentBlockHash === ZERO_BYTES32
1148
- || value.settlement_runtime_code_keccak256 !== settlementRuntimeHash
1149
- || settlementRuntimeHash === ZERO_BYTES32) {
1150
- throw new Error("V10 dynamic Settlement deployment boundary is invalid");
1151
- }
1152
- validateReputationHistoryImport(value[REPUTATION_HISTORY_FIELD], value);
1153
- if (value.jury_randomness !== DYNAMIC_JURY_RANDOMNESS
1154
- || allowControlledTest !== true
1155
- || value.chain_id !== DEFAULT_CHAIN_ID
1156
- || typeof value.network_id !== "string" || !value.network_id.endsWith("-controlled-test")
1157
- || normalizeAddress(value.reward_token) !== ZERO_ADDRESS) {
1158
- throw new Error("V10 future-block jury selection requires explicit Sepolia controlled-test opt-in and disabled rewards");
1159
- }
1160
- }
1161
-
1162
- function validateReputationHistoryImport(history, deployment) {
1163
- if (!history || typeof history !== "object" || Array.isArray(history)
1164
- || Object.keys(history).length !== REPUTATION_HISTORY_FIELDS.length
1165
- || REPUTATION_HISTORY_FIELDS.some((name) => !Object.prototype.hasOwnProperty.call(history, name))) {
1166
- throw new Error("V10 reputation_history_import must be a non-null exact lineage object");
1167
- }
1168
- const genesisHash = normalizeBytes32(deployment.genesis_hash, "genesis_hash");
1169
- const sourceGenesis = normalizeBytes32(history.source_genesis_hash, "source_genesis_hash");
1170
- const sourceRuntime = normalizeBytes32(history.source_runtime_code_hash, "source_runtime_code_hash");
1171
- const sourceDeploymentBlockHash = normalizeBytes32(
1172
- history.source_deployment_block_hash, "source_deployment_block_hash",
1173
- );
1174
- const sourceHistoryThroughBlockHash = normalizeBytes32(
1175
- history.source_history_through_block_hash, "source_history_through_block_hash",
1176
- );
1177
- const artifactRoot = normalizeBytes32(history.artifact_root, "artifact_root");
1178
- const sourceSettlement = normalizeAddress(history.source_settlement_contract, "source_settlement_contract");
1179
- if (deployment.genesis_hash !== genesisHash || genesisHash === ZERO_BYTES32
1180
- || history.schema !== REPUTATION_HISTORY_SCHEMA
1181
- || typeof history.source_network_id !== "string" || !history.source_network_id
1182
- || history.source_network_id !== history.source_network_id.trim()
1183
- || history.source_network_id.length > 160
1184
- || history.source_network_id === deployment.network_id
1185
- || ![9, 10].includes(history.source_protocol_version)
1186
- || !Number.isSafeInteger(history.source_chain_id)
1187
- || history.source_chain_id !== deployment.chain_id
1188
- || history.source_genesis_hash !== sourceGenesis || sourceGenesis !== genesisHash
1189
- || history.source_settlement_contract !== sourceSettlement
1190
- || sourceSettlement === ZERO_ADDRESS || sourceSettlement === normalizeAddress(deployment.settlement)
1191
- || history.source_runtime_code_hash !== sourceRuntime || sourceRuntime === ZERO_BYTES32
1192
- || !Number.isSafeInteger(history.source_deployment_block)
1193
- || history.source_deployment_block <= 0
1194
- || history.source_deployment_block_hash !== sourceDeploymentBlockHash
1195
- || sourceDeploymentBlockHash === ZERO_BYTES32
1196
- || !Number.isSafeInteger(history.source_history_through_block)
1197
- || history.source_history_through_block < history.source_deployment_block
1198
- || history.source_history_through_block_hash !== sourceHistoryThroughBlockHash
1199
- || sourceHistoryThroughBlockHash === ZERO_BYTES32
1200
- || !Number.isSafeInteger(history.confirmations)
1201
- || history.confirmations < 2 || history.confirmations > 256
1202
- || typeof history.artifact_sha256 !== "string"
1203
- || !/^[0-9a-f]{64}$/.test(history.artifact_sha256)
1204
- || history.artifact_sha256 === "0".repeat(64)
1205
- || history.artifact_root !== artifactRoot || artifactRoot === ZERO_BYTES32) {
1206
- throw new Error("V10 reputation history lineage is invalid or not a prior same-chain deployment");
1207
- }
1208
- return history;
1209
- }
1210
-
1211
- function defaultDataDir() {
1212
- return join(homedir(), ".mycomesh", "consumer");
1213
- }
1214
-
1215
- function resolveBaseUrl(value) {
1216
- const base = String(value || DEFAULT_BASE_URL).replace(/\/+$/, "");
1217
- return base.endsWith("/v1") ? base : `${base}/v1`;
1218
- }
1219
-
1220
- function rootBaseUrl(baseUrl) {
1221
- return baseUrl.replace(/\/v1\/?$/, "");
1222
- }
1223
-
1224
- function parseRelayUrls(env, explicit) {
1225
- const raw = explicit || env.MYCOMESH_V8_RELAY_URLS || env.MYCOMESH_CONSUMER_RELAY_URL || DEFAULT_RELAY_URL;
1226
- const urls = String(raw).split(",").map((item) => item.trim().replace(/\/+$/, "")).filter(Boolean);
1227
- if (!urls.length) throw new Error("at least one Relay URL is required");
1228
- for (const url of urls) {
1229
- const parsed = new URL(url);
1230
- if (!/^https?:$/.test(parsed.protocol)) throw new Error("Relay URLs must use HTTP or HTTPS");
1231
- }
1232
- return urls;
1233
- }
1234
-
1235
- function resolveTlsCaFile(configured, manifestPath, { allowControlledTest = false } = {}) {
1236
- if (configured === undefined || configured === null || configured === "") return null;
1237
- if (!allowControlledTest) {
1238
- throw new Error("custom Relay TLS CA is only allowed for an explicit controlled test (--controlled-test)");
1239
- }
1240
- if (typeof configured !== "string" || configured.length > 1024 || configured.includes("\0")) {
1241
- throw new Error("tls_ca_file must be a valid path");
1242
- }
1243
- const resolved = configured.startsWith("/") ? configured : join(dirname(manifestPath), configured);
1244
- if (!existsSync(resolved)) throw new Error("configured Relay TLS CA file is missing");
1245
- let certificate;
1246
- try { certificate = readFileSync(resolved); } catch { throw new Error("configured Relay TLS CA file is unreadable"); }
1247
- if (!certificate.toString("ascii").includes("BEGIN CERTIFICATE")) {
1248
- throw new Error("configured Relay TLS CA file does not contain a PEM certificate");
1249
- }
1250
- return resolved;
1251
- }
1252
-
1253
- function sameSecret(first, second) {
1254
- const a = Buffer.from(String(first || ""));
1255
- const b = Buffer.from(String(second || ""));
1256
- return a.length === b.length && timingSafeEqual(a, b);
1257
- }
1258
-
1259
- export class NativeConsumerState {
1260
- constructor(options = {}) {
1261
- const env = options.env || process.env;
1262
- this.dataDir = options.dataDir || env.MYCOMESH_CONSUMER_DATA_DIR || defaultDataDir();
1263
- mkdirSync(this.dataDir, { recursive: true, mode: 0o700 });
1264
- try { chmodSync(this.dataDir, 0o700); } catch {}
1265
- this.baseUrl = resolveBaseUrl(options.baseUrl || env.MYCOMESH_CONSUMER_PUBLIC_BASE_URL || DEFAULT_BASE_URL);
1266
- this.maxFeeUnits = options.maxFeeUnits || Number(env.MYCOMESH_V8_MAX_FEE_UNITS || DEFAULT_MAX_FEE_UNITS);
1267
- if (!Number.isSafeInteger(this.maxFeeUnits) || this.maxFeeUnits <= 0) throw new Error("max fee must be a positive integer");
1268
- this.timeoutMs = options.timeoutMs ?? Number(env.MYCOMESH_V8_REQUEST_TIMEOUT_SECONDS || 300) * 1000;
1269
- this.healthTimeoutMs = options.healthTimeoutMs ?? Number(env.MYCOMESH_V8_HEALTH_TIMEOUT_SECONDS || 10) * 1000;
1270
- this.network = parseNetworkConfig(options.networkConfig || env.MYCOMESH_CONSUMER_NETWORK_CONFIG, { allowControlledTest: options.allowControlledTest === true });
1271
- this.relayUrls = parseRelayUrls(env, options.relayUrls || env.MYCOMESH_V8_RELAY_URLS
1272
- || env.MYCOMESH_CONSUMER_RELAY_URL || this.network.relay_urls?.join(","));
1273
- this.staticRelayUrls = [...this.relayUrls];
1274
- if (env.MYCOMESH_CONSUMER_SETTLEMENT_RPC_URLS) {
1275
- this.network.rpc_urls = String(env.MYCOMESH_CONSUMER_SETTLEMENT_RPC_URLS).split(",").map((item) => item.trim()).filter(Boolean);
1276
- }
1277
- // A controlled IP testnet may use a private CA. The manifest path is
1278
- // already trusted and resolves relative CA paths; an explicit env/option
1279
- // is supported for generated manifests whose CA is staged separately.
1280
- const caFile = options.caFile || env.MYCOMESH_CONSUMER_CA_FILE || this.network.tls_ca_file;
1281
- if (caFile && options.allowControlledTest !== true) {
1282
- throw new Error("custom Relay TLS CA is only allowed for an explicit controlled test (--controlled-test)");
1283
- }
1284
- this.tlsCaFile = caFile ? (String(caFile).startsWith("/") ? String(caFile) : caFile) : null;
1285
- this.tlsCa = null;
1286
- this.tlsCaBundle = rootCertificates;
1287
- if (this.tlsCaFile) {
1288
- if (!existsSync(this.tlsCaFile)) throw new Error("configured Relay TLS CA file is missing");
1289
- try { this.tlsCa = readFileSync(this.tlsCaFile); } catch { throw new Error("configured Relay TLS CA file is unreadable"); }
1290
- if (!this.tlsCa.toString("ascii").includes("BEGIN CERTIFICATE")) throw new Error("configured Relay TLS CA file does not contain a PEM certificate");
1291
- // `ca` replaces (rather than augments) Node's defaults. Preserve public
1292
- // trust roots so a manifest may safely mix private test Relays and
1293
- // publicly certified fallback Relays.
1294
- this.tlsCaBundle = [...rootCertificates, this.tlsCa.toString("utf8")];
1295
- }
1296
- this.proxy = options.proxy || env.MYCOMESH_CONSUMER_PROXY || "";
1297
- // Public testnet routes can need more than Undici's 10-second handshake
1298
- // default. Each operation still has its own, often shorter, total deadline.
1299
- this.dispatcher = this.proxy
1300
- ? new ProxyAgent({ uri: this.proxy, proxyTls: { timeout: 30000 }, requestTls: { timeout: 30000, ...(this.tlsCa ? { ca: this.tlsCaBundle } : {}) } })
1301
- : new Agent({ connect: { timeout: 30000, ...(this.tlsCa ? { ca: this.tlsCaBundle } : {}) } });
1302
- this.relayDiscovery = this.network.relay_discovery ? new ConsumerRelayDiscovery({
1303
- config: this.network.relay_discovery, cachePath: join(this.dataDir, "relay-discovery-cache.json"), dispatcher: this.dispatcher,
1304
- }) : null;
1305
- this.syncDiscoveredRelays();
1306
- this.healthCache = new Map();
1307
- this.healthRequests = new Map();
1308
- // `/ready` is a bounded Relay/model liveness probe used by local tooling.
1309
- // It never claims paid readiness; `/paid-ready` and inference perform the
1310
- // wallet, network, and chain-backed budget checks.
1311
- this.readinessCache = null;
1312
- this.readinessRequest = null;
1313
- this.preferredRpcUrl = null;
1314
- this.capacityChannelsRequest = null;
1315
- this.relayFailures = new Map();
1316
- this.sessions = new ConsumerSessions();
1317
- this.sessionRoutes = new Map();
1318
- this.sessionSelections = new Map();
1319
- this.relayInFlight = new Map();
1320
- this.relayReservations = new Map();
1321
- this.relaySelectionCursor = 0;
1322
- this.activeInferenceRequests = 0;
1323
- this.historyPath = join(this.dataDir, "receipt-history.jsonl");
1324
- this.pendingKeyPath = join(this.dataDir, "pending-payment-key");
1325
- this.paymentKeyFromEnv = Boolean(String(env.MYCOMESH_V8_PAYMENT_KEY || "").trim());
1326
- this.paymentKey = this.loadPaymentKey(env.MYCOMESH_V8_PAYMENT_KEY);
1327
- this.paymentAddress = paymentKeyAddress(this.paymentKey);
1328
- this.historyDir = options.historyDir || env.MYCOMESH_CONSUMER_HISTORY_DIR;
1329
- this.historyLedger = new ConsumerHistoryLedger({
1330
- localPath: this.historyPath,
1331
- sharedDir: this.historyDir,
1332
- chainId: this.network.chain_id,
1333
- contract: this.network.settlement_contract,
1334
- keyAddress: this.paymentAddress,
1335
- });
1336
- this.historySync = null;
1337
- this.historySyncAt = 0;
1338
- this.historySyncError = null;
1339
- this.tunnelCommand = options.tunnelCommand || env.MYCOMESH_CONSUMER_TUNNEL_COMMAND || "cloudflared";
1340
- this.tunnelSpawn = options.tunnelSpawn || defaultSpawn;
1341
- this.share = null;
1342
- this.walletChallenge = null;
1343
- this.managementToken = null;
1344
- this.unlockedWallet = null;
1345
- this.paymentUnlocked = false;
1346
- }
1347
-
1348
- loadPaymentKey(configured) {
1349
- const value = String(configured || "").trim();
1350
- const path = join(this.dataDir, "payment-key");
1351
- if (value) {
1352
- paymentPrivateKey(value);
1353
- return value;
1354
- }
1355
- if (existsSync(path)) {
1356
- const stored = readFileSync(path, "utf8").trim();
1357
- paymentPrivateKey(stored);
1358
- return stored;
1359
- }
1360
- const generated = generatePaymentKey();
1361
- writeFileSync(path, `${generated}\n`, { mode: 0o600, flag: "wx" });
1362
- try { chmodSync(path, 0o600); } catch {}
1363
- return generated;
1364
- }
1365
-
1366
- credentialsText() {
1367
- return `export OPENAI_BASE_URL=${shellQuote(this.baseUrl)}\nexport OPENAI_API_KEY=${shellQuote(this.paymentKey)}`;
1368
- }
1369
-
1370
- capabilities(health) {
1371
- const version = protocolVersion(this.network.protocol_version);
1372
- const capabilities = health?.[`v${version}`];
1373
- if (!capabilities || typeof capabilities !== "object") throw new Error(`Relay has no Settlement V${version} capabilities`);
1374
- return capabilities;
1375
- }
1376
-
1377
- healthPayload() {
1378
- this.syncDiscoveredRelays();
1379
- return {
1380
- ok: true,
1381
- protocol: `mycomesh-consumer/v${this.network.protocol_version}`,
1382
- runtime: "node-native",
1383
- docker: false,
1384
- browser_app_ready: true,
1385
- gateway_dependency: false,
1386
- routing_mode: `relay-scheduled-payment-key-v${this.network.protocol_version}`,
1387
- relay_urls: this.relayUrls,
1388
- relay_discovery: this.relayDiscovery ? { enabled: true, active: this.relayDiscovery.active().length,
1389
- error: this.relayDiscovery.lastError } : { enabled: false },
1390
- payment_key_address: this.paymentAddress,
1391
- payment_key_persisted: !this.paymentKeyFromEnv,
1392
- wallet_unlocked: this.paymentUnlocked,
1393
- // This reports trust configuration only; server-certificate verification
1394
- // is completed on each HTTPS request and is never claimed in advance.
1395
- relay_tls: { mode: this.tlsCa ? "controlled_test_ca" : "system_trust_store", configured: Boolean(this.tlsCa) },
1396
- responses_transports: ["http", "sse"],
1397
- };
1398
- }
1399
-
1400
- walletAuthPayload() {
1401
- return {
1402
- authenticated: Boolean(this.managementToken && this.unlockedWallet),
1403
- wallet: this.unlockedWallet,
1404
- key_ready: this.paymentUnlocked,
1405
- };
1406
- }
1407
-
1408
- createWalletChallenge(walletValue) {
1409
- const wallet = normalizeAddress(walletValue, "wallet");
1410
- const issuedAt = Date.now();
1411
- const expiresAt = issuedAt + WALLET_CHALLENGE_TTL_MS;
1412
- const nonce = randomBytes(24).toString("base64url");
1413
- const message = [
1414
- "MycoMesh Consumer wallet login",
1415
- `Wallet: ${wallet}`,
1416
- `Payment key: ${this.paymentAddress}`,
1417
- `Nonce: ${nonce}`,
1418
- `Issued at: ${new Date(issuedAt).toISOString()}`,
1419
- "This signature unlocks this local Consumer process only.",
1420
- ].join("\n");
1421
- this.walletChallenge = { wallet, message, expiresAt };
1422
- return { wallet, key_address: this.paymentAddress, message, expires_at: Math.floor(expiresAt / 1000) };
1423
- }
1424
-
1425
- async authenticateWallet(raw) {
1426
- const wallet = normalizeAddress(raw?.wallet, "wallet");
1427
- const challenge = this.walletChallenge;
1428
- if (!challenge || challenge.wallet !== wallet || challenge.expiresAt < Date.now()) {
1429
- this.walletChallenge = null;
1430
- throw new Error("wallet login challenge is missing or expired");
1431
- }
1432
- this.walletChallenge = null;
1433
- const recovered = recoverAddress(walletMessageDigest(challenge.message), raw?.signature);
1434
- if (recovered !== wallet) throw new Error("wallet signature does not match the selected account");
1435
- let grant;
1436
- try { grant = await this.keyGrant(this.paymentAddress); }
1437
- catch {
1438
- throw Object.assign(new Error("Cannot verify payment-key ownership on-chain. Please retry when the connection recovers."), {
1439
- code: "wallet_verification_unavailable", selected_wallet: wallet, payment_key_address: this.paymentAddress,
1440
- });
1441
- }
1442
- if (grant.owner !== ZERO_ADDRESS && grant.owner !== wallet) {
1443
- throw Object.assign(new Error(`This local payment key belongs to a different wallet. Select ${grant.owner} in your wallet extension, or use a separate Consumer data directory for ${wallet}. Keep the existing key and billing history.`), {
1444
- code: "payment_key_owner_mismatch", selected_wallet: wallet, expected_owner: grant.owner, payment_key_address: this.paymentAddress,
1445
- });
1446
- }
1447
- let paymentUnlocked = grant.active && grant.owner === wallet;
1448
- if (this.network.protocol_version === 10 && !paymentUnlocked) {
1449
- // Revocation prevents new channels; existing owner-approved snapshots
1450
- // keep their bounded authorization until their original expiry.
1451
- try {
1452
- paymentUnlocked = (await this.capacityChannels()).some(c => !c.closed && c.consumer_owner === wallet
1453
- && c.consumer_key === this.paymentAddress && Number(c.admit_until) >= Math.floor(Date.now()/1000));
1454
- } catch {
1455
- throw Object.assign(new Error("Cannot verify existing payment channels on-chain. Please retry when the connection recovers."), {
1456
- code: "wallet_verification_unavailable", selected_wallet: wallet, payment_key_address: this.paymentAddress,
1457
- });
1458
- }
1459
- }
1460
- // Commit the session only after every chain check succeeds. A failed login
1461
- // must neither create a partial session nor replace an existing owner session.
1462
- this.unlockedWallet = wallet;
1463
- this.managementToken = `myco_local_${randomBytes(32).toString("base64url")}`;
1464
- this.paymentUnlocked = paymentUnlocked;
1465
- return { ok: true, token: this.managementToken, auth: this.walletAuthPayload(), grant };
1466
- }
1467
-
1468
- authorizeManagement(authorization) {
1469
- return Boolean(this.managementToken && sameSecret(authorization, `Bearer ${this.managementToken}`));
1470
- }
1471
-
1472
- assertUnlockedWallet(walletValue = this.unlockedWallet) {
1473
- const wallet = normalizeAddress(walletValue, "wallet");
1474
- if (!this.managementToken || wallet !== this.unlockedWallet) {
1475
- throw new Error("sign in with the payment-key owner wallet first");
1476
- }
1477
- return wallet;
1478
- }
1479
-
1480
- async activateCurrentPaymentKey() {
1481
- const wallet = this.assertUnlockedWallet();
1482
- const grant = await this.keyGrant(this.paymentAddress);
1483
- if (!grant.active || grant.owner !== wallet) {
1484
- throw new Error("the payment key is not active for this wallet on-chain");
1485
- }
1486
- this.paymentUnlocked = true;
1487
- return { ok: true, auth: this.walletAuthPayload(), grant };
1488
- }
1489
-
1490
- async lockWallet() {
1491
- this.walletChallenge = null;
1492
- this.managementToken = null;
1493
- this.unlockedWallet = null;
1494
- this.paymentUnlocked = false;
1495
- await this.stopShare();
1496
- return { ok: true, auth: this.walletAuthPayload() };
1497
- }
1498
-
1499
- history(limit = 100) {
1500
- return this.historyLedger.history(limit);
1501
- }
1502
-
1503
- // Keep recovery state explicit for the local dashboard. The history ledger
1504
- // is durable, so a restarted Consumer must show the operator whether a
1505
- // request is still open, needs reconciliation, or is terminally failed;
1506
- // callers must never infer this from a missing/zero fee.
1507
- recoveryPayload(allHistory, authenticated = false) {
1508
- if (!authenticated) {
1509
- return {
1510
- status: "locked",
1511
- open_count: 0,
1512
- uncertain_count: 0,
1513
- failed_count: 0,
1514
- requires_reconcile: false,
1515
- last_checked_at: 0,
1516
- error: null,
1517
- };
1518
- }
1519
- const rows = Array.isArray(allHistory) ? allHistory : [];
1520
- const openStatuses = new Set(["dispatching", "outcome_unknown", "queued", "pending", "submitted", "broadcast_unknown"]);
1521
- const uncertainStatuses = new Set(["dispatching", "outcome_unknown", "submitted", "broadcast_unknown"]);
1522
- const failedStatuses = new Set(["failed", "rejected"]);
1523
- const open = rows.filter((item) => openStatuses.has(item.status));
1524
- const uncertain = rows.filter((item) => uncertainStatuses.has(item.status)
1525
- || ["broadcast_unknown", "confirmation_timeout"].includes(item.error_code));
1526
- const failed = rows.filter((item) => failedStatuses.has(item.status));
1527
- const error = this.historySyncError || null;
1528
- const status = this.historySync ? "syncing"
1529
- : error ? "degraded"
1530
- : uncertain.length ? "needs_reconcile"
1531
- : open.length ? "pending"
1532
- : failed.length ? "failed" : "clear";
1533
- return {
1534
- status,
1535
- open_count: open.length,
1536
- uncertain_count: uncertain.length,
1537
- failed_count: failed.length,
1538
- requires_reconcile: Boolean(uncertain.length || error),
1539
- last_checked_at: Number(this.historySyncAt || 0),
1540
- error,
1541
- };
1542
- }
1543
-
1544
- recordDispatch(relayUrl, endpoint, payment, sessionId, status = "dispatching") {
1545
- const auth = payment.payment.authorization;
1546
- this.historyLedger.append({
1547
- timestamp: Math.floor(Date.now() / 1000), updated_at: Math.floor(Date.now() / 1000),
1548
- request_id: auth.request_id, request_hash: auth.request_hash,
1549
- key_address: this.paymentAddress, owner: this.unlockedWallet || undefined,
1550
- chain_id: this.network.chain_id, settlement_contract: this.network.settlement_contract,
1551
- relay_url: relayUrl, relay: payment.channel?.relay || auth.relay, capacity_channel_id: auth.channel_id, endpoint, model: payment.model,
1552
- route_model: payment.model, session_id: sessionId, source: "local-dispatch",
1553
- max_fee_units: Number(auth.max_fee), authorization_deadline: Number(auth.deadline),
1554
- accepted: false, status,
1555
- });
1556
- }
1557
-
1558
- recordReceipt(relayUrl, endpoint, model, settlement, routeModel = model, sessionId = null, contentVerification = "receipt-only") {
1559
- const signed = settlement?.signed_receipt;
1560
- const receipt = signed?.receipt;
1561
- const auth = signed?.authorization?.authorization;
1562
- if (!receipt || !auth) return;
1563
- const jury = settlement?.jury_evidence_reference;
1564
- const entry = {
1565
- timestamp: Math.floor(Date.now() / 1000),
1566
- request_id: String(auth.request_id || ""),
1567
- request_hash: auth.request_hash, capacity_channel_id: auth.channel_id, max_fee_units: Number(auth.max_fee),
1568
- settlement_key: String(settlement.settlement_key || ""),
1569
- // The unsigned Relay envelope cannot assert escrow, payout or refund.
1570
- status: ["queued", "pending", "submitted", "broadcast_unknown", "failed", "rejected"].includes(settlement.status) ? settlement.status : "pending",
1571
- accepted: Boolean(settlement.accepted),
1572
- endpoint,
1573
- model,
1574
- route_model: routeModel,
1575
- relay_url: relayUrl,
1576
- provider: String(receipt.provider || ""),
1577
- provider_signer: String(receipt.provider_signer || ""),
1578
- response_hash: String(receipt.response_hash || ""),
1579
- content_verification: contentVerification,
1580
- owner: String(this.unlockedWallet || ""),
1581
- key_address: this.paymentAddress,
1582
- chain_id: this.network.chain_id,
1583
- settlement_contract: this.network.settlement_contract,
1584
- source: "local-receipt",
1585
- session_id: sessionId,
1586
- input_tokens: Number(receipt.input_tokens || 0),
1587
- output_tokens: Number(receipt.output_tokens || 0),
1588
- actual_fee_units: Number(receipt.actual_fee || 0),
1589
- authorization_deadline: Number(auth.deadline || 0),
1590
- ...(jury ? {
1591
- jury_evidence_hash: jury.evidence_hash,
1592
- jury_report_id: jury.predicted_report_id,
1593
- jury_reporter: jury.reporter,
1594
- jury_origin_relay_public_key: jury.origin_relay_public_key,
1595
- jury_reference_signature: jury.signature.signature,
1596
- jury_reference_timestamp: jury.signature.timestamp,
1597
- } : {}),
1598
- };
1599
- this.historyLedger.append(entry);
1600
- }
1601
-
1602
- async rpcValue(callback) {
1603
- const errors = [];
1604
- let candidates = [...new Set(this.network.rpc_urls)];
1605
- if (candidates.includes(this.preferredRpcUrl)) {
1606
- candidates = [this.preferredRpcUrl, ...candidates.filter((url) => url !== this.preferredRpcUrl)];
1607
- }
1608
- // Each callback is a complete read against one endpoint. Restart the whole
1609
- // verification on failover; retry transient read failures at most once.
1610
- for (let attempt = 0; attempt < 2 && candidates.length; attempt += 1) {
1611
- const retry = [];
1612
- for (const rpcUrl of candidates) {
1613
- try {
1614
- const value = await callback(rpcUrl);
1615
- this.preferredRpcUrl = rpcUrl;
1616
- return value;
1617
- } catch (error) {
1618
- errors.push(error.message);
1619
- if (error.rpcRetryable === true) retry.push(rpcUrl);
1620
- }
1621
- }
1622
- candidates = retry;
1623
- }
1624
- const error = new Error(`all configured Settlement V${this.network.protocol_version} RPC endpoints failed: ${errors.join("; ")}`);
1625
- error.code = "rpc_unavailable";
1626
- error.statusCode = 503;
1627
- throw error;
1628
- }
1629
-
1630
- async callRpc(rpcUrl, method, params) {
1631
- try {
1632
- const response = await fetchWithTimeout(rpcUrl, {
1633
- method: "POST",
1634
- headers: { "content-type": "application/json" },
1635
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
1636
- dispatcher: this.dispatcher,
1637
- }, this.healthTimeoutMs);
1638
- const payload = await readJsonResponse(response);
1639
- if (!response.ok || payload.error) {
1640
- const error = new Error(payload.error?.message || `RPC ${response.status}`);
1641
- error.rpcRetryable = [408, 429, 500, 502, 503, 504].includes(response.status)
1642
- || [-32002, -32005].includes(payload.error?.code);
1643
- throw error;
1644
- }
1645
- return payload.result;
1646
- } catch (error) {
1647
- if (error.name === "TimeoutError" || error.name === "AbortError"
1648
- || (error instanceof TypeError && ["fetch failed", "terminated"].includes(error.message))) {
1649
- error.rpcRetryable = true;
1650
- }
1651
- throw error;
1652
- }
1653
- }
1654
-
1655
- async contractCall(rpcUrl, contract, signature, args, blockTag = "latest") {
1656
- const data = `0x${bytesToHex(keccak_256(Buffer.from(signature, "ascii")).slice(0, 4))}${args.map((arg) => abiWord(arg).toString("hex")).join("")}`;
1657
- return this.callRpc(rpcUrl, "eth_call", [{ to: normalizeAddress(contract), data }, blockTag]);
1658
- }
1659
-
1660
- async keyGrant(address) {
1661
- const output = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.settlement_contract, "keyGrants(address)", [normalizeAddress(address)]));
1662
- const words = String(output || "").replace(/^0x/, "").match(/.{64}/g) || [];
1663
- if (words.length < 4) throw new Error("invalid key grant response");
1664
- return {
1665
- owner: `0x${words[0].slice(-40)}`,
1666
- max_per_request: Number(BigInt(`0x${words[1]}`)),
1667
- valid_until: Number(BigInt(`0x${words[2]}`)),
1668
- active: BigInt(`0x${words[3]}`) !== 0n,
1669
- };
1670
- }
1671
-
1672
- async dynamicJuryNetworkReady(rpc) {
1673
- if (this.network.committee_mode !== DYNAMIC_PROVIDER_AI_COMMITTEE) return true;
1674
- const settlement = this.network.settlement_contract;
1675
- const registry = this.network.jury_registry;
1676
- const history = this.network.reputation_history_import;
1677
- const settlementDeploymentBlock = await this.callRpc(
1678
- rpc, "eth_getBlockByNumber", [`0x${this.network.deployment_block.toString(16)}`, false],
1679
- );
1680
- if (!settlementDeploymentBlock
1681
- || BigInt(settlementDeploymentBlock.number) !== BigInt(this.network.deployment_block)
1682
- || normalizeBytes32(
1683
- settlementDeploymentBlock.hash, "Settlement deployment block hash",
1684
- ) !== this.network.deployment_block_hash) {
1685
- throw new Error("Settlement deployment block is not canonical");
1686
- }
1687
- const settlementRuntime = String(await this.callRpc(
1688
- rpc, "eth_getCode", [settlement, {
1689
- blockHash: this.network.deployment_block_hash, requireCanonical: true,
1690
- }],
1691
- ) || "");
1692
- if (!/^0x[0-9a-f]+$/.test(settlementRuntime)
1693
- || settlementRuntime === "0x" || /^0x0+$/.test(settlementRuntime)) {
1694
- throw new Error("Settlement runtime is unavailable at its deployment boundary");
1695
- }
1696
- const settlementRuntimeHash = `0x${bytesToHex(keccak_256(hexToBytes(
1697
- settlementRuntime, "Settlement deployment runtime",
1698
- )))}`;
1699
- if (settlementRuntimeHash !== this.network.settlement_runtime_code_keccak256) {
1700
- throw new Error("Settlement runtime differs from its deployment manifest pin");
1701
- }
1702
- const genesis = await this.callRpc(rpc, "eth_getBlockByNumber", ["0x0", false]);
1703
- if (!genesis || normalizeBytes32(genesis.hash, "RPC genesis hash") !== this.network.genesis_hash) {
1704
- throw new Error("Settlement RPC genesis differs from reputation history lineage");
1705
- }
1706
- const sourceDeploymentNumber = BigInt(history.source_deployment_block);
1707
- const sourceThroughNumber = BigInt(history.source_history_through_block);
1708
- const [sourceDeploymentBlock, sourceThroughBlock, sourcePredeploymentBlock, sourceHeadRaw] = await Promise.all([
1709
- this.callRpc(rpc, "eth_getBlockByNumber", [`0x${sourceDeploymentNumber.toString(16)}`, false]),
1710
- this.callRpc(rpc, "eth_getBlockByNumber", [`0x${sourceThroughNumber.toString(16)}`, false]),
1711
- this.callRpc(rpc, "eth_getBlockByNumber", [`0x${(sourceDeploymentNumber - 1n).toString(16)}`, false]),
1712
- this.callRpc(rpc, "eth_blockNumber", []),
1713
- ]);
1714
- const sourceHead = BigInt(sourceHeadRaw);
1715
- const sourceBlocks = [
1716
- [sourceDeploymentBlock, sourceDeploymentNumber, history.source_deployment_block_hash, "deployment"],
1717
- [sourceThroughBlock, sourceThroughNumber, history.source_history_through_block_hash, "history cutoff"],
1718
- ];
1719
- for (const [block, expectedNumber, expectedHash, label] of sourceBlocks) {
1720
- if (!block || BigInt(block.number) !== expectedNumber
1721
- || normalizeBytes32(block.hash, `source ${label} block hash`) !== expectedHash) {
1722
- throw new Error(`reputation history source ${label} block is not canonical`);
1723
- }
1724
- }
1725
- if (!sourcePredeploymentBlock
1726
- || BigInt(sourcePredeploymentBlock.number) !== sourceDeploymentNumber - 1n) {
1727
- throw new Error("reputation history source predeployment block is unavailable");
1728
- }
1729
- if (sourceHead - sourceThroughNumber + 1n < BigInt(history.confirmations)) {
1730
- throw new Error("reputation history cutoff does not have the required confirmations");
1731
- }
1732
- const predeploymentHash = normalizeBytes32(
1733
- sourcePredeploymentBlock.hash, "source predeployment block hash",
1734
- );
1735
- const predeploymentCode = String(await this.callRpc(
1736
- rpc, "eth_getCode", [history.source_settlement_contract, {
1737
- blockHash: predeploymentHash, requireCanonical: true,
1738
- }],
1739
- ) || "");
1740
- if (predeploymentCode !== "0x") {
1741
- throw new Error("historical Settlement existed before its pinned deployment block");
1742
- }
1743
- const historicalCode = String(await this.callRpc(
1744
- rpc, "eth_getCode", [history.source_settlement_contract, {
1745
- blockHash: history.source_deployment_block_hash, requireCanonical: true,
1746
- }],
1747
- ) || "");
1748
- if (!/^0x[0-9a-f]+$/.test(historicalCode) || historicalCode === "0x" || /^0x0+$/.test(historicalCode)) {
1749
- throw new Error("historical Settlement contract is unavailable on the configured network");
1750
- }
1751
- const historicalRuntimeHash = `0x${bytesToHex(keccak_256(hexToBytes(
1752
- historicalCode, "historical Settlement runtime",
1753
- )))}`;
1754
- if (historicalRuntimeHash !== history.source_runtime_code_hash) {
1755
- throw new Error("historical Settlement runtime differs from reputation history lineage");
1756
- }
1757
- const registryCode = String(await this.callRpc(rpc, "eth_getCode", [registry, "latest"]) || "");
1758
- if (!/^0x[0-9a-f]+$/i.test(registryCode) || registryCode === "0x" || /^0x0+$/i.test(registryCode)) {
1759
- throw new Error("jury registry contract is unavailable on the configured network");
1760
- }
1761
- const [settlementRegistry, settlementThreshold, registrySettlement, registryGovernance, reputationAuthority,
1762
- bondPenaltyRecipient, minimumReputation, jurySize, registryThreshold, selectionDelay, randomnessMode,
1763
- rosterVersion, providerCount, canFormJury] = await Promise.all([
1764
- this.contractCall(rpc, settlement, "juryRegistry()", []),
1765
- this.contractCall(rpc, settlement, "adjudicationThreshold()", []),
1766
- this.contractCall(rpc, registry, "settlement()", []),
1767
- this.contractCall(rpc, registry, "governance()", []),
1768
- this.contractCall(rpc, registry, "reputationAuthority()", []),
1769
- this.contractCall(rpc, registry, "bondPenaltyRecipient()", []),
1770
- this.contractCall(rpc, registry, "minimumReputation()", []),
1771
- this.contractCall(rpc, registry, "jurySize()", []),
1772
- this.contractCall(rpc, registry, "threshold()", []),
1773
- this.contractCall(rpc, registry, "selectionDelayBlocks()", []),
1774
- this.contractCall(rpc, registry, "RANDOMNESS_MODE_HASH()", []),
1775
- this.contractCall(rpc, registry, "rosterVersion()", []),
1776
- this.contractCall(rpc, registry, "providerCount()", []),
1777
- this.contractCall(rpc, registry, "canFormJury()", []),
1778
- ]);
1779
- const checks = [
1780
- [decodeAbiAddress(settlementRegistry, "Settlement juryRegistry"), registry, "Settlement juryRegistry"],
1781
- [decodeAbiUint(settlementThreshold, "Settlement adjudicationThreshold", 16), BigInt(this.network.adjudication_threshold), "Settlement adjudicationThreshold"],
1782
- [decodeAbiAddress(registrySettlement, "jury registry settlement"), settlement, "jury registry settlement"],
1783
- [decodeAbiAddress(registryGovernance, "jury registry governance"), this.network.jury_registry_governance, "jury registry governance"],
1784
- [decodeAbiAddress(reputationAuthority, "jury registry reputationAuthority"), this.network.reputation_authority, "jury registry reputationAuthority"],
1785
- [decodeAbiAddress(bondPenaltyRecipient, "jury registry bondPenaltyRecipient"), this.network.bond_penalty_recipient, "jury registry bondPenaltyRecipient"],
1786
- [decodeAbiUint(minimumReputation, "jury registry minimumReputation", 64), BigInt(this.network.minimum_provider_reputation), "jury registry minimumReputation"],
1787
- [decodeAbiUint(jurySize, "jury registry jurySize", 16), BigInt(this.network.jury_size), "jury registry jurySize"],
1788
- [decodeAbiUint(registryThreshold, "jury registry threshold", 16), BigInt(this.network.adjudication_threshold), "jury registry threshold"],
1789
- [decodeAbiUint(selectionDelay, "jury registry selectionDelayBlocks", 16), BigInt(this.network.jury_selection_delay_blocks), "jury registry selectionDelayBlocks"],
1790
- [normalizeBytes32(randomnessMode, "jury registry RANDOMNESS_MODE_HASH"), DYNAMIC_JURY_RANDOMNESS_HASH, "jury registry RANDOMNESS_MODE_HASH"],
1791
- ];
1792
- for (const [actual, expected, label] of checks) {
1793
- if (actual !== expected) throw new Error(`${label} does not match the deployment manifest`);
1794
- }
1795
- const liveProviderCount = decodeAbiUint(providerCount, "jury registry providerCount");
1796
- if (liveProviderCount < BigInt(this.network.jury_size)
1797
- || liveProviderCount > BigInt(MAX_DYNAMIC_PROVIDERS)) throw new Error("jury registry Provider pool is outside supported bounds");
1798
- if (decodeAbiUint(rosterVersion, "jury registry rosterVersion", 64) === 0n) throw new Error("jury registry reputation pool is uninitialized");
1799
- if (!decodeAbiBool(canFormJury, "jury registry canFormJury")) throw new Error("jury registry cannot form the declared jury");
1800
- return true;
1801
- }
1802
-
1803
- async settlementNetworkReady() {
1804
- return this.rpcValue(async (rpc) => {
1805
- const chainId = await this.callRpc(rpc, "eth_chainId", []);
1806
- if (BigInt(chainId) !== BigInt(this.network.chain_id)) {
1807
- const error = new Error("Settlement RPC is connected to the wrong chain");
1808
- error.code = "network_mismatch";
1809
- throw error;
1810
- }
1811
- const code = String(await this.callRpc(rpc, "eth_getCode", [this.network.settlement_contract, "latest"]) || "");
1812
- if (!/^0x[0-9a-f]+$/i.test(code) || code === "0x" || /^0x0+$/i.test(code)) {
1813
- const error = new Error("Settlement contract is unavailable on the configured network");
1814
- error.code = "settlement_unavailable";
1815
- throw error;
1816
- }
1817
- await this.dynamicJuryNetworkReady(rpc);
1818
- return true;
1819
- });
1820
- }
1821
-
1822
- async accountBalance(owner) {
1823
- const output = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.settlement_contract, "availableBalance(address)", [normalizeAddress(owner)]));
1824
- return BigInt(output || "0x0").toString();
1825
- }
1826
-
1827
- // These two reads are independent. Keep them in one helper so the local
1828
- // dashboard can fetch the wallet snapshot in one RPC round instead of
1829
- // serialising the token balance behind the allowance read. Both calls are
1830
- // read-only and still use rpcValue's endpoint failover independently.
1831
- async walletSnapshot(owner) {
1832
- const [token, allowance] = await Promise.all([
1833
- this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "balanceOf(address)", [normalizeAddress(owner)])),
1834
- this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "allowance(address,address)", [normalizeAddress(owner), this.network.settlement_contract])),
1835
- ]);
1836
- return {
1837
- address: owner,
1838
- token_balance_units: BigInt(token || "0x0").toString(),
1839
- allowance_units: BigInt(allowance || "0x0").toString(),
1840
- };
1841
- }
1842
-
1843
- async dashboardPayload(managementAuthorized = false) {
1844
- const authenticated = managementAuthorized && Boolean(this.unlockedWallet);
1845
- if (authenticated) {
1846
- await this.refreshDisputeTransactions().catch(() => {
1847
- this.disputeSyncError = "申诉交易状态暂时无法核验";
1848
- });
1849
- void this.refreshReceiptStatuses().catch(() => { this.historySyncError = "暂时无法同步账单状态"; });
1850
- }
1851
- const allHistory = this.history(0);
1852
- const now = Math.floor(Date.now() / 1000);
1853
- const dashboardHistory = authenticated ? allHistory.slice(0, 100).map((item) => {
1854
- const hasReference = Boolean(item.jury_evidence_hash && item.jury_report_id
1855
- && item.jury_reporter === this.unlockedWallet && item.jury_reference_signature);
1856
- const disputeActive = ["wallet_prompted", "submitted", "uncertain", "confirmed"].includes(item.dispute_stage);
1857
- const disputeEligible = this.network.protocol_version === 10
1858
- && this.network.committee_mode === DYNAMIC_PROVIDER_AI_COMMITTEE
1859
- && item.status === "escrowed" && hasReference && !disputeActive
1860
- && Number(item.settlement_release_at || 0) > now;
1861
- return {
1862
- ...item,
1863
- dispute_eligible: disputeEligible,
1864
- ...(item.dispute_tx_hash && this.network.explorer_url ? {
1865
- dispute_explorer_url: `${String(this.network.explorer_url).replace(/\/$/, "")}/tx/${item.dispute_tx_hash}`,
1866
- } : {}),
1867
- };
1868
- }) : [];
1869
- const unknownFees = authenticated ? allHistory.filter((item) => item.actual_fee_units == null
1870
- && !["not_dispatched", "failed", "rejected"].includes(item.status)) : [];
1871
- const pending = authenticated ? this.pendingPaymentKey() : null;
1872
- const payload = {
1873
- ok: true,
1874
- liveness_ready: true,
1875
- paid_readiness_checked: authenticated,
1876
- paid_ready: false,
1877
- wallet_ready: false,
1878
- network_ready: false,
1879
- budget_ready: false,
1880
- models_ready: false,
1881
- protocol_version: this.network.protocol_version,
1882
- runtime: "node-native",
1883
- auth: authenticated ? this.walletAuthPayload() : { authenticated: false, wallet: null, key_ready: false },
1884
- credentials: authenticated && this.paymentUnlocked
1885
- ? { base_url: this.baseUrl, api_key: this.paymentKey, export: this.credentialsText() }
1886
- : null,
1887
- key: {
1888
- address: this.paymentAddress,
1889
- max_fee_units: this.maxFeeUnits,
1890
- pending: pending ? { payment_key_address: pending.payment_key_address } : null,
1891
- },
1892
- settlement: this.network,
1893
- history: dashboardHistory,
1894
- history_scope: "current-payment-key-on-this-device",
1895
- history_sync: authenticated ? { running: Boolean(this.historySync), last_checked_at: this.historySyncAt, error: this.historySyncError } : null,
1896
- recovery: this.recoveryPayload(allHistory, authenticated),
1897
- dispute_sync: authenticated ? { running: Boolean(this.disputeSync), last_checked_at: this.disputeSyncAt || 0, error: this.disputeSyncError || null } : null,
1898
- usage: {
1899
- request_count: authenticated ? allHistory.length : 0,
1900
- total_spent_units: authenticated ? allHistory.reduce((total, item) => total + Number(item.actual_fee_units || 0), 0) : 0,
1901
- settled_units: authenticated ? allHistory.filter((item) => ["confirmed", "released", "dismissed", "timed_out"].includes(item.status)).reduce((sum, item) => sum + Number(item.actual_fee_units || 0), 0) : 0,
1902
- pending_units: authenticated ? allHistory.filter((item) => !["confirmed", "released", "refunded", "dismissed", "timed_out", "failed", "rejected"].includes(item.status)).reduce((sum, item) => sum + Number(item.actual_fee_units || 0), 0) : 0,
1903
- unknown_fee_count: unknownFees.length,
1904
- unknown_fee_authorized_maximum_units: unknownFees.reduce((sum, item) => sum + Number(item.max_fee_units || 0), 0),
1905
- refunded_units: authenticated ? allHistory.filter((item) => item.status === "refunded").reduce((sum, item) => sum + Number(item.actual_fee_units || 0), 0) : 0,
1906
- failed_units: authenticated ? allHistory.filter((item) => ["failed", "rejected"].includes(item.status)).reduce((sum, item) => sum + Number(item.actual_fee_units || 0), 0) : 0,
1907
- input_tokens: authenticated ? allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0) : 0,
1908
- output_tokens: authenticated ? allHistory.reduce((total, item) => total + Number(item.output_tokens || 0), 0) : 0,
1909
- },
1910
- share: authenticated ? this.sharePayload() : { active: false },
1911
- };
1912
- let channelSnapshot = null;
1913
- let dashboardGrant = null;
1914
- // Chain/code identity, key ownership, and fixed-budget channel state are
1915
- // independent reads. Preserve successful snapshots when one RPC fails.
1916
- const [networkResult, grantResult, channelsResult] = await Promise.allSettled([
1917
- this.settlementNetworkReady(),
1918
- this.keyGrant(this.paymentAddress),
1919
- authenticated && this.network.protocol_version === 10 ? this.capacityChannels() : Promise.resolve(null),
1920
- ]);
1921
- payload.network_ready = networkResult.status === "fulfilled" && networkResult.value === true;
1922
- if (grantResult.status === "fulfilled") {
1923
- dashboardGrant = grantResult.value;
1924
- payload.key.grant = dashboardGrant;
1925
- }
1926
- if (channelsResult.status === "fulfilled") channelSnapshot = channelsResult.value;
1927
- else if (authenticated && this.network.protocol_version === 10) {
1928
- // Capacity is chain state, not an empty budget. If it cannot be read,
1929
- // fail the network dimension instead of telling the user to fund again.
1930
- payload.network_ready = false;
1931
- }
1932
- const chainErrors = [networkResult, grantResult, channelsResult]
1933
- .filter((result) => result.status === "rejected")
1934
- .map((result) => result.reason?.message || String(result.reason));
1935
- if (chainErrors.length) payload.chain_error = chainErrors.join("; ");
1936
- if (authenticated && this.network.protocol_version === 10 && channelSnapshot) {
1937
- const now = Math.floor(Date.now()/1000);
1938
- payload.capacity_channels = channelSnapshot.filter(c => c.consumer_owner === this.unlockedWallet)
1939
- .map(c => ({ ...c, budget: this.capacityBudget(c, allHistory, now) }));
1940
- payload.budget_locked_units = payload.capacity_channels.filter(c=>!c.closed).reduce((n,c)=>n+BigInt(c.credit_remaining),0n).toString();
1941
- payload.budget_available_units = payload.capacity_channels.filter(c => c.budget.ready)
1942
- .reduce((total,c) => total+BigInt(c.budget.remaining_units),0n).toString();
1943
- payload.budget_note = "固定预算在结算窗口结束前不会撤回;每次请求只占用授权上限。结算按 2 小时或 100 笔先到触发。撤销 Key 只会停止新请求,不会改变已有账单。";
1944
- }
1945
- if (authenticated && this.paymentUnlocked) {
1946
- const now = Math.floor(Date.now() / 1000);
1947
- const activeGrant = Boolean(dashboardGrant
1948
- && dashboardGrant.owner === this.unlockedWallet && dashboardGrant.active === true
1949
- && Number(dashboardGrant.max_per_request) >= this.maxFeeUnits
1950
- && (Number(dashboardGrant.valid_until) === 0 || Number(dashboardGrant.valid_until) > now + Math.ceil(this.timeoutMs / 1000)));
1951
- const activeChannel = this.network.protocol_version === 10 && Array.isArray(channelSnapshot)
1952
- && channelSnapshot.some((channel) => !channel.closed && channel.consumer_owner === this.unlockedWallet
1953
- && channel.consumer_key === this.paymentAddress && Number(channel.admit_until) > now);
1954
- payload.wallet_ready = activeGrant || activeChannel;
1955
- }
1956
- if (authenticated) {
1957
- if (dashboardGrant && (dashboardGrant.owner === this.unlockedWallet
1958
- || (this.network.protocol_version === 10 && dashboardGrant.owner === ZERO_ADDRESS))) {
1959
- try {
1960
- payload.account = { owner: this.unlockedWallet, available_balance_units: await this.accountBalance(this.unlockedWallet) };
1961
- } catch (error) {
1962
- payload.account_error = error.message;
1963
- }
1964
- }
1965
- try {
1966
- const address = this.unlockedWallet;
1967
- payload.wallet = await this.walletSnapshot(address);
1968
- } catch (error) {
1969
- payload.wallet_error = error.message;
1970
- }
1971
- try {
1972
- const selected = await this.chooseRelay(new Set(), { checkCapacity: false });
1973
- this.verifyRelayIdentity(selected.relayUrl, selected.health, { requirePin: true });
1974
- payload.models_ready = true;
1975
- } catch (error) {
1976
- payload.models_ready = false;
1977
- payload.inference_error = error.message;
1978
- payload.inference_code = error.code || "models_unavailable";
1979
- }
1980
- if (this.network.protocol_version === 10) {
1981
- const channels = payload.capacity_channels || [];
1982
- const ready = channels.filter((channel) => channel.budget?.ready);
1983
- if (!ready.length) {
1984
- const availableAt = channels.map((channel) => channel.budget?.available_at).filter(Number.isFinite).sort((a, b) => a - b)[0];
1985
- payload.budget_ready = false;
1986
- payload.inference_code = availableAt ? "budget_not_started" : "budget_unavailable";
1987
- payload.inference_error = availableAt
1988
- ? `Fixed budget becomes available at ${new Date(availableAt * 1000).toISOString()}`
1989
- : "No active funded channel covers this model and Provider; renew the fixed budget";
1990
- if (availableAt) payload.budget_available_at = availableAt;
1991
- } else {
1992
- try {
1993
- // A locally usable channel is not enough: require a currently
1994
- // healthy Relay/Provider route bound to that same channel.
1995
- payload.budget_ready = Boolean(await this.chooseRelay());
1996
- } catch (error) {
1997
- payload.budget_ready = false;
1998
- payload.inference_error = error.message;
1999
- payload.inference_code = error.code || "budget_unavailable";
2000
- if (error.code === "rpc_unavailable") payload.network_ready = false;
2001
- if (error.availableAt) payload.budget_available_at = error.availableAt;
2002
- }
2003
- }
2004
- } else {
2005
- payload.budget_ready = Boolean(payload.account
2006
- && BigInt(payload.account.available_balance_units) >= BigInt(this.maxFeeUnits));
2007
- if (!payload.budget_ready && !payload.inference_code) payload.inference_code = "budget_unavailable";
2008
- }
2009
- payload.paid_ready = payload.wallet_ready && payload.network_ready && payload.budget_ready && payload.models_ready;
2010
- payload.inference_ready = payload.paid_ready;
2011
- if (!payload.paid_ready) {
2012
- payload.inference_code = !payload.network_ready ? "network_unavailable"
2013
- : !payload.wallet_ready ? "payment_key_not_ready"
2014
- : !payload.models_ready ? "models_unavailable"
2015
- : payload.inference_code === "budget_not_started" ? "budget_not_started" : "budget_unavailable";
2016
- }
2017
- }
2018
- return payload;
2019
- }
2020
-
2021
- async queryReceiptStatus(entry, privateKey = this.paymentKey) {
2022
- if (!this.relayUrls.includes(entry.relay_url)) return null;
2023
- const query = createReceiptStatusQuery(privateKey, { chainId: this.network.chain_id,
2024
- contract: this.network.settlement_contract, requestId: entry.request_id, channelId: entry.capacity_channel_id });
2025
- const response = await fetchWithTimeout(`${entry.relay_url}/v1/mycomesh/receipts/status`, {
2026
- method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(query), dispatcher: this.dispatcher,
2027
- }, 5000);
2028
- const value = await readJsonResponse(response);
2029
- if (response.status === 404) return null; // Old Relay or an unknown receipt; never assume settled/failed.
2030
- if (!response.ok || value.request_id !== entry.request_id || (entry.capacity_channel_id && value.channel_id !== entry.capacity_channel_id)) throw new Error("invalid Relay receipt status response");
2031
- if (!["pending", "submitted", "broadcast_unknown", "confirmed", "escrowed", "failed"].includes(value.status)) throw new Error("invalid receipt state");
2032
- return value;
2033
- }
2034
-
2035
- async refreshReceiptStatuses(force = false) {
2036
- if (!this.unlockedWallet || !this.managementToken) return;
2037
- if (this.historySync) return this.historySync;
2038
- if (!force && Date.now() - this.historySyncAt < 30_000) return;
2039
- this.historySyncAt = Date.now();
2040
- const owner = this.unlockedWallet, key = this.paymentAddress, privateKey = this.paymentKey, ledger = this.historyLedger;
2041
- const candidates = ledger.history(0).filter((entry) => (entry.accepted || ["dispatching", "outcome_unknown"].includes(entry.status)) && !["confirmed", "released", "refunded", "dismissed", "timed_out"].includes(entry.status)
2042
- && /^0x[0-9a-f]{64}$/i.test(entry.request_id));
2043
- const live = candidates.filter((entry) => !["failed", "rejected"].includes(entry.status));
2044
- const terminal = candidates.filter((entry) => ["failed", "rejected"].includes(entry.status));
2045
- this.historySyncCursors ||= { live: 0, terminal: 0 };
2046
- const take = (rows, count, group) => {
2047
- if (!rows.length) return [];
2048
- const start = this.historySyncCursors[group] % rows.length;
2049
- const selected = Array.from({ length: Math.min(count, rows.length) }, (_, index) => rows[(start + index) % rows.length]);
2050
- this.historySyncCursors[group] = (start + selected.length) % rows.length;
2051
- return selected;
2052
- };
2053
- // Reserve a small share for rechecking failed records while rotating all
2054
- // live work. Old pending receipts cannot be hidden by recent failures.
2055
- const pending = take(live, terminal.length ? 16 : 20, "live");
2056
- pending.push(...take(terminal, 20 - pending.length, "terminal"));
2057
- this.historySyncError = null;
2058
- const check = async () => {
2059
- while (pending.length) {
2060
- const entry = pending.shift();
2061
- try {
2062
- const settlementKey = this.network.protocol_version === 10 ? reservedSettlementKey(entry.capacity_channel_id, entry.request_id) : keccakHex(Buffer.concat([abiWord(owner), abiWord(key), abiWord(entry.request_id)]));
2063
- if (this.network.protocol_version >= 9) {
2064
- const snapshot = await this.rpcValue(async (rpc) => {
2065
- const block = canonicalBlock(
2066
- await this.callRpc(rpc, "eth_getBlockByNumber", ["safe", false]),
2067
- "safe Settlement block",
2068
- );
2069
- const record = await this.contractCall(rpc, this.network.settlement_contract, "settlementInfo(bytes32)", [settlementKey],
2070
- { blockHash: block.hash, requireCanonical: true });
2071
- return { record, block };
2072
- });
2073
- const result = snapshot.record;
2074
- if (typeof result !== "string" || !/^0x[0-9a-f]{1280}$/i.test(result)) throw new Error("invalid V9 settlement record");
2075
- const words = result.slice(2).match(/.{64}/g);
2076
- if (words.slice(0, 8).some((word) => !/^0{24}/.test(word)) || BigInt(`0x${words[19]}`) > 6n
2077
- || BigInt(`0x${words[17]}`) >= (1n << 64n) || BigInt(`0x${words[18]}`) >= (1n << 64n)) throw new Error("noncanonical V9 settlement record");
2078
- const status = [null, "escrowed", "disputed", "released", "refunded", "dismissed", "timed_out"][Number(BigInt(`0x${words[19]}`))];
2079
- if (status) {
2080
- if (`0x${words[0].slice(-40)}` !== owner.toLowerCase() || `0x${words[1].slice(-40)}` !== key.toLowerCase() || `0x${words[8]}` !== entry.request_id.toLowerCase()
2081
- || (entry.request_hash && `0x${words[9]}` !== entry.request_hash.toLowerCase())
2082
- || (entry.actual_fee_units != null && BigInt(`0x${words[12]}`) !== BigInt(entry.actual_fee_units))
2083
- || (entry.provider && `0x${words[2].slice(-40)}` !== entry.provider.toLowerCase())
2084
- || (entry.provider_signer && `0x${words[3].slice(-40)}` !== entry.provider_signer.toLowerCase())
2085
- || (entry.response_hash && `0x${words[11]}` !== entry.response_hash.toLowerCase())) throw new Error("V9 escrow scope mismatch");
2086
- const releaseAt = Number(BigInt(`0x${words[18]}`));
2087
- if (entry.status !== status || entry.settlement_release_at !== releaseAt) {
2088
- ledger.append({ ...entry, owner, status, accepted: true,
2089
- actual_fee_units: jsonInteger(BigInt(`0x${words[12]}`)),
2090
- settlement_release_at: releaseAt,
2091
- settlement_checked_at: snapshot.block.timestamp,
2092
- updated_at: Math.floor(Date.now() / 1000) });
2093
- }
2094
- continue;
2095
- }
2096
- // No safe-block escrow: a Relay cannot declare escrow or release.
2097
- const remote = await this.queryReceiptStatus(entry, privateKey);
2098
- if (remote && ["pending", "submitted", "broadcast_unknown", "failed"].includes(remote.status) && remote.status !== entry.status) ledger.append({ ...entry, accepted: true, status: remote.status, updated_at: Math.floor(Date.now() / 1000) });
2099
- continue;
2100
- }
2101
- const result = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.settlement_contract,
2102
- "settled(bytes32)", [settlementKey]));
2103
- if (BigInt(result || "0x0") === 1n) {
2104
- ledger.append({ ...entry, owner, status: "confirmed", confirmed_at: Math.floor(Date.now() / 1000) });
2105
- } else {
2106
- const remote = await this.queryReceiptStatus(entry, privateKey);
2107
- if (remote && remote.status !== "confirmed") {
2108
- // A Relay cannot claim a final on-chain success without the RPC
2109
- // check above. Its scoped authenticated status can report failure.
2110
- const update = { status: remote.status, error_code: remote.error_code ?? (entry.error_code ? "" : undefined),
2111
- tx_hash: remote.tx_hash, authorization_deadline: remote.authorization_deadline || entry.authorization_deadline };
2112
- if (Object.entries(update).some(([field, value]) => value != null && value !== entry[field])) {
2113
- ledger.append({ ...entry, ...update, accepted: true, updated_at: Math.floor(Date.now() / 1000) });
2114
- }
2115
- }
2116
- }
2117
- } catch {
2118
- this.historySyncError = "暂时无法同步链上结算状态,已保留本地账单";
2119
- pending.length = 0;
2120
- }
2121
- }
2122
- };
2123
- this.historySync = Promise.all([check(), check(), check()]);
2124
- try { await this.historySync; } finally { this.historySync = null; }
2125
- }
2126
-
2127
- sharePayload() {
2128
- const share = this.activeShare();
2129
- return share ? {
2130
- active: true,
2131
- base_url: share.baseUrl,
2132
- api_key: share.apiKey,
2133
- expires_at: share.expiresAt,
2134
- } : { active: false };
2135
- }
2136
-
2137
- activeShare() {
2138
- if (this.share && this.share.expiresAt <= Math.floor(Date.now() / 1000)) {
2139
- void this.stopShare();
2140
- return null;
2141
- }
2142
- return this.share;
2143
- }
2144
-
2145
- authorizeBearer(authorization, { shareOnly = false } = {}) {
2146
- if (!shareOnly && this.paymentUnlocked && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
2147
- const share = this.activeShare();
2148
- return Boolean(share && sameSecret(authorization, `Bearer ${share.apiKey}`));
2149
- }
2150
-
2151
- async startShare(minutesValue) {
2152
- if (!this.paymentUnlocked) throw new Error("sign in and activate the payment key before sharing");
2153
- const minutes = Number(minutesValue);
2154
- if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_SHARE_MINUTES) {
2155
- throw new Error(`share duration must be between 1 and ${MAX_SHARE_MINUTES} minutes`);
2156
- }
2157
- await this.stopShare();
2158
- const runtime = createConsumerServer(this, { host: "127.0.0.1", port: 0, publicOnly: true });
2159
- const address = await runtime.listen();
2160
- const apiKey = `myco_share_${randomBytes(24).toString("base64url")}`;
2161
- const share = {
2162
- apiKey,
2163
- baseUrl: "",
2164
- expiresAt: Math.floor(Date.now() / 1000) + minutes * 60,
2165
- process: null,
2166
- runtime,
2167
- timer: null,
2168
- };
2169
- this.share = share;
2170
- try {
2171
- let publicUrl;
2172
- let lastError;
2173
- for (let attempt = 0; attempt < 2; attempt += 1) {
2174
- const child = this.tunnelSpawn(
2175
- this.tunnelCommand,
2176
- ["tunnel", "--no-autoupdate", "--protocol", "http2", "--url", `http://127.0.0.1:${address.port}`],
2177
- { stdio: ["ignore", "pipe", "pipe"] },
2178
- );
2179
- share.process = child;
2180
- try {
2181
- publicUrl = await waitForTunnelUrl(child);
2182
- break;
2183
- } catch (error) {
2184
- lastError = error;
2185
- await stopTunnelProcess(child);
2186
- if (share.process === child) share.process = null;
2187
- if (this.share !== share || attempt === 1) throw error;
2188
- await new Promise((resolve) => setTimeout(resolve, 1000));
2189
- }
2190
- }
2191
- if (!publicUrl) throw lastError || new Error("cloudflared did not publish a URL");
2192
- if (this.share !== share) throw new Error("temporary share was cancelled");
2193
- share.baseUrl = `${publicUrl.replace(/\/+$/, "")}/v1`;
2194
- share.timer = setTimeout(() => { void this.stopShare(); }, minutes * 60 * 1000);
2195
- share.timer.unref?.();
2196
- share.process?.once("exit", () => { if (this.share === share) void this.stopShare(); });
2197
- return this.sharePayload();
2198
- } catch (error) {
2199
- await this.stopShare();
2200
- throw error;
2201
- }
2202
- }
2203
-
2204
- async stopShare() {
2205
- const share = this.share;
2206
- this.share = null;
2207
- if (!share) return { active: false };
2208
- if (share.timer) clearTimeout(share.timer);
2209
- await stopTunnelProcess(share.process);
2210
- await share.runtime?.close();
2211
- return { active: false };
2212
- }
2213
-
2214
- pendingPaymentKey() {
2215
- if (!existsSync(this.pendingKeyPath)) return null;
2216
- const value = readFileSync(this.pendingKeyPath, "utf8").trim();
2217
- paymentPrivateKey(value);
2218
- return { payment_key: value, payment_key_address: paymentKeyAddress(value) };
2219
- }
2220
-
2221
- preparePaymentKey() {
2222
- if (this.paymentKeyFromEnv) throw new Error("payment-key rotation is disabled while MYCOMESH_V8_PAYMENT_KEY is set");
2223
- const pending = this.pendingPaymentKey();
2224
- if (pending) return pending;
2225
- const value = generatePaymentKey();
2226
- writeFileSync(this.pendingKeyPath, `${value}\n`, { mode: 0o600, flag: "wx" });
2227
- return { payment_key: value, payment_key_address: paymentKeyAddress(value) };
2228
- }
2229
-
2230
- async activatePendingPaymentKey(wallet) {
2231
- const owner = this.assertUnlockedWallet(wallet);
2232
- const pending = this.pendingPaymentKey();
2233
- if (!pending) throw new Error("no pending payment key exists");
2234
- const grant = await this.keyGrant(pending.payment_key_address);
2235
- if (!grant.active || grant.owner !== owner) throw new Error("the pending payment key is not active for this wallet on-chain");
2236
- if (this.activeInferenceRequests > 0) {
2237
- throw new Error("wait for active inference requests before rotating the payment key");
2238
- }
2239
- const previous = this.paymentAddress;
2240
- const destination = join(this.dataDir, "payment-key");
2241
- writeFileSync(destination, `${pending.payment_key}\n`, { mode: 0o600 });
2242
- chmodSync(destination, 0o600);
2243
- unlinkSync(this.pendingKeyPath);
2244
- this.paymentKey = pending.payment_key;
2245
- this.paymentAddress = pending.payment_key_address;
2246
- this.sessions = new ConsumerSessions();
2247
- this.sessionRoutes.clear();
2248
- this.historyLedger = new ConsumerHistoryLedger({ localPath: this.historyPath, sharedDir: this.historyDir,
2249
- chainId: this.network.chain_id, contract: this.network.settlement_contract, keyAddress: this.paymentAddress });
2250
- this.historySyncAt = 0;
2251
- this.paymentUnlocked = true;
2252
- return { payment_key_address: this.paymentAddress, previous_key_address: previous };
2253
- }
2254
-
2255
- async transactionPlan(raw) {
2256
- const action = String(raw?.action || "");
2257
- const wallet = this.assertUnlockedWallet(raw?.wallet);
2258
- const settlement = this.network.settlement_contract;
2259
- const token = this.network.stablecoin;
2260
- if (action === "open_dispute") {
2261
- if (this.network.protocol_version !== 10
2262
- || this.network.committee_mode !== DYNAMIC_PROVIDER_AI_COMMITTEE) {
2263
- throw new Error("open_dispute requires the dynamic Settlement V10 jury");
2264
- }
2265
- const allowed = new Set(["action", "wallet", "request_id", "channel_id"]);
2266
- if (!raw || Object.keys(raw).some((name) => !allowed.has(name))) {
2267
- throw new Error("open_dispute accepts only a local request and channel reference");
2268
- }
2269
- const { entry, reference, requestId, channelId, settlementKey } = this.disputeHistoryEntry(raw, wallet);
2270
- if (["wallet_prompted", "submitted", "uncertain", "confirmed"].includes(entry.dispute_stage)) {
2271
- throw new Error("this dispute is already awaiting a wallet or chain outcome");
2272
- }
2273
- const snapshot = await this.disputePlanningSnapshot(entry, reference, wallet);
2274
- const transactions = [];
2275
- if (snapshot.allowance < snapshot.bond) {
2276
- transactions.push({
2277
- kind: "approval",
2278
- label: "Approve exact reporter bond",
2279
- to: token,
2280
- data: contractData("approve(address,uint256)", [settlement, snapshot.bond]),
2281
- });
2282
- }
2283
- transactions.push({
2284
- kind: "open_dispute",
2285
- label: "Open objective-fraud dispute",
2286
- to: settlement,
2287
- data: contractData("openDispute(bytes32,bytes32)", [settlementKey, reference.evidence_hash]),
2288
- });
2289
- const now = Math.floor(Date.now() / 1000);
2290
- this.historyLedger.append({
2291
- ...entry,
2292
- dispute_stage: "planned",
2293
- dispute_tx_kind: "open_dispute",
2294
- dispute_bond_units: snapshot.bond.toString(),
2295
- settlement_release_at: snapshot.record.release_at,
2296
- settlement_checked_at: snapshot.block.timestamp,
2297
- dispute_updated_at: now,
2298
- dispute_error: "",
2299
- updated_at: now,
2300
- });
2301
- return {
2302
- action,
2303
- request_id: requestId,
2304
- channel_id: channelId,
2305
- settlement_key: settlementKey,
2306
- evidence_hash: reference.evidence_hash,
2307
- report_id: reference.predicted_report_id,
2308
- reporter: reference.reporter,
2309
- bond_units: snapshot.bond.toString(),
2310
- release_at: snapshot.record.release_at,
2311
- chain_timestamp: snapshot.block.timestamp,
2312
- warning: "Only report objective fraud or non-delivery supported by the signed evidence commitment.",
2313
- transactions,
2314
- };
2315
- }
2316
- if (action === "top_up") {
2317
- const amount = parseUsdc(raw.amount_usdc);
2318
- const allowance = BigInt(await this.rpcValue((rpc) => this.contractCall(rpc, token, "allowance(address,address)", [wallet, settlement])) || "0x0");
2319
- const transactions = [];
2320
- if (allowance < amount) transactions.push({ label: "Approve stablecoin", to: token, data: contractData("approve(address,uint256)", [settlement, amount]) });
2321
- transactions.push({ label: "Deposit prepaid balance", to: settlement, data: contractData("deposit(uint256)", [amount]) });
2322
- return { action, amount_units: amount.toString(), transactions };
2323
- }
2324
- if (action === "register_key") {
2325
- const pending = this.pendingPaymentKey();
2326
- const keyAddress = pending?.payment_key_address || this.paymentAddress;
2327
- const grant = await this.keyGrant(keyAddress);
2328
- if (grant.owner === wallet && grant.active === true
2329
- && grant.max_per_request === this.maxFeeUnits && grant.valid_until === 0) {
2330
- return { action, key_address: keyAddress, transactions: [] };
2331
- }
2332
- return { action, key_address: keyAddress, transactions: [{ label: "Register payment key", to: settlement, data: contractData("registerKey(address,uint256,uint64)", [keyAddress, this.maxFeeUnits, 0]) }] };
2333
- }
2334
- if (action === "revoke_key") {
2335
- const keyAddress = normalizeAddress(raw.key_address, "key_address");
2336
- return { action, key_address: keyAddress, transactions: [{ label: "Revoke previous payment key", to: settlement, data: contractData("revokeKey(address)", [keyAddress]) }] };
2337
- }
2338
- if (action === "close_capacity_channel" && this.network.protocol_version === 10) {
2339
- const id = normalizeBytes32(raw.channel_id, "channel_id");
2340
- const channel = (await this.capacityChannels()).find(c => c.channel_id === id && c.consumer_owner === wallet);
2341
- if (!channel || channel.closed || Math.floor(Date.now()/1000) <= Number(channel.claim_until)) throw new Error("该预算尚未到可释放时间");
2342
- return { action, channel_id: id, transactions: [{ label: "释放已到期的剩余预算", to: settlement, data: contractData("closeExpiredChannel(bytes32)", [id]) }] };
2343
- }
2344
- throw new Error("unsupported transaction action");
2345
- }
2346
-
2347
- disputeHistoryEntry(raw, walletValue = this.unlockedWallet) {
2348
- const wallet = normalizeAddress(walletValue, "wallet");
2349
- const requestId = normalizeBytes32(raw?.request_id, "request_id");
2350
- const channelId = normalizeBytes32(raw?.channel_id, "channel_id");
2351
- const matches = this.historyLedger.history(0).filter((item) => (
2352
- item.request_id === requestId && item.capacity_channel_id === channelId
2353
- ));
2354
- if (matches.length !== 1) throw new Error("no unique locally verified receipt exists for this request and channel");
2355
- const entry = matches[0];
2356
- if (entry.owner !== wallet || entry.jury_reporter !== wallet) {
2357
- throw new Error("only the authenticated settlement owner may open this dispute");
2358
- }
2359
- const settlementKey = reservedSettlementKey(channelId, requestId);
2360
- if (entry.settlement_key !== settlementKey) throw new Error("history settlement key is not canonical");
2361
- const reference = restoreJuryEvidenceReference(entry, this.network, wallet);
2362
- if (reference.settlement_key !== settlementKey) throw new Error("jury evidence belongs to another settlement");
2363
- return { entry, reference, requestId, channelId, settlementKey };
2364
- }
2365
-
2366
- async disputePlanningSnapshot(entry, reference, wallet) {
2367
- const settlement = this.network.settlement_contract;
2368
- const token = this.network.stablecoin;
2369
- const manifestBondText = String(this.network.reporter_bond_units || "");
2370
- if (!/^[1-9][0-9]*$/.test(manifestBondText)) {
2371
- throw new Error("dynamic jury reporter bond is not pinned by the network manifest");
2372
- }
2373
- const manifestBond = BigInt(manifestBondText);
2374
- return this.rpcValue(async (rpc) => {
2375
- const chainId = await this.callRpc(rpc, "eth_chainId", []);
2376
- if (BigInt(chainId) !== BigInt(this.network.chain_id)) throw new Error("Settlement RPC is connected to the wrong chain");
2377
- const block = canonicalBlock(
2378
- await this.callRpc(rpc, "eth_getBlockByNumber", ["safe", false]),
2379
- "safe Settlement V10 block",
2380
- );
2381
- const blockTag = { blockHash: block.hash, requireCanonical: true };
2382
- const [rawRecord, rawPolicy, rawAllowance, rawBalance, rawRegistry] = await Promise.all([
2383
- this.contractCall(rpc, settlement, "settlementInfo(bytes32)", [reference.settlement_key], blockTag),
2384
- this.contractCall(rpc, settlement, "policy()", [], blockTag),
2385
- this.contractCall(rpc, token, "allowance(address,address)", [wallet, settlement], blockTag),
2386
- this.contractCall(rpc, token, "balanceOf(address)", [wallet], blockTag),
2387
- this.contractCall(rpc, settlement, "juryRegistry()", [], blockTag),
2388
- ]);
2389
- const record = decodeV10Settlement(rawRecord);
2390
- const policy = decodeV10Policy(rawPolicy);
2391
- const allowance = decodeAbiUint(rawAllowance, "reporter bond allowance");
2392
- const balance = decodeAbiUint(rawBalance, "reporter bond balance");
2393
- const registry = decodeAbiAddress(rawRegistry, "Settlement juryRegistry");
2394
- if (registry !== this.network.jury_registry) throw new Error("Settlement jury Registry differs from the network manifest");
2395
- if (policy.reporter_bond !== manifestBond) throw new Error("on-chain reporter bond differs from the network manifest");
2396
- if (balance < policy.reporter_bond) throw new Error("wallet balance is below the required reporter bond");
2397
- if (record.status !== 1) throw new Error("settlement is not pending and cannot be disputed");
2398
- if (record.owner !== wallet || record.key !== this.paymentAddress
2399
- || record.request_id !== entry.request_id
2400
- || record.request_hash !== entry.request_hash
2401
- || record.response_hash !== entry.response_hash) {
2402
- throw new Error("on-chain settlement differs from the locally verified receipt");
2403
- }
2404
- if (block.timestamp >= record.release_at) throw new Error("the settlement dispute window has closed");
2405
- if (reference.reporter !== wallet
2406
- || reference.predicted_report_id !== keccakHex(Buffer.concat([
2407
- abiWord(reference.settlement_key), abiWord(wallet), abiWord(reference.evidence_hash),
2408
- ]))) {
2409
- throw new Error("jury evidence report binding is invalid");
2410
- }
2411
- return { block, record, policy, allowance, balance, bond: policy.reporter_bond };
2412
- });
2413
- }
2414
-
2415
- recordDisputeProgress(raw) {
2416
- const wallet = this.assertUnlockedWallet(raw?.wallet);
2417
- const { entry } = this.disputeHistoryEntry(raw, wallet);
2418
- const stage = String(raw?.stage || "");
2419
- const kind = String(raw?.transaction_kind || entry.dispute_tx_kind || "");
2420
- if (!["wallet_prompted", "submitted", "uncertain", "failed"].includes(stage)
2421
- || !["approval", "open_dispute"].includes(kind)) {
2422
- throw new Error("invalid dispute transaction progress");
2423
- }
2424
- const previous = entry.dispute_stage;
2425
- const allowed = (
2426
- (stage === "wallet_prompted" && (previous === "planned"
2427
- || (["submitted", "uncertain"].includes(previous) && entry.dispute_tx_kind === "approval" && kind === "open_dispute")))
2428
- || (stage === "submitted" && previous === "wallet_prompted")
2429
- || (stage === "uncertain" && ["wallet_prompted", "submitted", "uncertain"].includes(previous))
2430
- || (stage === "failed" && ["planned", "wallet_prompted", "submitted", "uncertain"].includes(previous))
2431
- );
2432
- if (!allowed) throw new Error("invalid dispute transaction state transition");
2433
- const txHash = raw?.tx_hash === undefined ? null : normalizeBytes32(raw.tx_hash, "transaction hash");
2434
- if (stage === "submitted" && !txHash) throw new Error("submitted dispute progress requires a transaction hash");
2435
- if (stage === "uncertain" && !txHash
2436
- && !entry.dispute_tx_hash && !entry.dispute_approval_tx_hash) {
2437
- // The wallet may have accepted a transaction without returning its hash.
2438
- // Persist uncertainty and block automatic retry instead of guessing.
2439
- }
2440
- const errors = {
2441
- wallet_rejected: "The wallet rejected the transaction before submission.",
2442
- wallet_unknown: "The wallet outcome is unknown; do not retry automatically.",
2443
- confirmation_timeout: "The transaction was submitted but confirmation is still unknown.",
2444
- transaction_failed: "The transaction failed on-chain.",
2445
- };
2446
- const now = Math.floor(Date.now() / 1000);
2447
- const update = {
2448
- ...entry,
2449
- dispute_stage: stage,
2450
- dispute_tx_kind: kind,
2451
- dispute_updated_at: now,
2452
- dispute_error: errors[String(raw?.error_code || "")] || "",
2453
- updated_at: now,
2454
- };
2455
- if (txHash && kind === "approval") update.dispute_approval_tx_hash = txHash;
2456
- if (txHash && kind === "open_dispute") update.dispute_tx_hash = txHash;
2457
- this.historyLedger.append(update);
2458
- return { ok: true, request_id: entry.request_id, channel_id: entry.capacity_channel_id,
2459
- stage, transaction_kind: kind, tx_hash: txHash };
2460
- }
2461
-
2462
- async reconcileDisputeEntry(entry) {
2463
- const wallet = this.assertUnlockedWallet();
2464
- const { entry: checked, reference } = this.disputeHistoryEntry({
2465
- request_id: entry.request_id,
2466
- channel_id: entry.capacity_channel_id,
2467
- }, wallet);
2468
- const kind = checked.dispute_tx_kind;
2469
- const txHash = kind === "approval" ? checked.dispute_approval_tx_hash : checked.dispute_tx_hash;
2470
- if (!txHash || !["approval", "open_dispute"].includes(kind)) return { state: "pending" };
2471
- const bond = BigInt(String(checked.dispute_bond_units || ""));
2472
- if (bond <= 0n) throw new Error("stored dispute reporter bond is invalid");
2473
- const expectedTo = kind === "approval" ? this.network.stablecoin : this.network.settlement_contract;
2474
- const expectedData = kind === "approval"
2475
- ? contractData("approve(address,uint256)", [this.network.settlement_contract, bond])
2476
- : contractData("openDispute(bytes32,bytes32)", [reference.settlement_key, reference.evidence_hash]);
2477
- const result = await this.rpcValue(async (rpc) => {
2478
- const [receipt, transaction] = await Promise.all([
2479
- this.callRpc(rpc, "eth_getTransactionReceipt", [txHash]),
2480
- this.callRpc(rpc, "eth_getTransactionByHash", [txHash]),
2481
- ]);
2482
- if (receipt == null) return { state: "pending" };
2483
- if (!transaction || typeof transaction !== "object") throw new Error("submitted dispute transaction is unavailable");
2484
- if (String(receipt.transactionHash || "").toLowerCase() !== txHash
2485
- || String(transaction.hash || "").toLowerCase() !== txHash
2486
- || String(receipt.blockHash || "").toLowerCase() !== String(transaction.blockHash || "").toLowerCase()
2487
- || String(receipt.blockNumber || "").toLowerCase() !== String(transaction.blockNumber || "").toLowerCase()
2488
- || normalizeAddress(receipt.from) !== wallet || normalizeAddress(transaction.from) !== wallet
2489
- || normalizeAddress(receipt.to) !== expectedTo || normalizeAddress(transaction.to) !== expectedTo
2490
- || String(transaction.input || "").toLowerCase() !== expectedData.toLowerCase()) {
2491
- throw new Error("submitted transaction differs from the dispute plan");
2492
- }
2493
- if (!/^0x[0-9a-f]{64}$/i.test(String(receipt.blockHash || ""))
2494
- || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(String(receipt.blockNumber || ""))
2495
- || !["0x0", "0x1"].includes(String(receipt.status || "").toLowerCase())) {
2496
- throw new Error("submitted transaction receipt is non-canonical");
2497
- }
2498
- const [rawBlock, rawCanonicalBlock, rawHead] = await Promise.all([
2499
- this.callRpc(rpc, "eth_getBlockByHash", [receipt.blockHash, false]),
2500
- this.callRpc(rpc, "eth_getBlockByNumber", [receipt.blockNumber, false]),
2501
- this.callRpc(rpc, "eth_blockNumber", []),
2502
- ]);
2503
- const block = canonicalBlock(rawBlock, "dispute transaction block");
2504
- const canonicalHeightBlock = canonicalBlock(rawCanonicalBlock, "canonical dispute transaction block");
2505
- if (block.hash !== String(receipt.blockHash).toLowerCase()
2506
- || block.number !== String(receipt.blockNumber).toLowerCase()
2507
- || canonicalHeightBlock.hash !== block.hash
2508
- || canonicalHeightBlock.number !== block.number) {
2509
- throw new Error("dispute transaction block is not canonical");
2510
- }
2511
- if (!/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(String(rawHead || ""))) {
2512
- throw new Error("dispute confirmation head is non-canonical");
2513
- }
2514
- const head = BigInt(rawHead);
2515
- const receiptHeight = BigInt(block.number);
2516
- if (head < receiptHeight) throw new Error("dispute confirmation head predates the receipt");
2517
- const confirmations = head - receiptHeight + 1n;
2518
- if (confirmations < DISPUTE_CONFIRMATIONS) {
2519
- return { state: "pending", confirmations: Number(confirmations) };
2520
- }
2521
- if (String(receipt.status).toLowerCase() === "0x0") return { state: "failed", block };
2522
- if (kind === "approval") return { state: "approval_confirmed", block };
2523
- const blockTag = { blockHash: block.hash, requireCanonical: true };
2524
- const [rawRecord, rawReport] = await Promise.all([
2525
- this.contractCall(rpc, this.network.settlement_contract, "settlementInfo(bytes32)", [reference.settlement_key], blockTag),
2526
- this.contractCall(rpc, this.network.settlement_contract, "reports(bytes32,bytes32)",
2527
- [reference.settlement_key, reference.predicted_report_id], blockTag),
2528
- ]);
2529
- return { state: "confirmed", block, record: decodeV10Settlement(rawRecord), report: decodeV10Report(rawReport) };
2530
- });
2531
- if (result.state === "pending") return result;
2532
- const now = Math.floor(Date.now() / 1000);
2533
- if (result.state === "failed") {
2534
- this.historyLedger.append({ ...checked, dispute_stage: "failed", dispute_error: "The transaction failed on-chain.",
2535
- dispute_updated_at: now, updated_at: now });
2536
- return result;
2537
- }
2538
- if (result.state === "approval_confirmed") {
2539
- this.historyLedger.append({ ...checked, dispute_stage: "failed",
2540
- dispute_error: "Reporter-bond approval confirmed; submit the dispute transaction again.",
2541
- dispute_updated_at: now, updated_at: now });
2542
- return result;
2543
- }
2544
- const { record, report, block } = result;
2545
- if (record.status !== 2 || record.owner !== wallet || record.key !== this.paymentAddress
2546
- || record.request_id !== checked.request_id || record.request_hash !== checked.request_hash
2547
- || record.response_hash !== checked.response_hash || block.timestamp >= record.release_at
2548
- || report.reporter !== wallet || report.evidence_hash !== reference.evidence_hash) {
2549
- throw new Error("confirmed dispute transaction did not create the bound on-chain report");
2550
- }
2551
- this.historyLedger.append({ ...checked, status: "disputed", accepted: true,
2552
- dispute_stage: "confirmed", dispute_error: "", dispute_confirmed_at: block.timestamp,
2553
- dispute_updated_at: now, settlement_release_at: record.release_at,
2554
- settlement_checked_at: block.timestamp, updated_at: now });
2555
- return result;
2556
- }
2557
-
2558
- async refreshDisputeTransactions(force = false) {
2559
- if (!this.unlockedWallet || !this.managementToken
2560
- || this.network.protocol_version !== 10
2561
- || this.network.committee_mode !== DYNAMIC_PROVIDER_AI_COMMITTEE) return;
2562
- if (this.disputeSync) return this.disputeSync;
2563
- if (!force && Date.now() - Number(this.disputeSyncAt || 0) < 10_000) return;
2564
- const candidates = this.historyLedger.history(0).filter((entry) =>
2565
- ["submitted", "uncertain"].includes(entry.dispute_stage)
2566
- && (entry.dispute_tx_hash || entry.dispute_approval_tx_hash)).slice(0, 10);
2567
- if (!candidates.length) return;
2568
- this.disputeSyncAt = Date.now();
2569
- this.disputeSyncError = null;
2570
- const work = (async () => {
2571
- for (const entry of candidates) {
2572
- try {
2573
- await this.reconcileDisputeEntry(entry);
2574
- } catch {
2575
- const current = this.historyLedger.history(0).find((item) =>
2576
- item.request_id === entry.request_id && item.capacity_channel_id === entry.capacity_channel_id);
2577
- if (current && current.dispute_stage !== "uncertain") {
2578
- const now = Math.floor(Date.now() / 1000);
2579
- this.historyLedger.append({ ...current, dispute_stage: "uncertain",
2580
- dispute_error: "The submitted transaction cannot yet be reconciled; do not retry.",
2581
- dispute_updated_at: now, updated_at: now });
2582
- }
2583
- this.disputeSyncError = "申诉交易状态暂时无法核验";
2584
- }
2585
- }
2586
- })();
2587
- this.disputeSync = work;
2588
- try { await work; } finally { this.disputeSync = null; }
2589
- }
2590
-
2591
- noteRelayFailure(relayUrl, retryAfter) {
2592
- this.readinessCache = null;
2593
- this.healthCache.delete(relayUrl);
2594
- const count = Math.min(6, (this.relayFailures.get(relayUrl)?.count || 0) + 1);
2595
- const seconds = Number(retryAfter);
2596
- const cooldown = Math.max(Math.min(30_000, 1000 * 2 ** (count - 1)),
2597
- Number.isFinite(seconds) && seconds > 0 ? Math.min(30_000, seconds * 1000) : 0);
2598
- this.relayFailures.set(relayUrl, { count, until: Date.now() + cooldown });
2599
- }
2600
-
2601
- async relayHealth(relayUrl, refresh = false) {
2602
- const failed = this.relayFailures.get(relayUrl);
2603
- if (failed && failed.until > Date.now()) throw new Error("Relay is cooling down after a failure");
2604
- const running = this.healthRequests.get(relayUrl);
2605
- if (running) return running;
2606
- const pending = this.loadRelayHealth(relayUrl, refresh).catch((error) => {
2607
- this.noteRelayFailure(relayUrl);
2608
- throw error;
2609
- });
2610
- this.healthRequests.set(relayUrl, pending);
2611
- try { return await pending; } finally { this.healthRequests.delete(relayUrl); }
2612
- }
2613
-
2614
- async loadRelayHealth(relayUrl, refresh = false) {
2615
- const cached = this.healthCache.get(relayUrl);
2616
- const cacheAge = cached ? Date.now() - cached.at : Infinity;
2617
- if (cached && !refresh && cacheAge < RELAY_HEALTH_CACHE_MS) {
2618
- this.verifyRelayIdentity(relayUrl, cached.payload);
2619
- return cached.payload;
2620
- }
2621
- const request = { dispatcher: this.dispatcher, headers: { accept: "application/json" } };
2622
- if (this.relayDiscovery && !this.staticRelayUrls.includes(relayUrl)) await this.relayDiscovery.probe(relayUrl);
2623
- let payload;
2624
- let lastError;
2625
- for (let attempt = 0; attempt < 2 && !payload; attempt += 1) {
2626
- try {
2627
- const response = await fetchWithTimeout(`${relayUrl}/relay/health`, request, this.healthTimeoutMs);
2628
- payload = await readJsonResponse(response);
2629
- if (!response.ok || payload?.ok !== true) throw new Error(`Relay health is invalid for ${relayUrl}`);
2630
- } catch (error) {
2631
- // Never fall back to an insecure transport when a private CA or host
2632
- // certificate is wrong. Keep the route eligible for retry, but expose
2633
- // an actionable error so operators can install the manifest CA or fix
2634
- // the Relay certificate/SAN.
2635
- const tlsCode = String(error?.code || error?.cause?.code || "");
2636
- const tlsMessage = String(error?.message || error?.cause?.message || "");
2637
- if (/CERT|TLS|SSL|UNABLE_TO_VERIFY|ERR_TLS/i.test(`${tlsCode} ${tlsMessage}`)) {
2638
- lastError = Object.assign(new Error(`Relay TLS verification failed for ${relayUrl}; install the network manifest CA or use a publicly trusted certificate`), {
2639
- code: "relay_tls_untrusted", cause: error,
2640
- });
2641
- } else {
2642
- lastError = error;
2643
- }
2644
- payload = undefined;
2645
- if (attempt === 0) await new Promise((resolve) => setTimeout(resolve, RELAY_HEALTH_RETRY_DELAY_MS));
2646
- }
2647
- }
2648
- if (!payload) {
2649
- throw lastError || new Error(`Relay health is unavailable for ${relayUrl}`);
2650
- }
2651
- const v8 = this.capabilities(payload);
2652
- this.verifyRelayIdentity(relayUrl, payload);
2653
- if (!v8 || v8.enabled !== true || Number(v8.providers || 0) <= 0) throw new Error(`Relay has no live Settlement V${this.network.protocol_version} Provider: ${relayUrl}`);
2654
- if (payload.inference_ready === false || payload.settlement_ready === false || v8.inference_ready === false || v8.settlement_ready === false
2655
- || payload.settlement_submitter?.ready === false || payload.settlement_submitter?.admission_ready === false) {
2656
- throw new Error("Relay is not ready for paid inference or settlement");
2657
- }
2658
- this.healthCache.set(relayUrl, { at: Date.now(), payload });
2659
- return payload;
2660
- }
2661
-
2662
- verifyRelayIdentity(relayUrl, health, { requirePin = false } = {}) {
2663
- const capabilities = this.capabilities(health);
2664
- const announcement = this.relayDiscovery?.recordFor(relayUrl);
2665
- if (this.relayDiscovery && !this.staticRelayUrls.includes(relayUrl) && !announcement) {
2666
- throw new Error("Relay discovery announcement expired or unavailable");
2667
- }
2668
- const pins = [this.network.relay_pins?.[relayUrl], announcement].filter(Boolean);
2669
- if (requirePin && pins.length === 0) {
2670
- const error = new Error("Relay identity is not pinned by the Consumer network or a verified discovery announcement");
2671
- error.code = "relay_identity_unpinned";
2672
- throw error;
2673
- }
2674
- // Address pins authenticate the Relay identity, but deployment identity is
2675
- // mandatory for every route. An unpinned custom/static Relay must never
2676
- // make readiness look green for another chain or Settlement contract.
2677
- if (Number(capabilities.chain_id) !== this.network.chain_id
2678
- || normalizeAddress(capabilities.settlement_contract) !== this.network.settlement_contract) {
2679
- throw new Error("Relay deployment does not match the pinned Consumer network");
2680
- }
2681
- for (const pin of pins) {
2682
- if (normalizeAddress(capabilities.relay_payment_address) !== pin.payment_address
2683
- || normalizeAddress(capabilities.relay_signer_address) !== pin.attestation_address) {
2684
- throw new Error("Relay health identity differs from its pinned announcement");
2685
- }
2686
- }
2687
- }
2688
-
2689
- syncDiscoveredRelays() {
2690
- if (!this.relayDiscovery) return;
2691
- this.relayUrls = [...new Set([...this.staticRelayUrls, ...this.relayDiscovery.active().map((record) => record.public_url)])];
2692
- const current = new Set(this.relayUrls);
2693
- for (const map of [this.healthCache, this.relayFailures]) {
2694
- for (const url of map?.keys() || []) if (!current.has(url)) map.delete(url);
2695
- }
2696
- for (const map of [this.relayInFlight, this.relayReservations]) {
2697
- for (const [url, count] of map?.entries() || []) if (!current.has(url) && !count) map.delete(url);
2698
- }
2699
- }
2700
-
2701
- async refreshRelayDiscovery() {
2702
- if (!this.relayDiscovery) return;
2703
- this.syncDiscoveredRelays();
2704
- await this.relayDiscovery.refresh();
2705
- this.syncDiscoveredRelays();
2706
- }
2707
-
2708
- async modelCatalog() {
2709
- await this.refreshRelayDiscovery();
2710
- const catalog = new Map();
2711
- await Promise.allSettled(this.relayUrls.map(async (relayUrl) => {
2712
- const health = await this.relayHealth(relayUrl);
2713
- const capabilities = this.capabilities(health);
2714
- if (Number(capabilities.chain_id) !== this.network.chain_id
2715
- || normalizeAddress(capabilities.settlement_contract) !== this.network.settlement_contract) return;
2716
- const models = Array.isArray(capabilities.models) && capabilities.models.length
2717
- ? capabilities.models : [capabilities.model || DEFAULT_MODEL];
2718
- for (const id of models) {
2719
- if (typeof id !== "string" || !id.trim() || id.length > 256) continue;
2720
- if (!catalog.has(id)) catalog.set(id, new Set());
2721
- catalog.get(id).add(relayUrl);
2722
- }
2723
- }));
2724
- if (!catalog.size) throw new Error("No ready Relay advertises models for this network");
2725
- return [...catalog.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([id, urls]) => ({
2726
- id, object: "model", owned_by: "mycomesh", relays: [...urls].sort(), route_count: urls.size,
2727
- // Keep model-specific redundancy visible to clients. A healthy Relay
2728
- // list alone is misleading when (for example) gpt-5.6-sol is served by
2729
- // only one route and has no automatic failover path.
2730
- redundancy: urls.size > 1 ? "redundant" : "single_relay",
2731
- route_warning: urls.size > 1 ? undefined : "single Relay route; failover unavailable for this model",
2732
- }));
2733
- }
2734
-
2735
- async readinessPayload() {
2736
- const now = Date.now();
2737
- if (this.readinessCache && now - this.readinessCache.at < 2_000) return this.readinessCache.payload;
2738
- if (this.readinessRequest) return this.readinessRequest;
2739
- this.readinessRequest = (async () => {
2740
- const deadline = performance.now() + READINESS_TIMEOUT_MS;
2741
- // `/ready` is a fast liveness probe. V10 capacity is checked by the
2742
- // dashboard and immediately before inference; repeating that chain RPC
2743
- // here makes a cold probe time out even when both Relays are healthy.
2744
- const selected = await this.chooseRelay(new Set(), { deadline, checkCapacity: false });
2745
- const payload = {
2746
- ok: true,
2747
- liveness_ready: true,
2748
- paid_ready: null,
2749
- paid_readiness_checked: false,
2750
- relay: selected.relayUrl,
2751
- model: this.capabilities(selected.health).model || DEFAULT_MODEL,
2752
- };
2753
- this.readinessCache = { at: Date.now(), payload };
2754
- return payload;
2755
- })();
2756
- try { return await this.readinessRequest; }
2757
- finally { this.readinessRequest = null; }
2758
- }
2759
-
2760
- async paidReadinessPayload() {
2761
- const payload = {
2762
- ok: false,
2763
- liveness_ready: true,
2764
- paid_readiness_checked: true,
2765
- paid_ready: false,
2766
- wallet_ready: false,
2767
- network_ready: false,
2768
- budget_ready: false,
2769
- models_ready: false,
2770
- protocol_version: this.network.protocol_version,
2771
- };
2772
- const locallyUnlocked = Boolean(this.managementToken && this.unlockedWallet && this.paymentUnlocked);
2773
- let grant;
2774
- let capacityChannels = [];
2775
- let budgetError;
2776
-
2777
- try {
2778
- await this.settlementNetworkReady();
2779
- payload.network_ready = true;
2780
- } catch {
2781
- payload.network_ready = false;
2782
- }
2783
- if (payload.network_ready) {
2784
- try {
2785
- grant = await this.keyGrant(this.paymentAddress);
2786
- } catch {
2787
- // Keep a verified chain/contract distinct from an unavailable or
2788
- // malformed payment-key grant.
2789
- grant = undefined;
2790
- }
2791
- }
2792
-
2793
- if (locallyUnlocked && grant) {
2794
- const now = Math.floor(Date.now() / 1000);
2795
- payload.wallet_ready = grant.owner === this.unlockedWallet
2796
- && grant.active === true
2797
- && Number(grant.max_per_request) >= this.maxFeeUnits
2798
- && (Number(grant.valid_until) === 0 || Number(grant.valid_until) > now + Math.ceil(this.timeoutMs / 1000));
2799
- }
2800
-
2801
- try {
2802
- const selected = await this.chooseRelay(new Set(), { deadline: performance.now() + READINESS_TIMEOUT_MS, checkCapacity: false });
2803
- this.verifyRelayIdentity(selected.relayUrl, selected.health, { requirePin: true });
2804
- payload.models_ready = true;
2805
- } catch {
2806
- payload.models_ready = false;
2807
- }
2808
-
2809
- if (this.network.protocol_version === 10 && payload.network_ready) {
2810
- try {
2811
- capacityChannels = await this.capacityChannels();
2812
- // V10 deliberately permits an already owner-approved bounded channel
2813
- // to finish after the registration itself was revoked. It is still
2814
- // tied to the locally verified wallet and payment key.
2815
- if (locallyUnlocked && !payload.wallet_ready) {
2816
- const now = Math.floor(Date.now() / 1000);
2817
- payload.wallet_ready = capacityChannels.some((channel) => !channel.closed
2818
- && channel.consumer_owner === this.unlockedWallet
2819
- && channel.consumer_key === this.paymentAddress
2820
- && Number(channel.admit_until) > now);
2821
- }
2822
- await this.chooseRelay(new Set(), { deadline: performance.now() + READINESS_TIMEOUT_MS, checkCapacity: true });
2823
- payload.budget_ready = true;
2824
- } catch (error) {
2825
- budgetError = error;
2826
- payload.budget_ready = false;
2827
- if (error?.code === "rpc_unavailable") payload.network_ready = false;
2828
- }
2829
- } else if (this.network.protocol_version !== 10 && payload.network_ready && payload.wallet_ready) {
2830
- try {
2831
- payload.budget_ready = BigInt(await this.accountBalance(this.unlockedWallet)) >= BigInt(this.maxFeeUnits);
2832
- if (!payload.budget_ready) budgetError = Object.assign(new Error("Prepaid balance is below the request maximum"), { code: "budget_unavailable" });
2833
- } catch (error) {
2834
- budgetError = error;
2835
- payload.budget_ready = false;
2836
- payload.network_ready = false;
2837
- }
2838
- }
2839
-
2840
- payload.paid_ready = payload.wallet_ready && payload.network_ready && payload.budget_ready && payload.models_ready;
2841
- payload.ok = payload.paid_ready;
2842
- if (payload.paid_ready) {
2843
- payload.code = "paid_ready";
2844
- payload.action = "none";
2845
- return payload;
2846
- }
2847
- if (!locallyUnlocked) {
2848
- payload.code = this.unlockedWallet ? "payment_key_not_ready" : "wallet_locked";
2849
- payload.action = this.unlockedWallet
2850
- ? "Activate or re-verify the payment key for the connected wallet."
2851
- : "Open the local Consumer dashboard and verify the payment-key owner wallet.";
2852
- return payload;
2853
- }
2854
- if (!payload.network_ready) {
2855
- payload.code = "network_unavailable";
2856
- payload.action = "Check the configured chain, Settlement contract, and RPC endpoints, then retry.";
2857
- return payload;
2858
- }
2859
- if (!payload.wallet_ready) {
2860
- payload.code = "payment_key_not_ready";
2861
- payload.action = "Activate or re-verify the payment key for the connected wallet.";
2862
- return payload;
2863
- }
2864
- if (!payload.models_ready) {
2865
- payload.code = "models_unavailable";
2866
- payload.action = "Check Relay health and ensure at least one model route is advertised for this network.";
2867
- return payload;
2868
- }
2869
- if (!payload.budget_ready) {
2870
- payload.code = ["budget_not_started", "budget_unavailable"].includes(budgetError?.code)
2871
- ? budgetError.code : "budget_unavailable";
2872
- payload.action = payload.code === "budget_not_started"
2873
- ? "Wait until the fixed budget becomes active, then retry."
2874
- : this.network.protocol_version === 10
2875
- ? "Open or renew a fixed budget channel covering an available Provider route."
2876
- : "Top up enough prepaid balance for one request.";
2877
- if (budgetError?.availableAt) payload.available_at = budgetError.availableAt;
2878
- return payload;
2879
- }
2880
- payload.code = "paid_readiness_unknown";
2881
- payload.action = "Retry the paid readiness check.";
2882
- return payload;
2883
- }
2884
-
2885
- async chooseRelay(exclude = new Set(), { model, requireSessions = false, reserve = false, deadline = Infinity, providerSigner, discoveryRetry = true, checkCapacity = true } = {}) {
2886
- const refresh = this.refreshRelayDiscovery();
2887
- const candidates = [...this.relayUrls];
2888
- const errors = [];
2889
- let timedOut = false;
2890
- let rpcFailure;
2891
- let budgetFailure;
2892
- let modelUnavailable = false;
2893
- const available = [];
2894
- const checkRelay = async (relayUrl) => {
2895
- try {
2896
- const health = await withinDeadline(this.relayHealth(relayUrl), deadline);
2897
- const models = this.capabilities(health).models;
2898
- if (model && Array.isArray(models) && !models.includes(model)) {
2899
- modelUnavailable = true;
2900
- throw new Error("requested model is unavailable");
2901
- }
2902
- if (requireSessions && this.capabilities(health).scheduler?.session_affinity !== true) throw new Error("Relay upgrade required for session routing");
2903
- if (providerSigner && !this.capabilities(health).provider_signers?.some((signer) => String(signer).toLowerCase() === providerSigner.toLowerCase())) {
2904
- throw new Error("original Provider is not available on this Relay");
2905
- }
2906
- if (checkCapacity && this.network.protocol_version === 10) await withinDeadline(this.selectCapacityChannel(health, model, providerSigner), deadline);
2907
- const item = { relayUrl, health };
2908
- available.push(item);
2909
- return item;
2910
- } catch (error) {
2911
- timedOut ||= error.statusCode === 504;
2912
- if (error.code === "rpc_unavailable") rpcFailure = error;
2913
- if (error.code === "budget_not_started" || error.code === "budget_unavailable") {
2914
- if (!budgetFailure || (error.availableAt && (!budgetFailure.availableAt || error.availableAt < budgetFailure.availableAt))) budgetFailure = error;
2915
- }
2916
- errors.push(`${relayUrl}: ${error.message}`);
2917
- throw error;
2918
- }
2919
- };
2920
- const checks = candidates.filter((url) => !exclude.has(url)).map(checkRelay);
2921
- if (this.relayDiscovery && discoveryRetry) {
2922
- checks.push(withinDeadline(refresh, deadline).then(() => {
2923
- remainingMs(deadline);
2924
- return Promise.any(this.relayUrls.filter((url) => !candidates.includes(url) && !exclude.has(url)).map(checkRelay));
2925
- }));
2926
- }
2927
- if (checks.length) {
2928
- const all = Promise.allSettled(checks);
2929
- try {
2930
- await Promise.any(checks);
2931
- // Give other healthy replies a small comparison window, without making
2932
- // a healthy route wait for an unreachable backup's full health timeout.
2933
- await withinDeadline(Promise.race([all, new Promise((resolve) => setTimeout(resolve, 30))]), deadline);
2934
- } catch {}
2935
- }
2936
- if (available.length) {
2937
- remainingMs(deadline);
2938
- const offset = this.relaySelectionCursor++ % this.relayUrls.length;
2939
- const score = ({ relayUrl, health }) => {
2940
- const scheduling = this.capabilities(health).scheduler;
2941
- const slots = Math.max(1, Number(scheduling?.total_slots || this.capabilities(health).providers || 1));
2942
- const localLoad = (this.relayInFlight.get(relayUrl) || 0) + (this.relayReservations.get(relayUrl) || 0);
2943
- const outstanding = Math.max(Number(scheduling?.outstanding_jobs || 0), localLoad);
2944
- const assigned = [...this.sessionRoutes.values()].filter((entry) => entry.relayUrl === relayUrl).length;
2945
- return [outstanding / slots, assigned / slots, (this.relayUrls.indexOf(relayUrl) - offset + this.relayUrls.length) % this.relayUrls.length];
2946
- };
2947
- available.sort((a, b) => {
2948
- const left = score(a), right = score(b);
2949
- return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
2950
- });
2951
- const selected = available[0];
2952
- if (reserve) {
2953
- this.relayReservations.set(selected.relayUrl, (this.relayReservations.get(selected.relayUrl) || 0) + 1);
2954
- selected.reserved = true;
2955
- }
2956
- return selected;
2957
- }
2958
- if (timedOut) throw deadlineError();
2959
- remainingMs(deadline);
2960
- if (rpcFailure) throw rpcFailure;
2961
- if (budgetFailure) throw budgetFailure;
2962
- if (modelUnavailable && model) {
2963
- throw Object.assign(new Error(`model ${model} has no healthy Relay route; inspect /models for redundancy and configure a backup Provider`), {
2964
- code: "model_route_unavailable", model,
2965
- });
2966
- }
2967
- throw new Error(`no healthy Settlement V${this.network.protocol_version} Relay is available: ${errors.join("; ") || "all relays excluded"}`);
2968
- }
2969
-
2970
- ensureSessionRouteCapacity() {
2971
- if (this.sessionRoutes.size + this.sessionSelections.size >= 4096) {
2972
- const idle = [...this.sessionRoutes].filter(([id, entry]) => !entry.inFlight && !entry.dispatchPending && !entry.explicit && !this.sessionSelections.has(id))
2973
- .sort((a, b) => a[1].lastUsed - b[1].lastUsed);
2974
- for (const [id] of idle) {
2975
- this.sessionRoutes.delete(id);
2976
- if (this.sessionRoutes.size + this.sessionSelections.size < 4096) break;
2977
- }
2978
- }
2979
- if (this.sessionRoutes.size + this.sessionSelections.size >= 4096) throw new Error("session routing capacity reached; retry later");
2980
- }
2981
-
2982
- async chooseSessionRelay(session, exclude, model, deadline = Infinity) {
2983
- void this.refreshRelayDiscovery();
2984
- const now = Date.now();
2985
- for (const [id, entry] of this.sessionRoutes) {
2986
- if (!entry.inFlight && !entry.dispatchPending && now - entry.lastUsed > 24 * 60 * 60_000) this.sessionRoutes.delete(id);
2987
- }
2988
- if (this.sessionSelections.has(session.id)) return withinDeadline(this.sessionSelections.get(session.id), deadline);
2989
- let pinned = this.sessionRoutes.get(session.id);
2990
- if (!pinned) {
2991
- const savedRoutes = this.history(0).filter((entry) => entry.accepted && entry.session_id === session.id
2992
- && entry.relay_url && /^0x[0-9a-f]{40}$/i.test(entry.provider_signer || ""));
2993
- if (new Set(savedRoutes.map((entry) => entry.provider_signer.toLowerCase())).size > 1) {
2994
- const error = new Error("session history contains conflicting Provider routes; automatic migration is disabled");
2995
- error.statusCode = 409;
2996
- throw error;
2997
- }
2998
- const saved = savedRoutes[0];
2999
- if (saved) {
3000
- this.ensureSessionRouteCapacity();
3001
- pinned = { relayUrl: saved.relay_url, providerSigner: saved.provider_signer, explicit: true, lastUsed: now, inFlight: 0 };
3002
- this.sessionRoutes.set(session.id, pinned);
3003
- }
3004
- }
3005
- if (pinned) {
3006
- pinned.lastUsed = now;
3007
- pinned.explicit ||= session.explicit || session.requiresOriginalRoute;
3008
- try {
3009
- if (exclude.has(pinned.relayUrl) || !this.relayUrls.includes(pinned.relayUrl)) throw new Error("original session Relay is unavailable");
3010
- const health = await withinDeadline(this.relayHealth(pinned.relayUrl), deadline);
3011
- if (this.capabilities(health).scheduler?.session_affinity !== true) throw new Error("Relay upgrade required to continue this session");
3012
- if (pinned.providerSigner && Array.isArray(this.capabilities(health).provider_signers)
3013
- && !this.capabilities(health).provider_signers.some((signer) => String(signer).toLowerCase() === pinned.providerSigner.toLowerCase())) {
3014
- throw new Error("original Provider has disconnected from this Relay");
3015
- }
3016
- return { relayUrl: pinned.relayUrl, health };
3017
- } catch (error) {
3018
- if (error.statusCode === 504 || !pinned.providerSigner || pinned.inFlight || pinned.dispatchPending) throw error;
3019
- if (this.sessionSelections.has(session.id)) return withinDeadline(this.sessionSelections.get(session.id), deadline);
3020
- // A Relay may change only when the SAME verified Provider identity is
3021
- // advertised at another configured route. Never replay unknown work.
3022
- const recovery = (async () => {
3023
- const selected = await this.chooseRelay(new Set([...exclude, pinned.relayUrl]), {
3024
- model, requireSessions: true, reserve: true, deadline, providerSigner: pinned.providerSigner,
3025
- });
3026
- pinned.relayUrl = selected.relayUrl;
3027
- pinned.lastUsed = Date.now();
3028
- pinned.dispatchPending = true;
3029
- return selected;
3030
- })();
3031
- this.sessionSelections.set(session.id, recovery);
3032
- try { return await recovery; } finally { this.sessionSelections.delete(session.id); }
3033
- }
3034
- }
3035
- if (session.requiresOriginalRoute) {
3036
- const error = new Error("original session route expired or is unavailable; automatic migration is disabled");
3037
- error.statusCode = 409;
3038
- throw error;
3039
- }
3040
- this.ensureSessionRouteCapacity();
3041
- const selection = (async () => {
3042
- const selected = await this.chooseRelay(exclude, { model, requireSessions: session.explicit, reserve: true, deadline });
3043
- // Keep the route leased across promise resolution until runRelayInference
3044
- // synchronously transfers this reservation into its in-flight count.
3045
- this.sessionRoutes.set(session.id, { relayUrl: selected.relayUrl, lastUsed: Date.now(), inFlight: 0, explicit: session.explicit, dispatchPending: true });
3046
- return selected;
3047
- })();
3048
- this.sessionSelections.set(session.id, selection);
3049
- try { return await selection; } finally { this.sessionSelections.delete(session.id); }
3050
- }
3051
-
3052
- async capacityChannels() {
3053
- if (this.network.protocol_version !== 10) return [];
3054
- if (!this.network.capacity_channel_ids.length) return [];
3055
- if (this.capacityChannelsRequest) return this.capacityChannelsRequest;
3056
- // Share only an in-flight canonical snapshot. A later call always rereads
3057
- // the chain, including after errors or a channel's expiry/closure.
3058
- this.capacityChannelsRequest = this.rpcValue(async rpc => {
3059
- if (BigInt(await this.callRpc(rpc, "eth_chainId", [])) !== BigInt(this.network.chain_id)) throw new Error("capacity RPC chain mismatch");
3060
- const block = await this.callRpc(rpc, "eth_getBlockByNumber", ["latest", false]);
3061
- const now = Math.floor(Date.now() / 1000);
3062
- if (!block || !/^0x[0-9a-f]{64}$/i.test(block.hash) || !/^0x[0-9a-f]+$/i.test(block.timestamp || "")
3063
- || Math.abs(Number(BigInt(block.timestamp)) - now) > 300) throw new Error("capacity RPC clock mismatch");
3064
- const height = BigInt(block.number);
3065
- if (height < 6n) throw new Error("capacity RPC confirmation depth unavailable");
3066
- const confirmed = await this.callRpc(rpc, "eth_getBlockByNumber", ['0x'+(height-6n).toString(16), false]);
3067
- if (!confirmed || !/^0x[0-9a-f]{64}$/i.test(confirmed.hash)) throw new Error("confirmed capacity block unavailable");
3068
- const tag = { blockHash: confirmed.hash, requireCanonical: true };
3069
- const ttl = await this.contractCall(rpc, this.network.settlement_contract, "MAX_AUTHORIZATION_TTL()", [], tag);
3070
- if (!/^0x[0-9a-f]{64}$/i.test(ttl) || BigInt(ttl) !== 10800n) throw new Error("capacity contract lifetime mismatch");
3071
- const duration = await this.contractCall(rpc, this.network.settlement_contract, "MAX_CHANNEL_DURATION()", [], tag);
3072
- if (!/^0x[0-9a-f]{64}$/i.test(duration) || BigInt(duration) !== BigInt(this.network.max_channel_duration_seconds)) throw new Error("capacity contract channel duration mismatch");
3073
- const channels = await Promise.all(this.network.capacity_channel_ids.map(async id => decodeCapacityChannel(
3074
- await this.contractCall(rpc, this.network.settlement_contract, "channelInfo(bytes32)", [id], tag),
3075
- id, this.network.chain_id, this.network.settlement_contract)));
3076
- const canonical = await this.callRpc(rpc, "eth_getBlockByNumber", [confirmed.number, false]);
3077
- if (!canonical || canonical.hash !== confirmed.hash) throw new Error("capacity block reorged during verification");
3078
- return channels;
3079
- });
3080
- try { return await this.capacityChannelsRequest; }
3081
- finally { this.capacityChannelsRequest = null; }
3082
- }
3083
-
3084
- capacityBudget(channel, history = this.history(0), now = Math.floor(Date.now()/1000)) {
3085
- const maximum = BigInt(this.maxFeeUnits);
3086
- // Local unknown requests retain their full authorization. Another device's
3087
- // unsettled reservations still require the Provider's authoritative check.
3088
- const local = history.filter(r => r.capacity_channel_id === channel.channel_id && r.status !== "not_dispatched")
3089
- .reduce((sum,r) => sum+BigInt(r.max_fee_units ?? channel.max_fee_per_request),0n);
3090
- const settled = BigInt(channel.settled_max_fee), used = local > settled ? local : settled;
3091
- const capacity = BigInt(channel.capacity)-used;
3092
- const remaining = [capacity, BigInt(channel.credit_remaining), BigInt(channel.stake_remaining)]
3093
- .reduce((a,b) => a < b ? a : b);
3094
- const remainingUnits = remaining > 0n ? remaining : 0n;
3095
- let reason = "available";
3096
- if (channel.closed) reason = "closed";
3097
- else if (now > Number(channel.claim_until)) reason = "releasable";
3098
- else if (channel.consumer_key !== this.paymentAddress) reason = "previous_key";
3099
- else if (this.unlockedWallet && channel.consumer_owner !== this.unlockedWallet) reason = "different_owner";
3100
- else if (now >= Number(channel.admit_until)) reason = "admission_closed";
3101
- else if (now + Math.ceil(this.timeoutMs/1000) + 60 > Number(channel.admit_until)) reason = "admission_window_short";
3102
- else if (now + this.network.authorization_deadline_seconds > Number(channel.claim_until)) reason = "claim_window_short";
3103
- else if (BigInt(channel.max_fee_per_request) < maximum) reason = "request_limit";
3104
- else if (capacity < maximum) reason = "capacity_exhausted";
3105
- else if (BigInt(channel.credit_remaining) < maximum) reason = "credit_insufficient";
3106
- else if (BigInt(channel.stake_remaining) < maximum) reason = "stake_insufficient";
3107
- else if (Number(channel.valid_from) > now) reason = "not_started";
3108
- return { ready: reason === "available", reason, remaining_units: remainingUnits.toString(),
3109
- request_maximum_units: maximum.toString(), estimated_requests: reason === "available" && maximum > 0n ? (remainingUnits/maximum).toString() : "0",
3110
- ...(reason === "not_started" ? { available_at: Number(channel.valid_from) } : {}) };
3111
- }
3112
-
3113
- async selectCapacityChannel(health, model, providerSigner) {
3114
- const capability = this.capabilities(health);
3115
- if (capability.reservation_mode !== "provider_bound_channel") throw new Error("Relay does not support fixed-budget channels");
3116
- const now = Math.floor(Date.now()/1000), history = this.history(0);
3117
- const channels = await this.capacityChannels();
3118
- const routed = channels.filter(c => c.relay === String(capability.relay_payment_address).toLowerCase() && c.relay_signer === String(capability.relay_signer_address).toLowerCase()
3119
- && c.channel === capability.channel_hash && String(c.pricing_version) === String(capability.pricing_version) && c.pricing_hash === capability.pricing_hash
3120
- && (!providerSigner || c.provider_signer === providerSigner.toLowerCase())
3121
- && capability.provider_routes?.some(p => p.provider_signer === c.provider_signer && p.provider === c.provider_owner && (!model || p.models?.includes(model))));
3122
- let availableAt;
3123
- for (const channel of routed) {
3124
- const budget = this.capacityBudget(channel, history, now);
3125
- if (budget.ready) return channel;
3126
- if (budget.reason === "not_started") availableAt = Math.min(availableAt ?? Infinity, budget.available_at);
3127
- }
3128
- const error = new Error(availableAt
3129
- ? `Fixed budget becomes available at ${new Date(availableAt * 1000).toISOString()}`
3130
- : "No active funded channel covers this model and Provider; renew the fixed budget");
3131
- error.code = availableAt ? "budget_not_started" : "budget_unavailable";
3132
- if (availableAt) error.availableAt = availableAt;
3133
- throw error;
3134
- }
3135
-
3136
- async authorizationTiming(health, body) {
3137
- if (this.network.protocol_version === 10) {
3138
- const channel = await this.selectCapacityChannel(health, body?.model, body?.metadata?.mycomesh_provider_signer);
3139
- return { now: Math.floor(Date.now()/1000), seconds: this.network.authorization_deadline_seconds, maxTtl: 10800, channel };
3140
- }
3141
- const seconds = this.network.authorization_deadline_seconds ?? 900;
3142
- const maxTtl = this.network.max_authorization_ttl_seconds ?? 3600;
3143
- if (this.network.protocol_version !== 9 || seconds <= 900) return { now: Math.floor(Date.now() / 1000), seconds, maxTtl: 3600 };
3144
- return await this.rpcValue(async (rpc) => {
3145
- const chain = await this.callRpc(rpc, "eth_chainId", []);
3146
- if (BigInt(chain) !== BigInt(this.network.chain_id)) throw new Error("authorization RPC chain mismatch");
3147
- const block = await this.callRpc(rpc, "eth_getBlockByNumber", ["latest", false]);
3148
- const now = Math.floor(Date.now() / 1000);
3149
- if (!block || !/^0x[0-9a-f]{64}$/i.test(block.hash) || !/^0x[0-9a-f]+$/i.test(block.timestamp || "")
3150
- || Math.abs(Number(BigInt(block.timestamp)) - now) > AUTHORIZATION_CLOCK_SKEW_SECONDS) throw new Error("authorization RPC clock mismatch");
3151
- const tag = { blockHash: block.hash, requireCanonical: true };
3152
- const limit = await this.contractCall(rpc, this.network.settlement_contract, "MAX_AUTHORIZATION_TTL()", [], tag);
3153
- if (!/^0x[0-9a-f]{64}$/i.test(limit) || BigInt(limit) !== BigInt(maxTtl)) throw new Error("contract authorization lifetime differs from manifest");
3154
- const grant = await this.contractCall(rpc, this.network.settlement_contract, "keyGrants(address)", [this.paymentAddress], tag);
3155
- if (!/^0x[0-9a-f]{256}$/i.test(grant)) throw new Error("invalid authorization key grant");
3156
- const words = grant.slice(2).match(/.{64}/g);
3157
- const validUntil = BigInt(`0x${words[2]}`);
3158
- if (!/^0{24}/.test(words[0]) || BigInt(`0x${words[0]}`) === 0n || BigInt(`0x${words[3]}`) !== 1n
3159
- || BigInt(`0x${words[1]}`) < BigInt(this.maxFeeUnits)
3160
- || (validUntil !== 0n && validUntil < BigInt(now + seconds))) throw new Error("payment key does not cover the full settlement window");
3161
- return { now, seconds, maxTtl };
3162
- });
3163
- }
3164
-
3165
- buildRelayPayment(path, body, health, requestId, timing) {
3166
- const endpoint = path.endsWith("/chat/completions") ? "chat" : "responses";
3167
- const v8 = this.capabilities(health);
3168
- if (!v8) throw new Error("Relay health has no V8 payment requirements");
3169
- if (Number(v8.chain_id) !== this.network.chain_id || normalizeAddress(v8.settlement_contract) !== this.network.settlement_contract) {
3170
- throw new Error("Relay deployment does not match the pinned Consumer network");
3171
- }
3172
- if ((this.network.protocol_version >= 9 || this.network.require_response_proof)
3173
- && v8.response_proof !== RESPONSE_PROOF_SCHEMA) throw new Error("Relay upgrade required for independently verified response content");
3174
- const hasCatalog = Array.isArray(v8.models) && v8.models.length > 0;
3175
- const advertised = hasCatalog ? v8.models.map(String) : [String(v8.model || DEFAULT_MODEL)];
3176
- const requested = String(body.model || "").trim();
3177
- const model = hasCatalog
3178
- ? (requested || String(v8.model || advertised[0] || DEFAULT_MODEL))
3179
- : String(v8.model || requested || DEFAULT_MODEL);
3180
- if (hasCatalog && !advertised.includes(model)) throw new Error(`model is not advertised by the selected Relay: ${model}`);
3181
- const maxOutput = body.max_output_tokens ?? body.max_tokens ?? v8.maxOutputTokens ?? 2000;
3182
- const options = {};
3183
- for (const field of [...RESPONSES_REQUEST_OPTION_FIELDS, ...RESPONSES_LOCAL_OPTION_FIELDS]) {
3184
- if (Object.prototype.hasOwnProperty.call(body, field)) options[field] = body[field];
3185
- }
3186
- const normalizedOptions = normalizeInferenceOptions(endpoint, options);
3187
- const requestHash = inferenceRequestHash({
3188
- endpoint,
3189
- model,
3190
- input: body.input,
3191
- messages: body.messages,
3192
- maxOutputTokens: maxOutput,
3193
- options: normalizedOptions || undefined,
3194
- });
3195
- if ((this.network.authorization_deadline_seconds ?? 900) > 900 && !timing) throw new Error("long authorization requires verified on-chain timing");
3196
- const now = timing?.now ?? Math.floor(Date.now() / 1000);
3197
- if (this.network.protocol_version === 10) {
3198
- if (!timing?.channel) throw new Error("verified capacity channel required before dispatch");
3199
- const channel = timing.channel;
3200
- return { payment: buildReservedAuthorization({ paymentKey: this.paymentKey, chainId: this.network.chain_id,
3201
- settlementContract: this.network.settlement_contract, channelId: channel.channel_id, requestId, requestHash,
3202
- maxFee: this.maxFeeUnits, issuedAt: Math.max(Number(channel.valid_from), now-300),
3203
- executeBy: now+Math.ceil(this.timeoutMs/1000)+60, deadline: now+timing.seconds }), channel, request_id: requestId, model };
3204
- }
3205
- const payment = buildAuthorization({
3206
- protocolVersion: this.network.protocol_version,
3207
- paymentKey: this.paymentKey,
3208
- chainId: Number(v8.chain_id),
3209
- settlementContract: v8.settlement_contract,
3210
- requestId,
3211
- requestHash,
3212
- relay: v8.relay_payment_address,
3213
- relaySigner: v8.relay_signer_address,
3214
- channelHash: v8.channel_hash,
3215
- pricingVersion: v8.pricing_version,
3216
- pricingHash: v8.pricing_hash,
3217
- maxFee: this.maxFeeUnits,
3218
- issuedAt: now - AUTHORIZATION_CLOCK_SKEW_SECONDS,
3219
- deadline: now + (timing?.seconds ?? 900),
3220
- maxAuthorizationTtlSeconds: timing?.maxTtl ?? 3600,
3221
- });
3222
- return { payment, request_id: requestId, model };
3223
- }
3224
-
3225
- async relayInference(path, body, requestHeaders = {}) {
3226
- this.activeInferenceRequests += 1;
3227
- let journal, claim;
3228
- try {
3229
- if (this.network.protocol_version === 10) {
3230
- const headers = Object.fromEntries(Object.entries(requestHeaders).map(([k,v]) => [k.toLowerCase(),v]));
3231
- const idempotencyKey = headers["idempotency-key"];
3232
- if (!idempotencyKey && Number(headers["x-stainless-retry-count"] || 0) > 0) {
3233
- return { status: 422, headers: { "x-should-retry": "false" },
3234
- payload: { error: { message: "Automatic retry has no stable Idempotency-Key. Check the original request before submitting again.",
3235
- type: "idempotency_key_required", code: "idempotency_key_required", retryable: false } } };
3236
- }
3237
- if (idempotencyKey !== undefined) {
3238
- journal = new ConsumerRequestJournal({ directory: join(this.dataDir, "request-journal"), chainId: this.network.chain_id,
3239
- contract: this.network.settlement_contract, paymentKeyAddress: this.paymentAddress });
3240
- claim = journal.claim({ idempotencyKey, payloadHash: requestPayloadHash({ path, body,
3241
- session: headers["x-mycomesh-session-id"] || "" }) });
3242
- if (claim.action === "replay") return claim.result;
3243
- }
3244
- }
3245
- const result = await this.runRelayInference(path, body, requestHeaders, claim?.requestId);
3246
- if (claim && journal) {
3247
- if (result.payload?.error?.execution_status === "unknown") journal.markOutcomeUnknown(claim);
3248
- else {
3249
- try { return journal.complete(claim, result); }
3250
- catch {
3251
- journal.markOutcomeUnknown(claim);
3252
- result.headers = { ...result.headers, "x-should-retry": "false", "x-mycomesh-history-status": "response-cache-unavailable" };
3253
- }
3254
- }
3255
- }
3256
- return result;
3257
- } catch (error) {
3258
- if (claim?.action === "execute") { try { journal.markOutcomeUnknown(claim); } catch {} }
3259
- if (this.network.protocol_version === 10) return { status: 422, headers: { "x-should-retry": "false",
3260
- ...(error.requestId || claim?.requestId ? { "x-mycomesh-request-id": error.requestId || claim.requestId } : {}) },
3261
- payload: { error: { message: error.message, type: error.code || "request_tracking_unavailable", code: error.code || "request_tracking_unavailable", retryable: false } } };
3262
- throw error;
3263
- }
3264
- finally { this.activeInferenceRequests -= 1; }
3265
- }
3266
-
3267
- async runRelayInference(path, body, requestHeaders = {}, stableRequestId) {
3268
- const deadline = performance.now() + this.timeoutMs;
3269
- let session;
3270
- try { session = this.sessions.resolve(body, requestHeaders); }
3271
- catch (error) { return { payload: openaiError(error.message, "invalid_session"), status: error.statusCode || 400, headers: {} }; }
3272
- const clientBody = body;
3273
- body = session.body;
3274
- const keepSession = session.explicit || session.requiresOriginalRoute || this.sessionRoutes.has(session.id);
3275
- const requestId = stableRequestId || `0x${bytesToHex(randomBytes(32))}`;
3276
- const preDispatchFailure = (message, type, status = 503) => ({
3277
- payload: { error: { ...openaiError(message, type).error, execution_status: "not_dispatched", request_id: requestId } },
3278
- status,
3279
- headers: { "Retry-After": "2", "x-mycomesh-session-id": session.id, "x-mycomesh-request-id": requestId },
3280
- });
3281
- const used = new Set();
3282
- let lastError = "no Relay accepted the request";
3283
- let lastResponse;
3284
- while (used.size < this.relayUrls.length) {
3285
- let selected;
3286
- try { selected = await this.chooseSessionRelay(session, used, body.model, deadline); }
3287
- catch (error) {
3288
- lastError = error.message;
3289
- if (error.code === "rpc_unavailable") lastResponse = preDispatchFailure(lastError, "rpc_unavailable");
3290
- if (error.code === "budget_not_started" || error.code === "budget_unavailable") lastResponse = preDispatchFailure(lastError, error.code, 402);
3291
- if (error.statusCode === 409) lastResponse = preDispatchFailure(lastError, "session_continuation_unavailable", 409);
3292
- if (error.statusCode === 504) lastResponse = preDispatchFailure(lastError, "request_timeout", 504);
3293
- break;
3294
- }
3295
- used.add(selected.relayUrl);
3296
- if (selected.reserved) {
3297
- this.relayReservations.set(selected.relayUrl, Math.max(0, (this.relayReservations.get(selected.relayUrl) || 1) - 1));
3298
- selected.reserved = false;
3299
- }
3300
- const route = this.sessionRoutes.get(session.id);
3301
- if (route) { route.inFlight += 1; route.dispatchPending = false; }
3302
- this.relayInFlight.set(selected.relayUrl, (this.relayInFlight.get(selected.relayUrl) || 0) + 1);
3303
- let successfulHttpResponse = false;
3304
- let postStarted = false;
3305
- let dispatchPayment;
3306
- try {
3307
- const requestBody = {
3308
- ...body,
3309
- model: Array.isArray(this.capabilities(selected.health).models) && this.capabilities(selected.health).models.length
3310
- ? (body.model || this.capabilities(selected.health).model || DEFAULT_MODEL)
3311
- : (this.capabilities(selected.health).model || body.model || DEFAULT_MODEL),
3312
- };
3313
- if (route?.providerSigner) {
3314
- const supplied = requestBody.metadata?.mycomesh_provider_signer;
3315
- if (supplied && String(supplied).toLowerCase() !== route.providerSigner.toLowerCase()) {
3316
- return { payload: openaiError("Provider hint conflicts with the original session", "invalid_session"), status: 409, headers: {} };
3317
- }
3318
- requestBody.metadata = { ...requestBody.metadata, mycomesh_provider_signer: route.providerSigner };
3319
- }
3320
- if (!String(requestBody.prompt_cache_key || "").trim()) {
3321
- const cacheKey = derivePromptCacheKey({
3322
- endpoint: path.endsWith("/chat/completions") ? "chat" : "responses",
3323
- model: requestBody.model,
3324
- input: requestBody.input,
3325
- messages: requestBody.messages,
3326
- options: requestBody,
3327
- });
3328
- if (cacheKey) requestBody.prompt_cache_key = cacheKey;
3329
- }
3330
- const timing = await withinDeadline(this.authorizationTiming(selected.health, requestBody), deadline);
3331
- if (timing.channel) requestBody.metadata = { ...requestBody.metadata, mycomesh_provider_signer: timing.channel.provider_signer };
3332
- const payment = this.buildRelayPayment(path, requestBody, selected.health, requestId, timing);
3333
- const proofRequired = this.network.protocol_version >= 9 || this.network.require_response_proof
3334
- || this.capabilities(selected.health).response_proof === RESPONSE_PROOF_SCHEMA;
3335
- requestBody.model = payment.model;
3336
- const encodedPayment = base64Url(Buffer.from(stableStringify(payment.payment), "utf8"));
3337
- let milliseconds = remainingMs(deadline);
3338
- this.verifyRelayIdentity(selected.relayUrl, selected.health);
3339
- // Persist the request identity before any execution can occur. A crash
3340
- // or lost response must leave a scoped record that can be reconciled.
3341
- dispatchPayment = payment;
3342
- try { this.recordDispatch(selected.relayUrl, path, payment, session.id); }
3343
- catch { return { payload: openaiError("Cannot save request tracking; request was not dispatched", "request_tracking_unavailable"), status: 503, headers: { "x-mycomesh-request-id": requestId } }; }
3344
- milliseconds = remainingMs(deadline);
3345
- postStarted = true;
3346
- const response = await fetchWithTimeout(`${selected.relayUrl}${path}`, {
3347
- method: "POST",
3348
- headers: { "content-type": "application/json", accept: "application/json", "PAYMENT-SIGNATURE": encodedPayment,
3349
- ...(proofRequired ? { "X-MycoMesh-Response-Proof": RESPONSE_PROOF_SCHEMA } : {}),
3350
- "X-MycoMesh-Request-Timeout-Ms": String(milliseconds) },
3351
- body: JSON.stringify(requestBody),
3352
- dispatcher: this.dispatcher,
3353
- }, milliseconds);
3354
- successfulHttpResponse = response.status >= 200 && response.status < 300;
3355
- const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
3356
- let payload, bodyError = null;
3357
- try { payload = await readJsonResponse(response); } catch (error) { bodyError = error; }
3358
- if (!bodyError && successfulHttpResponse && (!payload || typeof payload !== "object" || Array.isArray(payload)
3359
- || (payload.error && !(proofRequired && payload.schema === RESPONSE_PROOF_SCHEMA)))) {
3360
- bodyError = new Error("Relay returned an invalid response body; request was not replayed");
3361
- }
3362
- if (bodyError && !paymentResponse) throw bodyError;
3363
- const retryAfter = response.headers.get("retry-after");
3364
- const headers = { "x-mycomesh-session-id": session.id, "x-mycomesh-request-id": requestId, ...(retryAfter ? { "Retry-After": retryAfter } : {}) };
3365
- if (this.network.protocol_version === 10 && !paymentResponse && response.status >= 400) {
3366
- try { this.recordDispatch(selected.relayUrl, path, payment, session.id, "outcome_unknown"); } catch {}
3367
- const error = openaiError("The request reached the Relay; execution and payment status must be reconciled before retrying. "
3368
- + String(payload?.error?.message || ""), "relay_outcome_unknown");
3369
- Object.assign(error.error, { execution_status: "unknown", retryable: false, request_id: requestId });
3370
- return { payload: error, status: 422, headers: { ...headers, "x-should-retry": "false" } };
3371
- }
3372
- if (this.network.protocol_version !== 10 && !paymentResponse && payload?.error?.execution_status === "not_dispatched") {
3373
- try { this.recordDispatch(selected.relayUrl, path, payment, session.id, "not_dispatched"); } catch {}
3374
- }
3375
- if (!paymentResponse && RETRYABLE_RELAY_STATUS.has(response.status)) {
3376
- this.noteRelayFailure(selected.relayUrl, retryAfter);
3377
- lastError = payload?.error?.message || `Relay returned HTTP ${response.status}`;
3378
- lastResponse = { payload, status: response.status, headers };
3379
- if (this.network.protocol_version === 10 || keepSession || payload?.error?.execution_status !== "not_dispatched") break;
3380
- this.sessionRoutes.delete(session.id);
3381
- continue;
3382
- }
3383
- if (!paymentResponse && response.status >= 400) return { payload: normalizeOpenAiError(payload, "relay_error"), status: response.status, headers };
3384
- this.relayFailures.delete(selected.relayUrl);
3385
- if (!paymentResponse && proofRequired) throw new Error("Relay success is missing its signed payment receipt");
3386
- if (paymentResponse) {
3387
- const settlement = decodePaymentResponse(paymentResponse, {
3388
- protocolVersion: this.network.protocol_version,
3389
- chainId: this.network.chain_id, contract: this.network.settlement_contract,
3390
- requestId, requestHash: payment.payment.authorization.request_hash,
3391
- relay: payment.payment.authorization.relay, relaySigner: payment.payment.authorization.relay_signer,
3392
- channel: payment.channel,
3393
- requireJuryEvidence: this.network.committee_mode === DYNAMIC_PROVIDER_AI_COMMITTEE,
3394
- juryRelayPublicKeys: this.network.jury_relay_public_keys,
3395
- reporter: payment.channel?.consumer_owner,
3396
- });
3397
- const returnedAuthorization = settlement.signed_receipt.authorization;
3398
- if (returnedAuthorization.authorization_hash !== payment.payment.authorization_hash || returnedAuthorization.key_signature !== payment.payment.key_signature) {
3399
- throw new Error("Relay receipt changed the dispatched payment authorization");
3400
- }
3401
- const receipt = settlement.signed_receipt.receipt;
3402
- const authorization = settlement.signed_receipt.authorization.authorization;
3403
- if (authorization.request_id !== requestId || authorization.key.toLowerCase() !== this.paymentAddress
3404
- || (route?.providerSigner && receipt.provider_signer.toLowerCase() !== route.providerSigner.toLowerCase())) {
3405
- throw new Error("Relay receipt does not match this request or its pinned Provider");
3406
- }
3407
- // A fully signed, request-bound receipt may settle regardless of an
3408
- // unsigned Relay accepted=false label; keep it eligible for RPC sync.
3409
- settlement.accepted = true;
3410
- let contentError = bodyError
3411
- || (settlement.jury_evidence_error
3412
- ? new Error(`Relay returned no valid recoverable jury evidence: ${settlement.jury_evidence_error}`)
3413
- : null)
3414
- || (!successfulHttpResponse ? new Error("Relay returned a payment receipt with an HTTP error") : null);
3415
- headers["x-mycomesh-content-verification"] = contentError ? "failed" : "receipt-only";
3416
- if (proofRequired && !contentError) {
3417
- try {
3418
- payload = verifyResponseProof(payload, receipt, { requestId,
3419
- endpoint: path.endsWith("/chat/completions") ? "chat" : "responses", model: payment.model });
3420
- headers["x-mycomesh-content-verification"] = "provider-signed";
3421
- } catch (error) {
3422
- contentError = error;
3423
- headers["x-mycomesh-content-verification"] = "failed";
3424
- }
3425
- }
3426
- if (route) route.providerSigner = receipt.provider_signer;
3427
- try {
3428
- this.recordReceipt(selected.relayUrl, path, String(body.model || payment.model), settlement, payment.model, session.id,
3429
- headers["x-mycomesh-content-verification"]);
3430
- } catch {
3431
- // A persistence failure must never replay a completed, billed inference.
3432
- this.historySyncError = "付款回执已收到,但账单保存失败;请保留本次响应回执";
3433
- headers["x-mycomesh-history-status"] = "persistence-error";
3434
- }
3435
- headers["PAYMENT-RESPONSE"] = paymentResponse;
3436
- if (contentError) {
3437
- this.noteRelayFailure(selected.relayUrl);
3438
- const result = { payload: openaiError("Response content verification failed; a valid payment receipt was received and may settle. Request was not replayed.", "content_verification_failed"), status: 502, headers };
3439
- if (this.network.protocol_version === 10) {
3440
- result.status = 422; result.headers["x-should-retry"] = "false";
3441
- Object.assign(result.payload.error, { execution_status: "executed", retryable: false, request_id: requestId });
3442
- }
3443
- return result;
3444
- }
3445
- }
3446
- this.sessions.remember(payload, session.id);
3447
- return { payload: restoreClientResponse(payload, clientBody), status: 200, headers };
3448
- } catch (error) {
3449
- lastError = error.message;
3450
- if (!postStarted && dispatchPayment) {
3451
- try { this.recordDispatch(selected.relayUrl, path, dispatchPayment, session.id, "not_dispatched"); } catch {}
3452
- }
3453
- if (postStarted && dispatchPayment) {
3454
- try { this.recordDispatch(selected.relayUrl, path, dispatchPayment, session.id, "outcome_unknown"); } catch {}
3455
- }
3456
- if (postStarted) this.noteRelayFailure(selected.relayUrl);
3457
- if (postStarted && this.network.protocol_version === 10) {
3458
- const payload = openaiError("Execution or payment status is unknown; this request was not replayed. Check its request ID before retrying.", "relay_outcome_unknown");
3459
- Object.assign(payload.error, { execution_status: "unknown", retryable: false, request_id: requestId });
3460
- const transportCode = error?.cause?.code ?? error?.code;
3461
- if (["UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_SOCKET", "ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"].includes(transportCode)) {
3462
- payload.error.transport_code = transportCode;
3463
- }
3464
- return { payload, status: 422, headers: { "x-should-retry": "false", "x-mycomesh-session-id": session.id, "x-mycomesh-request-id": requestId } };
3465
- }
3466
- if (!postStarted && error.code === "rpc_unavailable") {
3467
- lastResponse = preDispatchFailure(lastError, "rpc_unavailable");
3468
- break;
3469
- }
3470
- if (!postStarted && error.statusCode === 504) {
3471
- lastResponse = preDispatchFailure(lastError, "request_timeout", 504);
3472
- break;
3473
- }
3474
- if (successfulHttpResponse) {
3475
- lastResponse = { payload: openaiError(lastError, "invalid_relay_response"), status: 502,
3476
- headers: { "x-mycomesh-session-id": session.id, "x-mycomesh-request-id": requestId } };
3477
- break;
3478
- }
3479
- if (postStarted) {
3480
- // A lost connection is not proof that a billed inference did not run.
3481
- lastResponse = { payload: openaiError("Relay outcome is unknown; request was not replayed", "relay_outcome_unknown"), status: 502,
3482
- headers: { "x-mycomesh-session-id": session.id, "x-mycomesh-request-id": requestId } };
3483
- break;
3484
- }
3485
- if (keepSession) break;
3486
- this.sessionRoutes.delete(session.id);
3487
- } finally {
3488
- this.relayInFlight.set(selected.relayUrl, Math.max(0, (this.relayInFlight.get(selected.relayUrl) || 1) - 1));
3489
- if (route) { route.inFlight = Math.max(0, route.inFlight - 1); route.lastUsed = Date.now(); }
3490
- }
3491
- }
3492
- if (lastResponse) return lastResponse;
3493
- return preDispatchFailure(lastError, keepSession ? "session_unavailable" : "relay_unavailable");
3494
- }
3495
- }
3496
-
3497
- function decodePaymentResponse(value, expected = {}) {
3498
- let payload;
3499
- try { payload = JSON.parse(Buffer.from(String(value), "base64url").toString("utf8")); } catch { throw new Error("Relay returned an invalid PAYMENT-RESPONSE"); }
3500
- if (!payload || typeof payload !== "object" || !payload.signed_receipt) throw new Error("Relay PAYMENT-RESPONSE is missing its signed receipt");
3501
- try {
3502
- const verified = verifySignedReceipt(payload.signed_receipt, expected);
3503
- if (expected.protocolVersion === 10) {
3504
- payload.signed_receipt = { ...payload.signed_receipt, receipt: verified.receipt };
3505
- if (expected.requireJuryEvidence) {
3506
- try {
3507
- payload.jury_evidence_reference = verifyJuryEvidenceReference(
3508
- payload.jury_evidence_reference,
3509
- { ...expected, settlementKey: payload.settlement_key,
3510
- issuedAt: verified.authorization.authorization.issued_at },
3511
- );
3512
- } catch (error) {
3513
- delete payload.jury_evidence_reference;
3514
- payload.jury_evidence_error = String(error?.message || "invalid jury evidence reference");
3515
- }
3516
- }
3517
- }
3518
- } catch (error) { throw new Error(`Relay returned an invalid signed receipt: ${error.message}`); }
3519
- return payload;
3520
- }
3521
-
3522
- export function verifyResponseProof(proof, receipt, { requestId, endpoint, model }) {
3523
- if (!proof || proof.schema !== RESPONSE_PROOF_SCHEMA || typeof proof.commitment_b64 !== "string"
3524
- || proof.commitment_b64.length > MAX_BODY_BYTES) throw new Error("Relay response is missing its Provider content proof");
3525
- const bytes = Buffer.from(proof.commitment_b64, "base64");
3526
- if (bytes.toString("base64") !== proof.commitment_b64) throw new Error("Invalid Provider content proof encoding");
3527
- if (`0x${createHash("sha256").update(bytes).digest("hex")}` !== receipt.response_hash) {
3528
- throw new Error("Response content differs from its Provider-signed commitment");
3529
- }
3530
- const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
3531
- if (!value || value.schema !== "mycomesh.provider-response-commitment.v1"
3532
- || value.request_id !== requestId || value.endpoint !== endpoint || value.model !== model
3533
- || !value.raw || typeof value.raw !== "object" || Array.isArray(value.raw)) {
3534
- throw new Error("Provider content proof does not match this request");
3535
- }
3536
- // Only authenticated bytes supply the API result. Ignore unauthenticated
3537
- // sibling fields; verify before model/schema restoration or SSE generation.
3538
- return value.raw;
3539
- }
3540
-
3541
- function normalizeOpenAiError(value, fallbackType) {
3542
- if (!value || typeof value !== "object") return openaiError(String(value || fallbackType), fallbackType);
3543
- if (!value.error || typeof value.error !== "object") return openaiError(value.detail || value.message || fallbackType, fallbackType);
3544
- return { ...value, error: { ...value.error, message: String(value.error.message || fallbackType), type: String(value.error.type || fallbackType), param: value.error.param ?? null, code: value.error.code || value.error.type || fallbackType } };
3545
- }
3546
-
3547
- function clientToolSchema(tools, name) {
3548
- if (!Array.isArray(tools)) return undefined;
3549
- for (const tool of tools) {
3550
- if (!tool || typeof tool !== "object") continue;
3551
- const definition = tool.function && typeof tool.function === "object" ? tool.function : tool;
3552
- if (definition.name === name && definition.parameters && typeof definition.parameters === "object") return definition.parameters;
3553
- }
3554
- return undefined;
3555
- }
3556
-
3557
- function orderBySchema(value, schema) {
3558
- if (Array.isArray(value)) return value.map((item) => orderBySchema(item, schema?.items));
3559
- if (!value || typeof value !== "object" || Array.isArray(schema)) return value;
3560
- const properties = schema?.properties;
3561
- if (!properties || typeof properties !== "object") return value;
3562
- const ordered = {};
3563
- for (const key of Object.keys(properties)) {
3564
- if (Object.prototype.hasOwnProperty.call(value, key)) ordered[key] = orderBySchema(value[key], properties[key]);
3565
- }
3566
- for (const key of Object.keys(value)) {
3567
- if (!Object.prototype.hasOwnProperty.call(ordered, key)) ordered[key] = value[key];
3568
- }
3569
- return ordered;
3570
- }
3571
-
3572
- function restoreClientResponse(payload, body) {
3573
- if (!payload || typeof payload !== "object") return payload;
3574
- if (typeof body.model === "string" && body.model) payload.model = body.model;
3575
- const calls = [];
3576
- for (const choice of Array.isArray(payload.choices) ? payload.choices : []) {
3577
- calls.push(...(Array.isArray(choice?.message?.tool_calls) ? choice.message.tool_calls : []));
3578
- }
3579
- calls.push(...(Array.isArray(payload.output) ? payload.output.filter((item) => item?.type === "function_call") : []));
3580
- for (const call of calls) {
3581
- const fn = call?.function && typeof call.function === "object" ? call.function : call;
3582
- if (typeof fn?.name !== "string" || typeof fn.arguments !== "string") continue;
3583
- const schema = clientToolSchema(body.tools, fn.name);
3584
- if (!schema) continue;
3585
- try { fn.arguments = JSON.stringify(orderBySchema(JSON.parse(fn.arguments), schema)); } catch {}
3586
- }
3587
- return payload;
3588
- }
3589
-
3590
- function decodeRequestBody(request) {
3591
- return new Promise((resolve, reject) => {
3592
- let size = 0;
3593
- const chunks = [];
3594
- request.on("data", (chunk) => {
3595
- size += chunk.length;
3596
- if (size > MAX_BODY_BYTES) {
3597
- reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));
3598
- request.destroy();
3599
- return;
3600
- }
3601
- chunks.push(chunk);
3602
- });
3603
- request.on("end", () => {
3604
- try {
3605
- const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
3606
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("request body must be an object");
3607
- resolve(value);
3608
- } catch (error) { reject(error); }
3609
- });
3610
- request.on("error", reject);
3611
- });
3612
- }
3613
-
3614
- function writeJson(response, status, value, headers = {}) {
3615
- const body = JSON.stringify(value);
3616
- response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...headers });
3617
- response.end(body);
3618
- }
3619
-
3620
- function waitForTunnelUrl(child) {
3621
- return new Promise((resolve, reject) => {
3622
- let output = "";
3623
- let publicUrl = "";
3624
- let connected = false;
3625
- const diagnostics = () => output.trim().replace(/\s+/g, " ").slice(-1000);
3626
- const finish = (error, url) => {
3627
- clearTimeout(timer);
3628
- child.removeListener("error", onError);
3629
- child.removeListener("exit", onExit);
3630
- child.stdout?.removeListener("data", onData);
3631
- child.stderr?.removeListener("data", onData);
3632
- if (error) reject(error); else resolve(url);
3633
- };
3634
- const onData = (chunk) => {
3635
- output = `${output}${String(chunk)}`.slice(-16_384);
3636
- const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
3637
- if (match) publicUrl = match[0];
3638
- if (/Registered tunnel connection/i.test(output)) connected = true;
3639
- if (publicUrl && connected) finish(null, publicUrl);
3640
- };
3641
- const onError = (error) => finish(new Error(`could not start cloudflared: ${error.message}`));
3642
- const onExit = (code) => {
3643
- const detail = diagnostics();
3644
- finish(new Error(`cloudflared exited before publishing a URL (${code ?? "signal"})${detail ? `: ${detail}` : ""}`));
3645
- };
3646
- const timer = setTimeout(
3647
- () => {
3648
- const detail = diagnostics();
3649
- finish(new Error(`timed out waiting for the temporary HTTPS URL${detail ? `: ${detail}` : ""}`));
3650
- },
3651
- TUNNEL_START_TIMEOUT_MS,
3652
- );
3653
- child.once("error", onError);
3654
- child.once("exit", onExit);
3655
- child.stdout?.on("data", onData);
3656
- child.stderr?.on("data", onData);
3657
- });
3658
- }
3659
-
3660
- async function stopTunnelProcess(child) {
3661
- if (!child || child.exitCode !== null || child.signalCode != null) return;
3662
- await new Promise((resolve) => {
3663
- let settled = false;
3664
- const finish = () => {
3665
- if (settled) return;
3666
- settled = true;
3667
- clearTimeout(timer);
3668
- child.removeListener("exit", finish);
3669
- resolve();
3670
- };
3671
- const timer = setTimeout(finish, TUNNEL_STOP_TIMEOUT_MS);
3672
- child.once("exit", finish);
3673
- try { child.kill("SIGTERM"); } catch { finish(); }
3674
- });
3675
- }
3676
-
3677
- async function handleInference(state, request, response, path, alphaSearchQuery, shareOnly = false) {
3678
- const authorization = String(request.headers.authorization || "");
3679
- if (!state.authorizeBearer(authorization, { shareOnly })) {
3680
- writeJson(response, 401, openaiError("invalid MycoMesh access key", "invalid_api_key"));
3681
- return;
3682
- }
3683
- let body;
3684
- try { body = await decodeRequestBody(request); } catch (error) {
3685
- writeJson(response, 400, openaiError(error.message, "invalid_request_error"));
3686
- return;
3687
- }
3688
- const alphaSearch = alphaSearchQuery !== undefined;
3689
- if (alphaSearch) {
3690
- body = {
3691
- model: body.model,
3692
- ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
3693
- ...(body.conversation !== undefined ? { conversation: body.conversation } : {}),
3694
- input: [{ type: "mycomesh_alpha_search_request", request: body, query: alphaSearchQuery }],
3695
- max_output_tokens: body.max_output_tokens ?? 2000,
3696
- };
3697
- path = "/v1/responses";
3698
- }
3699
- if (path.endsWith("/responses/compact") && !hasCompactionTrigger(body.input)) {
3700
- const items = Array.isArray(body.input) ? [...body.input] : [];
3701
- if (body.input !== undefined && body.input !== "" && !Array.isArray(body.input)) {
3702
- items.push(typeof body.input === "string" ? { type: "message", role: "user", content: body.input } : body.input);
3703
- }
3704
- items.push({ type: "compaction_trigger" });
3705
- body.input = items;
3706
- }
3707
- try {
3708
- const result = await state.relayInference(path, body, request.headers);
3709
- if (result.status >= 400) {
3710
- writeJson(response, result.status, result.payload, result.headers);
3711
- return;
3712
- }
3713
- for (const [name, value] of Object.entries(result.headers || {})) response.setHeader(name, value);
3714
- if (alphaSearch) {
3715
- const payload = result.payload?.mycomesh_alpha_search_response;
3716
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
3717
- writeJson(response, 502, openaiError("Provider returned an invalid alpha/search response", "mycomesh_provider_error"));
3718
- return;
3719
- }
3720
- writeJson(response, 200, payload, result.headers);
3721
- return;
3722
- }
3723
- if (body.stream === true) {
3724
- response.writeHead(200, { ...result.headers, "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache", connection: "keep-alive", "x-mycomesh-streaming-mode": "buffered" });
3725
- const events = path.endsWith("/chat/completions")
3726
- ? chatCompletionSse(result.payload, body.stream_options?.include_usage === true)
3727
- : responseSse(result.payload, path.endsWith("/responses/compact") || hasCompactionTrigger(body.input));
3728
- for (const chunk of events) response.write(chunk);
3729
- response.end();
3730
- return;
3731
- }
3732
- writeJson(response, 200, result.payload, result.headers);
3733
- } catch (error) {
3734
- writeJson(response, 502, openaiError(error.message, "mycomesh_relay_error"));
3735
- }
3736
- }
3737
-
3738
- export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, publicOnly = false } = {}) {
3739
- const server = createServer(async (request, response) => {
3740
- try {
3741
- const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`);
3742
- const path = url.pathname.startsWith("/v1/v1/") ? url.pathname.slice(3) : url.pathname;
3743
- if (publicOnly) {
3744
- response.setHeader("Access-Control-Allow-Origin", "*");
3745
- response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-MycoMesh-Session-Id, X-Session-Id, session_id");
3746
- response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3747
- response.setHeader("Access-Control-Expose-Headers", "PAYMENT-RESPONSE, Retry-After, X-MycoMesh-Session-Id, X-MycoMesh-History-Status, X-MycoMesh-Content-Verification");
3748
- if (request.method === "OPTIONS") { response.writeHead(204); response.end(); return; }
3749
- if (request.method === "GET" && (path === "/health" || path === "/v1/health")) {
3750
- if (!state.authorizeBearer(String(request.headers.authorization || ""), { shareOnly: true })) { writeJson(response, 401, openaiError("invalid temporary access key", "invalid_api_key")); return; }
3751
- writeJson(response, 200, { ok: true, protocol: "mycomesh-temporary-share/v1", expires_at: state.activeShare()?.expiresAt });
3752
- return;
3753
- }
3754
- if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
3755
- if (!state.authorizeBearer(String(request.headers.authorization || ""), { shareOnly: true })) { writeJson(response, 401, openaiError("invalid temporary access key", "invalid_api_key")); return; }
3756
- try { const selected = await state.chooseRelay(); const models = Array.isArray(state.capabilities(selected.health).models) ? state.capabilities(selected.health).models : [state.capabilities(selected.health).model || DEFAULT_MODEL]; writeJson(response, 200, { object: "list", data: models.map((id) => ({ id: String(id), object: "model", owned_by: "mycomesh", relay: selected.relayUrl })) }); }
3757
- catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
3758
- return;
3759
- }
3760
- if (request.method === "POST" && ["/responses", "/v1/responses", "/v1/v1/responses", "/backend-api/codex/responses", "/responses/compact", "/v1/responses/compact", "/v1/v1/responses/compact", "/backend-api/codex/responses/compact", "/chat/completions", "/v1/chat/completions"].includes(path)) {
3761
- const relayPath = path.endsWith("/chat/completions") ? "/v1/chat/completions" : path.endsWith("/responses/compact") ? "/v1/responses/compact" : "/v1/responses";
3762
- await handleInference(state, request, response, relayPath, undefined, true); return;
3763
- }
3764
- if (request.method === "POST" && ["/alpha/search", "/v1/alpha/search", "/backend-api/codex/alpha/search"].includes(path)) {
3765
- await handleInference(state, request, response, "/v1/responses", Object.fromEntries(url.searchParams), true); return;
3766
- }
3767
- writeJson(response, 404, openaiError("route not found", "invalid_request_error"));
3768
- return;
3769
- }
3770
- if (path === "/" || path === "/credentials" || path === "/codex-env" || path.startsWith("/v1/mycomesh/local/")) {
3771
- const remote = request.socket.remoteAddress || "";
3772
- const authority = String(request.headers.host || "").toLowerCase();
3773
- const localPort = server.address()?.port;
3774
- const localAuthority = new Set([`127.0.0.1:${localPort}`, `localhost:${localPort}`, `[::1]:${localPort}`]);
3775
- const origin = request.headers.origin;
3776
- if (!(remote === "::1" || /^(?:::ffff:)?127\.\d+\.\d+\.\d+$/.test(remote))
3777
- || !localAuthority.has(authority)
3778
- || (origin !== undefined && origin !== `http://${authority}`)
3779
- || request.headers["sec-fetch-site"] === "cross-site") {
3780
- writeJson(response, 403, { ok: false, error: "local management requires a same-origin loopback connection" });
3781
- return;
3782
- }
3783
- response.setHeader("X-Frame-Options", "DENY");
3784
- response.setHeader("Content-Security-Policy", "frame-ancestors 'none'; base-uri 'none'; object-src 'none'");
3785
- response.setHeader("Referrer-Policy", "no-referrer");
3786
- response.setHeader("X-Content-Type-Options", "nosniff");
3787
- }
3788
- if (request.method === "GET" && path === "/") {
3789
- response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
3790
- response.end(consumerHtml());
3791
- return;
3792
- }
3793
- if (request.method === "GET" && path === "/health") { writeJson(response, 200, state.healthPayload()); return; }
3794
- if (request.method === "GET" && path === "/ready") {
3795
- try {
3796
- writeJson(response, 200, await state.readinessPayload());
3797
- }
3798
- catch (error) {
3799
- const timedOut = error?.statusCode === 504;
3800
- const payload = { ok: false, liveness_ready: false, paid_ready: null, paid_readiness_checked: false,
3801
- error: timedOut ? "readiness check timed out; retry shortly" : error.message,
3802
- code: timedOut ? "readiness_timeout" : (error.code || "relay_unavailable") };
3803
- if (error.availableAt) payload.available_at = error.availableAt;
3804
- writeJson(response, 503, payload, timedOut
3805
- ? { "retry-after": "2" }
3806
- : error.code === "budget_not_started" && error.availableAt
3807
- ? { "retry-after": String(Math.max(1, Math.ceil(error.availableAt - Date.now() / 1000))) } : {});
3808
- }
3809
- return;
3810
- }
3811
- if (request.method === "GET" && path === "/paid-ready") {
3812
- const payload = await state.paidReadinessPayload();
3813
- const status = payload.paid_ready ? 200
3814
- : ["wallet_locked", "payment_key_not_ready"].includes(payload.code) ? 423
3815
- : payload.code === "budget_unavailable" || payload.code === "budget_not_started" ? 402
3816
- : 503;
3817
- const headers = payload.code === "budget_not_started" && payload.available_at
3818
- ? { "retry-after": String(Math.max(1, Math.ceil(payload.available_at - Date.now() / 1000))) }
3819
- : {};
3820
- writeJson(response, status, payload, headers);
3821
- return;
3822
- }
3823
- if (request.method === "GET" && (path === "/credentials" || path === "/codex-env")) {
3824
- if (!state.paymentUnlocked) { writeJson(response, 423, { ok: false, error: "sign in with the payment-key owner wallet first" }); return; }
3825
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3826
- response.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); response.end(`${state.credentialsText()}\n`); return;
3827
- }
3828
- if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
3829
- try { writeJson(response, 200, { object: "list", data: await state.modelCatalog() }); }
3830
- catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
3831
- return;
3832
- }
3833
- if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/challenge") {
3834
- try { const value = await decodeRequestBody(request); writeJson(response, 200, state.createWalletChallenge(value.wallet)); }
3835
- catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3836
- return;
3837
- }
3838
- if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/authenticate") {
3839
- try { writeJson(response, 200, await state.authenticateWallet(await decodeRequestBody(request))); }
3840
- catch (error) {
3841
- const payload = { ok: false, error: error.message };
3842
- // Keep the legacy error string, and expose only explicit public fields.
3843
- if (["payment_key_owner_mismatch", "wallet_verification_unavailable"].includes(error.code)) {
3844
- payload.code = error.code;
3845
- payload.selected_wallet = error.selected_wallet;
3846
- payload.payment_key_address = error.payment_key_address;
3847
- if (error.expected_owner) payload.expected_owner = error.expected_owner;
3848
- }
3849
- writeJson(response, error.code === "wallet_verification_unavailable" ? 503 : 401, payload);
3850
- }
3851
- return;
3852
- }
3853
- if (request.method === "GET" && path === "/v1/mycomesh/local/dashboard") {
3854
- writeJson(response, 200, await state.dashboardPayload(state.authorizeManagement(String(request.headers.authorization || "")))); return;
3855
- }
3856
- if (request.method === "POST" && path === "/v1/mycomesh/local/history/sync") {
3857
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3858
- await state.refreshDisputeTransactions(true);
3859
- await state.refreshReceiptStatuses(true);
3860
- writeJson(response, 200, { ok: true, history_sync: {
3861
- running: Boolean(state.historySync), last_checked_at: state.historySyncAt, error: state.historySyncError,
3862
- } }); return;
3863
- }
3864
- if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/activate") {
3865
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3866
- try { writeJson(response, 200, await state.activateCurrentPaymentKey()); }
3867
- catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3868
- return;
3869
- }
3870
- if (request.method === "POST" && path === "/v1/mycomesh/local/wallet/lock") {
3871
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3872
- writeJson(response, 200, await state.lockWallet()); return;
3873
- }
3874
- if (request.method === "POST" && path === "/v1/mycomesh/local/transactions") {
3875
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3876
- try { writeJson(response, 200, await state.transactionPlan(await decodeRequestBody(request))); }
3877
- catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3878
- return;
3879
- }
3880
- if (request.method === "POST" && path === "/v1/mycomesh/local/disputes/progress") {
3881
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3882
- try { writeJson(response, 200, state.recordDisputeProgress(await decodeRequestBody(request))); }
3883
- catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3884
- return;
3885
- }
3886
- if (request.method === "POST" && path === "/v1/mycomesh/local/key/prepare") {
3887
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3888
- try { writeJson(response, 200, state.preparePaymentKey()); } catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3889
- return;
3890
- }
3891
- if (request.method === "POST" && path === "/v1/mycomesh/local/key/activate") {
3892
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3893
- try { const value = await decodeRequestBody(request); writeJson(response, 200, await state.activatePendingPaymentKey(value.wallet)); }
3894
- catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3895
- return;
3896
- }
3897
- if (request.method === "POST" && path === "/v1/mycomesh/local/share/start") {
3898
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3899
- try { const value = await decodeRequestBody(request); writeJson(response, 200, { ok: true, share: await state.startShare(value.minutes) }); }
3900
- catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
3901
- return;
3902
- }
3903
- if (request.method === "POST" && path === "/v1/mycomesh/local/share/stop") {
3904
- if (!state.authorizeManagement(String(request.headers.authorization || ""))) { writeJson(response, 401, { ok: false, error: "wallet login required" }); return; }
3905
- writeJson(response, 200, { ok: true, share: await state.stopShare() });
3906
- return;
3907
- }
3908
- if (request.method === "POST" && ["/responses", "/v1/responses", "/v1/v1/responses", "/backend-api/codex/responses", "/responses/compact", "/v1/responses/compact", "/v1/v1/responses/compact", "/backend-api/codex/responses/compact", "/chat/completions", "/v1/chat/completions"].includes(path)) {
3909
- const relayPath = path.endsWith("/chat/completions")
3910
- ? "/v1/chat/completions"
3911
- : path.endsWith("/responses/compact")
3912
- ? "/v1/responses/compact"
3913
- : "/v1/responses";
3914
- await handleInference(state, request, response, relayPath); return;
3915
- }
3916
- if (request.method === "POST" && ["/alpha/search", "/v1/alpha/search", "/backend-api/codex/alpha/search"].includes(path)) {
3917
- await handleInference(state, request, response, "/v1/responses", Object.fromEntries(url.searchParams)); return;
3918
- }
3919
- writeJson(response, 404, openaiError("route not found", "invalid_request_error"));
3920
- } catch (error) {
3921
- if (!response.headersSent) writeJson(response, 500, openaiError(error.message)); else response.destroy();
3922
- }
3923
- });
3924
- return { server, host, port, listen: () => new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, () => resolve(server.address())); }), close: () => new Promise((resolve) => server.close(() => resolve())) };
3925
- }
3926
-
3927
- function hasCompactionTrigger(value) {
3928
- return Array.isArray(value) && value.some((item) => item && typeof item === "object" && item.type === "compaction_trigger");
3929
- }
3930
-
3931
- function shellQuote(value) {
3932
- return `'${String(value).replaceAll("'", "'\\''")}'`;
3933
- }
3934
-
3935
- function parseUsdc(value) {
3936
- const text = String(value || "").trim();
3937
- if (!/^\d+(?:\.\d{1,6})?$/.test(text)) throw new Error("enter a valid positive top-up amount");
3938
- const [whole, fraction = ""] = text.split(".");
3939
- const amount = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
3940
- if (amount <= 0n) throw new Error("enter a valid positive top-up amount");
3941
- return amount;
3942
- }
3943
-
3944
- function contractData(signature, args) {
3945
- return `0x${bytesToHex(keccak_256(Buffer.from(signature, "ascii")).slice(0, 4))}${args.map((arg) => abiWord(arg).toString("hex")).join("")}`;
3946
- }
3947
-
3948
- function normalizedResponse(payload) {
3949
- const response = JSON.parse(JSON.stringify(payload || {}));
3950
- response.id = String(response.id || response.request_id || `resp_${bytesToHex(randomBytes(16))}`);
3951
- response.object ||= "response";
3952
- response.created_at ||= Math.floor(Date.now() / 1000);
3953
- response.status = ["completed", "failed", "incomplete", "cancelled"].includes(String(response.status || "completed")) ? String(response.status || "completed") : "completed";
3954
- response.output = Array.isArray(response.output) ? response.output.filter((item) => item && typeof item === "object").map(normalizedItem) : [];
3955
- response.error ??= null;
3956
- response.incomplete_details ??= null;
3957
- if (typeof response.output_text !== "string") response.output_text = response.output.filter((item) => item.type === "message").flatMap((item) => item.content || []).filter((part) => part.type === "output_text").map((part) => String(part.text || "")).join("");
3958
- return response;
3959
- }
3960
-
3961
- function normalizedItem(raw) {
3962
- const item = JSON.parse(JSON.stringify(raw));
3963
- item.type = String(item.type || "unknown");
3964
- const prefix = { message: "msg", reasoning: "rs", function_call: "fc", custom_tool_call: "ct", web_search_call: "ws", file_search_call: "fs", code_interpreter_call: "ci", mcp_call: "mcp", mcp_tool_call: "mcp" }[item.type] || "item";
3965
- item.id = String(item.id || `${prefix}_${bytesToHex(randomBytes(16))}`);
3966
- if (item.type === "message") { item.role = String(item.role || "assistant"); item.content = Array.isArray(item.content) ? item.content.map((part) => ({ ...part, type: String(part.type || "output_text"), ...(part.type === "output_text" ? { text: String(part.text || ""), annotations: part.annotations || [], logprobs: part.logprobs || [] } : {}) })) : []; item.status ||= "completed"; }
3967
- if (item.type === "reasoning") { item.summary = Array.isArray(item.summary) ? item.summary : []; item.status ||= "completed"; }
3968
- if (item.type === "function_call") { item.call_id = String(item.call_id || ""); item.name = String(item.name || ""); item.arguments = String(item.arguments || ""); item.status ||= "completed"; }
3969
- return item;
3970
- }
3971
-
3972
- function responseEvents(payload, compact = false) {
3973
- const final = normalizedResponse(payload);
3974
- let sequence = 0;
3975
- const events = [];
3976
- const event = (type, fields = {}) => events.push({ type, sequence_number: sequence++, ...fields });
3977
- if (compact) { final.output.forEach((item, index) => event("response.output_item.done", { output_index: index, item })); event(terminalEvent(final), { response: final }); return events; }
3978
- const created = { ...final, status: "in_progress", output: [], output_text: "", error: null, incomplete_details: null, usage: null };
3979
- event("response.created", { response: created }); event("response.in_progress", { response: created });
3980
- final.output.forEach((item, outputIndex) => {
3981
- const added = { ...item, status: "in_progress" }; if (item.type === "message") added.content = []; if (item.type === "function_call") added.arguments = "";
3982
- event("response.output_item.added", { output_index: outputIndex, item: added });
3983
- if (item.type === "message") {
3984
- (item.content || []).forEach((part, contentIndex) => {
3985
- const partAdded = { ...part }; if (partAdded.type === "output_text") partAdded.text = "";
3986
- event("response.content_part.added", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, part: partAdded });
3987
- if (part.type === "output_text") { const text = String(part.text || ""); if (text) event("response.output_text.delta", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, delta: text, logprobs: part.logprobs || [] }); event("response.output_text.done", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, text, logprobs: part.logprobs || [] }); }
3988
- if (part.type === "refusal") { const refusal = String(part.refusal || ""); if (refusal) event("response.refusal.delta", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, delta: refusal }); event("response.refusal.done", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, refusal }); }
3989
- event("response.content_part.done", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, part });
3990
- });
3991
- } else if (item.type === "reasoning") {
3992
- (item.summary || []).forEach((part, summaryIndex) => { const text = String(part.text || ""); event("response.reasoning_summary_part.added", { item_id: item.id, output_index: outputIndex, summary_index: summaryIndex, part: { ...part, type: "summary_text", text: "" } }); if (text) event("response.reasoning_summary_text.delta", { item_id: item.id, output_index: outputIndex, summary_index: summaryIndex, delta: text }); event("response.reasoning_summary_text.done", { item_id: item.id, output_index: outputIndex, summary_index: summaryIndex, text }); event("response.reasoning_summary_part.done", { item_id: item.id, output_index: outputIndex, summary_index: summaryIndex, part }); });
3993
- } else if (item.type === "function_call") {
3994
- if (item.arguments) event("response.function_call_arguments.delta", { item_id: item.id, output_index: outputIndex, delta: item.arguments });
3995
- event("response.function_call_arguments.done", { item_id: item.id, output_index: outputIndex, call_id: item.call_id, name: item.name, arguments: item.arguments });
3996
- }
3997
- event("response.output_item.done", { output_index: outputIndex, item });
3998
- });
3999
- event(terminalEvent(final), { response: final });
4000
- return events;
4001
- }
4002
-
4003
- function terminalEvent(response) {
4004
- return { completed: "response.completed", failed: "response.failed", incomplete: "response.incomplete", cancelled: "response.incomplete" }[response.status] || "response.completed";
4005
- }
4006
-
4007
- export function responseSse(payload, compact = false) {
4008
- return responseEvents(payload, compact).map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
4009
- }
4010
-
4011
- export function chatCompletionSse(payload, includeUsage = false) {
4012
- const id = String(payload?.id || `chatcmpl_${bytesToHex(randomBytes(16))}`); const model = String(payload?.model || ""); const created = Number(payload?.created || Math.floor(Date.now() / 1000)); const chunks = [];
4013
- const chunk = (index, delta, finishReason = null) => chunks.push(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index, delta, finish_reason: finishReason }] })}\n\n`);
4014
- (Array.isArray(payload?.choices) ? payload.choices : []).forEach((choice, fallbackIndex) => {
4015
- const index = Number.isInteger(choice.index) ? choice.index : fallbackIndex;
4016
- const message = choice.message || {};
4017
- chunk(index, { role: String(message.role || "assistant") });
4018
- if (message.content) chunk(index, { content: String(message.content) });
4019
- (Array.isArray(message.tool_calls) ? message.tool_calls : []).forEach((call, toolIndex) => {
4020
- chunk(index, { tool_calls: [{ index: toolIndex, id: call.id, type: call.type || "function", function: { name: call.function?.name, arguments: String(call.function?.arguments || "") } }] });
4021
- });
4022
- chunk(index, {}, choice.finish_reason || "stop");
4023
- });
4024
- if (includeUsage && payload?.usage) chunks.push(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage: payload.usage })}\n\n`);
4025
- chunks.push("data: [DONE]\n\n"); return chunks;
4026
- }
4027
-
4028
- function consumerHtml() {
4029
- return `<!doctype html>
4030
- <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="light">
4031
- <title>Consumer | MycoMesh</title>
4032
- <style>
4033
- :root{--ink:#17211d;--muted:#68736e;--line:#d8dfdb;--soft:#f2f5f3;--paper:#fff;--green:#147553;--green-dark:#0d5b40;--amber:#9a6413;--red:#ae3d38;--blue:#365d86}*{box-sizing:border-box}[hidden]{display:none!important}html{background:#edf1ee}body{min-width:320px;margin:0;color:var(--ink);background:#edf1ee;font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0}button,input,select{font:inherit;letter-spacing:0}button{cursor:pointer}.shell{min-height:100vh}.topbar{position:sticky;z-index:10;top:0;display:flex;min-height:60px;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);background:rgba(255,255,255,.97);padding:0 max(18px,env(safe-area-inset-left))}.brand{display:flex;align-items:center;gap:10px;font-weight:780}.mark{display:grid;width:30px;height:30px;place-items:center;border-radius:6px;background:var(--ink);color:#fff;font-size:12px}.network{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:12px}.dot{width:7px;height:7px;border-radius:50%;background:#9ba49f}.dot.ok{background:var(--green)}.workspace{width:min(760px,100%);margin:0 auto;background:var(--paper);min-height:calc(100vh - 60px)}.locked{display:grid;min-height:calc(100vh - 60px);align-content:center;padding:42px 24px 88px}.locked-inner{width:min(420px,100%);margin:0 auto}.locked-badge{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:999px;padding:5px 10px;color:var(--muted);font-size:12px}.locked h1{margin:20px 0 9px;font-size:30px;line-height:1.16}.locked p{margin:0 0 26px;color:var(--muted)}.key-preview{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:14px 0;margin:0 0 22px}.label{display:block;color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.key-preview .mono{display:block;margin-top:5px;font-size:12px}.button{display:inline-flex;min-height:42px;align-items:center;justify-content:center;border:1px solid var(--line);border-radius:6px;background:var(--paper);padding:0 15px;color:var(--ink);font-weight:700}.button:hover{border-color:#95a39b;background:#f8faf9}.button.primary{border-color:var(--green);background:var(--green);color:#fff}.button.primary:hover{background:var(--green-dark)}.button.danger{border-color:#e5b6b3;color:var(--red)}.button.small{min-height:34px;padding:0 11px;font-size:12px}.button:disabled{cursor:not-allowed;opacity:.52}.locked .button{width:100%;min-height:48px}.app-head{padding:24px 20px 17px}.app-head-row{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.app-head h1{margin:0;font-size:22px}.wallet-button{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.balance{margin-top:22px}.balance strong{display:block;margin-top:4px;font-size:31px;font-weight:760}.balance-meta{display:flex;gap:16px;margin-top:8px;color:var(--muted);font-size:12px}.tabs{position:sticky;z-index:8;top:60px;display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:rgba(255,255,255,.97);padding:0 12px}.tab{min-width:0;border:0;border-bottom:2px solid transparent;background:transparent;padding:12px 4px;color:var(--muted);font-weight:650}.tab.active{border-color:var(--green);color:var(--ink)}.view{padding:2px 20px 84px}.band{border-top:1px solid var(--line);padding:23px 0}.band:first-child{border-top:0}.section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.section-head h2{margin:0;font-size:16px}.section-head p{margin:3px 0 0;color:var(--muted);font-size:12px}.status{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:999px;padding:4px 9px;color:var(--muted);font-size:11px;white-space:nowrap}.status:before{width:6px;height:6px;border-radius:50%;background:var(--amber);content:""}.status.ok:before{background:var(--green)}.field+.field{margin-top:13px}.field-row{display:flex;align-items:stretch;gap:7px;margin-top:6px}.value{min-width:0;flex:1;border:1px solid var(--line);border-radius:5px;background:var(--soft);padding:10px 11px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.value.exports{min-height:76px;white-space:pre-wrap}.metrics{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--line);border-radius:6px;overflow:hidden}.metric{min-width:0;padding:14px 10px;border-right:1px solid var(--line)}.metric:last-child{border-right:0}.metric span{display:block;color:var(--muted);font-size:11px}.metric strong{display:block;overflow:hidden;margin-top:4px;font-size:17px;text-overflow:ellipsis}.list{margin:0}.list div{display:grid;grid-template-columns:104px minmax(0,1fr);gap:12px;border-bottom:1px solid var(--line);padding:12px 0}.list div:last-child{border-bottom:0}.list dt{color:var(--muted)}.list dd{overflow:hidden;margin:0;text-align:right;text-overflow:ellipsis;white-space:nowrap}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}.topup{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.input,.select{width:100%;min-height:42px;border:1px solid var(--line);border-radius:5px;background:#fff;padding:8px 10px;color:var(--ink)}.input:focus,.select:focus{border-color:var(--green);outline:2px solid rgba(20,117,83,.13)}.share-output{margin-top:16px}.empty{padding:34px 0;color:var(--muted);text-align:center}.table-wrap{overflow-x:auto;border-top:1px solid var(--line)}table{width:100%;min-width:620px;border-collapse:collapse}th,td{padding:11px 8px;border-bottom:1px solid var(--line);text-align:left;font-size:12px}th{color:var(--muted);font-weight:650}.notice{border-left:3px solid var(--amber);background:#fff8e9;padding:10px 12px;color:#76511c;font-size:12px}.notice.error{border-color:var(--red);background:#fff3f2;color:#892f2b}.recovery{display:flex;align-items:center;justify-content:space-between;gap:12px}.recovery span{flex:1}.toast{position:fixed;z-index:30;right:16px;bottom:calc(18px + env(safe-area-inset-bottom));left:16px;max-width:520px;margin:auto;border-radius:6px;background:var(--ink);padding:11px 14px;color:#fff;box-shadow:0 10px 30px rgba(23,33,29,.2);text-align:center}.toast.error{background:#7e2925}@media(min-width:761px){body{padding:22px}.shell{border:1px solid var(--line);border-radius:8px;overflow:hidden;box-shadow:0 18px 50px rgba(23,33,29,.08)}.workspace{min-height:calc(100vh - 106px)}.topbar{position:relative}.tabs{top:0}.view,.app-head{padding-right:28px;padding-left:28px}.toast{bottom:28px}}@media(max-width:420px){.balance strong{font-size:27px}.metrics{grid-template-columns:1fr}.metric{display:flex;align-items:center;justify-content:space-between;border-right:0;border-bottom:1px solid var(--line)}.metric:last-child{border-bottom:0}.metric strong{margin:0}.field-row{flex-direction:column}.field-row .button{width:100%}.topup{grid-template-columns:1fr}.topup .button{width:100%}}
4034
- </style></head><body>
4035
- <div class="shell"><header class="topbar"><div class="brand"><span class="mark">M</span><span>MycoMesh</span></div><div class="network"><span id="networkDot" class="dot"></span><span id="networkName">MycoMesh</span></div></header><main class="workspace">
4036
- <div id="locked" class="locked"><div class="locked-inner"><span class="locked-badge">MycoMesh API</span><h1>连接钱包以继续</h1><p>连接钱包后获取 API 地址和访问密钥。无需填写钱包私钥。</p><p>请在已安装钱包扩展的浏览器中打开此本机地址;未安装钱包时可先安装,再刷新连接。</p><p id="loginOwnerHint" class="notice mono" aria-live="polite">正在核对本机付款 Key 的归属…</p><details class="key-preview"><summary>设备访问凭证详情</summary><span id="lockedKey" class="mono">读取中...</span></details><button id="login" class="button primary" type="button">连接钱包并签名</button><p id="loginError" class="notice error mono" role="alert" hidden></p></div></div>
4037
- <div id="app" hidden><div class="app-head"><div class="app-head-row"><div><span class="label">预付账户</span><h1>Consumer</h1></div><button id="walletButton" class="button small wallet-button" type="button">退出</button></div><div class="balance"><span id="balanceLabel" class="label">可用余额</span><strong id="balance">--</strong><div class="balance-meta"><span id="keyStatus">Key 状态 --</span><span id="requestCount">0 次请求</span></div></div></div>
4038
- <nav class="tabs" aria-label="Consumer navigation"><button class="tab active" data-view="overview" type="button">概览</button><button class="tab" data-view="wallet" type="button">钱包</button><button class="tab" data-view="activity" type="button">记录</button><button class="tab" data-view="share" type="button">分享</button></nav>
4039
- <div id="view-overview" class="view"><div class="band"><div class="section-head"><div><h2>调用就绪状态</h2><p>四项全部就绪后才可发起付费请求</p></div><span id="paidReadiness" class="status">检查中</span></div><dl class="list"><div><dt>钱包与 Key</dt><dd><span id="walletReadiness" class="status">检查中</span></dd></div><div><dt>网络与 RPC</dt><dd><span id="networkReadiness" class="status">检查中</span></dd></div><div><dt>固定预算</dt><dd><span id="budgetReadinessBadge" class="status">检查中</span></dd></div><div><dt>可用模型</dt><dd><span id="modelsReadiness" class="status">检查中</span></dd></div></dl><p id="readinessAction" class="notice"></p></div><div class="band"><div class="section-head"><div><h2>访问凭证</h2><p id="credentialState">等待 Key 激活</p></div><span id="credentialBadge" class="status">锁定</span></div><div id="credentials" hidden><div class="field"><span class="label">API URL</span><div class="field-row"><div id="url" class="value"></div><button class="button small copy" data-copy="url" type="button">复制</button></div></div><div class="field"><span class="label">Key</span><div class="field-row"><div id="key" class="value"></div><button class="button small copy" data-copy="key" type="button">复制</button></div></div><div class="field"><span class="label">Export</span><div class="field-row"><div id="export" class="value exports"></div><button class="button small copy" data-copy="export" type="button">复制</button></div></div></div><div id="inactiveKey" class="notice"><span id="activationNotice">首次使用需在钱包中确认一次访问授权。</span><div class="actions"><button id="setupAccess" class="button primary" type="button">启用 API 访问</button></div></div></div><div class="band"><div class="section-head"><div><h2>网络与模型</h2><p>只显示通过部署校验的可用 Relay 路由</p></div><span id="modelStatus" class="status">读取中</span></div><dl id="modelList" class="list"><div><dt>模型目录</dt><dd>正在读取…</dd></div></dl><p id="modelError" class="notice error" hidden></p></div><div class="band"><div class="section-head"><h2>本地用量</h2></div><div class="metrics"><div class="metric"><span>累计消费</span><strong id="spent">--</strong></div><div class="metric"><span>输入 Tokens</span><strong id="inputTokens">0</strong></div><div class="metric"><span>输出 Tokens</span><strong id="outputTokens">0</strong></div></div></div></div>
4040
- <div id="view-wallet" class="view" hidden><div id="budgetPanel" class="band" hidden><div class="section-head"><h2>固定预算</h2><span id="budgetLocked" class="status"></span></div><p id="budgetNote" class="notice"></p><p id="budgetUnallocated" class="notice"></p><p class="notice">充值余额需开通固定预算后才能调用;充值不等于已有可调用预算。</p><p id="budgetReadiness" class="notice"></p><div id="budgetChannels"></div></div><div class="band"><div class="section-head"><h2>钱包与 Key</h2><span id="chainStatus" class="status">读取中</span></div><dl class="list"><div><dt>钱包</dt><dd id="walletAddress" class="mono"></dd></div><div><dt>Key 地址</dt><dd id="keyAddress" class="mono"></dd></div><div><dt>单次上限</dt><dd id="keyLimit"></dd></div><div><dt>有效期</dt><dd id="keyValidity"></dd></div></dl><p id="chainError" class="notice error" hidden></p><div class="actions"><button id="activate" class="button primary" type="button">激活 Key</button><button id="rotate" class="button danger" type="button" hidden>更换 Key</button></div></div><div class="band"><div class="section-head"><div><h2>充值</h2><p id="walletBalance">钱包余额 --</p></div></div><div class="topup"><input id="amount" class="input" type="number" min="0.000001" step="0.000001" inputmode="decimal" autocomplete="off" placeholder="10.00 USDC" aria-label="充值金额(USDC)"><button id="topup" class="button primary" type="button">充值</button></div></div></div>
4041
- <div id="view-activity" class="view" hidden><div class="band"><div class="section-head"><div><h2>消费记录</h2><p>当前 Key 在本机各 Consumer 的账单,按请求去重</p></div><button id="refresh" class="button small" type="button">刷新</button></div><p id="historySync" class="notice" hidden></p><p class="notice">申诉仅用于签名证据能够证明的客观欺诈或服务未交付;错误申诉可能损失举报保证金。页面不会公开完整提示词或 Provider 响应。</p><div id="historyRecovery" class="notice error recovery" hidden><span id="historyRecoveryText"></span><button id="historyRecoveryButton" class="button small" type="button">立即核验</button></div><div id="historyEmpty" class="empty">暂无消费记录</div><div id="historyTable" class="table-wrap" hidden><table><thead><tr><th>时间</th><th>模型</th><th>Tokens</th><th>费用</th><th>Provider</th><th>状态</th><th>会话</th><th>申诉</th></tr></thead><tbody id="history"></tbody></table></div></div></div>
4042
- <div id="view-share" class="view" hidden><div class="band"><div class="section-head"><div><h2>临时分享</h2><p>到期后自动关闭</p></div><span id="shareStatus" class="status">未启用</span></div><div class="topup"><select id="shareMinutes" class="select"><option value="10">10 分钟</option><option value="30" selected>30 分钟</option><option value="60">1 小时</option><option value="360">6 小时</option></select><button id="shareStart" class="button primary" type="button">开始分享</button></div><div id="shareOutput" class="share-output" hidden><div class="field"><span class="label">API URL</span><div class="field-row"><div id="shareUrl" class="value"></div><button class="button small copy" data-copy="shareUrl" type="button">复制</button></div></div><div class="field"><span class="label">临时 Key</span><div class="field-row"><div id="shareKey" class="value"></div><button class="button small copy" data-copy="shareKey" type="button">复制</button></div></div><p id="shareExpiry" class="notice"></p><div class="actions"><button id="shareStop" class="button danger" type="button">停止分享</button></div></div></div></div></div>
4043
- </main></div><div id="toast" class="toast" hidden></div>
4044
- <script>
4045
- let state=null,wallet=null,managementToken=null,busy=false,loading=null;const $=id=>document.getElementById(id);const short=value=>value?value.slice(0,6)+'...'+value.slice(-4):'--';
4046
- function units(value,decimals=6){const raw=BigInt(value||0),base=10n**BigInt(decimals),whole=raw/base,fraction=(raw%base).toString().padStart(decimals,'0').replace(/0+$/,'');return whole.toLocaleString()+(fraction?'.'+fraction.slice(0,Math.max(6,Math.min(decimals,8))):'')}
4047
- function authHeaders(headers={}){const next=new Headers(headers);if(managementToken)next.set('authorization','Bearer '+managementToken);return next}
4048
- async function api(path,options={}){const response=await fetch(path,{...options,headers:authHeaders(options.headers)}),data=await response.json();if(!response.ok){const error=new Error(typeof data.error==='string'?data.error:(data.error?.message||'请求失败'));for(const field of ['code','selected_wallet','expected_owner','payment_key_address'])if(typeof data[field]==='string')error[field]=data[field];if(error.code==='payment_key_owner_mismatch')error.message=ownerMismatchMessage(error);if(error.code==='wallet_verification_unavailable')error.message='暂时无法核验链上钱包归属或预算,请稍后重试。现有 Key、账单和已登录会话保持不变。';throw error}return data}
4049
- function toast(message,error=false){const node=$('toast');node.textContent=message;node.className='toast'+(error?' error':'');node.hidden=false;clearTimeout(toast.timer);toast.timer=setTimeout(()=>node.hidden=true,3200)}
4050
- function setBusy(value){busy=value;for(const button of document.querySelectorAll('button'))button.disabled=value}
4051
- async function run(task){if(busy)return;setBusy(true);try{await task()}catch(error){const message=error?.message||String(error);if(!managementToken){$('loginError').textContent=message;$('loginError').hidden=false}toast(message,true)}finally{setBusy(false)}}
4052
- async function load(){if(loading)return loading;loading=(async()=>{state=await api('/v1/mycomesh/local/dashboard');render()})();try{await loading}finally{loading=null}}
4053
- function render(){const authenticated=Boolean(managementToken&&state.auth?.authenticated),ready=authenticated&&state.auth.key_ready,grant=state.key.grant||{},decimals=state.settlement?.stablecoin_decimals||6,symbol=state.settlement?.stablecoin_symbol||'USDC';$('networkName').textContent=state.chain_error?'链上不可用':(state.settlement?.network_name||'MycoMesh');$('networkDot').className='dot'+(state.inference_ready===true&&!state.chain_error?' ok':'');$('lockedKey').textContent=state.key.address;renderLoginOwner();$('locked').hidden=authenticated;$('app').hidden=!authenticated;if(!authenticated)return;renderReadiness();$('walletButton').textContent=short(state.auth.wallet);$('walletAddress').textContent=state.auth.wallet;$('keyAddress').textContent=state.key.address;$('balanceLabel').textContent=state.protocol_version===10?'可用请求预算':'可用余额';$('balance').textContent=state.protocol_version===10?units(state.budget_available_units||0,decimals)+' '+symbol:(state.account?units(state.account.available_balance_units,decimals)+' '+symbol:'--');$('requestCount').textContent=state.usage.request_count+' 次请求';$('keyStatus').textContent=ready?'Key 已激活':'Key 待激活';$('credentialBadge').className='status'+(ready?' ok':'');$('credentialBadge').textContent=ready?'可用':'待激活';$('credentialState').textContent=ready?'仅在本机显示':'链上确认后显示';$('credentials').hidden=!ready;$('inactiveKey').hidden=ready;if(ready){$('url').textContent=state.credentials.base_url;$('key').textContent=state.credentials.api_key;$('export').textContent=state.credentials.export}$('spent').textContent=units(state.usage.total_spent_units,decimals)+' '+symbol;$('inputTokens').textContent=Number(state.usage.input_tokens||0).toLocaleString();$('outputTokens').textContent=Number(state.usage.output_tokens||0).toLocaleString();$('keyLimit').textContent=grant.max_per_request?units(grant.max_per_request,decimals)+' '+symbol:'--';$('keyValidity').textContent=grant.valid_until?new Date(grant.valid_until*1000).toLocaleString():'长期有效';$('chainStatus').className='status'+(grant.active?' ok':'');$('chainStatus').textContent=grant.active?'链上有效':'等待激活';$('activate').hidden=ready;$('rotate').hidden=!ready;$('chainError').hidden=!state.chain_error;$('chainError').textContent=state.chain_error||'';$('walletBalance').textContent=state.wallet?'钱包余额 '+units(state.wallet.token_balance_units,decimals)+' '+symbol:'钱包余额 --';renderActivation();renderHistory();renderShare();renderBudget()}
4054
- function renderReadiness(){
4055
- const set=(id,ready,yes,no)=>{const node=$(id);node.className='status'+(ready?' ok':'');node.textContent=ready?yes:no};
4056
- set('paidReadiness',state.paid_ready===true,'可调用','未就绪');
4057
- set('walletReadiness',state.wallet_ready===true,'已验证','待验证');
4058
- set('networkReadiness',state.network_ready===true,'已连接','不可用');
4059
- set('budgetReadinessBadge',state.budget_ready===true,'可用','不可用');
4060
- set('modelsReadiness',state.models_ready===true,'有可用线路','无可用线路');
4061
- $('readinessAction').textContent=state.paid_ready===true?'钱包、网络、预算和模型线路均已就绪。':state.inference_code==='payment_key_not_ready'?'请先激活并验证当前钱包的付款 Key。':state.inference_code==='network_unavailable'?'请检查网络与 RPC 配置后刷新。':state.inference_code==='budget_not_started'?'固定预算尚未到启用时间。':state.inference_code==='budget_unavailable'?'当前没有覆盖可用 Provider 线路的固定预算;充值余额不等于可调用预算。':'当前没有可用模型或 Relay 线路。';
4062
- }
4063
- function renderActivation(){
4064
- const grant=state.key.grant||{},ownedActive=grant.active===true&&grant.owner===state.auth?.wallet&&!state.key.pending;
4065
- const exact=ownedActive&&grant.max_per_request===state.key.max_fee_units&&grant.valid_until===0;
4066
- $('activate').textContent=ownedActive?'启用本地访问':'激活 Key';
4067
- $('setupAccess').textContent=ownedActive?'启用本地访问':'启用 API 访问';
4068
- $('activationNotice').textContent=exact?'链上授权已生效。启用此设备的本地访问后即可显示 API 凭证;可调用额度以账户预算为准。':ownedActive?'链上 Key 已生效,启用时将同步访问权限设置。':'首次使用需在钱包中确认一次访问授权。';
4069
- }
4070
- function renderBudget(){
4071
- const panel=$('budgetPanel');panel.hidden=state.protocol_version!==10;if(panel.hidden)return;
4072
- const decimals=state.settlement?.stablecoin_decimals||6,symbol=state.settlement?.stablecoin_symbol||'TestUSDC',now=Math.floor(Date.now()/1000);
4073
- $('budgetLocked').textContent='可用 '+units(state.budget_available_units||0,decimals)+' '+symbol+' · 预算池 '+units(state.budget_locked_units||0,decimals)+' '+symbol;
4074
- $('budgetNote').textContent=state.budget_note||'预算在结算窗口结束前不会撤回;结算按 2 小时或 100 笔先到触发。';
4075
- const ownAccount=state.account&&state.account.owner===state.auth?.wallet;
4076
- $('budgetUnallocated').textContent='已充值、尚未分配预算:'+(ownAccount?units(state.account.available_balance_units,decimals)+' '+symbol:'--');
4077
- $('budgetReadiness').textContent=state.inference_ready?'预算与线路就绪 · 满 2 小时或 100 笔批量结算':state.inference_code==='budget_not_started'?'预算等待启用 · '+new Date(state.budget_available_at*1000).toLocaleString()+';启用前不会派发或扣费':state.inference_code==='budget_unavailable'?'当前没有可用固定预算,无法派发请求;不会扣费。请联系运营方续期开通预算后刷新本页':(state.inference_error||'正在确认预算和可用线路');
4078
- const list=$('budgetChannels');list.replaceChildren();
4079
- const channels=state.capacity_channels||[];
4080
- if(!channels.length){list.textContent='尚未开通固定预算。受控测试期间,由运营方与 Provider 确认预算后开通。';return}
4081
- for(const c of channels){
4082
- const box=document.createElement('div');box.className='band';
4083
- const title=document.createElement('strong');title.textContent='Provider '+short(c.provider_signer);box.appendChild(title);
4084
- const info=document.createElement('p');const budget=c.budget||{};
4085
- const phase=({closed:'已释放',releasable:'可释放余额',previous_key:'此前 Key 的预算',different_owner:'属于其他钱包',admission_closed:'已停止新请求,等待结算',admission_window_short:'剩余接单时间不足',claim_window_short:'剩余结算时间不足',request_limit:'单次请求上限超过预算限制',capacity_exhausted:'请求额度已用完',credit_insufficient:'预算余额不足',stake_insufficient:'Provider 线路暂不可用',not_started:'等待启用,约 '+Math.max(0,Math.ceil((c.valid_from-now)/60))+' 分钟',available:'预算可用'})[budget.reason]||'预算待核验';
4086
- info.textContent=phase+' · 当前可用 '+units(budget.remaining_units||0,decimals)+' '+symbol+(budget.ready?' · 预计还可请求 '+budget.estimated_requests+' 次':'')+' · 最早释放 '+new Date(c.claim_until*1000).toLocaleString();box.appendChild(info);
4087
- if(!c.closed&&now>c.claim_until){const button=document.createElement('button');button.className='button';button.textContent='释放剩余预算';button.onclick=()=>run(async()=>{const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'close_capacity_channel',wallet,channel_id:c.channel_id})});await sendPlan(plan);await load();toast('预算释放交易已确认')});box.appendChild(button)}
4088
- list.appendChild(box)
4089
- }
4090
- }
4091
- function renderHistory(){
4092
- const body=$('history'),items=state.history||[],decimals=state.settlement?.stablecoin_decimals||6,symbol=state.settlement?.stablecoin_symbol||'USDC';
4093
- body.replaceChildren();$('historyEmpty').hidden=items.length>0;$('historyTable').hidden=items.length===0;
4094
- $('spent').previousElementSibling.textContent='已结算消费';$('spent').textContent=units(state.usage.settled_units||0,decimals)+' '+symbol;
4095
- const sync=state.history_sync||{},recoverySummary=state.recovery||{},summary='已结算 '+units(state.usage.settled_units||0,decimals)+' · 已知待结算 '+units(state.usage.pending_units||0,decimals)+' · 结算失败 '+units(state.usage.failed_units||0,decimals)+' '+symbol+(state.usage.unknown_fee_count?' · 费用待核实 '+state.usage.unknown_fee_count+' 笔,授权上限 '+units(state.usage.unknown_fee_authorized_maximum_units,decimals)+' '+symbol:'')+(recoverySummary.uncertain_count?' · 待核验 '+recoverySummary.uncertain_count+' 笔':'');
4096
- $('historySync').hidden=false;$('historySync').textContent=summary+(sync.error?' — '+sync.error:(sync.running?' — 正在同步结算状态…':''));
4097
- const unknown=items.filter(item=>['dispatching','outcome_unknown','broadcast_unknown'].includes(item.status)||['broadcast_unknown','confirmation_timeout'].includes(item.error_code));
4098
- const recovery=$('historyRecovery');recovery.hidden=unknown.length===0&&!sync.error;
4099
- if(unknown.length){const ids=unknown.slice(0,3).map(item=>short(item.request_id)).join('、');$('historyRecoveryText').textContent='有 '+unknown.length+' 笔请求的执行或付款状态尚未核实('+ids+(unknown.length>3?' 等':'')+')。请勿重复提交相同请求;先核验状态,系统只会在链上或 Relay 返回可验证结果后更新账单。'}
4100
- else if(sync.error)$('historyRecoveryText').textContent='账单状态暂时无法核验,原记录已保留。请重试“立即核验”;在状态明确前不要重复提交相同请求。';
4101
- for(const item of items){
4102
- const row=document.createElement('tr'),status=({dispatching:'已发送,结果待核实',outcome_unknown:'结果待核实,请勿重复提交',not_dispatched:'未执行',escrowed:'已托管,争议期内',disputed:'裁决中',released:'已释放',refunded:'已退回余额',dismissed:'举报驳回,已释放',timed_out:'裁决超时,已释放'})[item.status]||(item.status==='confirmed'?'已结算':item.status==='failed'?(item.error_code==='authorization_expired'?'授权过期,未结算':'结算失败'):item.status==='rejected'?'未接受':(item.status==='broadcast_unknown'||item.error_code==='broadcast_unknown'||item.error_code==='confirmation_timeout')?'结果待核实':item.status==='submitted'?'已提交,待确认':'待结算');
4103
- for(const value of [new Date(item.timestamp*1000).toLocaleString(),item.model||'--',(item.input_tokens??'--')+' / '+(item.output_tokens??'--'),item.actual_fee_units==null?'--':units(item.actual_fee_units,decimals),short(item.provider_signer||item.provider),(item.content_verification==='failed'?'正文核验失败;':'')+status,short(item.session_id)]){
4104
- const cell=document.createElement('td');cell.textContent=value;row.appendChild(cell)
4105
- }
4106
- const dispute=document.createElement('td');
4107
- if(item.jury_evidence_hash){
4108
- const evidence=document.createElement('div');evidence.className='mono';evidence.textContent=short(item.jury_evidence_hash);evidence.title=item.jury_evidence_hash;dispute.appendChild(evidence);
4109
- const bond=item.dispute_bond_units||state.settlement?.reporter_bond_units;
4110
- const deadline=item.settlement_release_at?new Date(item.settlement_release_at*1000).toLocaleString():'待链上核验';
4111
- const detail=document.createElement('div');detail.textContent=(bond?'保证金 '+units(bond,decimals)+' '+symbol+' · ':'')+'截止 '+deadline;dispute.appendChild(detail);
4112
- if(item.dispute_explorer_url){const link=document.createElement('a');link.href=item.dispute_explorer_url;link.target='_blank';link.rel='noopener noreferrer';link.textContent='查看交易';dispute.appendChild(link)}
4113
- if(item.dispute_eligible){const button=document.createElement('button');button.className='button small danger';button.type='button';button.textContent=item.dispute_stage==='failed'?'重试申诉':'发起申诉';button.onclick=()=>run(()=>openDispute(item));dispute.appendChild(button)}
4114
- else if(item.dispute_stage){const phase=({planned:'已生成计划',wallet_prompted:'等待钱包结果',submitted:'已提交',uncertain:'结果待核实,请勿重试',failed:'未提交或失败',confirmed:'链上已进入裁决'})[item.dispute_stage]||item.dispute_stage;const stage=document.createElement('div');stage.textContent=phase;dispute.appendChild(stage)}
4115
- }else dispute.textContent='--';
4116
- row.appendChild(dispute);
4117
- body.appendChild(row)
4118
- }
4119
- }
4120
- function renderShare(){const share=state.share||{};$('shareStatus').className='status'+(share.active?' ok':'');$('shareStatus').textContent=share.active?'分享中':'未启用';$('shareOutput').hidden=!share.active;if(share.active){$('shareUrl').textContent=share.base_url;$('shareKey').textContent=share.api_key;$('shareExpiry').textContent='到期时间 '+new Date(share.expires_at*1000).toLocaleString()}}
4121
- function renderModels(items){const list=$('modelList'),status=$('modelStatus'),error=$('modelError');list.replaceChildren();error.hidden=true;if(!Array.isArray(items)||!items.length){status.className='status';status.textContent='暂无可用模型';list.innerHTML='<div><dt>模型目录</dt><dd>暂无通过校验的 Relay 路由</dd></div>';return}const single=items.filter(item=>Number(item.route_count||0)<2);status.className='status'+(single.length?'':' ok');status.textContent=single.length?(items.length+' 个模型 · '+single.length+' 个单 Relay'):(items.length+' 个模型 · 已冗余');for(const item of items){const row=document.createElement('div'),name=document.createElement('dt'),value=document.createElement('dd');name.textContent=String(item.id||'未知模型');value.textContent=Number(item.route_count||0)>1?('双 Relay · '+item.route_count+' 条路由'):'单 Relay · 无自动故障切换';row.append(name,value);list.appendChild(row)}}
4122
- async function loadModels(){try{const response=await fetch('/v1/models',{cache:'no-store'}),data=await response.json();if(!response.ok)throw new Error(data?.error?.message||'模型目录暂时不可用');renderModels(data.data)}catch(error){$('modelStatus').className='status';$('modelStatus').textContent='读取失败';$('modelError').hidden=false;$('modelError').textContent=error?.message||'模型目录暂时不可用'}}
4123
- async function syncHistoryNow(){await api('/v1/mycomesh/local/history/sync',{method:'POST'});await load();const sync=state.history_sync||{};toast(sync.error?'暂时无法完成状态核验,原记录已保留':'已完成状态核验')}
4124
- function walletMessage(value){return '0x'+Array.from(new TextEncoder().encode(value),byte=>byte.toString(16).padStart(2,'0')).join('')}
4125
- function walletProvider(){return window.okxwallet?.ethereum||window.okxwallet||window.ethereum?.providers?.find(p=>p.isOkxWallet||p.isOKExWallet)||window.ethereum}
4126
- function ownerMismatchMessage(value){return '当前选择的钱包:'+value.selected_wallet+'。本机付款 Key('+value.payment_key_address+')已绑定钱包:'+value.expected_owner+'。请在钱包扩展中切换到该归属钱包后重试;若要使用当前钱包,请另建独立 Consumer 配置目录。不要删除或更换现有 Key,原账单和预算仍属于原钱包。'}
4127
- function renderLoginOwner(){const owner=state?.key?.grant?.owner,hint=$('loginOwnerHint');hint.textContent=!owner?'暂时无法核验本机付款 Key 的归属,请稍后重试连接。':owner.toLowerCase()==='0x'+'0'.repeat(40)?'本机付款 Key 尚未绑定钱包,可连接你的钱包完成登录,再激活访问。':'本机付款 Key 已绑定钱包:'+owner+'。请在钱包扩展中选择该账户。若要使用其他钱包,请另建独立 Consumer 配置目录,保留原 Key 和账单。'}
4128
- async function login(){
4129
- $('loginError').hidden=true;
4130
- const provider=walletProvider();if(!provider)throw new Error('未检测到浏览器钱包,请安装钱包扩展后刷新,或在已安装钱包的浏览器中打开此本机地址');
4131
- await load();
4132
- const owner=state?.key?.grant?.owner?.toLowerCase();
4133
- if(!owner)throw new Error('暂时无法核验本机付款 Key 的链上归属,请稍后重试;本次尚未请求签名。');
4134
- const accounts=await provider.request({method:'eth_requestAccounts'}),selectedWallet=String(accounts[0]||'').toLowerCase();
4135
- if(!selectedWallet)throw new Error('钱包未连接');
4136
- if(owner!=='0x'+'0'.repeat(40)&&owner!==selectedWallet)throw Object.assign(new Error(ownerMismatchMessage({selected_wallet:selectedWallet,expected_owner:owner,payment_key_address:state.key.address})),{code:'payment_key_owner_mismatch'});
4137
- const challenge=await api('/v1/mycomesh/local/wallet/challenge',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet:selectedWallet})});
4138
- const signature=await provider.request({method:'personal_sign',params:[walletMessage(challenge.message),selectedWallet]});
4139
- const result=await api('/v1/mycomesh/local/wallet/authenticate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet:selectedWallet,signature})});
4140
- managementToken=result.token;wallet=selectedWallet;await load();toast(state.auth.key_ready?'钱包验证完成':'首次使用:点击“启用 API 访问”,并在钱包中确认');
4141
- }
4142
- async function ensureChain(){const provider=walletProvider();if(!provider)throw new Error('未检测到浏览器钱包,请安装钱包扩展后刷新,或在已安装钱包的浏览器中打开此本机地址');const expected='0x'+Number(state.settlement.chain_id).toString(16),current=await provider.request({method:'eth_chainId'});if(current.toLowerCase()!==expected.toLowerCase())await provider.request({method:'wallet_switchEthereumChain',params:[{chainId:expected}]})}
4143
- async function waitReceipt(hash){const provider=walletProvider();if(!provider)throw new Error('未检测到浏览器钱包,请安装钱包扩展后刷新,或在已安装钱包的浏览器中打开此本机地址');for(let count=0;count<120;count++){const receipt=await provider.request({method:'eth_getTransactionReceipt',params:[hash]});if(receipt){if(receipt.status!=='0x1')throw Object.assign(new Error('链上交易失败'),{code:'transaction_failed'});return receipt}await new Promise(resolve=>setTimeout(resolve,1500))}throw Object.assign(new Error('等待链上确认超时'),{code:'confirmation_timeout'})}
4144
- async function sendPlan(plan){if(Array.isArray(plan.transactions)&&plan.transactions.length===0)return;const provider=walletProvider();if(!provider)throw new Error('未检测到浏览器钱包,请安装钱包扩展后刷新,或在已安装钱包的浏览器中打开此本机地址');await ensureChain();for(const transaction of plan.transactions){toast(transaction.label+':请在钱包中确认');const hash=await provider.request({method:'eth_sendTransaction',params:[{from:wallet,to:transaction.to,data:transaction.data}]});toast(transaction.label+':交易已提交,等待链上确认…');await waitReceipt(hash);toast(transaction.label+':已确认')}}
4145
- async function disputeProgress(item,stage,kind,hash,errorCode){return api('/v1/mycomesh/local/disputes/progress',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet,request_id:item.request_id,channel_id:item.capacity_channel_id,stage,transaction_kind:kind,...(hash?{tx_hash:hash}:{}),...(errorCode?{error_code:errorCode}:{})})})}
4146
- async function openDispute(item){
4147
- const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'open_dispute',wallet,request_id:item.request_id,channel_id:item.capacity_channel_id})});
4148
- const provider=walletProvider();if(!provider)throw new Error('未检测到浏览器钱包');await ensureChain();
4149
- for(const transaction of plan.transactions){
4150
- const kind=transaction.kind;await disputeProgress(item,'wallet_prompted',kind);let hash;
4151
- try{hash=await provider.request({method:'eth_sendTransaction',params:[{from:wallet,to:transaction.to,data:transaction.data}]})}
4152
- catch(error){await disputeProgress(item,error?.code===4001?'failed':'uncertain',kind,null,error?.code===4001?'wallet_rejected':'wallet_unknown');throw error}
4153
- await disputeProgress(item,'submitted',kind,hash);toast(transaction.label+':交易已提交');
4154
- try{await waitReceipt(hash)}catch(error){await disputeProgress(item,error?.code==='transaction_failed'?'failed':'uncertain',kind,hash,error?.code||'confirmation_timeout');throw error}
4155
- }
4156
- await api('/v1/mycomesh/local/history/sync',{method:'POST'});await load();toast('申诉交易已确认并进入链上裁决');
4157
- }
4158
- async function activateCurrent(){const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'register_key',wallet})});await sendPlan(plan);for(let count=0;count<10;count++){try{await api('/v1/mycomesh/local/wallet/activate',{method:'POST'});await load();toast('Key 已激活');return}catch(error){if(count===9)throw error;await new Promise(resolve=>setTimeout(resolve,1600))}}}
4159
- async function rotateKey(){const oldAddress=state.key.address;await api('/v1/mycomesh/local/key/prepare',{method:'POST'});const register=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'register_key',wallet})});await sendPlan(register);for(let count=0;count<10;count++){try{await api('/v1/mycomesh/local/key/activate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({wallet})});break}catch(error){if(count===9)throw error;await new Promise(resolve=>setTimeout(resolve,1600))}}const revoke=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'revoke_key',wallet,key_address:oldAddress})});await sendPlan(revoke);await load();toast(state.protocol_version===10?'新 Key 已启用;旧通道预算到期后才能释放,新 Key 需另开预算':'新 Key 已启用,旧 Key 已撤销')}
4160
- document.querySelectorAll('.tab').forEach(tab=>tab.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(item=>item.classList.toggle('active',item===tab));document.querySelectorAll('.view').forEach(view=>view.hidden=view.id!=='view-'+tab.dataset.view);if(tab.dataset.view==='activity'&&managementToken&&!busy)load().catch(error=>toast(error.message,true))}));
4161
- setInterval(()=>{if(managementToken&&!busy&&!document.hidden)load().catch(()=>{})},15000);
4162
- document.addEventListener('visibilitychange',()=>{if(!document.hidden&&managementToken&&!busy)load().catch(()=>{})});
4163
- document.addEventListener('click',event=>{const button=event.target.closest('.copy');if(button)navigator.clipboard.writeText($(button.dataset.copy).textContent).then(()=>toast('已复制')).catch(()=>toast('复制失败',true))});
4164
- $('setupAccess').onclick=()=>run(activateCurrent);$('login').onclick=()=>run(login);$('activate').onclick=()=>run(activateCurrent);$('rotate').onclick=()=>run(rotateKey);$('refresh').onclick=()=>run(load);$('historyRecoveryButton').onclick=()=>run(syncHistoryNow);
4165
- function readTopupAmount(){const value=$('amount').value.trim();if(!/^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/.test(value)||Number(value)<=0)throw new Error('请输入大于 0 且最多 6 位小数的 USDC 金额');return value}
4166
- $('topup').onclick=()=>run(async()=>{const amount=readTopupAmount();const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'top_up',wallet,amount_usdc:amount})});await sendPlan(plan);$('amount').value='';await load();toast('充值已确认')});
4167
- $('shareStart').onclick=()=>run(async()=>{const value=await api('/v1/mycomesh/local/share/start',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({minutes:Number($('shareMinutes').value)})});state.share=value.share;renderShare();toast('临时分享已开启')});
4168
- $('shareStop').onclick=()=>run(async()=>{const value=await api('/v1/mycomesh/local/share/stop',{method:'POST'});state.share=value.share;renderShare();toast('临时分享已停止')});
4169
- $('walletButton').onclick=()=>run(async()=>{await api('/v1/mycomesh/local/wallet/lock',{method:'POST'});managementToken=null;wallet=null;await load()});
4170
- walletProvider()?.on?.('accountsChanged',()=>{if(managementToken)api('/v1/mycomesh/local/wallet/lock',{method:'POST'}).catch(()=>{}).finally(()=>{managementToken=null;wallet=null;load().catch(error=>toast(error.message,true))})});
4171
- load().catch(error=>{$('loginError').hidden=false;$('loginError').textContent=error.message});
4172
- loadModels();
4173
- </script></body></html>`;
4174
- }
4175
-
4176
- export { authorizationStructHash, receiptStructHash, verifyAuthorization, verifySignedReceipt };