mppx 0.5.3 → 0.5.4
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/CHANGELOG.md +7 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +11 -9
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +3 -3
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +10 -5
- package/dist/cli/utils.js.map +1 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +40 -21
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/internal/html/config.d.ts +137 -0
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js +300 -0
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/stripe/internal/types.d.ts +6 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +25 -16
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +23 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html/types.d.ts +2 -0
- package/dist/stripe/server/internal/html/types.d.ts.map +1 -0
- package/dist/stripe/server/internal/html/types.js +2 -0
- package/dist/stripe/server/internal/html/types.js.map +1 -0
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +32 -27
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +32 -3
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +11 -8
- package/src/cli/plugins/tempo.ts +3 -2
- package/src/cli/utils.test.ts +64 -0
- package/src/cli/utils.ts +10 -4
- package/src/server/Transport.test.ts +216 -0
- package/src/server/Transport.ts +47 -24
- package/src/server/internal/html/config.ts +406 -0
- package/src/stripe/internal/types.ts +20 -0
- package/src/stripe/server/Charge.ts +46 -4
- package/src/stripe/server/internal/html/main.ts +87 -19
- package/src/stripe/server/internal/html/types.ts +5 -0
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/Charge.ts +46 -4
- package/src/tempo/server/internal/html/main.ts +51 -11
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,
|
|
1
|
+
{"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,2r9aAA2r9a,CAAA"}
|
package/package.json
CHANGED
package/src/cli/cli.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
printResponseHeaders,
|
|
33
33
|
prompt,
|
|
34
34
|
resolveChain,
|
|
35
|
+
resolveRpcUrl,
|
|
35
36
|
} from './utils.js'
|
|
36
37
|
|
|
37
38
|
const packageJson = createRequire(import.meta.url)('../../package.json') as {
|
|
@@ -516,8 +517,9 @@ const account = Cli.create('account', {
|
|
|
516
517
|
? link(`${explorerUrl}/address/${acct.address}`, acct.address)
|
|
517
518
|
: acct.address
|
|
518
519
|
console.log(pc.dim(`Address ${addrDisplay}`))
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
const rpcUrl = resolveRpcUrl(c.options.rpcUrl)
|
|
521
|
+
resolveChain({ rpcUrl })
|
|
522
|
+
.then((chain) => createClient({ chain, transport: http(rpcUrl) }))
|
|
521
523
|
.then((client) =>
|
|
522
524
|
import('viem/tempo').then(({ Actions }) =>
|
|
523
525
|
Actions.faucet.fund(client, { account: acct }).catch(() => {}),
|
|
@@ -629,8 +631,9 @@ const account = Cli.create('account', {
|
|
|
629
631
|
return c.error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 })
|
|
630
632
|
}
|
|
631
633
|
const acct = privateKeyToAccount(key as `0x${string}`)
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
+
const rpcUrl = resolveRpcUrl(c.options.rpcUrl)
|
|
635
|
+
const chain = await resolveChain({ rpcUrl })
|
|
636
|
+
const client = createClient({ chain, transport: http(rpcUrl) })
|
|
634
637
|
console.log(`Funding "${accountName}" on ${chainName(chain)}`)
|
|
635
638
|
try {
|
|
636
639
|
const { Actions } = await import('viem/tempo')
|
|
@@ -711,8 +714,8 @@ const account = Cli.create('account', {
|
|
|
711
714
|
})
|
|
712
715
|
}
|
|
713
716
|
const address = tempoEntry.wallet_address as Address
|
|
714
|
-
const rpcUrl = c.options.rpcUrl
|
|
715
|
-
const chain =
|
|
717
|
+
const rpcUrl = resolveRpcUrl(c.options.rpcUrl)
|
|
718
|
+
const chain = await resolveChain({ rpcUrl })
|
|
716
719
|
const explorerUrl = chain.blockExplorers?.default?.url
|
|
717
720
|
const addrDisplay = explorerUrl
|
|
718
721
|
? link(`${explorerUrl}/address/${address}`, address)
|
|
@@ -744,8 +747,8 @@ const account = Cli.create('account', {
|
|
|
744
747
|
return c.error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 })
|
|
745
748
|
}
|
|
746
749
|
const acct = privateKeyToAccount(key as `0x${string}`)
|
|
747
|
-
const rpcUrl = c.options.rpcUrl
|
|
748
|
-
const chain =
|
|
750
|
+
const rpcUrl = resolveRpcUrl(c.options.rpcUrl)
|
|
751
|
+
const chain = await resolveChain({ rpcUrl })
|
|
749
752
|
const explorerUrl = chain.blockExplorers?.default?.url
|
|
750
753
|
const addrDisplay = explorerUrl
|
|
751
754
|
? link(`${explorerUrl}/address/${acct.address}`, acct.address)
|
package/src/cli/plugins/tempo.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
link,
|
|
25
25
|
pc,
|
|
26
26
|
resolveChain,
|
|
27
|
+
resolveRpcUrl,
|
|
27
28
|
} from '../utils.js'
|
|
28
29
|
import { createPlugin, type Plugin } from './plugin.js'
|
|
29
30
|
|
|
@@ -67,7 +68,7 @@ export function tempo() {
|
|
|
67
68
|
useTempoCliSign = true
|
|
68
69
|
const tempoEntry = resolveTempoAccount(accountName)
|
|
69
70
|
if (tempoEntry) {
|
|
70
|
-
const rpcUrl = options.rpcUrl
|
|
71
|
+
const rpcUrl = resolveRpcUrl(options.rpcUrl)
|
|
71
72
|
client = createClient({
|
|
72
73
|
chain: await resolveChain({ rpcUrl }),
|
|
73
74
|
transport: http(rpcUrl),
|
|
@@ -107,7 +108,7 @@ export function tempo() {
|
|
|
107
108
|
} else account = privateKeyToAccount(privateKey as `0x${string}`)
|
|
108
109
|
|
|
109
110
|
if (!useTempoCliSign && account) {
|
|
110
|
-
const rpcUrl = options.rpcUrl
|
|
111
|
+
const rpcUrl = resolveRpcUrl(options.rpcUrl)
|
|
111
112
|
client = createClient({
|
|
112
113
|
chain: await resolveChain({ rpcUrl }),
|
|
113
114
|
transport: http(rpcUrl),
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
|
|
2
|
+
import { afterEach, describe, expect, test } from 'vp/test'
|
|
3
|
+
|
|
4
|
+
import { resolveChain, resolveRpcUrl } from './utils.js'
|
|
5
|
+
|
|
6
|
+
describe('resolveRpcUrl', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
delete process.env.MPPX_RPC_URL
|
|
9
|
+
delete process.env.RPC_URL
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('returns explicit value when provided', () => {
|
|
13
|
+
process.env.MPPX_RPC_URL = 'https://env.example.com'
|
|
14
|
+
expect(resolveRpcUrl('https://explicit.example.com')).toBe('https://explicit.example.com')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('falls back to MPPX_RPC_URL env var', () => {
|
|
18
|
+
process.env.MPPX_RPC_URL = 'https://mppx.example.com'
|
|
19
|
+
process.env.RPC_URL = 'https://rpc.example.com'
|
|
20
|
+
expect(resolveRpcUrl()).toBe('https://mppx.example.com')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('falls back to RPC_URL env var when MPPX_RPC_URL is not set', () => {
|
|
24
|
+
process.env.RPC_URL = 'https://rpc.example.com'
|
|
25
|
+
expect(resolveRpcUrl()).toBe('https://rpc.example.com')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('returns undefined when nothing is set', () => {
|
|
29
|
+
expect(resolveRpcUrl()).toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('trims whitespace from env vars', () => {
|
|
33
|
+
process.env.MPPX_RPC_URL = ' https://mppx.example.com '
|
|
34
|
+
expect(resolveRpcUrl()).toBe('https://mppx.example.com')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('skips empty MPPX_RPC_URL and falls back to RPC_URL', () => {
|
|
38
|
+
process.env.MPPX_RPC_URL = ' '
|
|
39
|
+
process.env.RPC_URL = 'https://rpc.example.com'
|
|
40
|
+
expect(resolveRpcUrl()).toBe('https://rpc.example.com')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('resolveChain', () => {
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
delete process.env.MPPX_RPC_URL
|
|
47
|
+
delete process.env.RPC_URL
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('defaults to tempo mainnet when no rpcUrl is provided', async () => {
|
|
51
|
+
const chain = await resolveChain()
|
|
52
|
+
expect(chain.id).toBe(tempoMainnet.id)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('defaults to tempo mainnet when rpcUrl is undefined', async () => {
|
|
56
|
+
const chain = await resolveChain({ rpcUrl: undefined })
|
|
57
|
+
expect(chain.id).toBe(tempoMainnet.id)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('does not default to testnet', async () => {
|
|
61
|
+
const chain = await resolveChain()
|
|
62
|
+
expect(chain.id).not.toBe(tempoModerato.id)
|
|
63
|
+
})
|
|
64
|
+
})
|
package/src/cli/utils.ts
CHANGED
|
@@ -221,17 +221,23 @@ export function fmtBalance(
|
|
|
221
221
|
return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/** Resolve RPC URL from explicit option, then MPPX_RPC_URL, then RPC_URL env vars. */
|
|
225
|
+
export function resolveRpcUrl(explicit?: string | undefined): string | undefined {
|
|
226
|
+
return explicit ?? (process.env.MPPX_RPC_URL?.trim() || process.env.RPC_URL?.trim() || undefined)
|
|
227
|
+
}
|
|
228
|
+
|
|
224
229
|
export async function resolveChain(opts: { rpcUrl?: string | undefined } = {}): Promise<Chain> {
|
|
225
|
-
|
|
230
|
+
const rpcUrl = resolveRpcUrl(opts.rpcUrl)
|
|
231
|
+
if (!rpcUrl) return tempoMainnet
|
|
226
232
|
const { getChainId } = await import('viem/actions')
|
|
227
|
-
const chainId = await getChainId(createClient({ transport: http(
|
|
233
|
+
const chainId = await getChainId(createClient({ transport: http(rpcUrl) }))
|
|
228
234
|
const allExports = Object.values(await import('viem/chains')) as unknown[]
|
|
229
235
|
const candidates = allExports.filter(
|
|
230
236
|
(c): c is Chain =>
|
|
231
237
|
typeof c === 'object' && c !== null && 'id' in c && (c as Chain).id === chainId,
|
|
232
238
|
)
|
|
233
239
|
const found = candidates.find((c) => 'serializers' in c && c.serializers) ?? candidates[0]
|
|
234
|
-
if (!found) throw new Error(`Unknown chain ID ${chainId} from RPC ${
|
|
240
|
+
if (!found) throw new Error(`Unknown chain ID ${chainId} from RPC ${rpcUrl}`)
|
|
235
241
|
return found
|
|
236
242
|
}
|
|
237
243
|
|
|
@@ -306,7 +312,7 @@ export async function fetchBalanceLines(
|
|
|
306
312
|
|
|
307
313
|
const mainnetClient = createClient({
|
|
308
314
|
chain: tempoMainnet,
|
|
309
|
-
transport: http(
|
|
315
|
+
transport: http(resolveRpcUrl()),
|
|
310
316
|
})
|
|
311
317
|
const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url
|
|
312
318
|
const mainnetResults = await Promise.all(
|
|
@@ -101,6 +101,222 @@ describe('http', () => {
|
|
|
101
101
|
})
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
+
describe('respondChallenge html', () => {
|
|
105
|
+
const htmlOptions = {
|
|
106
|
+
config: { foo: 'bar' },
|
|
107
|
+
content: '<script src="/pay.js"></script>',
|
|
108
|
+
formatAmount: () => '$10.00',
|
|
109
|
+
text: undefined,
|
|
110
|
+
theme: undefined,
|
|
111
|
+
} satisfies Parameters<Transport.Http['respondChallenge']>[0]['html']
|
|
112
|
+
|
|
113
|
+
test('returns html when Accept includes text/html', async () => {
|
|
114
|
+
const transport = Transport.http()
|
|
115
|
+
const request = new Request('https://example.com', {
|
|
116
|
+
headers: { Accept: 'text/html' },
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const response = await transport.respondChallenge({
|
|
120
|
+
challenge,
|
|
121
|
+
input: request,
|
|
122
|
+
html: htmlOptions,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(response.status).toBe(402)
|
|
126
|
+
expect(response.headers.get('Content-Type')).toBe('text/html; charset=utf-8')
|
|
127
|
+
expect(response.headers.get('WWW-Authenticate')).toContain('Payment')
|
|
128
|
+
expect(response.headers.get('Cache-Control')).toBe('no-store')
|
|
129
|
+
|
|
130
|
+
const body = await response.text()
|
|
131
|
+
expect(body).toContain('<!doctype html>')
|
|
132
|
+
expect(body).toContain('<title>Payment Required</title>')
|
|
133
|
+
expect(body).toContain('$10.00')
|
|
134
|
+
expect(body).toContain('Payment Required')
|
|
135
|
+
expect(body).toContain('<script src="/pay.js"></script>')
|
|
136
|
+
expect(body).toContain('__MPPX_DATA__')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('returns service worker script when __mppx_worker param is set', async () => {
|
|
140
|
+
const transport = Transport.http()
|
|
141
|
+
const request = new Request('https://example.com?__mppx_worker')
|
|
142
|
+
|
|
143
|
+
const response = await transport.respondChallenge({
|
|
144
|
+
challenge,
|
|
145
|
+
input: request,
|
|
146
|
+
html: htmlOptions,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
expect(response.status).toBe(200)
|
|
150
|
+
expect(response.headers.get('Content-Type')).toBe('application/javascript')
|
|
151
|
+
expect(response.headers.get('Cache-Control')).toBe('no-store')
|
|
152
|
+
|
|
153
|
+
const body = await response.text()
|
|
154
|
+
expect(body).toContain('addEventListener')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('does not return html when Accept does not include text/html', async () => {
|
|
158
|
+
const transport = Transport.http()
|
|
159
|
+
const request = new Request('https://example.com', {
|
|
160
|
+
headers: { Accept: 'application/json' },
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const response = await transport.respondChallenge({
|
|
164
|
+
challenge,
|
|
165
|
+
input: request,
|
|
166
|
+
html: htmlOptions,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(response.status).toBe(402)
|
|
170
|
+
expect(response.headers.get('Content-Type')).toBeNull()
|
|
171
|
+
expect(await response.text()).toBe('')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('renders description when challenge has one', async () => {
|
|
175
|
+
const transport = Transport.http()
|
|
176
|
+
const request = new Request('https://example.com', {
|
|
177
|
+
headers: { Accept: 'text/html' },
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const challengeWithDescription = {
|
|
181
|
+
...challenge,
|
|
182
|
+
description: 'Access to premium content',
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const response = await transport.respondChallenge({
|
|
186
|
+
challenge: challengeWithDescription,
|
|
187
|
+
input: request,
|
|
188
|
+
html: htmlOptions,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const body = await response.text()
|
|
192
|
+
expect(body).toContain('Access to premium content')
|
|
193
|
+
expect(body).toContain('mppx-summary-description')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('renders expires when challenge has one', async () => {
|
|
197
|
+
const transport = Transport.http()
|
|
198
|
+
const request = new Request('https://example.com', {
|
|
199
|
+
headers: { Accept: 'text/html' },
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const response = await transport.respondChallenge({
|
|
203
|
+
challenge,
|
|
204
|
+
input: request,
|
|
205
|
+
html: htmlOptions,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const body = await response.text()
|
|
209
|
+
expect(body).toContain('Expires at')
|
|
210
|
+
expect(body).toContain('2025-01-01T00:00:00.000Z')
|
|
211
|
+
expect(body).toContain('mppx-summary-expires')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('does not render description when challenge lacks one', async () => {
|
|
215
|
+
const transport = Transport.http()
|
|
216
|
+
const request = new Request('https://example.com', {
|
|
217
|
+
headers: { Accept: 'text/html' },
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const challengeNoDescription = { ...challenge }
|
|
221
|
+
delete (challengeNoDescription as any).description
|
|
222
|
+
|
|
223
|
+
const response = await transport.respondChallenge({
|
|
224
|
+
challenge: challengeNoDescription,
|
|
225
|
+
input: request,
|
|
226
|
+
html: htmlOptions,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const body = await response.text()
|
|
230
|
+
expect(body).not.toMatch(/<p class="mppx-summary-description"/)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('applies custom text', async () => {
|
|
234
|
+
const transport = Transport.http()
|
|
235
|
+
const request = new Request('https://example.com', {
|
|
236
|
+
headers: { Accept: 'text/html' },
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const response = await transport.respondChallenge({
|
|
240
|
+
challenge,
|
|
241
|
+
input: request,
|
|
242
|
+
html: {
|
|
243
|
+
...htmlOptions,
|
|
244
|
+
text: { title: 'Pay Up', paymentRequired: 'Gotta Pay' },
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const body = await response.text()
|
|
249
|
+
expect(body).toContain('<title>Pay Up</title>')
|
|
250
|
+
expect(body).toContain('Gotta Pay')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('applies custom theme logo', async () => {
|
|
254
|
+
const transport = Transport.http()
|
|
255
|
+
const request = new Request('https://example.com', {
|
|
256
|
+
headers: { Accept: 'text/html' },
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const response = await transport.respondChallenge({
|
|
260
|
+
challenge,
|
|
261
|
+
input: request,
|
|
262
|
+
html: {
|
|
263
|
+
...htmlOptions,
|
|
264
|
+
theme: { logo: 'https://example.com/logo.png' },
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const body = await response.text()
|
|
269
|
+
expect(body).toContain('https://example.com/logo.png')
|
|
270
|
+
expect(body).toContain('mppx-logo')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('embeds config and challenge in data script', async () => {
|
|
274
|
+
const transport = Transport.http()
|
|
275
|
+
const request = new Request('https://example.com', {
|
|
276
|
+
headers: { Accept: 'text/html' },
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const response = await transport.respondChallenge({
|
|
280
|
+
challenge,
|
|
281
|
+
input: request,
|
|
282
|
+
html: htmlOptions,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const body = await response.text()
|
|
286
|
+
// Extract the JSON data from the script tag
|
|
287
|
+
const dataMatch = body.match(
|
|
288
|
+
/<script id="__MPPX_DATA__" type="application\/json">\s*([\s\S]*?)\s*<\/script>/,
|
|
289
|
+
)
|
|
290
|
+
expect(dataMatch).not.toBeNull()
|
|
291
|
+
|
|
292
|
+
const data = JSON.parse(dataMatch?.[1]?.replace(/\\u003c/g, '<') ?? '')
|
|
293
|
+
expect(data.config).toEqual({ foo: 'bar' })
|
|
294
|
+
expect(data.challenge.id).toBe(challenge.id)
|
|
295
|
+
expect(data.challenge.method).toBe('tempo')
|
|
296
|
+
expect(data.text.paymentRequired).toBe('Payment Required')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('sanitizes html in formatted amount', async () => {
|
|
300
|
+
const transport = Transport.http()
|
|
301
|
+
const request = new Request('https://example.com', {
|
|
302
|
+
headers: { Accept: 'text/html' },
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const response = await transport.respondChallenge({
|
|
306
|
+
challenge,
|
|
307
|
+
input: request,
|
|
308
|
+
html: {
|
|
309
|
+
...htmlOptions,
|
|
310
|
+
formatAmount: () => '<script>alert("xss")</script>',
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const body = await response.text()
|
|
315
|
+
expect(body).not.toContain('<script>alert("xss")</script>')
|
|
316
|
+
expect(body).toContain('<script>')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
104
320
|
describe('respondChallenge with error status codes', () => {
|
|
105
321
|
test('BadRequestError returns 400', async () => {
|
|
106
322
|
const transport = Transport.http()
|
package/src/server/Transport.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Distribute, UnionToIntersection } from '../internal/types.js'
|
|
|
7
7
|
import * as core_Mcp from '../Mcp.js'
|
|
8
8
|
import * as Receipt from '../Receipt.js'
|
|
9
9
|
import * as Html from './internal/html/config.js'
|
|
10
|
+
import { html } from './internal/html/config.js'
|
|
10
11
|
import { serviceWorker } from './internal/html/serviceWorker.gen.js'
|
|
11
12
|
|
|
12
13
|
export { type McpSdk, mcpSdk } from '../mcp-sdk/server/Transport.js'
|
|
@@ -128,7 +129,7 @@ export function http(): Http {
|
|
|
128
129
|
return Credential.deserialize(payment)
|
|
129
130
|
},
|
|
130
131
|
|
|
131
|
-
respondChallenge(options) {
|
|
132
|
+
async respondChallenge(options) {
|
|
132
133
|
const { challenge, error, input } = options
|
|
133
134
|
|
|
134
135
|
if (options.html && new URL(input.url).searchParams.has(Html.serviceWorkerParam))
|
|
@@ -145,38 +146,60 @@ export function http(): Http {
|
|
|
145
146
|
'Cache-Control': 'no-store',
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
const body = (() => {
|
|
149
|
+
const body = await (async () => {
|
|
149
150
|
if (options.html && input.headers.get('Accept')?.includes('text/html')) {
|
|
150
151
|
headers['Content-Type'] = 'text/html; charset=utf-8'
|
|
151
|
-
|
|
152
|
+
|
|
153
|
+
const theme = Html.mergeDefined(
|
|
154
|
+
{
|
|
155
|
+
favicon: undefined as Html.Theme['favicon'],
|
|
156
|
+
fontUrl: undefined as Html.Theme['fontUrl'],
|
|
157
|
+
logo: undefined as Html.Theme['logo'],
|
|
158
|
+
...Html.defaultTheme,
|
|
159
|
+
},
|
|
160
|
+
(options.html.theme as never) ?? {},
|
|
161
|
+
)
|
|
162
|
+
const text = Html.sanitizeRecord(
|
|
163
|
+
Html.mergeDefined(Html.defaultText, (options.html.text as never) ?? {}),
|
|
164
|
+
)
|
|
165
|
+
const amount = await options.html.formatAmount(challenge.request)
|
|
166
|
+
|
|
152
167
|
return html`<!doctype html>
|
|
153
168
|
<html lang="en">
|
|
154
169
|
<head>
|
|
155
170
|
<meta charset="UTF-8" />
|
|
156
171
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
</style>
|
|
172
|
+
<meta name="robots" content="noindex" />
|
|
173
|
+
<meta name="color-scheme" content="${theme.colorScheme}" />
|
|
174
|
+
<title>${text.title}</title>
|
|
175
|
+
${Html.favicon(theme, challenge.realm)} ${Html.font(theme)} ${Html.style(theme)}
|
|
163
176
|
</head>
|
|
164
177
|
<body>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
${
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
<main>
|
|
179
|
+
<header class="${Html.classNames.header}">
|
|
180
|
+
${Html.logo(theme)}
|
|
181
|
+
<span>${text.paymentRequired}</span>
|
|
182
|
+
</header>
|
|
183
|
+
<section class="${Html.classNames.summary}" aria-label="Payment summary">
|
|
184
|
+
<h1 class="${Html.classNames.summaryAmount}">${Html.sanitize(amount)}</h1>
|
|
185
|
+
${challenge.description
|
|
186
|
+
? `<p class="${Html.classNames.summaryDescription}">${Html.sanitize(challenge.description)}</p>`
|
|
187
|
+
: ''}
|
|
188
|
+
${challenge.expires
|
|
189
|
+
? `<p class="${Html.classNames.summaryExpires}">${text.expires} <time datetime="${new Date(challenge.expires).toISOString()}">${new Date(challenge.expires).toLocaleString()}</time></p>`
|
|
190
|
+
: ''}
|
|
191
|
+
</section>
|
|
192
|
+
<div id="${Html.rootId}" aria-label="Payment form"></div>
|
|
193
|
+
<script id="${Html.dataId}" type="application/json">
|
|
194
|
+
${Json.stringify({
|
|
195
|
+
config: options.html.config,
|
|
196
|
+
challenge,
|
|
197
|
+
text,
|
|
198
|
+
theme,
|
|
199
|
+
} satisfies Html.Data).replace(/</g, '\\u003c')}
|
|
200
|
+
</script>
|
|
201
|
+
${options.html.content}
|
|
202
|
+
</main>
|
|
180
203
|
</body>
|
|
181
204
|
</html> `
|
|
182
205
|
}
|