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
@@ -24,6 +24,16 @@ describe('GET /scopes', () => {
24
24
  "scope": "indexer:query",
25
25
  "selfServe": true,
26
26
  },
27
+ {
28
+ "description": "Read organization resources across projects and environments.",
29
+ "scope": "management:read",
30
+ "selfServe": true,
31
+ },
32
+ {
33
+ "description": "Create, update, and delete organization resources across projects and environments.",
34
+ "scope": "management:write",
35
+ "selfServe": true,
36
+ },
27
37
  {
28
38
  "description": "Fill and submit transactions through the relay.",
29
39
  "scope": "rpc-relay:read",
@@ -23,7 +23,7 @@ const outFile = path.join(here, '..', 'ui.generated.ts')
23
23
  // Total gzipped budget for the whole UI; the React + TanStack + Regen baseline
24
24
  // is heavier than a hand-rolled SPA, so fail loudly before it bloats the
25
25
  // package. Raise deliberately (and revisit code-splitting) if it trips.
26
- const maxGzipBytes = 400_000
26
+ const maxGzipBytes = 405_000
27
27
 
28
28
  const contentTypes: Record<string, string> = {
29
29
  '.css': 'text/css; charset=utf-8',
@@ -3,6 +3,7 @@ import { Identicon } from 'regen-ui'
3
3
  import { LogOut } from 'lucide-react'
4
4
 
5
5
  import * as Config from '../lib/config'
6
+ import { ThemeToggle } from './ThemeToggle'
6
7
 
7
8
  /** Mobile-only header profile: identicon + sign-out, shown when the sidebar is hidden. */
8
9
  export function MobileProfile() {
@@ -12,6 +13,7 @@ export function MobileProfile() {
12
13
  return (
13
14
  <div className="flex items-center gap-2">
14
15
  <Identicon address={identiconAddress(identity.email)} className="size-9 shrink-0" size={36} />
16
+ <ThemeToggle />
15
17
  {config.auth.logoutUrl ? (
16
18
  <a
17
19
  aria-label="Sign out"
@@ -0,0 +1,20 @@
1
+ import { Moon, Sun } from 'lucide-react'
2
+
3
+ import * as Theme from '../lib/theme'
4
+
5
+ /** Button that switches between the light and dark color themes. */
6
+ export function ThemeToggle() {
7
+ const theme = Theme.use()
8
+ const label = theme === 'dark' ? 'Light mode' : 'Dark mode'
9
+ return (
10
+ <button
11
+ aria-label={label}
12
+ className="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-button text-foreground-secondary hover:bg-surface-hover hover:text-foreground"
13
+ onClick={() => Theme.set(theme === 'dark' ? 'light' : 'dark')}
14
+ title={label}
15
+ type="button"
16
+ >
17
+ {theme === 'dark' ? <Sun className="size-[18px]" /> : <Moon className="size-[18px]" />}
18
+ </button>
19
+ )
20
+ }
@@ -1,22 +1,49 @@
1
1
  import * as ReactQuery from '@tanstack/react-query'
2
2
  import type { InferRequestType, InferResponseType } from 'hono/client'
3
+ import * as z from 'zod/mini'
3
4
 
4
5
  import { client } from './client'
5
6
 
6
7
  /** A single API key record from the list endpoint. */
7
8
  export type ApiKey = InferResponseType<(typeof client)['api-keys']['$get'], 200>['data'][number]
8
9
 
10
+ /** URL-backed API key filters. */
11
+ export const search = z.object({
12
+ orgId: z.catch(z.optional(z.string()), undefined),
13
+ projectId: z.catch(z.optional(z.string()), undefined),
14
+ })
15
+
16
+ /** API key list filters. */
17
+ export type Filters = z.output<typeof search>
18
+
9
19
  /** List API keys. */
10
20
  export namespace get {
11
- export const key = ['api-keys'] as const
12
- export function fn() {
13
- return client['api-keys'].$get().then((r) => r.json())
21
+ export const baseKey = ['api-keys'] as const
22
+ export function key(filters: Filters = {}) {
23
+ return [...baseKey, filters.orgId ?? '', filters.projectId ?? ''] as const
14
24
  }
15
- export function options() {
16
- return ReactQuery.queryOptions({ queryFn: fn, queryKey: key, staleTime: 30_000 })
25
+ export function fn(filters: Filters = {}) {
26
+ return client['api-keys']
27
+ .$get({ query: filters.orgId ? { orgId: filters.orgId } : {} })
28
+ .then(async (response) => {
29
+ if (!response.ok) throw new Error('Failed to load API keys.')
30
+ const body = await response.json()
31
+ return {
32
+ data: filters.projectId
33
+ ? body.data.filter((record) => record.projectId === filters.projectId)
34
+ : body.data,
35
+ }
36
+ })
37
+ }
38
+ export function options(filters: Filters = {}) {
39
+ return ReactQuery.queryOptions({
40
+ queryFn: () => fn(filters),
41
+ queryKey: key(filters),
42
+ staleTime: 30_000,
43
+ })
17
44
  }
18
- export function useQuery() {
19
- return ReactQuery.useQuery(options())
45
+ export function useQuery(filters: Filters = {}) {
46
+ return ReactQuery.useQuery(options(filters))
20
47
  }
21
48
  }
22
49
 
@@ -33,12 +60,7 @@ export namespace provision {
33
60
  const queryClient = ReactQuery.useQueryClient()
34
61
  return ReactQuery.useMutation({
35
62
  mutationFn: fn,
36
- onSuccess: ({ token: _token, ...record }) =>
37
- queryClient.setQueryData(
38
- get.key,
39
- (previous: { data: ApiKey[] } | undefined) =>
40
- previous && { data: [record, ...previous.data] },
41
- ),
63
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: get.baseKey }),
42
64
  })
43
65
  }
44
66
  }
@@ -52,12 +74,7 @@ export namespace revoke {
52
74
  const queryClient = ReactQuery.useQueryClient()
53
75
  return ReactQuery.useMutation({
54
76
  mutationFn: fn,
55
- onSuccess: (_response, id) =>
56
- queryClient.setQueryData(
57
- get.key,
58
- (previous: { data: ApiKey[] } | undefined) =>
59
- previous && { data: previous.data.filter((key) => key.id !== id) },
60
- ),
77
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: get.baseKey }),
61
78
  })
62
79
  }
63
80
  }
@@ -39,14 +39,13 @@ export async function getConfig(): Promise<Config> {
39
39
  }
40
40
 
41
41
  /**
42
- * Bootstrap config query. Seeded from the inlined value and never stale, so an
43
- * authenticated load reads identity synchronously with no round trip.
42
+ * Bootstrap config query. The query function reads the inlined value without a
43
+ * network round trip after the empty prerendered shell hydrates.
44
44
  */
45
45
  export function options() {
46
46
  return queryOptions({
47
47
  queryKey: key,
48
48
  queryFn: getConfig,
49
- initialData: inlineConfig,
50
49
  staleTime: Infinity,
51
50
  })
52
51
  }
@@ -0,0 +1,89 @@
1
+ import { QueryClient } from '@tanstack/react-query'
2
+ import { createMemoryHistory, createRouter } from '@tanstack/react-router'
3
+ import { describe, expect, it, vi } from 'vite-plus/test'
4
+
5
+ import { routeTree } from '../routeTree.gen'
6
+ import { Route as DetailRoute } from '../routes/organizations_.$orgId'
7
+ import { Route as ListRoute } from '../routes/organizations'
8
+ import * as Organizations from './organizations'
9
+
10
+ describe('schema.Search', () => {
11
+ it('normalizes valid lookup state', () => {
12
+ expect(Organizations.schema.Search.parse({ period: '24h', query: ' acme ' }))
13
+ .toMatchInlineSnapshot(`
14
+ {
15
+ "period": "24h",
16
+ "query": "acme",
17
+ }
18
+ `)
19
+ })
20
+
21
+ it('drops lookup state below the API minimum', () => {
22
+ expect(Organizations.schema.Search.parse({ query: 'ab' })).toMatchInlineSnapshot(`
23
+ {
24
+ "query": undefined,
25
+ }
26
+ `)
27
+ })
28
+
29
+ it('drops an invalid usage period', () => {
30
+ expect(Organizations.schema.Search.parse({ period: 'forever' })).toMatchInlineSnapshot(`
31
+ {
32
+ "period": undefined,
33
+ }
34
+ `)
35
+ })
36
+ })
37
+
38
+ describe('searchParameters', () => {
39
+ it('preserves a lookup across list and detail navigation', () => {
40
+ expect(Organizations.searchParameters({ query: 'acme' })).toMatchInlineSnapshot(`
41
+ {
42
+ "query": "acme",
43
+ }
44
+ `)
45
+ })
46
+
47
+ it('omits empty lookup state', () => {
48
+ expect(Organizations.searchParameters({})).toMatchInlineSnapshot(`{}`)
49
+ })
50
+ })
51
+
52
+ describe('organization routes', () => {
53
+ it('matches a direct detail URL without the list route as a parent', () => {
54
+ const router = createRouter({
55
+ context: { queryClient: new QueryClient() },
56
+ history: createMemoryHistory({
57
+ initialEntries: ['/organizations/org_example?query=acme'],
58
+ }),
59
+ routeTree,
60
+ })
61
+
62
+ expect(router.matchRoutes('/organizations/org_example').map((match) => match.routeId))
63
+ .toMatchInlineSnapshot(`
64
+ [
65
+ "__root__",
66
+ "/organizations_/$orgId",
67
+ ]
68
+ `)
69
+ })
70
+
71
+ it('blocks only on the primary organization query', async () => {
72
+ const ensureQueryData = vi.fn().mockResolvedValue({})
73
+ const ensureInfiniteQueryData = vi.fn()
74
+
75
+ const loader = DetailRoute.options.loader as unknown as (options: unknown) => Promise<unknown>
76
+ await loader({
77
+ context: { queryClient: { ensureInfiniteQueryData, ensureQueryData } },
78
+ params: { orgId: 'org_example' },
79
+ })
80
+
81
+ expect(ensureQueryData).toHaveBeenCalledOnce()
82
+ expect(ensureInfiniteQueryData).not.toHaveBeenCalled()
83
+ })
84
+
85
+ it('provides recoverable list and detail error surfaces', () => {
86
+ expect(ListRoute.options.errorComponent).toBeTypeOf('function')
87
+ expect(DetailRoute.options.errorComponent).toBeTypeOf('function')
88
+ })
89
+ })
@@ -0,0 +1,262 @@
1
+ import * as ReactQuery from '@tanstack/react-query'
2
+ import type { InferResponseType } from 'hono/client'
3
+ import * as z from 'zod/mini'
4
+
5
+ import { client } from './client'
6
+
7
+ type ListResponse = InferResponseType<(typeof client)['organizations']['$get'], 200>
8
+ type DetailResponse = InferResponseType<(typeof client)['organizations'][':orgId']['$get'], 200>
9
+ type MembersResponse = InferResponseType<
10
+ (typeof client)['organizations'][':orgId']['members']['$get'],
11
+ 200
12
+ >
13
+ type ProjectsResponse = InferResponseType<
14
+ (typeof client)['organizations'][':orgId']['projects']['$get'],
15
+ 200
16
+ >
17
+ type ApiKeysResponse = InferResponseType<
18
+ (typeof client)['organizations'][':orgId']['api-keys']['$get'],
19
+ 200
20
+ >
21
+ type SponsorshipResponse = InferResponseType<
22
+ (typeof client)['organizations'][':orgId']['sponsorship']['$get'],
23
+ 200
24
+ >
25
+ type UsageResponse = InferResponseType<
26
+ (typeof client)['organizations'][':orgId']['usage']['$get'],
27
+ 200
28
+ >
29
+ type WebhooksResponse = InferResponseType<
30
+ (typeof client)['organizations'][':orgId']['webhooks']['$get'],
31
+ 200
32
+ >
33
+
34
+ /** An organization returned by the admin lookup API. */
35
+ export type Organization = ListResponse['data'][number]
36
+
37
+ /** Detailed organization information, including billing state. */
38
+ export type Detail = DetailResponse
39
+
40
+ /** An organization member with resolved identity. */
41
+ export type Member = MembersResponse['data'][number]
42
+
43
+ /** A project owned by an organization. */
44
+ export type Project = ProjectsResponse['data'][number]
45
+
46
+ /** Operational API key state for an organization. */
47
+ export type ApiKey = ApiKeysResponse['data'][number]
48
+
49
+ /** Recent organization sponsorship activity. */
50
+ export type Sponsorship = SponsorshipResponse
51
+
52
+ /** Recent organization API usage. */
53
+ export type Usage = UsageResponse
54
+
55
+ /** Organization webhook subscriptions and failures. */
56
+ export type Webhooks = WebhooksResponse
57
+
58
+ /** Schemas shared by organization lookup routes. */
59
+ export namespace schema {
60
+ /** Search parameters accepted by organization list and detail routes. */
61
+ export const Search = z.object({
62
+ period: z.catch(z.optional(z.enum(['24h', '7d', '30d'])), undefined),
63
+ query: z.catch(
64
+ z.optional(
65
+ z
66
+ .pipe(
67
+ z.string(),
68
+ z.transform((value) => value.trim()),
69
+ )
70
+ .check(z.minLength(3), z.maxLength(200)),
71
+ ),
72
+ undefined,
73
+ ),
74
+ })
75
+ }
76
+
77
+ /** Builds canonical organization lookup search parameters. */
78
+ export function searchParameters(options: searchParameters.Options) {
79
+ return options.query ? { query: options.query } : {}
80
+ }
81
+
82
+ export declare namespace searchParameters {
83
+ /** Organization lookup navigation state. */
84
+ type Options = { query?: string | undefined }
85
+ }
86
+
87
+ /** List organizations, optionally filtered by id, name, or member email. */
88
+ export namespace list {
89
+ /** Organization lookup filters. */
90
+ export type Input = { query?: string | undefined }
91
+
92
+ export function key(options: Input) {
93
+ return ['organizations', 'list', options.query ?? ''] as const
94
+ }
95
+
96
+ export async function fn(options: Input & { cursor?: string | undefined }) {
97
+ const response = await client.organizations.$get({
98
+ query: {
99
+ ...(options.cursor ? { cursor: options.cursor } : {}),
100
+ limit: '25',
101
+ ...(options.query ? { query: options.query } : {}),
102
+ },
103
+ })
104
+ if (!response.ok) throw new Error('Failed to load organizations.')
105
+ return (await response.json()) as ListResponse
106
+ }
107
+
108
+ export function options(options: Input) {
109
+ return ReactQuery.infiniteQueryOptions({
110
+ getNextPageParam: (page: ListResponse) => page.nextCursor,
111
+ initialPageParam: null as string | null,
112
+ queryFn: ({ pageParam }) => fn({ ...options, cursor: pageParam ?? undefined }),
113
+ queryKey: key(options),
114
+ staleTime: 30_000,
115
+ })
116
+ }
117
+ }
118
+
119
+ /** Get one organization by id. */
120
+ export namespace get {
121
+ export function key(orgId: string) {
122
+ return ['organizations', 'detail', orgId] as const
123
+ }
124
+
125
+ export async function fn(orgId: string) {
126
+ const response = await client.organizations[':orgId'].$get({ param: { orgId } })
127
+ if (response.status === 404) throw new Error('Organization not found.')
128
+ if (!response.ok) throw new Error('Failed to load organization.')
129
+ return (await response.json()) as DetailResponse
130
+ }
131
+
132
+ export function options(orgId: string) {
133
+ return ReactQuery.queryOptions({
134
+ queryFn: () => fn(orgId),
135
+ queryKey: key(orgId),
136
+ staleTime: 30_000,
137
+ })
138
+ }
139
+ }
140
+
141
+ /** List members for one organization. */
142
+ export namespace members {
143
+ export function key(orgId: string) {
144
+ return ['organizations', orgId, 'members'] as const
145
+ }
146
+
147
+ export async function fn(options: { cursor?: string | undefined; orgId: string }) {
148
+ const response = await client.organizations[':orgId'].members.$get({
149
+ param: { orgId: options.orgId },
150
+ query: { ...(options.cursor ? { cursor: options.cursor } : {}), limit: '25' },
151
+ })
152
+ if (!response.ok) throw new Error('Failed to load organization members.')
153
+ return (await response.json()) as MembersResponse
154
+ }
155
+
156
+ export function options(orgId: string) {
157
+ return ReactQuery.infiniteQueryOptions({
158
+ getNextPageParam: (page: MembersResponse) => page.nextCursor,
159
+ initialPageParam: null as string | null,
160
+ queryFn: ({ pageParam }) => fn({ cursor: pageParam ?? undefined, orgId }),
161
+ queryKey: key(orgId),
162
+ staleTime: 30_000,
163
+ })
164
+ }
165
+ }
166
+
167
+ /** List projects for one organization. */
168
+ export namespace projects {
169
+ export function key(orgId: string) {
170
+ return ['organizations', orgId, 'projects'] as const
171
+ }
172
+
173
+ export async function fn(options: { cursor?: string | undefined; orgId: string }) {
174
+ const response = await client.organizations[':orgId'].projects.$get({
175
+ param: { orgId: options.orgId },
176
+ query: { ...(options.cursor ? { cursor: options.cursor } : {}), limit: '25' },
177
+ })
178
+ if (!response.ok) throw new Error('Failed to load organization projects.')
179
+ return (await response.json()) as ProjectsResponse
180
+ }
181
+
182
+ export function options(orgId: string) {
183
+ return ReactQuery.infiniteQueryOptions({
184
+ getNextPageParam: (page: ProjectsResponse) => page.nextCursor,
185
+ initialPageParam: null as string | null,
186
+ queryFn: ({ pageParam }) => fn({ cursor: pageParam ?? undefined, orgId }),
187
+ queryKey: key(orgId),
188
+ staleTime: 30_000,
189
+ })
190
+ }
191
+ }
192
+
193
+ /** List operational API key state for one organization. */
194
+ export namespace apiKeys {
195
+ export function options(orgId: string) {
196
+ return ReactQuery.queryOptions({
197
+ queryFn: async () => {
198
+ const response = await client.organizations[':orgId']['api-keys'].$get({
199
+ param: { orgId },
200
+ })
201
+ if (!response.ok) throw new Error('Failed to load organization API keys.')
202
+ return (await response.json()) as ApiKeysResponse
203
+ },
204
+ queryKey: ['organizations', orgId, 'api-keys'],
205
+ staleTime: 30_000,
206
+ })
207
+ }
208
+ }
209
+
210
+ /** Get recent sponsorship activity for one organization. */
211
+ export namespace sponsorship {
212
+ export function options(orgId: string) {
213
+ return ReactQuery.queryOptions({
214
+ queryFn: async () => {
215
+ const response = await client.organizations[':orgId'].sponsorship.$get({
216
+ param: { orgId },
217
+ })
218
+ if (!response.ok) throw new Error('Failed to load sponsorship activity.')
219
+ return (await response.json()) as SponsorshipResponse
220
+ },
221
+ queryKey: ['organizations', orgId, 'sponsorship'],
222
+ staleTime: 30_000,
223
+ })
224
+ }
225
+ }
226
+
227
+ /** Get recent API usage for one organization. */
228
+ export namespace usage {
229
+ export type Period = '24h' | '30d' | '7d'
230
+
231
+ export function options(orgId: string, period: Period) {
232
+ return ReactQuery.queryOptions({
233
+ queryFn: async () => {
234
+ const response = await client.organizations[':orgId'].usage.$get({
235
+ param: { orgId },
236
+ query: { period },
237
+ })
238
+ if (!response.ok) throw new Error('Failed to load API usage.')
239
+ return (await response.json()) as UsageResponse
240
+ },
241
+ queryKey: ['organizations', orgId, 'usage', period],
242
+ staleTime: 30_000,
243
+ })
244
+ }
245
+ }
246
+
247
+ /** Get webhook health for one organization. */
248
+ export namespace webhooks {
249
+ export function options(orgId: string) {
250
+ return ReactQuery.queryOptions({
251
+ queryFn: async () => {
252
+ const response = await client.organizations[':orgId'].webhooks.$get({
253
+ param: { orgId },
254
+ })
255
+ if (!response.ok) throw new Error('Failed to load webhook health.')
256
+ return (await response.json()) as WebhooksResponse
257
+ },
258
+ queryKey: ['organizations', orgId, 'webhooks'],
259
+ staleTime: 30_000,
260
+ })
261
+ }
262
+ }
@@ -21,6 +21,8 @@ export const persistOptions: Omit<PersistQueryClientOptions, 'queryClient'> = {
21
21
  persister,
22
22
  dehydrateOptions: {
23
23
  shouldDehydrateQuery: (query) =>
24
- query.queryKey[0] !== 'config' && defaultShouldDehydrateQuery(query),
24
+ query.queryKey[0] !== 'config' &&
25
+ query.queryKey[0] !== 'organizations' &&
26
+ defaultShouldDehydrateQuery(query),
25
27
  },
26
28
  }
@@ -0,0 +1,57 @@
1
+ import { useSyncExternalStore } from 'react'
2
+
3
+ /** Admin UI color theme. */
4
+ export type Theme = 'dark' | 'light'
5
+
6
+ const storageKey = 'tempo-api-admin.theme'
7
+
8
+ /** Background colors matching Regen's background token in each theme. */
9
+ export const backgrounds = { dark: '#0c0c0c', light: '#fafafa' } as const
10
+
11
+ /** Applies the saved theme and browser chrome color before the first paint. */
12
+ export const bootstrap = `try {
13
+ const backgrounds = { dark: '${backgrounds.dark}', light: '${backgrounds.light}' }
14
+ const stored = localStorage.getItem('${storageKey}')
15
+ const pinned = stored === 'dark' || stored === 'light' ? stored : null
16
+ if (pinned) document.documentElement.dataset.theme = pinned
17
+ for (const scheme of ['light', 'dark']) {
18
+ const meta = document.createElement('meta')
19
+ meta.name = 'theme-color'
20
+ meta.media = '(prefers-color-scheme: ' + scheme + ')'
21
+ meta.content = backgrounds[pinned ?? scheme]
22
+ document.head.append(meta)
23
+ }
24
+ } catch {}`
25
+
26
+ /** Returns the saved theme, falling back to the operating system preference. */
27
+ export function get(): Theme {
28
+ const stored = localStorage.getItem(storageKey)
29
+ if (stored === 'dark' || stored === 'light') return stored
30
+ return matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
31
+ }
32
+
33
+ /** Saves and applies the selected theme. */
34
+ export function set(theme: Theme) {
35
+ localStorage.setItem(storageKey, theme)
36
+ document.documentElement.dataset.theme = theme
37
+ for (const meta of document.querySelectorAll('meta[name="theme-color"]'))
38
+ meta.setAttribute('content', backgrounds[theme])
39
+ for (const listener of listeners) listener()
40
+ }
41
+
42
+ /** Reactively returns the effective theme. */
43
+ export function use(): Theme {
44
+ return useSyncExternalStore(subscribe, get, () => 'dark')
45
+ }
46
+
47
+ const listeners = new Set<() => void>()
48
+
49
+ function subscribe(listener: () => void) {
50
+ const media = matchMedia('(prefers-color-scheme: dark)')
51
+ listeners.add(listener)
52
+ media.addEventListener('change', listener)
53
+ return () => {
54
+ listeners.delete(listener)
55
+ media.removeEventListener('change', listener)
56
+ }
57
+ }