tapimo 0.4.3 → 0.5.0
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 +7222 -6316
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +4 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +24 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +116 -116
- package/dist/apps/data/App.d.ts +2541 -2607
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +135 -44
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +36 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingQuoteProviders.js +190 -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 +242 -242
- package/dist/apps/data/routes/addresses.d.ts +227 -227
- package/dist/apps/data/routes/balances.d.ts +12 -12
- 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 +125 -126
- 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 +55 -54
- 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/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +92 -92
- package/dist/apps/data/routes/funding.d.ts +91 -864
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +138 -450
- package/dist/apps/data/routes/funding.js.map +1 -1
- 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 +36 -36
- 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 +1843 -861
- 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 +121 -121
- 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 +2 -2
- 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/me.d.ts +13 -13
- package/dist/apps/management/routes/members.d.ts +77 -77
- package/dist/apps/management/routes/orgs.d.ts +96 -96
- package/dist/apps/management/routes/projects.d.ts +62 -62
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- 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 +79 -79
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +3 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +42 -4
- 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 +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/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 +42 -23
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +169 -20
- 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/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/FundingQuotes.d.ts +644 -0
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -289
- package/dist/internal/FundingQuotes.js.map +1 -0
- 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 +2 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +11 -2
- package/dist/internal/Viem.js.map +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/ApiKeys.test.ts +23 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.test.ts +60 -4
- package/src/App.ts +6 -0
- package/src/Client.test-d.ts +16 -54
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +30 -4
- package/src/apps/data/App.test.ts +354 -0
- package/src/apps/data/App.ts +159 -52
- package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
- package/src/apps/data/FundingQuoteProviders.ts +251 -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 +149 -306
- package/src/apps/data/routes/funding.ts +154 -604
- package/src/apps/data/routes/indexer.test.ts +99 -3
- 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 +23 -2
- 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/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 +55 -14
- package/src/apps/mcp.ts +3 -1
- package/src/apps/relay/App.test.ts +324 -4
- package/src/apps/relay/App.ts +40 -4
- package/src/cloudflare.ts +5 -0
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.ts +2 -0
- package/src/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +528 -37
- package/src/internal/Auth.ts +246 -45
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +174 -0
- package/src/internal/FundingQuotes.ts +735 -0
- package/src/internal/Log.test.ts +4 -4
- package/src/internal/Store.test-d.ts +15 -0
- package/src/internal/Store.test.ts +153 -2
- package/src/internal/Store.ts +157 -41
- package/src/internal/Viem.ts +14 -5
- package/src/internal/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
- package/dist/apps/data/FundingRouteProviders.d.ts +0 -143
- package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingRouteProviders.js +0 -546
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts +0 -1085
- 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 -485
- package/src/apps/data/FundingRouteProviders.ts +0 -744
- package/src/internal/FundingRoutes.test.ts +0 -239
- package/src/internal/FundingRoutes.ts +0 -1067
package/src/apps/mcp.test.ts
CHANGED
|
@@ -91,6 +91,46 @@ async function toolsCall(
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/** Searches every page of the progressive tool catalog. */
|
|
95
|
+
async function toolsSearch(host: Host) {
|
|
96
|
+
const tools: { annotations?: { readOnlyHint?: boolean }; name: string }[] = []
|
|
97
|
+
let offset = 0
|
|
98
|
+
while (true) {
|
|
99
|
+
const result = resultJson(await toolsCall(host, 'search_tools', { limit: 20, offset })) as {
|
|
100
|
+
nextOffset?: number
|
|
101
|
+
tools: typeof tools
|
|
102
|
+
}
|
|
103
|
+
tools.push(...result.tools)
|
|
104
|
+
if (result.nextOffset === undefined) return tools
|
|
105
|
+
offset = result.nextOffset
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Loads the complete schema and metadata for one catalog tool. */
|
|
110
|
+
async function toolDetails(host: Host, name: string) {
|
|
111
|
+
return resultJson(await toolsCall(host, 'get_tool_details', { name })) as {
|
|
112
|
+
annotations?: { readOnlyHint?: boolean }
|
|
113
|
+
inputSchema: { properties?: Record<string, { description?: string }> }
|
|
114
|
+
name: string
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Inspects and executes a generated tool through its matching read or write gate. */
|
|
119
|
+
async function generatedToolCall(
|
|
120
|
+
host: Host,
|
|
121
|
+
name: string,
|
|
122
|
+
args: Record<string, unknown>,
|
|
123
|
+
headers: Record<string, string> = {},
|
|
124
|
+
) {
|
|
125
|
+
const details = await toolDetails(host, name)
|
|
126
|
+
return toolsCall(
|
|
127
|
+
host,
|
|
128
|
+
details.annotations?.readOnlyHint === true ? 'call_read_tool' : 'call_write_tool',
|
|
129
|
+
{ arguments: args, name },
|
|
130
|
+
headers,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
94
134
|
/** Parses a tool result: structured content when present, else the JSON text item. */
|
|
95
135
|
function resultJson(result: Awaited<ReturnType<typeof toolsCall>>) {
|
|
96
136
|
return result.structuredContent ?? JSON.parse(result.content[0]!.text)
|
|
@@ -108,7 +148,13 @@ test('scopes tools to the data domain', async () => {
|
|
|
108
148
|
expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/orgs'))).toBe(true)
|
|
109
149
|
|
|
110
150
|
await initialize(host)
|
|
111
|
-
|
|
151
|
+
expect(await toolsList(host)).toEqual([
|
|
152
|
+
'search_tools',
|
|
153
|
+
'get_tool_details',
|
|
154
|
+
'call_read_tool',
|
|
155
|
+
'call_write_tool',
|
|
156
|
+
])
|
|
157
|
+
const tools = (await toolsSearch(host)).map((tool) => tool.name)
|
|
112
158
|
|
|
113
159
|
expect(tools).toContain('v1_tokens_get')
|
|
114
160
|
expect(tools).toContain('docs_search')
|
|
@@ -125,13 +171,8 @@ test('keeps tool input schemas compact', async () => {
|
|
|
125
171
|
const { host } = createHost()
|
|
126
172
|
await initialize(host)
|
|
127
173
|
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
name: string
|
|
131
|
-
inputSchema: { properties?: Record<string, { description?: string }> }
|
|
132
|
-
}[]
|
|
133
|
-
const tool = tools.find((entry) => entry.name === 'v1_tokens_get')
|
|
134
|
-
const properties = tool?.inputSchema.properties ?? {}
|
|
174
|
+
const tool = await toolDetails(host, 'v1_tokens_get')
|
|
175
|
+
const properties = tool.inputSchema.properties ?? {}
|
|
135
176
|
|
|
136
177
|
// Credential inputs stay off the schema; auth rides the forwarded header.
|
|
137
178
|
expect(properties['authorization']).toBeUndefined()
|
|
@@ -142,7 +183,7 @@ test('keeps tool input schemas compact', async () => {
|
|
|
142
183
|
'Keyset cursor from a previous response `nextCursor`; omit for the first page.',
|
|
143
184
|
)
|
|
144
185
|
// `compact` strips examples from generated schemas.
|
|
145
|
-
expect(JSON.stringify(tool
|
|
186
|
+
expect(JSON.stringify(tool.inputSchema)).not.toContain('"examples"')
|
|
146
187
|
})
|
|
147
188
|
|
|
148
189
|
test('executes generated tools against the composed app', async () => {
|
|
@@ -152,7 +193,7 @@ test('executes generated tools against the composed app', async () => {
|
|
|
152
193
|
const { host } = createHost({ app: TestApp.create({ db }) })
|
|
153
194
|
|
|
154
195
|
await initialize(host)
|
|
155
|
-
const result = await
|
|
196
|
+
const result = await generatedToolCall(host, 'v1_tokens_get', { verified: true })
|
|
156
197
|
|
|
157
198
|
expect(result.isError).toBeFalsy()
|
|
158
199
|
const body = resultJson(result) as { data: { verified: boolean }[]; nextCursor: unknown }
|
|
@@ -164,10 +205,10 @@ test('forwards the caller Authorization header to authenticated tools', async ()
|
|
|
164
205
|
const { host } = createHost({ app: TestApp.create({ auth: { keys: [reader] } }) })
|
|
165
206
|
await initialize(host)
|
|
166
207
|
|
|
167
|
-
const denied = await
|
|
208
|
+
const denied = await generatedToolCall(host, 'v1_webhooks_get', {})
|
|
168
209
|
expect(denied.isError).toBe(true)
|
|
169
210
|
|
|
170
|
-
const allowed = await
|
|
211
|
+
const allowed = await generatedToolCall(
|
|
171
212
|
host,
|
|
172
213
|
'v1_webhooks_get',
|
|
173
214
|
{},
|
|
@@ -181,7 +222,7 @@ test('proxies docs tool calls to the remote MCP source', async () => {
|
|
|
181
222
|
const { host } = createHost({ docs: docsStub() })
|
|
182
223
|
await initialize(host)
|
|
183
224
|
|
|
184
|
-
const result = await
|
|
225
|
+
const result = await generatedToolCall(host, 'docs_search', { query: 'fees' })
|
|
185
226
|
|
|
186
227
|
expect(result.isError).toBeFalsy()
|
|
187
228
|
expect(resultJson(result)).toMatchObject({ hits: ['fees'] })
|
|
@@ -210,7 +251,7 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
|
|
|
210
251
|
// Documenting the route makes it auth-enforced; the public lane keeps it
|
|
211
252
|
// anonymously reachable, and the endpoint never surfaces itself as a tool.
|
|
212
253
|
await initialize(app)
|
|
213
|
-
const tools = await
|
|
254
|
+
const tools = (await toolsSearch(app)).map((tool) => tool.name)
|
|
214
255
|
expect(tools.length).toBeGreaterThan(0)
|
|
215
256
|
expect(tools.some((name) => name.startsWith('mcp'))).toBe(false)
|
|
216
257
|
|
package/src/apps/mcp.ts
CHANGED
|
@@ -147,7 +147,9 @@ export function mcp(options: mcp.Options) {
|
|
|
147
147
|
example: {
|
|
148
148
|
jsonrpc: '2.0',
|
|
149
149
|
id: 1,
|
|
150
|
-
result: {
|
|
150
|
+
result: {
|
|
151
|
+
tools: [{ name: 'search_tools', description: 'Search available tools' }],
|
|
152
|
+
},
|
|
151
153
|
},
|
|
152
154
|
schema: OpenApi.resolver(schema.Response),
|
|
153
155
|
},
|
|
@@ -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()
|
|
@@ -821,10 +1141,10 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
821
1141
|
() => {
|
|
822
1142
|
// prettier-ignore
|
|
823
1143
|
const webhookSecret = `whsec_${'c'.repeat(32)}`
|
|
824
|
-
let node: Awaited<ReturnType<typeof startTempo>> | undefined
|
|
1144
|
+
let node: Awaited<ReturnType<typeof Containers.startTempo>> | undefined
|
|
825
1145
|
|
|
826
1146
|
beforeAll(async () => {
|
|
827
|
-
node = await startTempo({ chainId: Viem.chainId.mainnet })
|
|
1147
|
+
node = await Containers.startTempo({ chainId: Viem.chainId.mainnet })
|
|
828
1148
|
}, 240_000)
|
|
829
1149
|
|
|
830
1150
|
afterAll(async () => {
|
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'
|
|
@@ -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
|
package/src/cloudflare.ts
CHANGED
|
@@ -26,6 +26,11 @@ export class DurableObject extends CloudflareDurableObject {
|
|
|
26
26
|
await this.#store.delete(key)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Deletes a key only when its value matches `expected`. */
|
|
30
|
+
async deleteIf(key: string, expected: null | string) {
|
|
31
|
+
return this.#store.deleteIf!(key, expected)
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
/** Gets a string value. Returns `null` for missing or expired keys. */
|
|
30
35
|
async get(key: string) {
|
|
31
36
|
return this.#store.get(key)
|
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
|
/**
|