mppx 0.8.13 → 0.8.14
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 +16 -0
- package/dist/Errors.d.ts +10 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +11 -2
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +105 -3
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +102 -2
- package/dist/Method.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -2
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +17 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +11 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/server/Mppx.d.ts +23 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +91 -82
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +2 -1
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +38 -77
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +289 -209
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +12 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +48 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +177 -0
- package/dist/tempo/server/Relay.js.map +1 -0
- package/dist/tempo/server/SponsorBudget.d.ts +59 -0
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
- package/dist/tempo/server/SponsorBudget.js +144 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- 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/client/ChannelOps.d.ts +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +9 -4
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +5 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +37 -19
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +8 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
- package/dist/tempo/session/client/Runtime.js +17 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +15 -5
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +93 -36
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +8 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +54 -26
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +7 -5
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +48 -24
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +96 -8
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/precompile/Channel.d.ts +2 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Channel.js +4 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -1
- package/dist/tempo/session/server/RequestState.js +3 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/package.json +1 -1
- package/src/Errors.test.ts +23 -0
- package/src/Errors.ts +21 -2
- package/src/Method.test.ts +102 -1
- package/src/Method.ts +241 -5
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.ts +16 -7
- package/src/client/internal/MethodChallenge.ts +30 -0
- package/src/server/Mppx.test-d.ts +2 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/tempo/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +435 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +523 -0
- package/src/tempo/server/Relay.ts +288 -0
- package/src/tempo/server/SponsorBudget.test.ts +125 -0
- package/src/tempo/server/SponsorBudget.ts +213 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/ChannelOps.test.ts +17 -2
- package/src/tempo/session/client/ChannelOps.ts +12 -4
- package/src/tempo/session/client/CredentialState.ts +61 -20
- package/src/tempo/session/client/Runtime.test.ts +12 -0
- package/src/tempo/session/client/Runtime.ts +22 -0
- package/src/tempo/session/client/Session.test.ts +235 -2
- package/src/tempo/session/client/Session.ts +130 -41
- package/src/tempo/session/client/SessionManager.test.ts +102 -4
- package/src/tempo/session/client/SessionManager.ts +71 -27
- package/src/tempo/session/client/Transports.test.ts +32 -1
- package/src/tempo/session/client/Transports.ts +79 -35
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +197 -10
- package/src/tempo/session/precompile/Chain.ts +102 -8
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/RequestState.test.ts +8 -1
- package/src/tempo/session/server/RequestState.ts +2 -2
package/src/server/Mppx.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as Expires from '../Expires.js'
|
|
|
9
9
|
import * as AcceptPayment from '../internal/AcceptPayment.js'
|
|
10
10
|
import * as Env from '../internal/env.js'
|
|
11
11
|
import type { MaybePromise } from '../internal/types.js'
|
|
12
|
-
import
|
|
12
|
+
import * as Method from '../Method.js'
|
|
13
13
|
import * as PaymentRequest from '../PaymentRequest.js'
|
|
14
14
|
import type * as Receipt from '../Receipt.js'
|
|
15
15
|
import * as x402_Header from '../x402/Header.js'
|
|
@@ -248,14 +248,18 @@ export type Mppx<
|
|
|
248
248
|
/**
|
|
249
249
|
* Verify a credential string or object end-to-end: deserialize,
|
|
250
250
|
* HMAC-check, match to a registered method, validate payload schema,
|
|
251
|
-
* check expiry, and call the method's verify function.
|
|
251
|
+
* check expiry, and call the method's broadcast or legacy verify function.
|
|
252
252
|
*
|
|
253
|
-
* Method verification can
|
|
253
|
+
* Method verification can broadcast payments and persist state. For example,
|
|
254
254
|
* subscription credentials may activate or renew a subscription. Failed
|
|
255
255
|
* standalone verification emits `payment.failed` once a credential challenge
|
|
256
256
|
* can be parsed; strings that cannot be deserialized have no challenge
|
|
257
257
|
* context to report.
|
|
258
258
|
*
|
|
259
|
+
* @deprecated Use `broadcastCredential()` instead. This legacy name maps
|
|
260
|
+
* to the same mutating operation and does not provide validation-only
|
|
261
|
+
* semantics.
|
|
262
|
+
*
|
|
259
263
|
* @example
|
|
260
264
|
* ```ts
|
|
261
265
|
* const receipt = await mppx.verifyCredential('eyJjaGFsbGVuZ2...')
|
|
@@ -267,6 +271,27 @@ export type Mppx<
|
|
|
267
271
|
credential: string | Credential.Credential,
|
|
268
272
|
options?: VerifyCredentialOptions | undefined,
|
|
269
273
|
): Promise<Receipt.Receipt>
|
|
274
|
+
/**
|
|
275
|
+
* Validate a credential without consuming or broadcasting it.
|
|
276
|
+
*
|
|
277
|
+
* This is an advisory pre-check. Methods that support it must not write
|
|
278
|
+
* replay state, sign fee-payer transactions, broadcast, or otherwise
|
|
279
|
+
* mutate payment state. Use `broadcastCredential()` when accepting payment.
|
|
280
|
+
*/
|
|
281
|
+
validateCredential(
|
|
282
|
+
credential: string | Credential.Credential,
|
|
283
|
+
options?: VerifyCredentialOptions | undefined,
|
|
284
|
+
): Promise<Method.Validation>
|
|
285
|
+
/**
|
|
286
|
+
* Re-validates and broadcasts a credential, returning a receipt.
|
|
287
|
+
*
|
|
288
|
+
* `verifyCredential()` is retained as a backwards-compatible alias for
|
|
289
|
+
* this mutating path.
|
|
290
|
+
*/
|
|
291
|
+
broadcastCredential(
|
|
292
|
+
credential: string | Credential.Credential,
|
|
293
|
+
options?: VerifyCredentialOptions | undefined,
|
|
294
|
+
): Promise<Receipt.Receipt>
|
|
270
295
|
}
|
|
271
296
|
|
|
272
297
|
/** Extracts the transport override from a method, if any. */
|
|
@@ -292,7 +317,9 @@ const reservedMppxKeyValues = [
|
|
|
292
317
|
'onPaymentFailed',
|
|
293
318
|
'onPaymentSuccess',
|
|
294
319
|
'realm',
|
|
320
|
+
'broadcastCredential',
|
|
295
321
|
'transport',
|
|
322
|
+
'validateCredential',
|
|
296
323
|
'verifyCredential',
|
|
297
324
|
] as const
|
|
298
325
|
|
|
@@ -457,9 +484,11 @@ export function create<
|
|
|
457
484
|
preflight: mi.preflight as never,
|
|
458
485
|
request: mi.request as never,
|
|
459
486
|
respond: mi.respond as never,
|
|
487
|
+
broadcast: mi.broadcast as never,
|
|
460
488
|
secretKey,
|
|
461
489
|
stableBinding: mi.stableBinding as never,
|
|
462
490
|
transport: (mi.transport ?? transport) as never,
|
|
491
|
+
validate: mi.validate as never,
|
|
463
492
|
verify: mi.verify as never,
|
|
464
493
|
})
|
|
465
494
|
const wireKey = `${mi.name}/${mi.intent}`
|
|
@@ -496,125 +525,118 @@ export function create<
|
|
|
496
525
|
})
|
|
497
526
|
}
|
|
498
527
|
|
|
499
|
-
|
|
500
|
-
async function verifyCredentialFn(
|
|
528
|
+
async function prepareStandaloneCredential(
|
|
501
529
|
input: string | Credential.Credential,
|
|
502
|
-
options
|
|
503
|
-
|
|
530
|
+
options: VerifyCredentialOptions | undefined,
|
|
531
|
+
parameters: { emitFailures?: boolean | undefined; requireValidate?: boolean | undefined } = {},
|
|
532
|
+
) {
|
|
504
533
|
const credential = hydrateCredentialMeta(
|
|
505
534
|
typeof input === 'string' ? Credential.deserialize(input) : input,
|
|
506
535
|
)
|
|
507
536
|
|
|
508
|
-
|
|
509
|
-
// when multiple handlers intentionally share the same wire identity.
|
|
537
|
+
const emitFailures = parameters.emitFailures === true
|
|
510
538
|
const { method: credMethod, intent: credIntent } = credential.challenge
|
|
511
539
|
const methodCandidates = (methods as readonly Method.AnyServer[]).filter(
|
|
512
540
|
(m) => m.name === credMethod && m.intent === credIntent,
|
|
513
541
|
)
|
|
514
|
-
const mi =
|
|
542
|
+
const mi = Method.selectServerMethod(methodCandidates, credential.challenge)
|
|
515
543
|
const eventMethod =
|
|
516
544
|
mi ?? ({ intent: credIntent, name: credMethod } satisfies ServerMethodDescriptor)
|
|
517
545
|
|
|
518
|
-
const emitStandalonePaymentFailed = async (
|
|
546
|
+
const emitStandalonePaymentFailed = async (failure: {
|
|
519
547
|
challenge: Challenge.Challenge
|
|
520
548
|
credential: Credential.Credential | null
|
|
521
549
|
error: Errors.PaymentError
|
|
522
550
|
request: Record<string, unknown>
|
|
523
551
|
submittedChallenge?: Challenge.Challenge | undefined
|
|
524
552
|
}) => {
|
|
553
|
+
if (!emitFailures) return
|
|
525
554
|
await serverEvents.emit(
|
|
526
555
|
'payment.failed',
|
|
527
556
|
createPaymentFailedContext({
|
|
528
557
|
capturedRequest: options?.capturedRequest,
|
|
529
|
-
challenge:
|
|
530
|
-
credential:
|
|
531
|
-
error:
|
|
558
|
+
challenge: failure.challenge,
|
|
559
|
+
credential: failure.credential,
|
|
560
|
+
error: failure.error,
|
|
532
561
|
method: eventMethod,
|
|
533
|
-
request:
|
|
534
|
-
submittedChallenge:
|
|
562
|
+
request: failure.request,
|
|
563
|
+
submittedChallenge: failure.submittedChallenge,
|
|
535
564
|
}) as never,
|
|
536
565
|
)
|
|
537
566
|
}
|
|
538
567
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
568
|
+
const fail = async (
|
|
569
|
+
error: Errors.PaymentError,
|
|
570
|
+
failure: {
|
|
571
|
+
credential?: Credential.Credential | null | undefined
|
|
572
|
+
request?: Record<string, unknown> | undefined
|
|
573
|
+
submittedChallenge?: Challenge.Challenge | undefined
|
|
574
|
+
} = {},
|
|
575
|
+
thrown: unknown = error,
|
|
576
|
+
): Promise<never> => {
|
|
544
577
|
await emitStandalonePaymentFailed({
|
|
545
578
|
challenge: credential.challenge,
|
|
546
|
-
credential,
|
|
579
|
+
credential: failure.credential ?? credential,
|
|
547
580
|
error,
|
|
548
|
-
request: credential.challenge.request as Record<string, unknown
|
|
549
|
-
submittedChallenge: credential.challenge,
|
|
581
|
+
request: failure.request ?? (credential.challenge.request as Record<string, unknown>),
|
|
582
|
+
submittedChallenge: failure.submittedChallenge ?? credential.challenge,
|
|
550
583
|
})
|
|
551
|
-
throw
|
|
584
|
+
throw thrown
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!mi) {
|
|
588
|
+
await fail(
|
|
589
|
+
new Errors.InvalidChallengeError({
|
|
590
|
+
id: credential.challenge.id,
|
|
591
|
+
reason: `no registered method for ${credMethod}/${credIntent}`,
|
|
592
|
+
}),
|
|
593
|
+
)
|
|
552
594
|
}
|
|
553
595
|
|
|
554
|
-
|
|
596
|
+
if (parameters.requireValidate && !mi.validate)
|
|
597
|
+
await fail(
|
|
598
|
+
new Errors.VerificationFailedError({
|
|
599
|
+
details: { intent: credIntent, method: credMethod },
|
|
600
|
+
reason: `${credMethod}/${credIntent} does not support non-mutating credential validation`,
|
|
601
|
+
}),
|
|
602
|
+
)
|
|
603
|
+
|
|
555
604
|
if (!Challenge.verify(credential.challenge, { secretKey: secretKey! })) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
credential,
|
|
563
|
-
error,
|
|
564
|
-
request: credential.challenge.request as Record<string, unknown>,
|
|
565
|
-
submittedChallenge: credential.challenge,
|
|
566
|
-
})
|
|
567
|
-
throw error
|
|
605
|
+
await fail(
|
|
606
|
+
new Errors.InvalidChallengeError({
|
|
607
|
+
id: credential.challenge.id,
|
|
608
|
+
reason: 'challenge was not issued by this server',
|
|
609
|
+
}),
|
|
610
|
+
)
|
|
568
611
|
}
|
|
569
612
|
|
|
570
|
-
// Expiry check
|
|
571
613
|
try {
|
|
572
614
|
Expires.assert(credential.challenge.expires, credential.challenge.id)
|
|
573
615
|
} catch (e) {
|
|
574
|
-
if (e instanceof Errors.PaymentError)
|
|
575
|
-
await emitStandalonePaymentFailed({
|
|
576
|
-
challenge: credential.challenge,
|
|
577
|
-
credential,
|
|
578
|
-
error: e,
|
|
579
|
-
request: credential.challenge.request as Record<string, unknown>,
|
|
580
|
-
submittedChallenge: credential.challenge,
|
|
581
|
-
})
|
|
616
|
+
if (e instanceof Errors.PaymentError) await fail(e)
|
|
582
617
|
throw e
|
|
583
618
|
}
|
|
584
619
|
|
|
585
|
-
|
|
586
|
-
let parsedCredential: Credential.Credential
|
|
620
|
+
let parsedCredential!: Credential.Credential
|
|
587
621
|
try {
|
|
588
622
|
parsedCredential = withParsedCredentialPayload(
|
|
589
623
|
credential,
|
|
590
624
|
mi.schema.credential.payload.parse(credential.payload),
|
|
591
625
|
)
|
|
592
626
|
} catch (e) {
|
|
593
|
-
await
|
|
594
|
-
challenge: credential.challenge,
|
|
595
|
-
credential,
|
|
596
|
-
error: new Errors.InvalidPayloadError(),
|
|
597
|
-
request: credential.challenge.request as Record<string, unknown>,
|
|
598
|
-
submittedChallenge: credential.challenge,
|
|
599
|
-
})
|
|
600
|
-
throw e
|
|
627
|
+
await fail(new Errors.InvalidPayloadError(), {}, e)
|
|
601
628
|
}
|
|
602
629
|
|
|
603
630
|
const expectedMeta = Scope.merge({ meta: options?.meta, scope: options?.scope })
|
|
604
631
|
|
|
605
632
|
if (options?.scope !== undefined && Scope.read(credential.challenge.meta) !== options.scope) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
error,
|
|
614
|
-
request: credential.challenge.request as Record<string, unknown>,
|
|
615
|
-
submittedChallenge: credential.challenge,
|
|
616
|
-
})
|
|
617
|
-
throw error
|
|
633
|
+
await fail(
|
|
634
|
+
new Errors.InvalidChallengeError({
|
|
635
|
+
id: credential.challenge.id,
|
|
636
|
+
reason: "credential scope does not match this route's requirements",
|
|
637
|
+
}),
|
|
638
|
+
{ credential: parsedCredential },
|
|
639
|
+
)
|
|
618
640
|
}
|
|
619
641
|
|
|
620
642
|
const shouldValidateRoute =
|
|
@@ -660,12 +682,9 @@ export function create<
|
|
|
660
682
|
: (credential.challenge.request as z.input<typeof mi.schema.request>)
|
|
661
683
|
} catch (e) {
|
|
662
684
|
if (e instanceof Errors.PaymentError)
|
|
663
|
-
await
|
|
664
|
-
challenge: credential.challenge,
|
|
685
|
+
await fail(e, {
|
|
665
686
|
credential: parsedCredential,
|
|
666
|
-
error: e,
|
|
667
687
|
request: credential.challenge.request as Record<string, unknown>,
|
|
668
|
-
submittedChallenge: credential.challenge,
|
|
669
688
|
})
|
|
670
689
|
throw e
|
|
671
690
|
}
|
|
@@ -679,17 +698,72 @@ export function create<
|
|
|
679
698
|
} as Method.VerifiedChallengeEnvelope)
|
|
680
699
|
: undefined
|
|
681
700
|
|
|
701
|
+
return {
|
|
702
|
+
credential,
|
|
703
|
+
envelope,
|
|
704
|
+
eventMethod,
|
|
705
|
+
method: mi,
|
|
706
|
+
parsedCredential,
|
|
707
|
+
parsedRequest,
|
|
708
|
+
request,
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function validateCredentialFn(
|
|
713
|
+
input: string | Credential.Credential,
|
|
714
|
+
options?: VerifyCredentialOptions,
|
|
715
|
+
): Promise<Method.Validation> {
|
|
716
|
+
const prepared = await prepareStandaloneCredential(input, options, { requireValidate: true })
|
|
717
|
+
return prepared.method.validate!({
|
|
718
|
+
credential: prepared.parsedCredential,
|
|
719
|
+
envelope: prepared.envelope,
|
|
720
|
+
request: prepared.request,
|
|
721
|
+
} as never)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// broadcastCredential: single-call end-to-end validation and broadcast
|
|
725
|
+
async function broadcastCredentialFn(
|
|
726
|
+
input: string | Credential.Credential,
|
|
727
|
+
options?: VerifyCredentialOptions,
|
|
728
|
+
): Promise<Receipt.Receipt> {
|
|
729
|
+
const prepared = await prepareStandaloneCredential(input, options, { emitFailures: true })
|
|
730
|
+
const { method: mi, parsedCredential, parsedRequest, request, envelope } = prepared
|
|
731
|
+
|
|
732
|
+
const emitStandalonePaymentFailed = async (parameters: {
|
|
733
|
+
challenge: Challenge.Challenge
|
|
734
|
+
credential: Credential.Credential | null
|
|
735
|
+
error: Errors.PaymentError
|
|
736
|
+
request: Record<string, unknown>
|
|
737
|
+
submittedChallenge?: Challenge.Challenge | undefined
|
|
738
|
+
}) => {
|
|
739
|
+
await serverEvents.emit(
|
|
740
|
+
'payment.failed',
|
|
741
|
+
createPaymentFailedContext({
|
|
742
|
+
capturedRequest: options?.capturedRequest,
|
|
743
|
+
challenge: parameters.challenge,
|
|
744
|
+
credential: parameters.credential,
|
|
745
|
+
error: parameters.error,
|
|
746
|
+
method: prepared.eventMethod,
|
|
747
|
+
request: parameters.request,
|
|
748
|
+
submittedChallenge: parameters.submittedChallenge,
|
|
749
|
+
}) as never,
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
682
753
|
let receipt: Receipt.Receipt
|
|
683
754
|
try {
|
|
684
|
-
|
|
755
|
+
if (mi.broadcast && mi.validate)
|
|
756
|
+
await mi.validate({ credential: parsedCredential, envelope, request } as never)
|
|
757
|
+
const broadcast = mi.broadcast ?? mi.verify
|
|
758
|
+
receipt = await broadcast({ credential: parsedCredential, envelope, request } as never)
|
|
685
759
|
} catch (e) {
|
|
686
760
|
const error = e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError()
|
|
687
761
|
await emitStandalonePaymentFailed({
|
|
688
|
-
challenge: credential.challenge,
|
|
762
|
+
challenge: prepared.credential.challenge,
|
|
689
763
|
credential: parsedCredential,
|
|
690
764
|
error,
|
|
691
765
|
request: parsedRequest,
|
|
692
|
-
submittedChallenge: credential.challenge,
|
|
766
|
+
submittedChallenge: prepared.credential.challenge,
|
|
693
767
|
})
|
|
694
768
|
throw e
|
|
695
769
|
}
|
|
@@ -698,7 +772,7 @@ export function create<
|
|
|
698
772
|
'payment.success',
|
|
699
773
|
createPaymentSuccessContext({
|
|
700
774
|
capturedRequest: options?.capturedRequest,
|
|
701
|
-
challenge: credential.challenge,
|
|
775
|
+
challenge: prepared.credential.challenge,
|
|
702
776
|
credential: parsedCredential,
|
|
703
777
|
envelope,
|
|
704
778
|
method: mi,
|
|
@@ -710,6 +784,8 @@ export function create<
|
|
|
710
784
|
return receipt
|
|
711
785
|
}
|
|
712
786
|
|
|
787
|
+
const verifyCredentialFn = broadcastCredentialFn
|
|
788
|
+
|
|
713
789
|
function composeFn(
|
|
714
790
|
...entries: readonly [
|
|
715
791
|
Method.AnyServer | AnyMethodFnWithMethod | string,
|
|
@@ -760,7 +836,9 @@ export function create<
|
|
|
760
836
|
onPaymentFailed,
|
|
761
837
|
onPaymentSuccess,
|
|
762
838
|
realm: realm as string | undefined,
|
|
839
|
+
broadcastCredential: broadcastCredentialFn,
|
|
763
840
|
transport,
|
|
841
|
+
validateCredential: validateCredentialFn,
|
|
764
842
|
verifyCredential: verifyCredentialFn,
|
|
765
843
|
...handlers,
|
|
766
844
|
} as never
|
|
@@ -809,8 +887,10 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
809
887
|
realm,
|
|
810
888
|
respond,
|
|
811
889
|
secretKey,
|
|
890
|
+
broadcast,
|
|
812
891
|
stableBinding,
|
|
813
892
|
transport,
|
|
893
|
+
validate,
|
|
814
894
|
verify,
|
|
815
895
|
} = parameters
|
|
816
896
|
|
|
@@ -1256,7 +1336,14 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
1256
1336
|
// If verification fails, re-issue the challenge so the client can retry.
|
|
1257
1337
|
let receiptData: Receipt.Receipt
|
|
1258
1338
|
try {
|
|
1259
|
-
|
|
1339
|
+
if (broadcast && validate)
|
|
1340
|
+
await validate({ credential: parsedCredential, envelope, request } as never)
|
|
1341
|
+
const broadcastCredential = broadcast ?? verify
|
|
1342
|
+
receiptData = await broadcastCredential({
|
|
1343
|
+
credential: parsedCredential,
|
|
1344
|
+
envelope,
|
|
1345
|
+
request,
|
|
1346
|
+
} as never)
|
|
1260
1347
|
} catch (e) {
|
|
1261
1348
|
if (!(e instanceof Errors.PaymentError))
|
|
1262
1349
|
console.error('mppx: internal verification error', e)
|
|
@@ -1375,9 +1462,11 @@ declare namespace createMethodFn {
|
|
|
1375
1462
|
realm: string | undefined
|
|
1376
1463
|
request?: Method.RequestFn<method>
|
|
1377
1464
|
respond?: Method.RespondFn<method>
|
|
1465
|
+
broadcast?: Method.BroadcastFn<method>
|
|
1378
1466
|
secretKey: string
|
|
1379
1467
|
stableBinding?: Method.StableBindingFn<method>
|
|
1380
1468
|
transport: transport
|
|
1469
|
+
validate?: Method.ValidateFn<method>
|
|
1381
1470
|
verify: Method.VerifyFn<method>
|
|
1382
1471
|
}
|
|
1383
1472
|
|
|
@@ -1843,31 +1932,6 @@ type PinnedRequestBindingField = (typeof pinnedRequestBindingFields)[number]
|
|
|
1843
1932
|
type PinnedChallengeField = 'method' | 'intent' | 'realm' | 'opaque' | PinnedRequestBindingField
|
|
1844
1933
|
type StableBinding = Record<string, unknown>
|
|
1845
1934
|
|
|
1846
|
-
function selectVerificationMethod(
|
|
1847
|
-
methods: readonly Method.AnyServer[],
|
|
1848
|
-
challenge: Challenge.Challenge,
|
|
1849
|
-
): Method.AnyServer | undefined {
|
|
1850
|
-
if (methods.length <= 1) return methods[0]
|
|
1851
|
-
if (
|
|
1852
|
-
challenge.method !== Constants.Methods.tempo ||
|
|
1853
|
-
challenge.intent !== Constants.Intents.session
|
|
1854
|
-
)
|
|
1855
|
-
return methods[0]
|
|
1856
|
-
|
|
1857
|
-
const sessionProtocolMarker = Constants.getMethodDetail(
|
|
1858
|
-
challenge.request.methodDetails,
|
|
1859
|
-
Constants.MethodDetailKeys.sessionProtocol,
|
|
1860
|
-
)
|
|
1861
|
-
if (
|
|
1862
|
-
sessionProtocolMarker === undefined ||
|
|
1863
|
-
sessionProtocolMarker === Constants.SessionProtocols.v1
|
|
1864
|
-
)
|
|
1865
|
-
return methods.find((method) => method.alias === 'sessionLegacy') ?? methods[0]
|
|
1866
|
-
if (sessionProtocolMarker === Constants.SessionProtocols.v2)
|
|
1867
|
-
return methods.find((method) => method.alias === undefined) ?? methods[0]
|
|
1868
|
-
return undefined
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
1935
|
function getChallengeBindingMismatch(
|
|
1872
1936
|
expectedChallenge: Challenge.Challenge,
|
|
1873
1937
|
actualChallenge: Challenge.Challenge,
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
SessionManagerWebSocketOptions as ClientSessionManagerWebSocketOptions,
|
|
9
9
|
} from './client/index.js'
|
|
10
10
|
import * as Tempo from './index.js'
|
|
11
|
+
import type { charge as ServerCharge } from './server/Charge.js'
|
|
11
12
|
import type { SettlementSchedule as ServerSettlementSchedule } from './server/index.js'
|
|
12
13
|
import type {
|
|
13
14
|
ActiveSessionState,
|
|
@@ -31,6 +32,25 @@ test('tempo session public barrels expose manager and schedule interfaces', () =
|
|
|
31
32
|
expectTypeOf(Tempo.Session.Precompile).toBeObject()
|
|
32
33
|
expectTypeOf(Tempo.Session.Server).toBeObject()
|
|
33
34
|
expectTypeOf<typeof Tempo>().not.toHaveProperty('Precompile')
|
|
35
|
+
expectTypeOf<serverTempo.RelayOptions>().toEqualTypeOf<ServerCharge.RelayOptions>()
|
|
36
|
+
expectTypeOf<serverTempo.RelayErrorCode>().toEqualTypeOf<
|
|
37
|
+
| 'already_used'
|
|
38
|
+
| 'broadcast_failed'
|
|
39
|
+
| 'expired'
|
|
40
|
+
| 'invalid_payment'
|
|
41
|
+
| 'insufficient_funds'
|
|
42
|
+
| 'policy_denied'
|
|
43
|
+
| 'screen_rejected'
|
|
44
|
+
| 'simulation_failed'
|
|
45
|
+
| 'temporarily_unavailable'
|
|
46
|
+
| 'unsupported'
|
|
47
|
+
| 'unknown'
|
|
48
|
+
>()
|
|
49
|
+
expectTypeOf<serverTempo.RelayErrorDetails>().toEqualTypeOf<
|
|
50
|
+
| { code: 'already_used' | 'broadcast_failed' | 'insufficient_funds' | 'invalid_payment' }
|
|
51
|
+
| { code: 'simulation_failed' | 'unsupported' }
|
|
52
|
+
| { code: 'temporarily_unavailable'; retry: 'same_credential' }
|
|
53
|
+
>()
|
|
34
54
|
|
|
35
55
|
expectTypeOf<ClientPaymentResponse>().toEqualTypeOf<SessionPaymentResponse>()
|
|
36
56
|
expectTypeOf<ClientSessionManager>().toEqualTypeOf<SessionManager>()
|
|
@@ -636,14 +636,13 @@ export function assertTransactionPolicy(parameters: {
|
|
|
636
636
|
maxFeePerGas: maxFeePerGasValue.toString(),
|
|
637
637
|
})
|
|
638
638
|
|
|
639
|
-
const maxTotalFee = gasLimit * maxFeePerGasValue
|
|
639
|
+
const maxTotalFee = BigInt(gasLimit) * BigInt(maxFeePerGasValue)
|
|
640
640
|
if (maxTotalFee > policy.maxTotalFee)
|
|
641
641
|
fail('fee-sponsored transaction total fee budget exceeds sponsor policy', {
|
|
642
642
|
gas: gasLimit.toString(),
|
|
643
643
|
maxFeePerGas: maxFeePerGasValue.toString(),
|
|
644
644
|
totalFee: maxTotalFee.toString(),
|
|
645
645
|
})
|
|
646
|
-
|
|
647
646
|
if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas > maxFeePerGasValue)
|
|
648
647
|
fail('fee-sponsored transaction maxPriorityFeePerGas exceeds maxFeePerGas', {
|
|
649
648
|
maxFeePerGas: maxFeePerGasValue.toString(),
|
|
@@ -677,7 +676,12 @@ export function assertTransactionPolicy(parameters: {
|
|
|
677
676
|
validBefore: String(validBeforeValue),
|
|
678
677
|
})
|
|
679
678
|
|
|
680
|
-
return {
|
|
679
|
+
return {
|
|
680
|
+
gasLimit,
|
|
681
|
+
maxFeePerGasValue,
|
|
682
|
+
totalFee: maxTotalFee,
|
|
683
|
+
validBeforeValue,
|
|
684
|
+
}
|
|
681
685
|
}
|
|
682
686
|
|
|
683
687
|
export function prepareSponsoredTransaction(parameters: {
|
|
@@ -245,6 +245,13 @@ describe.runIf(isLocalnet)('createOpenPayload', () => {
|
|
|
245
245
|
expect(result.payload).toHaveProperty('transaction')
|
|
246
246
|
expect(result.payload).toHaveProperty('signature')
|
|
247
247
|
expect(result.payload.channelId).toBe(result.entry.channelId)
|
|
248
|
+
|
|
249
|
+
if (result.payload.action !== 'open') throw new Error('unexpected action')
|
|
250
|
+
const transaction = Transaction.deserialize(result.payload.transaction)
|
|
251
|
+
if (!('nonceKey' in transaction)) throw new Error('unexpected transaction type')
|
|
252
|
+
expect(transaction.nonce).toBe(0)
|
|
253
|
+
expect(transaction.nonceKey).toBe((1n << 256n) - 1n)
|
|
254
|
+
expect((transaction as { validBefore?: bigint }).validBefore).toBeDefined()
|
|
248
255
|
})
|
|
249
256
|
|
|
250
257
|
test('defaults authorizedSigner to account.address', async () => {
|
|
@@ -192,7 +192,8 @@ export async function createOpenPayload(
|
|
|
192
192
|
{ to: escrowContract, data: openData },
|
|
193
193
|
],
|
|
194
194
|
feeToken: currency,
|
|
195
|
-
|
|
195
|
+
nonceKey: 'expiring',
|
|
196
|
+
validBefore,
|
|
196
197
|
} as never)
|
|
197
198
|
// Estimate before enabling fee-payer mode so Tempo includes sender
|
|
198
199
|
// signature and access-key verification costs in the gas budget.
|