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.
Files changed (77) hide show
  1. package/dist/App.d.ts +2491 -2861
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  4. package/dist/apps/data/App.d.ts +2354 -2724
  5. package/dist/apps/data/App.d.ts.map +1 -1
  6. package/dist/apps/data/FundingRouteProviders.d.ts +3 -110
  7. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  8. package/dist/apps/data/FundingRouteProviders.js +1 -302
  9. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  10. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  11. package/dist/apps/data/routes/fee-amm.d.ts +92 -92
  12. package/dist/apps/data/routes/funding.d.ts +138 -730
  13. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  14. package/dist/apps/data/routes/funding.js +7 -241
  15. package/dist/apps/data/routes/funding.js.map +1 -1
  16. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  17. package/dist/apps/data/routes/transactions.d.ts +281 -281
  18. package/dist/apps/data/routes/transfers.d.ts +58 -58
  19. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  20. package/dist/apps/management/App.d.ts +855 -855
  21. package/dist/apps/management/routes/api-keys.d.ts +40 -40
  22. package/dist/apps/management/routes/billing.d.ts +207 -207
  23. package/dist/apps/management/routes/me.d.ts +6 -6
  24. package/dist/apps/management/routes/members.d.ts +30 -30
  25. package/dist/apps/management/routes/orgs.d.ts +92 -92
  26. package/dist/apps/management/routes/projects.d.ts +28 -28
  27. package/dist/apps/management/routes/usage.d.ts +24 -24
  28. package/dist/apps/mcp.d.ts.map +1 -1
  29. package/dist/apps/mcp.js +3 -1
  30. package/dist/apps/mcp.js.map +1 -1
  31. package/dist/cloudflare.d.ts +2 -0
  32. package/dist/cloudflare.d.ts.map +1 -1
  33. package/dist/cloudflare.js +4 -0
  34. package/dist/cloudflare.js.map +1 -1
  35. package/dist/internal/Auth.d.ts +33 -22
  36. package/dist/internal/Auth.d.ts.map +1 -1
  37. package/dist/internal/Auth.js +40 -11
  38. package/dist/internal/Auth.js.map +1 -1
  39. package/dist/internal/Errors.d.ts +6 -0
  40. package/dist/internal/Errors.d.ts.map +1 -0
  41. package/dist/internal/Errors.js +16 -0
  42. package/dist/internal/Errors.js.map +1 -0
  43. package/dist/internal/FundingRoutes.d.ts +15 -213
  44. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  45. package/dist/internal/FundingRoutes.js +1 -72
  46. package/dist/internal/FundingRoutes.js.map +1 -1
  47. package/dist/internal/Store.d.ts +61 -16
  48. package/dist/internal/Store.d.ts.map +1 -1
  49. package/dist/internal/Store.js +72 -17
  50. package/dist/internal/Store.js.map +1 -1
  51. package/dist/internal/Viem.d.ts +1 -1
  52. package/dist/internal/index.d.ts +27 -0
  53. package/dist/internal/index.d.ts.map +1 -0
  54. package/dist/internal/index.js +27 -0
  55. package/dist/internal/index.js.map +1 -0
  56. package/package.json +8 -3
  57. package/src/App.test.ts +60 -4
  58. package/src/Client.test-d.ts +0 -47
  59. package/src/apps/data/FundingRouteProviders.test.ts +37 -297
  60. package/src/apps/data/FundingRouteProviders.ts +3 -435
  61. package/src/apps/data/routes/funding.test.ts +7 -109
  62. package/src/apps/data/routes/funding.ts +8 -323
  63. package/src/apps/data/routes/indexer.test.ts +2 -2
  64. package/src/apps/data/routes/webhooks.test.ts +2 -2
  65. package/src/apps/mcp.test.ts +55 -14
  66. package/src/apps/mcp.ts +3 -1
  67. package/src/cloudflare.ts +5 -0
  68. package/src/internal/Auth.test.ts +397 -16
  69. package/src/internal/Auth.ts +76 -32
  70. package/src/internal/FundingRoutes.test.ts +8 -34
  71. package/src/internal/FundingRoutes.ts +1 -174
  72. package/src/internal/Log.test.ts +4 -4
  73. package/src/internal/Store.test-d.ts +15 -0
  74. package/src/internal/Store.test.ts +153 -2
  75. package/src/internal/Store.ts +157 -41
  76. package/src/internal/index.test.ts +34 -0
  77. 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 { RateLimit } from 'tapimo'
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: { charge: { amount: '1' } } } : {}),
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: { charge: { amount: '1', description: 'Protected data' } },
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: overQuotaOnce(),
888
+ rateLimit: overQuotaExceptMpp(),
770
889
  },
771
890
  policy: {
772
- mpp: { charge: { amount: '1', description: 'Protected data' } },
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: { charge: { amount: '1', description: 'Protected data' } },
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: overQuotaOnce(),
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: { charge: { amount: '1', description: 'Protected data' } },
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: { charge: { amount: '1', description: 'Protected data' } },
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.charge({
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 overQuotaOnce(): RateLimit.Store {
1549
- let consumed = false
1912
+ function underQuota(): RateLimit.Store {
1550
1913
  return {
1551
1914
  async consume(options) {
1552
- if (consumed) throw new Error('rate limit should not be consumed after payment')
1553
- consumed = true
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,