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
package/src/Client.test-d.ts
CHANGED
|
@@ -1189,25 +1189,28 @@ describe('narrows responses by status', () => {
|
|
|
1189
1189
|
}
|
|
1190
1190
|
})
|
|
1191
1191
|
|
|
1192
|
-
test('GET /v1/funding/
|
|
1192
|
+
test('GET /v1/funding/quotes', async () => {
|
|
1193
1193
|
const client = Client.create()
|
|
1194
|
-
const res = await client.v1.funding.
|
|
1194
|
+
const res = await client.v1.funding.quotes.$get({
|
|
1195
1195
|
query: {
|
|
1196
|
+
provider: 'across',
|
|
1196
1197
|
sourceAmount: '1000000',
|
|
1197
1198
|
sourceChain: 'base',
|
|
1198
1199
|
sourceToken: 'USDC',
|
|
1199
1200
|
},
|
|
1200
1201
|
})
|
|
1201
1202
|
|
|
1202
|
-
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | PaidAccessStatus>()
|
|
1203
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | 502 | PaidAccessStatus>()
|
|
1203
1204
|
|
|
1204
1205
|
switch (res.status) {
|
|
1205
1206
|
case 200: {
|
|
1206
1207
|
const json = await res.json()
|
|
1207
1208
|
expectTypeOf(json.data[0]!.sourceAmount).toEqualTypeOf<
|
|
1208
|
-
z.output<typeof Funding.schema.
|
|
1209
|
+
z.output<typeof Funding.schema.quoteFunding.Response>['data'][number]['sourceAmount']
|
|
1209
1210
|
>()
|
|
1210
|
-
expectTypeOf(json.data[0]!.provider.id).toEqualTypeOf<
|
|
1211
|
+
expectTypeOf(json.data[0]!.provider.id).toEqualTypeOf<'across' | 'relay' | 'rhino'>()
|
|
1212
|
+
expectTypeOf(json.data[0]!.quote.expiresAt).toEqualTypeOf<string | undefined>()
|
|
1213
|
+
expectTypeOf(json.data[0]!.quote.outputAmount).toEqualTypeOf<string>()
|
|
1211
1214
|
void json
|
|
1212
1215
|
return
|
|
1213
1216
|
}
|
|
@@ -1218,6 +1221,13 @@ describe('narrows responses by status', () => {
|
|
|
1218
1221
|
void json
|
|
1219
1222
|
return
|
|
1220
1223
|
}
|
|
1224
|
+
case 502: {
|
|
1225
|
+
const json = await res.json()
|
|
1226
|
+
expectTypeOf(json.error.code).toEqualTypeOf<'upstream_error'>()
|
|
1227
|
+
expectTypeOf(json.error.message).toEqualTypeOf<string>()
|
|
1228
|
+
void json
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1221
1231
|
}
|
|
1222
1232
|
})
|
|
1223
1233
|
|
|
@@ -1248,24 +1258,24 @@ describe('narrows responses by status', () => {
|
|
|
1248
1258
|
}
|
|
1249
1259
|
})
|
|
1250
1260
|
|
|
1251
|
-
test('GET /v1/providers', async () => {
|
|
1261
|
+
test('GET /v1/funding/providers', async () => {
|
|
1252
1262
|
const client = Client.create()
|
|
1253
|
-
const res = await client.v1.providers.$get(
|
|
1263
|
+
const res = await client.v1.funding.providers.$get()
|
|
1254
1264
|
|
|
1255
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
1265
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | PaidAccessStatus>()
|
|
1256
1266
|
|
|
1257
1267
|
switch (res.status) {
|
|
1258
1268
|
case 200: {
|
|
1259
1269
|
const json = await res.json()
|
|
1260
|
-
expectTypeOf(json.data[0]!.
|
|
1261
|
-
z.output<typeof Funding.schema.listProviders.Response>['data'][number]['
|
|
1270
|
+
expectTypeOf(json.data[0]!.status).toEqualTypeOf<
|
|
1271
|
+
z.output<typeof Funding.schema.listProviders.Response>['data'][number]['status']
|
|
1262
1272
|
>()
|
|
1263
1273
|
void json
|
|
1264
1274
|
return
|
|
1265
1275
|
}
|
|
1266
1276
|
case 400: {
|
|
1267
1277
|
const json = await res.json()
|
|
1268
|
-
expectTypeOf(json.error.code).toEqualTypeOf<
|
|
1278
|
+
expectTypeOf(json.error.code).toEqualTypeOf<'api_key_malformed'>()
|
|
1269
1279
|
expectTypeOf(json.error.message).toEqualTypeOf<string>()
|
|
1270
1280
|
void json
|
|
1271
1281
|
return
|
package/src/Scope.test.ts
CHANGED
package/src/Scope.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* API-key validation.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as z from 'zod/mini'
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Wildcard scope. A key whose `scopes` contains this value satisfies every
|
|
9
11
|
* required scope, so it never needs re-provisioning when new scopes are added.
|
|
@@ -69,8 +71,32 @@ export const catalog = [
|
|
|
69
71
|
},
|
|
70
72
|
] as const satisfies readonly Entry[]
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Templated `/v1/scopes` entry for per-zone read scopes. Concrete grants use
|
|
76
|
+
* `zone:<chainId>:read` and are never self-serve.
|
|
77
|
+
*/
|
|
78
|
+
export const zoneEntry = {
|
|
79
|
+
description: 'Read RPC and indexed data for one zone chain id.',
|
|
80
|
+
scope: 'zone:<chainId>:read',
|
|
81
|
+
selfServe: false,
|
|
82
|
+
} as const satisfies Entry
|
|
83
|
+
|
|
84
|
+
/** Zod schemas owned by the scope vocabulary. */
|
|
85
|
+
export namespace schema {
|
|
86
|
+
/** A concrete per-zone read scope (`zone:421700001:read`). */
|
|
87
|
+
export const Zone = z
|
|
88
|
+
.templateLiteral(['zone:', z.int(), ':read'])
|
|
89
|
+
.check(
|
|
90
|
+
z.regex(/^zone:[1-9]\d*:read$/),
|
|
91
|
+
z.describe('Per-zone read scope (`zone:<chainId>:read`).'),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Per-zone read scope, granting access to one zone chain id. */
|
|
96
|
+
export type Zone = z.output<typeof schema.Zone>
|
|
97
|
+
|
|
72
98
|
/** Issuable API-key scope identifier, including host-defined scopes. */
|
|
73
|
-
export type Id = (typeof catalog)[number]['scope'] | (string & {})
|
|
99
|
+
export type Id = (typeof catalog)[number]['scope'] | Zone | (string & {})
|
|
74
100
|
|
|
75
101
|
/** Scope catalog used by API-key issuance and discovery. */
|
|
76
102
|
export type Catalog = readonly Entry[]
|
|
@@ -92,7 +118,7 @@ export const selfServe: readonly Id[] = catalog
|
|
|
92
118
|
*/
|
|
93
119
|
export function extend(entries: readonly Entry[] = []): Catalog {
|
|
94
120
|
const identifiers = new Set<string>()
|
|
95
|
-
const result = [...catalog, ...entries]
|
|
121
|
+
const result = [...catalog, zoneEntry, ...entries]
|
|
96
122
|
for (const entry of result) {
|
|
97
123
|
if (!entry.scope.trim()) throw new Error('Scope identifier must not be empty.')
|
|
98
124
|
if (identifiers.has(entry.scope)) throw new Error(`Duplicate scope \`${entry.scope}\`.`)
|
|
@@ -103,10 +129,12 @@ export function extend(entries: readonly Entry[] = []): Catalog {
|
|
|
103
129
|
|
|
104
130
|
/** Returns whether a catalog contains a scope identifier. */
|
|
105
131
|
export function includes(catalog: Catalog, scope: string): scope is Id {
|
|
106
|
-
return catalog.some((entry) => entry.scope === scope)
|
|
132
|
+
return schema.Zone.safeParse(scope).success || catalog.some((entry) => entry.scope === scope)
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
/** Returns the self-issuable identifiers in a scope catalog. */
|
|
110
136
|
export function selfServeFrom(catalog: Catalog): readonly Id[] {
|
|
111
|
-
return catalog
|
|
137
|
+
return catalog
|
|
138
|
+
.filter((entry) => entry.selfServe && !schema.Zone.safeParse(entry.scope).success)
|
|
139
|
+
.map((entry) => entry.scope)
|
|
112
140
|
}
|
package/src/Webhooks.ts
CHANGED
|
@@ -28,7 +28,7 @@ export { eventTypes, schema } from './apps/data/routes/webhooks.js'
|
|
|
28
28
|
*
|
|
29
29
|
* The monitoring event types (`log:emitted`, `block:created`,
|
|
30
30
|
* `transaction:included`) scan a confirmed window directly over JSON-RPC, so the
|
|
31
|
-
* poller builds memoized viem clients (via {@link Viem.
|
|
31
|
+
* poller builds memoized viem clients (via {@link Viem.createGetClient}); the legacy
|
|
32
32
|
* `token:transfer` type still reads from TIDX, so it
|
|
33
33
|
* also builds memoized TIDX clients (via {@link Tidx.createGetClient}) when a
|
|
34
34
|
* `tidx` config is supplied. The poller keeps `App.create` a pure Hono app: it
|
|
@@ -42,15 +42,10 @@ export function createPoller(options: createPoller.Options): createPoller.Poller
|
|
|
42
42
|
const getTidx = options.tidx
|
|
43
43
|
? Tidx.createGetClient({ defaultChainId: options.defaultChainId, tidx: options.tidx })
|
|
44
44
|
: undefined
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
client = Viem.getClient({ chainId, rpc: options.rpc })
|
|
50
|
-
clients.set(chainId, client)
|
|
51
|
-
}
|
|
52
|
-
return client
|
|
53
|
-
}
|
|
45
|
+
const getClient = Viem.createGetClient({
|
|
46
|
+
defaultChainId: options.defaultChainId,
|
|
47
|
+
rpc: options.rpc,
|
|
48
|
+
})
|
|
54
49
|
|
|
55
50
|
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
56
51
|
|
|
@@ -68,6 +68,19 @@ describe('POST /api-keys', () => {
|
|
|
68
68
|
expect(resolved?.environment).toBe('sandbox')
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
test('mints a key with a concrete Zone read scope', async () => {
|
|
72
|
+
const { client, store } = TestAdmin.setup()
|
|
73
|
+
const scope = 'zone:421700001:read' as const
|
|
74
|
+
const response = await client['api-keys'].$post({
|
|
75
|
+
json: { ...validBody, scopes: [scope] },
|
|
76
|
+
})
|
|
77
|
+
const body = await TestApp.json(response, ApiKeys.schema.createApiKey.Response)
|
|
78
|
+
|
|
79
|
+
expect(response.status).toBe(200)
|
|
80
|
+
expect(body.scopes).toEqual([scope])
|
|
81
|
+
expect((await core_ApiKeys.resolve(store, body.token))?.scopes).toEqual([scope])
|
|
82
|
+
})
|
|
83
|
+
|
|
71
84
|
test('records the expiry when provided', async () => {
|
|
72
85
|
const { client, store } = TestAdmin.setup()
|
|
73
86
|
const expiresAt = '2100-01-01T00:00:00.000Z'
|
|
@@ -91,6 +104,20 @@ describe('POST /api-keys', () => {
|
|
|
91
104
|
expect(body.error?.code).toMatchInlineSnapshot(`"body_invalid"`)
|
|
92
105
|
})
|
|
93
106
|
|
|
107
|
+
test('rejects non-concrete or non-canonical Zone scopes with 400', async () => {
|
|
108
|
+
for (const scope of ['zone:<chainId>:read', 'zone:0:read', 'zone:000421700001:read']) {
|
|
109
|
+
const { client } = TestAdmin.setup()
|
|
110
|
+
const response = await client['api-keys'].$post({
|
|
111
|
+
// Cast past the typed client to exercise runtime validation.
|
|
112
|
+
json: { ...validBody, scopes: [scope] } as never,
|
|
113
|
+
})
|
|
114
|
+
const body = (await response.json()) as { error?: { code?: string } }
|
|
115
|
+
|
|
116
|
+
expect(response.status).toBe(400)
|
|
117
|
+
expect(body.error?.code).toBe('body_invalid')
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
94
121
|
test('ignores a client-supplied createdBy (audit trail is server-set)', async () => {
|
|
95
122
|
const { client } = TestAdmin.setup()
|
|
96
123
|
const response = await client['api-keys'].$post({
|
|
@@ -19,7 +19,14 @@ export namespace schema {
|
|
|
19
19
|
export const Body = Schema.describe(
|
|
20
20
|
z.extend(z.omit(ApiKeys.schema.MintInput, { createdBy: true }), {
|
|
21
21
|
scopes: z
|
|
22
|
-
.readonly(
|
|
22
|
+
.readonly(
|
|
23
|
+
z.array(
|
|
24
|
+
z.union([
|
|
25
|
+
z.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard]),
|
|
26
|
+
Scope.schema.Zone,
|
|
27
|
+
]),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
23
30
|
.check(z.describe("Granted scopes. '*' grants all scopes.")),
|
|
24
31
|
}),
|
|
25
32
|
'Fields accepted when minting an API key.',
|
|
@@ -54,17 +54,22 @@ describe('GET /scopes', () => {
|
|
|
54
54
|
"scope": "webhooks:write",
|
|
55
55
|
"selfServe": false,
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
"description": "Read RPC and indexed data for one zone chain id.",
|
|
59
|
+
"scope": "zone:<chainId>:read",
|
|
60
|
+
"selfServe": false,
|
|
61
|
+
},
|
|
57
62
|
],
|
|
58
63
|
}
|
|
59
64
|
`)
|
|
60
65
|
})
|
|
61
66
|
|
|
62
|
-
test('serves the Scope
|
|
67
|
+
test('serves the extended Scope catalog source of truth verbatim', async () => {
|
|
63
68
|
const { client } = TestAdmin.setup()
|
|
64
69
|
const response = await client.scopes.$get()
|
|
65
70
|
const body = await TestApp.json(response, Scopes.schema.getScopes.Response)
|
|
66
71
|
|
|
67
|
-
expect(body.data).toEqual(Scope.
|
|
72
|
+
expect(body.data).toEqual(Scope.extend())
|
|
68
73
|
})
|
|
69
74
|
|
|
70
75
|
test('is never cached (admin responses are per-principal)', async () => {
|
package/src/admin/apps/scopes.ts
CHANGED
|
@@ -33,7 +33,7 @@ export namespace schema {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** Catalog of
|
|
36
|
+
/** Catalog of API-key scopes, including dynamic scope templates. */
|
|
37
37
|
export function scopes() {
|
|
38
38
|
return new Hono<App.Environment>().get(
|
|
39
39
|
'/',
|
|
@@ -45,6 +45,6 @@ export function scopes() {
|
|
|
45
45
|
summary: 'List scopes',
|
|
46
46
|
tags: ['Scopes'],
|
|
47
47
|
}),
|
|
48
|
-
(c) => c.json({ data: Scope.
|
|
48
|
+
(c) => c.json({ data: Scope.extend() }),
|
|
49
49
|
)
|
|
50
50
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { Dialog as Base } from '@base-ui/react/dialog'
|
|
2
2
|
import type { ComponentProps, ReactNode } from 'react'
|
|
3
|
+
import { cx } from 'regen-ui'
|
|
3
4
|
|
|
4
5
|
/** Backdrop + centered popup shell. Children render inside; pair with Title/Description. */
|
|
5
|
-
function Popup(props: { children: ReactNode }) {
|
|
6
|
+
function Popup(props: { children: ReactNode; className?: string | undefined }) {
|
|
6
7
|
return (
|
|
7
8
|
<Base.Portal>
|
|
8
9
|
<Base.Backdrop className="fixed inset-0 z-40 bg-black/60 transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" />
|
|
9
|
-
<Base.Popup
|
|
10
|
+
<Base.Popup
|
|
11
|
+
className={cx(
|
|
12
|
+
'fixed left-1/2 top-1/2 z-50 w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 border border-border bg-surface p-6 shadow-lg outline-none transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0',
|
|
13
|
+
props.className,
|
|
14
|
+
)}
|
|
15
|
+
>
|
|
10
16
|
{props.children}
|
|
11
17
|
</Base.Popup>
|
|
12
18
|
</Base.Portal>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { fromZoneChainId } from './scopes'
|
|
4
|
+
|
|
5
|
+
describe('fromZoneChainId', () => {
|
|
6
|
+
it('builds a canonical Zone read scope', () => {
|
|
7
|
+
expect(fromZoneChainId(' 000421700001 ')).toBe('zone:421700001:read')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('rejects non-positive and malformed chain ids', () => {
|
|
11
|
+
expect(['', '0', '-1', '1.5', 'mainnet'].map(fromZoneChainId)).toEqual([
|
|
12
|
+
undefined,
|
|
13
|
+
undefined,
|
|
14
|
+
undefined,
|
|
15
|
+
undefined,
|
|
16
|
+
undefined,
|
|
17
|
+
])
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -6,6 +6,18 @@ import { client } from './client'
|
|
|
6
6
|
/** A single issuable API-key scope from the catalog endpoint. */
|
|
7
7
|
export type Scope = InferResponseType<(typeof client)['scopes']['$get'], 200>['data'][number]
|
|
8
8
|
|
|
9
|
+
/** Dynamic Zone scope template returned by the scope catalog. */
|
|
10
|
+
export const zoneTemplate = 'zone:<chainId>:read'
|
|
11
|
+
|
|
12
|
+
/** Builds a canonical Zone read scope from a positive integer chain id. */
|
|
13
|
+
export function fromZoneChainId(value: string): `zone:${string}:read` | undefined {
|
|
14
|
+
const chainId = value.trim()
|
|
15
|
+
if (!/^\d+$/.test(chainId)) return undefined
|
|
16
|
+
const normalized = BigInt(chainId)
|
|
17
|
+
if (normalized === 0n) return undefined
|
|
18
|
+
return `zone:${normalized}:read`
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/** List the issuable API-key scope catalog. */
|
|
10
22
|
export namespace get {
|
|
11
23
|
export const key = ['scopes'] as const
|
|
@@ -36,7 +36,18 @@ function RouteComponent() {
|
|
|
36
36
|
const rateLimit = field('rateLimit')
|
|
37
37
|
const perMinute = rateLimit ? Number(rateLimit) : undefined
|
|
38
38
|
const environment = field('environment') === 'sandbox' ? 'sandbox' : 'production'
|
|
39
|
-
const
|
|
39
|
+
const zoneChainId = field('zoneChainId')
|
|
40
|
+
const pendingZoneScope = zoneChainId ? Scopes.fromZoneChainId(zoneChainId) : undefined
|
|
41
|
+
if (zoneChainId && !pendingZoneScope) {
|
|
42
|
+
alert('Enter a positive integer Zone chain ID.')
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const scopes = [
|
|
46
|
+
...new Set([
|
|
47
|
+
...form.getAll('scopes').filter((v): v is string => typeof v === 'string'),
|
|
48
|
+
...(pendingZoneScope ? [pendingZoneScope] : []),
|
|
49
|
+
]),
|
|
50
|
+
]
|
|
40
51
|
if (scopes.length === 0) {
|
|
41
52
|
alert('Select at least one scope.')
|
|
42
53
|
return
|
|
@@ -98,7 +109,7 @@ function RouteComponent() {
|
|
|
98
109
|
)}
|
|
99
110
|
loading={keys.isPending}
|
|
100
111
|
/>
|
|
101
|
-
<Dialog.Popup>
|
|
112
|
+
<Dialog.Popup className="flex max-h-[calc(100dvh-2rem)] flex-col overflow-hidden">
|
|
102
113
|
<ProvisionForm
|
|
103
114
|
onSubmit={onSubmit}
|
|
104
115
|
orgId={filters.orgId}
|
|
@@ -169,7 +180,7 @@ function ProvisionForm(props: {
|
|
|
169
180
|
: 'Create a key that authenticates requests to the API.'}
|
|
170
181
|
</Dialog.Description>
|
|
171
182
|
<form
|
|
172
|
-
className="mt-6 flex flex-col gap-8"
|
|
183
|
+
className="mt-6 flex min-h-0 flex-1 flex-col gap-8"
|
|
173
184
|
onSubmit={(event) => {
|
|
174
185
|
event.preventDefault()
|
|
175
186
|
props.onSubmit(new FormData(event.currentTarget))
|
|
@@ -190,7 +201,10 @@ function ProvisionForm(props: {
|
|
|
190
201
|
) : null}
|
|
191
202
|
</div>
|
|
192
203
|
) : null}
|
|
193
|
-
<Fields
|
|
204
|
+
<Fields
|
|
205
|
+
className="min-h-0 flex-1 gap-6 [grid-template-rows:auto_auto_auto_minmax(0,1fr)]"
|
|
206
|
+
variant="default"
|
|
207
|
+
>
|
|
194
208
|
<Input label="Name" name="name" placeholder="e.g. production-api" />
|
|
195
209
|
<div className="flex flex-col gap-2">
|
|
196
210
|
<span className="label-13 text-foreground-secondary">Environment</span>
|
|
@@ -212,35 +226,41 @@ function ProvisionForm(props: {
|
|
|
212
226
|
suffix="req/min"
|
|
213
227
|
type="number"
|
|
214
228
|
/>
|
|
215
|
-
<div className="flex flex-col gap-2">
|
|
229
|
+
<div className="flex min-h-0 flex-col gap-2 overflow-hidden">
|
|
216
230
|
<span className="label-13 text-foreground-secondary">Scopes</span>
|
|
217
231
|
{scopes.isPending ? (
|
|
218
232
|
<span className="text-label-13 text-foreground-secondary">Loading scopes…</span>
|
|
219
233
|
) : scopes.error ? (
|
|
220
234
|
<span className="text-label-13 text-negative">{scopes.error.message}</span>
|
|
221
235
|
) : (
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
236
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain border border-border">
|
|
237
|
+
<Rows variant="surface">
|
|
238
|
+
{(scopes.data?.data ?? []).map((entry) =>
|
|
239
|
+
entry.scope === Scopes.zoneTemplate ? (
|
|
240
|
+
<ZoneScopeRow description={entry.description} key={entry.scope} />
|
|
241
|
+
) : (
|
|
242
|
+
<Rows.Row
|
|
243
|
+
key={entry.scope}
|
|
244
|
+
className="min-h-[56px] py-3"
|
|
245
|
+
render={<label className="cursor-pointer" />}
|
|
246
|
+
style={{ height: 'auto' }}
|
|
247
|
+
>
|
|
248
|
+
<Checkbox
|
|
249
|
+
defaultChecked={entry.scope === 'data:read'}
|
|
250
|
+
name="scopes"
|
|
251
|
+
value={entry.scope}
|
|
252
|
+
/>
|
|
253
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
254
|
+
<span className="text-label-13 text-foreground">{entry.scope}</span>
|
|
255
|
+
<span className="text-label-12 text-foreground-secondary">
|
|
256
|
+
{entry.description}
|
|
257
|
+
</span>
|
|
258
|
+
</span>
|
|
259
|
+
</Rows.Row>
|
|
260
|
+
),
|
|
261
|
+
)}
|
|
262
|
+
</Rows>
|
|
263
|
+
</div>
|
|
244
264
|
)}
|
|
245
265
|
</div>
|
|
246
266
|
</Fields>
|
|
@@ -255,6 +275,87 @@ function ProvisionForm(props: {
|
|
|
255
275
|
)
|
|
256
276
|
}
|
|
257
277
|
|
|
278
|
+
function ZoneScopeRow(props: { description: string }) {
|
|
279
|
+
const [chainId, setChainId] = useState('')
|
|
280
|
+
const [selected, setSelected] = useState<readonly string[]>([])
|
|
281
|
+
const scope = Scopes.fromZoneChainId(chainId)
|
|
282
|
+
const duplicate = scope ? selected.includes(scope) : false
|
|
283
|
+
const error =
|
|
284
|
+
chainId && !scope
|
|
285
|
+
? 'Enter a positive integer chain ID.'
|
|
286
|
+
: duplicate
|
|
287
|
+
? 'Zone already added.'
|
|
288
|
+
: false
|
|
289
|
+
|
|
290
|
+
function add() {
|
|
291
|
+
if (!scope || duplicate) return
|
|
292
|
+
setSelected((current) => [...current, scope])
|
|
293
|
+
setChainId('')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Rows.Row className="min-h-[56px] flex-col items-stretch py-3" style={{ height: 'auto' }}>
|
|
298
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
299
|
+
<span className="text-label-13 text-foreground">{Scopes.zoneTemplate}</span>
|
|
300
|
+
<span className="text-label-12 text-foreground-secondary">{props.description}</span>
|
|
301
|
+
</span>
|
|
302
|
+
<div className="flex items-start gap-2 max-sm:flex-col">
|
|
303
|
+
<div className="min-w-0 flex-1 max-sm:w-full">
|
|
304
|
+
<Input
|
|
305
|
+
aria-label="Zone chain ID"
|
|
306
|
+
autoComplete="off"
|
|
307
|
+
error={error}
|
|
308
|
+
inputMode="numeric"
|
|
309
|
+
name="zoneChainId"
|
|
310
|
+
onChange={(event) => setChainId(event.target.value)}
|
|
311
|
+
onKeyDown={(event) => {
|
|
312
|
+
if (event.key !== 'Enter') return
|
|
313
|
+
event.preventDefault()
|
|
314
|
+
add()
|
|
315
|
+
}}
|
|
316
|
+
pattern="[0-9]*"
|
|
317
|
+
placeholder="421700001"
|
|
318
|
+
prefix="zone:"
|
|
319
|
+
suffix=":read"
|
|
320
|
+
value={chainId}
|
|
321
|
+
/>
|
|
322
|
+
</div>
|
|
323
|
+
<Button
|
|
324
|
+
className="max-sm:w-full"
|
|
325
|
+
disabled={!scope || duplicate}
|
|
326
|
+
onClick={add}
|
|
327
|
+
type="button"
|
|
328
|
+
>
|
|
329
|
+
Add zone
|
|
330
|
+
</Button>
|
|
331
|
+
</div>
|
|
332
|
+
{selected.length > 0 ? (
|
|
333
|
+
<ul className="flex flex-col gap-2">
|
|
334
|
+
{selected.map((scope) => (
|
|
335
|
+
<li
|
|
336
|
+
className="flex min-w-0 items-center gap-2 border border-border bg-secondary py-1 pr-1 pl-2"
|
|
337
|
+
key={scope}
|
|
338
|
+
>
|
|
339
|
+
<input name="scopes" type="hidden" value={scope} />
|
|
340
|
+
<code className="min-w-0 flex-1 truncate text-label-12 text-foreground">{scope}</code>
|
|
341
|
+
<Button.Icon
|
|
342
|
+
onClick={() =>
|
|
343
|
+
setSelected((current) => current.filter((candidate) => candidate !== scope))
|
|
344
|
+
}
|
|
345
|
+
size="md"
|
|
346
|
+
title={`Remove ${scope}`}
|
|
347
|
+
variant="plain"
|
|
348
|
+
>
|
|
349
|
+
<X className="size-4" />
|
|
350
|
+
</Button.Icon>
|
|
351
|
+
</li>
|
|
352
|
+
))}
|
|
353
|
+
</ul>
|
|
354
|
+
) : null}
|
|
355
|
+
</Rows.Row>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
258
359
|
function KeyList(props: {
|
|
259
360
|
environment: 'production' | 'sandbox'
|
|
260
361
|
error: Error | null
|