mppx 0.4.12 → 0.5.1
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 +12 -0
- package/dist/Expires.d.ts +7 -0
- package/dist/Expires.d.ts.map +1 -1
- package/dist/Expires.js +21 -0
- package/dist/Expires.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +12 -2
- package/dist/cli/account.js.map +1 -1
- package/dist/server/Mppx.js +6 -5
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +3 -3
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/Methods.d.ts +3 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +3 -0
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +18 -2
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +3 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/proof.d.ts +29 -0
- package/dist/tempo/internal/proof.d.ts.map +1 -0
- package/dist/tempo/internal/proof.js +32 -0
- package/dist/tempo/internal/proof.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +11 -3
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +54 -4
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +3 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Expires.ts +25 -0
- package/src/cli/account.ts +13 -2
- package/src/cli/cli.test.ts +230 -1
- package/src/middlewares/elysia.test.ts +130 -9
- package/src/middlewares/express.test.ts +123 -59
- package/src/middlewares/hono.test.ts +81 -39
- package/src/middlewares/nextjs.test.ts +162 -41
- package/src/server/Mppx.test.ts +86 -0
- package/src/server/Mppx.ts +5 -5
- package/src/stripe/server/Charge.ts +3 -7
- package/src/tempo/Methods.test.ts +26 -0
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/Charge.ts +26 -3
- package/src/tempo/internal/charge.test.ts +66 -0
- package/src/tempo/internal/proof.test.ts +83 -0
- package/src/tempo/internal/proof.ts +35 -0
- package/src/tempo/server/Charge.test.ts +660 -1
- package/src/tempo/server/Charge.ts +80 -5
- package/src/tempo/server/Session.test.ts +1123 -53
- package/src/tempo/server/internal/transport.test.ts +32 -0
- package/src/tempo/session/Chain.test.ts +35 -0
- package/src/tempo/session/Sse.test.ts +31 -0
|
@@ -5,7 +5,12 @@ import type { Hex } from 'ox'
|
|
|
5
5
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
6
6
|
import { Handler } from 'tempo.ts/server'
|
|
7
7
|
import { createClient, custom, encodeFunctionData, parseUnits } from 'viem'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getTransactionReceipt,
|
|
10
|
+
prepareTransactionRequest,
|
|
11
|
+
signTypedData,
|
|
12
|
+
signTransaction,
|
|
13
|
+
} from 'viem/actions'
|
|
9
14
|
import { Abis, Account, Actions, Addresses, Secp256k1, Tick, Transaction } from 'viem/tempo'
|
|
10
15
|
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
11
16
|
import * as Http from '~test/Http.js'
|
|
@@ -14,6 +19,7 @@ import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js
|
|
|
14
19
|
|
|
15
20
|
import * as Store from '../../Store.js'
|
|
16
21
|
import * as Attribution from '../Attribution.js'
|
|
22
|
+
import * as Proof from '../internal/proof.js'
|
|
17
23
|
import { signVoucher } from '../session/Voucher.js'
|
|
18
24
|
|
|
19
25
|
const realm = 'api.example.com'
|
|
@@ -1920,6 +1926,659 @@ describe('tempo', () => {
|
|
|
1920
1926
|
})
|
|
1921
1927
|
})
|
|
1922
1928
|
|
|
1929
|
+
describe('intent: charge; type: proof (zero-dollar auth)', () => {
|
|
1930
|
+
test('default: end-to-end zero-dollar auth via SDK', async () => {
|
|
1931
|
+
const mppx = Mppx_client.create({
|
|
1932
|
+
polyfill: false,
|
|
1933
|
+
methods: [
|
|
1934
|
+
tempo_client({
|
|
1935
|
+
account: accounts[1],
|
|
1936
|
+
getClient: () => client,
|
|
1937
|
+
}),
|
|
1938
|
+
],
|
|
1939
|
+
})
|
|
1940
|
+
|
|
1941
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1942
|
+
const result = await Mppx_server.toNodeListener(
|
|
1943
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
1944
|
+
)(req, res)
|
|
1945
|
+
if (result.status === 402) return
|
|
1946
|
+
res.end('OK')
|
|
1947
|
+
})
|
|
1948
|
+
|
|
1949
|
+
const response = await mppx.fetch(httpServer.url)
|
|
1950
|
+
expect(response.status).toBe(200)
|
|
1951
|
+
|
|
1952
|
+
const receipt = Receipt.fromResponse(response)
|
|
1953
|
+
expect(receipt.status).toBe('success')
|
|
1954
|
+
expect(receipt.method).toBe('tempo')
|
|
1955
|
+
expect(receipt.reference).toBeDefined()
|
|
1956
|
+
|
|
1957
|
+
httpServer.close()
|
|
1958
|
+
})
|
|
1959
|
+
|
|
1960
|
+
test('behavior: proof credential contains valid source DID', async () => {
|
|
1961
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1962
|
+
const result = await Mppx_server.toNodeListener(
|
|
1963
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
1964
|
+
)(req, res)
|
|
1965
|
+
if (result.status === 402) return
|
|
1966
|
+
res.end('OK')
|
|
1967
|
+
})
|
|
1968
|
+
|
|
1969
|
+
const response1 = await fetch(httpServer.url)
|
|
1970
|
+
expect(response1.status).toBe(402)
|
|
1971
|
+
|
|
1972
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
1973
|
+
methods: [tempo_client.charge()],
|
|
1974
|
+
})
|
|
1975
|
+
|
|
1976
|
+
const signature = await signTypedData(client, {
|
|
1977
|
+
account: accounts[1],
|
|
1978
|
+
domain: Proof.domain(chain.id),
|
|
1979
|
+
types: Proof.types,
|
|
1980
|
+
primaryType: 'Proof',
|
|
1981
|
+
message: Proof.message(challenge.id),
|
|
1982
|
+
})
|
|
1983
|
+
|
|
1984
|
+
const credential = Credential.from({
|
|
1985
|
+
challenge,
|
|
1986
|
+
payload: { signature, type: 'proof' as const },
|
|
1987
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
1988
|
+
})
|
|
1989
|
+
|
|
1990
|
+
const response2 = await fetch(httpServer.url, {
|
|
1991
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
1992
|
+
})
|
|
1993
|
+
expect(response2.status).toBe(200)
|
|
1994
|
+
|
|
1995
|
+
httpServer.close()
|
|
1996
|
+
})
|
|
1997
|
+
|
|
1998
|
+
test('behavior: proof credential remains reusable until expiry without store', async () => {
|
|
1999
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2000
|
+
const result = await Mppx_server.toNodeListener(
|
|
2001
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2002
|
+
)(req, res)
|
|
2003
|
+
if (result.status === 402) return
|
|
2004
|
+
res.end('OK')
|
|
2005
|
+
})
|
|
2006
|
+
|
|
2007
|
+
const response1 = await fetch(httpServer.url)
|
|
2008
|
+
expect(response1.status).toBe(402)
|
|
2009
|
+
|
|
2010
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2011
|
+
methods: [tempo_client.charge()],
|
|
2012
|
+
})
|
|
2013
|
+
|
|
2014
|
+
const signature = await signTypedData(client, {
|
|
2015
|
+
account: accounts[1],
|
|
2016
|
+
domain: Proof.domain(chain.id),
|
|
2017
|
+
types: Proof.types,
|
|
2018
|
+
primaryType: 'Proof',
|
|
2019
|
+
message: Proof.message(challenge.id),
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
const credential = Credential.from({
|
|
2023
|
+
challenge,
|
|
2024
|
+
payload: { signature, type: 'proof' as const },
|
|
2025
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2026
|
+
})
|
|
2027
|
+
|
|
2028
|
+
const response2 = await fetch(httpServer.url, {
|
|
2029
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2030
|
+
})
|
|
2031
|
+
expect(response2.status).toBe(200)
|
|
2032
|
+
|
|
2033
|
+
const replayResponse = await fetch(httpServer.url, {
|
|
2034
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2035
|
+
})
|
|
2036
|
+
expect(replayResponse.status).toBe(200)
|
|
2037
|
+
|
|
2038
|
+
httpServer.close()
|
|
2039
|
+
})
|
|
2040
|
+
|
|
2041
|
+
test('behavior: rejects replayed proof credential when store is configured', async () => {
|
|
2042
|
+
const replayStore = Store.memory()
|
|
2043
|
+
const server_ = Mppx_server.create({
|
|
2044
|
+
methods: [
|
|
2045
|
+
tempo_server.charge({
|
|
2046
|
+
getClient() {
|
|
2047
|
+
return client
|
|
2048
|
+
},
|
|
2049
|
+
currency: asset,
|
|
2050
|
+
account: accounts[0],
|
|
2051
|
+
store: replayStore,
|
|
2052
|
+
}),
|
|
2053
|
+
],
|
|
2054
|
+
realm,
|
|
2055
|
+
secretKey,
|
|
2056
|
+
})
|
|
2057
|
+
|
|
2058
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2059
|
+
const result = await Mppx_server.toNodeListener(
|
|
2060
|
+
server_.charge({ amount: '0', decimals: 6 }),
|
|
2061
|
+
)(req, res)
|
|
2062
|
+
if (result.status === 402) return
|
|
2063
|
+
res.end('OK')
|
|
2064
|
+
})
|
|
2065
|
+
|
|
2066
|
+
const response1 = await fetch(httpServer.url)
|
|
2067
|
+
expect(response1.status).toBe(402)
|
|
2068
|
+
|
|
2069
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2070
|
+
methods: [tempo_client.charge()],
|
|
2071
|
+
})
|
|
2072
|
+
|
|
2073
|
+
const signature = await signTypedData(client, {
|
|
2074
|
+
account: accounts[1],
|
|
2075
|
+
domain: Proof.domain(chain.id),
|
|
2076
|
+
types: Proof.types,
|
|
2077
|
+
primaryType: 'Proof',
|
|
2078
|
+
message: Proof.message(challenge.id),
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
const credential = Credential.from({
|
|
2082
|
+
challenge,
|
|
2083
|
+
payload: { signature, type: 'proof' as const },
|
|
2084
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2085
|
+
})
|
|
2086
|
+
|
|
2087
|
+
const response2 = await fetch(httpServer.url, {
|
|
2088
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2089
|
+
})
|
|
2090
|
+
expect(response2.status).toBe(200)
|
|
2091
|
+
|
|
2092
|
+
const replayResponse = await fetch(httpServer.url, {
|
|
2093
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2094
|
+
})
|
|
2095
|
+
expect(replayResponse.status).toBe(402)
|
|
2096
|
+
const replayBody = (await replayResponse.json()) as { detail: string }
|
|
2097
|
+
expect(replayBody.detail).toContain('Proof credential has already been used.')
|
|
2098
|
+
|
|
2099
|
+
httpServer.close()
|
|
2100
|
+
})
|
|
2101
|
+
|
|
2102
|
+
test('behavior: shared store rejects proof replay across server instances', async () => {
|
|
2103
|
+
const replayStore = Store.memory()
|
|
2104
|
+
const serverA = Mppx_server.create({
|
|
2105
|
+
methods: [
|
|
2106
|
+
tempo_server.charge({
|
|
2107
|
+
getClient() {
|
|
2108
|
+
return client
|
|
2109
|
+
},
|
|
2110
|
+
currency: asset,
|
|
2111
|
+
account: accounts[0],
|
|
2112
|
+
store: replayStore,
|
|
2113
|
+
}),
|
|
2114
|
+
],
|
|
2115
|
+
realm,
|
|
2116
|
+
secretKey,
|
|
2117
|
+
})
|
|
2118
|
+
const serverB = Mppx_server.create({
|
|
2119
|
+
methods: [
|
|
2120
|
+
tempo_server.charge({
|
|
2121
|
+
getClient() {
|
|
2122
|
+
return client
|
|
2123
|
+
},
|
|
2124
|
+
currency: asset,
|
|
2125
|
+
account: accounts[0],
|
|
2126
|
+
store: replayStore,
|
|
2127
|
+
}),
|
|
2128
|
+
],
|
|
2129
|
+
realm,
|
|
2130
|
+
secretKey,
|
|
2131
|
+
})
|
|
2132
|
+
|
|
2133
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2134
|
+
const route = new URL(req.url!, 'https://example.com').pathname
|
|
2135
|
+
const handler = route === '/a' ? serverA : serverB
|
|
2136
|
+
const result = await Mppx_server.toNodeListener(
|
|
2137
|
+
handler.charge({ amount: '0', decimals: 6 }),
|
|
2138
|
+
)(req, res)
|
|
2139
|
+
if (result.status === 402) return
|
|
2140
|
+
res.end('OK')
|
|
2141
|
+
})
|
|
2142
|
+
|
|
2143
|
+
const response1 = await fetch(`${httpServer.url}/a`)
|
|
2144
|
+
expect(response1.status).toBe(402)
|
|
2145
|
+
|
|
2146
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2147
|
+
methods: [tempo_client.charge()],
|
|
2148
|
+
})
|
|
2149
|
+
|
|
2150
|
+
const signature = await signTypedData(client, {
|
|
2151
|
+
account: accounts[1],
|
|
2152
|
+
domain: Proof.domain(chain.id),
|
|
2153
|
+
types: Proof.types,
|
|
2154
|
+
primaryType: 'Proof',
|
|
2155
|
+
message: Proof.message(challenge.id),
|
|
2156
|
+
})
|
|
2157
|
+
|
|
2158
|
+
const credential = Credential.from({
|
|
2159
|
+
challenge,
|
|
2160
|
+
payload: { signature, type: 'proof' as const },
|
|
2161
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2162
|
+
})
|
|
2163
|
+
|
|
2164
|
+
const response2 = await fetch(`${httpServer.url}/a`, {
|
|
2165
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2166
|
+
})
|
|
2167
|
+
expect(response2.status).toBe(200)
|
|
2168
|
+
|
|
2169
|
+
const replayResponse = await fetch(`${httpServer.url}/b`, {
|
|
2170
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2171
|
+
})
|
|
2172
|
+
expect(replayResponse.status).toBe(402)
|
|
2173
|
+
const replayBody = (await replayResponse.json()) as { detail: string }
|
|
2174
|
+
expect(replayBody.detail).toContain('Proof credential has already been used.')
|
|
2175
|
+
|
|
2176
|
+
httpServer.close()
|
|
2177
|
+
})
|
|
2178
|
+
|
|
2179
|
+
test('behavior: store keys proof replay protection by challenge ID', async () => {
|
|
2180
|
+
const replayStore = Store.memory()
|
|
2181
|
+
const server_ = Mppx_server.create({
|
|
2182
|
+
methods: [
|
|
2183
|
+
tempo_server.charge({
|
|
2184
|
+
getClient() {
|
|
2185
|
+
return client
|
|
2186
|
+
},
|
|
2187
|
+
currency: asset,
|
|
2188
|
+
account: accounts[0],
|
|
2189
|
+
store: replayStore,
|
|
2190
|
+
}),
|
|
2191
|
+
],
|
|
2192
|
+
realm,
|
|
2193
|
+
secretKey,
|
|
2194
|
+
})
|
|
2195
|
+
|
|
2196
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2197
|
+
const result = await Mppx_server.toNodeListener(
|
|
2198
|
+
server_.charge({ amount: '0', decimals: 6 }),
|
|
2199
|
+
)(req, res)
|
|
2200
|
+
if (result.status === 402) return
|
|
2201
|
+
res.end('OK')
|
|
2202
|
+
})
|
|
2203
|
+
|
|
2204
|
+
const response1 = await fetch(httpServer.url)
|
|
2205
|
+
expect(response1.status).toBe(402)
|
|
2206
|
+
|
|
2207
|
+
const challenge1 = Challenge.fromResponse(response1, {
|
|
2208
|
+
methods: [tempo_client.charge()],
|
|
2209
|
+
})
|
|
2210
|
+
|
|
2211
|
+
const signature1 = await signTypedData(client, {
|
|
2212
|
+
account: accounts[1],
|
|
2213
|
+
domain: Proof.domain(chain.id),
|
|
2214
|
+
types: Proof.types,
|
|
2215
|
+
primaryType: 'Proof',
|
|
2216
|
+
message: Proof.message(challenge1.id),
|
|
2217
|
+
})
|
|
2218
|
+
|
|
2219
|
+
const credential1 = Credential.from({
|
|
2220
|
+
challenge: challenge1,
|
|
2221
|
+
payload: { signature: signature1, type: 'proof' as const },
|
|
2222
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2223
|
+
})
|
|
2224
|
+
|
|
2225
|
+
const response2 = await fetch(httpServer.url, {
|
|
2226
|
+
headers: { Authorization: Credential.serialize(credential1) },
|
|
2227
|
+
})
|
|
2228
|
+
expect(response2.status).toBe(200)
|
|
2229
|
+
|
|
2230
|
+
const response3 = await fetch(httpServer.url)
|
|
2231
|
+
expect(response3.status).toBe(402)
|
|
2232
|
+
|
|
2233
|
+
const challenge2 = Challenge.fromResponse(response3, {
|
|
2234
|
+
methods: [tempo_client.charge()],
|
|
2235
|
+
})
|
|
2236
|
+
expect(challenge2.id).not.toBe(challenge1.id)
|
|
2237
|
+
|
|
2238
|
+
const signature2 = await signTypedData(client, {
|
|
2239
|
+
account: accounts[1],
|
|
2240
|
+
domain: Proof.domain(chain.id),
|
|
2241
|
+
types: Proof.types,
|
|
2242
|
+
primaryType: 'Proof',
|
|
2243
|
+
message: Proof.message(challenge2.id),
|
|
2244
|
+
})
|
|
2245
|
+
|
|
2246
|
+
const credential2 = Credential.from({
|
|
2247
|
+
challenge: challenge2,
|
|
2248
|
+
payload: { signature: signature2, type: 'proof' as const },
|
|
2249
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2250
|
+
})
|
|
2251
|
+
|
|
2252
|
+
const response4 = await fetch(httpServer.url, {
|
|
2253
|
+
headers: { Authorization: Credential.serialize(credential2) },
|
|
2254
|
+
})
|
|
2255
|
+
expect(response4.status).toBe(200)
|
|
2256
|
+
|
|
2257
|
+
httpServer.close()
|
|
2258
|
+
})
|
|
2259
|
+
|
|
2260
|
+
test('behavior: rejects proof with wrong signer', async () => {
|
|
2261
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2262
|
+
const result = await Mppx_server.toNodeListener(
|
|
2263
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2264
|
+
)(req, res)
|
|
2265
|
+
if (result.status === 402) return
|
|
2266
|
+
res.end('OK')
|
|
2267
|
+
})
|
|
2268
|
+
|
|
2269
|
+
const response1 = await fetch(httpServer.url)
|
|
2270
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2271
|
+
methods: [tempo_client.charge()],
|
|
2272
|
+
})
|
|
2273
|
+
|
|
2274
|
+
// Sign with accounts[2] but claim source is accounts[1]
|
|
2275
|
+
const signature = await signTypedData(client, {
|
|
2276
|
+
account: accounts[2],
|
|
2277
|
+
domain: Proof.domain(chain.id),
|
|
2278
|
+
types: Proof.types,
|
|
2279
|
+
primaryType: 'Proof',
|
|
2280
|
+
message: Proof.message(challenge.id),
|
|
2281
|
+
})
|
|
2282
|
+
|
|
2283
|
+
const credential = Credential.from({
|
|
2284
|
+
challenge,
|
|
2285
|
+
payload: { signature, type: 'proof' as const },
|
|
2286
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2287
|
+
})
|
|
2288
|
+
|
|
2289
|
+
const response2 = await fetch(httpServer.url, {
|
|
2290
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2291
|
+
})
|
|
2292
|
+
expect(response2.status).toBe(402)
|
|
2293
|
+
|
|
2294
|
+
httpServer.close()
|
|
2295
|
+
})
|
|
2296
|
+
|
|
2297
|
+
test('behavior: rejects proof without source', async () => {
|
|
2298
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2299
|
+
const result = await Mppx_server.toNodeListener(
|
|
2300
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2301
|
+
)(req, res)
|
|
2302
|
+
if (result.status === 402) return
|
|
2303
|
+
res.end('OK')
|
|
2304
|
+
})
|
|
2305
|
+
|
|
2306
|
+
const response1 = await fetch(httpServer.url)
|
|
2307
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2308
|
+
methods: [tempo_client.charge()],
|
|
2309
|
+
})
|
|
2310
|
+
|
|
2311
|
+
const signature = await signTypedData(client, {
|
|
2312
|
+
account: accounts[1],
|
|
2313
|
+
domain: Proof.domain(chain.id),
|
|
2314
|
+
types: Proof.types,
|
|
2315
|
+
primaryType: 'Proof',
|
|
2316
|
+
message: Proof.message(challenge.id),
|
|
2317
|
+
})
|
|
2318
|
+
|
|
2319
|
+
const credential = Credential.from({
|
|
2320
|
+
challenge,
|
|
2321
|
+
payload: { signature, type: 'proof' as const },
|
|
2322
|
+
// no source
|
|
2323
|
+
})
|
|
2324
|
+
|
|
2325
|
+
const response2 = await fetch(httpServer.url, {
|
|
2326
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2327
|
+
})
|
|
2328
|
+
expect(response2.status).toBe(402)
|
|
2329
|
+
|
|
2330
|
+
httpServer.close()
|
|
2331
|
+
})
|
|
2332
|
+
|
|
2333
|
+
test('behavior: rejects transaction payload for zero-amount', async () => {
|
|
2334
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2335
|
+
const result = await Mppx_server.toNodeListener(
|
|
2336
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2337
|
+
)(req, res)
|
|
2338
|
+
if (result.status === 402) return
|
|
2339
|
+
res.end('OK')
|
|
2340
|
+
})
|
|
2341
|
+
|
|
2342
|
+
const response1 = await fetch(httpServer.url)
|
|
2343
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2344
|
+
methods: [tempo_client.charge()],
|
|
2345
|
+
})
|
|
2346
|
+
|
|
2347
|
+
const credential = Credential.from({
|
|
2348
|
+
challenge,
|
|
2349
|
+
payload: { signature: '0xdead', type: 'transaction' as const },
|
|
2350
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2351
|
+
})
|
|
2352
|
+
|
|
2353
|
+
const response2 = await fetch(httpServer.url, {
|
|
2354
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2355
|
+
})
|
|
2356
|
+
expect(response2.status).toBe(402)
|
|
2357
|
+
const body = (await response2.json()) as { detail: string }
|
|
2358
|
+
expect(body.detail).toContain('Zero-amount challenges require a proof credential.')
|
|
2359
|
+
|
|
2360
|
+
httpServer.close()
|
|
2361
|
+
})
|
|
2362
|
+
|
|
2363
|
+
test('behavior: rejects hash payload for zero-amount', async () => {
|
|
2364
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2365
|
+
const result = await Mppx_server.toNodeListener(
|
|
2366
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2367
|
+
)(req, res)
|
|
2368
|
+
if (result.status === 402) return
|
|
2369
|
+
res.end('OK')
|
|
2370
|
+
})
|
|
2371
|
+
|
|
2372
|
+
const response1 = await fetch(httpServer.url)
|
|
2373
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2374
|
+
methods: [tempo_client.charge()],
|
|
2375
|
+
})
|
|
2376
|
+
|
|
2377
|
+
const credential = Credential.from({
|
|
2378
|
+
challenge,
|
|
2379
|
+
payload: {
|
|
2380
|
+
hash: '0x0000000000000000000000000000000000000000000000000000000000000001',
|
|
2381
|
+
type: 'hash' as const,
|
|
2382
|
+
},
|
|
2383
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2384
|
+
})
|
|
2385
|
+
|
|
2386
|
+
const response2 = await fetch(httpServer.url, {
|
|
2387
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2388
|
+
})
|
|
2389
|
+
expect(response2.status).toBe(402)
|
|
2390
|
+
const body = (await response2.json()) as { detail: string }
|
|
2391
|
+
expect(body.detail).toContain('Zero-amount challenges require a proof credential.')
|
|
2392
|
+
|
|
2393
|
+
httpServer.close()
|
|
2394
|
+
})
|
|
2395
|
+
|
|
2396
|
+
test('behavior: rejects proof payload for non-zero amount', async () => {
|
|
2397
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2398
|
+
const result = await Mppx_server.toNodeListener(
|
|
2399
|
+
server.charge({ amount: '1', decimals: 6 }),
|
|
2400
|
+
)(req, res)
|
|
2401
|
+
if (result.status === 402) return
|
|
2402
|
+
res.end('OK')
|
|
2403
|
+
})
|
|
2404
|
+
|
|
2405
|
+
const response1 = await fetch(httpServer.url)
|
|
2406
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2407
|
+
methods: [tempo_client.charge()],
|
|
2408
|
+
})
|
|
2409
|
+
|
|
2410
|
+
const signature = await signTypedData(client, {
|
|
2411
|
+
account: accounts[1],
|
|
2412
|
+
domain: Proof.domain(chain.id),
|
|
2413
|
+
types: Proof.types,
|
|
2414
|
+
primaryType: 'Proof',
|
|
2415
|
+
message: Proof.message(challenge.id),
|
|
2416
|
+
})
|
|
2417
|
+
|
|
2418
|
+
const credential = Credential.from({
|
|
2419
|
+
challenge,
|
|
2420
|
+
payload: { signature, type: 'proof' as const },
|
|
2421
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2422
|
+
})
|
|
2423
|
+
|
|
2424
|
+
const response2 = await fetch(httpServer.url, {
|
|
2425
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2426
|
+
})
|
|
2427
|
+
expect(response2.status).toBe(402)
|
|
2428
|
+
const body = (await response2.json()) as { detail: string }
|
|
2429
|
+
expect(body.detail).toContain('Proof credentials are only valid for zero-amount challenges.')
|
|
2430
|
+
|
|
2431
|
+
httpServer.close()
|
|
2432
|
+
})
|
|
2433
|
+
|
|
2434
|
+
test('behavior: receipt reference is the challenge ID', async () => {
|
|
2435
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2436
|
+
const result = await Mppx_server.toNodeListener(
|
|
2437
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2438
|
+
)(req, res)
|
|
2439
|
+
if (result.status === 402) return
|
|
2440
|
+
res.end('OK')
|
|
2441
|
+
})
|
|
2442
|
+
|
|
2443
|
+
const response1 = await fetch(httpServer.url)
|
|
2444
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2445
|
+
methods: [tempo_client.charge()],
|
|
2446
|
+
})
|
|
2447
|
+
|
|
2448
|
+
const signature = await signTypedData(client, {
|
|
2449
|
+
account: accounts[1],
|
|
2450
|
+
domain: Proof.domain(chain.id),
|
|
2451
|
+
types: Proof.types,
|
|
2452
|
+
primaryType: 'Proof',
|
|
2453
|
+
message: Proof.message(challenge.id),
|
|
2454
|
+
})
|
|
2455
|
+
|
|
2456
|
+
const credential = Credential.from({
|
|
2457
|
+
challenge,
|
|
2458
|
+
payload: { signature, type: 'proof' as const },
|
|
2459
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2460
|
+
})
|
|
2461
|
+
|
|
2462
|
+
const response2 = await fetch(httpServer.url, {
|
|
2463
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2464
|
+
})
|
|
2465
|
+
expect(response2.status).toBe(200)
|
|
2466
|
+
const receipt = Receipt.fromResponse(response2)
|
|
2467
|
+
expect(receipt.reference).toBe(challenge.id)
|
|
2468
|
+
|
|
2469
|
+
httpServer.close()
|
|
2470
|
+
})
|
|
2471
|
+
|
|
2472
|
+
test('behavior: rejects proof signed with wrong chainId domain', async () => {
|
|
2473
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2474
|
+
const result = await Mppx_server.toNodeListener(
|
|
2475
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2476
|
+
)(req, res)
|
|
2477
|
+
if (result.status === 402) return
|
|
2478
|
+
res.end('OK')
|
|
2479
|
+
})
|
|
2480
|
+
|
|
2481
|
+
const response1 = await fetch(httpServer.url)
|
|
2482
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2483
|
+
methods: [tempo_client.charge()],
|
|
2484
|
+
})
|
|
2485
|
+
|
|
2486
|
+
// Sign with a different chainId in the EIP-712 domain
|
|
2487
|
+
const signature = await signTypedData(client, {
|
|
2488
|
+
account: accounts[1],
|
|
2489
|
+
domain: Proof.domain(99999),
|
|
2490
|
+
types: Proof.types,
|
|
2491
|
+
primaryType: 'Proof',
|
|
2492
|
+
message: Proof.message(challenge.id),
|
|
2493
|
+
})
|
|
2494
|
+
|
|
2495
|
+
const credential = Credential.from({
|
|
2496
|
+
challenge,
|
|
2497
|
+
payload: { signature, type: 'proof' as const },
|
|
2498
|
+
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
|
|
2499
|
+
})
|
|
2500
|
+
|
|
2501
|
+
const response2 = await fetch(httpServer.url, {
|
|
2502
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2503
|
+
})
|
|
2504
|
+
expect(response2.status).toBe(402)
|
|
2505
|
+
|
|
2506
|
+
httpServer.close()
|
|
2507
|
+
})
|
|
2508
|
+
|
|
2509
|
+
test('behavior: rejects proof with mismatched source DID chainId', async () => {
|
|
2510
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2511
|
+
const result = await Mppx_server.toNodeListener(
|
|
2512
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2513
|
+
)(req, res)
|
|
2514
|
+
if (result.status === 402) return
|
|
2515
|
+
res.end('OK')
|
|
2516
|
+
})
|
|
2517
|
+
|
|
2518
|
+
const response1 = await fetch(httpServer.url)
|
|
2519
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2520
|
+
methods: [tempo_client.charge()],
|
|
2521
|
+
})
|
|
2522
|
+
|
|
2523
|
+
const signature = await signTypedData(client, {
|
|
2524
|
+
account: accounts[1],
|
|
2525
|
+
domain: Proof.domain(chain.id),
|
|
2526
|
+
types: Proof.types,
|
|
2527
|
+
primaryType: 'Proof',
|
|
2528
|
+
message: Proof.message(challenge.id),
|
|
2529
|
+
})
|
|
2530
|
+
|
|
2531
|
+
const credential = Credential.from({
|
|
2532
|
+
challenge,
|
|
2533
|
+
payload: { signature, type: 'proof' as const },
|
|
2534
|
+
source: `did:pkh:eip155:1:${accounts[1].address}`,
|
|
2535
|
+
})
|
|
2536
|
+
|
|
2537
|
+
const response2 = await fetch(httpServer.url, {
|
|
2538
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2539
|
+
})
|
|
2540
|
+
expect(response2.status).toBe(402)
|
|
2541
|
+
|
|
2542
|
+
httpServer.close()
|
|
2543
|
+
})
|
|
2544
|
+
|
|
2545
|
+
test('behavior: rejects proof with malformed source DID', async () => {
|
|
2546
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2547
|
+
const result = await Mppx_server.toNodeListener(
|
|
2548
|
+
server.charge({ amount: '0', decimals: 6 }),
|
|
2549
|
+
)(req, res)
|
|
2550
|
+
if (result.status === 402) return
|
|
2551
|
+
res.end('OK')
|
|
2552
|
+
})
|
|
2553
|
+
|
|
2554
|
+
const response1 = await fetch(httpServer.url)
|
|
2555
|
+
const challenge = Challenge.fromResponse(response1, {
|
|
2556
|
+
methods: [tempo_client.charge()],
|
|
2557
|
+
})
|
|
2558
|
+
|
|
2559
|
+
const signature = await signTypedData(client, {
|
|
2560
|
+
account: accounts[1],
|
|
2561
|
+
domain: Proof.domain(chain.id),
|
|
2562
|
+
types: Proof.types,
|
|
2563
|
+
primaryType: 'Proof',
|
|
2564
|
+
message: Proof.message(challenge.id),
|
|
2565
|
+
})
|
|
2566
|
+
|
|
2567
|
+
const credential = Credential.from({
|
|
2568
|
+
challenge,
|
|
2569
|
+
payload: { signature, type: 'proof' as const },
|
|
2570
|
+
source: 'not-a-valid-did',
|
|
2571
|
+
})
|
|
2572
|
+
|
|
2573
|
+
const response2 = await fetch(httpServer.url, {
|
|
2574
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
2575
|
+
})
|
|
2576
|
+
expect(response2.status).toBe(402)
|
|
2577
|
+
|
|
2578
|
+
httpServer.close()
|
|
2579
|
+
})
|
|
2580
|
+
})
|
|
2581
|
+
|
|
1923
2582
|
describe('intent: unknown', () => {
|
|
1924
2583
|
test('behavior: returns 402 for invalid payload schema', async () => {
|
|
1925
2584
|
const httpServer = await Http.createServer(async (req, res) => {
|