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.
Files changed (255) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +2451 -1533
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/ui.generated.d.ts +1 -1
  41. package/dist/admin/ui.generated.d.ts.map +1 -1
  42. package/dist/admin/ui.generated.js +53 -13
  43. package/dist/admin/ui.generated.js.map +1 -1
  44. package/dist/analytics/Analytics.d.ts +10 -0
  45. package/dist/analytics/Analytics.d.ts.map +1 -1
  46. package/dist/analytics/Analytics.js +8 -0
  47. package/dist/analytics/Analytics.js.map +1 -1
  48. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  49. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  50. package/dist/analytics/tables/requestEvents.js +14 -0
  51. package/dist/analytics/tables/requestEvents.js.map +1 -1
  52. package/dist/apps/data/App.d.ts +1354 -984
  53. package/dist/apps/data/App.d.ts.map +1 -1
  54. package/dist/apps/data/App.js +20 -3
  55. package/dist/apps/data/App.js.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
  57. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  58. package/dist/apps/data/FundingRouteProviders.js +196 -5
  59. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  60. package/dist/apps/data/routes/activities.d.ts +38 -38
  61. package/dist/apps/data/routes/addresses.d.ts +24 -24
  62. package/dist/apps/data/routes/balances.d.ts +45 -45
  63. package/dist/apps/data/routes/blocks.d.ts +39 -39
  64. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  65. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  66. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  67. package/dist/apps/data/routes/funding.d.ts +730 -138
  68. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  69. package/dist/apps/data/routes/funding.js +234 -0
  70. package/dist/apps/data/routes/funding.js.map +1 -1
  71. package/dist/apps/data/routes/indexer.d.ts +5 -5
  72. package/dist/apps/data/routes/rpc.d.ts +31 -31
  73. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  74. package/dist/apps/management/App.d.ts +1156 -615
  75. package/dist/apps/management/App.d.ts.map +1 -1
  76. package/dist/apps/management/App.js +2 -2
  77. package/dist/apps/management/Billing.d.ts +3 -0
  78. package/dist/apps/management/Billing.d.ts.map +1 -1
  79. package/dist/apps/management/Billing.js +7 -2
  80. package/dist/apps/management/Billing.js.map +1 -1
  81. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  82. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.js +41 -30
  84. package/dist/apps/management/routes/api-keys.js.map +1 -1
  85. package/dist/apps/management/routes/billing.d.ts +278 -146
  86. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  87. package/dist/apps/management/routes/billing.js +14 -14
  88. package/dist/apps/management/routes/billing.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +93 -20
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +12 -15
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +13 -13
  94. package/dist/apps/management/routes/members.d.ts +89 -18
  95. package/dist/apps/management/routes/members.d.ts.map +1 -1
  96. package/dist/apps/management/routes/members.js +10 -10
  97. package/dist/apps/management/routes/members.js.map +1 -1
  98. package/dist/apps/management/routes/orgs.d.ts +130 -70
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +19 -19
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/projects.d.ts +137 -48
  103. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  104. package/dist/apps/management/routes/projects.js +12 -13
  105. package/dist/apps/management/routes/projects.js.map +1 -1
  106. package/dist/apps/management/routes/scopes.d.ts +14 -28
  107. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  108. package/dist/apps/management/routes/scopes.js +6 -7
  109. package/dist/apps/management/routes/scopes.js.map +1 -1
  110. package/dist/apps/management/routes/usage.d.ts +63 -19
  111. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  112. package/dist/apps/management/routes/usage.js +9 -10
  113. package/dist/apps/management/routes/usage.js.map +1 -1
  114. package/dist/apps/mcp.d.ts.map +1 -1
  115. package/dist/apps/mcp.js +35 -3
  116. package/dist/apps/mcp.js.map +1 -1
  117. package/dist/apps/relay/App.d.ts +26 -4
  118. package/dist/apps/relay/App.d.ts.map +1 -1
  119. package/dist/apps/relay/App.js +17 -14
  120. package/dist/apps/relay/App.js.map +1 -1
  121. package/dist/db/Db.d.ts +5 -3
  122. package/dist/db/Db.d.ts.map +1 -1
  123. package/dist/db/Db.js +52 -0
  124. package/dist/db/Db.js.map +1 -1
  125. package/dist/db/Schema.d.ts +13 -0
  126. package/dist/db/Schema.d.ts.map +1 -1
  127. package/dist/db/Schema.js +16 -1
  128. package/dist/db/Schema.js.map +1 -1
  129. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  130. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  131. package/dist/db/tables/adminAuditLogs.js +19 -0
  132. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  133. package/dist/db/tables/invitations.d.ts +1 -1
  134. package/dist/db/tables/invitations.d.ts.map +1 -1
  135. package/dist/db/tables/memberships.d.ts +32 -4
  136. package/dist/db/tables/memberships.d.ts.map +1 -1
  137. package/dist/db/tables/memberships.js +31 -0
  138. package/dist/db/tables/memberships.js.map +1 -1
  139. package/dist/db/tables/organizations.d.ts +43 -1
  140. package/dist/db/tables/organizations.d.ts.map +1 -1
  141. package/dist/db/tables/organizations.js +103 -0
  142. package/dist/db/tables/organizations.js.map +1 -1
  143. package/dist/db/tables/projects.d.ts +25 -0
  144. package/dist/db/tables/projects.d.ts.map +1 -1
  145. package/dist/db/tables/projects.js +22 -0
  146. package/dist/db/tables/projects.js.map +1 -1
  147. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  148. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  149. package/dist/db/tables/webhookDeliveries.js +37 -0
  150. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  151. package/dist/handlers/relay.d.ts +13 -10
  152. package/dist/handlers/relay.d.ts.map +1 -1
  153. package/dist/handlers/relay.js +46 -16
  154. package/dist/handlers/relay.js.map +1 -1
  155. package/dist/internal/Auth.d.ts +13 -18
  156. package/dist/internal/Auth.d.ts.map +1 -1
  157. package/dist/internal/Auth.js +78 -58
  158. package/dist/internal/Auth.js.map +1 -1
  159. package/dist/internal/Cursor.d.ts +1 -1
  160. package/dist/internal/Cursor.d.ts.map +1 -1
  161. package/dist/internal/Cursor.js +4 -0
  162. package/dist/internal/Cursor.js.map +1 -1
  163. package/dist/internal/EdgeCache.d.ts.map +1 -1
  164. package/dist/internal/EdgeCache.js +2 -0
  165. package/dist/internal/EdgeCache.js.map +1 -1
  166. package/dist/internal/FundingRoutes.d.ts +170 -0
  167. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  168. package/dist/internal/FundingRoutes.js +68 -6
  169. package/dist/internal/FundingRoutes.js.map +1 -1
  170. package/dist/internal/Log.d.ts +16 -1
  171. package/dist/internal/Log.d.ts.map +1 -1
  172. package/dist/internal/Log.js +5 -3
  173. package/dist/internal/Log.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/ApiKey.ts +2 -3
  176. package/src/ApiKeys.ts +95 -17
  177. package/src/App.test.ts +44 -2
  178. package/src/App.ts +29 -9
  179. package/src/Assets.ts +8 -0
  180. package/src/Auth.test-d.ts +36 -0
  181. package/src/Auth.ts +20 -1
  182. package/src/Client.test-d.ts +64 -17
  183. package/src/Scope.test.ts +34 -0
  184. package/src/Scope.ts +53 -17
  185. package/src/admin/App.test.ts +33 -3
  186. package/src/admin/App.ts +49 -14
  187. package/src/admin/apps/api-keys.ts +10 -3
  188. package/src/admin/apps/organizations.test.ts +455 -0
  189. package/src/admin/apps/organizations.ts +953 -0
  190. package/src/admin/apps/scopes.test.ts +10 -0
  191. package/src/admin/ui/plugin.ts +1 -1
  192. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  193. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  194. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  195. package/src/admin/ui/src/lib/config.ts +2 -3
  196. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  197. package/src/admin/ui/src/lib/organizations.ts +262 -0
  198. package/src/admin/ui/src/lib/query-client.ts +3 -1
  199. package/src/admin/ui/src/lib/theme.ts +57 -0
  200. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  201. package/src/admin/ui/src/routes/__root.tsx +18 -3
  202. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  203. package/src/admin/ui/src/routes/index.tsx +1 -1
  204. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  206. package/src/admin/ui/src/start.ts +6 -0
  207. package/src/admin/ui.generated.ts +63 -13
  208. package/src/analytics/Analytics.ts +25 -0
  209. package/src/analytics/tables/requestEvents.ts +33 -0
  210. package/src/apps/composition.test.ts +82 -2
  211. package/src/apps/data/App.ts +24 -3
  212. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  213. package/src/apps/data/FundingRouteProviders.ts +294 -9
  214. package/src/apps/data/routes/funding.test.ts +90 -23
  215. package/src/apps/data/routes/funding.ts +315 -0
  216. package/src/apps/data/routes/rpc.test.ts +33 -0
  217. package/src/apps/management/App.ts +2 -2
  218. package/src/apps/management/Billing.ts +10 -2
  219. package/src/apps/management/routes/api-keys.test.ts +137 -1
  220. package/src/apps/management/routes/api-keys.ts +63 -38
  221. package/src/apps/management/routes/billing.ts +14 -14
  222. package/src/apps/management/routes/invitations.test.ts +102 -1
  223. package/src/apps/management/routes/invitations.ts +13 -15
  224. package/src/apps/management/routes/members.ts +10 -10
  225. package/src/apps/management/routes/orgs.test.ts +78 -16
  226. package/src/apps/management/routes/orgs.ts +21 -20
  227. package/src/apps/management/routes/projects.ts +12 -13
  228. package/src/apps/management/routes/scopes.test.ts +16 -0
  229. package/src/apps/management/routes/scopes.ts +6 -7
  230. package/src/apps/management/routes/usage.ts +9 -14
  231. package/src/apps/mcp.test.ts +25 -1
  232. package/src/apps/mcp.ts +43 -3
  233. package/src/apps/relay/App.test.ts +23 -18
  234. package/src/apps/relay/App.ts +35 -14
  235. package/src/db/Db.ts +59 -3
  236. package/src/db/Schema.ts +20 -1
  237. package/src/db/tables/adminAuditLogs.ts +47 -0
  238. package/src/db/tables/invitations.ts +1 -1
  239. package/src/db/tables/memberships.ts +59 -3
  240. package/src/db/tables/organizations.ts +148 -1
  241. package/src/db/tables/projects.ts +47 -0
  242. package/src/db/tables/webhookDeliveries.ts +83 -0
  243. package/src/handlers/relay.test-d.ts +8 -0
  244. package/src/handlers/relay.test.ts +2 -2
  245. package/src/handlers/relay.ts +62 -22
  246. package/src/internal/Auth.test.ts +174 -7
  247. package/src/internal/Auth.ts +90 -62
  248. package/src/internal/Cursor.test.ts +10 -0
  249. package/src/internal/Cursor.ts +5 -1
  250. package/src/internal/EdgeCache.test.ts +11 -0
  251. package/src/internal/EdgeCache.ts +2 -0
  252. package/src/internal/FundingRoutes.test.ts +26 -0
  253. package/src/internal/FundingRoutes.ts +187 -18
  254. package/src/internal/Log.test.ts +41 -3
  255. 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('Owning project id (`prj_…`).')),
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.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard])))
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 `mint` validation. Derived from the resolved
24
- * `ApiKey.schema` (minus the generated `id`) so the scope vocabulary stays in
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(state: Store.State, input: MintInput): Promise<MintResult> {
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(state: Store.State, token: string): Promise<ApiKey.ApiKey | null> {
134
- return read(state, ApiKey.keyFor(token), Date.now())
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(state: Store.State, id: string): Promise<Record | null> {
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 ? await read(state, recordKey, Date.now()) : null
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 owning project id (`prj_…`). */
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(state: Store.State): Promise<backfill.Result> {
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(state: Store.State, key: string, now: number): Promise<Record | null> {
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 the deprecated x-api-key header and key query param', async () => {
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
- // Deprecated `key` query parameter authenticates the same way.
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({ auth: options.auth, db: db.source, kv: options.kv })
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
- * Sets per-operation `security` and the `402` payment-challenge response from
605
- * each route's resolved access lanes (see {@link Auth.describeAccess}).
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
- export { middleware } from './internal/Auth.js'
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'