tapimo 0.4.4 → 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/dist/App.d.ts +4701 -3416
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +24 -2
- package/dist/App.js.map +1 -1
- package/dist/Assets.js +3 -3
- package/dist/Assets.js.map +1 -1
- package/dist/Scope.d.ts +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +23 -3
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -10
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +6 -34
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +3 -3
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +4 -1
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +6 -34
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +2 -2
- package/dist/admin/apps/scopes.js.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.js +33 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +4574 -4277
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +155 -77
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +109 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingQuoteProviders.js +464 -0
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -1
- package/dist/apps/data/index.js +1 -1
- package/dist/apps/data/routes/activities.d.ts +26 -26
- package/dist/apps/data/routes/addresses.d.ts +38 -38
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +67 -149
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +2 -3
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +3 -5
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +177 -176
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +3 -8
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +131 -228
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +152 -219
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +9 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +228 -228
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +86 -16
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +340 -0
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +13 -1
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/management/App.d.ts +1833 -851
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +4 -0
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +41 -41
- package/dist/apps/management/routes/api-keys.js +2 -2
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +48 -48
- package/dist/apps/management/routes/invitations.d.ts +132 -132
- package/dist/apps/management/routes/invite-links.d.ts +1100 -0
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
- package/dist/apps/management/routes/invite-links.js +411 -0
- package/dist/apps/management/routes/invite-links.js.map +1 -0
- package/dist/apps/management/routes/members.d.ts +41 -41
- package/dist/apps/management/routes/orgs.d.ts +92 -92
- package/dist/apps/management/routes/projects.d.ts +9 -9
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/mcp.js +2 -2
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +2 -2
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +44 -6
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +3 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +57 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +31 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +31 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +42 -0
- package/dist/db/tables/inviteLinks.d.ts.map +1 -0
- package/dist/db/tables/inviteLinks.js +148 -0
- package/dist/db/tables/inviteLinks.js.map +1 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +2 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +6 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +13 -3
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +9 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +138 -10
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +3 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +17 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +240 -369
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +144 -183
- package/dist/internal/FundingQuotes.js.map +1 -0
- package/dist/internal/Log.d.ts +18 -12
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +2 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Tidx.d.ts +9 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -7
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +17 -2
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +30 -3
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/package.json +2 -2
- package/src/ApiKeys.test.ts +25 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.test.ts +13 -2
- package/src/App.ts +37 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +21 -11
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +32 -4
- package/src/Webhooks.ts +5 -10
- package/src/admin/apps/api-keys.test.ts +27 -0
- package/src/admin/apps/api-keys.ts +8 -1
- package/src/admin/apps/scopes.test.ts +7 -2
- package/src/admin/apps/scopes.ts +2 -2
- package/src/admin/ui/src/components/Dialog.tsx +8 -2
- package/src/admin/ui/src/lib/scopes.test.ts +19 -0
- package/src/admin/ui/src/lib/scopes.ts +12 -0
- package/src/admin/ui/src/routes/api-keys.tsx +128 -27
- package/src/admin/ui.generated.ts +33 -33
- package/src/apps/composition.test.ts +77 -0
- package/src/apps/data/App.test.ts +354 -0
- package/src/apps/data/App.ts +178 -91
- package/src/apps/data/FundingQuoteProviders.test.ts +577 -0
- package/src/apps/data/FundingQuoteProviders.ts +633 -0
- package/src/apps/data/index.ts +1 -1
- package/src/apps/data/routes/balances.test.ts +64 -18
- package/src/apps/data/routes/balances.ts +76 -177
- package/src/apps/data/routes/blocks.ts +3 -5
- package/src/apps/data/routes/coingecko.ts +3 -8
- package/src/apps/data/routes/funding.test.ts +304 -192
- package/src/apps/data/routes/funding.ts +166 -286
- package/src/apps/data/routes/indexer.test.ts +173 -15
- package/src/apps/data/routes/indexer.ts +8 -2
- package/src/apps/data/routes/rpc.test.ts +219 -0
- package/src/apps/data/routes/rpc.ts +92 -17
- package/src/apps/data/routes/webhooks.test.ts +21 -0
- package/src/apps/data/routes/webhooks.ts +13 -1
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +4 -0
- package/src/apps/management/Billing.test.ts +2 -2
- package/src/apps/management/routes/api-keys.test.ts +47 -3
- package/src/apps/management/routes/api-keys.ts +2 -2
- package/src/apps/management/routes/invite-links.test.ts +385 -0
- package/src/apps/management/routes/invite-links.ts +570 -0
- package/src/apps/management/routes/scopes.test.ts +3 -2
- package/src/apps/management/routes/scopes.ts +4 -2
- package/src/apps/mcp.test.ts +18 -0
- package/src/apps/mcp.ts +2 -2
- package/src/apps/relay/App.test.ts +328 -7
- package/src/apps/relay/App.ts +42 -6
- package/src/apps/relay/Sponsorships.test.ts +2 -2
- package/src/db/Db.test.ts +5 -5
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.test.ts +2 -2
- package/src/db/tables/organizations.ts +2 -0
- package/src/db/tables/sponsoredTransactions.test.ts +19 -2
- package/src/db/tables/sponsoredTransactions.ts +21 -14
- package/src/db/tables/stripeCustomers.test.ts +2 -2
- package/src/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +132 -22
- package/src/internal/Auth.ts +181 -14
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +268 -0
- package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +216 -277
- package/src/internal/Log.ts +21 -14
- package/src/internal/Tidx.test.ts +48 -0
- package/src/internal/Tidx.ts +21 -7
- package/src/internal/Viem.test.ts +46 -0
- package/src/internal/Viem.ts +48 -6
- package/src/internal/index.test.ts +1 -1
- package/src/internal/index.ts +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +0 -36
- package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingRouteProviders.js +0 -245
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts.map +0 -1
- package/dist/internal/FundingRoutes.js.map +0 -1
- package/src/apps/data/FundingRouteProviders.test.ts +0 -225
- package/src/apps/data/FundingRouteProviders.ts +0 -312
- package/src/internal/FundingRoutes.test.ts +0 -213
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
|
-
import { Hash, Secp256k1 } from 'ox'
|
|
2
|
+
import { Hash, Hex, Secp256k1 } from 'ox'
|
|
3
|
+
import { ZoneRpcAuthentication } from 'ox/tempo'
|
|
3
4
|
import { createClient, http } from 'viem'
|
|
4
5
|
import { prepareTransactionRequest, sendRawTransactionSync, signTransaction } from 'viem/actions'
|
|
5
6
|
import { Account as TempoAccount, Actions, Transaction, withRelay } from 'viem/tempo'
|
|
6
7
|
|
|
7
8
|
import * as TestApp from '../../../test/App.js'
|
|
8
|
-
import
|
|
9
|
+
import * as Containers from '../../../test/containers.js'
|
|
9
10
|
import * as Relay from '../../../test/Relay.js'
|
|
10
11
|
import * as Runtime from '../../../test/runtime.js'
|
|
11
12
|
import * as Tempo from '../../../test/Tempo.js'
|
|
@@ -25,6 +26,12 @@ const runtime = Runtime.get()
|
|
|
25
26
|
|
|
26
27
|
const feePayerAccount = Tempo.accounts[10]!
|
|
27
28
|
const senderAccount = Tempo.accounts[9]!
|
|
29
|
+
const zone = runtime.zone.chainId
|
|
30
|
+
const zoneScope = `zone:${zone}:read` as const
|
|
31
|
+
const zoneOptions = {
|
|
32
|
+
chainId: zone,
|
|
33
|
+
forwardRpcUrl: runtime.zone.forwardRpcUrl,
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
/** Super admin secret configured on the end-to-end billing app. */
|
|
30
37
|
const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
|
|
@@ -71,6 +78,22 @@ const keys = [
|
|
|
71
78
|
scopes: ['rpc-relay:read'],
|
|
72
79
|
token: 'secret_unscoped',
|
|
73
80
|
},
|
|
81
|
+
{
|
|
82
|
+
id: 'key_zone_relay',
|
|
83
|
+
orgId: 'org_1',
|
|
84
|
+
projectId: 'prj_1',
|
|
85
|
+
environment: 'sandbox',
|
|
86
|
+
scopes: ['rpc-relay:read', zoneScope],
|
|
87
|
+
token: 'secret_zone_relay',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'key_zone_sponsor',
|
|
91
|
+
orgId: 'org_1',
|
|
92
|
+
projectId: 'prj_1',
|
|
93
|
+
environment: 'sandbox',
|
|
94
|
+
scopes: ['rpc-relay:sponsor', zoneScope],
|
|
95
|
+
token: 'secret_zone_sponsor',
|
|
96
|
+
},
|
|
74
97
|
{
|
|
75
98
|
id: 'key_unattributed',
|
|
76
99
|
orgId: 'org_1',
|
|
@@ -80,6 +103,303 @@ const keys = [
|
|
|
80
103
|
},
|
|
81
104
|
] as const satisfies readonly TestApp.kvStore.Key[]
|
|
82
105
|
|
|
106
|
+
describe('behavior: Zone chain routing', () => {
|
|
107
|
+
const zoneSelectors = [
|
|
108
|
+
['relay', 'path'],
|
|
109
|
+
['relay', 'body'],
|
|
110
|
+
['sponsor', 'path'],
|
|
111
|
+
['sponsor', 'body'],
|
|
112
|
+
] as const
|
|
113
|
+
// Prool's default Zone dev key.
|
|
114
|
+
const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
|
|
115
|
+
|
|
116
|
+
function app() {
|
|
117
|
+
return TestApp.create({
|
|
118
|
+
auth: { keys },
|
|
119
|
+
zones: [zoneOptions],
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function token() {
|
|
124
|
+
const issuedAt = Math.floor(Date.now() / 1000)
|
|
125
|
+
const authentication = ZoneRpcAuthentication.from({
|
|
126
|
+
chainId: zone,
|
|
127
|
+
expiresAt: issuedAt + 300,
|
|
128
|
+
issuedAt,
|
|
129
|
+
zoneId: 1,
|
|
130
|
+
})
|
|
131
|
+
return ZoneRpcAuthentication.serialize(authentication, {
|
|
132
|
+
signature: Secp256k1.sign({
|
|
133
|
+
payload: ZoneRpcAuthentication.getSignPayload(authentication),
|
|
134
|
+
privateKey,
|
|
135
|
+
}),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function request(
|
|
140
|
+
app: ReturnType<typeof TestApp.create>,
|
|
141
|
+
options: {
|
|
142
|
+
apiKey?: string | undefined
|
|
143
|
+
basePath?: string | undefined
|
|
144
|
+
body: object | readonly object[]
|
|
145
|
+
mount: 'relay' | 'sponsor'
|
|
146
|
+
pathChainId?: number | undefined
|
|
147
|
+
zoneToken?: string | undefined
|
|
148
|
+
},
|
|
149
|
+
) {
|
|
150
|
+
const apiKey =
|
|
151
|
+
options.apiKey ?? (options.mount === 'relay' ? 'secret_unscoped' : 'secret_sponsor')
|
|
152
|
+
const path = `${options.basePath ?? ''}/rpc/${options.mount}${options.pathChainId ? `/${options.pathChainId}` : ''}`
|
|
153
|
+
const response = await app.fetch(
|
|
154
|
+
new Request(`http://tempo-api.test${path}`, {
|
|
155
|
+
body: JSON.stringify(options.body),
|
|
156
|
+
headers: {
|
|
157
|
+
'content-type': 'application/json',
|
|
158
|
+
'tempo-api-key': apiKey,
|
|
159
|
+
...(options.zoneToken ? { [ZoneRpcAuthentication.headerName]: options.zoneToken } : {}),
|
|
160
|
+
},
|
|
161
|
+
method: 'POST',
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
return await response.json()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
test.each(zoneSelectors)('rejects an unscoped Zone on %s via %s', async (mount, selector) => {
|
|
168
|
+
const body = await request(app(), {
|
|
169
|
+
body: {
|
|
170
|
+
id: 1,
|
|
171
|
+
jsonrpc: '2.0',
|
|
172
|
+
method: 'eth_chainId',
|
|
173
|
+
params: selector === 'body' ? [{ chainId: zone }] : [],
|
|
174
|
+
},
|
|
175
|
+
mount,
|
|
176
|
+
...(selector === 'path' ? { pathChainId: zone } : {}),
|
|
177
|
+
})
|
|
178
|
+
expect(body).toMatchObject({
|
|
179
|
+
error: {
|
|
180
|
+
code: -32602,
|
|
181
|
+
data: { code: 'api_key_forbidden' },
|
|
182
|
+
message: expect.stringContaining(zoneScope),
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test.each(zoneSelectors)('allows a Zone token on %s via %s', async (mount, selector) => {
|
|
188
|
+
const body = await request(app(), {
|
|
189
|
+
body: {
|
|
190
|
+
id: 1,
|
|
191
|
+
jsonrpc: '2.0',
|
|
192
|
+
method: 'eth_chainId',
|
|
193
|
+
params: selector === 'body' ? [{ chainId: zone }] : [],
|
|
194
|
+
},
|
|
195
|
+
mount,
|
|
196
|
+
...(selector === 'path' ? { pathChainId: zone } : {}),
|
|
197
|
+
zoneToken: token(),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect(body).toMatchObject({ result: `0x${zone.toString(16)}` })
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('allows a Zone token on the default chain', async () => {
|
|
204
|
+
const api = TestApp.create({
|
|
205
|
+
auth: { keys },
|
|
206
|
+
defaultChainId: zone,
|
|
207
|
+
zones: [zoneOptions],
|
|
208
|
+
})
|
|
209
|
+
const body = await request(api, {
|
|
210
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId' },
|
|
211
|
+
mount: 'relay',
|
|
212
|
+
zoneToken: token(),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
expect(body).toMatchObject({ result: `0x${zone.toString(16)}` })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('allows a Zone token in a batch', async () => {
|
|
219
|
+
const body = (await request(app(), {
|
|
220
|
+
body: [
|
|
221
|
+
{ id: 1, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
222
|
+
{ id: 2, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
223
|
+
],
|
|
224
|
+
mount: 'relay',
|
|
225
|
+
zoneToken: token(),
|
|
226
|
+
})) as { result?: string }[]
|
|
227
|
+
|
|
228
|
+
expect(body.map((entry) => entry.result)).toEqual([
|
|
229
|
+
`0x${zone.toString(16)}`,
|
|
230
|
+
`0x${zone.toString(16)}`,
|
|
231
|
+
])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test.each([
|
|
235
|
+
['relay', 'secret_zone_relay'],
|
|
236
|
+
['sponsor', 'secret_zone_sponsor'],
|
|
237
|
+
] as const)('allows a Zone-scoped key on %s', async (mount, apiKey) => {
|
|
238
|
+
const body = await request(app(), {
|
|
239
|
+
apiKey,
|
|
240
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
241
|
+
mount,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expect(body).toMatchObject({ result: `0x${zone.toString(16)}` })
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('forwards RPC basic auth to a Zone internal RPC', async () => {
|
|
248
|
+
const authorization: (string | null)[] = []
|
|
249
|
+
const server = await Relay.createServer(
|
|
250
|
+
RequestListener.fromFetchHandler(async (request) => {
|
|
251
|
+
authorization.push(request.headers.get('authorization'))
|
|
252
|
+
return Response.json({ id: 1, jsonrpc: '2.0', result: Hex.fromNumber(zone) })
|
|
253
|
+
}),
|
|
254
|
+
)
|
|
255
|
+
try {
|
|
256
|
+
const api = TestApp.create({
|
|
257
|
+
auth: { keys },
|
|
258
|
+
rpc: ({ chainId }) =>
|
|
259
|
+
chainId === zone ? { basicAuth: 'rpc-secret', url: server.url } : {},
|
|
260
|
+
zones: [zoneOptions],
|
|
261
|
+
})
|
|
262
|
+
const body = await request(api, {
|
|
263
|
+
apiKey: 'secret_zone_relay',
|
|
264
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
265
|
+
mount: 'relay',
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
expect(body).toMatchObject({ result: Hex.fromNumber(zone) })
|
|
269
|
+
expect(authorization).toEqual(['Basic cnBjLXNlY3JldA=='])
|
|
270
|
+
} finally {
|
|
271
|
+
await server.closeAsync()
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('rejects a blank Zone token', async () => {
|
|
276
|
+
const body = await request(app(), {
|
|
277
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
278
|
+
mount: 'relay',
|
|
279
|
+
zoneToken: ' ',
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
expect(body).toMatchObject({ error: { data: { code: 'api_key_forbidden' } } })
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('does not forward a Zone token to a non-Zone RPC', async () => {
|
|
286
|
+
const tokens: (string | null)[] = []
|
|
287
|
+
const server = await Relay.createServer(
|
|
288
|
+
RequestListener.fromFetchHandler(async (request) => {
|
|
289
|
+
tokens.push(request.headers.get(ZoneRpcAuthentication.headerName))
|
|
290
|
+
return Response.json({ id: 1, jsonrpc: '2.0', result: Hex.fromNumber(runtime.chainId) })
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
try {
|
|
294
|
+
const api = TestApp.create({
|
|
295
|
+
auth: { keys },
|
|
296
|
+
rpc: { url: () => server.url },
|
|
297
|
+
zones: [zoneOptions],
|
|
298
|
+
})
|
|
299
|
+
const body = await request(api, {
|
|
300
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId' },
|
|
301
|
+
mount: 'relay',
|
|
302
|
+
pathChainId: runtime.chainId,
|
|
303
|
+
zoneToken: token(),
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
expect(body).toMatchObject({ result: Hex.fromNumber(runtime.chainId) })
|
|
307
|
+
expect(tokens).toEqual([null])
|
|
308
|
+
} finally {
|
|
309
|
+
await server.closeAsync()
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('rejects conflicting path and body chain ids', async () => {
|
|
314
|
+
const body = await request(app(), {
|
|
315
|
+
body: {
|
|
316
|
+
id: 1,
|
|
317
|
+
jsonrpc: '2.0',
|
|
318
|
+
method: 'eth_chainId',
|
|
319
|
+
params: [{ chainId: zone }],
|
|
320
|
+
},
|
|
321
|
+
mount: 'relay',
|
|
322
|
+
pathChainId: runtime.chainId,
|
|
323
|
+
})
|
|
324
|
+
expect(body).toMatchObject({ error: { code: -32602, message: 'Conflicting chain ids.' } })
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('rejects every Zone request in a batch', async () => {
|
|
328
|
+
const body = (await request(app(), {
|
|
329
|
+
body: [
|
|
330
|
+
{ id: 1, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
331
|
+
{ id: 2, jsonrpc: '2.0', method: 'eth_chainId', params: [{ chainId: zone }] },
|
|
332
|
+
],
|
|
333
|
+
mount: 'relay',
|
|
334
|
+
})) as { error?: { data?: { code?: string } } }[]
|
|
335
|
+
|
|
336
|
+
expect(body).toHaveLength(2)
|
|
337
|
+
expect(body.every((entry) => entry.error?.data?.code === 'api_key_forbidden')).toBe(true)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('rejects a Zone configured as the default chain', async () => {
|
|
341
|
+
const api = TestApp.create({
|
|
342
|
+
auth: { keys },
|
|
343
|
+
defaultChainId: zone,
|
|
344
|
+
zones: [zoneOptions],
|
|
345
|
+
})
|
|
346
|
+
const body = await request(api, {
|
|
347
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId' },
|
|
348
|
+
mount: 'relay',
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
expect(body).toMatchObject({ error: { data: { code: 'api_key_forbidden' } } })
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('rejects Zone selectors under a base path', async () => {
|
|
355
|
+
const api = TestApp.create({
|
|
356
|
+
auth: { keys },
|
|
357
|
+
path: '/api',
|
|
358
|
+
zones: [zoneOptions],
|
|
359
|
+
})
|
|
360
|
+
const body = await request(api, {
|
|
361
|
+
basePath: '/api',
|
|
362
|
+
body: { id: 1, jsonrpc: '2.0', method: 'eth_chainId' },
|
|
363
|
+
mount: 'relay',
|
|
364
|
+
pathChainId: zone,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
expect(body).toMatchObject({ error: { data: { code: 'api_key_forbidden' } } })
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('requires Zone authorization for an embedded signed transaction', async () => {
|
|
371
|
+
const api = TestApp.create({
|
|
372
|
+
auth: { keys },
|
|
373
|
+
relay: { feePayer: { account: feePayerAccount } },
|
|
374
|
+
zones: [zoneOptions],
|
|
375
|
+
})
|
|
376
|
+
const serialized = await senderAccount.signTransaction({
|
|
377
|
+
chainId: zone,
|
|
378
|
+
feePayer: true,
|
|
379
|
+
gas: 100_000n,
|
|
380
|
+
maxFeePerGas: 1_000_000n,
|
|
381
|
+
maxPriorityFeePerGas: 0n,
|
|
382
|
+
nonce: 0,
|
|
383
|
+
to: Tempo.accounts[8]!.address,
|
|
384
|
+
value: 0n,
|
|
385
|
+
})
|
|
386
|
+
const rpc = {
|
|
387
|
+
body: {
|
|
388
|
+
id: 1,
|
|
389
|
+
jsonrpc: '2.0',
|
|
390
|
+
method: 'eth_signRawTransaction',
|
|
391
|
+
params: [serialized],
|
|
392
|
+
},
|
|
393
|
+
mount: 'sponsor',
|
|
394
|
+
} as const
|
|
395
|
+
const refused = await request(api, rpc)
|
|
396
|
+
const authorized = await request(api, { ...rpc, zoneToken: token() })
|
|
397
|
+
|
|
398
|
+
expect(refused).toMatchObject({ error: { data: { code: 'api_key_forbidden' } } })
|
|
399
|
+
expect(authorized).toMatchObject({ result: expect.stringMatching(/^0x/) })
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
83
403
|
describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
84
404
|
test('behavior: scoped keys sponsor, recorded with attribution', async () => {
|
|
85
405
|
const db = TestApp.database()
|
|
@@ -130,7 +450,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
130
450
|
`)
|
|
131
451
|
})
|
|
132
452
|
|
|
133
|
-
test('behavior: production keys record billable rows', async () => {
|
|
453
|
+
test('behavior: non-mainnet production keys record billable rows', async () => {
|
|
134
454
|
const db = TestApp.database()
|
|
135
455
|
const app = TestApp.create({
|
|
136
456
|
auth: { keys },
|
|
@@ -821,15 +1141,15 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
821
1141
|
() => {
|
|
822
1142
|
// prettier-ignore
|
|
823
1143
|
const webhookSecret = `whsec_${'c'.repeat(32)}`
|
|
824
|
-
let node: Awaited<ReturnType<typeof
|
|
1144
|
+
let node: Awaited<ReturnType<typeof Containers.tempo>> | undefined
|
|
825
1145
|
|
|
826
1146
|
beforeAll(async () => {
|
|
827
|
-
node = await
|
|
1147
|
+
node = await Containers.tempo({ chainId: Viem.chainId.mainnet })
|
|
828
1148
|
}, 240_000)
|
|
829
1149
|
|
|
830
1150
|
afterAll(async () => {
|
|
831
1151
|
await TestStripe.sweep()
|
|
832
|
-
await node?.
|
|
1152
|
+
await node?.stop()
|
|
833
1153
|
})
|
|
834
1154
|
|
|
835
1155
|
test(
|
|
@@ -837,7 +1157,8 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
837
1157
|
{ retry: 0, timeout: 300_000 },
|
|
838
1158
|
async () => {
|
|
839
1159
|
// prettier-ignore
|
|
840
|
-
const
|
|
1160
|
+
const endpoint = node!.endpoints.default
|
|
1161
|
+
const url = `http://${endpoint.host}:${endpoint.port}`
|
|
841
1162
|
const stripe = TestStripe.client()
|
|
842
1163
|
const db = TestApp.database()
|
|
843
1164
|
// The key attributes to org_1; the row must exist for the usage read.
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import { createMiddleware } from 'hono/factory'
|
|
3
3
|
import { RpcResponse } from 'ox'
|
|
4
|
+
import { ZoneRpcAuthentication } from 'ox/tempo'
|
|
4
5
|
import { Addresses, TokenId, Transaction } from 'viem/tempo'
|
|
5
6
|
|
|
6
7
|
import type * as App from '../../App.js'
|
|
@@ -19,8 +20,8 @@ import * as Scope from '../../Scope.js'
|
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* The relay route group exposes wallet features at `/rpc/relay` and fee
|
|
22
|
-
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes
|
|
23
|
-
*
|
|
23
|
+
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes the
|
|
24
|
+
* verified-token and zone context used by relay handlers.
|
|
24
25
|
*
|
|
25
26
|
* ```ts
|
|
26
27
|
* App.create(options).route('/', data()).route('/', relay())
|
|
@@ -79,11 +80,13 @@ function middleware(options: middleware.Options) {
|
|
|
79
80
|
? Billing.toBaseUnits(options.feePayer.txFeeLimit ?? '1.00')
|
|
80
81
|
: 0n
|
|
81
82
|
return createMiddleware<App.Environment>(async (c) => {
|
|
83
|
+
const principal = Auth.getPrincipal(c)
|
|
82
84
|
const verifiedTokens = c.get('verifiedTokens')
|
|
85
|
+
const zoneToken_header = c.req.header(ZoneRpcAuthentication.headerName)
|
|
86
|
+
const zoneToken = zoneToken_header?.trim() ? zoneToken_header : undefined
|
|
83
87
|
|
|
84
88
|
const feePayer = (() => {
|
|
85
89
|
if (!options.feePayer) return undefined
|
|
86
|
-
const principal = Auth.getPrincipal(c)
|
|
87
90
|
const attribution =
|
|
88
91
|
principal?.type === 'api_key' && principal.projectId !== undefined
|
|
89
92
|
? {
|
|
@@ -224,13 +227,46 @@ function middleware(options: middleware.Options) {
|
|
|
224
227
|
} satisfies Handler.relay.Options['feePayer']
|
|
225
228
|
})()
|
|
226
229
|
|
|
230
|
+
const authorizeChainId = (chainId: number | undefined) => {
|
|
231
|
+
const resolved = chainId ?? c.get('chainId')
|
|
232
|
+
if (!c.get('supportedChainIds').has(resolved))
|
|
233
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Unsupported chain id.' })
|
|
234
|
+
if (c.get('zones').has(resolved)) {
|
|
235
|
+
const scopes = principal?.type === 'api_key' ? principal.apiKey.scopes : undefined
|
|
236
|
+
const allowed =
|
|
237
|
+
scopes?.includes(Scope.wildcard) ||
|
|
238
|
+
scopes?.includes(`zone:${resolved}:read`) ||
|
|
239
|
+
zoneToken !== undefined
|
|
240
|
+
if (allowed) return resolved
|
|
241
|
+
throw new RpcResponse.InvalidParamsError({
|
|
242
|
+
data: { code: 'api_key_forbidden' },
|
|
243
|
+
message: `Chain id ${resolved} is a zone. Use an API key granting \`zone:${resolved}:read\`, or pass a Zone token via \`X-Authorization-Token\`.`,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
return resolved
|
|
247
|
+
}
|
|
248
|
+
const zoneClients = new Map<number, Viem.getClient.ReturnType>()
|
|
249
|
+
const getClient = (chainId: number | undefined) => {
|
|
250
|
+
const resolved = authorizeChainId(chainId) as Viem.ChainId
|
|
251
|
+
const zone = c.get('zones').get(resolved)
|
|
252
|
+
if (!zone || !zoneToken) return c.get('getClient')(resolved)
|
|
253
|
+
const existing = zoneClients.get(resolved)
|
|
254
|
+
if (existing) return existing
|
|
255
|
+
const client = Viem.getClient({
|
|
256
|
+
chainId: resolved,
|
|
257
|
+
principal,
|
|
258
|
+
rpc: { url: () => zone.forwardRpcUrl },
|
|
259
|
+
zoneToken,
|
|
260
|
+
})
|
|
261
|
+
zoneClients.set(resolved, client)
|
|
262
|
+
return client
|
|
263
|
+
}
|
|
227
264
|
// Dispatching a standalone Hono via `fetch(c.req.raw)` bypasses Hono's
|
|
228
|
-
// mount-path stripping, so its
|
|
229
|
-
// full external path (`basePath` + the RPC mount).
|
|
265
|
+
// mount-path stripping, so its routes use the full external path.
|
|
230
266
|
const handler = Handler.relay({
|
|
231
267
|
cache: c.get('store'),
|
|
232
268
|
...(options.features ? { features: options.features } : {}),
|
|
233
|
-
getClient
|
|
269
|
+
getClient,
|
|
234
270
|
path: Path.join(c.get('basePath'), options.path),
|
|
235
271
|
...(feePayer ? { feePayer } : {}),
|
|
236
272
|
...(verifiedTokens
|
|
@@ -2,12 +2,12 @@ import { nanoid } from 'nanoid'
|
|
|
2
2
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
3
3
|
import type { Client } from 'viem'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
6
6
|
import * as Db from '../../db/Db.js'
|
|
7
7
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
8
8
|
import * as Sponsorships from './Sponsorships.js'
|
|
9
9
|
|
|
10
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
10
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
11
11
|
|
|
12
12
|
/** Baseline insert input; tests override per case. */
|
|
13
13
|
const input = {
|
package/src/db/Db.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Runtime from '../../test/runtime.js'
|
|
4
4
|
import * as Db from './Db.js'
|
|
5
5
|
import * as Organizations from './tables/organizations.js'
|
|
6
6
|
|
|
7
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
7
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
8
8
|
|
|
9
9
|
describe('postgres', () => {
|
|
10
10
|
test('behavior: migrate is idempotent', async () => {
|
|
@@ -16,7 +16,7 @@ describe('postgres', () => {
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
test('behavior: creates a Postgres db without schema scoping', async () => {
|
|
19
|
-
const db = Db.postgres({ connectionString: postgresUrl })
|
|
19
|
+
const db = Db.postgres({ connectionString: Runtime.postgresUrl })
|
|
20
20
|
await db.close()
|
|
21
21
|
})
|
|
22
22
|
|
|
@@ -24,14 +24,14 @@ describe('postgres', () => {
|
|
|
24
24
|
const schema = `t_${nanoid()}`
|
|
25
25
|
// Pre-create the schema so concurrent migrate races only on the
|
|
26
26
|
// `migrations` ledger, not `CREATE SCHEMA`.
|
|
27
|
-
const seed = Db.postgres({ connectionString: postgresUrl, schema })
|
|
27
|
+
const seed = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
|
|
28
28
|
await Organizations.get(seed, 'x').catch(() => {})
|
|
29
29
|
await seed.close()
|
|
30
30
|
|
|
31
31
|
// Several runners migrate the same fresh schema at once (mirrors parallel
|
|
32
32
|
// deploys). The session advisory lock serializes them; none throw.
|
|
33
33
|
const dbs = Array.from({ length: 5 }, () =>
|
|
34
|
-
Db.postgres({ connectionString: postgresUrl, schema }),
|
|
34
|
+
Db.postgres({ connectionString: Runtime.postgresUrl, schema }),
|
|
35
35
|
)
|
|
36
36
|
await Promise.all(dbs.map((db) => db.migrate()))
|
|
37
37
|
const created = await Organizations.create(dbs[0]!, { id: 'org_race', name: 'Race' })
|
package/src/db/Db.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as pg from 'pg'
|
|
|
13
13
|
import type * as AdminAuditLogs from './tables/adminAuditLogs.js'
|
|
14
14
|
import type * as BillingSettings from './tables/billingSettings.js'
|
|
15
15
|
import type * as Invitations from './tables/invitations.js'
|
|
16
|
+
import type * as InviteLinks from './tables/inviteLinks.js'
|
|
16
17
|
import type * as Memberships from './tables/memberships.js'
|
|
17
18
|
import type * as Organizations from './tables/organizations.js'
|
|
18
19
|
import type * as Projects from './tables/projects.js'
|
|
@@ -32,6 +33,8 @@ export type Database = {
|
|
|
32
33
|
admin_audit_logs: AdminAuditLogs.Table
|
|
33
34
|
billing_settings: BillingSettings.Table
|
|
34
35
|
invitations: Invitations.Table
|
|
36
|
+
invite_link_redemptions: InviteLinks.RedemptionTable
|
|
37
|
+
invite_links: InviteLinks.Table
|
|
35
38
|
memberships: Memberships.Table
|
|
36
39
|
migrations: {
|
|
37
40
|
appliedAt: string
|
|
@@ -576,6 +579,68 @@ export const migrations: readonly Migration[] = [
|
|
|
576
579
|
.on('adminAuditLogs')
|
|
577
580
|
.columns(['createdAt', 'id']),
|
|
578
581
|
},
|
|
582
|
+
{
|
|
583
|
+
name: '0053_invite_links',
|
|
584
|
+
up: (db) =>
|
|
585
|
+
db.schema
|
|
586
|
+
.createTable('inviteLinks')
|
|
587
|
+
.ifNotExists()
|
|
588
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
589
|
+
.addColumn('token', 'text', (c) => c.notNull().unique())
|
|
590
|
+
.addColumn('orgId', 'text', (c) => c.notNull())
|
|
591
|
+
.addColumn('name', 'text', (c) => c.notNull())
|
|
592
|
+
.addColumn('role', 'text', (c) => c.notNull())
|
|
593
|
+
.addColumn('maxUses', 'integer')
|
|
594
|
+
.addColumn('useCount', 'integer', (c) => c.notNull().defaultTo(0))
|
|
595
|
+
.addColumn('expiresAt', 'text')
|
|
596
|
+
.addColumn('enabled', 'boolean', (c) => c.notNull().defaultTo(true))
|
|
597
|
+
.addColumn('createdBy', 'text', (c) => c.notNull())
|
|
598
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
599
|
+
.addColumn('lastUsedAt', 'text')
|
|
600
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull())
|
|
601
|
+
.addColumn('deletedAt', 'text'),
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: '0054_invite_links_org_index',
|
|
605
|
+
up: (db) =>
|
|
606
|
+
db.schema
|
|
607
|
+
.createIndex('invite_links_org_idx')
|
|
608
|
+
.ifNotExists()
|
|
609
|
+
.on('inviteLinks')
|
|
610
|
+
.columns(['orgId', 'createdAt']),
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: '0055_invite_link_redemptions',
|
|
614
|
+
up: (db) =>
|
|
615
|
+
db.schema
|
|
616
|
+
.createTable('inviteLinkRedemptions')
|
|
617
|
+
.ifNotExists()
|
|
618
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
619
|
+
.addColumn('inviteLinkId', 'text', (c) => c.notNull())
|
|
620
|
+
.addColumn('inviteLinkName', 'text', (c) => c.notNull())
|
|
621
|
+
.addColumn('orgId', 'text', (c) => c.notNull())
|
|
622
|
+
.addColumn('userId', 'text', (c) => c.notNull())
|
|
623
|
+
.addColumn('email', 'text', (c) => c.notNull())
|
|
624
|
+
.addColumn('createdAt', 'text', (c) => c.notNull()),
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: '0056_invite_link_redemptions_org_index',
|
|
628
|
+
up: (db) =>
|
|
629
|
+
db.schema
|
|
630
|
+
.createIndex('invite_link_redemptions_org_idx')
|
|
631
|
+
.ifNotExists()
|
|
632
|
+
.on('inviteLinkRedemptions')
|
|
633
|
+
.columns(['orgId', 'createdAt']),
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: '0057_invite_link_redemptions_link_index',
|
|
637
|
+
up: (db) =>
|
|
638
|
+
db.schema
|
|
639
|
+
.createIndex('invite_link_redemptions_link_idx')
|
|
640
|
+
.ifNotExists()
|
|
641
|
+
.on('inviteLinkRedemptions')
|
|
642
|
+
.columns(['inviteLinkId', 'createdAt']),
|
|
643
|
+
},
|
|
579
644
|
]
|
|
580
645
|
|
|
581
646
|
/**
|
package/src/db/Schema.ts
CHANGED
|
@@ -81,6 +81,45 @@ export const Invitation = z.object({
|
|
|
81
81
|
/** Columns of the `invitations` table. */
|
|
82
82
|
export type Invitation = z.output<typeof Invitation>
|
|
83
83
|
|
|
84
|
+
/** A reusable bearer link granting organization membership. */
|
|
85
|
+
export const InviteLink = z.object({
|
|
86
|
+
createdAt: z.string().check(z.describe('When the link was created (ISO 8601).')),
|
|
87
|
+
createdBy: z.string().check(z.describe('User id that created the link, or `super_admin`.')),
|
|
88
|
+
deletedAt: z.nullable(z.string()).check(z.describe('When the link was soft deleted, or null.')),
|
|
89
|
+
enabled: z.boolean().check(z.describe('Whether the link is enabled.')),
|
|
90
|
+
expiresAt: z.nullable(z.string()).check(z.describe('When the link expires, or null.')),
|
|
91
|
+
id: z.string().check(z.describe('Opaque invite-link resource id (`iln_…`).')),
|
|
92
|
+
lastUsedAt: z
|
|
93
|
+
.nullable(z.string())
|
|
94
|
+
.check(z.describe('When the link last created a membership, or null.')),
|
|
95
|
+
maxUses: z
|
|
96
|
+
.nullable(z.number().check(z.int(), z.positive()))
|
|
97
|
+
.check(z.describe('Maximum redemptions, or null.')),
|
|
98
|
+
name: z.string().check(z.describe('Link name.')),
|
|
99
|
+
orgId: z.string().check(z.describe('Organization id (`org_…`).')),
|
|
100
|
+
role: z.literal('member').check(z.describe('Non-privileged role granted by the link.')),
|
|
101
|
+
token: z.string().check(z.describe('Opaque invite-link bearer token (`lnk_…`).')),
|
|
102
|
+
updatedAt: z.string().check(z.describe('When the link was last changed (ISO 8601).')),
|
|
103
|
+
useCount: z.number().check(z.int(), z.nonnegative(), z.describe('Successful redemption count.')),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/** Columns of the `invite_links` table. */
|
|
107
|
+
export type InviteLink = z.output<typeof InviteLink>
|
|
108
|
+
|
|
109
|
+
/** An audit record for one membership created through an invite link. */
|
|
110
|
+
export const InviteLinkRedemption = z.object({
|
|
111
|
+
createdAt: z.string().check(z.describe('When the link was redeemed (ISO 8601).')),
|
|
112
|
+
email: z.string().check(z.describe('Verified email used for redemption.')),
|
|
113
|
+
id: z.string().check(z.describe('Opaque redemption id (`ilr_…`).')),
|
|
114
|
+
inviteLinkId: z.string().check(z.describe('Invite-link id.')),
|
|
115
|
+
inviteLinkName: z.string().check(z.describe('Invite-link name at redemption time.')),
|
|
116
|
+
orgId: z.string().check(z.describe('Organization id.')),
|
|
117
|
+
userId: z.string().check(z.describe('Redeeming user id.')),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
/** Columns of the `invite_link_redemptions` table. */
|
|
121
|
+
export type InviteLinkRedemption = z.output<typeof InviteLinkRedemption>
|
|
122
|
+
|
|
84
123
|
/** A user's role within an organization. */
|
|
85
124
|
export const Membership = z.object({
|
|
86
125
|
createdAt: z.string().check(z.describe('When the membership was created (ISO 8601).')),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
4
4
|
import * as Db from '../Db.js'
|
|
5
5
|
import * as BillingSettings from './billingSettings.js'
|
|
6
6
|
|
|
7
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
7
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
8
8
|
|
|
9
9
|
/** Redacts nondeterministic row fields (timestamps). */
|
|
10
10
|
function redact(record: BillingSettings.Record) {
|