mppx 0.6.19 → 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.
Files changed (148) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/Challenge.d.ts +2 -2
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +1 -1
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Method.d.ts +34 -0
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +3 -1
  9. package/dist/Method.js.map +1 -1
  10. package/dist/Receipt.d.ts +1 -0
  11. package/dist/Receipt.d.ts.map +1 -1
  12. package/dist/Receipt.js +2 -0
  13. package/dist/Receipt.js.map +1 -1
  14. package/dist/client/Methods.d.ts +1 -0
  15. package/dist/client/Methods.d.ts.map +1 -1
  16. package/dist/client/Methods.js +1 -0
  17. package/dist/client/Methods.js.map +1 -1
  18. package/dist/middlewares/elysia.d.ts.map +1 -1
  19. package/dist/middlewares/elysia.js +14 -0
  20. package/dist/middlewares/elysia.js.map +1 -1
  21. package/dist/middlewares/express.d.ts.map +1 -1
  22. package/dist/middlewares/express.js +1 -2
  23. package/dist/middlewares/express.js.map +1 -1
  24. package/dist/middlewares/hono.d.ts.map +1 -1
  25. package/dist/middlewares/hono.js +14 -0
  26. package/dist/middlewares/hono.js.map +1 -1
  27. package/dist/middlewares/nextjs.d.ts.map +1 -1
  28. package/dist/middlewares/nextjs.js +14 -0
  29. package/dist/middlewares/nextjs.js.map +1 -1
  30. package/dist/proxy/Proxy.d.ts.map +1 -1
  31. package/dist/proxy/Proxy.js +2 -2
  32. package/dist/proxy/Proxy.js.map +1 -1
  33. package/dist/proxy/Service.d.ts.map +1 -1
  34. package/dist/proxy/Service.js +1 -1
  35. package/dist/proxy/Service.js.map +1 -1
  36. package/dist/server/Mppx.d.ts +15 -3
  37. package/dist/server/Mppx.d.ts.map +1 -1
  38. package/dist/server/Mppx.js +190 -40
  39. package/dist/server/Mppx.js.map +1 -1
  40. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  41. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  42. package/dist/stripe/server/internal/html.gen.js +1 -1
  43. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  44. package/dist/tempo/Methods.d.ts +96 -0
  45. package/dist/tempo/Methods.d.ts.map +1 -1
  46. package/dist/tempo/Methods.js +97 -0
  47. package/dist/tempo/Methods.js.map +1 -1
  48. package/dist/tempo/client/Methods.d.ts +3 -0
  49. package/dist/tempo/client/Methods.d.ts.map +1 -1
  50. package/dist/tempo/client/Methods.js +3 -0
  51. package/dist/tempo/client/Methods.js.map +1 -1
  52. package/dist/tempo/client/Subscription.d.ts +114 -0
  53. package/dist/tempo/client/Subscription.d.ts.map +1 -0
  54. package/dist/tempo/client/Subscription.js +100 -0
  55. package/dist/tempo/client/Subscription.js.map +1 -0
  56. package/dist/tempo/client/index.d.ts +1 -0
  57. package/dist/tempo/client/index.d.ts.map +1 -1
  58. package/dist/tempo/client/index.js +1 -0
  59. package/dist/tempo/client/index.js.map +1 -1
  60. package/dist/tempo/index.d.ts +1 -0
  61. package/dist/tempo/index.d.ts.map +1 -1
  62. package/dist/tempo/index.js +1 -0
  63. package/dist/tempo/index.js.map +1 -1
  64. package/dist/tempo/server/Methods.d.ts +5 -0
  65. package/dist/tempo/server/Methods.d.ts.map +1 -1
  66. package/dist/tempo/server/Methods.js +5 -0
  67. package/dist/tempo/server/Methods.js.map +1 -1
  68. package/dist/tempo/server/Subscription.d.ts +221 -0
  69. package/dist/tempo/server/Subscription.d.ts.map +1 -0
  70. package/dist/tempo/server/Subscription.js +637 -0
  71. package/dist/tempo/server/Subscription.js.map +1 -0
  72. package/dist/tempo/server/index.d.ts +1 -0
  73. package/dist/tempo/server/index.d.ts.map +1 -1
  74. package/dist/tempo/server/index.js +1 -0
  75. package/dist/tempo/server/index.js.map +1 -1
  76. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  77. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  78. package/dist/tempo/server/internal/html.gen.js +1 -1
  79. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  80. package/dist/tempo/subscription/KeyAuthorization.d.ts +282 -0
  81. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -0
  82. package/dist/tempo/subscription/KeyAuthorization.js +297 -0
  83. package/dist/tempo/subscription/KeyAuthorization.js.map +1 -0
  84. package/dist/tempo/subscription/Receipt.d.ts +10 -0
  85. package/dist/tempo/subscription/Receipt.d.ts.map +1 -0
  86. package/dist/tempo/subscription/Receipt.js +16 -0
  87. package/dist/tempo/subscription/Receipt.js.map +1 -0
  88. package/dist/tempo/subscription/Store.d.ts +99 -0
  89. package/dist/tempo/subscription/Store.d.ts.map +1 -0
  90. package/dist/tempo/subscription/Store.js +292 -0
  91. package/dist/tempo/subscription/Store.js.map +1 -0
  92. package/dist/tempo/subscription/Types.d.ts +65 -0
  93. package/dist/tempo/subscription/Types.d.ts.map +1 -0
  94. package/dist/tempo/subscription/Types.js +2 -0
  95. package/dist/tempo/subscription/Types.js.map +1 -0
  96. package/dist/tempo/subscription/index.d.ts +6 -0
  97. package/dist/tempo/subscription/index.d.ts.map +1 -0
  98. package/dist/tempo/subscription/index.js +4 -0
  99. package/dist/tempo/subscription/index.js.map +1 -0
  100. package/dist/zod.d.ts +7 -0
  101. package/dist/zod.d.ts.map +1 -1
  102. package/dist/zod.js +18 -0
  103. package/dist/zod.js.map +1 -1
  104. package/package.json +3 -3
  105. package/src/Challenge.test.ts +13 -0
  106. package/src/Challenge.ts +3 -3
  107. package/src/Method.ts +46 -1
  108. package/src/Receipt.ts +2 -0
  109. package/src/client/Methods.ts +1 -0
  110. package/src/middlewares/elysia.test.ts +31 -1
  111. package/src/middlewares/elysia.ts +13 -0
  112. package/src/middlewares/express.ts +1 -5
  113. package/src/middlewares/hono.test.ts +30 -1
  114. package/src/middlewares/hono.ts +13 -0
  115. package/src/middlewares/nextjs.test.ts +28 -1
  116. package/src/middlewares/nextjs.ts +13 -0
  117. package/src/proxy/Proxy.ts +2 -5
  118. package/src/proxy/Service.test.ts +34 -0
  119. package/src/proxy/Service.ts +7 -0
  120. package/src/server/Mppx.authorize.test.ts +210 -0
  121. package/src/server/Mppx.test-d.ts +23 -1
  122. package/src/server/Mppx.test.ts +73 -3
  123. package/src/server/Mppx.ts +291 -58
  124. package/src/stripe/server/internal/html/package.json +1 -1
  125. package/src/stripe/server/internal/html.gen.ts +1 -1
  126. package/src/tempo/Methods.test.ts +131 -0
  127. package/src/tempo/Methods.ts +136 -0
  128. package/src/tempo/Subscription.integration.test.ts +591 -0
  129. package/src/tempo/client/Methods.ts +3 -0
  130. package/src/tempo/client/Subscription.test.ts +131 -0
  131. package/src/tempo/client/Subscription.ts +155 -0
  132. package/src/tempo/client/index.ts +1 -0
  133. package/src/tempo/index.ts +1 -0
  134. package/src/tempo/server/Methods.ts +5 -0
  135. package/src/tempo/server/Subscription.test.ts +1410 -0
  136. package/src/tempo/server/Subscription.ts +1014 -0
  137. package/src/tempo/server/index.ts +1 -0
  138. package/src/tempo/server/internal/html/package.json +1 -1
  139. package/src/tempo/server/internal/html.gen.ts +1 -1
  140. package/src/tempo/subscription/KeyAuthorization.test.ts +204 -0
  141. package/src/tempo/subscription/KeyAuthorization.ts +394 -0
  142. package/src/tempo/subscription/Receipt.ts +28 -0
  143. package/src/tempo/subscription/Store.test.ts +554 -0
  144. package/src/tempo/subscription/Store.ts +431 -0
  145. package/src/tempo/subscription/Types.ts +68 -0
  146. package/src/tempo/subscription/index.ts +23 -0
  147. package/src/zod.test.ts +23 -1
  148. package/src/zod.ts +24 -0
@@ -0,0 +1,131 @@
1
+ import { Challenge, Credential } from 'mppx'
2
+ import { KeyAuthorization } from 'ox/tempo'
3
+ import { privateKeyToAccount } from 'viem/accounts'
4
+ import { describe, expect, test } from 'vp/test'
5
+
6
+ import * as Methods from '../Methods.js'
7
+ import { signSubscriptionKeyAuthorization } from '../subscription/KeyAuthorization.js'
8
+ import type { SubscriptionAccessKey } from '../subscription/Types.js'
9
+ import { subscription } from './Subscription.js'
10
+
11
+ const chainId = 4217
12
+ const currency = '0x20c0000000000000000000000000000000000001'
13
+ const recipient = '0x1234567890abcdef1234567890abcdef12345678'
14
+ const selectedAccount = privateKeyToAccount(
15
+ '0x0000000000000000000000000000000000000000000000000000000000000001',
16
+ )
17
+ const accessAccount = privateKeyToAccount(
18
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
19
+ )
20
+ const otherRootAccount = privateKeyToAccount(
21
+ '0x0000000000000000000000000000000000000000000000000000000000000003',
22
+ )
23
+ const accessKey = {
24
+ accessKeyAddress: accessAccount.address,
25
+ keyType: 'secp256k1',
26
+ } as const satisfies SubscriptionAccessKey
27
+
28
+ type SubscriptionRequest = ReturnType<typeof Methods.subscription.schema.request.parse>
29
+
30
+ function secondsFromNow(milliseconds: number) {
31
+ return new Date(Math.ceil((Date.now() + milliseconds) / 1_000) * 1_000).toISOString()
32
+ }
33
+
34
+ function createChallenge(
35
+ overrides: Partial<Parameters<typeof Methods.subscription.schema.request.parse>[0]> = {},
36
+ ): Challenge.Challenge<SubscriptionRequest, 'subscription', 'tempo'> {
37
+ const request = Methods.subscription.schema.request.parse({
38
+ accessKey,
39
+ amount: '1',
40
+ chainId,
41
+ currency,
42
+ decimals: 6,
43
+ periodCount: '1',
44
+ periodUnit: 'day',
45
+ recipient,
46
+ subscriptionExpires: secondsFromNow(86_400_000),
47
+ ...overrides,
48
+ })
49
+ return Challenge.from({
50
+ id: 'test-challenge-id',
51
+ intent: 'subscription',
52
+ method: 'tempo',
53
+ realm: 'api.example.com',
54
+ request,
55
+ }) as Challenge.Challenge<SubscriptionRequest, 'subscription', 'tempo'>
56
+ }
57
+
58
+ describe('tempo.subscription client', () => {
59
+ test('uses Tempo testnet as the default subscription chain', async () => {
60
+ const challenge = createChallenge({ chainId: undefined })
61
+ const method = subscription({
62
+ account: selectedAccount,
63
+ })
64
+
65
+ const credential = Credential.deserialize(
66
+ await method.createCredential({ challenge, context: {} }),
67
+ )
68
+ const payload = Methods.subscription.schema.credential.payload.parse(credential.payload)
69
+ const authorization = KeyAuthorization.deserialize(payload.signature as `0x${string}`)
70
+
71
+ expect(authorization.chainId).toBe(42431n)
72
+ })
73
+
74
+ test('can reject subscription expiry from custom request validation', async () => {
75
+ const challenge = createChallenge({
76
+ subscriptionExpires: secondsFromNow(2 * 86_400_000),
77
+ })
78
+ const method = subscription({
79
+ account: selectedAccount,
80
+ validateRequest: (request) => {
81
+ const maxExpiry = Date.now() + 86_400_000
82
+ if (new Date(request.subscriptionExpires).getTime() > maxExpiry) {
83
+ throw new Error('subscription expiry too late')
84
+ }
85
+ },
86
+ })
87
+
88
+ await expect(method.createCredential({ challenge, context: {} })).rejects.toThrow(
89
+ 'subscription expiry too late',
90
+ )
91
+ })
92
+
93
+ test('runs custom request validation before authorizing the access key', async () => {
94
+ const challenge = createChallenge()
95
+ const method = subscription({
96
+ account: selectedAccount,
97
+ validateRequest: () => {
98
+ throw new Error('unexpected subscription request')
99
+ },
100
+ })
101
+
102
+ await expect(method.createCredential({ challenge, context: {} })).rejects.toThrow(
103
+ 'unexpected subscription request',
104
+ )
105
+ })
106
+
107
+ test('rejects key authorizations signed by a different account', async () => {
108
+ const challenge = createChallenge()
109
+ const keyAuthorization = await signSubscriptionKeyAuthorization({
110
+ accessKey,
111
+ account: otherRootAccount,
112
+ chainId,
113
+ request: challenge.request,
114
+ })
115
+ if (!keyAuthorization) throw new Error('expected key authorization')
116
+
117
+ const method = subscription({
118
+ account: selectedAccount.address,
119
+ getClient: async () =>
120
+ ({
121
+ request: async () => ({
122
+ keyAuthorization: KeyAuthorization.toRpc(keyAuthorization),
123
+ }),
124
+ }) as never,
125
+ })
126
+
127
+ await expect(method.createCredential({ challenge, context: {} })).rejects.toThrow(
128
+ 'keyAuthorization signer does not match the selected account',
129
+ )
130
+ })
131
+ })
@@ -0,0 +1,155 @@
1
+ import { KeyAuthorization } from 'ox/tempo'
2
+ import { isAddressEqual, type Address } from 'viem'
3
+ import { tempo as tempo_chain } from 'viem/chains'
4
+
5
+ import * as Credential from '../../Credential.js'
6
+ import type { MaybePromise } from '../../internal/types.js'
7
+ import * as Method from '../../Method.js'
8
+ import * as Account from '../../viem/Account.js'
9
+ import * as Client from '../../viem/Client.js'
10
+ import * as z from '../../zod.js'
11
+ import * as defaults from '../internal/defaults.js'
12
+ import * as Methods from '../Methods.js'
13
+ import {
14
+ getSubscriptionRpcAllowedCalls,
15
+ signSubscriptionKeyAuthorization,
16
+ toSubscriptionExpiryDate,
17
+ toSubscriptionExpirySeconds,
18
+ toSubscriptionPeriodSeconds,
19
+ verifySubscriptionKeyAuthorization,
20
+ } from '../subscription/KeyAuthorization.js'
21
+ import type { SubscriptionAccessKey } from '../subscription/Types.js'
22
+
23
+ type SubscriptionRequest = ReturnType<typeof Methods.subscription.schema.request.parse>
24
+
25
+ /** Context accepted by the Tempo subscription client method. */
26
+ export const subscriptionContextSchema = z.object({
27
+ accessKey: z.optional(z.custom<SubscriptionAccessKey>()),
28
+ account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
29
+ })
30
+
31
+ /** Runtime context for creating a Tempo subscription credential. */
32
+ export type SubscriptionContext = z.infer<typeof subscriptionContextSchema>
33
+
34
+ /** Creates a Tempo subscription client method. */
35
+ export function subscription(parameters: subscription.Parameters = {}) {
36
+ const getClient = Client.getResolver({
37
+ chain: tempo_chain,
38
+ getClient: parameters.getClient,
39
+ rpcUrl: defaults.rpcUrl,
40
+ })
41
+ const getAccount = Account.getResolver({ account: parameters.account })
42
+
43
+ return Method.toClient(Methods.subscription, {
44
+ context: subscriptionContextSchema,
45
+
46
+ async createCredential({ challenge, context }) {
47
+ const chainId = challenge.request.methodDetails?.chainId ?? defaults.chainId.testnet
48
+ const client = await getClient({ chainId })
49
+ const account = getAccount(client, context)
50
+ const accessKey =
51
+ context?.accessKey ?? parameters.accessKey ?? challenge.request.methodDetails?.accessKey
52
+ if (!accessKey) {
53
+ throw new Error(
54
+ 'No `accessKey` provided. The subscription challenge must include `accessKey`, or the client must pass one to parameters/context.',
55
+ )
56
+ }
57
+
58
+ assertSubscriptionRequestRepresentable(challenge.request)
59
+ await parameters.validateRequest?.(challenge.request)
60
+
61
+ const keyAuthorization = await authorizeAccessKey(client, {
62
+ accessKey,
63
+ account,
64
+ chainId,
65
+ request: challenge.request,
66
+ } as never)
67
+
68
+ const verified = verifySubscriptionKeyAuthorization({
69
+ accessKey,
70
+ chainId,
71
+ payload: {
72
+ signature: KeyAuthorization.serialize(keyAuthorization as never),
73
+ type: 'keyAuthorization',
74
+ },
75
+ request: challenge.request,
76
+ })
77
+ if (!isAddressEqual(verified.source.address, account.address)) {
78
+ throw new Error('keyAuthorization signer does not match the selected account')
79
+ }
80
+
81
+ return Credential.serialize({
82
+ challenge,
83
+ payload: {
84
+ signature: KeyAuthorization.serialize(keyAuthorization as never),
85
+ type: 'keyAuthorization',
86
+ },
87
+ source: `did:pkh:eip155:${chainId}:${account.address.toLowerCase()}`,
88
+ })
89
+ },
90
+ })
91
+ }
92
+
93
+ async function authorizeAccessKey(
94
+ client: Awaited<ReturnType<ReturnType<typeof Client.getResolver>>>,
95
+ parameters: {
96
+ accessKey: SubscriptionAccessKey
97
+ account: Account.Account
98
+ chainId: number
99
+ request: Pick<
100
+ SubscriptionRequest,
101
+ 'amount' | 'currency' | 'periodCount' | 'periodUnit' | 'recipient' | 'subscriptionExpires'
102
+ >
103
+ },
104
+ ) {
105
+ const { accessKey, account, chainId, request } = parameters
106
+
107
+ const local = await signSubscriptionKeyAuthorization({
108
+ accessKey,
109
+ account,
110
+ chainId,
111
+ request,
112
+ })
113
+ if (local) return local
114
+
115
+ const result = (await client.request({
116
+ method: 'wallet_authorizeAccessKey',
117
+ params: [
118
+ {
119
+ address: accessKey.accessKeyAddress,
120
+ allowedCalls: getSubscriptionRpcAllowedCalls(request),
121
+ expiry: toSubscriptionExpirySeconds(toSubscriptionExpiryDate(request.subscriptionExpires)),
122
+ keyType: accessKey.keyType,
123
+ limits: [
124
+ {
125
+ token: request.currency as Address,
126
+ limit: BigInt(request.amount),
127
+ period: toSubscriptionPeriodSeconds(request),
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ } as never)) as {
133
+ keyAuthorization: Parameters<typeof KeyAuthorization.fromRpc>[0]
134
+ }
135
+
136
+ return KeyAuthorization.fromRpc(result.keyAuthorization)
137
+ }
138
+
139
+ function assertSubscriptionRequestRepresentable(request: SubscriptionRequest) {
140
+ toSubscriptionPeriodSeconds(request)
141
+ toSubscriptionExpirySeconds(toSubscriptionExpiryDate(request.subscriptionExpires))
142
+ }
143
+
144
+ export declare namespace subscription {
145
+ /** Parameters for creating a Tempo subscription credential. */
146
+ type Parameters = Account.getResolver.Parameters &
147
+ Client.getResolver.Parameters & {
148
+ accessKey?: SubscriptionAccessKey | undefined
149
+ validateRequest?:
150
+ | ((
151
+ request: ReturnType<typeof Methods.subscription.schema.request.parse>,
152
+ ) => MaybePromise<void>)
153
+ | undefined
154
+ }
155
+ }
@@ -1,5 +1,6 @@
1
1
  export { charge } from './Charge.js'
2
2
  export { tempo } from './Methods.js'
3
3
  export { session } from './Session.js'
4
+ export { subscription } from './Subscription.js'
4
5
  export type { PaymentResponse, SessionManager } from './SessionManager.js'
5
6
  export { sessionManager } from './SessionManager.js'
@@ -1,3 +1,4 @@
1
1
  export * as Proof from './Proof.js'
2
2
  export * as Methods from './Methods.js'
3
3
  export * as Session from './session/index.js'
4
+ export * as Subscription from './subscription/index.js'
@@ -1,6 +1,7 @@
1
1
  import * as Ws_ from '../session/Ws.js'
2
2
  import { charge as charge_ } from './Charge.js'
3
3
  import { session as session_, settle as settle_ } from './Session.js'
4
+ import { renew as renewSubscription_, subscription as subscription_ } from './Subscription.js'
4
5
 
5
6
  /**
6
7
  * Creates both Tempo `charge` and `session` methods from shared parameters.
@@ -28,6 +29,10 @@ export namespace tempo {
28
29
  export const charge = charge_
29
30
  /** Creates a Tempo `session` method for session-based TIP-20 token payments. */
30
31
  export const session = session_
32
+ /** Creates a Tempo `subscription` method for recurring TIP-20 token payments. */
33
+ export const subscription = subscription_
34
+ /** Renews an overdue Tempo subscription outside of the HTTP request path. */
35
+ export const renewSubscription = renewSubscription_
31
36
  /** One-shot settle: reads highest voucher from storage and submits on-chain. */
32
37
  export const settle = settle_
33
38
  /** Experimental websocket helpers for Tempo sessions. */