mppx 0.8.13 → 0.8.15
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 +37 -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/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +18 -4
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +98 -5
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +19 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +15 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/client/internal/MethodResponse.d.ts +15 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -1
- package/dist/client/internal/MethodResponse.js +10 -0
- package/dist/client/internal/MethodResponse.js.map +1 -1
- 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/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/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 +46 -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 +57 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +179 -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 +150 -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 +216 -45
- 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 +114 -44
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +16 -6
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +84 -37
- 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/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js +3 -0
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.js +14 -3
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +6 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- 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/cli/validate/payment.ts +18 -4
- package/src/cli/validate.test.ts +16 -0
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.test.ts +77 -0
- package/src/client/internal/Fetch.ts +122 -11
- package/src/client/internal/MethodChallenge.ts +35 -0
- package/src/client/internal/MethodResponse.ts +28 -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/stripe/server/internal/html.gen.ts +1 -1
- 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 +443 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +677 -0
- package/src/tempo/server/Relay.ts +297 -0
- package/src/tempo/server/SponsorBudget.test.ts +154 -0
- package/src/tempo/server/SponsorBudget.ts +219 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +7 -0
- 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 +506 -7
- package/src/tempo/session/client/Session.ts +279 -54
- package/src/tempo/session/client/SessionManager.test.ts +318 -13
- package/src/tempo/session/client/SessionManager.ts +132 -42
- package/src/tempo/session/client/Transports.test.ts +54 -1
- package/src/tempo/session/client/Transports.ts +137 -57
- 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/tempo/session/server/Session.test.ts +66 -4
- package/src/tempo/session/server/Ws.ts +3 -0
- package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
- package/src/tempo/subscription/KeyAuthorization.ts +14 -3
- package/src/viem/Client.test.ts +18 -1
- package/src/viem/Client.ts +7 -1
package/src/server/Mppx.test.ts
CHANGED
|
@@ -4872,6 +4872,211 @@ describe('verifyCredential', () => {
|
|
|
4872
4872
|
expect(verifyArgs).toBeDefined()
|
|
4873
4873
|
})
|
|
4874
4874
|
|
|
4875
|
+
test('validateCredential uses pure method validation without broadcast', async () => {
|
|
4876
|
+
const calls: string[] = []
|
|
4877
|
+
const splitServer = Method.toServer(mockCharge, {
|
|
4878
|
+
async validate({ credential, request }) {
|
|
4879
|
+
calls.push('validate')
|
|
4880
|
+
return {
|
|
4881
|
+
challenge: credential.challenge,
|
|
4882
|
+
credential,
|
|
4883
|
+
details: { token: credential.payload.token },
|
|
4884
|
+
intent: 'charge',
|
|
4885
|
+
method: 'alpha',
|
|
4886
|
+
request,
|
|
4887
|
+
source: credential.source,
|
|
4888
|
+
}
|
|
4889
|
+
},
|
|
4890
|
+
async broadcast() {
|
|
4891
|
+
calls.push('broadcast')
|
|
4892
|
+
return mockReceipt('broadcast')
|
|
4893
|
+
},
|
|
4894
|
+
})
|
|
4895
|
+
const mppx = Mppx.create({ methods: [splitServer], realm, secretKey })
|
|
4896
|
+
const challenge = await mppx.challenge.alpha.charge(challengeOpts)
|
|
4897
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
4898
|
+
|
|
4899
|
+
const validation = await mppx.validateCredential(credential)
|
|
4900
|
+
|
|
4901
|
+
expect(validation.details).toEqual({ token: 'valid' })
|
|
4902
|
+
expect(calls).toEqual(['validate'])
|
|
4903
|
+
})
|
|
4904
|
+
|
|
4905
|
+
test('broadcastCredential revalidates and uses method broadcast', async () => {
|
|
4906
|
+
const calls: string[] = []
|
|
4907
|
+
const splitServer = Method.toServer(mockCharge, {
|
|
4908
|
+
async validate({ credential, request }) {
|
|
4909
|
+
calls.push('validate')
|
|
4910
|
+
return {
|
|
4911
|
+
challenge: credential.challenge,
|
|
4912
|
+
credential,
|
|
4913
|
+
details: {},
|
|
4914
|
+
intent: 'charge',
|
|
4915
|
+
method: 'alpha',
|
|
4916
|
+
request,
|
|
4917
|
+
source: credential.source,
|
|
4918
|
+
}
|
|
4919
|
+
},
|
|
4920
|
+
async broadcast() {
|
|
4921
|
+
calls.push('broadcast')
|
|
4922
|
+
return mockReceipt('broadcast')
|
|
4923
|
+
},
|
|
4924
|
+
})
|
|
4925
|
+
const mppx = Mppx.create({ methods: [splitServer], realm, secretKey })
|
|
4926
|
+
const challenge = await mppx.challenge.alpha.charge(challengeOpts)
|
|
4927
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
4928
|
+
|
|
4929
|
+
const receipt = await mppx.broadcastCredential(credential)
|
|
4930
|
+
|
|
4931
|
+
expect(receipt.method).toBe('broadcast')
|
|
4932
|
+
expect(calls).toEqual(['validate', 'broadcast'])
|
|
4933
|
+
})
|
|
4934
|
+
|
|
4935
|
+
test('verifyCredential remains a legacy alias for broadcast', async () => {
|
|
4936
|
+
const calls: string[] = []
|
|
4937
|
+
const splitServer = Method.toServer(mockCharge, {
|
|
4938
|
+
async validate({ credential, request }) {
|
|
4939
|
+
calls.push('validate')
|
|
4940
|
+
return {
|
|
4941
|
+
challenge: credential.challenge,
|
|
4942
|
+
credential,
|
|
4943
|
+
details: {},
|
|
4944
|
+
intent: 'charge',
|
|
4945
|
+
method: 'alpha',
|
|
4946
|
+
request,
|
|
4947
|
+
source: credential.source,
|
|
4948
|
+
}
|
|
4949
|
+
},
|
|
4950
|
+
async broadcast() {
|
|
4951
|
+
calls.push('broadcast')
|
|
4952
|
+
return mockReceipt('broadcast')
|
|
4953
|
+
},
|
|
4954
|
+
})
|
|
4955
|
+
const mppx = Mppx.create({ methods: [splitServer], realm, secretKey })
|
|
4956
|
+
const challenge = await mppx.challenge.alpha.charge(challengeOpts)
|
|
4957
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
4958
|
+
|
|
4959
|
+
const receipt = await mppx.verifyCredential(credential)
|
|
4960
|
+
|
|
4961
|
+
expect(receipt.method).toBe('broadcast')
|
|
4962
|
+
expect(calls).toEqual(['validate', 'broadcast'])
|
|
4963
|
+
})
|
|
4964
|
+
|
|
4965
|
+
test('validateCredential rejects legacy-only methods without emitting payment failure', async () => {
|
|
4966
|
+
const events: string[] = []
|
|
4967
|
+
const mppx = Mppx.create({ methods: [alphaChargeServer], realm, secretKey })
|
|
4968
|
+
mppx.onPaymentFailed((context) => {
|
|
4969
|
+
events.push(context.error.name)
|
|
4970
|
+
})
|
|
4971
|
+
const challenge = await mppx.challenge.alpha.charge(challengeOpts)
|
|
4972
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
4973
|
+
|
|
4974
|
+
await expect(mppx.validateCredential(credential)).rejects.toThrow(
|
|
4975
|
+
'does not support non-mutating credential validation',
|
|
4976
|
+
)
|
|
4977
|
+
|
|
4978
|
+
expect(events).toEqual([])
|
|
4979
|
+
})
|
|
4980
|
+
|
|
4981
|
+
test('validateCredential enforces supplied route requirements', async () => {
|
|
4982
|
+
const splitServer = Method.toServer(mockCharge, {
|
|
4983
|
+
async validate({ credential, request }) {
|
|
4984
|
+
return {
|
|
4985
|
+
challenge: credential.challenge,
|
|
4986
|
+
credential,
|
|
4987
|
+
details: { amount: request.amount },
|
|
4988
|
+
intent: 'charge',
|
|
4989
|
+
method: 'alpha',
|
|
4990
|
+
request,
|
|
4991
|
+
source: credential.source,
|
|
4992
|
+
}
|
|
4993
|
+
},
|
|
4994
|
+
async broadcast() {
|
|
4995
|
+
return mockReceipt('broadcast')
|
|
4996
|
+
},
|
|
4997
|
+
})
|
|
4998
|
+
const mppx = Mppx.create({ methods: [splitServer], realm, secretKey })
|
|
4999
|
+
const challenge = await mppx.challenge.alpha.charge(challengeOpts)
|
|
5000
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
5001
|
+
|
|
5002
|
+
const validation = await mppx.validateCredential(credential, { request: challengeOpts })
|
|
5003
|
+
expect(validation.details).toEqual({ amount: '1000' })
|
|
5004
|
+
|
|
5005
|
+
await expect(
|
|
5006
|
+
mppx.validateCredential(credential, {
|
|
5007
|
+
request: {
|
|
5008
|
+
...challengeOpts,
|
|
5009
|
+
amount: '2000',
|
|
5010
|
+
},
|
|
5011
|
+
}),
|
|
5012
|
+
).rejects.toThrow('credential amount does not match this route')
|
|
5013
|
+
})
|
|
5014
|
+
|
|
5015
|
+
test('broadcastCredential emits payment failure when split validation fails', async () => {
|
|
5016
|
+
const calls: string[] = []
|
|
5017
|
+
const events: string[] = []
|
|
5018
|
+
const splitServer = Method.toServer(mockCharge, {
|
|
5019
|
+
async validate() {
|
|
5020
|
+
calls.push('validate')
|
|
5021
|
+
throw new Errors.VerificationFailedError({ reason: 'risk denied' })
|
|
5022
|
+
},
|
|
5023
|
+
async broadcast() {
|
|
5024
|
+
calls.push('broadcast')
|
|
5025
|
+
return mockReceipt('broadcast')
|
|
5026
|
+
},
|
|
5027
|
+
})
|
|
5028
|
+
const mppx = Mppx.create({ methods: [splitServer], realm, secretKey })
|
|
5029
|
+
mppx.onPaymentFailed((context) => {
|
|
5030
|
+
events.push(context.error.name)
|
|
5031
|
+
})
|
|
5032
|
+
const challenge = await mppx.challenge.alpha.charge(challengeOpts)
|
|
5033
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
5034
|
+
|
|
5035
|
+
await expect(mppx.broadcastCredential(credential)).rejects.toThrow('risk denied')
|
|
5036
|
+
|
|
5037
|
+
expect(calls).toEqual(['validate'])
|
|
5038
|
+
expect(events).toEqual(['VerificationFailedError'])
|
|
5039
|
+
})
|
|
5040
|
+
|
|
5041
|
+
test('route handlers revalidate before broadcast for split methods', async () => {
|
|
5042
|
+
const calls: string[] = []
|
|
5043
|
+
const splitServer = Method.toServer(mockCharge, {
|
|
5044
|
+
async validate({ credential, request }) {
|
|
5045
|
+
calls.push('validate')
|
|
5046
|
+
return {
|
|
5047
|
+
challenge: credential.challenge,
|
|
5048
|
+
credential,
|
|
5049
|
+
details: {},
|
|
5050
|
+
intent: 'charge',
|
|
5051
|
+
method: 'alpha',
|
|
5052
|
+
request,
|
|
5053
|
+
source: credential.source,
|
|
5054
|
+
}
|
|
5055
|
+
},
|
|
5056
|
+
async broadcast() {
|
|
5057
|
+
calls.push('broadcast')
|
|
5058
|
+
return mockReceipt('broadcast')
|
|
5059
|
+
},
|
|
5060
|
+
})
|
|
5061
|
+
const mppx = Mppx.create({ methods: [splitServer], realm, secretKey })
|
|
5062
|
+
const firstResult = await mppx.charge(challengeOpts)(
|
|
5063
|
+
new Request('https://api.example.com/resource'),
|
|
5064
|
+
)
|
|
5065
|
+
expect(firstResult.status).toBe(402)
|
|
5066
|
+
if (firstResult.status !== 402) throw new Error()
|
|
5067
|
+
|
|
5068
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
5069
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
5070
|
+
const result = await mppx.charge(challengeOpts)(
|
|
5071
|
+
new Request('https://api.example.com/resource', {
|
|
5072
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
5073
|
+
}),
|
|
5074
|
+
)
|
|
5075
|
+
|
|
5076
|
+
expect(result.status).toBe(200)
|
|
5077
|
+
expect(calls).toEqual(['validate', 'broadcast'])
|
|
5078
|
+
})
|
|
5079
|
+
|
|
4875
5080
|
test('verifies a parsed Credential object (charge)', async () => {
|
|
4876
5081
|
verifyArgs = undefined
|
|
4877
5082
|
const mppx = Mppx.create({
|
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,
|