tapimo 0.4.3 → 0.4.4
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/dist/App.d.ts +2491 -2861
- package/dist/App.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/apps/data/App.d.ts +2354 -2724
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +3 -110
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +1 -302
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +92 -92
- package/dist/apps/data/routes/funding.d.ts +138 -730
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +7 -241
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts +58 -58
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/management/App.d.ts +855 -855
- package/dist/apps/management/routes/api-keys.d.ts +40 -40
- package/dist/apps/management/routes/billing.d.ts +207 -207
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/orgs.d.ts +92 -92
- package/dist/apps/management/routes/projects.d.ts +28 -28
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +3 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +4 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/internal/Auth.d.ts +33 -22
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +40 -11
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Errors.d.ts +6 -0
- package/dist/internal/Errors.d.ts.map +1 -0
- package/dist/internal/Errors.js +16 -0
- package/dist/internal/Errors.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +15 -213
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +1 -72
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Store.d.ts +61 -16
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +72 -17
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/dist/internal/index.d.ts +27 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +27 -0
- package/dist/internal/index.js.map +1 -0
- package/package.json +8 -3
- package/src/App.test.ts +60 -4
- package/src/Client.test-d.ts +0 -47
- package/src/apps/data/FundingRouteProviders.test.ts +37 -297
- package/src/apps/data/FundingRouteProviders.ts +3 -435
- package/src/apps/data/routes/funding.test.ts +7 -109
- package/src/apps/data/routes/funding.ts +8 -323
- package/src/apps/data/routes/indexer.test.ts +2 -2
- package/src/apps/data/routes/webhooks.test.ts +2 -2
- package/src/apps/mcp.test.ts +55 -14
- package/src/apps/mcp.ts +3 -1
- package/src/cloudflare.ts +5 -0
- package/src/internal/Auth.test.ts +397 -16
- package/src/internal/Auth.ts +76 -32
- package/src/internal/FundingRoutes.test.ts +8 -34
- package/src/internal/FundingRoutes.ts +1 -174
- package/src/internal/Log.test.ts +4 -4
- package/src/internal/Store.test-d.ts +15 -0
- package/src/internal/Store.test.ts +153 -2
- package/src/internal/Store.ts +157 -41
- package/src/internal/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Hono, type Context } from 'hono'
|
|
2
2
|
import { requestId } from 'hono/request-id'
|
|
3
|
+
import { Challenge, Receipt } from 'mppx'
|
|
3
4
|
import { Mppx, tempo } from 'mppx/hono'
|
|
4
|
-
import {
|
|
5
|
+
import type { Session } from 'mppx/tempo'
|
|
6
|
+
import { RateLimit, Store } from 'tapimo'
|
|
7
|
+
import { Actions } from 'viem/tempo'
|
|
5
8
|
import * as ApiKey from '../ApiKey.js'
|
|
6
9
|
import type * as Db from '../db/Db.js'
|
|
7
10
|
import * as Memberships from '../db/tables/memberships.js'
|
|
@@ -14,6 +17,7 @@ import * as Scope from '../Scope.js'
|
|
|
14
17
|
import * as Viem from './Viem.js'
|
|
15
18
|
import * as TestApp from '../../test/App.js'
|
|
16
19
|
import { createClient as createMppClient } from '../../test/Mppx.js'
|
|
20
|
+
import * as TestStore from '../../test/Store.js'
|
|
17
21
|
import * as Tempo from '../../test/Tempo.js'
|
|
18
22
|
|
|
19
23
|
const key = {
|
|
@@ -559,7 +563,7 @@ describe('require', () => {
|
|
|
559
563
|
const policy: Auth.require.Policy = {
|
|
560
564
|
apiKey: { scopes: [Scope.wildcard] },
|
|
561
565
|
...(options.public === false ? {} : { public: { rateLimit: { perMinute: 60 } } }),
|
|
562
|
-
...(options.mpp ? { mpp: {
|
|
566
|
+
...(options.mpp ? { mpp: { session: { amount: '1' } } } : {}),
|
|
563
567
|
}
|
|
564
568
|
const app = new Hono<Auth.Environment>()
|
|
565
569
|
app.use('*', async (c, next) => {
|
|
@@ -739,6 +743,121 @@ describe('require', () => {
|
|
|
739
743
|
})
|
|
740
744
|
|
|
741
745
|
describe('mpp', () => {
|
|
746
|
+
test('uses a default paid-request limit of 100 per minute', async () => {
|
|
747
|
+
const rateLimit = underQuota()
|
|
748
|
+
const consume = vi.spyOn(rateLimit, 'consume')
|
|
749
|
+
const app = createApp({
|
|
750
|
+
auth: {
|
|
751
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
752
|
+
mpp: createMppHandler(),
|
|
753
|
+
rateLimit,
|
|
754
|
+
},
|
|
755
|
+
policy: { mpp: { session: { amount: '1' } } },
|
|
756
|
+
})
|
|
757
|
+
const response = await app.request('/protected', {
|
|
758
|
+
headers: {
|
|
759
|
+
authorization: 'Payment paid',
|
|
760
|
+
'x-forwarded-for': '203.0.113.10',
|
|
761
|
+
},
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
expect(response.status).toBe(200)
|
|
765
|
+
expect(response.headers.get('RateLimit-Limit')).toBe('100')
|
|
766
|
+
expect(response.headers.get('RateLimit-Scope')).toBe('mpp')
|
|
767
|
+
expect(consume).toHaveBeenCalledWith({
|
|
768
|
+
key: 'mpp:public:203.0.113.10',
|
|
769
|
+
limit: { perMinute: 100 },
|
|
770
|
+
})
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
test('buckets paid API-key requests by key ID', async () => {
|
|
774
|
+
const rateLimit = underQuota()
|
|
775
|
+
const consume = vi.spyOn(rateLimit, 'consume')
|
|
776
|
+
const app = createApp({
|
|
777
|
+
auth: {
|
|
778
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
779
|
+
mpp: createMppHandler(),
|
|
780
|
+
rateLimit,
|
|
781
|
+
},
|
|
782
|
+
policy: {
|
|
783
|
+
apiKey: { scopes: ['data:read'] },
|
|
784
|
+
mpp: { session: { amount: '1' } },
|
|
785
|
+
},
|
|
786
|
+
})
|
|
787
|
+
const response = await app.request('/protected', {
|
|
788
|
+
headers: {
|
|
789
|
+
authorization: 'Payment paid',
|
|
790
|
+
'tempo-api-key': token,
|
|
791
|
+
'x-forwarded-for': '203.0.113.10',
|
|
792
|
+
},
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
expect(response.status).toBe(200)
|
|
796
|
+
expect(consume).toHaveBeenCalledWith({
|
|
797
|
+
key: 'mpp:api_key:key_test',
|
|
798
|
+
limit: { perMinute: 100 },
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
test('uses the configured global paid-request limit', async () => {
|
|
803
|
+
const app = createApp({
|
|
804
|
+
auth: {
|
|
805
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
806
|
+
mpp: createMppHandler({ rateLimit: { perMinute: 25 } }),
|
|
807
|
+
rateLimit: underQuota(),
|
|
808
|
+
},
|
|
809
|
+
policy: { mpp: { session: { amount: '1' } } },
|
|
810
|
+
})
|
|
811
|
+
const response = await app.request('/protected', {
|
|
812
|
+
headers: { authorization: 'Payment paid' },
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
expect(response.status).toBe(200)
|
|
816
|
+
expect(response.headers.get('RateLimit-Limit')).toBe('25')
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
test('route paid-request limit overrides the global limit', async () => {
|
|
820
|
+
const app = createApp({
|
|
821
|
+
auth: {
|
|
822
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
823
|
+
mpp: createMppHandler({ rateLimit: { perMinute: 25 } }),
|
|
824
|
+
rateLimit: underQuota(),
|
|
825
|
+
},
|
|
826
|
+
policy: {
|
|
827
|
+
mpp: {
|
|
828
|
+
rateLimit: { perMinute: 5 },
|
|
829
|
+
session: { amount: '1' },
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
})
|
|
833
|
+
const response = await app.request('/protected', {
|
|
834
|
+
headers: { authorization: 'Payment paid' },
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
expect(response.status).toBe(200)
|
|
838
|
+
expect(response.headers.get('RateLimit-Limit')).toBe('5')
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
test('rejects paid requests over quota before accepting payment', async () => {
|
|
842
|
+
const onPayment = vi.fn()
|
|
843
|
+
const app = createApp({
|
|
844
|
+
auth: {
|
|
845
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
846
|
+
mpp: createMppHandler({ onPayment }),
|
|
847
|
+
rateLimit: overQuota(),
|
|
848
|
+
},
|
|
849
|
+
policy: { mpp: { session: { amount: '1' } } },
|
|
850
|
+
})
|
|
851
|
+
const response = await app.request('/protected', {
|
|
852
|
+
headers: { authorization: 'Payment paid' },
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
expect(response.status).toBe(429)
|
|
856
|
+
expect(response.headers.get('RateLimit-Limit')).toBe('100')
|
|
857
|
+
expect((await response.json()).error.code).toBe('rate_limit_exceeded')
|
|
858
|
+
expect(onPayment).not.toHaveBeenCalled()
|
|
859
|
+
})
|
|
860
|
+
|
|
742
861
|
test('returns payment challenge for public over-quota requests', async () => {
|
|
743
862
|
const mpp = createMppServer()
|
|
744
863
|
const app = createApp({
|
|
@@ -748,7 +867,7 @@ describe('require', () => {
|
|
|
748
867
|
rateLimit: overQuota(),
|
|
749
868
|
},
|
|
750
869
|
policy: {
|
|
751
|
-
mpp: {
|
|
870
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
752
871
|
public: { rateLimit: { perMinute: 1 } },
|
|
753
872
|
},
|
|
754
873
|
})
|
|
@@ -760,16 +879,16 @@ describe('require', () => {
|
|
|
760
879
|
).toMatchInlineSnapshot(`true`)
|
|
761
880
|
})
|
|
762
881
|
|
|
763
|
-
test('accepts paid public retry without consuming quota', async () => {
|
|
882
|
+
test('accepts paid public retry without consuming public quota', async () => {
|
|
764
883
|
const mpp = createMppServer()
|
|
765
884
|
const app = createApp({
|
|
766
885
|
auth: {
|
|
767
886
|
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
768
887
|
mpp,
|
|
769
|
-
rateLimit:
|
|
888
|
+
rateLimit: overQuotaExceptMpp(),
|
|
770
889
|
},
|
|
771
890
|
policy: {
|
|
772
|
-
mpp: {
|
|
891
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
773
892
|
public: { rateLimit: { perMinute: 1 } },
|
|
774
893
|
},
|
|
775
894
|
})
|
|
@@ -804,7 +923,7 @@ describe('require', () => {
|
|
|
804
923
|
rateLimit: { perMinute: 1 },
|
|
805
924
|
scopes: ['data:read'],
|
|
806
925
|
},
|
|
807
|
-
mpp: {
|
|
926
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
808
927
|
},
|
|
809
928
|
})
|
|
810
929
|
const response = await app.request('/protected', {
|
|
@@ -823,14 +942,14 @@ describe('require', () => {
|
|
|
823
942
|
auth: {
|
|
824
943
|
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
825
944
|
mpp,
|
|
826
|
-
rateLimit:
|
|
945
|
+
rateLimit: overQuotaExceptMpp(),
|
|
827
946
|
},
|
|
828
947
|
policy: {
|
|
829
948
|
apiKey: {
|
|
830
949
|
rateLimit: { perMinute: 1 },
|
|
831
950
|
scopes: ['data:read'],
|
|
832
951
|
},
|
|
833
|
-
mpp: {
|
|
952
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
834
953
|
},
|
|
835
954
|
})
|
|
836
955
|
const response = await createMppClient(app).fetch('http://tempo-api.test/protected', {
|
|
@@ -855,6 +974,220 @@ describe('require', () => {
|
|
|
855
974
|
}
|
|
856
975
|
`)
|
|
857
976
|
})
|
|
977
|
+
|
|
978
|
+
test('session challenge advertises the configured request defaults', async () => {
|
|
979
|
+
const mpp = createMppServer()
|
|
980
|
+
const app = createApp({
|
|
981
|
+
auth: {
|
|
982
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
983
|
+
mpp,
|
|
984
|
+
rateLimit: overQuota(),
|
|
985
|
+
},
|
|
986
|
+
policy: {
|
|
987
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
988
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
989
|
+
},
|
|
990
|
+
})
|
|
991
|
+
const response = await app.request('/protected')
|
|
992
|
+
const challenge = Challenge.fromHeaders(response.headers)
|
|
993
|
+
|
|
994
|
+
expect(response.status).toBe(402)
|
|
995
|
+
expect(challenge.method).toBe('tempo')
|
|
996
|
+
expect(challenge.intent).toBe('session')
|
|
997
|
+
expect(challenge.realm).toBe('tempo-api-test')
|
|
998
|
+
expect(challenge.description).toBe('Protected data')
|
|
999
|
+
expect(challenge.request).toMatchObject({
|
|
1000
|
+
// Wire amounts are raw token units: '1' at 6 decimals.
|
|
1001
|
+
amount: '1000000',
|
|
1002
|
+
currency: Tempo.currency,
|
|
1003
|
+
methodDetails: {
|
|
1004
|
+
chainId: Tempo.chain.id,
|
|
1005
|
+
escrowContract: '0x4d50500000000000000000000000000000000000',
|
|
1006
|
+
sessionProtocol: 'v2',
|
|
1007
|
+
},
|
|
1008
|
+
recipient: Tempo.accounts[2].address,
|
|
1009
|
+
unitType: 'request',
|
|
1010
|
+
})
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
test('route session overrides propagate to the challenge', async () => {
|
|
1014
|
+
const mpp = createMppServer()
|
|
1015
|
+
const app = createApp({
|
|
1016
|
+
auth: {
|
|
1017
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1018
|
+
mpp,
|
|
1019
|
+
rateLimit: overQuota(),
|
|
1020
|
+
},
|
|
1021
|
+
policy: {
|
|
1022
|
+
mpp: { session: { amount: '2.5', suggestedDeposit: '10' } },
|
|
1023
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1024
|
+
},
|
|
1025
|
+
})
|
|
1026
|
+
const response = await app.request('/protected')
|
|
1027
|
+
const challenge = Challenge.fromHeaders(response.headers)
|
|
1028
|
+
|
|
1029
|
+
expect(response.status).toBe(402)
|
|
1030
|
+
expect(challenge.request).toMatchObject({
|
|
1031
|
+
// Wire amounts are raw token units at 6 decimals.
|
|
1032
|
+
amount: '2500000',
|
|
1033
|
+
suggestedDeposit: '10000000',
|
|
1034
|
+
})
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
test('paid retry opens a funded on-chain channel', async () => {
|
|
1038
|
+
const mpp = createMppServer()
|
|
1039
|
+
const app = createApp({
|
|
1040
|
+
auth: {
|
|
1041
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1042
|
+
mpp,
|
|
1043
|
+
rateLimit: overQuotaExceptMpp(),
|
|
1044
|
+
},
|
|
1045
|
+
policy: {
|
|
1046
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
1047
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1048
|
+
},
|
|
1049
|
+
})
|
|
1050
|
+
const response = await createMppClient(app).fetch('http://tempo-api.test/protected')
|
|
1051
|
+
|
|
1052
|
+
expect(response.status).toBe(200)
|
|
1053
|
+
const receipt = sessionReceipt(response)
|
|
1054
|
+
expect(receipt.method).toBe('tempo')
|
|
1055
|
+
expect(receipt.intent).toBe('session')
|
|
1056
|
+
expect(receipt.status).toBe('success')
|
|
1057
|
+
expect(receipt.channelId).toMatch(/^0x[0-9a-fA-F]{64}$/)
|
|
1058
|
+
// One paid request at amount '1' with 6 decimals.
|
|
1059
|
+
expect(receipt.spent).toBe('1000000')
|
|
1060
|
+
expect(BigInt(receipt.acceptedCumulative)).toBeGreaterThanOrEqual(1_000_000n)
|
|
1061
|
+
|
|
1062
|
+
const state = await Actions.channel.getStates(Tempo.client, {
|
|
1063
|
+
channel: receipt.channelId,
|
|
1064
|
+
})
|
|
1065
|
+
// The client deposits the request amount when no deposit is suggested.
|
|
1066
|
+
expect(state.deposit).toBe(1_000_000n)
|
|
1067
|
+
// No settlement schedule configured, so nothing settles inline.
|
|
1068
|
+
expect(state.settled).toBe(0n)
|
|
1069
|
+
expect(state.closeRequestedAt).toBe(0)
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
test('reuses the session channel across paid requests', async () => {
|
|
1073
|
+
const mpp = createMppServer()
|
|
1074
|
+
const app = createApp({
|
|
1075
|
+
auth: {
|
|
1076
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1077
|
+
mpp,
|
|
1078
|
+
rateLimit: overQuotaExceptMpp(),
|
|
1079
|
+
},
|
|
1080
|
+
policy: {
|
|
1081
|
+
// The suggested deposit pre-funds the channel for both requests.
|
|
1082
|
+
mpp: { session: { amount: '1', suggestedDeposit: '2' } },
|
|
1083
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1084
|
+
},
|
|
1085
|
+
})
|
|
1086
|
+
const client = createMppClient(app)
|
|
1087
|
+
|
|
1088
|
+
const first = await client.fetch('http://tempo-api.test/protected')
|
|
1089
|
+
const second = await client.fetch('http://tempo-api.test/protected')
|
|
1090
|
+
|
|
1091
|
+
expect(first.status).toBe(200)
|
|
1092
|
+
expect(second.status).toBe(200)
|
|
1093
|
+
|
|
1094
|
+
const receipt1 = sessionReceipt(first)
|
|
1095
|
+
const receipt2 = sessionReceipt(second)
|
|
1096
|
+
expect(receipt2.channelId).toBe(receipt1.channelId)
|
|
1097
|
+
expect(BigInt(receipt2.acceptedCumulative)).toBeGreaterThan(
|
|
1098
|
+
BigInt(receipt1.acceptedCumulative),
|
|
1099
|
+
)
|
|
1100
|
+
expect(receipt2.spent).toBe('2000000')
|
|
1101
|
+
|
|
1102
|
+
const state = await Actions.channel.getStates(Tempo.client, {
|
|
1103
|
+
channel: receipt1.channelId,
|
|
1104
|
+
})
|
|
1105
|
+
expect(state.deposit).toBe(2_000_000n)
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
test('reuses persisted session state after server restart', async () => {
|
|
1109
|
+
const { namespace } = TestStore.durableObjectNamespace()
|
|
1110
|
+
const createStore = () => Store.durableObject(namespace)
|
|
1111
|
+
let app = createApp({
|
|
1112
|
+
auth: {
|
|
1113
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1114
|
+
mpp: createMppServer({ store: createStore() }),
|
|
1115
|
+
rateLimit: overQuotaExceptMpp(),
|
|
1116
|
+
},
|
|
1117
|
+
policy: {
|
|
1118
|
+
mpp: { session: { amount: '1', suggestedDeposit: '2' } },
|
|
1119
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1120
|
+
},
|
|
1121
|
+
})
|
|
1122
|
+
const client = createMppClient({ fetch: (request) => app.fetch(request) })
|
|
1123
|
+
|
|
1124
|
+
const first = await client.fetch('http://tempo-api.test/protected')
|
|
1125
|
+
app = createApp({
|
|
1126
|
+
auth: {
|
|
1127
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1128
|
+
mpp: createMppServer({ store: createStore() }),
|
|
1129
|
+
rateLimit: overQuotaExceptMpp(),
|
|
1130
|
+
},
|
|
1131
|
+
policy: {
|
|
1132
|
+
mpp: { session: { amount: '1', suggestedDeposit: '2' } },
|
|
1133
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1134
|
+
},
|
|
1135
|
+
})
|
|
1136
|
+
const second = await client.fetch('http://tempo-api.test/protected')
|
|
1137
|
+
|
|
1138
|
+
expect(first.status).toBe(200)
|
|
1139
|
+
expect(second.status).toBe(200)
|
|
1140
|
+
expect(sessionReceipt(second)).toMatchObject({
|
|
1141
|
+
channelId: sessionReceipt(first).channelId,
|
|
1142
|
+
spent: '2000000',
|
|
1143
|
+
})
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
test('rejects a voucher that exceeds the channel deposit', async () => {
|
|
1147
|
+
const mpp = createMppServer()
|
|
1148
|
+
const app = createApp({
|
|
1149
|
+
auth: {
|
|
1150
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1151
|
+
mpp,
|
|
1152
|
+
rateLimit: overQuotaExceptMpp(),
|
|
1153
|
+
},
|
|
1154
|
+
policy: {
|
|
1155
|
+
mpp: { session: { amount: '1' } },
|
|
1156
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1157
|
+
},
|
|
1158
|
+
})
|
|
1159
|
+
const client = createMppClient(app)
|
|
1160
|
+
|
|
1161
|
+
// The opening deposit covers one request; the second voucher overruns
|
|
1162
|
+
// it and the low-level client cannot top up, so the server re-challenges.
|
|
1163
|
+
const first = await client.fetch('http://tempo-api.test/protected')
|
|
1164
|
+
const second = await client.fetch('http://tempo-api.test/protected')
|
|
1165
|
+
|
|
1166
|
+
expect(first.status).toBe(200)
|
|
1167
|
+
expect(second.status).toBe(402)
|
|
1168
|
+
expect(second.headers.get('www-authenticate')?.startsWith('Payment ')).toBe(true)
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
test('rejects a malformed payment credential with a fresh challenge', async () => {
|
|
1172
|
+
const mpp = createMppServer()
|
|
1173
|
+
const app = createApp({
|
|
1174
|
+
auth: {
|
|
1175
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1176
|
+
mpp,
|
|
1177
|
+
rateLimit: underQuota(),
|
|
1178
|
+
},
|
|
1179
|
+
policy: {
|
|
1180
|
+
mpp: { session: { amount: '1' } },
|
|
1181
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1182
|
+
},
|
|
1183
|
+
})
|
|
1184
|
+
const response = await app.request('/protected', {
|
|
1185
|
+
headers: { authorization: 'Payment not-a-credential' },
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
expect(response.status).toBe(402)
|
|
1189
|
+
expect(response.headers.get('www-authenticate')?.startsWith('Payment ')).toBe(true)
|
|
1190
|
+
})
|
|
858
1191
|
})
|
|
859
1192
|
|
|
860
1193
|
describe('rateLimit', () => {
|
|
@@ -1306,7 +1639,7 @@ describe('require', () => {
|
|
|
1306
1639
|
},
|
|
1307
1640
|
policy: {
|
|
1308
1641
|
apiKey: { rateLimit: { perMinute: 1 }, scopes: ['data:read'] },
|
|
1309
|
-
mpp: {
|
|
1642
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
1310
1643
|
},
|
|
1311
1644
|
})
|
|
1312
1645
|
const response = await app.request('/protected', {
|
|
@@ -1516,15 +1849,18 @@ function createApp(options: Options = {}) {
|
|
|
1516
1849
|
return app
|
|
1517
1850
|
}
|
|
1518
1851
|
|
|
1519
|
-
function createMppServer() {
|
|
1852
|
+
function createMppServer(options: createMppServer.Options = {}) {
|
|
1520
1853
|
return Mppx.create({
|
|
1521
1854
|
methods: [
|
|
1522
|
-
tempo.
|
|
1855
|
+
tempo.session({
|
|
1856
|
+
bootstrap: false,
|
|
1523
1857
|
chainId: Tempo.chain.id,
|
|
1524
1858
|
currency: Tempo.currency,
|
|
1525
1859
|
decimals: 6,
|
|
1526
1860
|
getClient: () => Tempo.client,
|
|
1527
1861
|
recipient: Tempo.accounts[2].address,
|
|
1862
|
+
...options,
|
|
1863
|
+
unitType: 'request',
|
|
1528
1864
|
}),
|
|
1529
1865
|
],
|
|
1530
1866
|
realm: 'tempo-api-test',
|
|
@@ -1532,6 +1868,34 @@ function createMppServer() {
|
|
|
1532
1868
|
})
|
|
1533
1869
|
}
|
|
1534
1870
|
|
|
1871
|
+
declare namespace createMppServer {
|
|
1872
|
+
type Options = Pick<NonNullable<Parameters<typeof tempo.session>[0]>, 'store'>
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function createMppHandler(
|
|
1876
|
+
options: createMppHandler.Options = {},
|
|
1877
|
+
): NonNullable<Auth.Context['mpp']> {
|
|
1878
|
+
return {
|
|
1879
|
+
...(options.rateLimit ? { rateLimit: options.rateLimit } : {}),
|
|
1880
|
+
session: () => async (_c, next) => {
|
|
1881
|
+
options.onPayment?.()
|
|
1882
|
+
await next()
|
|
1883
|
+
},
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
declare namespace createMppHandler {
|
|
1888
|
+
type Options = {
|
|
1889
|
+
onPayment?: (() => void) | undefined
|
|
1890
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** Reads the session receipt attached to a paid response. */
|
|
1895
|
+
function sessionReceipt(response: Response) {
|
|
1896
|
+
return Receipt.fromResponse(response) as Session.Precompile.Protocol.SessionReceipt
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1535
1899
|
function overQuota(): RateLimit.Store {
|
|
1536
1900
|
return {
|
|
1537
1901
|
async consume(options) {
|
|
@@ -1545,12 +1909,29 @@ function overQuota(): RateLimit.Store {
|
|
|
1545
1909
|
}
|
|
1546
1910
|
}
|
|
1547
1911
|
|
|
1548
|
-
function
|
|
1549
|
-
let consumed = false
|
|
1912
|
+
function underQuota(): RateLimit.Store {
|
|
1550
1913
|
return {
|
|
1551
1914
|
async consume(options) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1915
|
+
return {
|
|
1916
|
+
allowed: true,
|
|
1917
|
+
limit: options.limit.perMinute,
|
|
1918
|
+
remaining: Math.max(options.limit.perMinute - 1, 0),
|
|
1919
|
+
reset: Math.ceil(Date.now() / 1_000) + 60,
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function overQuotaExceptMpp(): RateLimit.Store {
|
|
1926
|
+
return {
|
|
1927
|
+
async consume(options) {
|
|
1928
|
+
if (options.key.startsWith('mpp:'))
|
|
1929
|
+
return {
|
|
1930
|
+
allowed: true,
|
|
1931
|
+
limit: options.limit.perMinute,
|
|
1932
|
+
remaining: Math.max(options.limit.perMinute - 1, 0),
|
|
1933
|
+
reset: Math.ceil(Date.now() / 1_000) + 60,
|
|
1934
|
+
}
|
|
1554
1935
|
return {
|
|
1555
1936
|
allowed: false,
|
|
1556
1937
|
limit: options.limit.perMinute,
|