mycomesh-consumer 0.1.30 → 0.1.32

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.
@@ -0,0 +1,1180 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { homedir } from "node:os";
4
+ import { join, dirname } from "node:path";
5
+ import {
6
+ appendFileSync,
7
+ chmodSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+
15
+ import { secp256k1 } from "@noble/curves/secp256k1";
16
+ import { keccak_256 } from "@noble/hashes/sha3.js";
17
+ import { ProxyAgent } from "undici";
18
+
19
+ export const DEFAULT_BASE_URL = "http://127.0.0.1:8110/v1";
20
+ export const DEFAULT_RELAY_URL = "https://bridge.mycomesh.xyz";
21
+ export const DEFAULT_MAX_FEE_UNITS = 100000;
22
+ export const DEFAULT_MODEL = "mycomesh-codex-standard-v1";
23
+ export const DEFAULT_CHAIN_ID = 11155111;
24
+ export const DEFAULT_SETTLEMENT = "0x6b543a0ff6fae02172c6f205759b1b9de8a6d218";
25
+ export const DEFAULT_CHANNEL_HASH =
26
+ "0xdedf8b58276b80863f354409c963cbaddf4ca7d5b866d528ff1386d74b339104";
27
+ export const DEFAULT_PRICING_HASH =
28
+ "0x365dfdf311ab90468009d2a665803ca4321c50ab9ed0809ac2c6dc4a73ac9734";
29
+ export const DEFAULT_RELAY_PAYMENT_ADDRESS =
30
+ "0x27bd63aef83554700042685c2862da6f6a9197e8";
31
+ export const DEFAULT_RELAY_SIGNER_ADDRESS =
32
+ "0x36390747ae29f5f8ae55ddd7daace89ad57644cf";
33
+ export const DEFAULT_STABLECOIN = "0xeb487c6e778248e16361dc313e4223c20d4c23b5";
34
+ export const DEFAULT_RPC_URLS = [
35
+ "https://ethereum-sepolia-rpc.publicnode.com",
36
+ "https://sepolia.drpc.org",
37
+ "https://rpc.sepolia.ethpandaops.io",
38
+ "https://sepolia.gateway.tenderly.co",
39
+ ];
40
+
41
+ const ZERO_ADDRESS = "0x" + "0".repeat(40);
42
+ const AUTH_SCHEMA = "mycomesh.x402.myco-credit-v2";
43
+ const SIGNED_SCHEMA = "mycomesh.settlement.v8.signed.v1";
44
+ const DOMAIN_TYPE =
45
+ "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
46
+ const AUTHORIZATION_TYPE =
47
+ "PaymentAuthorization(bytes32 requestId,bytes32 requestHash,address key,address relay,address relaySigner,bytes32 channel,uint64 pricingVersion,bytes32 pricingHash,uint256 maxFee,uint64 issuedAt,uint64 deadline)";
48
+ const RECEIPT_TYPE =
49
+ "UsageReceipt(bytes32 authorizationHash,bytes32 responseHash,address provider,address providerSigner,address relay,address pool,uint256 inputTokens,uint256 outputTokens,uint256 actualFee)";
50
+ const RETRYABLE_RELAY_STATUS = new Set([408, 429, 500, 502, 503, 504]);
51
+ const MAX_AUTHORIZATION_TTL = 3600;
52
+ const MAX_BODY_BYTES = 32 * 1024 * 1024;
53
+ const AUTHORIZATION_CLOCK_SKEW_SECONDS = 300;
54
+ const RESPONSES_REQUEST_OPTION_FIELDS = new Set([
55
+ "background",
56
+ "client_metadata",
57
+ "context_management",
58
+ "conversation",
59
+ "include",
60
+ "instructions",
61
+ "metadata",
62
+ "max_tool_calls",
63
+ "moderation",
64
+ "parallel_tool_calls",
65
+ "previous_response_id",
66
+ "prompt",
67
+ "prompt_cache_key",
68
+ "prompt_cache_options",
69
+ "prompt_cache_retention",
70
+ "reasoning",
71
+ "safety_identifier",
72
+ "service_tier",
73
+ "store",
74
+ "temperature",
75
+ "text",
76
+ "tool_choice",
77
+ "tools",
78
+ "top_logprobs",
79
+ "top_p",
80
+ "truncation",
81
+ "user",
82
+ ]);
83
+ const RESPONSES_LOCAL_OPTION_FIELDS = new Set(["stream", "stream_options"]);
84
+
85
+ const DEFAULT_NETWORK = Object.freeze({
86
+ chain_id: DEFAULT_CHAIN_ID,
87
+ network_name: "Sepolia testnet",
88
+ settlement_contract: DEFAULT_SETTLEMENT,
89
+ stablecoin: DEFAULT_STABLECOIN,
90
+ stablecoin_symbol: "tUSDC",
91
+ stablecoin_decimals: 6,
92
+ rpc_urls: DEFAULT_RPC_URLS,
93
+ explorer_url: "https://sepolia.etherscan.io",
94
+ });
95
+
96
+ function bytesToHex(value) {
97
+ return Buffer.from(value).toString("hex");
98
+ }
99
+
100
+ function hexToBytes(value, label = "hex value") {
101
+ const text = String(value || "");
102
+ if (!/^0x[0-9a-fA-F]*$/.test(text) || text.length % 2 !== 0) {
103
+ throw new Error(`${label} must be hexadecimal`);
104
+ }
105
+ return Uint8Array.from(Buffer.from(text.slice(2), "hex"));
106
+ }
107
+
108
+ function normalizeBytes32(value, label = "bytes32") {
109
+ const text = String(value || "").toLowerCase();
110
+ if (!/^0x[0-9a-f]{64}$/.test(text)) {
111
+ throw new Error(`${label} must be a 32-byte 0x-prefixed hex value`);
112
+ }
113
+ return text;
114
+ }
115
+
116
+ export function normalizeAddress(value, label = "address") {
117
+ const text = String(value || "").toLowerCase();
118
+ if (!/^0x[0-9a-f]{40}$/.test(text)) {
119
+ throw new Error(`${label} must be a 20-byte 0x-prefixed hex value`);
120
+ }
121
+ return text;
122
+ }
123
+
124
+ function nonzeroAddress(value, label) {
125
+ const address = normalizeAddress(value, label);
126
+ if (address === ZERO_ADDRESS) throw new Error(`${label} cannot be zero`);
127
+ return address;
128
+ }
129
+
130
+ function positiveBigInt(value, label) {
131
+ if (typeof value === "boolean" || value === undefined || value === null || value === "") {
132
+ throw new Error(`${label} must be an integer`);
133
+ }
134
+ let parsed;
135
+ try {
136
+ parsed = BigInt(value);
137
+ } catch {
138
+ throw new Error(`${label} must be an integer`);
139
+ }
140
+ if (parsed <= 0n) throw new Error(`${label} must be positive`);
141
+ return parsed;
142
+ }
143
+
144
+ function uintBigInt(value, label) {
145
+ if (typeof value === "boolean" || value === undefined || value === null || value === "") {
146
+ throw new Error(`${label} must be an integer`);
147
+ }
148
+ let parsed;
149
+ try {
150
+ parsed = BigInt(value);
151
+ } catch {
152
+ throw new Error(`${label} must be an integer`);
153
+ }
154
+ if (parsed < 0n) throw new Error(`${label} cannot be negative`);
155
+ return parsed;
156
+ }
157
+
158
+ function jsonInteger(value) {
159
+ const parsed = typeof value === "bigint" ? value : BigInt(value);
160
+ return parsed <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(parsed) : parsed.toString();
161
+ }
162
+
163
+ function base64Url(value) {
164
+ return Buffer.from(value).toString("base64url");
165
+ }
166
+
167
+ function decodeBase64Url(value) {
168
+ return Uint8Array.from(Buffer.from(String(value), "base64url"));
169
+ }
170
+
171
+ function stableStringify(value) {
172
+ if (value === null) return "null";
173
+ if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
174
+ if (typeof value === "number") {
175
+ if (!Number.isFinite(value)) throw new Error("request must contain canonical JSON data");
176
+ return JSON.stringify(value);
177
+ }
178
+ if (typeof value === "bigint") return JSON.stringify(value.toString());
179
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
180
+ if (typeof value === "object") {
181
+ return `{${Object.keys(value)
182
+ .filter((key) => value[key] !== undefined)
183
+ .sort()
184
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
185
+ .join(",")}}`;
186
+ }
187
+ throw new Error("request must contain canonical JSON data");
188
+ }
189
+
190
+ function abiWord(value) {
191
+ if (typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value)) {
192
+ return Buffer.concat([Buffer.alloc(12), Buffer.from(value.slice(2), "hex")]);
193
+ }
194
+ if (typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value)) {
195
+ return Buffer.from(value.slice(2), "hex");
196
+ }
197
+ const parsed = uintBigInt(value, "ABI uint");
198
+ const output = Buffer.alloc(32);
199
+ let remaining = parsed;
200
+ for (let index = 31; index >= 0; index -= 1) {
201
+ output[index] = Number(remaining & 0xffn);
202
+ remaining >>= 8n;
203
+ }
204
+ if (remaining !== 0n) throw new Error("ABI uint is too large");
205
+ return output;
206
+ }
207
+
208
+ function hashText(value) {
209
+ return Uint8Array.from(keccak_256(Buffer.from(value, "utf8")));
210
+ }
211
+
212
+ function keccakHex(value) {
213
+ return `0x${bytesToHex(keccak_256(value))}`;
214
+ }
215
+
216
+ function paymentPrivateKey(value) {
217
+ const text = String(value || "").trim();
218
+ let raw;
219
+ if (text.startsWith("myco_sk_")) {
220
+ raw = decodeBase64Url(text.slice("myco_sk_".length));
221
+ } else {
222
+ raw = hexToBytes(text.startsWith("0x") ? text : `0x${text}`, "payment key");
223
+ }
224
+ if (raw.length !== 32) throw new Error("payment key must be 32 bytes");
225
+ const scalar = BigInt(`0x${bytesToHex(raw)}`);
226
+ if (scalar <= 0n || scalar >= secp256k1.CURVE.n) {
227
+ throw new Error("payment key is outside secp256k1 range");
228
+ }
229
+ return raw;
230
+ }
231
+
232
+ export function generatePaymentKey() {
233
+ while (true) {
234
+ const raw = randomBytes(32);
235
+ try {
236
+ paymentPrivateKey(raw.toString("hex"));
237
+ return `myco_sk_${raw.toString("base64url")}`;
238
+ } catch {
239
+ // A uniformly random 32-byte scalar is almost always valid.
240
+ }
241
+ }
242
+ }
243
+
244
+ export function paymentKeyAddress(value) {
245
+ const publicKey = secp256k1.getPublicKey(paymentPrivateKey(value), false).slice(1);
246
+ return `0x${bytesToHex(keccak_256(publicKey).slice(-20))}`;
247
+ }
248
+
249
+ function signDigest(privateKeyValue, digest) {
250
+ const signature = secp256k1.sign(digest, paymentPrivateKey(privateKeyValue), {
251
+ lowS: true,
252
+ prehash: false,
253
+ });
254
+ const raw = Buffer.concat([
255
+ Buffer.from(signature.toCompactRawBytes()),
256
+ Buffer.from([27 + signature.recovery]),
257
+ ]);
258
+ return `0x${raw.toString("hex")}`;
259
+ }
260
+
261
+ function recoverAddress(digest, signatureValue) {
262
+ const raw = hexToBytes(signatureValue, "signature");
263
+ if (raw.length !== 65) throw new Error("signature must be 65 bytes");
264
+ const recovery = raw[64] >= 27 ? raw[64] - 27 : raw[64];
265
+ if (recovery < 0 || recovery > 3) throw new Error("signature recovery id is invalid");
266
+ const signature = secp256k1.Signature.fromCompact(raw.slice(0, 64)).addRecoveryBit(recovery);
267
+ const publicKey = signature.recoverPublicKey(digest).toRawBytes(false).slice(1);
268
+ return `0x${bytesToHex(keccak_256(publicKey).slice(-20))}`;
269
+ }
270
+
271
+ function typedDigest(structHash, chainId, contract) {
272
+ const domain = keccak_256(
273
+ Buffer.concat([
274
+ Buffer.from(hashText(DOMAIN_TYPE)),
275
+ Buffer.from(hashText("MycoMesh Settlement")),
276
+ Buffer.from(hashText("8")),
277
+ abiWord(chainId),
278
+ abiWord(normalizeAddress(contract)),
279
+ ]),
280
+ );
281
+ return Uint8Array.from(
282
+ keccak_256(Buffer.concat([Buffer.from([0x19, 0x01]), Buffer.from(domain), Buffer.from(hexToBytes(structHash))])),
283
+ );
284
+ }
285
+
286
+ function authorizationStructHash(authorization) {
287
+ const encoded = Buffer.concat([
288
+ Buffer.from(hashText(AUTHORIZATION_TYPE)),
289
+ abiWord(authorization.request_id),
290
+ abiWord(authorization.request_hash),
291
+ abiWord(authorization.key),
292
+ abiWord(authorization.relay),
293
+ abiWord(authorization.relay_signer),
294
+ abiWord(authorization.channel),
295
+ abiWord(authorization.pricing_version),
296
+ abiWord(authorization.pricing_hash),
297
+ abiWord(authorization.max_fee),
298
+ abiWord(authorization.issued_at),
299
+ abiWord(authorization.deadline),
300
+ ]);
301
+ return keccakHex(encoded);
302
+ }
303
+
304
+ function receiptStructHash(receipt) {
305
+ const encoded = Buffer.concat([
306
+ Buffer.from(hashText(RECEIPT_TYPE)),
307
+ abiWord(receipt.authorization_hash),
308
+ abiWord(receipt.response_hash),
309
+ abiWord(receipt.provider),
310
+ abiWord(receipt.provider_signer),
311
+ abiWord(receipt.relay),
312
+ abiWord(receipt.pool || ZERO_ADDRESS),
313
+ abiWord(receipt.input_tokens),
314
+ abiWord(receipt.output_tokens),
315
+ abiWord(receipt.actual_fee),
316
+ ]);
317
+ return keccakHex(encoded);
318
+ }
319
+
320
+ export function inferenceRequestHash({
321
+ endpoint,
322
+ model,
323
+ input,
324
+ messages,
325
+ maxOutputTokens,
326
+ options,
327
+ }) {
328
+ const normalizedEndpoint = String(endpoint || "").trim().toLowerCase();
329
+ if (!["responses", "chat"].includes(normalizedEndpoint)) {
330
+ throw new Error("inference request endpoint must be responses or chat");
331
+ }
332
+ const normalizedModel = String(model || "");
333
+ if (!normalizedModel) throw new Error("inference request model is required");
334
+ const outputLimit = positiveBigInt(maxOutputTokens, "max_output_tokens");
335
+ const requestOptions = normalizeInferenceOptions(normalizedEndpoint, options);
336
+ const envelope = {
337
+ request_hash_version: requestOptions
338
+ ? "mycomesh.inference.request.v3"
339
+ : "mycomesh.inference.request.v2",
340
+ endpoint: normalizedEndpoint,
341
+ model: normalizedModel,
342
+ [normalizedEndpoint === "chat" ? "messages" : "input"]:
343
+ normalizedEndpoint === "chat"
344
+ ? messages ?? [{ role: "user", content: String(input || "") }]
345
+ : input ?? "",
346
+ max_output_tokens: jsonInteger(outputLimit),
347
+ };
348
+ if (requestOptions) envelope.options = requestOptions;
349
+ return `0x${createHash("sha256").update(stableStringify(envelope), "utf8").digest("hex")}`;
350
+ }
351
+
352
+ function normalizeInferenceOptions(endpoint, options) {
353
+ if (options === undefined || options === null) return null;
354
+ if (typeof options !== "object" || Array.isArray(options)) {
355
+ throw new Error("inference request options must be a JSON object");
356
+ }
357
+ if (endpoint !== "responses") {
358
+ if (Object.keys(options).length) {
359
+ throw new Error("inference request options are supported only for responses");
360
+ }
361
+ return null;
362
+ }
363
+ const allowed = new Set([...RESPONSES_REQUEST_OPTION_FIELDS, ...RESPONSES_LOCAL_OPTION_FIELDS]);
364
+ const unknown = Object.keys(options).filter((key) => !allowed.has(key)).sort();
365
+ if (unknown.length) throw new Error(`unsupported Responses request options: ${unknown.join(", ")}`);
366
+ const normalized = {};
367
+ for (const key of [...RESPONSES_REQUEST_OPTION_FIELDS].sort()) {
368
+ if (Object.prototype.hasOwnProperty.call(options, key)) normalized[key] = options[key];
369
+ }
370
+ return Object.keys(normalized).length ? normalized : null;
371
+ }
372
+
373
+ export function buildAuthorization({
374
+ paymentKey,
375
+ chainId,
376
+ settlementContract,
377
+ requestId,
378
+ requestHash,
379
+ relay,
380
+ relaySigner,
381
+ channelHash,
382
+ pricingVersion,
383
+ pricingHash,
384
+ maxFee,
385
+ issuedAt = Math.floor(Date.now() / 1000),
386
+ deadline = issuedAt + 900,
387
+ }) {
388
+ const key = paymentKeyAddress(paymentKey);
389
+ const authorization = {
390
+ request_id: normalizeBytes32(requestId, "request_id"),
391
+ request_hash: normalizeBytes32(requestHash, "request_hash"),
392
+ key,
393
+ relay: nonzeroAddress(relay, "relay"),
394
+ relay_signer: nonzeroAddress(relaySigner, "relay_signer"),
395
+ channel: normalizeBytes32(channelHash, "channel"),
396
+ pricing_version: jsonInteger(positiveBigInt(pricingVersion, "pricing_version")),
397
+ pricing_hash: normalizeBytes32(pricingHash, "pricing_hash"),
398
+ max_fee: jsonInteger(positiveBigInt(maxFee, "max_fee")),
399
+ issued_at: jsonInteger(positiveBigInt(issuedAt, "issued_at")),
400
+ deadline: jsonInteger(positiveBigInt(deadline, "deadline")),
401
+ };
402
+ if (authorization.request_id === `0x${"0".repeat(64)}`) throw new Error("V8 request_id cannot be zero");
403
+ if (authorization.request_hash === `0x${"0".repeat(64)}`) throw new Error("V8 request_hash cannot be zero");
404
+ if (authorization.channel === `0x${"0".repeat(64)}`) throw new Error("V8 channel cannot be zero");
405
+ if (authorization.pricing_hash === `0x${"0".repeat(64)}`) throw new Error("V8 pricing_hash cannot be zero");
406
+ const issued = BigInt(authorization.issued_at);
407
+ const expires = BigInt(authorization.deadline);
408
+ if (expires <= issued || expires - issued > BigInt(MAX_AUTHORIZATION_TTL)) {
409
+ throw new Error("V8 authorization lifetime must be between 1 and 3600 seconds");
410
+ }
411
+ const authorizationHash = authorizationStructHash(authorization);
412
+ const digest = typedDigest(authorizationHash, chainId, settlementContract);
413
+ return {
414
+ schema: AUTH_SCHEMA,
415
+ chain_id: jsonInteger(positiveBigInt(chainId, "chain_id")),
416
+ settlement_contract: nonzeroAddress(settlementContract, "settlement_contract"),
417
+ authorization,
418
+ authorization_hash: authorizationHash,
419
+ authorization_digest: `0x${bytesToHex(digest)}`,
420
+ key_signature: signDigest(paymentKey, digest),
421
+ };
422
+ }
423
+
424
+ function verifyAuthorization(value, expected = {}) {
425
+ if (!value || value.schema !== AUTH_SCHEMA || !value.authorization) {
426
+ throw new Error("unsupported V8 payment authorization");
427
+ }
428
+ const authorization = value.authorization;
429
+ const chainId = uintBigInt(value.chain_id, "chain_id");
430
+ const contract = nonzeroAddress(value.settlement_contract, "settlement_contract");
431
+ const normalized = {
432
+ request_id: normalizeBytes32(authorization.request_id, "request_id"),
433
+ request_hash: normalizeBytes32(authorization.request_hash, "request_hash"),
434
+ key: nonzeroAddress(authorization.key, "key"),
435
+ relay: nonzeroAddress(authorization.relay, "relay"),
436
+ relay_signer: nonzeroAddress(authorization.relay_signer, "relay_signer"),
437
+ channel: normalizeBytes32(authorization.channel, "channel"),
438
+ pricing_version: jsonInteger(positiveBigInt(authorization.pricing_version, "pricing_version")),
439
+ pricing_hash: normalizeBytes32(authorization.pricing_hash, "pricing_hash"),
440
+ max_fee: jsonInteger(positiveBigInt(authorization.max_fee, "max_fee")),
441
+ issued_at: jsonInteger(positiveBigInt(authorization.issued_at, "issued_at")),
442
+ deadline: jsonInteger(positiveBigInt(authorization.deadline, "deadline")),
443
+ };
444
+ if (expected.chainId !== undefined && chainId !== BigInt(expected.chainId)) throw new Error("V8 chain_id mismatch");
445
+ if (expected.contract && contract !== normalizeAddress(expected.contract)) throw new Error("V8 settlement_contract mismatch");
446
+ if (expected.relay && normalized.relay !== normalizeAddress(expected.relay)) throw new Error("V8 relay mismatch");
447
+ if (expected.relaySigner && normalized.relay_signer !== normalizeAddress(expected.relaySigner)) throw new Error("V8 relay_signer mismatch");
448
+ if (expected.requestId && normalized.request_id !== normalizeBytes32(expected.requestId)) throw new Error("V8 request_id mismatch");
449
+ if (expected.requestHash && normalized.request_hash !== normalizeBytes32(expected.requestHash)) throw new Error("V8 request_hash mismatch");
450
+ const now = Math.floor(Date.now() / 1000);
451
+ const issued = BigInt(normalized.issued_at);
452
+ const deadline = BigInt(normalized.deadline);
453
+ if (issued > BigInt(now + 30) || deadline < BigInt(now)) throw new Error("V8 payment authorization is outside its time window");
454
+ if (deadline <= issued || deadline - issued > BigInt(MAX_AUTHORIZATION_TTL)) throw new Error("V8 payment authorization lifetime is invalid");
455
+ const structHash = authorizationStructHash(normalized);
456
+ if (normalizeBytes32(value.authorization_hash, "authorization_hash") !== structHash) throw new Error("V8 authorization hash mismatch");
457
+ const digest = typedDigest(structHash, chainId, contract);
458
+ if (normalizeBytes32(value.authorization_digest, "authorization_digest") !== `0x${bytesToHex(digest)}`) throw new Error("V8 authorization digest mismatch");
459
+ if (recoverAddress(digest, value.key_signature) !== normalized.key) throw new Error("V8 payment key signature mismatch");
460
+ return { ...value, chain_id: jsonInteger(chainId), settlement_contract: contract, authorization: normalized };
461
+ }
462
+
463
+ function verifySignedReceipt(value) {
464
+ if (!value || value.schema !== SIGNED_SCHEMA) throw new Error("unsupported signed V8 receipt");
465
+ const authorization = verifyAuthorization(value.authorization);
466
+ const receipt = value.receipt;
467
+ if (!receipt || typeof receipt !== "object") throw new Error("V8 signed receipt is missing");
468
+ const normalizedReceipt = {
469
+ authorization_hash: normalizeBytes32(receipt.authorization_hash, "authorization_hash"),
470
+ response_hash: normalizeBytes32(receipt.response_hash, "response_hash"),
471
+ provider: nonzeroAddress(receipt.provider, "provider"),
472
+ provider_signer: nonzeroAddress(receipt.provider_signer, "provider_signer"),
473
+ relay: nonzeroAddress(receipt.relay, "relay"),
474
+ pool: normalizeAddress(receipt.pool || ZERO_ADDRESS, "pool"),
475
+ input_tokens: jsonInteger(uintBigInt(receipt.input_tokens, "input_tokens")),
476
+ output_tokens: jsonInteger(uintBigInt(receipt.output_tokens, "output_tokens")),
477
+ actual_fee: jsonInteger(positiveBigInt(receipt.actual_fee, "actual_fee")),
478
+ };
479
+ if (normalizedReceipt.authorization_hash !== authorization.authorization_hash) throw new Error("V8 signed receipt authorization mismatch");
480
+ if (normalizedReceipt.relay !== authorization.authorization.relay) throw new Error("V8 signed receipt Relay payout mismatch");
481
+ if (value.key_signature !== authorization.key_signature) throw new Error("V8 signed receipt payment signature mismatch");
482
+ const digest = typedDigest(receiptStructHash(normalizedReceipt), authorization.chain_id, authorization.settlement_contract);
483
+ if (recoverAddress(digest, value.provider_signature) !== normalizedReceipt.provider_signer) throw new Error("V8 Provider signature mismatch");
484
+ if (recoverAddress(digest, value.relay_signature) !== authorization.authorization.relay_signer) throw new Error("V8 Relay signature mismatch");
485
+ return { authorization, receipt: normalizedReceipt };
486
+ }
487
+
488
+ async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
489
+ const controller = new AbortController();
490
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
491
+ timer.unref?.();
492
+ try {
493
+ return await fetch(url, { redirect: "error", ...options, signal: controller.signal });
494
+ } finally {
495
+ clearTimeout(timer);
496
+ }
497
+ }
498
+
499
+ async function readJsonResponse(response) {
500
+ const text = await response.text();
501
+ try {
502
+ return text ? JSON.parse(text) : {};
503
+ } catch {
504
+ return { error: { message: text.slice(0, 1000), type: "relay_error" } };
505
+ }
506
+ }
507
+
508
+ function openaiError(message, type = "server_error", code = type) {
509
+ return { error: { message: String(message), type, param: null, code } };
510
+ }
511
+
512
+ function parseNetworkConfig(path) {
513
+ if (!path || !existsSync(path)) return { ...DEFAULT_NETWORK };
514
+ try {
515
+ const network = JSON.parse(readFileSync(path, "utf8"));
516
+ const deploymentPath = network.deployment ? join(dirname(path), network.deployment) : path;
517
+ const deployment = network.deployment && existsSync(deploymentPath)
518
+ ? JSON.parse(readFileSync(deploymentPath, "utf8"))
519
+ : network;
520
+ if (Number(deployment.protocol_version || 0) !== 8) return { ...DEFAULT_NETWORK };
521
+ return {
522
+ chain_id: Number(deployment.chain_id),
523
+ network_name: Number(deployment.chain_id) === DEFAULT_CHAIN_ID ? "Sepolia testnet" : "EVM network",
524
+ settlement_contract: normalizeAddress(deployment.settlement),
525
+ stablecoin: normalizeAddress(deployment.stablecoin),
526
+ stablecoin_symbol: "tUSDC",
527
+ stablecoin_decimals: 6,
528
+ rpc_urls: (network.settlement_rpc_urls || [network.settlement_rpc_url]).filter(Boolean),
529
+ explorer_url: Number(deployment.chain_id) === DEFAULT_CHAIN_ID ? "https://sepolia.etherscan.io" : "",
530
+ };
531
+ } catch {
532
+ return { ...DEFAULT_NETWORK };
533
+ }
534
+ }
535
+
536
+ function defaultDataDir() {
537
+ return join(homedir(), ".mycomesh", "consumer");
538
+ }
539
+
540
+ function resolveBaseUrl(value) {
541
+ const base = String(value || DEFAULT_BASE_URL).replace(/\/+$/, "");
542
+ return base.endsWith("/v1") ? base : `${base}/v1`;
543
+ }
544
+
545
+ function rootBaseUrl(baseUrl) {
546
+ return baseUrl.replace(/\/v1\/?$/, "");
547
+ }
548
+
549
+ function parseRelayUrls(env, explicit) {
550
+ const raw = explicit || env.MYCOMESH_V8_RELAY_URLS || env.MYCOMESH_CONSUMER_RELAY_URL || DEFAULT_RELAY_URL;
551
+ const urls = String(raw).split(",").map((item) => item.trim().replace(/\/+$/, "")).filter(Boolean);
552
+ if (!urls.length) throw new Error("at least one Relay URL is required");
553
+ for (const url of urls) {
554
+ const parsed = new URL(url);
555
+ if (!/^https?:$/.test(parsed.protocol)) throw new Error("Relay URLs must use HTTP or HTTPS");
556
+ }
557
+ return urls;
558
+ }
559
+
560
+ function sameSecret(first, second) {
561
+ const a = Buffer.from(String(first || ""));
562
+ const b = Buffer.from(String(second || ""));
563
+ return a.length === b.length && timingSafeEqual(a, b);
564
+ }
565
+
566
+ export class NativeConsumerState {
567
+ constructor(options = {}) {
568
+ const env = options.env || process.env;
569
+ this.dataDir = options.dataDir || env.MYCOMESH_CONSUMER_DATA_DIR || defaultDataDir();
570
+ mkdirSync(this.dataDir, { recursive: true, mode: 0o700 });
571
+ try { chmodSync(this.dataDir, 0o700); } catch {}
572
+ this.baseUrl = resolveBaseUrl(options.baseUrl || env.MYCOMESH_CONSUMER_PUBLIC_BASE_URL || DEFAULT_BASE_URL);
573
+ this.relayUrls = parseRelayUrls(env, options.relayUrls);
574
+ this.maxFeeUnits = options.maxFeeUnits || Number(env.MYCOMESH_V8_MAX_FEE_UNITS || DEFAULT_MAX_FEE_UNITS);
575
+ if (!Number.isSafeInteger(this.maxFeeUnits) || this.maxFeeUnits <= 0) throw new Error("max fee must be a positive integer");
576
+ this.timeoutMs = options.timeoutMs ?? Number(env.MYCOMESH_V8_REQUEST_TIMEOUT_SECONDS || 300) * 1000;
577
+ this.healthTimeoutMs = options.healthTimeoutMs ?? Number(env.MYCOMESH_V8_HEALTH_TIMEOUT_SECONDS || 5) * 1000;
578
+ this.network = parseNetworkConfig(options.networkConfig || env.MYCOMESH_CONSUMER_NETWORK_CONFIG);
579
+ if (env.MYCOMESH_CONSUMER_SETTLEMENT_RPC_URLS) {
580
+ this.network.rpc_urls = String(env.MYCOMESH_CONSUMER_SETTLEMENT_RPC_URLS).split(",").map((item) => item.trim()).filter(Boolean);
581
+ }
582
+ this.proxy = options.proxy || env.MYCOMESH_CONSUMER_PROXY || "";
583
+ this.dispatcher = this.proxy ? new ProxyAgent(this.proxy) : undefined;
584
+ this.healthCache = new Map();
585
+ this.historyPath = join(this.dataDir, "receipt-history.jsonl");
586
+ this.pendingKeyPath = join(this.dataDir, "pending-payment-key");
587
+ this.paymentKeyFromEnv = Boolean(String(env.MYCOMESH_V8_PAYMENT_KEY || "").trim());
588
+ this.paymentKey = this.loadPaymentKey(env.MYCOMESH_V8_PAYMENT_KEY);
589
+ this.paymentAddress = paymentKeyAddress(this.paymentKey);
590
+ }
591
+
592
+ loadPaymentKey(configured) {
593
+ const value = String(configured || "").trim();
594
+ const path = join(this.dataDir, "payment-key");
595
+ if (value) {
596
+ paymentPrivateKey(value);
597
+ return value;
598
+ }
599
+ if (existsSync(path)) {
600
+ const stored = readFileSync(path, "utf8").trim();
601
+ paymentPrivateKey(stored);
602
+ return stored;
603
+ }
604
+ const generated = generatePaymentKey();
605
+ writeFileSync(path, `${generated}\n`, { mode: 0o600, flag: "wx" });
606
+ try { chmodSync(path, 0o600); } catch {}
607
+ return generated;
608
+ }
609
+
610
+ credentialsText() {
611
+ return `export OPENAI_BASE_URL=${shellQuote(this.baseUrl)}\nexport OPENAI_API_KEY=${shellQuote(this.paymentKey)}`;
612
+ }
613
+
614
+ healthPayload() {
615
+ return {
616
+ ok: true,
617
+ protocol: "mycomesh-consumer/v8",
618
+ runtime: "node-native",
619
+ docker: false,
620
+ browser_app_ready: true,
621
+ gateway_dependency: false,
622
+ routing_mode: "relay-scheduled-payment-key-v8",
623
+ relay_urls: this.relayUrls,
624
+ payment_key_address: this.paymentAddress,
625
+ payment_key_persisted: !this.paymentKeyFromEnv,
626
+ responses_transports: ["http", "sse"],
627
+ };
628
+ }
629
+
630
+ history(limit = 100) {
631
+ if (!existsSync(this.historyPath)) return [];
632
+ const lines = readFileSync(this.historyPath, "utf8").split("\n").filter(Boolean);
633
+ const selected = limit <= 0 ? lines : lines.slice(-Math.min(Math.max(limit, 1), 500));
634
+ return selected.map((line) => {
635
+ try { return JSON.parse(line); } catch { return null; }
636
+ }).filter((value) => value && typeof value === "object").reverse();
637
+ }
638
+
639
+ recordReceipt(relayUrl, endpoint, model, settlement) {
640
+ const signed = settlement?.signed_receipt;
641
+ const receipt = signed?.receipt;
642
+ const auth = signed?.authorization?.authorization;
643
+ if (!receipt || !auth) return;
644
+ const entry = {
645
+ timestamp: Math.floor(Date.now() / 1000),
646
+ request_id: String(auth.request_id || ""),
647
+ settlement_key: String(settlement.settlement_key || ""),
648
+ status: String(settlement.status || "queued"),
649
+ accepted: Boolean(settlement.accepted),
650
+ endpoint,
651
+ model,
652
+ relay_url: relayUrl,
653
+ provider: String(receipt.provider || ""),
654
+ input_tokens: Number(receipt.input_tokens || 0),
655
+ output_tokens: Number(receipt.output_tokens || 0),
656
+ actual_fee_units: Number(receipt.actual_fee || 0),
657
+ };
658
+ appendFileSync(this.historyPath, `${stableStringify(entry)}\n`, { mode: 0o600 });
659
+ try { chmodSync(this.historyPath, 0o600); } catch {}
660
+ }
661
+
662
+ async rpcValue(callback) {
663
+ const errors = [];
664
+ for (const rpcUrl of this.network.rpc_urls) {
665
+ try { return await callback(rpcUrl); } catch (error) { errors.push(error.message); }
666
+ }
667
+ throw new Error(`all configured Settlement V8 RPC endpoints failed: ${errors.join("; ")}`);
668
+ }
669
+
670
+ async callRpc(rpcUrl, method, params) {
671
+ const response = await fetchWithTimeout(rpcUrl, {
672
+ method: "POST",
673
+ headers: { "content-type": "application/json" },
674
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
675
+ dispatcher: this.dispatcher,
676
+ }, this.healthTimeoutMs);
677
+ const payload = await readJsonResponse(response);
678
+ if (!response.ok || payload.error) throw new Error(payload.error?.message || `RPC ${response.status}`);
679
+ return payload.result;
680
+ }
681
+
682
+ async contractCall(rpcUrl, contract, signature, args) {
683
+ const data = `0x${bytesToHex(keccak_256(Buffer.from(signature, "ascii")).slice(0, 4))}${args.map((arg) => abiWord(arg).toString("hex")).join("")}`;
684
+ return this.callRpc(rpcUrl, "eth_call", [{ to: normalizeAddress(contract), data }, "latest"]);
685
+ }
686
+
687
+ async keyGrant(address) {
688
+ const output = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.settlement_contract, "keyGrants(address)", [normalizeAddress(address)]));
689
+ const words = String(output || "").replace(/^0x/, "").match(/.{64}/g) || [];
690
+ if (words.length < 4) throw new Error("invalid key grant response");
691
+ return {
692
+ owner: `0x${words[0].slice(-40)}`,
693
+ max_per_request: Number(BigInt(`0x${words[1]}`)),
694
+ valid_until: Number(BigInt(`0x${words[2]}`)),
695
+ active: BigInt(`0x${words[3]}`) !== 0n,
696
+ };
697
+ }
698
+
699
+ async accountBalance(owner) {
700
+ const output = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.settlement_contract, "availableBalance(address)", [normalizeAddress(owner)]));
701
+ return BigInt(output || "0x0").toString();
702
+ }
703
+
704
+ async dashboardPayload(wallet) {
705
+ const allHistory = this.history(0);
706
+ const payload = {
707
+ ok: true,
708
+ protocol_version: 8,
709
+ runtime: "node-native",
710
+ credentials: { base_url: this.baseUrl, api_key: this.paymentKey, export: this.credentialsText() },
711
+ key: { address: this.paymentAddress, max_fee_units: this.maxFeeUnits, pending: this.pendingPaymentKey() },
712
+ settlement: this.network,
713
+ history: allHistory.slice(0, 100),
714
+ usage: {
715
+ request_count: allHistory.length,
716
+ total_spent_units: allHistory.reduce((total, item) => total + Number(item.actual_fee_units || 0), 0),
717
+ input_tokens: allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0),
718
+ output_tokens: allHistory.reduce((total, item) => total + Number(item.output_tokens || 0), 0),
719
+ },
720
+ };
721
+ try {
722
+ const grant = await this.keyGrant(this.paymentAddress);
723
+ payload.key.grant = grant;
724
+ if (grant.owner !== ZERO_ADDRESS) {
725
+ payload.account = { owner: grant.owner, available_balance_units: await this.accountBalance(grant.owner) };
726
+ }
727
+ } catch (error) {
728
+ payload.chain_error = error.message;
729
+ }
730
+ if (wallet) {
731
+ try {
732
+ const address = normalizeAddress(wallet);
733
+ const token = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "balanceOf(address)", [address]));
734
+ const allowance = await this.rpcValue((rpc) => this.contractCall(rpc, this.network.stablecoin, "allowance(address,address)", [address, this.network.settlement_contract]));
735
+ payload.wallet = { address, token_balance_units: BigInt(token || "0x0").toString(), allowance_units: BigInt(allowance || "0x0").toString() };
736
+ } catch (error) {
737
+ payload.wallet_error = error.message;
738
+ }
739
+ }
740
+ return payload;
741
+ }
742
+
743
+ pendingPaymentKey() {
744
+ if (!existsSync(this.pendingKeyPath)) return null;
745
+ const value = readFileSync(this.pendingKeyPath, "utf8").trim();
746
+ paymentPrivateKey(value);
747
+ return { payment_key: value, payment_key_address: paymentKeyAddress(value) };
748
+ }
749
+
750
+ preparePaymentKey() {
751
+ if (this.paymentKeyFromEnv) throw new Error("payment-key rotation is disabled while MYCOMESH_V8_PAYMENT_KEY is set");
752
+ const pending = this.pendingPaymentKey();
753
+ if (pending) return pending;
754
+ const value = generatePaymentKey();
755
+ writeFileSync(this.pendingKeyPath, `${value}\n`, { mode: 0o600, flag: "wx" });
756
+ return { payment_key: value, payment_key_address: paymentKeyAddress(value) };
757
+ }
758
+
759
+ async activatePendingPaymentKey(wallet) {
760
+ const owner = normalizeAddress(wallet);
761
+ const pending = this.pendingPaymentKey();
762
+ if (!pending) throw new Error("no pending payment key exists");
763
+ const grant = await this.keyGrant(pending.payment_key_address);
764
+ if (!grant.active || grant.owner !== owner) throw new Error("the pending payment key is not active for this wallet on-chain");
765
+ const previous = this.paymentAddress;
766
+ const destination = join(this.dataDir, "payment-key");
767
+ writeFileSync(destination, `${pending.payment_key}\n`, { mode: 0o600 });
768
+ chmodSync(destination, 0o600);
769
+ unlinkSync(this.pendingKeyPath);
770
+ this.paymentKey = pending.payment_key;
771
+ this.paymentAddress = pending.payment_key_address;
772
+ return { payment_key: this.paymentKey, payment_key_address: this.paymentAddress, previous_key_address: previous };
773
+ }
774
+
775
+ async transactionPlan(raw) {
776
+ const action = String(raw?.action || "");
777
+ const wallet = normalizeAddress(raw?.wallet, "wallet");
778
+ const settlement = this.network.settlement_contract;
779
+ const token = this.network.stablecoin;
780
+ if (action === "top_up") {
781
+ const amount = parseUsdc(raw.amount_usdc);
782
+ const allowance = BigInt(await this.rpcValue((rpc) => this.contractCall(rpc, token, "allowance(address,address)", [wallet, settlement])) || "0x0");
783
+ const transactions = [];
784
+ if (allowance < amount) transactions.push({ label: "Approve stablecoin", to: token, data: contractData("approve(address,uint256)", [settlement, (1n << 256n) - 1n]) });
785
+ transactions.push({ label: "Deposit prepaid balance", to: settlement, data: contractData("deposit(uint256)", [amount]) });
786
+ return { action, amount_units: amount.toString(), transactions };
787
+ }
788
+ if (action === "register_key") {
789
+ const pending = this.pendingPaymentKey();
790
+ const keyAddress = pending?.payment_key_address || this.paymentAddress;
791
+ return { action, key_address: keyAddress, transactions: [{ label: "Register payment key", to: settlement, data: contractData("registerKey(address,uint256,uint64)", [keyAddress, this.maxFeeUnits, 0]) }] };
792
+ }
793
+ if (action === "revoke_key") {
794
+ const keyAddress = normalizeAddress(raw.key_address, "key_address");
795
+ return { action, key_address: keyAddress, transactions: [{ label: "Revoke previous payment key", to: settlement, data: contractData("revokeKey(address)", [keyAddress]) }] };
796
+ }
797
+ throw new Error("unsupported transaction action");
798
+ }
799
+
800
+ async relayHealth(relayUrl, refresh = false) {
801
+ const cached = this.healthCache.get(relayUrl);
802
+ if (cached && !refresh && Date.now() - cached.at < 5000) return cached.payload;
803
+ const request = { dispatcher: this.dispatcher, headers: { accept: "application/json" } };
804
+ let response = await fetchWithTimeout(`${relayUrl}/health`, request, this.healthTimeoutMs);
805
+ let payload = await readJsonResponse(response);
806
+ if (!response.ok || !payload?.v8) {
807
+ response = await fetchWithTimeout(`${relayUrl}/relay/health`, request, this.healthTimeoutMs);
808
+ payload = await readJsonResponse(response);
809
+ }
810
+ if (!response.ok || payload?.ok !== true) throw new Error(`Relay health is invalid for ${relayUrl}`);
811
+ const v8 = payload.v8;
812
+ if (!v8 || v8.enabled !== true || Number(v8.providers || 0) <= 0) throw new Error(`Relay has no live Settlement V8 Provider: ${relayUrl}`);
813
+ this.healthCache.set(relayUrl, { at: Date.now(), payload });
814
+ return payload;
815
+ }
816
+
817
+ async chooseRelay(exclude = new Set()) {
818
+ const errors = [];
819
+ for (const relayUrl of this.relayUrls) {
820
+ if (exclude.has(relayUrl)) continue;
821
+ try { return { relayUrl, health: await this.relayHealth(relayUrl) }; } catch (error) { errors.push(error.message); }
822
+ }
823
+ throw new Error(`no healthy Settlement V8 Relay is available: ${errors.join("; ") || "all relays excluded"}`);
824
+ }
825
+
826
+ buildRelayPayment(path, body, health, requestId) {
827
+ const endpoint = path.endsWith("/chat/completions") ? "chat" : "responses";
828
+ const v8 = health?.v8;
829
+ if (!v8) throw new Error("Relay health has no V8 payment requirements");
830
+ const model = String(v8.model || body.model || DEFAULT_MODEL);
831
+ const maxOutput = body.max_output_tokens ?? body.max_tokens ?? v8.maxOutputTokens ?? 2000;
832
+ const options = {};
833
+ for (const field of [...RESPONSES_REQUEST_OPTION_FIELDS, ...RESPONSES_LOCAL_OPTION_FIELDS]) {
834
+ if (Object.prototype.hasOwnProperty.call(body, field)) options[field] = body[field];
835
+ }
836
+ const normalizedOptions = normalizeInferenceOptions(endpoint, options);
837
+ const requestHash = inferenceRequestHash({
838
+ endpoint,
839
+ model,
840
+ input: body.input,
841
+ messages: body.messages,
842
+ maxOutputTokens: maxOutput,
843
+ options: normalizedOptions || undefined,
844
+ });
845
+ const now = Math.floor(Date.now() / 1000);
846
+ const payment = buildAuthorization({
847
+ paymentKey: this.paymentKey,
848
+ chainId: Number(v8.chain_id),
849
+ settlementContract: v8.settlement_contract,
850
+ requestId,
851
+ requestHash,
852
+ relay: v8.relay_payment_address,
853
+ relaySigner: v8.relay_signer_address,
854
+ channelHash: v8.channel_hash,
855
+ pricingVersion: v8.pricing_version,
856
+ pricingHash: v8.pricing_hash,
857
+ maxFee: this.maxFeeUnits,
858
+ issuedAt: now - AUTHORIZATION_CLOCK_SKEW_SECONDS,
859
+ deadline: now + 900,
860
+ });
861
+ return { payment, request_id: requestId, model };
862
+ }
863
+
864
+ async relayInference(path, body) {
865
+ const requestId = `0x${bytesToHex(randomBytes(32))}`;
866
+ const used = new Set();
867
+ let lastError = "no Relay accepted the request";
868
+ let lastResponse;
869
+ while (used.size < this.relayUrls.length) {
870
+ let selected;
871
+ try { selected = await this.chooseRelay(used); } catch (error) { lastError = error.message; break; }
872
+ used.add(selected.relayUrl);
873
+ try {
874
+ const payment = this.buildRelayPayment(path, body, selected.health, requestId);
875
+ const requestBody = { ...body, model: payment.model };
876
+ const encodedPayment = base64Url(Buffer.from(stableStringify(payment.payment), "utf8"));
877
+ const response = await fetchWithTimeout(`${selected.relayUrl}${path}`, {
878
+ method: "POST",
879
+ headers: { "content-type": "application/json", accept: "application/json", "PAYMENT-SIGNATURE": encodedPayment },
880
+ body: JSON.stringify(requestBody),
881
+ dispatcher: this.dispatcher,
882
+ }, this.timeoutMs);
883
+ const payload = await readJsonResponse(response);
884
+ const retryAfter = response.headers.get("retry-after");
885
+ const headers = retryAfter ? { "Retry-After": retryAfter } : {};
886
+ if (RETRYABLE_RELAY_STATUS.has(response.status)) {
887
+ lastError = payload?.error?.message || `Relay returned HTTP ${response.status}`;
888
+ lastResponse = { payload, status: response.status, headers };
889
+ continue;
890
+ }
891
+ if (response.status >= 400) return { payload: normalizeOpenAiError(payload, "relay_error"), status: response.status, headers };
892
+ const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
893
+ if (paymentResponse) {
894
+ const settlement = decodePaymentResponse(paymentResponse);
895
+ this.recordReceipt(selected.relayUrl, path, payment.model, settlement);
896
+ headers["PAYMENT-RESPONSE"] = paymentResponse;
897
+ }
898
+ return { payload, status: 200, headers };
899
+ } catch (error) {
900
+ lastError = error.message;
901
+ }
902
+ }
903
+ if (lastResponse) return lastResponse;
904
+ return { payload: openaiError(lastError, "relay_unavailable"), status: 503, headers: { "Retry-After": "2" } };
905
+ }
906
+ }
907
+
908
+ function decodePaymentResponse(value) {
909
+ let payload;
910
+ try { payload = JSON.parse(Buffer.from(String(value), "base64url").toString("utf8")); } catch { throw new Error("Relay returned an invalid PAYMENT-RESPONSE"); }
911
+ if (!payload || typeof payload !== "object" || !payload.signed_receipt) throw new Error("Relay PAYMENT-RESPONSE is missing its signed receipt");
912
+ try { verifySignedReceipt(payload.signed_receipt); } catch (error) { throw new Error(`Relay returned an invalid signed receipt: ${error.message}`); }
913
+ return payload;
914
+ }
915
+
916
+ function normalizeOpenAiError(value, fallbackType) {
917
+ if (!value || typeof value !== "object") return openaiError(String(value || fallbackType), fallbackType);
918
+ if (!value.error || typeof value.error !== "object") return openaiError(value.detail || value.message || fallbackType, fallbackType);
919
+ 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 } };
920
+ }
921
+
922
+ function decodeRequestBody(request) {
923
+ return new Promise((resolve, reject) => {
924
+ let size = 0;
925
+ const chunks = [];
926
+ request.on("data", (chunk) => {
927
+ size += chunk.length;
928
+ if (size > MAX_BODY_BYTES) {
929
+ reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));
930
+ request.destroy();
931
+ return;
932
+ }
933
+ chunks.push(chunk);
934
+ });
935
+ request.on("end", () => {
936
+ try {
937
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
938
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("request body must be an object");
939
+ resolve(value);
940
+ } catch (error) { reject(error); }
941
+ });
942
+ request.on("error", reject);
943
+ });
944
+ }
945
+
946
+ function writeJson(response, status, value, headers = {}) {
947
+ const body = JSON.stringify(value);
948
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...headers });
949
+ response.end(body);
950
+ }
951
+
952
+ async function handleInference(state, request, response, path) {
953
+ const authorization = String(request.headers.authorization || "");
954
+ if (!sameSecret(authorization, `Bearer ${state.paymentKey}`)) {
955
+ writeJson(response, 401, openaiError("invalid MycoMesh payment key", "invalid_api_key"));
956
+ return;
957
+ }
958
+ let body;
959
+ try { body = await decodeRequestBody(request); } catch (error) {
960
+ writeJson(response, 400, openaiError(error.message, "invalid_request_error"));
961
+ return;
962
+ }
963
+ if (path.endsWith("/responses/compact") && !hasCompactionTrigger(body.input)) {
964
+ const items = Array.isArray(body.input) ? [...body.input] : [];
965
+ if (body.input !== undefined && body.input !== "" && !Array.isArray(body.input)) {
966
+ items.push(typeof body.input === "string" ? { type: "message", role: "user", content: body.input } : body.input);
967
+ }
968
+ items.push({ type: "compaction_trigger" });
969
+ body.input = items;
970
+ }
971
+ try {
972
+ const result = await state.relayInference(path, body);
973
+ if (result.status >= 400) {
974
+ writeJson(response, result.status, result.payload, result.headers);
975
+ return;
976
+ }
977
+ for (const [name, value] of Object.entries(result.headers || {})) response.setHeader(name, value);
978
+ if (body.stream === true) {
979
+ response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache", connection: "keep-alive", "x-mycomesh-streaming-mode": "buffered" });
980
+ const events = path.endsWith("/chat/completions")
981
+ ? chatCompletionSse(result.payload, body.stream_options?.include_usage === true)
982
+ : responseSse(result.payload, path.endsWith("/responses/compact") || hasCompactionTrigger(body.input));
983
+ for (const chunk of events) response.write(chunk);
984
+ response.end();
985
+ return;
986
+ }
987
+ writeJson(response, 200, result.payload, result.headers);
988
+ } catch (error) {
989
+ writeJson(response, 502, openaiError(error.message, "mycomesh_relay_error"));
990
+ }
991
+ }
992
+
993
+ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110 } = {}) {
994
+ const server = createServer(async (request, response) => {
995
+ try {
996
+ const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`);
997
+ const path = url.pathname;
998
+ if (request.method === "GET" && path === "/") {
999
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
1000
+ response.end(consumerHtml());
1001
+ return;
1002
+ }
1003
+ if (request.method === "GET" && path === "/health") { writeJson(response, 200, state.healthPayload()); return; }
1004
+ if (request.method === "GET" && path === "/ready") {
1005
+ try { const selected = await state.chooseRelay(); writeJson(response, 200, { ok: true, relay: selected.relayUrl, model: selected.health.v8.model || DEFAULT_MODEL }); }
1006
+ catch (error) { writeJson(response, 503, { ok: false, error: error.message }); }
1007
+ return;
1008
+ }
1009
+ if (request.method === "GET" && (path === "/credentials" || path === "/codex-env")) {
1010
+ response.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); response.end(`${state.credentialsText()}\n`); return;
1011
+ }
1012
+ if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
1013
+ try { const selected = await state.chooseRelay(); writeJson(response, 200, { object: "list", data: [{ id: selected.health.v8.model || DEFAULT_MODEL, object: "model", owned_by: "mycomesh", relay: selected.relayUrl }] }); }
1014
+ catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
1015
+ return;
1016
+ }
1017
+ if (request.method === "GET" && path === "/v1/mycomesh/local/dashboard") { writeJson(response, 200, await state.dashboardPayload(url.searchParams.get("wallet"))); return; }
1018
+ if (request.method === "POST" && path === "/v1/mycomesh/local/transactions") {
1019
+ try { writeJson(response, 200, await state.transactionPlan(await decodeRequestBody(request))); }
1020
+ catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
1021
+ return;
1022
+ }
1023
+ if (request.method === "POST" && path === "/v1/mycomesh/local/key/prepare") {
1024
+ if (!sameSecret(String(request.headers.authorization || ""), `Bearer ${state.paymentKey}`)) { writeJson(response, 401, { ok: false, error: "invalid local payment key" }); return; }
1025
+ try { writeJson(response, 200, state.preparePaymentKey()); } catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
1026
+ return;
1027
+ }
1028
+ if (request.method === "POST" && path === "/v1/mycomesh/local/key/activate") {
1029
+ if (!sameSecret(String(request.headers.authorization || ""), `Bearer ${state.paymentKey}`)) { writeJson(response, 401, { ok: false, error: "invalid local payment key" }); return; }
1030
+ try { const value = await decodeRequestBody(request); writeJson(response, 200, await state.activatePendingPaymentKey(value.wallet)); }
1031
+ catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
1032
+ return;
1033
+ }
1034
+ 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)) {
1035
+ const relayPath = path.endsWith("/chat/completions")
1036
+ ? "/v1/chat/completions"
1037
+ : path.endsWith("/responses/compact")
1038
+ ? "/v1/responses/compact"
1039
+ : "/v1/responses";
1040
+ await handleInference(state, request, response, relayPath); return;
1041
+ }
1042
+ writeJson(response, 404, openaiError("route not found", "invalid_request_error"));
1043
+ } catch (error) {
1044
+ if (!response.headersSent) writeJson(response, 500, openaiError(error.message)); else response.destroy();
1045
+ }
1046
+ });
1047
+ 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())) };
1048
+ }
1049
+
1050
+ function hasCompactionTrigger(value) {
1051
+ return Array.isArray(value) && value.some((item) => item && typeof item === "object" && item.type === "compaction_trigger");
1052
+ }
1053
+
1054
+ function shellQuote(value) {
1055
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
1056
+ }
1057
+
1058
+ function parseUsdc(value) {
1059
+ const text = String(value || "").trim();
1060
+ if (!/^\d+(?:\.\d{1,6})?$/.test(text)) throw new Error("enter a valid positive top-up amount");
1061
+ const [whole, fraction = ""] = text.split(".");
1062
+ const amount = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
1063
+ if (amount <= 0n) throw new Error("enter a valid positive top-up amount");
1064
+ return amount;
1065
+ }
1066
+
1067
+ function contractData(signature, args) {
1068
+ return `0x${bytesToHex(keccak_256(Buffer.from(signature, "ascii")).slice(0, 4))}${args.map((arg) => abiWord(arg).toString("hex")).join("")}`;
1069
+ }
1070
+
1071
+ function normalizedResponse(payload) {
1072
+ const response = JSON.parse(JSON.stringify(payload || {}));
1073
+ response.id = String(response.id || response.request_id || `resp_${bytesToHex(randomBytes(16))}`);
1074
+ response.object ||= "response";
1075
+ response.created_at ||= Math.floor(Date.now() / 1000);
1076
+ response.status = ["completed", "failed", "incomplete", "cancelled"].includes(String(response.status || "completed")) ? String(response.status || "completed") : "completed";
1077
+ response.output = Array.isArray(response.output) ? response.output.filter((item) => item && typeof item === "object").map(normalizedItem) : [];
1078
+ response.error ??= null;
1079
+ response.incomplete_details ??= null;
1080
+ 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("");
1081
+ return response;
1082
+ }
1083
+
1084
+ function normalizedItem(raw) {
1085
+ const item = JSON.parse(JSON.stringify(raw));
1086
+ item.type = String(item.type || "unknown");
1087
+ 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";
1088
+ item.id = String(item.id || `${prefix}_${bytesToHex(randomBytes(16))}`);
1089
+ 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"; }
1090
+ if (item.type === "reasoning") { item.summary = Array.isArray(item.summary) ? item.summary : []; item.status ||= "completed"; }
1091
+ 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"; }
1092
+ return item;
1093
+ }
1094
+
1095
+ function responseEvents(payload, compact = false) {
1096
+ const final = normalizedResponse(payload);
1097
+ let sequence = 0;
1098
+ const events = [];
1099
+ const event = (type, fields = {}) => events.push({ type, sequence_number: sequence++, ...fields });
1100
+ if (compact) { final.output.forEach((item, index) => event("response.output_item.done", { output_index: index, item })); event(terminalEvent(final), { response: final }); return events; }
1101
+ const created = { ...final, status: "in_progress", output: [], output_text: "", error: null, incomplete_details: null, usage: null };
1102
+ event("response.created", { response: created }); event("response.in_progress", { response: created });
1103
+ final.output.forEach((item, outputIndex) => {
1104
+ const added = { ...item, status: "in_progress" }; if (item.type === "message") added.content = []; if (item.type === "function_call") added.arguments = "";
1105
+ event("response.output_item.added", { output_index: outputIndex, item: added });
1106
+ if (item.type === "message") {
1107
+ (item.content || []).forEach((part, contentIndex) => {
1108
+ const partAdded = { ...part }; if (partAdded.type === "output_text") partAdded.text = "";
1109
+ event("response.content_part.added", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, part: partAdded });
1110
+ 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 || [] }); }
1111
+ 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 }); }
1112
+ event("response.content_part.done", { item_id: item.id, output_index: outputIndex, content_index: contentIndex, part });
1113
+ });
1114
+ } else if (item.type === "reasoning") {
1115
+ (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 }); });
1116
+ } else if (item.type === "function_call") {
1117
+ if (item.arguments) event("response.function_call_arguments.delta", { item_id: item.id, output_index: outputIndex, delta: item.arguments });
1118
+ event("response.function_call_arguments.done", { item_id: item.id, output_index: outputIndex, call_id: item.call_id, name: item.name, arguments: item.arguments });
1119
+ }
1120
+ event("response.output_item.done", { output_index: outputIndex, item });
1121
+ });
1122
+ event(terminalEvent(final), { response: final });
1123
+ return events;
1124
+ }
1125
+
1126
+ function terminalEvent(response) {
1127
+ return { completed: "response.completed", failed: "response.failed", incomplete: "response.incomplete", cancelled: "response.incomplete" }[response.status] || "response.completed";
1128
+ }
1129
+
1130
+ export function responseSse(payload, compact = false) {
1131
+ return responseEvents(payload, compact).map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
1132
+ }
1133
+
1134
+ export function chatCompletionSse(payload, includeUsage = false) {
1135
+ 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 = [];
1136
+ 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`);
1137
+ (Array.isArray(payload?.choices) ? payload.choices : []).forEach((choice, fallbackIndex) => { const index = Number.isInteger(choice.index) ? choice.index : fallbackIndex; const message = choice.message || {}; chunk(index, { role: String(message.role || "assistant") }); if (message.content) chunk(index, { content: String(message.content) }); chunk(index, {}, choice.finish_reason || "stop"); });
1138
+ if (includeUsage && payload?.usage) chunks.push(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage: payload.usage })}\n\n`);
1139
+ chunks.push("data: [DONE]\n\n"); return chunks;
1140
+ }
1141
+
1142
+ function consumerHtml() {
1143
+ return `<!doctype html>
1144
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1145
+ <title>MycoMesh Consumer</title>
1146
+ <style>
1147
+ body{font:15px system-ui;max-width:960px;margin:32px auto;padding:0 18px;color:#17211d;background:#f5f7f5}
1148
+ h1{margin-bottom:4px}.muted{color:#68736e}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
1149
+ .panel{background:white;border:1px solid #d9e0dc;border-radius:6px;padding:18px;margin:14px 0}.value{word-break:break-all;background:#f1f4f2;padding:10px;border-radius:4px;font:12px monospace;white-space:pre-wrap}
1150
+ button{padding:9px 12px;border:1px solid #177b57;border-radius:4px;background:#177b57;color:#fff;cursor:pointer}input{padding:9px;border:1px solid #c7d0ca;border-radius:4px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
1151
+ table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #e1e7e3;text-align:left;font-size:13px}
1152
+ </style></head><body>
1153
+ <h1>MycoMesh Consumer V8</h1><p class="muted">Native Node runtime. This page manages credentials, prepaid balance, payment keys, and local consumption history.</p>
1154
+ <div class="grid"><section class="panel"><h2>Credentials</h2><p>API URL</p><div id="url" class="value">Loading...</div><p>Payment key</p><div id="key" class="value"></div><p>Shell export</p><div id="export" class="value"></div><button id="copy" type="button">Copy export</button></section>
1155
+ <section class="panel"><h2>Usage</h2><p id="usage">Loading...</p><p id="balance"></p><p>Key address</p><div id="address" class="value"></div><div class="row"><button id="register" type="button">Register key</button><button id="rotate" type="button">Rotate key</button></div></section></div>
1156
+ <section class="panel"><h2>Prepaid top-up</h2><div class="row"><button id="connect" type="button">Connect wallet</button><input id="wallet" placeholder="Wallet address"><input id="amount" placeholder="10.00"><button id="topup" type="button">Top up</button></div><pre id="plan" class="value"></pre></section>
1157
+ <section class="panel"><h2>Consumption history</h2><table><thead><tr><th>Time</th><th>Model</th><th>Tokens</th><th>Fee</th><th>Relay</th></tr></thead><tbody id="history"></tbody></table></section>
1158
+ <script>
1159
+ let state;let wallet;const $=id=>document.getElementById(id);
1160
+ async function api(path,options={}){const r=await fetch(path,options);const v=await r.json();if(!r.ok)throw Error(v.error||'request failed');return v}
1161
+ function render(){
1162
+ $('url').textContent=state.credentials.base_url;$('key').textContent=state.credentials.api_key;$('export').textContent=state.credentials.export;$('address').textContent=state.key.address;
1163
+ $('usage').textContent=state.usage.request_count+' requests, '+state.usage.total_spent_units+' units';$('balance').textContent='Available balance: '+(state.account?.available_balance_units||'unknown');
1164
+ $('history').innerHTML=(state.history||[]).map(x=>'<tr><td>'+new Date(x.timestamp*1000).toLocaleString()+'</td><td>'+x.model+'</td><td>'+x.input_tokens+' / '+x.output_tokens+'</td><td>'+x.actual_fee_units+'</td><td>'+x.relay_url+'</td></tr>').join('')||'<tr><td colspan="5">No consumption recorded.</td></tr>';
1165
+ }
1166
+ async function load(){state=await api('/v1/mycomesh/local/dashboard');render()}
1167
+ async function connectWallet(){if(!window.ethereum)throw Error('No injected wallet found');const accounts=await window.ethereum.request({method:'eth_requestAccounts'});wallet=accounts[0];if(!wallet)throw Error('Wallet was not connected');$('wallet').value=wallet;$('connect').textContent=wallet.slice(0,6)+'...'+wallet.slice(-4)}
1168
+ async function requireWallet(){if(!wallet)await connectWallet();return wallet}
1169
+ async function waitReceipt(hash){for(let i=0;i<120;i++){const receipt=await window.ethereum.request({method:'eth_getTransactionReceipt',params:[hash]});if(receipt){if(receipt.status!=='0x1')throw Error('Chain transaction failed');return}await new Promise(resolve=>setTimeout(resolve,1500))}throw Error('Timed out waiting for chain confirmation')}
1170
+ async function sendPlan(plan){for(const transaction of plan.transactions){const hash=await window.ethereum.request({method:'eth_sendTransaction',params:[{from:wallet,to:transaction.to,data:transaction.data}]});await waitReceipt(hash)}return plan}
1171
+ async function showPlan(action,send=false){try{await requireWallet();const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action,wallet,key_address:state.key.address,amount_usdc:$('amount').value})});$('plan').textContent=JSON.stringify(plan,null,2);if(send){await sendPlan(plan);await load();$('plan').textContent='Transactions confirmed. '+JSON.stringify(plan,null,2)}}catch(error){$('plan').textContent=error.message}}
1172
+ $('copy').onclick=()=>navigator.clipboard?.writeText($('export').textContent);
1173
+ $('connect').onclick=()=>connectWallet().catch(error=>$('plan').textContent=error.message);
1174
+ $('register').onclick=()=>showPlan('register_key',true);$('topup').onclick=()=>showPlan('top_up',true);
1175
+ $('rotate').onclick=async()=>{try{await requireWallet();const oldAddress=state.key.address;const pending=await api('/v1/mycomesh/local/key/prepare',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key}});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);let activated;for(let i=0;i<8&&!activated;i++){try{activated=await api('/v1/mycomesh/local/key/activate',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key,'content-type':'application/json'},body:JSON.stringify({wallet})})}catch(error){if(i===7)throw error;await new Promise(resolve=>setTimeout(resolve,1800))}}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);$('plan').textContent=JSON.stringify({pending,activated,revoke},null,2);await load()}catch(error){$('plan').textContent=error.message}};
1176
+ load().catch(error=>$('plan').textContent=error.message);
1177
+ </script></body></html>`;
1178
+ }
1179
+
1180
+ export { authorizationStructHash, receiptStructHash, verifyAuthorization, verifySignedReceipt };