mppx 0.4.0 → 0.4.2

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +260 -0
  2. package/dist/bin.js +2 -2
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli/account.d.ts +53 -0
  5. package/dist/cli/account.d.ts.map +1 -0
  6. package/dist/cli/account.js +156 -0
  7. package/dist/cli/account.js.map +1 -0
  8. package/dist/{cli.d.ts → cli/cli.d.ts} +4 -3
  9. package/dist/cli/cli.d.ts.map +1 -0
  10. package/dist/cli/cli.js +852 -0
  11. package/dist/cli/cli.js.map +1 -0
  12. package/dist/cli/config.d.ts +39 -0
  13. package/dist/cli/config.d.ts.map +1 -0
  14. package/dist/cli/config.js +30 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/internal.d.ts +16 -0
  17. package/dist/cli/internal.d.ts.map +1 -0
  18. package/dist/cli/internal.js +58 -0
  19. package/dist/cli/internal.js.map +1 -0
  20. package/dist/cli/plugins/index.d.ts +4 -0
  21. package/dist/cli/plugins/index.d.ts.map +1 -0
  22. package/dist/cli/plugins/index.js +4 -0
  23. package/dist/cli/plugins/index.js.map +1 -0
  24. package/dist/cli/plugins/plugin.d.ts +68 -0
  25. package/dist/cli/plugins/plugin.d.ts.map +1 -0
  26. package/dist/cli/plugins/plugin.js +4 -0
  27. package/dist/cli/plugins/plugin.js.map +1 -0
  28. package/dist/cli/plugins/stripe.d.ts +2 -0
  29. package/dist/cli/plugins/stripe.d.ts.map +1 -0
  30. package/dist/cli/plugins/stripe.js +118 -0
  31. package/dist/cli/plugins/stripe.js.map +1 -0
  32. package/dist/cli/plugins/tempo.d.ts +11 -0
  33. package/dist/cli/plugins/tempo.d.ts.map +1 -0
  34. package/dist/cli/plugins/tempo.js +706 -0
  35. package/dist/cli/plugins/tempo.js.map +1 -0
  36. package/dist/cli/utils.d.ts +93 -0
  37. package/dist/cli/utils.d.ts.map +1 -0
  38. package/dist/cli/utils.js +274 -0
  39. package/dist/cli/utils.js.map +1 -0
  40. package/dist/tempo/client/Methods.d.ts +1 -1
  41. package/dist/tempo/client/Session.d.ts +2 -2
  42. package/package.json +13 -2
  43. package/src/bin.ts +2 -2
  44. package/src/cli/account.ts +157 -0
  45. package/src/{cli.test.ts → cli/cli.test.ts} +107 -51
  46. package/src/cli/cli.ts +907 -0
  47. package/src/cli/config.test.ts +82 -0
  48. package/src/cli/config.ts +44 -0
  49. package/src/cli/internal.ts +72 -0
  50. package/src/cli/plugins/index.ts +3 -0
  51. package/src/cli/plugins/plugin.ts +73 -0
  52. package/src/cli/plugins/stripe.ts +143 -0
  53. package/src/cli/plugins/tempo.ts +842 -0
  54. package/src/cli/utils.ts +336 -0
  55. package/dist/cli.d.ts.map +0 -1
  56. package/dist/cli.js +0 -1992
  57. package/dist/cli.js.map +0 -1
  58. package/src/cli.ts +0 -2178
@@ -0,0 +1,706 @@
1
+ import * as child from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { Errors, z } from 'incur';
7
+ import { Base64 } from 'ox';
8
+ import { createClient, http } from 'viem';
9
+ import { privateKeyToAccount } from 'viem/accounts';
10
+ import * as Credential from '../../Credential.js';
11
+ import { tempo as tempoMethods } from '../../tempo/client/index.js';
12
+ import { signVoucher } from '../../tempo/session/Voucher.js';
13
+ import { createDefaultStore, createKeychain, resolveAccountName } from '../account.js';
14
+ import { fetchTokenInfo, fmtBalance, isTempoAccount, isTestnet, link, pc, resolveChain, } from '../utils.js';
15
+ import { createPlugin } from './plugin.js';
16
+ const packageJson = createRequire(import.meta.url)('../../../package.json');
17
+ export function tempo() {
18
+ let _session;
19
+ return createPlugin({
20
+ method: 'tempo',
21
+ async setup({ challenge, options, methodOpts }) {
22
+ const accountName = resolveAccountName(options.account);
23
+ const challengeRequest = challenge.request;
24
+ const currency = challengeRequest.currency;
25
+ let tokenSymbol = currency ?? '';
26
+ let tokenDecimals = challengeRequest.decimals ?? 6;
27
+ let explorerUrl;
28
+ let account;
29
+ let client;
30
+ let useTempoCliSign = false;
31
+ const privateKey = process.env.MPPX_PRIVATE_KEY?.trim() ||
32
+ (isTempoAccount(accountName) ? undefined : await createKeychain(accountName).get());
33
+ if (!privateKey && isTempoAccount(accountName) && hasTempoCliSync()) {
34
+ useTempoCliSign = true;
35
+ const tempoEntry = resolveTempoAccount(accountName);
36
+ if (tempoEntry) {
37
+ const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
38
+ client = createClient({
39
+ chain: await resolveChain({ rpcUrl }),
40
+ transport: http(rpcUrl),
41
+ });
42
+ explorerUrl = client.chain?.blockExplorers?.default?.url;
43
+ const tokenInfo = currency
44
+ ? await fetchTokenInfo(client, currency, tempoEntry.wallet_address).catch(() => undefined)
45
+ : undefined;
46
+ tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
47
+ tokenDecimals =
48
+ tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
49
+ }
50
+ }
51
+ else if (!privateKey) {
52
+ const fallback = fallbackFromTempo();
53
+ if (fallback) {
54
+ const fallbackKey = await createKeychain(fallback).get();
55
+ if (fallbackKey)
56
+ account = privateKeyToAccount(fallbackKey);
57
+ }
58
+ if (!account) {
59
+ if (options.account)
60
+ throw new Errors.IncurError({
61
+ code: 'ACCOUNT_NOT_FOUND',
62
+ message: `Account "${accountName}" not found.`,
63
+ exitCode: 69,
64
+ });
65
+ else
66
+ throw new Errors.IncurError({
67
+ code: 'ACCOUNT_NOT_FOUND',
68
+ message: 'No account found.',
69
+ exitCode: 69,
70
+ });
71
+ }
72
+ }
73
+ else
74
+ account = privateKeyToAccount(privateKey);
75
+ if (!useTempoCliSign && account) {
76
+ const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
77
+ client = createClient({
78
+ chain: await resolveChain({ rpcUrl }),
79
+ transport: http(rpcUrl),
80
+ });
81
+ explorerUrl = client.chain?.blockExplorers?.default?.url;
82
+ const tokenInfo = currency
83
+ ? await fetchTokenInfo(client, currency, account.address).catch(() => undefined)
84
+ : undefined;
85
+ tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
86
+ tokenDecimals =
87
+ tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
88
+ }
89
+ if (useTempoCliSign)
90
+ return {
91
+ tokenSymbol,
92
+ tokenDecimals,
93
+ explorerUrl,
94
+ methods: [],
95
+ async createCredential(response) {
96
+ const wwwAuth = response.headers.get('www-authenticate');
97
+ if (!wwwAuth)
98
+ throw new Error('No WWW-Authenticate header in 402 response.');
99
+ return tempoCliSign(wwwAuth);
100
+ },
101
+ };
102
+ if (!account || !client)
103
+ throw new Errors.IncurError({
104
+ code: 'ACCOUNT_NOT_FOUND',
105
+ message: 'Tempo requires a configured account.',
106
+ exitCode: 69,
107
+ });
108
+ const tempoOpts = parseOptions(z.object({
109
+ channel: z.optional(z.coerce.string()),
110
+ deposit: z.optional(z.union([z.string(), z.number()])),
111
+ }), methodOpts);
112
+ const methods = tempoMethods({
113
+ account,
114
+ getClient: () => client,
115
+ deposit: (() => {
116
+ if (challenge.intent !== 'session')
117
+ return undefined;
118
+ const suggestedDeposit = challenge.request
119
+ .suggestedDeposit;
120
+ const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
121
+ const resolved = suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain) ? '10' : undefined);
122
+ if (!resolved) {
123
+ throw new Errors.IncurError({
124
+ code: 'MISSING_DEPOSIT',
125
+ message: 'Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.',
126
+ exitCode: 2,
127
+ });
128
+ }
129
+ return resolved;
130
+ })(),
131
+ });
132
+ const credentialContext = (() => {
133
+ if (!tempoOpts.channel)
134
+ return undefined;
135
+ const channelId = tempoOpts.channel;
136
+ const saved = readChannelCumulative(channelId);
137
+ return {
138
+ channelId,
139
+ ...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
140
+ };
141
+ })();
142
+ const chainId = client.chain.id;
143
+ // Store session support for use in lifecycle hooks
144
+ _session = {
145
+ async signVoucher({ channelId, cumulativeAmount, escrowContract, chainId }) {
146
+ return Credential.serialize({
147
+ challenge,
148
+ payload: {
149
+ action: 'voucher',
150
+ channelId,
151
+ cumulativeAmount: cumulativeAmount.toString(),
152
+ signature: await signVoucher(client, account, { channelId: channelId, cumulativeAmount }, escrowContract, chainId),
153
+ },
154
+ source: `did:pkh:eip155:${chainId}:${account.address}`,
155
+ });
156
+ },
157
+ source: `did:pkh:eip155:${chainId}:${account.address}`,
158
+ };
159
+ return {
160
+ tokenSymbol,
161
+ tokenDecimals,
162
+ explorerUrl,
163
+ methods: [...methods],
164
+ credentialContext,
165
+ };
166
+ },
167
+ prepareCredentialRequest({ challenge, headers }) {
168
+ if (challenge.intent === 'session')
169
+ headers.Accept = 'text/event-stream';
170
+ },
171
+ async handleResponse(ctx) {
172
+ if (ctx.challenge.intent !== 'session')
173
+ return false;
174
+ if (!_session)
175
+ return false;
176
+ const { challenge, credential, response, fetchUrl, fetchInit, verbose } = ctx;
177
+ const { silent, confirmEnabled, tokenSymbol, tokenDecimals, explorerUrl, shownKeys } = ctx;
178
+ const info = silent ? (_msg) => { } : (msg) => process.stderr.write(msg);
179
+ const parsed = Credential.deserialize(credential);
180
+ const challengeRequest = challenge.request;
181
+ const sessionMd = challengeRequest.methodDetails;
182
+ const channelId = parsed.payload.channelId;
183
+ const escrowContract = sessionMd?.escrowContract;
184
+ const chainId = sessionMd?.chainId ?? 0;
185
+ let cumulativeAmount = 'cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount
186
+ ? BigInt(parsed.payload.cumulativeAmount)
187
+ : 0n;
188
+ if (verbose >= 1) {
189
+ if (parsed.payload.action === 'open') {
190
+ const depositRaw = challengeRequest.suggestedDeposit;
191
+ const depositDisplay = depositRaw
192
+ ? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
193
+ : '';
194
+ const prefix = confirmEnabled ? '' : '\n';
195
+ info(`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
196
+ }
197
+ else {
198
+ const prefix = confirmEnabled ? '' : '\n';
199
+ info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`);
200
+ }
201
+ }
202
+ // Handle non-SSE session response (server returned non-streaming)
203
+ let credentialResponse = response;
204
+ if (credentialResponse.ok &&
205
+ !credentialResponse.headers.get('Content-Type')?.includes('text/event-stream')) {
206
+ if (parsed.payload.action === 'open' && 'cumulativeAmount' in parsed.payload) {
207
+ const tickAmount = BigInt(challengeRequest.amount);
208
+ cumulativeAmount = BigInt(parsed.payload.cumulativeAmount) + tickAmount;
209
+ if (escrowContract) {
210
+ const voucherCred = await _session.signVoucher({
211
+ channelId,
212
+ cumulativeAmount,
213
+ escrowContract,
214
+ chainId,
215
+ });
216
+ credentialResponse = await globalThis.fetch(fetchUrl, {
217
+ ...fetchInit,
218
+ headers: {
219
+ ...fetchInit.headers,
220
+ Accept: 'text/event-stream',
221
+ Authorization: voucherCred,
222
+ },
223
+ });
224
+ }
225
+ }
226
+ }
227
+ // Print receipt from initial response headers
228
+ const receiptHeader = credentialResponse.headers.get('Payment-Receipt');
229
+ if (receiptHeader) {
230
+ try {
231
+ const receiptJson = JSON.parse(Base64.toString(receiptHeader));
232
+ if (typeof receiptJson.acceptedCumulative === 'string' &&
233
+ receiptJson.acceptedCumulative) {
234
+ cumulativeAmount = BigInt(receiptJson.acceptedCumulative);
235
+ writeChannelCumulative(channelId, cumulativeAmount);
236
+ }
237
+ if (verbose >= 1)
238
+ printReceipt(receiptJson, {
239
+ info,
240
+ shownKeys,
241
+ tokenSymbol,
242
+ tokenDecimals,
243
+ explorerUrl,
244
+ handler: this,
245
+ prefix: '\n',
246
+ });
247
+ }
248
+ catch { }
249
+ }
250
+ const contentType = credentialResponse.headers.get('Content-Type') ?? '';
251
+ if (contentType.includes('text/event-stream')) {
252
+ await handleSseStream(credentialResponse, {
253
+ challenge,
254
+ channelId,
255
+ escrowContract,
256
+ chainId,
257
+ cumulativeAmount,
258
+ fetchUrl,
259
+ fetchInit,
260
+ session: _session,
261
+ info,
262
+ verbose,
263
+ shownKeys,
264
+ tokenSymbol,
265
+ tokenDecimals,
266
+ explorerUrl,
267
+ handler: this,
268
+ });
269
+ }
270
+ else {
271
+ // Non-SSE: print body, then close channel
272
+ const body = (await credentialResponse.text()).replace(/\n+$/, '');
273
+ console.log(body);
274
+ if (channelId && escrowContract && chainId) {
275
+ if (confirmEnabled)
276
+ info('\n');
277
+ if (confirmEnabled && !(await ctx.confirm('Close channel?', true))) {
278
+ if (verbose >= 1)
279
+ info(`${pc.dim('Kept channel open.')}\n`);
280
+ }
281
+ else {
282
+ await closeChannel({
283
+ channelId,
284
+ cumulativeAmount,
285
+ escrowContract,
286
+ chainId,
287
+ fetchUrl,
288
+ fetchInit,
289
+ session: _session,
290
+ info,
291
+ verbose,
292
+ tokenSymbol,
293
+ tokenDecimals,
294
+ explorerUrl,
295
+ confirmEnabled,
296
+ });
297
+ }
298
+ }
299
+ }
300
+ return true;
301
+ },
302
+ formatReceiptField(key, value) {
303
+ if ((key === 'reference' || key === 'txHash') &&
304
+ typeof value === 'string' &&
305
+ value.startsWith('0x'))
306
+ return undefined; // let default explorer link handling apply
307
+ },
308
+ });
309
+ }
310
+ // --- Session helpers ---
311
+ function printReceipt(receiptJson, opts) {
312
+ opts.info(`${opts.prefix ?? ''}${pc.bold(pc.green('Payment Receipt'))}\n`);
313
+ const rows = [];
314
+ const skipRef = receiptJson.channelId &&
315
+ receiptJson.reference &&
316
+ receiptJson.channelId === receiptJson.reference;
317
+ const receiptBalanceKeys = new Set(['acceptedCumulative', 'spent']);
318
+ for (const [key, value] of Object.entries(receiptJson)) {
319
+ if (value === undefined || opts.shownKeys.has(key))
320
+ continue;
321
+ if (key === 'reference' && skipRef)
322
+ continue;
323
+ const formatted = opts.handler.formatReceiptField?.(key, value);
324
+ if (formatted !== undefined) {
325
+ rows.push([key, formatted]);
326
+ }
327
+ else if (receiptBalanceKeys.has(key) && typeof value === 'string') {
328
+ rows.push([
329
+ key,
330
+ `${value} ${pc.dim(`(${fmtBalance(BigInt(value), opts.tokenSymbol, opts.tokenDecimals)})`)}`,
331
+ ]);
332
+ }
333
+ else if ((key === 'reference' || key === 'txHash') &&
334
+ typeof value === 'string' &&
335
+ opts.explorerUrl) {
336
+ rows.push([key, link(`${opts.explorerUrl}/tx/${value}`, value)]);
337
+ }
338
+ else
339
+ rows.push([key, String(value)]);
340
+ }
341
+ rows.sort(([a], [b]) => a.localeCompare(b));
342
+ const pad = Math.max(...rows.map(([k]) => k.length));
343
+ for (const [label, value] of rows)
344
+ opts.info(` ${pc.dim(label.padEnd(pad))} ${value}\n`);
345
+ if (opts.prefix)
346
+ opts.info('\n');
347
+ }
348
+ async function handleSseStream(response, opts) {
349
+ let cumulativeAmount = opts.cumulativeAmount;
350
+ const reader = response.body?.getReader();
351
+ if (!reader)
352
+ throw new Error('No response body');
353
+ const decoder = new TextDecoder();
354
+ let buffer = '';
355
+ let currentEvent = '';
356
+ const termBg = opts.verbose ? await detectTerminalBg() : undefined;
357
+ const chunkBgs = (() => {
358
+ if (!termBg || !pc.isColorSupported)
359
+ return undefined;
360
+ const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
361
+ const isDark = 0.299 * termBg.r + 0.587 * termBg.g + 0.114 * termBg.b < 128;
362
+ const offset = isDark ? 1 : -1;
363
+ const bgRgb = (d) => (s) => {
364
+ const r = clamp(termBg.r + d * offset);
365
+ const g = clamp(termBg.g + d * offset);
366
+ const b = clamp(termBg.b + d * offset);
367
+ return `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
368
+ };
369
+ return [bgRgb(12), bgRgb(24)];
370
+ })();
371
+ let chunkIdx = 0;
372
+ const writeContent = (chunk) => {
373
+ if (chunkBgs) {
374
+ const bgFn = chunkBgs[chunkIdx % chunkBgs.length];
375
+ process.stdout.write(chunk.replace(/[^\n]+/g, (m) => bgFn(m)));
376
+ chunkIdx++;
377
+ }
378
+ else {
379
+ process.stdout.write(chunk);
380
+ }
381
+ };
382
+ const processLines = async (lines) => {
383
+ for (const line of lines) {
384
+ if (line.startsWith('event: ')) {
385
+ currentEvent = line.slice(7).trim();
386
+ continue;
387
+ }
388
+ if (!line.startsWith('data: ')) {
389
+ if (line === '')
390
+ currentEvent = '';
391
+ continue;
392
+ }
393
+ const data = line.slice(6);
394
+ if (data.trim() === '[DONE]')
395
+ continue;
396
+ if (currentEvent === 'payment-need-voucher' &&
397
+ opts.channelId &&
398
+ opts.escrowContract &&
399
+ opts.chainId) {
400
+ try {
401
+ const event = JSON.parse(data);
402
+ const required = BigInt(event.requiredCumulative);
403
+ cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required;
404
+ const voucherCred = await opts.session.signVoucher({
405
+ channelId: opts.channelId,
406
+ cumulativeAmount,
407
+ escrowContract: opts.escrowContract,
408
+ chainId: opts.chainId,
409
+ });
410
+ await globalThis.fetch(opts.fetchUrl, {
411
+ method: 'POST',
412
+ headers: { Authorization: voucherCred },
413
+ });
414
+ }
415
+ catch (e) {
416
+ opts.info(pc.dim(pc.yellow(` [voucher failed: ${e instanceof Error ? e.message : e}]`)));
417
+ }
418
+ currentEvent = '';
419
+ continue;
420
+ }
421
+ if (currentEvent === 'payment-receipt') {
422
+ if (opts.verbose >= 1) {
423
+ try {
424
+ const receipt = JSON.parse(data);
425
+ printReceipt(receipt, {
426
+ info: opts.info,
427
+ shownKeys: opts.shownKeys,
428
+ tokenSymbol: opts.tokenSymbol,
429
+ tokenDecimals: opts.tokenDecimals,
430
+ explorerUrl: opts.explorerUrl,
431
+ handler: opts.handler,
432
+ prefix: '\n\n',
433
+ });
434
+ }
435
+ catch { }
436
+ }
437
+ currentEvent = '';
438
+ continue;
439
+ }
440
+ if (data.length === 0) {
441
+ writeContent('\n');
442
+ }
443
+ else {
444
+ try {
445
+ const parsed = JSON.parse(data);
446
+ writeContent(parsed.token ?? parsed.choices?.[0]?.delta?.content ?? data);
447
+ }
448
+ catch {
449
+ writeContent(data);
450
+ }
451
+ }
452
+ currentEvent = '';
453
+ }
454
+ };
455
+ while (true) {
456
+ const { done, value } = await reader.read();
457
+ if (done)
458
+ break;
459
+ buffer += decoder.decode(value, { stream: true });
460
+ const lines = buffer.split('\n');
461
+ buffer = lines.pop();
462
+ await processLines(lines);
463
+ }
464
+ if (buffer.trim())
465
+ await processLines([buffer]);
466
+ // Close channel after SSE stream ends
467
+ if (opts.channelId && opts.escrowContract && opts.chainId) {
468
+ await closeChannel({
469
+ channelId: opts.channelId,
470
+ cumulativeAmount,
471
+ escrowContract: opts.escrowContract,
472
+ chainId: opts.chainId,
473
+ fetchUrl: opts.fetchUrl,
474
+ fetchInit: opts.fetchInit,
475
+ session: opts.session,
476
+ info: opts.info,
477
+ verbose: opts.verbose,
478
+ tokenSymbol: opts.tokenSymbol,
479
+ tokenDecimals: opts.tokenDecimals,
480
+ explorerUrl: opts.explorerUrl,
481
+ confirmEnabled: false,
482
+ });
483
+ }
484
+ }
485
+ async function closeChannel(opts) {
486
+ const closeCred = await opts.session.signVoucher({
487
+ channelId: opts.channelId,
488
+ cumulativeAmount: opts.cumulativeAmount,
489
+ escrowContract: opts.escrowContract,
490
+ chainId: opts.chainId,
491
+ });
492
+ const closeRes = await globalThis.fetch(opts.fetchUrl, {
493
+ ...opts.fetchInit,
494
+ headers: {
495
+ ...opts.fetchInit.headers,
496
+ Authorization: closeCred,
497
+ },
498
+ });
499
+ if (closeRes.ok) {
500
+ deleteChannelState(opts.channelId);
501
+ if (opts.verbose >= 1) {
502
+ const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
503
+ let closeTxHash;
504
+ if (closeReceiptHeader) {
505
+ try {
506
+ const r = JSON.parse(Base64.toString(closeReceiptHeader));
507
+ if (typeof r.txHash === 'string')
508
+ closeTxHash = r.txHash;
509
+ }
510
+ catch { }
511
+ }
512
+ const txInfo = closeTxHash && opts.explorerUrl
513
+ ? ` ${pc.dim(link(`${opts.explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
514
+ : '';
515
+ opts.info(`${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(opts.cumulativeAmount, opts.tokenSymbol, opts.tokenDecimals)}.`)}${txInfo}\n`);
516
+ }
517
+ }
518
+ else {
519
+ const closeBody = await closeRes.text().catch(() => '');
520
+ opts.info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
521
+ opts.info(`${pc.dim(` channelId: ${opts.channelId}`)}\n` +
522
+ `${pc.dim(` cumulativeAmount: ${opts.cumulativeAmount}`)}\n` +
523
+ `${pc.dim(` escrowContract: ${opts.escrowContract}`)}\n` +
524
+ `${pc.dim(` chainId: ${opts.chainId}`)}\n` +
525
+ `${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`);
526
+ }
527
+ }
528
+ function detectTerminalBg(timeoutMs = 100) {
529
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
530
+ return Promise.resolve(undefined);
531
+ return new Promise((resolve) => {
532
+ const wasRaw = process.stdin.isRaw;
533
+ let buf = '';
534
+ const cleanup = () => {
535
+ clearTimeout(timer);
536
+ process.stdin.removeListener('data', onData);
537
+ if (process.stdin.isTTY)
538
+ process.stdin.setRawMode(wasRaw ?? false);
539
+ process.stdin.pause();
540
+ };
541
+ const timer = setTimeout(() => {
542
+ cleanup();
543
+ resolve(undefined);
544
+ }, timeoutMs);
545
+ const onData = (data) => {
546
+ buf += data.toString();
547
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence for terminal background detection
548
+ const match = buf.match(/\x1b\]11;rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
549
+ if (!match)
550
+ return;
551
+ cleanup();
552
+ const parse = (hex) => Number.parseInt(hex.slice(0, 2), 16);
553
+ resolve({ r: parse(match[1]), g: parse(match[2]), b: parse(match[3]) });
554
+ };
555
+ process.stdin.setRawMode(true);
556
+ process.stdin.resume();
557
+ process.stdin.on('data', onData);
558
+ process.stdout.write('\x1b]11;?\x07');
559
+ });
560
+ }
561
+ // --- Account helpers ---
562
+ function parseOptions(schema, rawOptions) {
563
+ const result = schema.safeParse(rawOptions ?? {});
564
+ if (result.success)
565
+ return result.data;
566
+ const summary = result.error.issues
567
+ .map((issue) => {
568
+ const path = issue.path.length ? issue.path.join('.') : 'options';
569
+ return `${path}: ${issue.message}`;
570
+ })
571
+ .join(', ');
572
+ throw new Error(`Invalid CLI options (${summary})`);
573
+ }
574
+ function channelStateDir() {
575
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'mppx', 'channels');
576
+ }
577
+ function readChannelCumulative(channelId) {
578
+ try {
579
+ const raw = fs.readFileSync(path.join(channelStateDir(), channelId), 'utf-8').trim();
580
+ return raw ? BigInt(raw) : undefined;
581
+ }
582
+ catch {
583
+ return undefined;
584
+ }
585
+ }
586
+ function writeChannelCumulative(channelId, cumulative) {
587
+ const dir = channelStateDir();
588
+ fs.mkdirSync(dir, { recursive: true });
589
+ fs.writeFileSync(path.join(dir, channelId), cumulative.toString(), 'utf-8');
590
+ }
591
+ function deleteChannelState(channelId) {
592
+ try {
593
+ fs.unlinkSync(path.join(channelStateDir(), channelId));
594
+ }
595
+ catch { }
596
+ }
597
+ function tempoKeystorePath() {
598
+ const platform = os.platform();
599
+ if (platform === 'darwin')
600
+ return path.join(os.homedir(), 'Library', 'Application Support', 'tempo', 'wallet', 'keys.toml');
601
+ return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tempo', 'wallet', 'keys.toml');
602
+ }
603
+ export function readTempoKeystore() {
604
+ try {
605
+ const raw = fs.readFileSync(tempoKeystorePath(), 'utf-8');
606
+ const entries = [];
607
+ let current;
608
+ for (const line of raw.split('\n')) {
609
+ const trimmed = line.trim();
610
+ if (trimmed === '[[keys]]') {
611
+ if (current?.wallet_address)
612
+ entries.push(current);
613
+ current = { wallet_type: 'local', wallet_address: '', chain_id: 0 };
614
+ continue;
615
+ }
616
+ if (!current)
617
+ continue;
618
+ const m = trimmed.match(/^(\w+)\s*=\s*"?([^"]*)"?$/);
619
+ if (!m)
620
+ continue;
621
+ const [, key, value] = m;
622
+ if (key === 'wallet_type')
623
+ current.wallet_type = value;
624
+ else if (key === 'wallet_address')
625
+ current.wallet_address = value;
626
+ else if (key === 'chain_id')
627
+ current.chain_id = Number.parseInt(value, 10);
628
+ }
629
+ if (current?.wallet_address)
630
+ entries.push(current);
631
+ return entries;
632
+ }
633
+ catch {
634
+ return [];
635
+ }
636
+ }
637
+ export function resolveTempoAccount(accountName) {
638
+ const entries = readTempoKeystore();
639
+ if (entries.length === 0)
640
+ return undefined;
641
+ const suffix = accountName.slice('tempo:'.length);
642
+ if (suffix === 'default' || suffix === '')
643
+ return entries[0];
644
+ const idx = Number.parseInt(suffix, 10);
645
+ if (!Number.isNaN(idx) && idx >= 0 && idx < entries.length)
646
+ return entries[idx];
647
+ return undefined;
648
+ }
649
+ let _tempoCliAvailable;
650
+ function hasTempoCliSync() {
651
+ if (_tempoCliAvailable !== undefined)
652
+ return _tempoCliAvailable;
653
+ try {
654
+ child.execFileSync('which', ['tempo'], { stdio: 'ignore' });
655
+ _tempoCliAvailable = true;
656
+ }
657
+ catch {
658
+ _tempoCliAvailable = false;
659
+ }
660
+ return _tempoCliAvailable;
661
+ }
662
+ async function tempoCliSign(wwwAuth) {
663
+ return new Promise((resolve, reject) => {
664
+ child.execFile('tempo', ['mpp', 'sign', '--challenge', wwwAuth], (error, stdout, stderr) => {
665
+ if (error) {
666
+ const msg = stderr?.trim() || error.message;
667
+ reject(new Error(`tempo mpp sign failed: ${msg}`));
668
+ return;
669
+ }
670
+ const trimmed = stdout.trim();
671
+ if (!trimmed) {
672
+ reject(new Error('tempo mpp sign returned empty output'));
673
+ return;
674
+ }
675
+ resolve(trimmed);
676
+ });
677
+ });
678
+ }
679
+ function fallbackFromTempo() {
680
+ const store = createDefaultStore();
681
+ const currentDefault = store.get();
682
+ if (!isTempoAccount(currentDefault))
683
+ return undefined;
684
+ if (hasTempoCliSync())
685
+ return undefined;
686
+ const platform = os.platform();
687
+ if (platform === 'darwin') {
688
+ try {
689
+ const stdout = child.execFileSync('security', ['dump-keychain'], { encoding: 'utf-8' });
690
+ const mppxAccounts = [];
691
+ for (const block of stdout.split('keychain:')) {
692
+ const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/);
693
+ const accountMatch = block.match(/"acct"<blob>="([^"]*)"/);
694
+ if (serviceMatch?.[1] === packageJson.name && accountMatch?.[1])
695
+ mppxAccounts.push(accountMatch[1]);
696
+ }
697
+ if (mppxAccounts.length > 0) {
698
+ store.set(mppxAccounts[0]);
699
+ return mppxAccounts[0];
700
+ }
701
+ }
702
+ catch { }
703
+ }
704
+ return undefined;
705
+ }
706
+ //# sourceMappingURL=tempo.js.map