tapimo 0.4.1 → 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/README.md +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +1990 -1442
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1304 -1304
- package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +10 -120
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/fee-amm.d.ts +20 -20
- package/dist/apps/data/routes/funding.js +7 -7
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/management/App.d.ts +808 -267
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +116 -100
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +182 -50
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +100 -40
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +122 -33
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +4 -18
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +38 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.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/db/Db.d.ts +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/internal/Auth.d.ts +46 -40
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +118 -69
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.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 -43
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +8 -17
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.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/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +104 -6
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +17 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/FundingRouteProviders.test.ts +37 -93
- package/src/apps/data/FundingRouteProviders.ts +14 -161
- package/src/apps/data/routes/funding.test.ts +7 -42
- package/src/apps/data/routes/funding.ts +8 -8
- package/src/apps/data/routes/indexer.test.ts +2 -2
- package/src/apps/data/routes/webhooks.test.ts +2 -2
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +72 -7
- package/src/apps/mcp.ts +46 -4
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/cloudflare.ts +5 -0
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/internal/Auth.test.ts +571 -23
- package/src/internal/Auth.ts +166 -94
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +8 -8
- package/src/internal/FundingRoutes.ts +25 -29
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -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,14 +1,23 @@
|
|
|
1
|
-
import { Hono } from 'hono'
|
|
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'
|
|
9
|
+
import type * as Db from '../db/Db.js'
|
|
10
|
+
import * as Memberships from '../db/tables/memberships.js'
|
|
11
|
+
import * as Organizations from '../db/tables/organizations.js'
|
|
12
|
+
import * as Projects from '../db/tables/projects.js'
|
|
13
|
+
import * as Users from '../db/tables/users.js'
|
|
6
14
|
import * as Auth from './Auth.js'
|
|
7
15
|
import * as Log from './Log.js'
|
|
8
16
|
import * as Scope from '../Scope.js'
|
|
9
17
|
import * as Viem from './Viem.js'
|
|
10
18
|
import * as TestApp from '../../test/App.js'
|
|
11
19
|
import { createClient as createMppClient } from '../../test/Mppx.js'
|
|
20
|
+
import * as TestStore from '../../test/Store.js'
|
|
12
21
|
import * as Tempo from '../../test/Tempo.js'
|
|
13
22
|
|
|
14
23
|
const key = {
|
|
@@ -75,12 +84,13 @@ describe('require', () => {
|
|
|
75
84
|
`)
|
|
76
85
|
})
|
|
77
86
|
|
|
78
|
-
test('accepts API key from
|
|
87
|
+
test('accepts API key from key query param', async () => {
|
|
79
88
|
const app = createApp()
|
|
80
89
|
const response = await app.request(`/protected?key=${token}`)
|
|
81
90
|
const body = await response.json()
|
|
82
91
|
|
|
83
92
|
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
93
|
+
expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
|
|
84
94
|
expect(body).toMatchInlineSnapshot(`
|
|
85
95
|
{
|
|
86
96
|
"id": "key_test",
|
|
@@ -90,11 +100,30 @@ describe('require', () => {
|
|
|
90
100
|
`)
|
|
91
101
|
})
|
|
92
102
|
|
|
93
|
-
test('
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
test('keeps query-authenticated responses private after downstream caching', async () => {
|
|
104
|
+
const auth = {
|
|
105
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
106
|
+
rateLimit: RateLimit.memory(),
|
|
107
|
+
}
|
|
108
|
+
const app = new Hono<Auth.Environment>()
|
|
109
|
+
app.use('*', async (c, next) => {
|
|
110
|
+
c.set('auth', auth)
|
|
111
|
+
await next()
|
|
112
|
+
})
|
|
113
|
+
app.use('/cached', Auth.require({ apiKey: { scopes: ['data:read'] } }))
|
|
114
|
+
app.get('/cached', (c) => {
|
|
115
|
+
c.header('Cache-Control', 'public, max-age=86400')
|
|
116
|
+
return c.json({ ok: true })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const response = await app.request(`/cached?key=${token}`)
|
|
120
|
+
|
|
121
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
122
|
+
expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('strips the key query param before route validation', async () => {
|
|
126
|
+
// Auth owns `key`, so strict route schemas must not see it.
|
|
98
127
|
const auth = {
|
|
99
128
|
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
100
129
|
rateLimit: RateLimit.memory(),
|
|
@@ -534,7 +563,7 @@ describe('require', () => {
|
|
|
534
563
|
const policy: Auth.require.Policy = {
|
|
535
564
|
apiKey: { scopes: [Scope.wildcard] },
|
|
536
565
|
...(options.public === false ? {} : { public: { rateLimit: { perMinute: 60 } } }),
|
|
537
|
-
...(options.mpp ? { mpp: {
|
|
566
|
+
...(options.mpp ? { mpp: { session: { amount: '1' } } } : {}),
|
|
538
567
|
}
|
|
539
568
|
const app = new Hono<Auth.Environment>()
|
|
540
569
|
app.use('*', async (c, next) => {
|
|
@@ -714,6 +743,121 @@ describe('require', () => {
|
|
|
714
743
|
})
|
|
715
744
|
|
|
716
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
|
+
|
|
717
861
|
test('returns payment challenge for public over-quota requests', async () => {
|
|
718
862
|
const mpp = createMppServer()
|
|
719
863
|
const app = createApp({
|
|
@@ -723,7 +867,7 @@ describe('require', () => {
|
|
|
723
867
|
rateLimit: overQuota(),
|
|
724
868
|
},
|
|
725
869
|
policy: {
|
|
726
|
-
mpp: {
|
|
870
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
727
871
|
public: { rateLimit: { perMinute: 1 } },
|
|
728
872
|
},
|
|
729
873
|
})
|
|
@@ -735,16 +879,16 @@ describe('require', () => {
|
|
|
735
879
|
).toMatchInlineSnapshot(`true`)
|
|
736
880
|
})
|
|
737
881
|
|
|
738
|
-
test('accepts paid public retry without consuming quota', async () => {
|
|
882
|
+
test('accepts paid public retry without consuming public quota', async () => {
|
|
739
883
|
const mpp = createMppServer()
|
|
740
884
|
const app = createApp({
|
|
741
885
|
auth: {
|
|
742
886
|
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
743
887
|
mpp,
|
|
744
|
-
rateLimit:
|
|
888
|
+
rateLimit: overQuotaExceptMpp(),
|
|
745
889
|
},
|
|
746
890
|
policy: {
|
|
747
|
-
mpp: {
|
|
891
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
748
892
|
public: { rateLimit: { perMinute: 1 } },
|
|
749
893
|
},
|
|
750
894
|
})
|
|
@@ -779,7 +923,7 @@ describe('require', () => {
|
|
|
779
923
|
rateLimit: { perMinute: 1 },
|
|
780
924
|
scopes: ['data:read'],
|
|
781
925
|
},
|
|
782
|
-
mpp: {
|
|
926
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
783
927
|
},
|
|
784
928
|
})
|
|
785
929
|
const response = await app.request('/protected', {
|
|
@@ -798,14 +942,14 @@ describe('require', () => {
|
|
|
798
942
|
auth: {
|
|
799
943
|
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
800
944
|
mpp,
|
|
801
|
-
rateLimit:
|
|
945
|
+
rateLimit: overQuotaExceptMpp(),
|
|
802
946
|
},
|
|
803
947
|
policy: {
|
|
804
948
|
apiKey: {
|
|
805
949
|
rateLimit: { perMinute: 1 },
|
|
806
950
|
scopes: ['data:read'],
|
|
807
951
|
},
|
|
808
|
-
mpp: {
|
|
952
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
809
953
|
},
|
|
810
954
|
})
|
|
811
955
|
const response = await createMppClient(app).fetch('http://tempo-api.test/protected', {
|
|
@@ -830,6 +974,220 @@ describe('require', () => {
|
|
|
830
974
|
}
|
|
831
975
|
`)
|
|
832
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
|
+
})
|
|
833
1191
|
})
|
|
834
1192
|
|
|
835
1193
|
describe('rateLimit', () => {
|
|
@@ -1281,7 +1639,7 @@ describe('require', () => {
|
|
|
1281
1639
|
},
|
|
1282
1640
|
policy: {
|
|
1283
1641
|
apiKey: { rateLimit: { perMinute: 1 }, scopes: ['data:read'] },
|
|
1284
|
-
mpp: {
|
|
1642
|
+
mpp: { session: { amount: '1', description: 'Protected data' } },
|
|
1285
1643
|
},
|
|
1286
1644
|
})
|
|
1287
1645
|
const response = await app.request('/protected', {
|
|
@@ -1298,6 +1656,87 @@ describe('require', () => {
|
|
|
1298
1656
|
})
|
|
1299
1657
|
})
|
|
1300
1658
|
|
|
1659
|
+
describe('ensureProject', () => {
|
|
1660
|
+
test('authorizes a flat project route against the API key organization', async () => {
|
|
1661
|
+
const db = TestApp.database()
|
|
1662
|
+
const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
|
|
1663
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1664
|
+
const response = await createProjectGuardApp(db, apiKeyPrincipal(organization.id)).request(
|
|
1665
|
+
`/v1/projects/${project.id}/resource`,
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
1669
|
+
expect(await response.json()).toEqual({
|
|
1670
|
+
membership: null,
|
|
1671
|
+
orgId: organization.id,
|
|
1672
|
+
projectId: project.id,
|
|
1673
|
+
})
|
|
1674
|
+
})
|
|
1675
|
+
|
|
1676
|
+
test('hides a flat project from an API key in another organization', async () => {
|
|
1677
|
+
const db = TestApp.database()
|
|
1678
|
+
const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
|
|
1679
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1680
|
+
const response = await createProjectGuardApp(db, apiKeyPrincipal('org_foreign')).request(
|
|
1681
|
+
`/v1/projects/${project.id}/resource`,
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
expect(response.status).toMatchInlineSnapshot(`404`)
|
|
1685
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
1686
|
+
{
|
|
1687
|
+
"error": {
|
|
1688
|
+
"code": "project_not_found",
|
|
1689
|
+
"message": "Project not found",
|
|
1690
|
+
},
|
|
1691
|
+
}
|
|
1692
|
+
`)
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
test('resolves session membership for a flat project route', async () => {
|
|
1696
|
+
const db = TestApp.database()
|
|
1697
|
+
const user = await Users.upsertByAddress(db, {
|
|
1698
|
+
address: '0x0000000000000000000000000000000000000001',
|
|
1699
|
+
})
|
|
1700
|
+
const organization = await Organizations.create(db, { name: 'Test org' })
|
|
1701
|
+
await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
|
|
1702
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1703
|
+
const response = await createProjectGuardApp(db, sessionPrincipal(user.id)).request(
|
|
1704
|
+
`/v1/projects/${project.id}/resource`,
|
|
1705
|
+
)
|
|
1706
|
+
const body = (await response.json()) as Record<string, unknown>
|
|
1707
|
+
|
|
1708
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
1709
|
+
expect(body).toEqual({
|
|
1710
|
+
membership: 'member',
|
|
1711
|
+
orgId: organization.id,
|
|
1712
|
+
projectId: project.id,
|
|
1713
|
+
})
|
|
1714
|
+
})
|
|
1715
|
+
|
|
1716
|
+
test('preserves nested organization role authorization', async () => {
|
|
1717
|
+
const db = TestApp.database()
|
|
1718
|
+
const user = await Users.upsertByAddress(db, {
|
|
1719
|
+
address: '0x0000000000000000000000000000000000000001',
|
|
1720
|
+
})
|
|
1721
|
+
const organization = await Organizations.create(db, { name: 'Test org' })
|
|
1722
|
+
await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
|
|
1723
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1724
|
+
const response = await createNestedProjectGuardApp(db, sessionPrincipal(user.id)).request(
|
|
1725
|
+
`/v1/orgs/${organization.id}/projects/${project.id}/resource`,
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1728
|
+
expect(response.status).toMatchInlineSnapshot(`403`)
|
|
1729
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
1730
|
+
{
|
|
1731
|
+
"error": {
|
|
1732
|
+
"code": "forbidden",
|
|
1733
|
+
"message": "Requires the admin role",
|
|
1734
|
+
},
|
|
1735
|
+
}
|
|
1736
|
+
`)
|
|
1737
|
+
})
|
|
1738
|
+
})
|
|
1739
|
+
|
|
1301
1740
|
function createScopedApp(options: {
|
|
1302
1741
|
auth?: Auth.Context | undefined
|
|
1303
1742
|
routes: readonly { path: string; policy: Auth.require.Policy }[]
|
|
@@ -1322,6 +1761,67 @@ function createScopedApp(options: {
|
|
|
1322
1761
|
return app
|
|
1323
1762
|
}
|
|
1324
1763
|
|
|
1764
|
+
type GuardEnvironment = {
|
|
1765
|
+
Variables: Auth.Variables & { db: Db.Source }
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function createProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
|
|
1769
|
+
const app = new Hono<GuardEnvironment>()
|
|
1770
|
+
app.use('*', async (c, next) => {
|
|
1771
|
+
c.set('db', db)
|
|
1772
|
+
c.set('principal', principal)
|
|
1773
|
+
await next()
|
|
1774
|
+
})
|
|
1775
|
+
app.get('/v1/projects/:projectId/resource', Auth.ensureProject(), projectGuardHandler)
|
|
1776
|
+
return app
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function createNestedProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
|
|
1780
|
+
const app = new Hono<GuardEnvironment>()
|
|
1781
|
+
app.use('*', async (c, next) => {
|
|
1782
|
+
c.set('db', db)
|
|
1783
|
+
c.set('principal', principal)
|
|
1784
|
+
await next()
|
|
1785
|
+
})
|
|
1786
|
+
app.get(
|
|
1787
|
+
'/v1/orgs/:orgId/projects/:projectId/resource',
|
|
1788
|
+
Auth.ensureOrg({ role: 'admin' }),
|
|
1789
|
+
Auth.ensureProject(),
|
|
1790
|
+
projectGuardHandler,
|
|
1791
|
+
)
|
|
1792
|
+
return app
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function projectGuardHandler(c: Context<GuardEnvironment>) {
|
|
1796
|
+
return c.json({
|
|
1797
|
+
membership: Auth.membership(c)?.role ?? null,
|
|
1798
|
+
orgId: Auth.org(c).id,
|
|
1799
|
+
projectId: Auth.project(c).id,
|
|
1800
|
+
})
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function apiKeyPrincipal(orgId: string): Auth.Principal {
|
|
1804
|
+
const apiKey = { ...key, orgId }
|
|
1805
|
+
return {
|
|
1806
|
+
apiKey,
|
|
1807
|
+
environment: apiKey.environment,
|
|
1808
|
+
id: apiKey.id,
|
|
1809
|
+
orgId,
|
|
1810
|
+
type: 'api_key',
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function sessionPrincipal(id: string): Auth.Principal {
|
|
1815
|
+
return {
|
|
1816
|
+
id,
|
|
1817
|
+
identity: {
|
|
1818
|
+
provider: 'wallet',
|
|
1819
|
+
subject: '0x0000000000000000000000000000000000000001',
|
|
1820
|
+
},
|
|
1821
|
+
type: 'session',
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1325
1825
|
type Options = {
|
|
1326
1826
|
auth?: Auth.Context | undefined
|
|
1327
1827
|
logger?: Log.Emit | undefined
|
|
@@ -1349,15 +1849,18 @@ function createApp(options: Options = {}) {
|
|
|
1349
1849
|
return app
|
|
1350
1850
|
}
|
|
1351
1851
|
|
|
1352
|
-
function createMppServer() {
|
|
1852
|
+
function createMppServer(options: createMppServer.Options = {}) {
|
|
1353
1853
|
return Mppx.create({
|
|
1354
1854
|
methods: [
|
|
1355
|
-
tempo.
|
|
1855
|
+
tempo.session({
|
|
1856
|
+
bootstrap: false,
|
|
1356
1857
|
chainId: Tempo.chain.id,
|
|
1357
1858
|
currency: Tempo.currency,
|
|
1358
1859
|
decimals: 6,
|
|
1359
1860
|
getClient: () => Tempo.client,
|
|
1360
1861
|
recipient: Tempo.accounts[2].address,
|
|
1862
|
+
...options,
|
|
1863
|
+
unitType: 'request',
|
|
1361
1864
|
}),
|
|
1362
1865
|
],
|
|
1363
1866
|
realm: 'tempo-api-test',
|
|
@@ -1365,6 +1868,34 @@ function createMppServer() {
|
|
|
1365
1868
|
})
|
|
1366
1869
|
}
|
|
1367
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
|
+
|
|
1368
1899
|
function overQuota(): RateLimit.Store {
|
|
1369
1900
|
return {
|
|
1370
1901
|
async consume(options) {
|
|
@@ -1378,12 +1909,29 @@ function overQuota(): RateLimit.Store {
|
|
|
1378
1909
|
}
|
|
1379
1910
|
}
|
|
1380
1911
|
|
|
1381
|
-
function
|
|
1382
|
-
|
|
1912
|
+
function underQuota(): RateLimit.Store {
|
|
1913
|
+
return {
|
|
1914
|
+
async consume(options) {
|
|
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 {
|
|
1383
1926
|
return {
|
|
1384
1927
|
async consume(options) {
|
|
1385
|
-
if (
|
|
1386
|
-
|
|
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
|
+
}
|
|
1387
1935
|
return {
|
|
1388
1936
|
allowed: false,
|
|
1389
1937
|
limit: options.limit.perMinute,
|