tapimo 0.4.1 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +1990 -1442
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1304 -1304
- package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +10 -120
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/fee-amm.d.ts +20 -20
- package/dist/apps/data/routes/funding.js +7 -7
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/management/App.d.ts +808 -267
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +116 -100
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +182 -50
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +100 -40
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +122 -33
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +4 -18
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +38 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +4 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/internal/Auth.d.ts +46 -40
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +118 -69
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Errors.d.ts +6 -0
- package/dist/internal/Errors.d.ts.map +1 -0
- package/dist/internal/Errors.js +16 -0
- package/dist/internal/Errors.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +15 -43
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +8 -17
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Store.d.ts +61 -16
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +72 -17
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/dist/internal/index.d.ts +27 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +27 -0
- package/dist/internal/index.js.map +1 -0
- package/package.json +8 -3
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +104 -6
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +17 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/FundingRouteProviders.test.ts +37 -93
- package/src/apps/data/FundingRouteProviders.ts +14 -161
- package/src/apps/data/routes/funding.test.ts +7 -42
- package/src/apps/data/routes/funding.ts +8 -8
- package/src/apps/data/routes/indexer.test.ts +2 -2
- package/src/apps/data/routes/webhooks.test.ts +2 -2
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +72 -7
- package/src/apps/mcp.ts +46 -4
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/cloudflare.ts +5 -0
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/internal/Auth.test.ts +571 -23
- package/src/internal/Auth.ts +166 -94
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +8 -8
- package/src/internal/FundingRoutes.ts +25 -29
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -4
- package/src/internal/Store.test-d.ts +15 -0
- package/src/internal/Store.test.ts +153 -2
- package/src/internal/Store.ts +157 -41
- package/src/internal/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
|
@@ -19,6 +19,8 @@ describe('scopes', () => {
|
|
|
19
19
|
[
|
|
20
20
|
"data:read",
|
|
21
21
|
"indexer:query",
|
|
22
|
+
"management:read",
|
|
23
|
+
"management:write",
|
|
22
24
|
"rpc-relay:read",
|
|
23
25
|
"rpc-relay:sponsor",
|
|
24
26
|
"webhooks:read",
|
|
@@ -26,4 +28,18 @@ describe('scopes', () => {
|
|
|
26
28
|
]
|
|
27
29
|
`)
|
|
28
30
|
})
|
|
31
|
+
|
|
32
|
+
test('includes host scopes configured on the app', async () => {
|
|
33
|
+
const custom = {
|
|
34
|
+
description: 'Read project accounts.',
|
|
35
|
+
scope: 'accounts:read',
|
|
36
|
+
selfServe: true,
|
|
37
|
+
} as const satisfies Scope.Entry
|
|
38
|
+
const app = TestApp.create({ scopes: [custom] })
|
|
39
|
+
const response = await app.request('/v1/scopes')
|
|
40
|
+
const body = await TestApp.json(response, Scopes.schema.getScopes.Response)
|
|
41
|
+
|
|
42
|
+
expect(response.status).toBe(200)
|
|
43
|
+
expect(body.data).toEqual([...Scope.catalog, custom])
|
|
44
|
+
})
|
|
29
45
|
})
|
|
@@ -6,7 +6,6 @@ import * as Auth from '../../../internal/Auth.js'
|
|
|
6
6
|
import * as Cache from '../../../internal/Cache.js'
|
|
7
7
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
8
8
|
import * as Response from '../../../internal/Response.js'
|
|
9
|
-
import * as Scope from '../../../Scope.js'
|
|
10
9
|
|
|
11
10
|
/** Zod schemas owned by the scope-catalog resource. */
|
|
12
11
|
export namespace schema {
|
|
@@ -22,7 +21,7 @@ export namespace schema {
|
|
|
22
21
|
z.meta({ examples: ['Read webhook subscriptions.'] }),
|
|
23
22
|
),
|
|
24
23
|
scope: z
|
|
25
|
-
.
|
|
24
|
+
.string()
|
|
26
25
|
.check(
|
|
27
26
|
z.describe('Scope identifier used in API-key grants and route policies.'),
|
|
28
27
|
z.meta({ examples: ['data:read'] }),
|
|
@@ -30,7 +29,7 @@ export namespace schema {
|
|
|
30
29
|
selfServe: z
|
|
31
30
|
.boolean()
|
|
32
31
|
.check(
|
|
33
|
-
z.describe('Whether
|
|
32
|
+
z.describe('Whether an eligible session may self-mint this scope.'),
|
|
34
33
|
z.meta({ examples: [true] }),
|
|
35
34
|
),
|
|
36
35
|
})
|
|
@@ -47,13 +46,13 @@ export namespace schema {
|
|
|
47
46
|
|
|
48
47
|
/**
|
|
49
48
|
* Mounts the `/scopes` resource: the issuable API-key scope vocabulary, served
|
|
50
|
-
* from
|
|
49
|
+
* from the configured catalog. Public catalog data: anonymous callers are served
|
|
51
50
|
* within the public quota; the session lane lets signed-in consoles skip it.
|
|
52
51
|
*/
|
|
53
52
|
export function scopes() {
|
|
54
53
|
return new Hono<App.Environment>().get(
|
|
55
54
|
'/v1/scopes',
|
|
56
|
-
Auth.policy({ public: true, session: true }),
|
|
55
|
+
Auth.policy({ apiKey: { scopes: [] }, public: true, session: true }),
|
|
57
56
|
OpenApi.describeRoute({
|
|
58
57
|
description: 'See which scopes can be granted to API keys.',
|
|
59
58
|
operationId: 'getScopes',
|
|
@@ -65,7 +64,7 @@ export function scopes() {
|
|
|
65
64
|
}),
|
|
66
65
|
Cache.response<App.Environment>({
|
|
67
66
|
cacheControl: Cache.policies.stable,
|
|
68
|
-
name: 'tempo-api:scopes:
|
|
67
|
+
name: 'tempo-api:scopes:v2',
|
|
69
68
|
key: (c) => {
|
|
70
69
|
const url = new URL(c.req.url)
|
|
71
70
|
url.search = ''
|
|
@@ -76,7 +75,7 @@ export function scopes() {
|
|
|
76
75
|
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
77
76
|
return c.json(
|
|
78
77
|
Response.validated(schema.getScopes.Response, {
|
|
79
|
-
data:
|
|
78
|
+
data: c.get('scopeCatalog').map((entry) => ({ ...entry })),
|
|
80
79
|
}),
|
|
81
80
|
200,
|
|
82
81
|
)
|
|
@@ -285,16 +285,12 @@ export namespace schema {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
/**
|
|
289
|
-
* Mounts the usage surface: project request usage (read from a host-supplied
|
|
290
|
-
* analytics capability) and an organization's sponsorship volume and fees
|
|
291
|
-
* over time. Session lane; reads for members.
|
|
292
|
-
*/
|
|
288
|
+
/** Mounts organization request and sponsorship usage. Reads require membership or a scoped API key. */
|
|
293
289
|
export function usage() {
|
|
294
290
|
return new Hono<Environment>()
|
|
295
291
|
.get(
|
|
296
292
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/usage/requests',
|
|
297
|
-
Auth.policy({ session: true }),
|
|
293
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
298
294
|
Auth.ensureOrg(),
|
|
299
295
|
OpenApi.validate('param', schema.getRequestUsage.Params, {
|
|
300
296
|
code: 'param_invalid',
|
|
@@ -327,7 +323,7 @@ export function usage() {
|
|
|
327
323
|
tags: ['Usage'],
|
|
328
324
|
}),
|
|
329
325
|
async (c) => {
|
|
330
|
-
if (Auth.narrowAccess) return Auth.
|
|
326
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
331
327
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
332
328
|
if (OpenApi.narrowValidation)
|
|
333
329
|
return OpenApi.validationError(c, {
|
|
@@ -398,7 +394,7 @@ export function usage() {
|
|
|
398
394
|
)
|
|
399
395
|
.get(
|
|
400
396
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/usage/sponsorships',
|
|
401
|
-
Auth.policy({ session: true }),
|
|
397
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
402
398
|
Auth.ensureOrg(),
|
|
403
399
|
OpenApi.validate('param', schema.getSponsorshipUsage.Params, {
|
|
404
400
|
code: 'param_invalid',
|
|
@@ -423,7 +419,7 @@ export function usage() {
|
|
|
423
419
|
tags: ['Usage'],
|
|
424
420
|
}),
|
|
425
421
|
async (c) => {
|
|
426
|
-
if (Auth.narrowAccess) return Auth.
|
|
422
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
427
423
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
428
424
|
if (OpenApi.narrowValidation)
|
|
429
425
|
return OpenApi.validationError(c, {
|
|
@@ -498,11 +494,10 @@ async function legacyKeyIds(
|
|
|
498
494
|
): Promise<readonly string[]> {
|
|
499
495
|
const kv = c.get('kv')
|
|
500
496
|
if (!kv) return []
|
|
501
|
-
const records = await ApiKeys.listByOrg(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
497
|
+
const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
|
|
498
|
+
...(options.projectId === undefined ? {} : { projectId: options.projectId }),
|
|
499
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
500
|
+
})
|
|
506
501
|
return records
|
|
507
502
|
.filter(
|
|
508
503
|
(record) => options.environment === undefined || record.environment === options.environment,
|
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')
|
|
@@ -121,6 +167,25 @@ test('scopes tools to the data domain', async () => {
|
|
|
121
167
|
expect(tools.some((name) => name.startsWith('v1_me'))).toBe(false)
|
|
122
168
|
})
|
|
123
169
|
|
|
170
|
+
test('keeps tool input schemas compact', async () => {
|
|
171
|
+
const { host } = createHost()
|
|
172
|
+
await initialize(host)
|
|
173
|
+
|
|
174
|
+
const tool = await toolDetails(host, 'v1_tokens_get')
|
|
175
|
+
const properties = tool.inputSchema.properties ?? {}
|
|
176
|
+
|
|
177
|
+
// Credential inputs stay off the schema; auth rides the forwarded header.
|
|
178
|
+
expect(properties['authorization']).toBeUndefined()
|
|
179
|
+
expect(properties['tempo-api-key']).toBeUndefined()
|
|
180
|
+
// Shared parameters carry the terse tool descriptions.
|
|
181
|
+
expect(properties['limit']?.description).toBe('Items per page.')
|
|
182
|
+
expect(properties['cursor']?.description).toBe(
|
|
183
|
+
'Keyset cursor from a previous response `nextCursor`; omit for the first page.',
|
|
184
|
+
)
|
|
185
|
+
// `compact` strips examples from generated schemas.
|
|
186
|
+
expect(JSON.stringify(tool.inputSchema)).not.toContain('"examples"')
|
|
187
|
+
})
|
|
188
|
+
|
|
124
189
|
test('executes generated tools against the composed app', async () => {
|
|
125
190
|
// Verified tokens are database-backed, so seed the default chain.
|
|
126
191
|
const db = TestApp.database()
|
|
@@ -128,7 +193,7 @@ test('executes generated tools against the composed app', async () => {
|
|
|
128
193
|
const { host } = createHost({ app: TestApp.create({ db }) })
|
|
129
194
|
|
|
130
195
|
await initialize(host)
|
|
131
|
-
const result = await
|
|
196
|
+
const result = await generatedToolCall(host, 'v1_tokens_get', { verified: true })
|
|
132
197
|
|
|
133
198
|
expect(result.isError).toBeFalsy()
|
|
134
199
|
const body = resultJson(result) as { data: { verified: boolean }[]; nextCursor: unknown }
|
|
@@ -140,10 +205,10 @@ test('forwards the caller Authorization header to authenticated tools', async ()
|
|
|
140
205
|
const { host } = createHost({ app: TestApp.create({ auth: { keys: [reader] } }) })
|
|
141
206
|
await initialize(host)
|
|
142
207
|
|
|
143
|
-
const denied = await
|
|
208
|
+
const denied = await generatedToolCall(host, 'v1_webhooks_get', {})
|
|
144
209
|
expect(denied.isError).toBe(true)
|
|
145
210
|
|
|
146
|
-
const allowed = await
|
|
211
|
+
const allowed = await generatedToolCall(
|
|
147
212
|
host,
|
|
148
213
|
'v1_webhooks_get',
|
|
149
214
|
{},
|
|
@@ -157,7 +222,7 @@ test('proxies docs tool calls to the remote MCP source', async () => {
|
|
|
157
222
|
const { host } = createHost({ docs: docsStub() })
|
|
158
223
|
await initialize(host)
|
|
159
224
|
|
|
160
|
-
const result = await
|
|
225
|
+
const result = await generatedToolCall(host, 'docs_search', { query: 'fees' })
|
|
161
226
|
|
|
162
227
|
expect(result.isError).toBeFalsy()
|
|
163
228
|
expect(resultJson(result)).toMatchObject({ hits: ['fees'] })
|
|
@@ -176,7 +241,7 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
|
|
|
176
241
|
expect(operation?.operationId).toBe('mcpRequest')
|
|
177
242
|
expect(operation?.tags).toEqual(['MCP'])
|
|
178
243
|
// Anonymous and any-valid-key access; no required scopes.
|
|
179
|
-
expect(operation?.security).toEqual([{ apiKey: [] }, { bearerAuth: [] }, {}])
|
|
244
|
+
expect(operation?.security).toEqual([{ apiKey: [] }, { queryApiKey: [] }, { bearerAuth: [] }, {}])
|
|
180
245
|
|
|
181
246
|
// Surfaces under `Data API` in the reference, but the tool filter excludes the `MCP` tag.
|
|
182
247
|
const groups = Object.fromEntries(spec['x-tagGroups'].map((group) => [group.name, group.tags]))
|
|
@@ -186,7 +251,7 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
|
|
|
186
251
|
// Documenting the route makes it auth-enforced; the public lane keeps it
|
|
187
252
|
// anonymously reachable, and the endpoint never surfaces itself as a tool.
|
|
188
253
|
await initialize(app)
|
|
189
|
-
const tools = await
|
|
254
|
+
const tools = (await toolsSearch(app)).map((tool) => tool.name)
|
|
190
255
|
expect(tools.length).toBeGreaterThan(0)
|
|
191
256
|
expect(tools.some((name) => name.startsWith('mcp'))).toBe(false)
|
|
192
257
|
|
package/src/apps/mcp.ts
CHANGED
|
@@ -15,7 +15,7 @@ const excludedTags = ['CoinGecko', 'MCP']
|
|
|
15
15
|
|
|
16
16
|
/** Usage guidance surfaced to MCP clients on initialize. */
|
|
17
17
|
const instructions =
|
|
18
|
-
"Tools mirror the Tempo API's data domain. Authenticated tools accept a Tempo API key
|
|
18
|
+
"Tools mirror the Tempo API's data domain. Authenticated tools accept a Tempo API key sent as an `Authorization: Bearer` header on the HTTP request. List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation."
|
|
19
19
|
|
|
20
20
|
/** Zod schemas owned by the MCP endpoint's OpenAPI description. */
|
|
21
21
|
export namespace schema {
|
|
@@ -97,7 +97,15 @@ export function mcp(options: mcp.Options) {
|
|
|
97
97
|
},
|
|
98
98
|
mcp: { instructions },
|
|
99
99
|
openapi: scope(document),
|
|
100
|
-
|
|
100
|
+
// `compact` + `security: false` keep tool schemas lean: no examples,
|
|
101
|
+
// oversized regexes, or per-tool credential inputs (auth rides the
|
|
102
|
+
// forwarded `Authorization` header).
|
|
103
|
+
openapiConfig: {
|
|
104
|
+
compact: true,
|
|
105
|
+
forwardHeaders: ['authorization'],
|
|
106
|
+
mode: 'namespace',
|
|
107
|
+
security: false,
|
|
108
|
+
},
|
|
101
109
|
version,
|
|
102
110
|
})
|
|
103
111
|
if (docs !== false)
|
|
@@ -139,7 +147,9 @@ export function mcp(options: mcp.Options) {
|
|
|
139
147
|
example: {
|
|
140
148
|
jsonrpc: '2.0',
|
|
141
149
|
id: 1,
|
|
142
|
-
result: {
|
|
150
|
+
result: {
|
|
151
|
+
tools: [{ name: 'search_tools', description: 'Search available tools' }],
|
|
152
|
+
},
|
|
143
153
|
},
|
|
144
154
|
schema: OpenApi.resolver(schema.Response),
|
|
145
155
|
},
|
|
@@ -217,11 +227,43 @@ function scope(document: Document): Openapi.OpenAPISpec {
|
|
|
217
227
|
return tags_operation?.some((tag) => tags.has(tag)) ?? false
|
|
218
228
|
})
|
|
219
229
|
if (entries.some(([method]) => methods.includes(method)))
|
|
220
|
-
paths[path] = Object.fromEntries(
|
|
230
|
+
paths[path] = Object.fromEntries(
|
|
231
|
+
entries.map(([method, operation]) =>
|
|
232
|
+
methods.includes(method) ? [method, condense(operation)] : [method, operation],
|
|
233
|
+
),
|
|
234
|
+
)
|
|
221
235
|
}
|
|
222
236
|
return { ...document, paths }
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
/** Terse tool descriptions for parameters repeated across most operations; the public document keeps the full prose. */
|
|
240
|
+
const parameterDescriptions: Record<string, string> = {
|
|
241
|
+
chainId: 'Chain: `mainnet` (default), `testnet`, or a numeric chain id.',
|
|
242
|
+
cursor: 'Keyset cursor from a previous response `nextCursor`; omit for the first page.',
|
|
243
|
+
include: 'Comma-separated related resources to include.',
|
|
244
|
+
limit: 'Items per page.',
|
|
245
|
+
order: 'Sort order: `asc` or `desc` (default).',
|
|
246
|
+
page: '1-indexed page number; mutually exclusive with `cursor`.',
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Swaps shared query-parameter prose for the terse tool descriptions. */
|
|
250
|
+
function condense(operation: unknown) {
|
|
251
|
+
const { parameters } = operation as {
|
|
252
|
+
parameters?: readonly { in?: string; name?: string }[] | undefined
|
|
253
|
+
}
|
|
254
|
+
if (!parameters) return operation
|
|
255
|
+
return {
|
|
256
|
+
...(operation as object),
|
|
257
|
+
parameters: parameters.map((parameter) => {
|
|
258
|
+
const description =
|
|
259
|
+
parameter.in === 'query' && parameter.name
|
|
260
|
+
? parameterDescriptions[parameter.name]
|
|
261
|
+
: undefined
|
|
262
|
+
return description ? { ...parameter, description } : parameter
|
|
263
|
+
}),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
225
267
|
export declare namespace mcp {
|
|
226
268
|
/** Options for the MCP route group. */
|
|
227
269
|
type Options = {
|
|
@@ -29,14 +29,14 @@ const senderAccount = Tempo.accounts[9]!
|
|
|
29
29
|
/** Super admin secret configured on the end-to-end billing app. */
|
|
30
30
|
const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
|
|
31
31
|
|
|
32
|
-
/** Keys covering the sponsorship policy matrix
|
|
32
|
+
/** Keys covering the sponsorship policy matrix. */
|
|
33
33
|
const keys = [
|
|
34
34
|
{
|
|
35
35
|
id: 'key_sponsor',
|
|
36
36
|
orgId: 'org_1',
|
|
37
37
|
projectId: 'prj_1',
|
|
38
38
|
environment: 'sandbox',
|
|
39
|
-
scopes: ['rpc-relay:
|
|
39
|
+
scopes: ['rpc-relay:sponsor'],
|
|
40
40
|
token: 'secret_sponsor',
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -44,7 +44,7 @@ const keys = [
|
|
|
44
44
|
orgId: 'org_1',
|
|
45
45
|
projectId: 'prj_1',
|
|
46
46
|
environment: 'production',
|
|
47
|
-
scopes: ['rpc-relay:
|
|
47
|
+
scopes: ['rpc-relay:sponsor'],
|
|
48
48
|
token: 'secret_sponsor_prod',
|
|
49
49
|
},
|
|
50
50
|
{
|
|
@@ -75,7 +75,7 @@ const keys = [
|
|
|
75
75
|
id: 'key_unattributed',
|
|
76
76
|
orgId: 'org_1',
|
|
77
77
|
environment: 'sandbox',
|
|
78
|
-
scopes: ['rpc-relay:
|
|
78
|
+
scopes: ['rpc-relay:sponsor'],
|
|
79
79
|
token: 'secret_unattributed',
|
|
80
80
|
},
|
|
81
81
|
] as const satisfies readonly TestApp.kvStore.Key[]
|
|
@@ -169,7 +169,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
169
169
|
expect(row?.orgId).toMatchInlineSnapshot(`"org_2"`)
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
test('behavior: keys
|
|
172
|
+
test('behavior: read-only keys cannot access the sponsor route', async () => {
|
|
173
173
|
const db = TestApp.database()
|
|
174
174
|
const app = TestApp.create({
|
|
175
175
|
auth: { keys },
|
|
@@ -177,14 +177,15 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
177
177
|
relay: { feePayer: { account: feePayerAccount } },
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
expect(
|
|
180
|
+
const response = await app.fetch(
|
|
181
|
+
new Request('http://tempo-api.test/rpc/sponsor', {
|
|
182
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
|
|
183
|
+
headers: { 'content-type': 'application/json', 'tempo-api-key': 'secret_unscoped' },
|
|
184
|
+
method: 'POST',
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
expect(response.status).toBe(403)
|
|
188
|
+
expect(await response.json()).toMatchObject({ error: { code: 'api_key_forbidden' } })
|
|
188
189
|
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
189
190
|
})
|
|
190
191
|
|
|
@@ -288,12 +289,16 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
288
289
|
nonce: '0x0',
|
|
289
290
|
},
|
|
290
291
|
],
|
|
291
|
-
token: '
|
|
292
|
+
token: 'secret_sponsor',
|
|
292
293
|
})
|
|
293
294
|
const result = body.result as unknown as
|
|
294
|
-
| {
|
|
295
|
+
| {
|
|
296
|
+
capabilities?: { balanceDiffs?: unknown; sponsored?: boolean }
|
|
297
|
+
tx?: { feePayerSignature?: string | null }
|
|
298
|
+
}
|
|
295
299
|
| undefined
|
|
296
300
|
expect(result?.tx?.feePayerSignature ?? null).toBeNull()
|
|
301
|
+
expect(result?.capabilities?.balanceDiffs).toBeUndefined()
|
|
297
302
|
expect(result?.capabilities?.sponsored).toBe(false)
|
|
298
303
|
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
299
304
|
})
|
|
@@ -629,7 +634,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
|
|
|
629
634
|
chain: Tempo.chain,
|
|
630
635
|
transport: withRelay(
|
|
631
636
|
Relay.http(),
|
|
632
|
-
http(`${server.url}/rpc/
|
|
637
|
+
http(`${server.url}/rpc/sponsor`, {
|
|
633
638
|
fetchOptions: { headers: { 'tempo-api-key': 'secret_sponsor' } },
|
|
634
639
|
}),
|
|
635
640
|
policy ? { policy } : {},
|
|
@@ -785,13 +790,13 @@ async function signSponsorable(
|
|
|
785
790
|
})
|
|
786
791
|
}
|
|
787
792
|
|
|
788
|
-
/** Posts one JSON-RPC request to the composed app's
|
|
793
|
+
/** Posts one JSON-RPC request to the composed app's sponsor mount. */
|
|
789
794
|
async function relayRequest(
|
|
790
795
|
app: ReturnType<typeof TestApp.create>,
|
|
791
796
|
options: { method: string; params: readonly unknown[]; token?: string | undefined },
|
|
792
797
|
) {
|
|
793
798
|
const response = await app.fetch(
|
|
794
|
-
new Request('http://tempo-api.test/rpc/
|
|
799
|
+
new Request('http://tempo-api.test/rpc/sponsor', {
|
|
795
800
|
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: options.method, params: options.params }), // prettier-ignore
|
|
796
801
|
headers: {
|
|
797
802
|
'content-type': 'application/json',
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -18,16 +18,17 @@ import * as Viem from '../../internal/Viem.js'
|
|
|
18
18
|
import * as Scope from '../../Scope.js'
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The relay route group
|
|
22
|
-
* after `data()`,
|
|
23
|
-
*
|
|
21
|
+
* The relay route group exposes wallet features at `/rpc/relay` and fee
|
|
22
|
+
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes
|
|
23
|
+
* chain clients and supported chains.
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
26
26
|
* App.create(options).route('/', data()).route('/', relay())
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export function relay(options: relay.Options = {}) {
|
|
30
|
-
const
|
|
30
|
+
const handler_relay = middleware({ ...options, features: 'all', path: 'rpc/relay' })
|
|
31
|
+
const handler_sponsor = middleware({ ...options, path: 'rpc/sponsor' })
|
|
31
32
|
return new Hono<App.Environment>()
|
|
32
33
|
.use(
|
|
33
34
|
'/rpc/relay',
|
|
@@ -39,8 +40,20 @@ export function relay(options: relay.Options = {}) {
|
|
|
39
40
|
Auth.policy({ apiKey: { scopes: ['rpc-relay:read'] }, mpp: false, public: false }),
|
|
40
41
|
OpenApi.describeRoute({ hide: true, summary: 'Relay RPC' }),
|
|
41
42
|
)
|
|
42
|
-
.all('/rpc/relay',
|
|
43
|
-
.all('/rpc/relay/:chainId',
|
|
43
|
+
.all('/rpc/relay', handler_relay)
|
|
44
|
+
.all('/rpc/relay/:chainId', handler_relay)
|
|
45
|
+
.use(
|
|
46
|
+
'/rpc/sponsor',
|
|
47
|
+
Auth.policy({ apiKey: { scopes: ['rpc-relay:sponsor'] }, mpp: false, public: false }),
|
|
48
|
+
OpenApi.describeRoute({ hide: true, summary: 'Sponsor RPC' }),
|
|
49
|
+
)
|
|
50
|
+
.use(
|
|
51
|
+
'/rpc/sponsor/*',
|
|
52
|
+
Auth.policy({ apiKey: { scopes: ['rpc-relay:sponsor'] }, mpp: false, public: false }),
|
|
53
|
+
OpenApi.describeRoute({ hide: true, summary: 'Sponsor RPC' }),
|
|
54
|
+
)
|
|
55
|
+
.all('/rpc/sponsor', handler_sponsor)
|
|
56
|
+
.all('/rpc/sponsor/:chainId', handler_sponsor)
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
export declare namespace relay {
|
|
@@ -58,11 +71,9 @@ export declare namespace relay {
|
|
|
58
71
|
}
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
// clients are memoized upstream, so per-request handler construction is cheap.
|
|
65
|
-
function middleware(options: relay.Options = {}) {
|
|
74
|
+
// Builds request-aware RPC middleware. Viem clients are memoized upstream, so
|
|
75
|
+
// constructing `Handler.relay` per request stays cheap.
|
|
76
|
+
function middleware(options: middleware.Options) {
|
|
66
77
|
// Platform-wide per-transaction cap; org settings tighten it, never loosen.
|
|
67
78
|
const txFeeLimit = options.feePayer
|
|
68
79
|
? Billing.toBaseUnits(options.feePayer.txFeeLimit ?? '1.00')
|
|
@@ -215,12 +226,12 @@ function middleware(options: relay.Options = {}) {
|
|
|
215
226
|
|
|
216
227
|
// Dispatching a standalone Hono via `fetch(c.req.raw)` bypasses Hono's
|
|
217
228
|
// mount-path stripping, so its internal routes register at the request's
|
|
218
|
-
// full external path (`basePath` +
|
|
229
|
+
// full external path (`basePath` + the RPC mount).
|
|
219
230
|
const handler = Handler.relay({
|
|
220
231
|
cache: c.get('store'),
|
|
221
|
-
features:
|
|
232
|
+
...(options.features ? { features: options.features } : {}),
|
|
222
233
|
getClient: (chainId) => c.get('getClient')(chainId as Viem.ChainId | undefined),
|
|
223
|
-
path: Path.join(c.get('basePath'),
|
|
234
|
+
path: Path.join(c.get('basePath'), options.path),
|
|
224
235
|
...(feePayer ? { feePayer } : {}),
|
|
225
236
|
...(verifiedTokens
|
|
226
237
|
? {
|
|
@@ -234,3 +245,13 @@ function middleware(options: relay.Options = {}) {
|
|
|
234
245
|
return handler.fetch(c.req.raw)
|
|
235
246
|
})
|
|
236
247
|
}
|
|
248
|
+
|
|
249
|
+
declare namespace middleware {
|
|
250
|
+
/** Internal relay mount configuration. */
|
|
251
|
+
type Options = relay.Options & {
|
|
252
|
+
/** Optional wallet feature bundle. */
|
|
253
|
+
features?: Handler.relay.Options['features']
|
|
254
|
+
/** RPC mount path. */
|
|
255
|
+
path: 'rpc/relay' | 'rpc/sponsor'
|
|
256
|
+
}
|
|
257
|
+
}
|
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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'kysely'
|
|
11
11
|
import * as pg from 'pg'
|
|
12
12
|
|
|
13
|
+
import type * as AdminAuditLogs from './tables/adminAuditLogs.js'
|
|
13
14
|
import type * as BillingSettings from './tables/billingSettings.js'
|
|
14
15
|
import type * as Invitations from './tables/invitations.js'
|
|
15
16
|
import type * as Memberships from './tables/memberships.js'
|
|
@@ -28,6 +29,10 @@ import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
|
|
|
28
29
|
* records, queries, and {@link migrations} DDL to snake_case storage columns.
|
|
29
30
|
*/
|
|
30
31
|
export type Database = {
|
|
32
|
+
admin_audit_logs: AdminAuditLogs.Table
|
|
33
|
+
billing_settings: BillingSettings.Table
|
|
34
|
+
invitations: Invitations.Table
|
|
35
|
+
memberships: Memberships.Table
|
|
31
36
|
migrations: {
|
|
32
37
|
appliedAt: string
|
|
33
38
|
checksum: string
|
|
@@ -35,9 +40,6 @@ export type Database = {
|
|
|
35
40
|
/** SQL as applied; null on rows written before the column existed. */
|
|
36
41
|
sql: string | null
|
|
37
42
|
}
|
|
38
|
-
billing_settings: BillingSettings.Table
|
|
39
|
-
invitations: Invitations.Table
|
|
40
|
-
memberships: Memberships.Table
|
|
41
43
|
organizations: Organizations.Table
|
|
42
44
|
projects: Projects.Table
|
|
43
45
|
request_usage_buckets: RequestUsage.BucketTable
|
|
@@ -520,6 +522,60 @@ export const migrations: readonly Migration[] = [
|
|
|
520
522
|
// Partial: the reporter scans only unreported (pending) rows.
|
|
521
523
|
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS request_usage_meter_events_pending_idx ON request_usage_meter_events (first_attempted_at, identifier) WHERE state = 'pending'`), // prettier-ignore
|
|
522
524
|
},
|
|
525
|
+
{
|
|
526
|
+
// Supports the admin organization's newest-first keyset pagination.
|
|
527
|
+
name: '0045_organizations_admin_list_index',
|
|
528
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS organizations_admin_list_idx ON organizations (created_at DESC, id DESC)`), // prettier-ignore
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
// Prefix search keeps organization-name lookup index-backed.
|
|
532
|
+
name: '0046_organizations_name_prefix_index',
|
|
533
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS organizations_name_prefix_idx ON organizations (lower(name) text_pattern_ops)`), // prettier-ignore
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
// Prefix search keeps member-email lookup index-backed.
|
|
537
|
+
name: '0047_users_email_prefix_index',
|
|
538
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS users_email_prefix_idx ON users (lower(email) text_pattern_ops) WHERE email IS NOT NULL`), // prettier-ignore
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
// Exact owner lookup for organizations created by a user.
|
|
542
|
+
name: '0048_organizations_user_index',
|
|
543
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS organizations_user_idx ON organizations (user_id) WHERE user_id IS NOT NULL`), // prettier-ignore
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
// Supports oldest-first admin membership keyset pagination.
|
|
547
|
+
name: '0049_memberships_admin_list_index',
|
|
548
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS memberships_admin_list_idx ON memberships (org_id, created_at, user_id)`), // prettier-ignore
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
// Supports newest-first admin project keyset pagination.
|
|
552
|
+
name: '0050_projects_admin_list_index',
|
|
553
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS projects_admin_list_idx ON projects (org_id, created_at DESC, id DESC)`), // prettier-ignore
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: '0051_admin_audit_logs',
|
|
557
|
+
up: (db) =>
|
|
558
|
+
db.schema
|
|
559
|
+
.createTable('adminAuditLogs')
|
|
560
|
+
.ifNotExists()
|
|
561
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
562
|
+
.addColumn('actor', 'text', (c) => c.notNull())
|
|
563
|
+
.addColumn('method', 'text', (c) => c.notNull())
|
|
564
|
+
.addColumn('path', 'text', (c) => c.notNull())
|
|
565
|
+
.addColumn('query', 'text')
|
|
566
|
+
.addColumn('requestId', 'text', (c) => c.notNull())
|
|
567
|
+
.addColumn('status', 'integer', (c) => c.notNull())
|
|
568
|
+
.addColumn('createdAt', 'text', (c) => c.notNull()),
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: '0052_admin_audit_logs_created_index',
|
|
572
|
+
up: (db) =>
|
|
573
|
+
db.schema
|
|
574
|
+
.createIndex('admin_audit_logs_created_idx')
|
|
575
|
+
.ifNotExists()
|
|
576
|
+
.on('adminAuditLogs')
|
|
577
|
+
.columns(['createdAt', 'id']),
|
|
578
|
+
},
|
|
523
579
|
]
|
|
524
580
|
|
|
525
581
|
/**
|