mppx 0.6.18 → 0.6.20
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 +13 -0
- package/dist/Challenge.d.ts +2 -2
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +34 -0
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +3 -1
- package/dist/Method.js.map +1 -1
- package/dist/Receipt.d.ts +1 -0
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +2 -0
- package/dist/Receipt.js.map +1 -1
- package/dist/client/Methods.d.ts +1 -0
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +1 -0
- package/dist/client/Methods.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +14 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +1 -2
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +14 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +14 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +2 -2
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +1 -1
- package/dist/proxy/Service.js.map +1 -1
- package/dist/server/Mppx.d.ts +15 -3
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +190 -40
- package/dist/server/Mppx.js.map +1 -1
- 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/Methods.d.ts +96 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +97 -0
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +3 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Methods.js +3 -0
- package/dist/tempo/client/Methods.js.map +1 -1
- package/dist/tempo/client/Subscription.d.ts +114 -0
- package/dist/tempo/client/Subscription.d.ts.map +1 -0
- package/dist/tempo/client/Subscription.js +100 -0
- package/dist/tempo/client/Subscription.js.map +1 -0
- package/dist/tempo/client/index.d.ts +1 -0
- package/dist/tempo/client/index.d.ts.map +1 -1
- package/dist/tempo/client/index.js +1 -0
- package/dist/tempo/client/index.js.map +1 -1
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/index.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/Methods.d.ts +5 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +5 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +221 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -0
- package/dist/tempo/server/Subscription.js +637 -0
- package/dist/tempo/server/Subscription.js.map +1 -0
- package/dist/tempo/server/index.d.ts +1 -0
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +1 -0
- package/dist/tempo/server/index.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/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +3 -4
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +282 -0
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -0
- package/dist/tempo/subscription/KeyAuthorization.js +297 -0
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -0
- package/dist/tempo/subscription/Receipt.d.ts +10 -0
- package/dist/tempo/subscription/Receipt.d.ts.map +1 -0
- package/dist/tempo/subscription/Receipt.js +16 -0
- package/dist/tempo/subscription/Receipt.js.map +1 -0
- package/dist/tempo/subscription/Store.d.ts +99 -0
- package/dist/tempo/subscription/Store.d.ts.map +1 -0
- package/dist/tempo/subscription/Store.js +292 -0
- package/dist/tempo/subscription/Store.js.map +1 -0
- package/dist/tempo/subscription/Types.d.ts +65 -0
- package/dist/tempo/subscription/Types.d.ts.map +1 -0
- package/dist/tempo/subscription/Types.js +2 -0
- package/dist/tempo/subscription/Types.js.map +1 -0
- package/dist/tempo/subscription/index.d.ts +6 -0
- package/dist/tempo/subscription/index.d.ts.map +1 -0
- package/dist/tempo/subscription/index.js +4 -0
- package/dist/tempo/subscription/index.js.map +1 -0
- package/dist/zod.d.ts +7 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +18 -0
- package/dist/zod.js.map +1 -1
- package/package.json +3 -3
- package/src/Challenge.test.ts +13 -0
- package/src/Challenge.ts +3 -3
- package/src/Method.ts +46 -1
- package/src/Receipt.ts +2 -0
- package/src/client/Methods.ts +1 -0
- package/src/middlewares/elysia.test.ts +31 -1
- package/src/middlewares/elysia.ts +13 -0
- package/src/middlewares/express.ts +1 -5
- package/src/middlewares/hono.test.ts +30 -1
- package/src/middlewares/hono.ts +13 -0
- package/src/middlewares/nextjs.test.ts +28 -1
- package/src/middlewares/nextjs.ts +13 -0
- package/src/proxy/Proxy.ts +2 -5
- package/src/proxy/Service.test.ts +34 -0
- package/src/proxy/Service.ts +7 -0
- package/src/server/Mppx.authorize.test.ts +210 -0
- package/src/server/Mppx.test-d.ts +23 -1
- package/src/server/Mppx.test.ts +73 -3
- package/src/server/Mppx.ts +291 -58
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/Methods.test.ts +131 -0
- package/src/tempo/Methods.ts +136 -0
- package/src/tempo/Subscription.integration.test.ts +591 -0
- package/src/tempo/client/Methods.ts +3 -0
- package/src/tempo/client/Subscription.test.ts +131 -0
- package/src/tempo/client/Subscription.ts +155 -0
- package/src/tempo/client/index.ts +1 -0
- package/src/tempo/index.ts +1 -0
- package/src/tempo/server/Charge.ts +2 -2
- package/src/tempo/server/Methods.ts +5 -0
- package/src/tempo/server/Subscription.test.ts +1410 -0
- package/src/tempo/server/Subscription.ts +1014 -0
- package/src/tempo/server/index.ts +1 -0
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/Chain.ts +3 -5
- package/src/tempo/subscription/KeyAuthorization.test.ts +204 -0
- package/src/tempo/subscription/KeyAuthorization.ts +394 -0
- package/src/tempo/subscription/Receipt.ts +28 -0
- package/src/tempo/subscription/Store.test.ts +554 -0
- package/src/tempo/subscription/Store.ts +431 -0
- package/src/tempo/subscription/Types.ts +68 -0
- package/src/tempo/subscription/index.ts +23 -0
- package/src/zod.test.ts +23 -1
- package/src/zod.ts +24 -0
package/src/server/Mppx.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as Env from '../internal/env.js'
|
|
|
10
10
|
import type * as Method from '../Method.js'
|
|
11
11
|
import * as PaymentRequest from '../PaymentRequest.js'
|
|
12
12
|
import type * as Receipt from '../Receipt.js'
|
|
13
|
-
import
|
|
13
|
+
import * as z from '../zod.js'
|
|
14
14
|
import * as Html from './internal/html/config.js'
|
|
15
15
|
import { serviceWorker } from './internal/html/serviceWorker.gen.js'
|
|
16
16
|
import * as Scope from './internal/scope.js'
|
|
@@ -53,6 +53,8 @@ export type Mppx<
|
|
|
53
53
|
* server methods passed to `Mppx.create()`, looked up by `name`+`intent`.
|
|
54
54
|
*
|
|
55
55
|
* Only available on HTTP transports.
|
|
56
|
+
* No-credential authorize hooks run in entry order; the first 200 response
|
|
57
|
+
* wins, and earlier hooks may have already run side effects.
|
|
56
58
|
*
|
|
57
59
|
* @example
|
|
58
60
|
* ```ts
|
|
@@ -105,6 +107,9 @@ export type Mppx<
|
|
|
105
107
|
* HMAC-check, match to a registered method, validate payload schema,
|
|
106
108
|
* check expiry, and call the method's verify function.
|
|
107
109
|
*
|
|
110
|
+
* Method verification can settle payments and persist state. For example,
|
|
111
|
+
* subscription credentials may activate or renew a subscription.
|
|
112
|
+
*
|
|
108
113
|
* @example
|
|
109
114
|
* ```ts
|
|
110
115
|
* const receipt = await mppx.verifyCredential('eyJjaGFsbGVuZ2...')
|
|
@@ -237,12 +242,14 @@ export function create<
|
|
|
237
242
|
for (const mi of methods) {
|
|
238
243
|
intentCount[mi.intent] = (intentCount[mi.intent] ?? 0) + 1
|
|
239
244
|
handlers[`${mi.name}/${mi.intent}`] = createMethodFn({
|
|
245
|
+
authorize: mi.authorize as never,
|
|
240
246
|
defaults: mi.defaults,
|
|
241
247
|
method: mi,
|
|
242
248
|
realm,
|
|
243
249
|
request: mi.request as never,
|
|
244
250
|
respond: mi.respond as never,
|
|
245
251
|
secretKey,
|
|
252
|
+
stableBinding: mi.stableBinding as never,
|
|
246
253
|
transport: (mi.transport ?? transport) as never,
|
|
247
254
|
verify: mi.verify as never,
|
|
248
255
|
})
|
|
@@ -339,7 +346,11 @@ export function create<
|
|
|
339
346
|
routeRequest: options?.request ?? {},
|
|
340
347
|
secretKey: secretKey!,
|
|
341
348
|
}).then((resolved) => {
|
|
342
|
-
const mismatch =
|
|
349
|
+
const mismatch = getChallengeBindingMismatch(
|
|
350
|
+
resolved.challenge,
|
|
351
|
+
credential.challenge,
|
|
352
|
+
mi.stableBinding as never,
|
|
353
|
+
)
|
|
343
354
|
if (mismatch)
|
|
344
355
|
throw new Errors.InvalidChallengeError({
|
|
345
356
|
id: credential.challenge.id,
|
|
@@ -421,7 +432,17 @@ function createMethodFn<
|
|
|
421
432
|
): createMethodFn.ReturnType<method, transport, defaults>
|
|
422
433
|
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
423
434
|
function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.ReturnType {
|
|
424
|
-
const {
|
|
435
|
+
const {
|
|
436
|
+
authorize,
|
|
437
|
+
defaults,
|
|
438
|
+
method,
|
|
439
|
+
realm,
|
|
440
|
+
respond,
|
|
441
|
+
secretKey,
|
|
442
|
+
stableBinding,
|
|
443
|
+
transport,
|
|
444
|
+
verify,
|
|
445
|
+
} = parameters
|
|
425
446
|
|
|
426
447
|
return (options) => {
|
|
427
448
|
const { description, meta, scope, ...rest } = options
|
|
@@ -430,7 +451,9 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
430
451
|
return Object.assign(
|
|
431
452
|
async (input: Transport.InputOf): Promise<MethodFn.Response> => {
|
|
432
453
|
const expires =
|
|
433
|
-
'expires' in options
|
|
454
|
+
'expires' in options
|
|
455
|
+
? normalizeExpires(options.expires as z.DatetimeInput | undefined)
|
|
456
|
+
: Expires.minutes(5)
|
|
434
457
|
const capturedRequest = await captureRequest(transport, input)
|
|
435
458
|
const effectiveMeta =
|
|
436
459
|
scope === undefined && input instanceof globalThis.Request
|
|
@@ -446,7 +469,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
446
469
|
return [null, e as Error] as const
|
|
447
470
|
}
|
|
448
471
|
})()
|
|
449
|
-
const
|
|
472
|
+
const routeChallenge = await resolveRouteChallenge({
|
|
450
473
|
capturedRequest,
|
|
451
474
|
credential,
|
|
452
475
|
defaults,
|
|
@@ -458,7 +481,29 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
458
481
|
request: parameters.request,
|
|
459
482
|
routeRequest: rest,
|
|
460
483
|
secretKey,
|
|
484
|
+
}).catch(async (e) => {
|
|
485
|
+
if (!(e instanceof Errors.PaymentError)) throw e
|
|
486
|
+
const challenge = createFallbackChallenge({
|
|
487
|
+
capturedRequest,
|
|
488
|
+
defaults: defaults ?? {},
|
|
489
|
+
description,
|
|
490
|
+
expires,
|
|
491
|
+
meta: effectiveMeta,
|
|
492
|
+
method,
|
|
493
|
+
realm,
|
|
494
|
+
routeRequest: rest,
|
|
495
|
+
secretKey,
|
|
496
|
+
})
|
|
497
|
+
const response = await transport.respondChallenge({
|
|
498
|
+
challenge,
|
|
499
|
+
input,
|
|
500
|
+
error: e,
|
|
501
|
+
html: method.html,
|
|
502
|
+
})
|
|
503
|
+
return { response }
|
|
461
504
|
})
|
|
505
|
+
if ('response' in routeChallenge) return { challenge: routeChallenge.response, status: 402 }
|
|
506
|
+
const { challenge, request } = routeChallenge
|
|
462
507
|
|
|
463
508
|
// Credential was provided but malformed
|
|
464
509
|
if (credentialError) {
|
|
@@ -472,8 +517,77 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
472
517
|
return { challenge: response, status: 402 }
|
|
473
518
|
}
|
|
474
519
|
|
|
520
|
+
const success = (
|
|
521
|
+
receiptData: Receipt.Receipt,
|
|
522
|
+
options: {
|
|
523
|
+
challengeId?: string | undefined
|
|
524
|
+
credentialForReceipt?: Credential.Credential | undefined
|
|
525
|
+
envelopeForReceipt?: Method.VerifiedChallengeEnvelope | undefined
|
|
526
|
+
managementResponse?: globalThis.Response | undefined
|
|
527
|
+
} = {},
|
|
528
|
+
): MethodFn.Response => {
|
|
529
|
+
const {
|
|
530
|
+
challengeId = challenge.id,
|
|
531
|
+
credentialForReceipt = { challenge, payload: {} } as Credential.Credential,
|
|
532
|
+
envelopeForReceipt,
|
|
533
|
+
managementResponse,
|
|
534
|
+
} = options
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
status: 200,
|
|
538
|
+
withReceipt<response>(response?: response) {
|
|
539
|
+
if (managementResponse) {
|
|
540
|
+
return transport.respondReceipt({
|
|
541
|
+
challengeId,
|
|
542
|
+
credential: credentialForReceipt,
|
|
543
|
+
...(envelopeForReceipt ? { envelope: envelopeForReceipt } : {}),
|
|
544
|
+
input,
|
|
545
|
+
receipt: receiptData,
|
|
546
|
+
response: managementResponse as never,
|
|
547
|
+
}) as response
|
|
548
|
+
}
|
|
549
|
+
if (!response) throw new MissingReceiptResponseError()
|
|
550
|
+
return transport.respondReceipt({
|
|
551
|
+
challengeId,
|
|
552
|
+
credential: credentialForReceipt,
|
|
553
|
+
...(envelopeForReceipt ? { envelope: envelopeForReceipt } : {}),
|
|
554
|
+
input,
|
|
555
|
+
receipt: receiptData,
|
|
556
|
+
response: response as never,
|
|
557
|
+
}) as response
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
475
562
|
// No credential provided—issue challenge
|
|
476
563
|
if (!credential) {
|
|
564
|
+
if (authorize && input instanceof globalThis.Request) {
|
|
565
|
+
try {
|
|
566
|
+
const authorized = await authorize({
|
|
567
|
+
challenge,
|
|
568
|
+
input,
|
|
569
|
+
request: challenge.request,
|
|
570
|
+
} as never)
|
|
571
|
+
if (authorized) {
|
|
572
|
+
return success(authorized.receipt, {
|
|
573
|
+
managementResponse: authorized.response,
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
} catch (e) {
|
|
577
|
+
if (!(e instanceof Errors.PaymentError))
|
|
578
|
+
console.error('mppx: internal authorization error', e)
|
|
579
|
+
const error =
|
|
580
|
+
e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError()
|
|
581
|
+
const response = await transport.respondChallenge({
|
|
582
|
+
challenge,
|
|
583
|
+
input,
|
|
584
|
+
error,
|
|
585
|
+
html: method.html,
|
|
586
|
+
})
|
|
587
|
+
return { challenge: response, status: 402 }
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
477
591
|
const response = await transport.respondChallenge({
|
|
478
592
|
challenge,
|
|
479
593
|
input,
|
|
@@ -530,7 +644,11 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
530
644
|
// `expires` still is not pinned here because its default is generated
|
|
531
645
|
// per invocation, and `digest` is already bound by the echoed HMAC.
|
|
532
646
|
{
|
|
533
|
-
const mismatch =
|
|
647
|
+
const mismatch = getChallengeBindingMismatch(
|
|
648
|
+
challenge,
|
|
649
|
+
credential.challenge,
|
|
650
|
+
stableBinding as never,
|
|
651
|
+
)
|
|
534
652
|
if (mismatch) {
|
|
535
653
|
const response = await transport.respondChallenge({
|
|
536
654
|
challenge,
|
|
@@ -601,30 +719,12 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
601
719
|
? await respond({ credential, envelope, input, receipt: receiptData, request } as never)
|
|
602
720
|
: undefined
|
|
603
721
|
|
|
604
|
-
return {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
credential,
|
|
611
|
-
envelope,
|
|
612
|
-
input,
|
|
613
|
-
receipt: receiptData,
|
|
614
|
-
response: managementResponse as never,
|
|
615
|
-
}) as response
|
|
616
|
-
}
|
|
617
|
-
if (!response) throw new Error('withReceipt() requires a response argument')
|
|
618
|
-
return transport.respondReceipt({
|
|
619
|
-
challengeId: credential.challenge.id,
|
|
620
|
-
credential,
|
|
621
|
-
envelope,
|
|
622
|
-
input,
|
|
623
|
-
receipt: receiptData,
|
|
624
|
-
response: response as never,
|
|
625
|
-
}) as response
|
|
626
|
-
},
|
|
627
|
-
}
|
|
722
|
+
return success(receiptData, {
|
|
723
|
+
challengeId: credential.challenge.id,
|
|
724
|
+
credentialForReceipt: credential,
|
|
725
|
+
envelopeForReceipt: envelope,
|
|
726
|
+
managementResponse,
|
|
727
|
+
})
|
|
628
728
|
},
|
|
629
729
|
{
|
|
630
730
|
_internal: {
|
|
@@ -635,6 +735,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
635
735
|
name: method.name,
|
|
636
736
|
intent: method.intent,
|
|
637
737
|
_canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
|
|
738
|
+
_stableBinding: stableBinding as never,
|
|
638
739
|
},
|
|
639
740
|
},
|
|
640
741
|
)
|
|
@@ -658,14 +759,16 @@ function createChallengeFn(parameters: {
|
|
|
658
759
|
return async (options) => {
|
|
659
760
|
const { description, meta, scope, ...rest } = options as {
|
|
660
761
|
description?: string
|
|
661
|
-
expires?:
|
|
762
|
+
expires?: z.DatetimeInput
|
|
662
763
|
meta?: Record<string, string>
|
|
663
764
|
scope?: string
|
|
664
765
|
[key: string]: unknown
|
|
665
766
|
}
|
|
666
767
|
const effectiveMeta = Scope.merge({ meta, scope })
|
|
667
768
|
const expires =
|
|
668
|
-
'expires' in options
|
|
769
|
+
'expires' in options
|
|
770
|
+
? normalizeExpires(options.expires as z.DatetimeInput | undefined)
|
|
771
|
+
: Expires.minutes(5)
|
|
669
772
|
|
|
670
773
|
return resolveRouteChallenge({
|
|
671
774
|
defaults,
|
|
@@ -694,12 +797,14 @@ declare namespace createMethodFn {
|
|
|
694
797
|
transport extends Transport.AnyTransport = Transport.Http,
|
|
695
798
|
defaults extends Record<string, unknown> = Record<string, unknown>,
|
|
696
799
|
> = {
|
|
800
|
+
authorize?: Method.AuthorizeFn<method>
|
|
697
801
|
defaults?: defaults
|
|
698
802
|
method: method
|
|
699
803
|
realm: string | undefined
|
|
700
804
|
request?: Method.RequestFn<method>
|
|
701
805
|
respond?: Method.RespondFn<method>
|
|
702
806
|
secretKey: string
|
|
807
|
+
stableBinding?: Method.StableBindingFn<method>
|
|
703
808
|
transport: transport
|
|
704
809
|
verify: Method.VerifyFn<method>
|
|
705
810
|
}
|
|
@@ -715,6 +820,34 @@ const defaultRealm = 'MPP Payment'
|
|
|
715
820
|
const Warnings = {
|
|
716
821
|
realmFallback: 'realm-fallback',
|
|
717
822
|
} as const
|
|
823
|
+
const missingReceiptResponseErrorName = 'MissingReceiptResponseError'
|
|
824
|
+
const missingReceiptResponseErrorMessage = 'withReceipt() requires a response argument'
|
|
825
|
+
|
|
826
|
+
/** Error thrown when `withReceipt()` needs a response but none was provided. */
|
|
827
|
+
export class MissingReceiptResponseError extends Error {
|
|
828
|
+
override name = missingReceiptResponseErrorName
|
|
829
|
+
|
|
830
|
+
constructor() {
|
|
831
|
+
super(missingReceiptResponseErrorMessage)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Returns true when an error is the typed `withReceipt()` no-response sentinel. */
|
|
836
|
+
export function isMissingReceiptResponseError(
|
|
837
|
+
error: unknown,
|
|
838
|
+
): error is MissingReceiptResponseError {
|
|
839
|
+
if (error instanceof MissingReceiptResponseError) return true
|
|
840
|
+
if (!error || typeof error !== 'object') return false
|
|
841
|
+
const value = error as { message?: unknown; name?: unknown }
|
|
842
|
+
return (
|
|
843
|
+
value.name === missingReceiptResponseErrorName &&
|
|
844
|
+
value.message === missingReceiptResponseErrorMessage
|
|
845
|
+
)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function normalizeExpires(expires: z.DatetimeInput | undefined): string | undefined {
|
|
849
|
+
return expires === undefined ? undefined : z.toDatetimeString(expires)
|
|
850
|
+
}
|
|
718
851
|
|
|
719
852
|
const _warned = new Set<string>()
|
|
720
853
|
function warnOnce(key: string, message: string) {
|
|
@@ -785,6 +918,31 @@ async function resolveRouteChallenge(parameters: {
|
|
|
785
918
|
}
|
|
786
919
|
}
|
|
787
920
|
|
|
921
|
+
function createFallbackChallenge(parameters: {
|
|
922
|
+
capturedRequest?: Method.CapturedRequest | undefined
|
|
923
|
+
defaults: Record<string, unknown>
|
|
924
|
+
description?: string | undefined
|
|
925
|
+
expires?: string | undefined
|
|
926
|
+
meta?: Record<string, string> | undefined
|
|
927
|
+
method: Method.Method
|
|
928
|
+
realm?: string | undefined
|
|
929
|
+
routeRequest: Record<string, unknown>
|
|
930
|
+
secretKey: string
|
|
931
|
+
}) {
|
|
932
|
+
return Challenge.fromMethod(parameters.method, {
|
|
933
|
+
description: parameters.description,
|
|
934
|
+
expires: parameters.expires,
|
|
935
|
+
meta: parameters.meta,
|
|
936
|
+
realm:
|
|
937
|
+
parameters.realm ??
|
|
938
|
+
(parameters.capturedRequest
|
|
939
|
+
? resolveRealmFromCapturedRequest(parameters.capturedRequest)
|
|
940
|
+
: defaultRealm),
|
|
941
|
+
request: { ...parameters.defaults, ...parameters.routeRequest } as never,
|
|
942
|
+
secretKey: parameters.secretKey,
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
|
|
788
946
|
/**
|
|
789
947
|
* Captures the transport request into a frozen snapshot at the start of the
|
|
790
948
|
* verification flow. This snapshot is threaded through request() → verify() →
|
|
@@ -831,6 +989,26 @@ type CoreBindingField = (typeof coreBindingFields)[number]
|
|
|
831
989
|
type MethodBindingField = (typeof methodBindingFields)[number]
|
|
832
990
|
type PinnedRequestBindingField = (typeof pinnedRequestBindingFields)[number]
|
|
833
991
|
type PinnedChallengeField = 'method' | 'intent' | 'realm' | 'opaque' | PinnedRequestBindingField
|
|
992
|
+
type StableBinding = Record<string, unknown>
|
|
993
|
+
|
|
994
|
+
function getChallengeBindingMismatch(
|
|
995
|
+
expectedChallenge: Challenge.Challenge,
|
|
996
|
+
actualChallenge: Challenge.Challenge,
|
|
997
|
+
stableBinding?: Method.StableBindingFn<Method.Method> | undefined,
|
|
998
|
+
): string | undefined {
|
|
999
|
+
if (!stableBinding) return getPinnedChallengeMismatch(expectedChallenge, actualChallenge)
|
|
1000
|
+
|
|
1001
|
+
for (const field of ['method', 'intent', 'realm'] as const) {
|
|
1002
|
+
if (actualChallenge[field] !== expectedChallenge[field]) return field
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (!opaqueValuesMatch(expectedChallenge.meta, actualChallenge.meta)) return 'opaque'
|
|
1006
|
+
|
|
1007
|
+
return getRequestBindingMismatch(
|
|
1008
|
+
getStableBinding(expectedChallenge.request as Record<string, unknown>, stableBinding),
|
|
1009
|
+
getStableBinding(actualChallenge.request as Record<string, unknown>, stableBinding),
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
834
1012
|
|
|
835
1013
|
/**
|
|
836
1014
|
* Compares only the fields that MUST be stable across request-hook transforms.
|
|
@@ -911,6 +1089,44 @@ function getPinnedRequestBinding(request: Record<string, unknown>): PinnedReques
|
|
|
911
1089
|
}
|
|
912
1090
|
}
|
|
913
1091
|
|
|
1092
|
+
function getRequestBindingMismatch(
|
|
1093
|
+
expected: StableBinding,
|
|
1094
|
+
actual: StableBinding,
|
|
1095
|
+
): string | undefined {
|
|
1096
|
+
const fields = [
|
|
1097
|
+
...Object.keys(expected),
|
|
1098
|
+
...Object.keys(actual).filter((key) => !(key in expected)),
|
|
1099
|
+
]
|
|
1100
|
+
|
|
1101
|
+
return fields.find(
|
|
1102
|
+
(field) =>
|
|
1103
|
+
!isDeepStrictEqual(normalizeComparable(expected[field]), normalizeComparable(actual[field])),
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function getStableBinding(
|
|
1108
|
+
request: Record<string, unknown>,
|
|
1109
|
+
stableBinding: Method.StableBindingFn<Method.Method>,
|
|
1110
|
+
): StableBinding {
|
|
1111
|
+
return stableBinding(request as never)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** Top-level economic fields that should never drift after challenge issuance. */
|
|
1115
|
+
type CoreBinding = {
|
|
1116
|
+
[field in CoreBindingField]?: string
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Method-specific fields that are pinned by the fallback binding check. */
|
|
1120
|
+
type MethodBinding = {
|
|
1121
|
+
[field in MethodBindingField]?: unknown
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** Normalized request subset used when a method does not provide a custom stable binding. */
|
|
1125
|
+
type PinnedRequestBinding = {
|
|
1126
|
+
coreBinding: CoreBinding
|
|
1127
|
+
methodBinding: MethodBinding
|
|
1128
|
+
}
|
|
1129
|
+
|
|
914
1130
|
function normalizeScalar(value: unknown): string | undefined {
|
|
915
1131
|
return value === undefined ? undefined : String(value)
|
|
916
1132
|
}
|
|
@@ -957,20 +1173,6 @@ function hydrateCredentialMeta<payload>(
|
|
|
957
1173
|
},
|
|
958
1174
|
}
|
|
959
1175
|
}
|
|
960
|
-
|
|
961
|
-
type CoreBinding = {
|
|
962
|
-
[field in CoreBindingField]?: string
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
type MethodBinding = {
|
|
966
|
-
[field in MethodBindingField]?: unknown
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
type PinnedRequestBinding = {
|
|
970
|
-
coreBinding: CoreBinding
|
|
971
|
-
methodBinding: MethodBinding
|
|
972
|
-
}
|
|
973
|
-
|
|
974
1176
|
export type MethodFn<
|
|
975
1177
|
method extends Method.Method,
|
|
976
1178
|
transport extends Transport.AnyTransport,
|
|
@@ -991,8 +1193,8 @@ declare namespace MethodFn {
|
|
|
991
1193
|
> = {
|
|
992
1194
|
/** Optional human-readable description of the payment. */
|
|
993
1195
|
description?: string | undefined
|
|
994
|
-
/** Optional challenge expiration timestamp (ISO 8601). */
|
|
995
|
-
expires?:
|
|
1196
|
+
/** Optional challenge expiration timestamp (ISO 8601) or Date. */
|
|
1197
|
+
expires?: z.DatetimeInput | undefined
|
|
996
1198
|
/** Optional server-defined correlation data (serialized as `opaque` in the request). Flat string-to-string map; clients MUST NOT modify. */
|
|
997
1199
|
meta?: Record<string, string> | undefined
|
|
998
1200
|
/** Optional route/resource scope bound via reserved challenge metadata. */
|
|
@@ -1019,6 +1221,7 @@ type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transpor
|
|
|
1019
1221
|
meta?: Record<string, string> | undefined
|
|
1020
1222
|
scope?: string | undefined
|
|
1021
1223
|
_canonicalRequest: Record<string, unknown>
|
|
1224
|
+
_stableBinding?: Method.StableBindingFn<Method.Method> | undefined
|
|
1022
1225
|
}
|
|
1023
1226
|
}
|
|
1024
1227
|
|
|
@@ -1139,15 +1342,20 @@ export function compose(
|
|
|
1139
1342
|
// transformed fields (e.g. amount with decimals) match correctly.
|
|
1140
1343
|
// Also checks inside methodDetails for fields moved there by transforms.
|
|
1141
1344
|
const candidates = handlers.filter((h) => {
|
|
1142
|
-
|
|
1143
|
-
|
|
1345
|
+
try {
|
|
1346
|
+
const internal = (h as ConfiguredHandler)._internal
|
|
1347
|
+
if (!internal || internal.name !== credMethod || internal.intent !== credIntent)
|
|
1348
|
+
return false
|
|
1349
|
+
const mismatch = internal._stableBinding
|
|
1350
|
+
? getRequestBindingMismatch(
|
|
1351
|
+
getStableBinding(internal._canonicalRequest, internal._stableBinding),
|
|
1352
|
+
getStableBinding(credReq, internal._stableBinding),
|
|
1353
|
+
)
|
|
1354
|
+
: getPinnedRequestBindingMismatch(internal._canonicalRequest, credReq)
|
|
1355
|
+
return !mismatch && opaqueValuesMatch(internal.meta, credential.challenge.meta)
|
|
1356
|
+
} catch {
|
|
1144
1357
|
return false
|
|
1145
|
-
|
|
1146
|
-
if (!canonical) return true
|
|
1147
|
-
return (
|
|
1148
|
-
!getPinnedRequestBindingMismatch(canonical, credReq) &&
|
|
1149
|
-
opaqueValuesMatch(internal.meta, credential.challenge.meta)
|
|
1150
|
-
)
|
|
1358
|
+
}
|
|
1151
1359
|
})
|
|
1152
1360
|
|
|
1153
1361
|
const match =
|
|
@@ -1164,8 +1372,14 @@ export function compose(
|
|
|
1164
1372
|
return handlers[0]!(input)
|
|
1165
1373
|
}
|
|
1166
1374
|
|
|
1167
|
-
// No credential —
|
|
1168
|
-
|
|
1375
|
+
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1376
|
+
// can safely claim the request without racing each other.
|
|
1377
|
+
const results: MethodFn.Response<Transport.Http>[] = []
|
|
1378
|
+
for (const handler of handlers) {
|
|
1379
|
+
const result = await handler(input)
|
|
1380
|
+
if (result.status === 200) return result
|
|
1381
|
+
results.push(result)
|
|
1382
|
+
}
|
|
1169
1383
|
|
|
1170
1384
|
const challengeEntries = (() => {
|
|
1171
1385
|
const entries: {
|
|
@@ -1316,6 +1530,12 @@ export function toNodeListener(
|
|
|
1316
1530
|
if (result.status === 402) {
|
|
1317
1531
|
await NodeListener.sendResponse(res, result.challenge as globalThis.Response)
|
|
1318
1532
|
} else {
|
|
1533
|
+
const managementResponse = getManagementResponse(result)
|
|
1534
|
+
if (managementResponse) {
|
|
1535
|
+
await NodeListener.sendResponse(res, managementResponse)
|
|
1536
|
+
return { challenge: managementResponse, status: 402 }
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1319
1539
|
const wrapped = result.withReceipt(new globalThis.Response()) as globalThis.Response
|
|
1320
1540
|
res.setHeader('Payment-Receipt', wrapped.headers.get('Payment-Receipt')!)
|
|
1321
1541
|
}
|
|
@@ -1324,6 +1544,19 @@ export function toNodeListener(
|
|
|
1324
1544
|
}
|
|
1325
1545
|
}
|
|
1326
1546
|
|
|
1547
|
+
function getManagementResponse(
|
|
1548
|
+
result: Extract<MethodFn.Response<Transport.Http>, { status: 200 }>,
|
|
1549
|
+
): globalThis.Response | null {
|
|
1550
|
+
try {
|
|
1551
|
+
return (result.withReceipt as () => globalThis.Response)()
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (isMissingReceiptResponseError(error)) {
|
|
1554
|
+
return null
|
|
1555
|
+
}
|
|
1556
|
+
throw error
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1327
1560
|
/**
|
|
1328
1561
|
* Flattens a methods config tuple, preserving positional types.
|
|
1329
1562
|
* @internal
|