mppx 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -52
- package/dist/Challenge.d.ts +8 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +20 -4
- package/dist/Challenge.js.map +1 -1
- package/dist/cli.js +193 -66
- package/dist/cli.js.map +1 -1
- package/dist/internal/types.d.ts +10 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +2 -0
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +2 -0
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +4 -0
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -0
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts +2 -0
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +4 -3
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/NodeListener.d.ts +6 -0
- package/dist/server/NodeListener.d.ts.map +1 -1
- package/dist/server/NodeListener.js +6 -0
- package/dist/server/NodeListener.js.map +1 -1
- package/dist/server/Response.d.ts +17 -0
- package/dist/server/Response.d.ts.map +1 -1
- package/dist/server/Response.js +17 -0
- package/dist/server/Response.js.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/internal/defaults.d.ts +34 -8
- package/dist/tempo/internal/defaults.d.ts.map +1 -1
- package/dist/tempo/internal/defaults.js +30 -8
- package/dist/tempo/internal/defaults.js.map +1 -1
- package/dist/tempo/server/Charge.js +2 -2
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +8 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.test.ts +201 -11
- package/src/Challenge.ts +34 -4
- package/src/Store.test.ts +93 -0
- package/src/cli.test.ts +233 -37
- package/src/cli.ts +229 -79
- package/src/client/Transport.test.ts +4 -4
- package/src/internal/env.test.ts +42 -0
- package/src/internal/types.ts +11 -0
- package/src/proxy/internal/Headers.ts +2 -0
- package/src/proxy/internal/Route.ts +4 -0
- package/src/server/Mppx.test.ts +173 -0
- package/src/server/Mppx.ts +6 -3
- package/src/server/NodeListener.ts +6 -0
- package/src/server/Response.ts +17 -0
- package/src/server/Transport.test.ts +5 -5
- package/src/tempo/client/ChannelOps.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +94 -0
- package/src/tempo/internal/defaults.ts +41 -8
- package/src/tempo/server/Charge.test.ts +150 -0
- package/src/tempo/server/Charge.ts +2 -2
- package/src/tempo/server/Session.test.ts +241 -1
- package/src/tempo/server/Session.ts +8 -3
- package/src/tempo/server/internal/transport.test.ts +285 -0
- package/src/tempo/session/Voucher.test.ts +46 -0
package/src/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { type ZodMiniType, z } from 'zod/mini'
|
|
|
15
15
|
import * as Challenge from './Challenge.js'
|
|
16
16
|
import * as Credential from './Credential.js'
|
|
17
17
|
import * as Mppx from './client/Mppx.js'
|
|
18
|
+
import { stripe } from './stripe/client/index.js'
|
|
18
19
|
import { tempo } from './tempo/client/index.js'
|
|
19
20
|
import type { SessionCredentialPayload } from './tempo/session/Types.js'
|
|
20
21
|
import { signVoucher } from './tempo/session/Voucher.js'
|
|
@@ -41,9 +42,8 @@ cli
|
|
|
41
42
|
.option('-H, --header <header>', 'Add header (repeatable)')
|
|
42
43
|
.option('-L, --location', 'Follow redirects')
|
|
43
44
|
.option('-X, --method <method>', 'HTTP method')
|
|
44
|
-
.option('--
|
|
45
|
+
.option('-M, --method-opt <opt>', 'Method-specific option (key=value, repeatable)')
|
|
45
46
|
.option('--confirm', 'Show confirmation prompts')
|
|
46
|
-
.option('--deposit <amount>', 'Deposit amount for session payments (human-readable units)')
|
|
47
47
|
.option('--json <json>', 'Send JSON body (sets Content-Type and Accept, implies POST)')
|
|
48
48
|
.example(`${name} example.com/content`)
|
|
49
49
|
.example(`${name} example.com/api --json '{"key":"value"}'`)
|
|
@@ -51,10 +51,8 @@ cli
|
|
|
51
51
|
const options = parseOptions(
|
|
52
52
|
z.object({
|
|
53
53
|
account: z.optional(z.string()),
|
|
54
|
-
channel: z.optional(z.coerce.string()),
|
|
55
54
|
confirm: z.optional(z.boolean()),
|
|
56
55
|
data: z.optional(z.string()),
|
|
57
|
-
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
58
56
|
fail: z.optional(z.boolean()),
|
|
59
57
|
header: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
60
58
|
include: z.optional(z.boolean()),
|
|
@@ -62,6 +60,7 @@ cli
|
|
|
62
60
|
json: z.optional(z.string()),
|
|
63
61
|
location: z.optional(z.boolean()),
|
|
64
62
|
method: z.optional(z.string()),
|
|
63
|
+
methodOpt: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
65
64
|
rpcUrl: z.optional(z.string()),
|
|
66
65
|
silent: z.optional(z.boolean()),
|
|
67
66
|
userAgent: z.optional(z.string()),
|
|
@@ -69,6 +68,7 @@ cli
|
|
|
69
68
|
}),
|
|
70
69
|
rawOptions,
|
|
71
70
|
)
|
|
71
|
+
const methodOpts = parseMethodOpts(options.methodOpt)
|
|
72
72
|
if (!rawUrl) {
|
|
73
73
|
cli.outputHelp()
|
|
74
74
|
return
|
|
@@ -79,12 +79,6 @@ cli
|
|
|
79
79
|
if (silent) options.confirm = false
|
|
80
80
|
|
|
81
81
|
const accountName = resolveAccountName(options.account)
|
|
82
|
-
const privateKey = process.env.MPPX_PRIVATE_KEY || (await createKeychain(accountName).get())
|
|
83
|
-
if (!privateKey) {
|
|
84
|
-
if (options.account) console.log(`Account "${accountName}" not found.`)
|
|
85
|
-
else console.log(`No account found.`)
|
|
86
|
-
process.exit(1)
|
|
87
|
-
}
|
|
88
82
|
|
|
89
83
|
const headers: Record<string, string> = {}
|
|
90
84
|
if (options.header) {
|
|
@@ -157,24 +151,43 @@ cli
|
|
|
157
151
|
return
|
|
158
152
|
}
|
|
159
153
|
|
|
160
|
-
const account = privateKeyToAccount(privateKey as `0x${string}`)
|
|
161
|
-
const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined)
|
|
162
|
-
const client = createClient({
|
|
163
|
-
chain: await resolveChain({ ...options, rpcUrl }),
|
|
164
|
-
transport: http(rpcUrl),
|
|
165
|
-
})
|
|
166
|
-
|
|
167
154
|
const challenge = Challenge.fromResponse(challengeResponse)
|
|
168
|
-
const explorerUrl = client.chain?.blockExplorers?.default?.url
|
|
169
|
-
const shownKeys = new Set<string>()
|
|
170
155
|
const challengeRequest = challenge.request as Record<string, unknown>
|
|
171
156
|
const currency = challengeRequest.currency as string | undefined
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
157
|
+
const shownKeys = new Set<string>()
|
|
158
|
+
|
|
159
|
+
let tokenSymbol =
|
|
160
|
+
challenge.method === 'stripe' ? (currency?.toUpperCase() ?? '') : (currency ?? '')
|
|
161
|
+
let tokenDecimals =
|
|
162
|
+
(challengeRequest.decimals as number | undefined) ?? (challenge.method === 'stripe' ? 2 : 6)
|
|
163
|
+
let explorerUrl: string | undefined
|
|
164
|
+
|
|
165
|
+
// Tempo-specific setup (private key, viem account/client, token info)
|
|
166
|
+
let account: ReturnType<typeof privateKeyToAccount> | undefined
|
|
167
|
+
let client: ReturnType<typeof createClient> | undefined
|
|
168
|
+
if (challenge.method === 'tempo') {
|
|
169
|
+
const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get())
|
|
170
|
+
if (!privateKey) {
|
|
171
|
+
if (options.account) console.error(`Account "${accountName}" not found.`)
|
|
172
|
+
else console.error(`No account found.`)
|
|
173
|
+
process.exit(1)
|
|
174
|
+
}
|
|
175
|
+
account = privateKeyToAccount(privateKey as `0x${string}`)
|
|
176
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL
|
|
177
|
+
client = createClient({
|
|
178
|
+
chain: await resolveChain({ ...options, rpcUrl }),
|
|
179
|
+
transport: http(rpcUrl),
|
|
180
|
+
})
|
|
181
|
+
explorerUrl = client.chain?.blockExplorers?.default?.url
|
|
182
|
+
const tokenInfo = currency
|
|
183
|
+
? await fetchTokenInfo(client, currency as Address, account.address).catch(
|
|
184
|
+
() => undefined,
|
|
185
|
+
)
|
|
186
|
+
: undefined
|
|
187
|
+
tokenSymbol = tokenInfo?.symbol ?? currency ?? ''
|
|
188
|
+
tokenDecimals =
|
|
189
|
+
tokenInfo?.decimals ?? (challengeRequest.decimals as number | undefined) ?? 6
|
|
190
|
+
}
|
|
178
191
|
|
|
179
192
|
{
|
|
180
193
|
printResponseHeaders(challengeResponse)
|
|
@@ -276,42 +289,149 @@ cli
|
|
|
276
289
|
}
|
|
277
290
|
}
|
|
278
291
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
let credential: string
|
|
293
|
+
if (challenge.method === 'tempo') {
|
|
294
|
+
if (!account || !client) {
|
|
295
|
+
console.error('Tempo requires a configured account.')
|
|
296
|
+
process.exit(1)
|
|
297
|
+
}
|
|
298
|
+
const tempoOpts = parseOptions(
|
|
299
|
+
z.object({
|
|
300
|
+
channel: z.optional(z.coerce.string()),
|
|
301
|
+
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
302
|
+
}),
|
|
303
|
+
methodOpts,
|
|
304
|
+
)
|
|
305
|
+
const mppx = Mppx.create({
|
|
306
|
+
methods: tempo({
|
|
307
|
+
account,
|
|
308
|
+
getClient: () => client!,
|
|
309
|
+
deposit: (() => {
|
|
310
|
+
if (challenge.intent !== 'session') return undefined
|
|
311
|
+
const suggestedDeposit = (challenge.request as Record<string, unknown>)
|
|
312
|
+
.suggestedDeposit as string | undefined
|
|
313
|
+
const cliDeposit =
|
|
314
|
+
tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined
|
|
315
|
+
const resolved =
|
|
316
|
+
suggestedDeposit ?? cliDeposit ?? (isTestnet(client!.chain!) ? '10' : undefined)
|
|
317
|
+
if (!resolved) {
|
|
318
|
+
console.error(
|
|
319
|
+
'Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.',
|
|
320
|
+
)
|
|
321
|
+
process.exit(1)
|
|
322
|
+
}
|
|
323
|
+
return resolved
|
|
324
|
+
})(),
|
|
325
|
+
}),
|
|
326
|
+
polyfill: false,
|
|
327
|
+
})
|
|
328
|
+
credential = await mppx.createCredential(
|
|
329
|
+
challengeResponse,
|
|
330
|
+
(() => {
|
|
331
|
+
if (!tempoOpts.channel) return undefined
|
|
332
|
+
const channelId = tempoOpts.channel
|
|
333
|
+
const saved = readChannelCumulative(channelId)
|
|
334
|
+
return {
|
|
335
|
+
channelId,
|
|
336
|
+
...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
|
|
295
337
|
}
|
|
296
|
-
return resolved
|
|
297
338
|
})(),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
339
|
+
)
|
|
340
|
+
} else if (challenge.method === 'stripe') {
|
|
341
|
+
const stripeOpts = parseOptions(
|
|
342
|
+
z.object({
|
|
343
|
+
paymentMethod: z.string(),
|
|
344
|
+
}),
|
|
345
|
+
methodOpts,
|
|
346
|
+
)
|
|
347
|
+
const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY
|
|
348
|
+
if (!stripeSecretKey) {
|
|
349
|
+
console.error(
|
|
350
|
+
'\nMPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
|
|
351
|
+
)
|
|
352
|
+
process.exit(1)
|
|
353
|
+
}
|
|
354
|
+
if (!stripeSecretKey.startsWith('sk_test_')) {
|
|
355
|
+
console.error(
|
|
356
|
+
'\nStripe CLI payments are currently only supported in test mode (sk_test_... keys).',
|
|
357
|
+
)
|
|
358
|
+
process.exit(1)
|
|
359
|
+
}
|
|
360
|
+
const mppx = Mppx.create({
|
|
361
|
+
methods: [
|
|
362
|
+
stripe.charge({
|
|
363
|
+
paymentMethod: stripeOpts.paymentMethod,
|
|
364
|
+
createToken: async ({
|
|
365
|
+
paymentMethod,
|
|
366
|
+
amount,
|
|
367
|
+
currency,
|
|
368
|
+
networkId,
|
|
369
|
+
expiresAt,
|
|
370
|
+
metadata,
|
|
371
|
+
}) => {
|
|
372
|
+
const body = new URLSearchParams({
|
|
373
|
+
payment_method: paymentMethod!,
|
|
374
|
+
'usage_limits[currency]': currency,
|
|
375
|
+
'usage_limits[max_amount]': amount,
|
|
376
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
377
|
+
})
|
|
378
|
+
if (networkId) body.set('seller_details[network_id]', networkId)
|
|
379
|
+
if (metadata) {
|
|
380
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
381
|
+
body.set(`metadata[${key}]`, value)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const sptUrl =
|
|
385
|
+
process.env.MPPX_STRIPE_SPT_URL ??
|
|
386
|
+
'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens'
|
|
387
|
+
const sptHeaders = {
|
|
388
|
+
Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
|
|
389
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
390
|
+
}
|
|
391
|
+
let response = await globalThis.fetch(sptUrl, {
|
|
392
|
+
method: 'POST',
|
|
393
|
+
headers: sptHeaders,
|
|
394
|
+
body,
|
|
395
|
+
})
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
const errorBody = (await response.json()) as { error: { message: string } }
|
|
398
|
+
if (
|
|
399
|
+
(metadata || networkId) &&
|
|
400
|
+
errorBody.error.message.includes('Received unknown parameter')
|
|
401
|
+
) {
|
|
402
|
+
const fallbackBody = new URLSearchParams({
|
|
403
|
+
payment_method: paymentMethod!,
|
|
404
|
+
'usage_limits[currency]': currency,
|
|
405
|
+
'usage_limits[max_amount]': amount,
|
|
406
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
407
|
+
})
|
|
408
|
+
response = await globalThis.fetch(sptUrl, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: sptHeaders,
|
|
411
|
+
body: fallbackBody,
|
|
412
|
+
})
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
const fallbackError = (await response.json()) as {
|
|
415
|
+
error: { message: string }
|
|
416
|
+
}
|
|
417
|
+
throw new Error(`Failed to create SPT: ${fallbackError.error.message}`)
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
throw new Error(`Failed to create SPT: ${errorBody.error.message}`)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const { id } = (await response.json()) as { id: string }
|
|
424
|
+
return id
|
|
425
|
+
},
|
|
426
|
+
}),
|
|
427
|
+
],
|
|
428
|
+
polyfill: false,
|
|
429
|
+
})
|
|
430
|
+
credential = await mppx.createCredential(challengeResponse)
|
|
431
|
+
} else {
|
|
432
|
+
console.error(`Unsupported payment method: ${challenge.method}`)
|
|
433
|
+
process.exit(1)
|
|
434
|
+
}
|
|
315
435
|
|
|
316
436
|
const sessionMd = challenge.request.methodDetails as
|
|
317
437
|
| { escrowContract?: string; chainId?: number }
|
|
@@ -324,18 +444,23 @@ cli
|
|
|
324
444
|
if (challenge.intent === 'session') {
|
|
325
445
|
const parsed = Credential.deserialize<SessionCredentialPayload>(credential)
|
|
326
446
|
sessionChannelId = parsed.payload.channelId
|
|
327
|
-
sessionChainId = sessionMd?.chainId ?? client
|
|
447
|
+
sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0
|
|
328
448
|
sessionEscrowContract = sessionMd?.escrowContract as Address | undefined
|
|
329
449
|
if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
|
|
330
450
|
sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
|
|
331
451
|
|
|
332
452
|
if (parsed.payload.action === 'open') {
|
|
333
|
-
const depositRaw =
|
|
334
|
-
(challengeRequest.suggestedDeposit as string | undefined) ?? options.deposit
|
|
453
|
+
const depositRaw = challengeRequest.suggestedDeposit as string | undefined
|
|
335
454
|
const depositDisplay = depositRaw
|
|
336
455
|
? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
|
|
337
456
|
: ''
|
|
338
|
-
|
|
457
|
+
const prefix = options.confirm ? '' : '\n'
|
|
458
|
+
info(
|
|
459
|
+
`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`,
|
|
460
|
+
)
|
|
461
|
+
} else {
|
|
462
|
+
const prefix = options.confirm ? '' : '\n'
|
|
463
|
+
info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`)
|
|
339
464
|
}
|
|
340
465
|
}
|
|
341
466
|
|
|
@@ -403,6 +528,15 @@ cli
|
|
|
403
528
|
explorerUrl
|
|
404
529
|
) {
|
|
405
530
|
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)])
|
|
531
|
+
} else if (
|
|
532
|
+
key === 'reference' &&
|
|
533
|
+
typeof value === 'string' &&
|
|
534
|
+
challenge.method === 'stripe' &&
|
|
535
|
+
value.startsWith('pi_')
|
|
536
|
+
) {
|
|
537
|
+
const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_')
|
|
538
|
+
const dashboardUrl = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`
|
|
539
|
+
rows.push([key, pc.link(dashboardUrl, value)])
|
|
406
540
|
} else rows.push([key, String(value)])
|
|
407
541
|
}
|
|
408
542
|
rows.sort(([a], [b]) => a.localeCompare(b))
|
|
@@ -430,7 +564,7 @@ cli
|
|
|
430
564
|
const md = challenge.request.methodDetails as
|
|
431
565
|
| { escrowContract?: string; chainId?: number }
|
|
432
566
|
| undefined
|
|
433
|
-
const sessionChainId = md?.chainId ?? client
|
|
567
|
+
const sessionChainId = md?.chainId ?? client?.chain?.id ?? 0
|
|
434
568
|
const escrowContract = md?.escrowContract as Address | undefined
|
|
435
569
|
let cumulativeAmount =
|
|
436
570
|
sessionCred?.payload &&
|
|
@@ -493,8 +627,8 @@ cli
|
|
|
493
627
|
cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required
|
|
494
628
|
|
|
495
629
|
const signature = await signVoucher(
|
|
496
|
-
client
|
|
497
|
-
account
|
|
630
|
+
client!,
|
|
631
|
+
account!,
|
|
498
632
|
{ channelId, cumulativeAmount },
|
|
499
633
|
escrowContract,
|
|
500
634
|
sessionChainId,
|
|
@@ -507,7 +641,7 @@ cli
|
|
|
507
641
|
cumulativeAmount: cumulativeAmount.toString(),
|
|
508
642
|
signature,
|
|
509
643
|
},
|
|
510
|
-
source: `did:pkh:eip155:${sessionChainId}:${account
|
|
644
|
+
source: `did:pkh:eip155:${sessionChainId}:${account!.address}`,
|
|
511
645
|
})
|
|
512
646
|
await globalThis.fetch(url, {
|
|
513
647
|
method: 'POST',
|
|
@@ -586,8 +720,8 @@ cli
|
|
|
586
720
|
|
|
587
721
|
if (channelId && escrowContract && sessionChainId) {
|
|
588
722
|
const signature = await signVoucher(
|
|
589
|
-
client
|
|
590
|
-
account
|
|
723
|
+
client!,
|
|
724
|
+
account!,
|
|
591
725
|
{ channelId, cumulativeAmount },
|
|
592
726
|
escrowContract,
|
|
593
727
|
sessionChainId,
|
|
@@ -601,7 +735,7 @@ cli
|
|
|
601
735
|
const closeCred = Credential.serialize({
|
|
602
736
|
challenge,
|
|
603
737
|
payload: closePayload,
|
|
604
|
-
source: `did:pkh:eip155:${sessionChainId}:${account
|
|
738
|
+
source: `did:pkh:eip155:${sessionChainId}:${account!.address}`,
|
|
605
739
|
})
|
|
606
740
|
const closeRes = await globalThis.fetch(url, {
|
|
607
741
|
method: 'POST',
|
|
@@ -649,8 +783,8 @@ cli
|
|
|
649
783
|
info(`${pc.dim('Kept channel open.')}\n`)
|
|
650
784
|
} else if (shouldClose) {
|
|
651
785
|
const signature = await signVoucher(
|
|
652
|
-
client
|
|
653
|
-
account
|
|
786
|
+
client!,
|
|
787
|
+
account!,
|
|
654
788
|
{ channelId: sessionChannelId!, cumulativeAmount: sessionCumulativeAmount },
|
|
655
789
|
sessionEscrowContract!,
|
|
656
790
|
sessionChainId,
|
|
@@ -664,7 +798,7 @@ cli
|
|
|
664
798
|
const closeCred = Credential.serialize({
|
|
665
799
|
challenge,
|
|
666
800
|
payload: closePayload,
|
|
667
|
-
source: `did:pkh:eip155:${sessionChainId}:${account
|
|
801
|
+
source: `did:pkh:eip155:${sessionChainId}:${account!.address}`,
|
|
668
802
|
})
|
|
669
803
|
const closeRes = await globalThis.fetch(url, {
|
|
670
804
|
...fetchInit,
|
|
@@ -690,20 +824,21 @@ cli
|
|
|
690
824
|
closeTxHash && explorerUrl
|
|
691
825
|
? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
|
|
692
826
|
: ''
|
|
827
|
+
const closePrefix = options.confirm ? '' : '\n'
|
|
693
828
|
info(
|
|
694
|
-
|
|
829
|
+
`${closePrefix}${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`,
|
|
695
830
|
)
|
|
696
831
|
} else {
|
|
697
832
|
const closeBody = await closeRes.text().catch(() => '')
|
|
698
833
|
info(
|
|
699
|
-
|
|
834
|
+
`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
|
|
700
835
|
)
|
|
701
836
|
info(
|
|
702
837
|
`${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
|
|
703
838
|
`${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
|
|
704
839
|
`${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
|
|
705
840
|
`${pc.dim(` chainId: ${sessionChainId}`)}\n` +
|
|
706
|
-
`${pc.dim(` account: ${account
|
|
841
|
+
`${pc.dim(` account: ${account?.address}`)}\n` +
|
|
707
842
|
`${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`,
|
|
708
843
|
)
|
|
709
844
|
}
|
|
@@ -988,6 +1123,21 @@ try {
|
|
|
988
1123
|
|
|
989
1124
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
990
1125
|
|
|
1126
|
+
function parseMethodOpts(raw: string | string[] | undefined): Record<string, string> {
|
|
1127
|
+
if (!raw) return {}
|
|
1128
|
+
const list = Array.isArray(raw) ? raw : [raw]
|
|
1129
|
+
const result: Record<string, string> = {}
|
|
1130
|
+
for (const item of list) {
|
|
1131
|
+
const idx = item.indexOf('=')
|
|
1132
|
+
if (idx === -1) {
|
|
1133
|
+
console.error(`Invalid method option format: ${item} (expected key=value)`)
|
|
1134
|
+
process.exit(1)
|
|
1135
|
+
}
|
|
1136
|
+
result[item.slice(0, idx)] = item.slice(idx + 1)
|
|
1137
|
+
}
|
|
1138
|
+
return result
|
|
1139
|
+
}
|
|
1140
|
+
|
|
991
1141
|
function parseOptions<const schema extends ZodMiniType>(
|
|
992
1142
|
schema: schema,
|
|
993
1143
|
rawOptions: unknown,
|
|
@@ -1285,8 +1435,8 @@ function chainName(chain: { id: number; name: string }) {
|
|
|
1285
1435
|
}
|
|
1286
1436
|
|
|
1287
1437
|
const pathUsd = '0x20c0000000000000000000000000000000000000' as Address
|
|
1288
|
-
const
|
|
1289
|
-
const mainnetTokens = [pathUsd,
|
|
1438
|
+
const usdc = '0x20C000000000000000000000b9537d11c60E8b50' as Address
|
|
1439
|
+
const mainnetTokens = [pathUsd, usdc] as const
|
|
1290
1440
|
const testnetTokens = [
|
|
1291
1441
|
'0x20c0000000000000000000000000000000000000',
|
|
1292
1442
|
'0x20c0000000000000000000000000000000000001',
|
|
@@ -1326,7 +1476,7 @@ async function fetchTokenInfo(
|
|
|
1326
1476
|
])
|
|
1327
1477
|
const knownSymbols: Record<string, string> = {
|
|
1328
1478
|
[pathUsd]: 'PathUSD',
|
|
1329
|
-
[
|
|
1479
|
+
[usdc]: 'USDC',
|
|
1330
1480
|
}
|
|
1331
1481
|
const symbol = knownSymbols[token] ?? metadata.symbol
|
|
1332
1482
|
const decimals = 'decimals' in metadata ? metadata.decimals : 6
|
|
@@ -60,7 +60,7 @@ describe('http', () => {
|
|
|
60
60
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
61
61
|
{
|
|
62
62
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
63
|
-
"id": "
|
|
63
|
+
"id": "z8dUi61lViOj6cwh_ISb_5X8nBJF2OjTydcEap8wX0o",
|
|
64
64
|
"intent": "charge",
|
|
65
65
|
"method": "tempo",
|
|
66
66
|
"realm": "api.example.com",
|
|
@@ -91,7 +91,7 @@ describe('http', () => {
|
|
|
91
91
|
const headers = result.headers as Headers
|
|
92
92
|
|
|
93
93
|
expect(headers.get('Authorization')).toMatchInlineSnapshot(
|
|
94
|
-
`"Payment
|
|
94
|
+
`"Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiIsImlkIjoiejhkVWk2MWxWaU9qNmN3aF9JU2JfNVg4bkJKRjJPalR5ZGNFYXA4d1gwbyIsImludGVudCI6ImNoYXJnZSIsIm1ldGhvZCI6InRlbXBvIiwicmVhbG0iOiJhcGkuZXhhbXBsZS5jb20iLCJyZXF1ZXN0IjoiZXlKaGJXOTFiblFpT2lJeE1EQXdJaXdpWTNWeWNtVnVZM2tpT2lJd2VESXdZekF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKbGVIQnBjbVZ6SWpvaU1qQXlOUzB3TVMwd01WUXdNRG93TURvd01DNHdNREJhSWl3aWNtVmphWEJwWlc1MElqb2lNSGczTkRKa016VkRZelkyTXpSRE1EVXpNamt5TldFellqZzBORUpqT1dVM05UazFaamhtUlRBd0luMCJ9LCJwYXlsb2FkIjp7InNpZ25hdHVyZSI6IjB4YWJjMTIzIiwidHlwZSI6InRyYW5zYWN0aW9uIn19"`,
|
|
95
95
|
)
|
|
96
96
|
})
|
|
97
97
|
|
|
@@ -182,7 +182,7 @@ describe('mcp', () => {
|
|
|
182
182
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
183
183
|
{
|
|
184
184
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
185
|
-
"id": "
|
|
185
|
+
"id": "z8dUi61lViOj6cwh_ISb_5X8nBJF2OjTydcEap8wX0o",
|
|
186
186
|
"intent": "charge",
|
|
187
187
|
"method": "tempo",
|
|
188
188
|
"realm": "api.example.com",
|
|
@@ -239,7 +239,7 @@ describe('mcp', () => {
|
|
|
239
239
|
"org.paymentauth/credential": {
|
|
240
240
|
"challenge": {
|
|
241
241
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
242
|
-
"id": "
|
|
242
|
+
"id": "z8dUi61lViOj6cwh_ISb_5X8nBJF2OjTydcEap8wX0o",
|
|
243
243
|
"intent": "charge",
|
|
244
244
|
"method": "tempo",
|
|
245
245
|
"realm": "api.example.com",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Env from './env.js'
|
|
2
|
+
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
vi.unstubAllEnvs()
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
describe('Env.get', () => {
|
|
8
|
+
test('returns default realm when no env vars are set', () => {
|
|
9
|
+
expect(Env.get('realm')).toBe('MPP Payment')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('returns default secretKey when MPP_SECRET_KEY is not set', () => {
|
|
13
|
+
expect(Env.get('secretKey')).toBe('tmp')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('returns MPP_SECRET_KEY when set', () => {
|
|
17
|
+
vi.stubEnv('MPP_SECRET_KEY', 'sk_live_abc123')
|
|
18
|
+
expect(Env.get('secretKey')).toBe('sk_live_abc123')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('returns FLY_APP_NAME when set', () => {
|
|
22
|
+
vi.stubEnv('FLY_APP_NAME', 'my-fly-app')
|
|
23
|
+
expect(Env.get('realm')).toBe('my-fly-app')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('FLY_APP_NAME takes precedence over HOST', () => {
|
|
27
|
+
vi.stubEnv('FLY_APP_NAME', 'fly-app')
|
|
28
|
+
vi.stubEnv('HOST', 'my-host')
|
|
29
|
+
expect(Env.get('realm')).toBe('fly-app')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('HOST takes precedence over MPP_REALM', () => {
|
|
33
|
+
vi.stubEnv('HOST', 'my-host')
|
|
34
|
+
vi.stubEnv('MPP_REALM', 'custom-realm')
|
|
35
|
+
expect(Env.get('realm')).toBe('my-host')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('falls through to later vars when earlier ones are unset', () => {
|
|
39
|
+
vi.stubEnv('MPP_REALM', 'fallback-realm')
|
|
40
|
+
expect(Env.get('realm')).toBe('fallback-realm')
|
|
41
|
+
})
|
|
42
|
+
})
|
package/src/internal/types.ts
CHANGED
|
@@ -401,3 +401,14 @@ export type Distribute<U, R> = U extends infer member ? (response: member) => R
|
|
|
401
401
|
|
|
402
402
|
/** @internal */
|
|
403
403
|
export type Flatten<element> = element extends readonly (infer item)[] ? item : element
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @description Creates a type that extracts the values of T.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ValueOf<{ a: string, b: number }>
|
|
410
|
+
* => string | number
|
|
411
|
+
*
|
|
412
|
+
* @internal
|
|
413
|
+
*/
|
|
414
|
+
export type ValueOf<T> = T[keyof T]
|
|
@@ -9,6 +9,7 @@ const hopByHopHeaders = new Set([
|
|
|
9
9
|
'trailer',
|
|
10
10
|
])
|
|
11
11
|
|
|
12
|
+
/** Strips hop-by-hop, auth, encoding, cookie, and forwarding headers from a request before proxying upstream. */
|
|
12
13
|
export function scrub(headers: Headers): Headers {
|
|
13
14
|
const scrubbed = new Headers()
|
|
14
15
|
|
|
@@ -28,6 +29,7 @@ export function scrub(headers: Headers): Headers {
|
|
|
28
29
|
return scrubbed
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Strips `content-encoding` and `content-length` from an upstream response so the proxy can re-stream it. */
|
|
31
33
|
export function scrubResponse(response: Response): Response {
|
|
32
34
|
const headers = new Headers(response.headers)
|
|
33
35
|
headers.delete('content-encoding')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const httpMethods = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'])
|
|
2
2
|
|
|
3
|
+
/** Extracts the pathname from a URL, stripping the optional `basePath` prefix. Returns `null` if the path doesn't match. */
|
|
3
4
|
export function pathname(url: URL, basePath?: string): string | null {
|
|
4
5
|
let pathname = url.pathname
|
|
5
6
|
if (basePath) {
|
|
@@ -10,6 +11,7 @@ export function pathname(url: URL, basePath?: string): string | null {
|
|
|
10
11
|
return pathname
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/** Splits a `/{serviceId}/rest/of/path` pathname into its service ID and upstream path. */
|
|
13
15
|
export function parse(pathname: string): { serviceId: string; upstreamPath: string } | null {
|
|
14
16
|
const segments = pathname.split('/').filter(Boolean)
|
|
15
17
|
const serviceId = segments[0]
|
|
@@ -19,6 +21,7 @@ export function parse(pathname: string): { serviceId: string; upstreamPath: stri
|
|
|
19
21
|
return { serviceId, upstreamPath }
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** Finds the first route matching both the HTTP method and path (via `URLPattern`). */
|
|
22
25
|
export function match(
|
|
23
26
|
routes: Record<string, unknown>,
|
|
24
27
|
method: string,
|
|
@@ -33,6 +36,7 @@ export function match(
|
|
|
33
36
|
return null
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/** Finds the first route matching just the path, ignoring the HTTP method. Used for management POST fallback. */
|
|
36
40
|
export function matchPath(
|
|
37
41
|
routes: Record<string, unknown>,
|
|
38
42
|
path: string,
|