tapimo 0.4.0 → 0.4.3
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 +2451 -1533
- 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/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 +1354 -984
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +20 -3
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +196 -5
- 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/balances.d.ts +45 -45
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +453 -453
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +730 -138
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +234 -0
- 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/rpc.d.ts +31 -31
- package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
- package/dist/apps/management/App.d.ts +1156 -615
- 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 +106 -90
- 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 +278 -146
- 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/me.d.ts +13 -13
- 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 +130 -70
- 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 +137 -48
- 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 +14 -28
- 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 +35 -3
- 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/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/handlers/relay.d.ts +13 -10
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +46 -16
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +13 -18
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +78 -58
- 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/FundingRoutes.d.ts +170 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +68 -6
- 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/package.json +2 -2
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +44 -2
- 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 +64 -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/App.ts +24 -3
- package/src/apps/data/FundingRouteProviders.test.ts +204 -0
- package/src/apps/data/FundingRouteProviders.ts +294 -9
- package/src/apps/data/routes/funding.test.ts +90 -23
- package/src/apps/data/routes/funding.ts +315 -0
- package/src/apps/data/routes/rpc.test.ts +33 -0
- 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 +25 -1
- package/src/apps/mcp.ts +43 -3
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- 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/handlers/relay.test-d.ts +8 -0
- package/src/handlers/relay.test.ts +2 -2
- package/src/handlers/relay.ts +62 -22
- package/src/internal/Auth.test.ts +174 -7
- package/src/internal/Auth.ts +90 -62
- 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 +26 -0
- package/src/internal/FundingRoutes.ts +187 -18
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -4
package/src/ApiKey.ts
CHANGED
|
@@ -16,7 +16,6 @@ import * as z from 'zod/mini'
|
|
|
16
16
|
|
|
17
17
|
import * as Id from './internal/Id.js'
|
|
18
18
|
import * as RateLimit from './internal/RateLimit.js'
|
|
19
|
-
import * as Scope from './Scope.js'
|
|
20
19
|
|
|
21
20
|
/** Zod schemas owned by the API-key module. */
|
|
22
21
|
export namespace schema {
|
|
@@ -40,7 +39,7 @@ export namespace schema {
|
|
|
40
39
|
id: z.string().check(z.describe('Stable key id.')),
|
|
41
40
|
name: z.optional(z.string()).check(z.describe('Human-readable key name.')),
|
|
42
41
|
orgId: z.string().check(z.describe('Owning organization id.')),
|
|
43
|
-
projectId: z.optional(z.string()).check(z.describe('
|
|
42
|
+
projectId: z.optional(z.string()).check(z.describe('Attributed project id (`prj_…`).')),
|
|
44
43
|
rateLimits: z
|
|
45
44
|
.optional(z.record(z.string(), RateLimit.schema.Limit))
|
|
46
45
|
.check(
|
|
@@ -49,7 +48,7 @@ export namespace schema {
|
|
|
49
48
|
),
|
|
50
49
|
),
|
|
51
50
|
scopes: z
|
|
52
|
-
.readonly(z.array(z.
|
|
51
|
+
.readonly(z.array(z.string().check(z.minLength(1))))
|
|
53
52
|
.check(z.describe("Granted scopes. '*' grants all scopes.")),
|
|
54
53
|
})
|
|
55
54
|
}
|
package/src/ApiKeys.ts
CHANGED
|
@@ -15,14 +15,14 @@ import * as z from 'zod/mini'
|
|
|
15
15
|
|
|
16
16
|
import * as ApiKey from './ApiKey.js'
|
|
17
17
|
import type * as Store from './internal/Store.js'
|
|
18
|
+
import * as Scope from './Scope.js'
|
|
18
19
|
|
|
19
20
|
/** Zod schemas owned by the API-keys module. */
|
|
20
21
|
export namespace schema {
|
|
21
22
|
/**
|
|
22
23
|
* Fields accepted when minting a key, and the single source of truth for the
|
|
23
|
-
* {@link MintInput} type and
|
|
24
|
-
*
|
|
25
|
-
* lockstep with the resolved shape.
|
|
24
|
+
* {@link MintInput} type and structural validation. {@link mint} validates
|
|
25
|
+
* scope identifiers against its configured catalog.
|
|
26
26
|
*/
|
|
27
27
|
export const MintInput = z.extend(z.omit(ApiKey.schema.ApiKey, { id: true }), {
|
|
28
28
|
createdBy: z
|
|
@@ -78,12 +78,19 @@ export type MintResult = {
|
|
|
78
78
|
*
|
|
79
79
|
* @param state - The KV state store holding key records.
|
|
80
80
|
* @param input - The key to mint.
|
|
81
|
+
* @param options - Scope catalog used for validation.
|
|
81
82
|
* @returns The persisted record and its one-time token.
|
|
82
83
|
*/
|
|
83
|
-
export async function mint(
|
|
84
|
+
export async function mint(
|
|
85
|
+
state: Store.State,
|
|
86
|
+
input: MintInput,
|
|
87
|
+
options: mint.Options = {},
|
|
88
|
+
): Promise<MintResult> {
|
|
84
89
|
const valid = schema.MintInput.safeParse(input)
|
|
85
90
|
if (!valid.success) throw new Error(`invalid API key input: ${z.prettifyError(valid.error)}`)
|
|
86
91
|
const data = valid.data
|
|
92
|
+
if (!validScopes(data.scopes, options.scopeCatalog ?? Scope.catalog))
|
|
93
|
+
throw new Error('invalid API key input: unknown scope')
|
|
87
94
|
|
|
88
95
|
const time = new Date()
|
|
89
96
|
const token = ApiKey.generateToken(data.environment)
|
|
@@ -121,6 +128,14 @@ export async function mint(state: Store.State, input: MintInput): Promise<MintRe
|
|
|
121
128
|
return { record, token }
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
export declare namespace mint {
|
|
132
|
+
/** Options for {@link mint}. */
|
|
133
|
+
type Options = {
|
|
134
|
+
/** Scope catalog accepted while minting the key. */
|
|
135
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
/**
|
|
125
140
|
* Resolves a plaintext API key token to its metadata, or `null`. Expiry is
|
|
126
141
|
* enforced on read — not just via the store's native TTL — so it holds on
|
|
@@ -128,10 +143,23 @@ export async function mint(state: Store.State, input: MintInput): Promise<MintRe
|
|
|
128
143
|
*
|
|
129
144
|
* @param state - The KV state store holding key records.
|
|
130
145
|
* @param token - The presented plaintext token.
|
|
146
|
+
* @param options - Scope catalog used for validation.
|
|
131
147
|
* @returns The resolved key metadata, or `null`.
|
|
132
148
|
*/
|
|
133
|
-
export async function resolve(
|
|
134
|
-
|
|
149
|
+
export async function resolve(
|
|
150
|
+
state: Store.State,
|
|
151
|
+
token: string,
|
|
152
|
+
options: resolve.Options = {},
|
|
153
|
+
): Promise<ApiKey.ApiKey | null> {
|
|
154
|
+
return read(state, ApiKey.keyFor(token), Date.now(), options.scopeCatalog ?? Scope.catalog)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export declare namespace resolve {
|
|
158
|
+
/** Options for {@link resolve}. */
|
|
159
|
+
type Options = {
|
|
160
|
+
/** Scope catalog accepted while resolving the key. */
|
|
161
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
162
|
+
}
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
/**
|
|
@@ -139,12 +167,25 @@ export async function resolve(state: Store.State, token: string): Promise<ApiKey
|
|
|
139
167
|
*
|
|
140
168
|
* @param state - The KV state store holding key records.
|
|
141
169
|
* @param id - The key id (`key_…`).
|
|
170
|
+
* @param options - Scope catalog used for validation.
|
|
142
171
|
* @returns The record, or `null`.
|
|
143
172
|
*/
|
|
144
|
-
export async function get(
|
|
173
|
+
export async function get(
|
|
174
|
+
state: Store.State,
|
|
175
|
+
id: string,
|
|
176
|
+
options: get.Options = {},
|
|
177
|
+
): Promise<Record | null> {
|
|
145
178
|
const recordKey = await state.get(idKeyFor(id))
|
|
146
179
|
if (!recordKey) return null
|
|
147
|
-
return read(state, recordKey, Date.now())
|
|
180
|
+
return read(state, recordKey, Date.now(), options.scopeCatalog ?? Scope.catalog)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export declare namespace get {
|
|
184
|
+
/** Options for {@link get}. */
|
|
185
|
+
type Options = {
|
|
186
|
+
/** Scope catalog accepted while reading the key. */
|
|
187
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
188
|
+
}
|
|
148
189
|
}
|
|
149
190
|
|
|
150
191
|
/**
|
|
@@ -162,7 +203,7 @@ export async function list(
|
|
|
162
203
|
const { keys } = await state.list({ prefix: ApiKey.recordPrefix })
|
|
163
204
|
const records: Record[] = []
|
|
164
205
|
for (const { name } of keys) {
|
|
165
|
-
const record = await read(state, name, Date.now())
|
|
206
|
+
const record = await read(state, name, Date.now(), options.scopeCatalog ?? Scope.catalog)
|
|
166
207
|
if (record && (!options.orgId || record.orgId === options.orgId)) records.push(record)
|
|
167
208
|
}
|
|
168
209
|
// Newest first; ids are random, so sort by creation time.
|
|
@@ -174,6 +215,8 @@ export declare namespace list {
|
|
|
174
215
|
type Options = {
|
|
175
216
|
/** Restrict results to keys owned by this organization. */
|
|
176
217
|
orgId?: string | undefined
|
|
218
|
+
/** Scope catalog accepted while listing keys. */
|
|
219
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
177
220
|
}
|
|
178
221
|
}
|
|
179
222
|
|
|
@@ -196,7 +239,9 @@ export async function listByOrg(
|
|
|
196
239
|
const records: Record[] = []
|
|
197
240
|
for (const { name } of keys) {
|
|
198
241
|
const recordKey = await state.get(name)
|
|
199
|
-
const record = recordKey
|
|
242
|
+
const record = recordKey
|
|
243
|
+
? await read(state, recordKey, Date.now(), options.scopeCatalog ?? Scope.catalog)
|
|
244
|
+
: null
|
|
200
245
|
if (!record) {
|
|
201
246
|
await state.delete(name)
|
|
202
247
|
continue
|
|
@@ -214,6 +259,8 @@ export declare namespace listByOrg {
|
|
|
214
259
|
environment?: 'production' | 'sandbox' | undefined
|
|
215
260
|
/** Restrict results to keys attributed to this project. */
|
|
216
261
|
projectId?: string | undefined
|
|
262
|
+
/** Scope catalog accepted while listing keys. */
|
|
263
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
217
264
|
}
|
|
218
265
|
}
|
|
219
266
|
|
|
@@ -251,16 +298,18 @@ export async function revoke(state: Store.State, id: string): Promise<boolean> {
|
|
|
251
298
|
* @param state - The KV state store holding key records.
|
|
252
299
|
* @param id - The key id (`key_…`).
|
|
253
300
|
* @param input - The attribution to apply.
|
|
301
|
+
* @param options - Scope catalog used for validation.
|
|
254
302
|
* @returns The updated record, or `null` when the key is absent.
|
|
255
303
|
*/
|
|
256
304
|
export async function attribute(
|
|
257
305
|
state: Store.State,
|
|
258
306
|
id: string,
|
|
259
307
|
input: attribute.Input,
|
|
308
|
+
options: attribute.Options = {},
|
|
260
309
|
): Promise<Record | null> {
|
|
261
310
|
const recordKey = await state.get(idKeyFor(id))
|
|
262
311
|
if (!recordKey) return null
|
|
263
|
-
const record = await read(state, recordKey, Date.now())
|
|
312
|
+
const record = await read(state, recordKey, Date.now(), options.scopeCatalog ?? Scope.catalog)
|
|
264
313
|
if (!record) return null
|
|
265
314
|
|
|
266
315
|
const updated: Record = {
|
|
@@ -283,9 +332,15 @@ export declare namespace attribute {
|
|
|
283
332
|
type Input = {
|
|
284
333
|
/** New owning organization id (`org_…`). */
|
|
285
334
|
orgId?: string | undefined
|
|
286
|
-
/** New
|
|
335
|
+
/** New attributed project id (`prj_…`). */
|
|
287
336
|
projectId?: string | undefined
|
|
288
337
|
}
|
|
338
|
+
|
|
339
|
+
/** Options for {@link attribute}. */
|
|
340
|
+
type Options = {
|
|
341
|
+
/** Scope catalog accepted while reading the key. */
|
|
342
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
343
|
+
}
|
|
289
344
|
}
|
|
290
345
|
|
|
291
346
|
/**
|
|
@@ -303,8 +358,8 @@ export async function setBillingActive(
|
|
|
303
358
|
state: Store.State,
|
|
304
359
|
options: setBillingActive.Options,
|
|
305
360
|
): Promise<number> {
|
|
306
|
-
const { active, environment, orgId } = options
|
|
307
|
-
const records = await listByOrg(state, orgId, { environment })
|
|
361
|
+
const { active, environment, orgId, scopeCatalog } = options
|
|
362
|
+
const records = await listByOrg(state, orgId, { environment, scopeCatalog })
|
|
308
363
|
let updated = 0
|
|
309
364
|
for (const record of records) {
|
|
310
365
|
if (record.billingActive === active) continue
|
|
@@ -330,6 +385,8 @@ export declare namespace setBillingActive {
|
|
|
330
385
|
environment: 'production' | 'sandbox'
|
|
331
386
|
/** Owning organization id (`org_…`). */
|
|
332
387
|
orgId: string
|
|
388
|
+
/** Scope catalog accepted while updating keys. */
|
|
389
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
333
390
|
}
|
|
334
391
|
}
|
|
335
392
|
|
|
@@ -339,13 +396,17 @@ export declare namespace setBillingActive {
|
|
|
339
396
|
* themselves are never modified, so existing tokens keep resolving.
|
|
340
397
|
*
|
|
341
398
|
* @param state - The KV state store holding key records.
|
|
399
|
+
* @param options - Scope catalog used for validation.
|
|
342
400
|
* @returns Scanned and indexed record counts.
|
|
343
401
|
*/
|
|
344
|
-
export async function backfill(
|
|
402
|
+
export async function backfill(
|
|
403
|
+
state: Store.State,
|
|
404
|
+
options: backfill.Options = {},
|
|
405
|
+
): Promise<backfill.Result> {
|
|
345
406
|
const { keys } = await state.list({ prefix: ApiKey.recordPrefix })
|
|
346
407
|
let indexed = 0
|
|
347
408
|
for (const { name } of keys) {
|
|
348
|
-
const record = await read(state, name, Date.now())
|
|
409
|
+
const record = await read(state, name, Date.now(), options.scopeCatalog ?? Scope.catalog)
|
|
349
410
|
if (!record) continue
|
|
350
411
|
const ttl =
|
|
351
412
|
record.expiresAt === undefined
|
|
@@ -359,6 +420,12 @@ export async function backfill(state: Store.State): Promise<backfill.Result> {
|
|
|
359
420
|
}
|
|
360
421
|
|
|
361
422
|
export declare namespace backfill {
|
|
423
|
+
/** Options for {@link backfill}. */
|
|
424
|
+
type Options = {
|
|
425
|
+
/** Scope catalog accepted while reading keys. */
|
|
426
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
427
|
+
}
|
|
428
|
+
|
|
362
429
|
/** Result of {@link backfill}. */
|
|
363
430
|
type Result = {
|
|
364
431
|
/** Records whose index entries were written. */
|
|
@@ -372,7 +439,12 @@ export declare namespace backfill {
|
|
|
372
439
|
* Reads and validates a stored record by storage key. Invalid, corrupt, or
|
|
373
440
|
* expired records (`expiresAt <= now`) resolve to `null`.
|
|
374
441
|
*/
|
|
375
|
-
async function read(
|
|
442
|
+
async function read(
|
|
443
|
+
state: Store.State,
|
|
444
|
+
key: string,
|
|
445
|
+
now: number,
|
|
446
|
+
scopeCatalog: Scope.Catalog,
|
|
447
|
+
): Promise<Record | null> {
|
|
376
448
|
const raw = await state.get(key)
|
|
377
449
|
if (!raw) return null
|
|
378
450
|
const value = (() => {
|
|
@@ -386,10 +458,16 @@ async function read(state: Store.State, key: string, now: number): Promise<Recor
|
|
|
386
458
|
const parsed = schema.Record.safeParse(value)
|
|
387
459
|
if (!parsed.success) return null
|
|
388
460
|
const record = parsed.data
|
|
461
|
+
if (!validScopes(record.scopes, scopeCatalog)) return null
|
|
389
462
|
if (record.expiresAt !== undefined && Date.parse(record.expiresAt) <= now) return null
|
|
390
463
|
return record
|
|
391
464
|
}
|
|
392
465
|
|
|
466
|
+
/** Returns whether every granted scope belongs to the configured catalog. */
|
|
467
|
+
function validScopes(scopes: readonly string[], scopeCatalog: Scope.Catalog): boolean {
|
|
468
|
+
return scopes.every((scope) => scope === Scope.wildcard || Scope.includes(scopeCatalog, scope))
|
|
469
|
+
}
|
|
470
|
+
|
|
393
471
|
/** Storage-key prefix for the `id → recordKey` index. */
|
|
394
472
|
const idPrefix = 'apikey_id:'
|
|
395
473
|
|
package/src/App.test.ts
CHANGED
|
@@ -72,6 +72,45 @@ describe('create', () => {
|
|
|
72
72
|
`)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
test('documents supported API key transports', async () => {
|
|
76
|
+
const app = TestApp.create()
|
|
77
|
+
const spec = await (await app.request('/openapi.json')).json()
|
|
78
|
+
|
|
79
|
+
expect({
|
|
80
|
+
queryApiKey: {
|
|
81
|
+
in: spec.components.securitySchemes.queryApiKey.in,
|
|
82
|
+
name: spec.components.securitySchemes.queryApiKey.name,
|
|
83
|
+
type: spec.components.securitySchemes.queryApiKey.type,
|
|
84
|
+
},
|
|
85
|
+
security: spec.security,
|
|
86
|
+
}).toMatchInlineSnapshot(`
|
|
87
|
+
{
|
|
88
|
+
"queryApiKey": {
|
|
89
|
+
"in": "query",
|
|
90
|
+
"name": "key",
|
|
91
|
+
"type": "apiKey",
|
|
92
|
+
},
|
|
93
|
+
"security": [
|
|
94
|
+
{
|
|
95
|
+
"apiKey": [],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"queryApiKey": [],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"bearerAuth": [],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
`)
|
|
106
|
+
expect(spec.paths['/v1/orgs'].get.security).toContainEqual({ queryApiKey: [] })
|
|
107
|
+
expect(spec.paths['/v1/orgs'].get['x-required-scopes']).toMatchInlineSnapshot(`
|
|
108
|
+
[
|
|
109
|
+
"management:read",
|
|
110
|
+
]
|
|
111
|
+
`)
|
|
112
|
+
})
|
|
113
|
+
|
|
75
114
|
test('authenticates a minted store-backed key and rejects after revoke', async () => {
|
|
76
115
|
const store = Store.memory()
|
|
77
116
|
const { record, token } = await ApiKeys.mint(store, {
|
|
@@ -93,7 +132,7 @@ describe('create', () => {
|
|
|
93
132
|
expect(revoked.status).toMatchInlineSnapshot(`401`)
|
|
94
133
|
})
|
|
95
134
|
|
|
96
|
-
test('authenticates via
|
|
135
|
+
test('authenticates via x-api-key header and key query param', async () => {
|
|
97
136
|
const store = Store.memory()
|
|
98
137
|
const { token } = await ApiKeys.mint(store, { orgId: 'org_test', scopes: ['data:read'] })
|
|
99
138
|
const app = TestApp.create({ auth: { cache: false }, kv: { store } })
|
|
@@ -105,10 +144,13 @@ describe('create', () => {
|
|
|
105
144
|
})
|
|
106
145
|
expect(viaHeader.status).toMatchInlineSnapshot(`400`)
|
|
107
146
|
|
|
108
|
-
//
|
|
147
|
+
// The `key` query parameter authenticates the same way.
|
|
109
148
|
const viaQuery = await app.request(`/v1/tokens/not-an-address?key=${encodeURIComponent(token)}`)
|
|
110
149
|
expect(viaQuery.status).toMatchInlineSnapshot(`400`)
|
|
111
150
|
|
|
151
|
+
const rejectedQuery = await app.request('/v1/tokens/not-an-address?key=wrong')
|
|
152
|
+
expect(rejectedQuery.status).toMatchInlineSnapshot(`401`)
|
|
153
|
+
|
|
112
154
|
// An unknown legacy credential is still rejected.
|
|
113
155
|
const rejected = await app.request('/v1/tokens/not-an-address', {
|
|
114
156
|
headers: { 'x-api-key': 'wrong' },
|
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'
|