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
package/src/App.ts
CHANGED
|
@@ -19,6 +19,7 @@ import * as OpenApi from './internal/OpenApi.js'
|
|
|
19
19
|
import * as Store from './internal/Store.js'
|
|
20
20
|
import * as Path from './internal/Path.js'
|
|
21
21
|
import * as Response from './internal/Response.js'
|
|
22
|
+
import * as Scope from './Scope.js'
|
|
22
23
|
import type * as Tidx from './internal/Tidx.js'
|
|
23
24
|
import * as Timing from './internal/Timing.js'
|
|
24
25
|
import * as Viem from './internal/Viem.js'
|
|
@@ -53,6 +54,8 @@ export type Environment = {
|
|
|
53
54
|
dbCached: Db.Source
|
|
54
55
|
/** KV state store backing API-key records, or undefined when key auth is closed. */
|
|
55
56
|
kv: { store: Store.State } | undefined
|
|
57
|
+
/** Issuable API-key scope catalog, including host-defined entries. */
|
|
58
|
+
scopeCatalog: Scope.Catalog
|
|
56
59
|
/** Gets one static API asset. */
|
|
57
60
|
getAsset: GetAsset
|
|
58
61
|
/** Gets a Tempo RPC client for a chain id. */
|
|
@@ -83,6 +86,11 @@ export type Environment = {
|
|
|
83
86
|
*/
|
|
84
87
|
export type App = ReturnType<typeof compose>
|
|
85
88
|
|
|
89
|
+
/** Defines an external route group with metadata collected by {@link create}. */
|
|
90
|
+
export function from<const app extends Hono<any, any>>(app: app, metadata: create.Metadata): app {
|
|
91
|
+
return Metadata.attach(app, metadata)
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
// Type anchor only — never invoked. Expresses the canonical composition so
|
|
87
95
|
// `App` (and thus `Client`) reflect the full route surface; real consumers
|
|
88
96
|
// compose explicitly.
|
|
@@ -138,6 +146,7 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
138
146
|
// Authoritative state lives elsewhere: webhooks + verified tokens in
|
|
139
147
|
// `options.db`, API-key records in `options.kv`.
|
|
140
148
|
const store = options.cache?.store ?? Store.memory()
|
|
149
|
+
const scopeCatalog = Scope.extend(options.scopes)
|
|
141
150
|
|
|
142
151
|
// Build the app in two phases: register middleware imperatively, then chain
|
|
143
152
|
// route mounts so the merged Hono schema flows through the return type. The
|
|
@@ -211,7 +220,12 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
211
220
|
|
|
212
221
|
// Built-in auth: the session surface (mounted last) plus the enforcement
|
|
213
222
|
// middleware (installed below), wired together in `Auth.install`.
|
|
214
|
-
const auth = Auth.install({
|
|
223
|
+
const auth = Auth.install({
|
|
224
|
+
auth: options.auth,
|
|
225
|
+
db: db.source,
|
|
226
|
+
kv: options.kv,
|
|
227
|
+
scopeCatalog,
|
|
228
|
+
})
|
|
215
229
|
|
|
216
230
|
base.use('*', Timing.middleware())
|
|
217
231
|
base.use('*', async (c, next) => {
|
|
@@ -220,6 +234,7 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
220
234
|
c.set('db', db.source)
|
|
221
235
|
c.set('dbCached', db.cached ?? db.source)
|
|
222
236
|
c.set('kv', options.kv)
|
|
237
|
+
c.set('scopeCatalog', scopeCatalog)
|
|
223
238
|
c.set('store', store)
|
|
224
239
|
await next()
|
|
225
240
|
})
|
|
@@ -331,6 +346,13 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
331
346
|
scheme: 'bearer',
|
|
332
347
|
type: 'http',
|
|
333
348
|
},
|
|
349
|
+
queryApiKey: {
|
|
350
|
+
description:
|
|
351
|
+
'Supported query-string alternative for API keys. Prefer `tempo-api-key` because URLs may be logged by clients and infrastructure.',
|
|
352
|
+
in: 'query',
|
|
353
|
+
name: 'key',
|
|
354
|
+
type: 'apiKey',
|
|
355
|
+
},
|
|
334
356
|
...contributedSchemes,
|
|
335
357
|
},
|
|
336
358
|
},
|
|
@@ -339,7 +361,7 @@ export function create<const path extends string = '/'>(options: create.Options<
|
|
|
339
361
|
title: 'Tempo API',
|
|
340
362
|
version: '1.0.0',
|
|
341
363
|
},
|
|
342
|
-
security: [{ apiKey: [] }, { bearerAuth: [] }],
|
|
364
|
+
security: [{ apiKey: [] }, { queryApiKey: [] }, { bearerAuth: [] }],
|
|
343
365
|
servers: [
|
|
344
366
|
...(options.servers ?? [
|
|
345
367
|
{ url: basePath || '/', description: 'Relative to the host serving this document.' },
|
|
@@ -441,6 +463,8 @@ export declare namespace create {
|
|
|
441
463
|
path?: path | undefined
|
|
442
464
|
/** OpenAPI `servers` for the generated document. Defaults to a single relative server (the host serving the doc), so the reference targets the deployment it is served from. Set the canonical public URL(s) here, e.g. `[{ url: 'https://api.tempo.xyz' }]`. */
|
|
443
465
|
servers?: readonly Server[] | undefined
|
|
466
|
+
/** Host-defined API-key scopes appended to the built-in {@link Scope.catalog}. */
|
|
467
|
+
scopes?: readonly Scope.Entry[] | undefined
|
|
444
468
|
}
|
|
445
469
|
|
|
446
470
|
/** KV state-store configuration. */
|
|
@@ -601,12 +625,8 @@ function sortPaths(paths: PathsObject): PathsObject {
|
|
|
601
625
|
}
|
|
602
626
|
|
|
603
627
|
/**
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* Security options compose from the enabled lanes — API keys (header +
|
|
607
|
-
* bearer), sessions (cookie + bearer token), anonymous (`{}`) — and the `402`
|
|
608
|
-
* response is emitted only where the MPP lane is enabled; required API-key
|
|
609
|
-
* scopes are surfaced as the `x-required-scopes` extension.
|
|
628
|
+
* Applies route security, required scopes, and payment responses from resolved access lanes.
|
|
629
|
+
* API keys support header, query, and bearer authentication.
|
|
610
630
|
*/
|
|
611
631
|
function applyAccess(paths: PathsObject, access: Record<string, Auth.OperationAccess>) {
|
|
612
632
|
for (const item of Object.values(paths)) {
|
|
@@ -615,7 +635,7 @@ function applyAccess(paths: PathsObject, access: Record<string, Auth.OperationAc
|
|
|
615
635
|
const lanes = access[op.operationId]
|
|
616
636
|
if (!lanes) continue
|
|
617
637
|
const security = [
|
|
618
|
-
...(lanes.apiKey ? [{ apiKey: [] }, { bearerAuth: [] }] : []),
|
|
638
|
+
...(lanes.apiKey ? [{ apiKey: [] }, { queryApiKey: [] }, { bearerAuth: [] }] : []),
|
|
619
639
|
...(lanes.session ? [{ session: [] }, ...(lanes.apiKey ? [] : [{ bearerAuth: [] }])] : []),
|
|
620
640
|
...(lanes.public ? [{}] : []),
|
|
621
641
|
]
|
package/src/Assets.ts
CHANGED
|
@@ -26,6 +26,9 @@ export type Assets = {
|
|
|
26
26
|
export function cloudflareR2(bucket: cloudflareR2.Bucket): Assets {
|
|
27
27
|
return {
|
|
28
28
|
async get(key) {
|
|
29
|
+
// Exact keys sort first, so expected misses avoid `NoSuchKey` spans.
|
|
30
|
+
const { objects } = await bucket.list({ limit: 1, prefix: key })
|
|
31
|
+
if (objects[0]?.key !== key) return undefined
|
|
29
32
|
const object = await bucket.get(key)
|
|
30
33
|
if (!object) return undefined
|
|
31
34
|
return {
|
|
@@ -48,6 +51,11 @@ export declare namespace cloudflareR2 {
|
|
|
48
51
|
/** HTTP metadata stored with the object. */
|
|
49
52
|
httpMetadata?: { contentType?: string | undefined } | undefined
|
|
50
53
|
} | null>
|
|
54
|
+
/** Lists objects matching a key prefix. */
|
|
55
|
+
list(options: { limit: number; prefix: string }): Promise<{
|
|
56
|
+
/** Matching objects. */
|
|
57
|
+
objects: readonly { key: string }[]
|
|
58
|
+
}>
|
|
51
59
|
/** Writes an object. */
|
|
52
60
|
put(
|
|
53
61
|
key: string,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { testClient } from 'hono/testing'
|
|
3
|
+
import { expectTypeOf } from 'vite-plus/test'
|
|
4
|
+
import { App, Auth } from 'tapimo'
|
|
5
|
+
|
|
6
|
+
describe('Auth', () => {
|
|
7
|
+
test('exports the external route-authoring surface', () => {
|
|
8
|
+
expectTypeOf(Auth.policy).toBeFunction()
|
|
9
|
+
expectTypeOf(Auth.ensureOrg).toBeFunction()
|
|
10
|
+
expectTypeOf(Auth.ensureProject).toBeFunction()
|
|
11
|
+
expectTypeOf<Auth.PolicyOverride>().toMatchTypeOf<{
|
|
12
|
+
apiKey?: { scopes?: readonly string[] | undefined } | boolean | undefined
|
|
13
|
+
}>()
|
|
14
|
+
expectTypeOf<Auth.Principal['type']>().toEqualTypeOf<
|
|
15
|
+
'api_key' | 'public' | 'session' | 'super_admin'
|
|
16
|
+
>()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('preserves route response unions', async () => {
|
|
20
|
+
const app = new Hono<App.Environment>().get(
|
|
21
|
+
'/v1/projects/:projectId',
|
|
22
|
+
Auth.policy({ apiKey: true }),
|
|
23
|
+
Auth.ensureProject(),
|
|
24
|
+
(c) => {
|
|
25
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
26
|
+
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
27
|
+
return c.json({ id: Auth.project(c).id }, 200)
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
const response = await testClient(app).v1.projects[':projectId'].$get({
|
|
31
|
+
param: { projectId: 'prj_test' },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expectTypeOf(response.status).toEqualTypeOf<200 | 400 | 401 | 403 | 404 | 429>()
|
|
35
|
+
})
|
|
36
|
+
})
|
package/src/Auth.ts
CHANGED
|
@@ -1 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
/** Supported authentication helpers for external route groups. */
|
|
2
|
+
export {
|
|
3
|
+
accessError,
|
|
4
|
+
ensureOrg,
|
|
5
|
+
ensureOrgError,
|
|
6
|
+
ensureProject,
|
|
7
|
+
ensureProjectError,
|
|
8
|
+
getPrincipal,
|
|
9
|
+
membership,
|
|
10
|
+
middleware,
|
|
11
|
+
narrowAccess,
|
|
12
|
+
narrowScope,
|
|
13
|
+
org,
|
|
14
|
+
paidAccessError,
|
|
15
|
+
policy,
|
|
16
|
+
project,
|
|
17
|
+
superAdminAccessError,
|
|
18
|
+
type PolicyOverride,
|
|
19
|
+
type Principal,
|
|
20
|
+
} from './internal/Auth.js'
|
package/src/Client.test-d.ts
CHANGED
|
@@ -182,7 +182,7 @@ describe('narrows responses by status', () => {
|
|
|
182
182
|
param: { orgId: 'org_test' },
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
185
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | PublicAccessStatus>()
|
|
186
186
|
|
|
187
187
|
switch (res.status) {
|
|
188
188
|
case 200: {
|
|
@@ -206,7 +206,7 @@ describe('narrows responses by status', () => {
|
|
|
206
206
|
param: { orgId: 'org_test' },
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
209
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 501 | 502 | PublicAccessStatus>()
|
|
210
210
|
|
|
211
211
|
switch (res.status) {
|
|
212
212
|
case 200: {
|
|
@@ -230,7 +230,7 @@ describe('narrows responses by status', () => {
|
|
|
230
230
|
param: { orgId: 'org_test' },
|
|
231
231
|
})
|
|
232
232
|
|
|
233
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
233
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
234
234
|
|
|
235
235
|
switch (res.status) {
|
|
236
236
|
case 200: {
|
|
@@ -257,7 +257,7 @@ describe('narrows responses by status', () => {
|
|
|
257
257
|
param: { orgId: 'org_test' },
|
|
258
258
|
})
|
|
259
259
|
|
|
260
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
260
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 501 | 502 | PublicAccessStatus>()
|
|
261
261
|
|
|
262
262
|
switch (res.status) {
|
|
263
263
|
case 200: {
|
|
@@ -284,7 +284,7 @@ describe('narrows responses by status', () => {
|
|
|
284
284
|
param: { orgId: 'org_test' },
|
|
285
285
|
})
|
|
286
286
|
|
|
287
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
287
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 501 | 502 | PublicAccessStatus>()
|
|
288
288
|
|
|
289
289
|
switch (res.status) {
|
|
290
290
|
case 200: {
|
|
@@ -310,7 +310,7 @@ describe('narrows responses by status', () => {
|
|
|
310
310
|
param: { orgId: 'org_test' },
|
|
311
311
|
})
|
|
312
312
|
|
|
313
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
313
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 501 | 502 | PublicAccessStatus>()
|
|
314
314
|
|
|
315
315
|
switch (res.status) {
|
|
316
316
|
case 200: {
|
|
@@ -338,7 +338,7 @@ describe('narrows responses by status', () => {
|
|
|
338
338
|
':methodId{pm_[A-Za-z0-9_-]+}'
|
|
339
339
|
].$delete({ param: { methodId: 'pm_test', orgId: 'org_test' } })
|
|
340
340
|
|
|
341
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
341
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 501 | 502 | PublicAccessStatus>()
|
|
342
342
|
|
|
343
343
|
switch (res.status) {
|
|
344
344
|
case 200: {
|
|
@@ -364,7 +364,7 @@ describe('narrows responses by status', () => {
|
|
|
364
364
|
param: { orgId: 'org_test' },
|
|
365
365
|
})
|
|
366
366
|
|
|
367
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
367
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
368
368
|
|
|
369
369
|
switch (res.status) {
|
|
370
370
|
case 200: {
|
|
@@ -388,7 +388,7 @@ describe('narrows responses by status', () => {
|
|
|
388
388
|
':projectId{prj_[A-Za-z0-9_-]+}'
|
|
389
389
|
].$get({ param: { orgId: 'org_test', projectId: 'prj_test' } })
|
|
390
390
|
|
|
391
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
391
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | PublicAccessStatus>()
|
|
392
392
|
|
|
393
393
|
switch (res.status) {
|
|
394
394
|
case 200: {
|
|
@@ -414,7 +414,7 @@ describe('narrows responses by status', () => {
|
|
|
414
414
|
':projectId{prj_[A-Za-z0-9_-]+}'
|
|
415
415
|
].$delete({ param: { orgId: 'org_test', projectId: 'prj_test' } })
|
|
416
416
|
|
|
417
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
417
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
418
418
|
|
|
419
419
|
switch (res.status) {
|
|
420
420
|
case 200: {
|
|
@@ -464,7 +464,7 @@ describe('narrows responses by status', () => {
|
|
|
464
464
|
param: { orgId: 'org_test' },
|
|
465
465
|
})
|
|
466
466
|
|
|
467
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
467
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
468
468
|
|
|
469
469
|
switch (res.status) {
|
|
470
470
|
case 200: {
|
|
@@ -487,7 +487,7 @@ describe('narrows responses by status', () => {
|
|
|
487
487
|
param: { keyId: 'key_test', orgId: 'org_test', projectId: 'prj_test' },
|
|
488
488
|
})
|
|
489
489
|
|
|
490
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
490
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
491
491
|
|
|
492
492
|
switch (res.status) {
|
|
493
493
|
case 404: {
|
|
@@ -511,7 +511,7 @@ describe('narrows responses by status', () => {
|
|
|
511
511
|
query: { projectId: 'prj_test' },
|
|
512
512
|
})
|
|
513
513
|
|
|
514
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
514
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 501 | 502 | PublicAccessStatus>()
|
|
515
515
|
|
|
516
516
|
switch (res.status) {
|
|
517
517
|
case 200: {
|
|
@@ -545,7 +545,7 @@ describe('narrows responses by status', () => {
|
|
|
545
545
|
param: { orgId: 'org_test' },
|
|
546
546
|
})
|
|
547
547
|
|
|
548
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
548
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
549
549
|
|
|
550
550
|
switch (res.status) {
|
|
551
551
|
case 200: {
|
|
@@ -563,7 +563,7 @@ describe('narrows responses by status', () => {
|
|
|
563
563
|
':userId{usr_[A-Za-z0-9_-]+}'
|
|
564
564
|
].$patch({ json: { role: 'admin' }, param: { orgId: 'org_test', userId: 'usr_test' } })
|
|
565
565
|
|
|
566
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
566
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 409 | 502 | PublicAccessStatus>()
|
|
567
567
|
|
|
568
568
|
switch (res.status) {
|
|
569
569
|
case 409: {
|
|
@@ -627,7 +627,7 @@ describe('narrows responses by status', () => {
|
|
|
627
627
|
const client = Client.create()
|
|
628
628
|
const res = await client.v1.orgs.$get()
|
|
629
629
|
|
|
630
|
-
expectTypeOf(res.status).toEqualTypeOf<200 |
|
|
630
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 502 | PublicAccessStatus>()
|
|
631
631
|
|
|
632
632
|
switch (res.status) {
|
|
633
633
|
case 200: {
|
|
@@ -640,7 +640,7 @@ describe('narrows responses by status', () => {
|
|
|
640
640
|
}
|
|
641
641
|
case 403: {
|
|
642
642
|
const json = await res.json()
|
|
643
|
-
expectTypeOf(json.error.code).toEqualTypeOf<'
|
|
643
|
+
expectTypeOf(json.error.code).toEqualTypeOf<'api_key_forbidden'>()
|
|
644
644
|
expectTypeOf(json.error.message).toEqualTypeOf<string>()
|
|
645
645
|
void json
|
|
646
646
|
return
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as Scope from './Scope.js'
|
|
2
|
+
|
|
3
|
+
describe('extend', () => {
|
|
4
|
+
test('appends host scopes to the built-in catalog', () => {
|
|
5
|
+
expect(
|
|
6
|
+
Scope.extend([
|
|
7
|
+
{
|
|
8
|
+
description: 'Read project accounts.',
|
|
9
|
+
scope: 'accounts:read',
|
|
10
|
+
selfServe: true,
|
|
11
|
+
},
|
|
12
|
+
]),
|
|
13
|
+
).toEqual([
|
|
14
|
+
...Scope.catalog,
|
|
15
|
+
{
|
|
16
|
+
description: 'Read project accounts.',
|
|
17
|
+
scope: 'accounts:read',
|
|
18
|
+
selfServe: true,
|
|
19
|
+
},
|
|
20
|
+
])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('rejects duplicate scope identifiers', () => {
|
|
24
|
+
expect(() =>
|
|
25
|
+
Scope.extend([
|
|
26
|
+
{
|
|
27
|
+
description: 'Duplicate data scope.',
|
|
28
|
+
scope: 'data:read',
|
|
29
|
+
selfServe: true,
|
|
30
|
+
},
|
|
31
|
+
]),
|
|
32
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: Duplicate scope \`data:read\`.]`)
|
|
33
|
+
})
|
|
34
|
+
})
|
package/src/Scope.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* admin tooling validate scopes against the same list the server enforces.
|
|
2
|
+
* The built-in API-key scope vocabulary. Hosts append entries through
|
|
3
|
+
* `App.create({ scopes })`; the merged catalog drives discovery, minting, and
|
|
4
|
+
* API-key validation.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -13,20 +12,18 @@
|
|
|
13
12
|
export const wildcard = '*'
|
|
14
13
|
|
|
15
14
|
/** Catalog entry describing an issuable API-key scope. */
|
|
16
|
-
type Entry = {
|
|
15
|
+
export type Entry = {
|
|
17
16
|
/** Human-readable explanation of what the scope grants. */
|
|
18
17
|
description: string
|
|
19
18
|
/** Scope identifier used in a key's `scopes` and in route policies. */
|
|
20
19
|
scope: string
|
|
21
|
-
/** Whether
|
|
20
|
+
/** Whether an eligible session may self-mint this scope (else super-admin-only). */
|
|
22
21
|
selfServe: boolean
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
* The issuable API-key
|
|
27
|
-
*
|
|
28
|
-
* set; {@link wildcard} (`'*'`) is an implicit grant-all and is intentionally
|
|
29
|
-
* not listed as an issuable scope.
|
|
25
|
+
* The built-in issuable API-key scopes. {@link wildcard} (`'*'`) is an implicit
|
|
26
|
+
* grant-all and is intentionally not listed as an issuable scope.
|
|
30
27
|
*/
|
|
31
28
|
export const catalog = [
|
|
32
29
|
{
|
|
@@ -39,6 +36,17 @@ export const catalog = [
|
|
|
39
36
|
scope: 'indexer:query',
|
|
40
37
|
selfServe: true,
|
|
41
38
|
},
|
|
39
|
+
{
|
|
40
|
+
description: 'Read organization resources across projects and environments.',
|
|
41
|
+
scope: 'management:read',
|
|
42
|
+
selfServe: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
description:
|
|
46
|
+
'Create, update, and delete organization resources across projects and environments.',
|
|
47
|
+
scope: 'management:write',
|
|
48
|
+
selfServe: true,
|
|
49
|
+
},
|
|
42
50
|
{
|
|
43
51
|
description: 'Fill and submit transactions through the relay.',
|
|
44
52
|
scope: 'rpc-relay:read',
|
|
@@ -61,16 +69,44 @@ export const catalog = [
|
|
|
61
69
|
},
|
|
62
70
|
] as const satisfies readonly Entry[]
|
|
63
71
|
|
|
64
|
-
/** Issuable API-key scope identifier
|
|
65
|
-
export type Id = (typeof catalog)[number]['scope']
|
|
72
|
+
/** Issuable API-key scope identifier, including host-defined scopes. */
|
|
73
|
+
export type Id = (typeof catalog)[number]['scope'] | (string & {})
|
|
74
|
+
|
|
75
|
+
/** Scope catalog used by API-key issuance and discovery. */
|
|
76
|
+
export type Catalog = readonly Entry[]
|
|
66
77
|
|
|
67
78
|
/**
|
|
68
|
-
* Scopes
|
|
69
|
-
* the catalog's `selfServe` flag.
|
|
70
|
-
*
|
|
71
|
-
* paid APIs for free. Webhooks stay super-admin-only, as does {@link wildcard}
|
|
72
|
-
* (intentionally absent from the catalog).
|
|
79
|
+
* Scopes an eligible session may self-mint through the developer platform,
|
|
80
|
+
* derived from the catalog's `selfServe` flag. Route-specific ownership rules
|
|
81
|
+
* may narrow issuance. Webhooks and {@link wildcard} stay super-admin-only.
|
|
73
82
|
*/
|
|
74
83
|
export const selfServe: readonly Id[] = catalog
|
|
75
84
|
.filter((entry) => entry.selfServe)
|
|
76
85
|
.map((entry) => entry.scope)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extends the built-in catalog with host-defined scopes.
|
|
89
|
+
*
|
|
90
|
+
* @param entries - Host-defined scope entries.
|
|
91
|
+
* @returns The merged scope catalog.
|
|
92
|
+
*/
|
|
93
|
+
export function extend(entries: readonly Entry[] = []): Catalog {
|
|
94
|
+
const identifiers = new Set<string>()
|
|
95
|
+
const result = [...catalog, ...entries]
|
|
96
|
+
for (const entry of result) {
|
|
97
|
+
if (!entry.scope.trim()) throw new Error('Scope identifier must not be empty.')
|
|
98
|
+
if (identifiers.has(entry.scope)) throw new Error(`Duplicate scope \`${entry.scope}\`.`)
|
|
99
|
+
identifiers.add(entry.scope)
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Returns whether a catalog contains a scope identifier. */
|
|
105
|
+
export function includes(catalog: Catalog, scope: string): scope is Id {
|
|
106
|
+
return catalog.some((entry) => entry.scope === scope)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Returns the self-issuable identifiers in a scope catalog. */
|
|
110
|
+
export function selfServeFrom(catalog: Catalog): readonly Id[] {
|
|
111
|
+
return catalog.filter((entry) => entry.selfServe).map((entry) => entry.scope)
|
|
112
|
+
}
|
package/src/admin/App.test.ts
CHANGED
|
@@ -18,6 +18,10 @@ describe('create', () => {
|
|
|
18
18
|
method: 'POST',
|
|
19
19
|
}),
|
|
20
20
|
new Request('http://admin/api-keys/key_x', { method: 'DELETE' }),
|
|
21
|
+
new Request('http://admin/organizations'),
|
|
22
|
+
new Request('http://admin/organizations/org_x'),
|
|
23
|
+
new Request('http://admin/organizations/org_x/members'),
|
|
24
|
+
new Request('http://admin/organizations/org_x/projects'),
|
|
21
25
|
new Request('http://admin/verified-tokens?chainId=42431', {
|
|
22
26
|
body: JSON.stringify({ tokens: [] }),
|
|
23
27
|
headers: { 'content-type': 'application/json' },
|
|
@@ -67,13 +71,37 @@ describe('create', () => {
|
|
|
67
71
|
describe('cache policy', () => {
|
|
68
72
|
test('every response is no-store (admin data is per-principal and mutating)', async () => {
|
|
69
73
|
const { app } = TestAdmin.setup()
|
|
70
|
-
for (const path of ['/scopes', '/api-keys', '/config.json']) {
|
|
74
|
+
for (const path of ['/scopes', '/api-keys', '/organizations', '/config.json']) {
|
|
71
75
|
const response = await app.request(`http://admin${path}`)
|
|
72
76
|
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
73
77
|
}
|
|
74
78
|
})
|
|
75
79
|
})
|
|
76
80
|
|
|
81
|
+
describe('audit logging', () => {
|
|
82
|
+
test('records authenticated data access with actor, request id, route, and status', async () => {
|
|
83
|
+
const { app, db } = TestAdmin.setup({
|
|
84
|
+
identify: () => ({ email: 'ops@tempo.xyz' }),
|
|
85
|
+
})
|
|
86
|
+
const response = await app.request('http://admin/organizations?limit=5')
|
|
87
|
+
const records = await db.kysely
|
|
88
|
+
.selectFrom('admin_audit_logs')
|
|
89
|
+
.selectAll()
|
|
90
|
+
.orderBy('createdAt', 'desc')
|
|
91
|
+
.execute()
|
|
92
|
+
|
|
93
|
+
expect(response.status).toBe(200)
|
|
94
|
+
expect(records[0]).toMatchObject({
|
|
95
|
+
actor: 'ops@tempo.xyz',
|
|
96
|
+
method: 'GET',
|
|
97
|
+
path: '/organizations',
|
|
98
|
+
query: 'limit=5',
|
|
99
|
+
status: 200,
|
|
100
|
+
})
|
|
101
|
+
expect(records[0]?.requestId).toBeTruthy()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
77
105
|
describe('GET /config.json', () => {
|
|
78
106
|
test('reports the signed-in identity and sign-in/out URLs', async () => {
|
|
79
107
|
const { app } = TestAdmin.setup({
|
|
@@ -120,8 +148,10 @@ describe('create', () => {
|
|
|
120
148
|
expect(response.headers.get('content-type')).toContain('text/html')
|
|
121
149
|
const html = await response.text()
|
|
122
150
|
expect(html).toContain('<div id="root">')
|
|
151
|
+
expect(html).not.toContain('data-theme="dark"')
|
|
152
|
+
expect(html).toContain("localStorage.getItem('tempo-api-admin.theme')")
|
|
123
153
|
// Standalone mount → `<base href="/">` so relative asset/API URLs resolve.
|
|
124
|
-
expect(html).toContain('<base href="/"/>')
|
|
154
|
+
expect(html).toContain('<base data-tempo-base="true" href="/"/>')
|
|
125
155
|
})
|
|
126
156
|
|
|
127
157
|
test('inlines the gated auth config into the shell (no bootstrap round trip)', async () => {
|
|
@@ -173,7 +203,7 @@ describe('create', () => {
|
|
|
173
203
|
expect(response.status).toBe(200)
|
|
174
204
|
// The base href is the mount root, not the deep link, so assets/API
|
|
175
205
|
// resolve under `/admin/` from any in-app route.
|
|
176
|
-
expect(await response.text()).toContain('<base href="/admin/"/>')
|
|
206
|
+
expect(await response.text()).toContain('<base data-tempo-base="true" href="/admin/"/>')
|
|
177
207
|
})
|
|
178
208
|
|
|
179
209
|
test('ui: false → no shell; `/` returns the JSON 404', async () => {
|