mppx 0.6.2 → 0.6.5

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 (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/Credential.d.ts +4 -0
  3. package/dist/Credential.d.ts.map +1 -1
  4. package/dist/Credential.js +18 -2
  5. package/dist/Credential.js.map +1 -1
  6. package/dist/bin.js +1 -1
  7. package/dist/bin.js.map +1 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +166 -54
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/discovery/Discovery.d.ts +234 -18
  12. package/dist/discovery/Discovery.d.ts.map +1 -1
  13. package/dist/discovery/Discovery.js +24 -2
  14. package/dist/discovery/Discovery.js.map +1 -1
  15. package/dist/discovery/OpenApi.d.ts +1 -1
  16. package/dist/discovery/OpenApi.d.ts.map +1 -1
  17. package/dist/discovery/OpenApi.js +2 -1
  18. package/dist/discovery/OpenApi.js.map +1 -1
  19. package/dist/discovery/Validate.d.ts.map +1 -1
  20. package/dist/discovery/Validate.js +2 -1
  21. package/dist/discovery/Validate.js.map +1 -1
  22. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  23. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  24. package/dist/stripe/server/internal/html.gen.js +1 -1
  25. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  26. package/dist/tempo/client/Charge.js +1 -1
  27. package/dist/tempo/client/Charge.js.map +1 -1
  28. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  29. package/dist/tempo/client/SessionManager.js +15 -1
  30. package/dist/tempo/client/SessionManager.js.map +1 -1
  31. package/dist/tempo/internal/proof.d.ts +6 -2
  32. package/dist/tempo/internal/proof.d.ts.map +1 -1
  33. package/dist/tempo/internal/proof.js +7 -4
  34. package/dist/tempo/internal/proof.js.map +1 -1
  35. package/dist/tempo/server/Charge.d.ts.map +1 -1
  36. package/dist/tempo/server/Charge.js +4 -3
  37. package/dist/tempo/server/Charge.js.map +1 -1
  38. package/dist/tempo/server/Session.d.ts.map +1 -1
  39. package/dist/tempo/server/Session.js +23 -0
  40. package/dist/tempo/server/Session.js.map +1 -1
  41. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  42. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  43. package/dist/tempo/server/internal/html.gen.js +1 -1
  44. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  45. package/dist/tempo/session/Chain.d.ts.map +1 -1
  46. package/dist/tempo/session/Chain.js +29 -0
  47. package/dist/tempo/session/Chain.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/Challenge.test.ts +45 -0
  50. package/src/Credential.test.ts +66 -0
  51. package/src/Credential.ts +23 -3
  52. package/src/bin.ts +1 -1
  53. package/src/cli/cli.ts +194 -58
  54. package/src/cli/mcp.test.ts +233 -0
  55. package/src/discovery/Discovery.test.ts +66 -4
  56. package/src/discovery/Discovery.ts +40 -7
  57. package/src/discovery/OpenApi.test.ts +61 -33
  58. package/src/discovery/OpenApi.ts +2 -2
  59. package/src/discovery/Validate.test.ts +117 -0
  60. package/src/discovery/Validate.ts +2 -1
  61. package/src/middlewares/elysia.test.ts +1 -1
  62. package/src/middlewares/express.test.ts +1 -1
  63. package/src/middlewares/hono.test.ts +1 -1
  64. package/src/middlewares/nextjs.test.ts +1 -1
  65. package/src/proxy/Proxy.test.ts +3 -3
  66. package/src/stripe/server/internal/html.gen.ts +1 -1
  67. package/src/tempo/client/Charge.ts +1 -1
  68. package/src/tempo/client/SessionManager.ts +13 -1
  69. package/src/tempo/internal/proof.test.ts +11 -5
  70. package/src/tempo/internal/proof.ts +7 -4
  71. package/src/tempo/server/Charge.test.ts +51 -15
  72. package/src/tempo/server/Charge.ts +5 -3
  73. package/src/tempo/server/Session.test.ts +265 -3
  74. package/src/tempo/server/Session.ts +30 -0
  75. package/src/tempo/server/internal/html.gen.ts +1 -1
  76. package/src/tempo/session/Chain.ts +55 -0
@@ -244,13 +244,14 @@ export function charge<const parameters extends charge.Parameters>(
244
244
  domain: Proof.domain(resolvedChainId),
245
245
  types: Proof.types,
246
246
  primaryType: 'Proof',
247
- message: Proof.message(challenge.id),
247
+ message: Proof.message(challenge.id, challenge.realm),
248
248
  signature: payload.signature as `0x${string}`,
249
249
  })
250
250
  if (!valid) {
251
251
  const proofSigner = recoverAuthorizedProofSigner({
252
252
  chainId: resolvedChainId,
253
253
  challengeId: challenge.id,
254
+ realm: challenge.realm,
254
255
  signature: payload.signature as `0x${string}`,
255
256
  sourceAddress: source.address,
256
257
  })
@@ -712,10 +713,11 @@ async function markProofUsed(
712
713
  function recoverAuthorizedProofSigner(parameters: {
713
714
  chainId: number
714
715
  challengeId: string
716
+ realm: string
715
717
  signature: `0x${string}`
716
718
  sourceAddress: `0x${string}`
717
719
  }): `0x${string}` | null {
718
- const { chainId, challengeId, signature, sourceAddress } = parameters
720
+ const { chainId, challengeId, realm, signature, sourceAddress } = parameters
719
721
 
720
722
  try {
721
723
  const envelope = SignatureEnvelope.from(signature)
@@ -723,7 +725,7 @@ function recoverAuthorizedProofSigner(parameters: {
723
725
  domain: Proof.domain(chainId),
724
726
  types: Proof.types,
725
727
  primaryType: 'Proof',
726
- message: Proof.message(challengeId),
728
+ message: Proof.message(challengeId, realm),
727
729
  })
728
730
 
729
731
  if (envelope.type === 'keychain') {
@@ -7,7 +7,7 @@ import {
7
7
  Transport as ServerTransport,
8
8
  tempo as tempo_server,
9
9
  } from 'mppx/server'
10
- import { Base64 } from 'ox'
10
+ import { Base64, Secp256k1 } from 'ox'
11
11
  import {
12
12
  type Address,
13
13
  createClient,
@@ -17,7 +17,7 @@ import {
17
17
  signatureToCompactSignature,
18
18
  } from 'viem'
19
19
  import { waitForTransactionReceipt } from 'viem/actions'
20
- import { Addresses } from 'viem/tempo'
20
+ import { Account as TempoAccount, Actions, Addresses } from 'viem/tempo'
21
21
  import { beforeAll, beforeEach, describe, expect, expectTypeOf, test } from 'vp/test'
22
22
  import { WebSocketServer } from 'ws'
23
23
  import { nodeEnv } from '~test/config.js'
@@ -50,7 +50,8 @@ import {
50
50
  import type * as Methods from '../Methods.js'
51
51
  import * as ChannelStore from '../session/ChannelStore.js'
52
52
  import { deserializeSessionReceipt } from '../session/Receipt.js'
53
- import type { SessionReceipt } from '../session/Types.js'
53
+ import { serializeSessionReceipt } from '../session/Receipt.js'
54
+ import type { SessionCredentialPayload, SessionReceipt } from '../session/Types.js'
54
55
  import { signVoucher } from '../session/Voucher.js'
55
56
  import * as TempoWs from '../session/Ws.js'
56
57
  import { charge, session, settle } from './Session.js'
@@ -1886,6 +1887,77 @@ describe.runIf(isLocalnet)('session', () => {
1886
1887
  expect(ch!.settledOnChain).toBe(5000000n)
1887
1888
  })
1888
1889
 
1890
+ test('accepts a Tempo access-key account for settlement', async () => {
1891
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
1892
+ const server = createServer()
1893
+
1894
+ await server.verify({
1895
+ credential: {
1896
+ challenge: makeChallenge({ id: 'settle-access-key-open', channelId }),
1897
+ payload: {
1898
+ action: 'open' as const,
1899
+ type: 'transaction' as const,
1900
+ channelId,
1901
+ transaction: serializedTransaction,
1902
+ cumulativeAmount: '5000000',
1903
+ signature: await signTestVoucher(channelId, 5000000n),
1904
+ },
1905
+ },
1906
+ request: makeRequest(),
1907
+ })
1908
+
1909
+ const privateKey = Secp256k1.randomPrivateKey()
1910
+ const accessKey = TempoAccount.fromSecp256k1(privateKey, {
1911
+ access: recipientAccount,
1912
+ })
1913
+
1914
+ await Actions.accessKey.authorizeSync(client, {
1915
+ account: recipientAccount,
1916
+ accessKey,
1917
+ feeToken: currency,
1918
+ })
1919
+
1920
+ const settleTxHash = await settle(store, client, channelId, {
1921
+ escrowContract,
1922
+ account: accessKey,
1923
+ })
1924
+ expect(settleTxHash).toMatch(/^0x/)
1925
+
1926
+ const ch = await store.getChannel(channelId)
1927
+ expect(ch!.settledOnChain).toBe(5000000n)
1928
+ })
1929
+
1930
+ test('rejects a raw delegated key account with a helpful error', async () => {
1931
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
1932
+ const server = createServer()
1933
+
1934
+ await server.verify({
1935
+ credential: {
1936
+ challenge: makeChallenge({ id: 'settle-raw-access-key-open', channelId }),
1937
+ payload: {
1938
+ action: 'open' as const,
1939
+ type: 'transaction' as const,
1940
+ channelId,
1941
+ transaction: serializedTransaction,
1942
+ cumulativeAmount: '5000000',
1943
+ signature: await signTestVoucher(channelId, 5000000n),
1944
+ },
1945
+ },
1946
+ request: makeRequest(),
1947
+ })
1948
+
1949
+ const rawAccessKey = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
1950
+
1951
+ await expect(
1952
+ settle(store, client, channelId, {
1953
+ escrowContract,
1954
+ account: rawAccessKey,
1955
+ }),
1956
+ ).rejects.toThrow(
1957
+ `Cannot settle channel ${channelId}: tx sender ${rawAccessKey.address} is not the channel payee ${recipientAccount.address}. If using an access key, pass a Tempo access-key account whose address is the payee wallet, not the raw delegated key address.`,
1958
+ )
1959
+ })
1960
+
1889
1961
  test('settle rejects when no channel found', async () => {
1890
1962
  const fakeChannelId =
1891
1963
  '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex
@@ -1895,6 +1967,196 @@ describe.runIf(isLocalnet)('session', () => {
1895
1967
  })
1896
1968
  })
1897
1969
 
1970
+ describe('close account shapes', () => {
1971
+ test('root payee account closes successfully', async () => {
1972
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
1973
+ const server = createServer()
1974
+ await server.verify({
1975
+ credential: {
1976
+ challenge: makeChallenge({ id: 'close-root-payee-open', channelId }),
1977
+ payload: {
1978
+ action: 'open' as const,
1979
+ type: 'transaction' as const,
1980
+ channelId,
1981
+ transaction: serializedTransaction,
1982
+ cumulativeAmount: '1000000',
1983
+ signature: await signTestVoucher(channelId, 1000000n),
1984
+ },
1985
+ },
1986
+ request: makeRequest(),
1987
+ })
1988
+
1989
+ const closeReceipt = await server.verify({
1990
+ credential: {
1991
+ challenge: makeChallenge({ id: 'close-root-payee', channelId }),
1992
+ payload: {
1993
+ action: 'close' as const,
1994
+ channelId,
1995
+ cumulativeAmount: '1000000',
1996
+ signature: await signTestVoucher(channelId, 1000000n),
1997
+ },
1998
+ },
1999
+ request: makeRequest(),
2000
+ })
2001
+
2002
+ expect(closeReceipt.status).toBe('success')
2003
+ expect((await store.getChannel(channelId))?.finalized).toBe(true)
2004
+ })
2005
+
2006
+ test('payee access-key account closes successfully', async () => {
2007
+ const accessKey = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey(), {
2008
+ access: recipientAccount,
2009
+ })
2010
+
2011
+ await Actions.accessKey.authorizeSync(client, {
2012
+ account: recipientAccount,
2013
+ accessKey,
2014
+ feeToken: currency,
2015
+ })
2016
+
2017
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
2018
+ const server = createServer({ account: accessKey })
2019
+ await server.verify({
2020
+ credential: {
2021
+ challenge: makeChallenge({ id: 'close-access-key-open', channelId }),
2022
+ payload: {
2023
+ action: 'open' as const,
2024
+ type: 'transaction' as const,
2025
+ channelId,
2026
+ transaction: serializedTransaction,
2027
+ cumulativeAmount: '1000000',
2028
+ signature: await signTestVoucher(channelId, 1000000n),
2029
+ },
2030
+ },
2031
+ request: makeRequest(),
2032
+ })
2033
+
2034
+ const closeReceipt = await server.verify({
2035
+ credential: {
2036
+ challenge: makeChallenge({ id: 'close-access-key', channelId }),
2037
+ payload: {
2038
+ action: 'close' as const,
2039
+ channelId,
2040
+ cumulativeAmount: '1000000',
2041
+ signature: await signTestVoucher(channelId, 1000000n),
2042
+ },
2043
+ },
2044
+ request: makeRequest(),
2045
+ })
2046
+
2047
+ expect(closeReceipt.status).toBe('success')
2048
+ expect((await store.getChannel(channelId))?.finalized).toBe(true)
2049
+ })
2050
+
2051
+ test('raw delegated server key fails clearly during close', async () => {
2052
+ const rawAccessKey = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
2053
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
2054
+ const server = createServer({ account: rawAccessKey, recipient })
2055
+ await server.verify({
2056
+ credential: {
2057
+ challenge: makeChallenge({ id: 'close-raw-access-key-open', channelId }),
2058
+ payload: {
2059
+ action: 'open' as const,
2060
+ type: 'transaction' as const,
2061
+ channelId,
2062
+ transaction: serializedTransaction,
2063
+ cumulativeAmount: '1000000',
2064
+ signature: await signTestVoucher(channelId, 1000000n),
2065
+ },
2066
+ },
2067
+ request: makeRequest(),
2068
+ })
2069
+
2070
+ await expect(
2071
+ server.verify({
2072
+ credential: {
2073
+ challenge: makeChallenge({ id: 'close-raw-access-key', channelId }),
2074
+ payload: {
2075
+ action: 'close' as const,
2076
+ channelId,
2077
+ cumulativeAmount: '1000000',
2078
+ signature: await signTestVoucher(channelId, 1000000n),
2079
+ },
2080
+ },
2081
+ request: makeRequest(),
2082
+ }),
2083
+ ).rejects.toThrow(
2084
+ `Cannot close channel ${channelId}: tx sender ${rawAccessKey.address} is not the channel payee ${recipientAccount.address}. If using an access key, pass a Tempo access-key account whose address is the payee wallet, not the raw delegated key address.`,
2085
+ )
2086
+ })
2087
+
2088
+ test('sessionManager.close surfaces problem details from HTTP close failures', async () => {
2089
+ const challenge = makeChallenge({
2090
+ id: 'close-http-failure',
2091
+ channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
2092
+ })
2093
+ let requests = 0
2094
+
2095
+ const fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
2096
+ requests++
2097
+
2098
+ const authorization = new Headers(init?.headers).get('Authorization')
2099
+ if (!authorization) {
2100
+ return new Response(null, {
2101
+ status: 402,
2102
+ headers: { 'WWW-Authenticate': Challenge.serialize(challenge) },
2103
+ })
2104
+ }
2105
+
2106
+ const credential = Credential.deserialize<SessionCredentialPayload>(authorization)
2107
+ if (credential.payload.action === 'open') {
2108
+ return new Response('ok', {
2109
+ status: 200,
2110
+ headers: {
2111
+ 'Payment-Receipt': serializeSessionReceipt({
2112
+ method: 'tempo',
2113
+ intent: 'session',
2114
+ status: 'success',
2115
+ timestamp: new Date().toISOString(),
2116
+ reference: credential.payload.channelId,
2117
+ challengeId: credential.challenge.id,
2118
+ channelId: credential.payload.channelId,
2119
+ acceptedCumulative: credential.payload.cumulativeAmount,
2120
+ spent: credential.payload.cumulativeAmount,
2121
+ units: 1,
2122
+ }),
2123
+ },
2124
+ })
2125
+ }
2126
+
2127
+ if (credential.payload.action === 'close') {
2128
+ return new Response(
2129
+ JSON.stringify({ detail: 'raw delegated key is not the payee wallet' }),
2130
+ {
2131
+ status: 400,
2132
+ headers: { 'Content-Type': 'application/problem+json' },
2133
+ },
2134
+ )
2135
+ }
2136
+
2137
+ throw new Error(
2138
+ `unexpected payment action ${(credential.payload as { action: string }).action}`,
2139
+ )
2140
+ }
2141
+
2142
+ const manager = sessionManager({
2143
+ account: payer,
2144
+ client,
2145
+ escrowContract,
2146
+ fetch,
2147
+ maxDeposit: '1',
2148
+ })
2149
+
2150
+ const response = await manager.fetch('https://api.example.com/resource')
2151
+ expect(response.status).toBe(200)
2152
+
2153
+ await expect(manager.close()).rejects.toThrow(
2154
+ 'Close request failed with status 400: raw delegated key is not the payee wallet',
2155
+ )
2156
+ expect(requests).toBe(3)
2157
+ })
2158
+ })
2159
+
1898
2160
  describe('non-persistent storage recovery', () => {
1899
2161
  test('open on existing on-chain channel initializes settledOnChain from chain', async () => {
1900
2162
  const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
@@ -369,6 +369,22 @@ export declare namespace session {
369
369
  }
370
370
  }
371
371
 
372
+ function assertSettlementSender(parameters: {
373
+ operation: 'close' | 'settle'
374
+ channelId: Hex
375
+ payee: Address
376
+ sender: Address | undefined
377
+ }) {
378
+ const { operation, channelId, payee, sender } = parameters
379
+ if (!sender) return
380
+ if (sender.toLowerCase() === payee.toLowerCase()) return
381
+ throw new BadRequestError({
382
+ reason:
383
+ `Cannot ${operation} channel ${channelId}: tx sender ${sender} is not the channel payee ${payee}. ` +
384
+ 'If using an access key, pass a Tempo access-key account whose address is the payee wallet, not the raw delegated key address.',
385
+ })
386
+ }
387
+
372
388
  /**
373
389
  * One-shot settle: reads highest voucher from store and submits on-chain.
374
390
  */
@@ -393,6 +409,13 @@ export async function settle(
393
409
  defaults.escrowContract[chainId as keyof typeof defaults.escrowContract]
394
410
  if (!resolvedEscrow) throw new Error(`No escrow contract for chainId ${chainId}.`)
395
411
 
412
+ assertSettlementSender({
413
+ operation: 'settle',
414
+ channelId,
415
+ payee: channel.payee,
416
+ sender: options?.account?.address ?? client.account?.address,
417
+ })
418
+
396
419
  const settledAmount = channel.highestVoucher.cumulativeAmount
397
420
  const txHash = await settleOnChain(client, resolvedEscrow, channel.highestVoucher, {
398
421
  ...(options?.feePayer && options?.account
@@ -891,6 +914,13 @@ async function handleClose(
891
914
  throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
892
915
  }
893
916
 
917
+ assertSettlementSender({
918
+ operation: 'close',
919
+ channelId: payload.channelId,
920
+ payee: onChain.payee,
921
+ sender: account?.address ?? client.account?.address,
922
+ })
923
+
894
924
  const txHash = await closeOnChain(client, methodDetails.escrowContract, voucher, {
895
925
  ...(feePayer && account ? { feePayer, account } : { account }),
896
926
  })