mppx 0.5.0 → 0.5.3
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 +19 -0
- package/dist/Credential.d.ts +12 -0
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js +22 -4
- package/dist/Credential.js.map +1 -1
- package/dist/Method.d.ts +4 -0
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +2 -1
- package/dist/Method.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +12 -2
- package/dist/cli/account.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +52 -8
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +7 -3
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +90 -71
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +5 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +52 -7
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/internal/html/config.d.ts +7 -0
- package/dist/server/internal/html/config.d.ts.map +1 -0
- package/dist/server/internal/html/config.js +3 -0
- package/dist/server/internal/html/config.js.map +1 -0
- package/dist/server/internal/html/serviceWorker.gen.d.ts +2 -0
- package/dist/server/internal/html/serviceWorker.gen.d.ts.map +1 -0
- package/dist/server/internal/html/serviceWorker.gen.js +3 -0
- package/dist/server/internal/html/serviceWorker.gen.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts +5 -0
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +14 -6
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +2 -0
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -0
- package/dist/stripe/server/internal/html.gen.js +3 -0
- package/dist/stripe/server/internal/html.gen.js.map +1 -0
- package/dist/tempo/internal/proof.d.ts +6 -0
- package/dist/tempo/internal/proof.d.ts.map +1 -1
- package/dist/tempo/internal/proof.js +15 -0
- package/dist/tempo/internal/proof.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +10 -3
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +38 -10
- 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 +3 -2
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +2 -0
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -0
- package/dist/tempo/server/internal/html.gen.js +3 -0
- package/dist/tempo/server/internal/html.gen.js.map +1 -0
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +45 -58
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/Credential.ts +28 -4
- package/src/Method.ts +6 -1
- package/src/cli/account.ts +13 -2
- package/src/env.d.ts +1 -0
- package/src/mcp-sdk/server/Transport.test.ts +6 -0
- package/src/middlewares/elysia.test.ts +3 -5
- package/src/middlewares/express.test.ts +3 -5
- package/src/middlewares/hono.test.ts +8 -5
- package/src/middlewares/nextjs.test.ts +3 -5
- package/src/proxy/Proxy.test.ts +188 -1
- package/src/proxy/Proxy.ts +58 -9
- package/src/proxy/internal/Route.test.ts +9 -0
- package/src/proxy/internal/Route.ts +5 -2
- package/src/server/Mppx.test.ts +171 -18
- package/src/server/Mppx.ts +120 -79
- package/src/server/Transport.test.ts +16 -2
- package/src/server/Transport.ts +61 -7
- package/src/server/internal/html/config.ts +8 -0
- package/src/server/internal/html/serviceWorker.client.ts +28 -0
- package/src/server/internal/html/serviceWorker.gen.ts +2 -0
- package/src/server/internal/html/serviceWorker.ts +27 -0
- package/src/server/internal/html/tsconfig.worker.client.json +8 -0
- package/src/server/internal/html/tsconfig.worker.json +8 -0
- package/src/stripe/server/Charge.ts +19 -5
- package/src/stripe/server/internal/html/main.ts +106 -0
- package/src/stripe/server/internal/html/node_modules/.bin/mppx.src +21 -0
- package/src/stripe/server/internal/html/package.json +9 -0
- package/src/stripe/server/internal/html/stripe-js-pure.d.ts +7 -0
- package/src/stripe/server/internal/html/tsconfig.json +8 -0
- package/src/stripe/server/internal/html.gen.ts +2 -0
- package/src/tempo/internal/proof.test.ts +47 -0
- package/src/tempo/internal/proof.ts +16 -0
- package/src/tempo/server/Charge.test.ts +298 -0
- package/src/tempo/server/Charge.ts +61 -12
- package/src/tempo/server/Session.ts +3 -2
- package/src/tempo/server/internal/html/main.ts +71 -0
- package/src/tempo/server/internal/html/node_modules/.bin/mppx.src +21 -0
- package/src/tempo/server/internal/html/package.json +10 -0
- package/src/tempo/server/internal/html/tsconfig.json +8 -0
- package/src/tempo/server/internal/html.gen.ts +2 -0
- package/src/tempo/server/internal/transport.test.ts +37 -31
- package/src/tempo/server/internal/transport.ts +44 -58
- package/src/tsconfig.json +1 -1
package/src/server/Mppx.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
3
|
|
|
3
4
|
import * as Challenge from '../Challenge.js'
|
|
4
5
|
import * as Credential from '../Credential.js'
|
|
@@ -300,10 +301,12 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
300
301
|
|
|
301
302
|
// Credential was provided but malformed
|
|
302
303
|
if (credentialError) {
|
|
304
|
+
const reason = getSafeCredentialReason(credentialError)
|
|
303
305
|
const response = await transport.respondChallenge({
|
|
304
306
|
challenge,
|
|
305
307
|
input,
|
|
306
|
-
error: new Errors.MalformedCredentialError({ reason:
|
|
308
|
+
error: new Errors.MalformedCredentialError(reason ? { reason } : {}),
|
|
309
|
+
html: method.html,
|
|
307
310
|
})
|
|
308
311
|
return { challenge: response, status: 402 }
|
|
309
312
|
}
|
|
@@ -314,6 +317,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
314
317
|
challenge,
|
|
315
318
|
input,
|
|
316
319
|
error: new Errors.PaymentRequiredError({ description }),
|
|
320
|
+
html: method.html,
|
|
317
321
|
})
|
|
318
322
|
return { challenge: response, status: 402 }
|
|
319
323
|
}
|
|
@@ -328,6 +332,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
328
332
|
id: credential.challenge.id,
|
|
329
333
|
reason: 'challenge was not issued by this server',
|
|
330
334
|
}),
|
|
335
|
+
html: method.html,
|
|
331
336
|
})
|
|
332
337
|
return { challenge: response, status: 402 }
|
|
333
338
|
}
|
|
@@ -339,13 +344,6 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
339
344
|
// Note: we compare specific payment parameters rather than the full
|
|
340
345
|
// request because the `request` hook may produce credential-dependent
|
|
341
346
|
// output (e.g. `feePayer` differs between 402 and credential calls).
|
|
342
|
-
//
|
|
343
|
-
// Skip this check for topUp and voucher actions: the route's
|
|
344
|
-
// `request` hook may produce a different amount because these
|
|
345
|
-
// requests carry no application body (e.g. no model field for
|
|
346
|
-
// dynamic pricing). The credential echoes a challenge obtained
|
|
347
|
-
// from the original request which had the correct amount; the
|
|
348
|
-
// on-chain voucher signature is the real validation.
|
|
349
347
|
{
|
|
350
348
|
for (const field of ['method', 'intent', 'realm'] as const) {
|
|
351
349
|
if (credential.challenge[field] !== challenge[field]) {
|
|
@@ -356,65 +354,26 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
356
354
|
id: credential.challenge.id,
|
|
357
355
|
reason: `credential ${field} does not match this route's requirements`,
|
|
358
356
|
}),
|
|
357
|
+
html: method.html,
|
|
359
358
|
})
|
|
360
359
|
return { challenge: response, status: 402 }
|
|
361
360
|
}
|
|
362
361
|
}
|
|
363
362
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
for (const field of ['amount', 'currency', 'recipient'] as const) {
|
|
379
|
-
const routeVal = routeReq[field] ?? routeDetails[field]
|
|
380
|
-
if (
|
|
381
|
-
routeVal !== undefined &&
|
|
382
|
-
String(routeVal) !== String(echoedReq[field] ?? echoedDetails[field])
|
|
383
|
-
) {
|
|
384
|
-
const response = await transport.respondChallenge({
|
|
385
|
-
challenge,
|
|
386
|
-
input,
|
|
387
|
-
error: new Errors.InvalidChallengeError({
|
|
388
|
-
id: credential.challenge.id,
|
|
389
|
-
reason: `credential ${field} does not match this route's requirements`,
|
|
390
|
-
}),
|
|
391
|
-
})
|
|
392
|
-
return { challenge: response, status: 402 }
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Compare payment-relevant methodDetails fields (memo, splits).
|
|
397
|
-
// These are excluded from the top-level field check above but
|
|
398
|
-
// affect verification semantics — a credential issued for a
|
|
399
|
-
// no-splits route must not be accepted on a splits route.
|
|
400
|
-
for (const field of ['memo', 'splits'] as const) {
|
|
401
|
-
const routeVal = routeDetails[field]
|
|
402
|
-
const echoedVal = echoedDetails[field]
|
|
403
|
-
if (
|
|
404
|
-
routeVal !== undefined &&
|
|
405
|
-
JSON.stringify(routeVal) !== JSON.stringify(echoedVal)
|
|
406
|
-
) {
|
|
407
|
-
const response = await transport.respondChallenge({
|
|
408
|
-
challenge,
|
|
409
|
-
input,
|
|
410
|
-
error: new Errors.InvalidChallengeError({
|
|
411
|
-
id: credential.challenge.id,
|
|
412
|
-
reason: `credential ${field} does not match this route's requirements`,
|
|
413
|
-
}),
|
|
414
|
-
})
|
|
415
|
-
return { challenge: response, status: 402 }
|
|
416
|
-
}
|
|
417
|
-
}
|
|
363
|
+
const mismatch = getRequestBindingMismatch(
|
|
364
|
+
challenge.request as Record<string, unknown>,
|
|
365
|
+
credential.challenge.request as Record<string, unknown>,
|
|
366
|
+
)
|
|
367
|
+
if (mismatch) {
|
|
368
|
+
const response = await transport.respondChallenge({
|
|
369
|
+
challenge,
|
|
370
|
+
input,
|
|
371
|
+
error: new Errors.InvalidChallengeError({
|
|
372
|
+
id: credential.challenge.id,
|
|
373
|
+
reason: `credential ${mismatch} does not match this route's requirements`,
|
|
374
|
+
}),
|
|
375
|
+
})
|
|
376
|
+
return { challenge: response, status: 402 }
|
|
418
377
|
}
|
|
419
378
|
}
|
|
420
379
|
|
|
@@ -432,11 +391,11 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
432
391
|
// Validate payload structure against method schema
|
|
433
392
|
try {
|
|
434
393
|
method.schema.credential.payload.parse(credential.payload)
|
|
435
|
-
} catch
|
|
394
|
+
} catch {
|
|
436
395
|
const response = await transport.respondChallenge({
|
|
437
396
|
challenge,
|
|
438
397
|
input,
|
|
439
|
-
error: new Errors.InvalidPayloadError(
|
|
398
|
+
error: new Errors.InvalidPayloadError(),
|
|
440
399
|
})
|
|
441
400
|
return { challenge: response, status: 402 }
|
|
442
401
|
}
|
|
@@ -447,10 +406,9 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
447
406
|
try {
|
|
448
407
|
receiptData = await verify({ credential, request } as never)
|
|
449
408
|
} catch (e) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
: new Errors.VerificationFailedError({ reason: (e as Error).message })
|
|
409
|
+
if (!(e instanceof Errors.PaymentError))
|
|
410
|
+
console.error('mppx: internal verification error', e)
|
|
411
|
+
const error = e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError()
|
|
454
412
|
const response = await transport.respondChallenge({
|
|
455
413
|
challenge,
|
|
456
414
|
input,
|
|
@@ -473,6 +431,8 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
473
431
|
withReceipt<response>(response?: response) {
|
|
474
432
|
if (managementResponse) {
|
|
475
433
|
return transport.respondReceipt({
|
|
434
|
+
credential,
|
|
435
|
+
input,
|
|
476
436
|
receipt: receiptData,
|
|
477
437
|
response: managementResponse as never,
|
|
478
438
|
challengeId: credential.challenge.id,
|
|
@@ -480,6 +440,8 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
480
440
|
}
|
|
481
441
|
if (!response) throw new Error('withReceipt() requires a response argument')
|
|
482
442
|
return transport.respondReceipt({
|
|
443
|
+
credential,
|
|
444
|
+
input,
|
|
483
445
|
receipt: receiptData,
|
|
484
446
|
response: response as never,
|
|
485
447
|
challengeId: credential.challenge.id,
|
|
@@ -501,6 +463,13 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
501
463
|
}
|
|
502
464
|
}
|
|
503
465
|
|
|
466
|
+
function getSafeCredentialReason(error: unknown): string | undefined {
|
|
467
|
+
if (error instanceof Credential.InvalidCredentialEncodingError) return error.message
|
|
468
|
+
if (error instanceof Credential.MissingAuthorizationHeaderError) return error.message
|
|
469
|
+
if (error instanceof Credential.MissingPaymentSchemeError) return error.message
|
|
470
|
+
return undefined
|
|
471
|
+
}
|
|
472
|
+
|
|
504
473
|
declare namespace createMethodFn {
|
|
505
474
|
type Parameters<
|
|
506
475
|
method extends Method.Method = Method.Method,
|
|
@@ -552,6 +521,88 @@ function resolveRealmFromRequest(input: unknown): string {
|
|
|
552
521
|
return defaultRealm
|
|
553
522
|
}
|
|
554
523
|
|
|
524
|
+
type RequestBindingField = 'amount' | 'currency' | 'recipient' | 'chainId' | 'memo' | 'splits'
|
|
525
|
+
|
|
526
|
+
const requestBindingFields = [
|
|
527
|
+
'amount',
|
|
528
|
+
'currency',
|
|
529
|
+
'recipient',
|
|
530
|
+
'chainId',
|
|
531
|
+
'memo',
|
|
532
|
+
'splits',
|
|
533
|
+
] as const satisfies readonly RequestBindingField[]
|
|
534
|
+
|
|
535
|
+
type RequestBinding = Partial<Record<RequestBindingField, unknown>>
|
|
536
|
+
|
|
537
|
+
function getRequestBindingMismatch(
|
|
538
|
+
expectedRequest: Record<string, unknown>,
|
|
539
|
+
actualRequest: Record<string, unknown>,
|
|
540
|
+
): RequestBindingField | undefined {
|
|
541
|
+
const expected = getRequestBinding(expectedRequest)
|
|
542
|
+
const actual = getRequestBinding(actualRequest)
|
|
543
|
+
|
|
544
|
+
return requestBindingFields.find(
|
|
545
|
+
(field) => !requestBindingValuesMatch(field, expected[field], actual[field]),
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function getRequestBinding(request: Record<string, unknown>): RequestBinding {
|
|
550
|
+
const methodDetails = (request.methodDetails ?? {}) as Record<string, unknown>
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
amount: request.amount ?? methodDetails.amount,
|
|
554
|
+
currency: request.currency ?? methodDetails.currency,
|
|
555
|
+
recipient: request.recipient ?? methodDetails.recipient,
|
|
556
|
+
chainId: request.chainId ?? methodDetails.chainId,
|
|
557
|
+
memo: methodDetails.memo,
|
|
558
|
+
splits: methodDetails.splits,
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function requestBindingValuesMatch(
|
|
563
|
+
field: RequestBindingField,
|
|
564
|
+
expected: unknown,
|
|
565
|
+
actual: unknown,
|
|
566
|
+
): boolean {
|
|
567
|
+
return isDeepStrictEqual(
|
|
568
|
+
normalizeRequestBindingValue(field, expected),
|
|
569
|
+
normalizeRequestBindingValue(field, actual),
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function normalizeRequestBindingValue(field: RequestBindingField, value: unknown): unknown {
|
|
574
|
+
switch (field) {
|
|
575
|
+
case 'memo':
|
|
576
|
+
return normalizeHex(value)
|
|
577
|
+
case 'splits':
|
|
578
|
+
return normalizeComparable(value)
|
|
579
|
+
default:
|
|
580
|
+
return normalizeScalar(value)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function normalizeScalar(value: unknown): string | undefined {
|
|
585
|
+
return value === undefined ? undefined : String(value)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function normalizeHex(value: unknown): unknown {
|
|
589
|
+
return typeof value === 'string' && value.startsWith('0x') ? value.toLowerCase() : value
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function normalizeComparable(value: unknown): unknown {
|
|
593
|
+
if (Array.isArray(value)) return value.map(normalizeComparable)
|
|
594
|
+
|
|
595
|
+
if (value && typeof value === 'object') {
|
|
596
|
+
return Object.fromEntries(
|
|
597
|
+
Object.entries(value as Record<string, unknown>)
|
|
598
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
599
|
+
.map(([key, nested]) => [key, normalizeComparable(nested)]),
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return normalizeHex(value)
|
|
604
|
+
}
|
|
605
|
+
|
|
555
606
|
export type MethodFn<
|
|
556
607
|
method extends Method.Method,
|
|
557
608
|
transport extends Transport.AnyTransport,
|
|
@@ -668,7 +719,6 @@ export function compose(
|
|
|
668
719
|
if (credential) {
|
|
669
720
|
const { method: credMethod, intent: credIntent } = credential.challenge
|
|
670
721
|
const credReq = credential.challenge.request as Record<string, unknown>
|
|
671
|
-
const credDetails = (credReq.methodDetails ?? {}) as Record<string, unknown>
|
|
672
722
|
|
|
673
723
|
// Filter by name+intent, then narrow by comparing stable request fields
|
|
674
724
|
// from the echoed challenge against each handler's canonical request.
|
|
@@ -680,16 +730,7 @@ export function compose(
|
|
|
680
730
|
if (!meta || meta.name !== credMethod || meta.intent !== credIntent) return false
|
|
681
731
|
const canonical = meta._canonicalRequest
|
|
682
732
|
if (!canonical) return true
|
|
683
|
-
|
|
684
|
-
for (const field of ['amount', 'currency', 'recipient', 'chainId'] as const) {
|
|
685
|
-
const canonicalVal = canonical[field] ?? canonicalDetails[field]
|
|
686
|
-
if (
|
|
687
|
-
canonicalVal !== undefined &&
|
|
688
|
-
String(canonicalVal) !== String(credReq[field] ?? credDetails[field])
|
|
689
|
-
)
|
|
690
|
-
return false
|
|
691
|
-
}
|
|
692
|
-
return true
|
|
733
|
+
return !getRequestBindingMismatch(canonical, credReq)
|
|
693
734
|
})
|
|
694
735
|
|
|
695
736
|
const match =
|
|
@@ -135,6 +135,8 @@ describe('http', () => {
|
|
|
135
135
|
const originalResponse = new Response('OK', { status: 200 })
|
|
136
136
|
|
|
137
137
|
const response = transport.respondReceipt({
|
|
138
|
+
credential,
|
|
139
|
+
input: new Request('https://example.com'),
|
|
138
140
|
receipt,
|
|
139
141
|
response: originalResponse,
|
|
140
142
|
challengeId: challenge.id,
|
|
@@ -252,7 +254,13 @@ describe('mcp', () => {
|
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
expect(
|
|
255
|
-
transport.respondReceipt({
|
|
257
|
+
transport.respondReceipt({
|
|
258
|
+
credential,
|
|
259
|
+
input: mcpRequest,
|
|
260
|
+
receipt,
|
|
261
|
+
response: successResponse,
|
|
262
|
+
challengeId: challenge.id,
|
|
263
|
+
}),
|
|
256
264
|
).toMatchInlineSnapshot(`
|
|
257
265
|
{
|
|
258
266
|
"id": 1,
|
|
@@ -285,7 +293,13 @@ describe('mcp', () => {
|
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
expect(
|
|
288
|
-
transport.respondReceipt({
|
|
296
|
+
transport.respondReceipt({
|
|
297
|
+
credential,
|
|
298
|
+
input: mcpRequest,
|
|
299
|
+
receipt,
|
|
300
|
+
response: errorResponse,
|
|
301
|
+
challengeId: challenge.id,
|
|
302
|
+
}),
|
|
289
303
|
).toBe(errorResponse)
|
|
290
304
|
})
|
|
291
305
|
})
|
package/src/server/Transport.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { Json } from 'ox'
|
|
2
|
+
|
|
1
3
|
import * as Challenge from '../Challenge.js'
|
|
2
4
|
import * as Credential from '../Credential.js'
|
|
3
5
|
import * as Errors from '../Errors.js'
|
|
4
6
|
import type { Distribute, UnionToIntersection } from '../internal/types.js'
|
|
5
7
|
import * as core_Mcp from '../Mcp.js'
|
|
6
8
|
import * as Receipt from '../Receipt.js'
|
|
9
|
+
import * as Html from './internal/html/config.js'
|
|
10
|
+
import { serviceWorker } from './internal/html/serviceWorker.gen.js'
|
|
7
11
|
|
|
8
12
|
export { type McpSdk, mcpSdk } from '../mcp-sdk/server/Transport.js'
|
|
9
13
|
|
|
@@ -30,11 +34,14 @@ export type Transport<
|
|
|
30
34
|
respondChallenge: (options: {
|
|
31
35
|
challenge: Challenge.Challenge
|
|
32
36
|
error?: Errors.PaymentError | undefined
|
|
37
|
+
html?: Html.Options | undefined
|
|
33
38
|
input: input
|
|
34
39
|
}) => challengeOutput | Promise<challengeOutput>
|
|
35
40
|
/** Attaches a receipt to a successful response. */
|
|
36
41
|
respondReceipt: (options: {
|
|
37
42
|
challengeId: string
|
|
43
|
+
credential: Credential.Credential
|
|
44
|
+
input: input
|
|
38
45
|
receipt: Receipt.Receipt
|
|
39
46
|
response: receiptResponse
|
|
40
47
|
}) => receiptOutput
|
|
@@ -87,7 +94,7 @@ export type WithReceipt<transport extends AnyTransport = Http> = WithReceiptOver
|
|
|
87
94
|
* name: 'custom',
|
|
88
95
|
* getCredential(input) { ... },
|
|
89
96
|
* respondChallenge({ challenge, input }) { ... },
|
|
90
|
-
* respondReceipt({ receipt, response, challengeId }) { ... },
|
|
97
|
+
* respondReceipt({ receipt, response, challengeId, credential, input }) { ... },
|
|
91
98
|
* })
|
|
92
99
|
* ```
|
|
93
100
|
*/
|
|
@@ -121,17 +128,64 @@ export function http(): Http {
|
|
|
121
128
|
return Credential.deserialize(payment)
|
|
122
129
|
},
|
|
123
130
|
|
|
124
|
-
respondChallenge(
|
|
131
|
+
respondChallenge(options) {
|
|
132
|
+
const { challenge, error, input } = options
|
|
133
|
+
|
|
134
|
+
if (options.html && new URL(input.url).searchParams.has(Html.serviceWorkerParam))
|
|
135
|
+
return new Response(serviceWorker, {
|
|
136
|
+
status: 200,
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/javascript',
|
|
139
|
+
'Cache-Control': 'no-store',
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
125
143
|
const headers: Record<string, string> = {
|
|
126
144
|
'WWW-Authenticate': Challenge.serialize(challenge),
|
|
127
145
|
'Cache-Control': 'no-store',
|
|
128
146
|
}
|
|
129
147
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
const body = (() => {
|
|
149
|
+
if (options.html && input.headers.get('Accept')?.includes('text/html')) {
|
|
150
|
+
headers['Content-Type'] = 'text/html; charset=utf-8'
|
|
151
|
+
const html = String.raw
|
|
152
|
+
return html`<!doctype html>
|
|
153
|
+
<html lang="en">
|
|
154
|
+
<head>
|
|
155
|
+
<meta charset="UTF-8" />
|
|
156
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
157
|
+
<title>Payment Required</title>
|
|
158
|
+
<style>
|
|
159
|
+
:root {
|
|
160
|
+
color-scheme: dark light;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
163
|
+
</head>
|
|
164
|
+
<body>
|
|
165
|
+
<h1>Payment Required</h1>
|
|
166
|
+
<pre>
|
|
167
|
+
${Json.stringify(challenge, null, 2)
|
|
168
|
+
.replace(/&/g, '&')
|
|
169
|
+
.replace(/</g, '<')
|
|
170
|
+
.replace(/>/g, '>')}</pre
|
|
171
|
+
>
|
|
172
|
+
<div id="root"></div>
|
|
173
|
+
<script id="${Html.dataId}" type="application/json">
|
|
174
|
+
${Json.stringify({ config: options.html.config, challenge }).replace(
|
|
175
|
+
/</g,
|
|
176
|
+
'\\u003c',
|
|
177
|
+
)}
|
|
178
|
+
</script>
|
|
179
|
+
${options.html.content}
|
|
180
|
+
</body>
|
|
181
|
+
</html> `
|
|
182
|
+
}
|
|
183
|
+
if (error) {
|
|
184
|
+
headers['Content-Type'] = 'application/problem+json'
|
|
185
|
+
return JSON.stringify(error.toProblemDetails(challenge.id))
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
})()
|
|
135
189
|
|
|
136
190
|
return new Response(body, { status: error?.status ?? 402, headers })
|
|
137
191
|
},
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { serviceWorkerParam } from './config.js'
|
|
2
|
+
|
|
3
|
+
export async function submitCredential(credential: string): Promise<void> {
|
|
4
|
+
const url = new URL(location.href)
|
|
5
|
+
url.searchParams.set(serviceWorkerParam, '')
|
|
6
|
+
|
|
7
|
+
const registration = await navigator.serviceWorker.register(url.pathname + url.search)
|
|
8
|
+
|
|
9
|
+
const serviceWorker = await new Promise<ServiceWorker>((resolve) => {
|
|
10
|
+
const mppxWorker = registration.installing ?? registration.waiting ?? registration.active
|
|
11
|
+
if (mppxWorker?.state === 'activated') return resolve(mppxWorker)
|
|
12
|
+
const target = mppxWorker ?? registration
|
|
13
|
+
target.addEventListener('statechange', function handler() {
|
|
14
|
+
const active = registration.active
|
|
15
|
+
if (active?.state === 'activated') {
|
|
16
|
+
target.removeEventListener('statechange', handler)
|
|
17
|
+
resolve(active)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
await new Promise<void>((resolve) => {
|
|
23
|
+
const channel = new MessageChannel()
|
|
24
|
+
channel.port1.onmessage = () => resolve()
|
|
25
|
+
serviceWorker.postMessage({ credential }, [channel.port2])
|
|
26
|
+
})
|
|
27
|
+
location.reload()
|
|
28
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated — do not edit.
|
|
2
|
+
export const serviceWorker = "(function(){let e=self,t;e.addEventListener(`activate`,t=>{t.waitUntil(e.clients.claim())}),e.addEventListener(`message`,e=>{if(!e.source)return;let n=e.data?.credential;typeof n!=`string`||!n.startsWith(`Payment `)||(t=n,e.ports[0]?.postMessage(`ack`))}),e.addEventListener(`fetch`,n=>{if(!t||n.request.mode!==`navigate`||new URL(n.request.url).origin!==e.location.origin)return;let r=new Headers(n.request.headers);r.set(`Authorization`,t),t=void 0,n.respondWith(fetch(n.request,{headers:r})),e.registration.unregister()})})();"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const serviceWorker = self as unknown as ServiceWorkerGlobalScope
|
|
2
|
+
|
|
3
|
+
let credential: string | undefined
|
|
4
|
+
|
|
5
|
+
serviceWorker.addEventListener('activate', (event) => {
|
|
6
|
+
event.waitUntil(serviceWorker.clients.claim())
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
serviceWorker.addEventListener('message', (event) => {
|
|
10
|
+
if (!event.source) return
|
|
11
|
+
const value = event.data?.credential
|
|
12
|
+
if (typeof value !== 'string' || !value.startsWith('Payment ')) return
|
|
13
|
+
credential = value
|
|
14
|
+
event.ports[0]?.postMessage('ack')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
serviceWorker.addEventListener('fetch', (event) => {
|
|
18
|
+
if (!credential || event.request.mode !== 'navigate') return
|
|
19
|
+
if (new URL(event.request.url).origin !== serviceWorker.location.origin) return
|
|
20
|
+
|
|
21
|
+
const headers = new Headers(event.request.headers)
|
|
22
|
+
headers.set('Authorization', credential)
|
|
23
|
+
credential = undefined
|
|
24
|
+
|
|
25
|
+
event.respondWith(fetch(event.request, { headers }))
|
|
26
|
+
serviceWorker.registration.unregister()
|
|
27
|
+
})
|
|
@@ -5,6 +5,7 @@ import type { LooseOmit, OneOf } from '../../internal/types.js'
|
|
|
5
5
|
import * as Method from '../../Method.js'
|
|
6
6
|
import type { StripeClient } from '../internal/types.js'
|
|
7
7
|
import * as Methods from '../Methods.js'
|
|
8
|
+
import { html as htmlContent } from './internal/html.gen.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Creates a Stripe charge method intent for usage on the server.
|
|
@@ -38,6 +39,7 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
|
|
|
38
39
|
decimals,
|
|
39
40
|
description,
|
|
40
41
|
externalId,
|
|
42
|
+
html,
|
|
41
43
|
metadata,
|
|
42
44
|
networkId,
|
|
43
45
|
paymentMethodTypes,
|
|
@@ -59,9 +61,11 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
|
|
|
59
61
|
paymentMethodTypes,
|
|
60
62
|
} as unknown as Defaults,
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
html: html ? { config: html, content: htmlContent } : undefined,
|
|
65
|
+
|
|
66
|
+
async verify({ credential, request }) {
|
|
63
67
|
const { challenge } = credential
|
|
64
|
-
const
|
|
68
|
+
const resolvedRequest = Methods.charge.schema.request.parse(request)
|
|
65
69
|
|
|
66
70
|
Expires.assert(challenge.expires, challenge.id)
|
|
67
71
|
|
|
@@ -72,15 +76,23 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
|
|
|
72
76
|
externalId?: string
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
const userMetadata =
|
|
79
|
+
const userMetadata = resolvedRequest.methodDetails?.metadata as
|
|
80
|
+
| Record<string, string>
|
|
81
|
+
| undefined
|
|
76
82
|
const resolvedMetadata = { ...buildAnalytics({ credential }), ...userMetadata }
|
|
77
83
|
|
|
78
84
|
const pi = client
|
|
79
|
-
? await createWithClient({
|
|
85
|
+
? await createWithClient({
|
|
86
|
+
client,
|
|
87
|
+
challenge,
|
|
88
|
+
request: resolvedRequest,
|
|
89
|
+
spt,
|
|
90
|
+
metadata: resolvedMetadata,
|
|
91
|
+
})
|
|
80
92
|
: await createWithSecretKey({
|
|
81
93
|
secretKey: secretKey!,
|
|
82
94
|
challenge,
|
|
83
|
-
request,
|
|
95
|
+
request: resolvedRequest,
|
|
84
96
|
spt,
|
|
85
97
|
metadata: resolvedMetadata,
|
|
86
98
|
})
|
|
@@ -108,6 +120,8 @@ export declare namespace charge {
|
|
|
108
120
|
type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.charge>, 'recipient'>
|
|
109
121
|
|
|
110
122
|
type Parameters = {
|
|
123
|
+
/** Render payment page when Accept header is text/html (e.g. in browsers) */
|
|
124
|
+
html?: { createTokenUrl: string; publishableKey: string } | undefined
|
|
111
125
|
/** Optional metadata to include in SPT creation requests. */
|
|
112
126
|
metadata?: Record<string, string> | undefined
|
|
113
127
|
} & Defaults &
|