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.
Files changed (263) 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 +1990 -1442
  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/apps/verified-tokens.d.ts +57 -57
  41. package/dist/admin/ui.generated.d.ts +1 -1
  42. package/dist/admin/ui.generated.d.ts.map +1 -1
  43. package/dist/admin/ui.generated.js +53 -13
  44. package/dist/admin/ui.generated.js.map +1 -1
  45. package/dist/analytics/Analytics.d.ts +10 -0
  46. package/dist/analytics/Analytics.d.ts.map +1 -1
  47. package/dist/analytics/Analytics.js +8 -0
  48. package/dist/analytics/Analytics.js.map +1 -1
  49. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  50. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  51. package/dist/analytics/tables/requestEvents.js +14 -0
  52. package/dist/analytics/tables/requestEvents.js.map +1 -1
  53. package/dist/apps/data/App.d.ts +1304 -1304
  54. package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
  55. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.js +10 -120
  57. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  58. package/dist/apps/data/routes/activities.d.ts +38 -38
  59. package/dist/apps/data/routes/addresses.d.ts +24 -24
  60. package/dist/apps/data/routes/blocks.d.ts +39 -39
  61. package/dist/apps/data/routes/fee-amm.d.ts +20 -20
  62. package/dist/apps/data/routes/funding.js +7 -7
  63. package/dist/apps/data/routes/funding.js.map +1 -1
  64. package/dist/apps/data/routes/indexer.d.ts +5 -5
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  67. package/dist/apps/management/App.d.ts +808 -267
  68. package/dist/apps/management/App.d.ts.map +1 -1
  69. package/dist/apps/management/App.js +2 -2
  70. package/dist/apps/management/Billing.d.ts +3 -0
  71. package/dist/apps/management/Billing.d.ts.map +1 -1
  72. package/dist/apps/management/Billing.js +7 -2
  73. package/dist/apps/management/Billing.js.map +1 -1
  74. package/dist/apps/management/routes/api-keys.d.ts +116 -100
  75. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  76. package/dist/apps/management/routes/api-keys.js +41 -30
  77. package/dist/apps/management/routes/api-keys.js.map +1 -1
  78. package/dist/apps/management/routes/billing.d.ts +182 -50
  79. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  80. package/dist/apps/management/routes/billing.js +14 -14
  81. package/dist/apps/management/routes/billing.js.map +1 -1
  82. package/dist/apps/management/routes/invitations.d.ts +93 -20
  83. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  84. package/dist/apps/management/routes/invitations.js +12 -15
  85. package/dist/apps/management/routes/invitations.js.map +1 -1
  86. package/dist/apps/management/routes/members.d.ts +89 -18
  87. package/dist/apps/management/routes/members.d.ts.map +1 -1
  88. package/dist/apps/management/routes/members.js +10 -10
  89. package/dist/apps/management/routes/members.js.map +1 -1
  90. package/dist/apps/management/routes/orgs.d.ts +100 -40
  91. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  92. package/dist/apps/management/routes/orgs.js +19 -19
  93. package/dist/apps/management/routes/orgs.js.map +1 -1
  94. package/dist/apps/management/routes/projects.d.ts +122 -33
  95. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  96. package/dist/apps/management/routes/projects.js +12 -13
  97. package/dist/apps/management/routes/projects.js.map +1 -1
  98. package/dist/apps/management/routes/scopes.d.ts +4 -18
  99. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  100. package/dist/apps/management/routes/scopes.js +6 -7
  101. package/dist/apps/management/routes/scopes.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +63 -19
  103. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  104. package/dist/apps/management/routes/usage.js +9 -10
  105. package/dist/apps/management/routes/usage.js.map +1 -1
  106. package/dist/apps/mcp.d.ts.map +1 -1
  107. package/dist/apps/mcp.js +38 -4
  108. package/dist/apps/mcp.js.map +1 -1
  109. package/dist/apps/relay/App.d.ts +26 -4
  110. package/dist/apps/relay/App.d.ts.map +1 -1
  111. package/dist/apps/relay/App.js +17 -14
  112. package/dist/apps/relay/App.js.map +1 -1
  113. package/dist/cloudflare.d.ts +2 -0
  114. package/dist/cloudflare.d.ts.map +1 -1
  115. package/dist/cloudflare.js +4 -0
  116. package/dist/cloudflare.js.map +1 -1
  117. package/dist/db/Db.d.ts +5 -3
  118. package/dist/db/Db.d.ts.map +1 -1
  119. package/dist/db/Db.js +52 -0
  120. package/dist/db/Db.js.map +1 -1
  121. package/dist/db/Schema.d.ts +13 -0
  122. package/dist/db/Schema.d.ts.map +1 -1
  123. package/dist/db/Schema.js +16 -1
  124. package/dist/db/Schema.js.map +1 -1
  125. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  126. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  127. package/dist/db/tables/adminAuditLogs.js +19 -0
  128. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  129. package/dist/db/tables/invitations.d.ts +1 -1
  130. package/dist/db/tables/invitations.d.ts.map +1 -1
  131. package/dist/db/tables/memberships.d.ts +32 -4
  132. package/dist/db/tables/memberships.d.ts.map +1 -1
  133. package/dist/db/tables/memberships.js +31 -0
  134. package/dist/db/tables/memberships.js.map +1 -1
  135. package/dist/db/tables/organizations.d.ts +43 -1
  136. package/dist/db/tables/organizations.d.ts.map +1 -1
  137. package/dist/db/tables/organizations.js +103 -0
  138. package/dist/db/tables/organizations.js.map +1 -1
  139. package/dist/db/tables/projects.d.ts +25 -0
  140. package/dist/db/tables/projects.d.ts.map +1 -1
  141. package/dist/db/tables/projects.js +22 -0
  142. package/dist/db/tables/projects.js.map +1 -1
  143. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  144. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  145. package/dist/db/tables/webhookDeliveries.js +37 -0
  146. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +46 -40
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +118 -69
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/Cursor.d.ts +1 -1
  152. package/dist/internal/Cursor.d.ts.map +1 -1
  153. package/dist/internal/Cursor.js +4 -0
  154. package/dist/internal/Cursor.js.map +1 -1
  155. package/dist/internal/EdgeCache.d.ts.map +1 -1
  156. package/dist/internal/EdgeCache.js +2 -0
  157. package/dist/internal/EdgeCache.js.map +1 -1
  158. package/dist/internal/Errors.d.ts +6 -0
  159. package/dist/internal/Errors.d.ts.map +1 -0
  160. package/dist/internal/Errors.js +16 -0
  161. package/dist/internal/Errors.js.map +1 -0
  162. package/dist/internal/FundingRoutes.d.ts +15 -43
  163. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  164. package/dist/internal/FundingRoutes.js +8 -17
  165. package/dist/internal/FundingRoutes.js.map +1 -1
  166. package/dist/internal/Log.d.ts +16 -1
  167. package/dist/internal/Log.d.ts.map +1 -1
  168. package/dist/internal/Log.js +5 -3
  169. package/dist/internal/Log.js.map +1 -1
  170. package/dist/internal/Store.d.ts +61 -16
  171. package/dist/internal/Store.d.ts.map +1 -1
  172. package/dist/internal/Store.js +72 -17
  173. package/dist/internal/Store.js.map +1 -1
  174. package/dist/internal/Viem.d.ts +1 -1
  175. package/dist/internal/index.d.ts +27 -0
  176. package/dist/internal/index.d.ts.map +1 -0
  177. package/dist/internal/index.js +27 -0
  178. package/dist/internal/index.js.map +1 -0
  179. package/package.json +8 -3
  180. package/src/ApiKey.ts +2 -3
  181. package/src/ApiKeys.ts +95 -17
  182. package/src/App.test.ts +104 -6
  183. package/src/App.ts +29 -9
  184. package/src/Assets.ts +8 -0
  185. package/src/Auth.test-d.ts +36 -0
  186. package/src/Auth.ts +20 -1
  187. package/src/Client.test-d.ts +17 -17
  188. package/src/Scope.test.ts +34 -0
  189. package/src/Scope.ts +53 -17
  190. package/src/admin/App.test.ts +33 -3
  191. package/src/admin/App.ts +49 -14
  192. package/src/admin/apps/api-keys.ts +10 -3
  193. package/src/admin/apps/organizations.test.ts +455 -0
  194. package/src/admin/apps/organizations.ts +953 -0
  195. package/src/admin/apps/scopes.test.ts +10 -0
  196. package/src/admin/ui/plugin.ts +1 -1
  197. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  198. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  199. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  200. package/src/admin/ui/src/lib/config.ts +2 -3
  201. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  202. package/src/admin/ui/src/lib/organizations.ts +262 -0
  203. package/src/admin/ui/src/lib/query-client.ts +3 -1
  204. package/src/admin/ui/src/lib/theme.ts +57 -0
  205. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  206. package/src/admin/ui/src/routes/__root.tsx +18 -3
  207. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  208. package/src/admin/ui/src/routes/index.tsx +1 -1
  209. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  210. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  211. package/src/admin/ui/src/start.ts +6 -0
  212. package/src/admin/ui.generated.ts +63 -13
  213. package/src/analytics/Analytics.ts +25 -0
  214. package/src/analytics/tables/requestEvents.ts +33 -0
  215. package/src/apps/composition.test.ts +82 -2
  216. package/src/apps/data/FundingRouteProviders.test.ts +37 -93
  217. package/src/apps/data/FundingRouteProviders.ts +14 -161
  218. package/src/apps/data/routes/funding.test.ts +7 -42
  219. package/src/apps/data/routes/funding.ts +8 -8
  220. package/src/apps/data/routes/indexer.test.ts +2 -2
  221. package/src/apps/data/routes/webhooks.test.ts +2 -2
  222. package/src/apps/management/App.ts +2 -2
  223. package/src/apps/management/Billing.ts +10 -2
  224. package/src/apps/management/routes/api-keys.test.ts +137 -1
  225. package/src/apps/management/routes/api-keys.ts +63 -38
  226. package/src/apps/management/routes/billing.ts +14 -14
  227. package/src/apps/management/routes/invitations.test.ts +102 -1
  228. package/src/apps/management/routes/invitations.ts +13 -15
  229. package/src/apps/management/routes/members.ts +10 -10
  230. package/src/apps/management/routes/orgs.test.ts +78 -16
  231. package/src/apps/management/routes/orgs.ts +21 -20
  232. package/src/apps/management/routes/projects.ts +12 -13
  233. package/src/apps/management/routes/scopes.test.ts +16 -0
  234. package/src/apps/management/routes/scopes.ts +6 -7
  235. package/src/apps/management/routes/usage.ts +9 -14
  236. package/src/apps/mcp.test.ts +72 -7
  237. package/src/apps/mcp.ts +46 -4
  238. package/src/apps/relay/App.test.ts +23 -18
  239. package/src/apps/relay/App.ts +35 -14
  240. package/src/cloudflare.ts +5 -0
  241. package/src/db/Db.ts +59 -3
  242. package/src/db/Schema.ts +20 -1
  243. package/src/db/tables/adminAuditLogs.ts +47 -0
  244. package/src/db/tables/invitations.ts +1 -1
  245. package/src/db/tables/memberships.ts +59 -3
  246. package/src/db/tables/organizations.ts +148 -1
  247. package/src/db/tables/projects.ts +47 -0
  248. package/src/db/tables/webhookDeliveries.ts +83 -0
  249. package/src/internal/Auth.test.ts +571 -23
  250. package/src/internal/Auth.ts +166 -94
  251. package/src/internal/Cursor.test.ts +10 -0
  252. package/src/internal/Cursor.ts +5 -1
  253. package/src/internal/EdgeCache.test.ts +11 -0
  254. package/src/internal/EdgeCache.ts +2 -0
  255. package/src/internal/FundingRoutes.test.ts +8 -8
  256. package/src/internal/FundingRoutes.ts +25 -29
  257. package/src/internal/Log.test.ts +41 -3
  258. package/src/internal/Log.ts +23 -4
  259. package/src/internal/Store.test-d.ts +15 -0
  260. package/src/internal/Store.test.ts +153 -2
  261. package/src/internal/Store.ts +157 -41
  262. package/src/internal/index.test.ts +34 -0
  263. 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({ 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'
@@ -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 | 400 | 401 | 403 | 404>()
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 | 400 | 401 | 403 | 404 | 501 | 502>()
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 | 400 | 401 | 403 | 404 | 502>()
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 | 400 | 401 | 403 | 404 | 501 | 502>()
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 | 400 | 401 | 403 | 404 | 501 | 502>()
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 | 400 | 401 | 403 | 404 | 501 | 502>()
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 | 400 | 401 | 403 | 404 | 501 | 502>()
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 | 400 | 401 | 403 | 404 | 502>()
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 | 400 | 401 | 403 | 404>()
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 | 400 | 401 | 403 | 404 | 502>()
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 | 400 | 401 | 403 | 404 | 502>()
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 | 400 | 401 | 403 | 404 | 502>()
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 | 400 | 401 | 403 | 404 | 501 | 502>()
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 | 400 | 401 | 403 | 404 | 502>()
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 | 400 | 401 | 403 | 404 | 409 | 502>()
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 | 400 | 401 | 403 | 502>()
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<'forbidden'>()
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 issuable API-key scope vocabulary: the single source of truth for the
3
- * `/scopes` catalog, for key minting (`ApiKeys.mint`), and for the scopes
4
- * required by route policies (`Auth.policy`). Exposing it publicly lets super
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 a session may self-mint this scope (else super-admin-only). */
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 scope vocabulary the single source of truth for the
27
- * `/scopes` catalog and for key minting. Route policies require values from this
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 the union of every {@link catalog} entry. */
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 a session may self-mint through the developer platform, derived from
69
- * the catalog's `selfServe` flag. Paid usage (mainnet sponsorship) is gated by
70
- * an active billing source at request time, so self-serve users cannot consume
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
+ }
@@ -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 () => {