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/db/Schema.ts CHANGED
@@ -2,6 +2,25 @@ import * as z from 'zod/mini'
2
2
 
3
3
  import type * as Webhooks from '../internal/Webhooks.js'
4
4
 
5
+ /** One authenticated administrator access to a protected admin API route. */
6
+ export const AdminAuditLog = z.object({
7
+ actor: z.string().check(z.describe('Verified administrator email.')),
8
+ createdAt: z.string().check(z.describe('When the access completed (ISO 8601).')),
9
+ id: z.string().check(z.describe('Opaque audit event id (`aud_…`).')),
10
+ method: z.string().check(z.describe('HTTP method used for the access.')),
11
+ path: z.string().check(z.describe('Admin API path accessed, without query parameters.')),
12
+ query: z
13
+ .nullable(z.string())
14
+ .check(z.describe('Query string used for the lookup, or null when absent.')),
15
+ requestId: z.string().check(z.describe('Request id correlating the access with request logs.')),
16
+ status: z
17
+ .number()
18
+ .check(z.int(), z.describe('HTTP response status returned to the administrator.')),
19
+ })
20
+
21
+ /** Columns of the `admin_audit_logs` table. */
22
+ export type AdminAuditLog = z.output<typeof AdminAuditLog>
23
+
5
24
  // Row schemas for database tables (`zod/mini`), the source of truth each
6
25
  // `tables/` module derives its `Table` type from. Nullable fields map to
7
26
  // nullable columns; the domain layer converts `null` back to absent fields.
@@ -49,7 +68,7 @@ export const Invitation = z.object({
49
68
  z.describe('Opaque invitation id (`inv_…`).'),
50
69
  z.meta({ examples: ['inv_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
51
70
  ),
52
- invitedBy: z.string().check(z.describe('User id (`usr_…`) that created the invitation.')),
71
+ invitedBy: z.string().check(z.describe('User or API key id that created the invitation.')),
53
72
  orgId: z.string().check(z.describe('Organization id (`org_…`) the invitation joins.')),
54
73
  revokedAt: z
55
74
  .nullable(z.string())
@@ -0,0 +1,47 @@
1
+ import type { Selectable } from 'kysely'
2
+
3
+ import * as Id from '../../internal/Id.js'
4
+ import type * as Db from '../Db.js'
5
+ import type * as db_Schema from '../Schema.js'
6
+
7
+ /** Columns of the `admin_audit_logs` table. */
8
+ export type Table = db_Schema.AdminAuditLog
9
+
10
+ /** One stored administrator access event. */
11
+ export type Record = Selectable<Table>
12
+
13
+ /** Records an authenticated access to a protected admin API route. */
14
+ export function insert(db: Db.Db, input: insert.Input): Promise<Record> {
15
+ return db.kysely
16
+ .insertInto('admin_audit_logs')
17
+ .values({
18
+ actor: input.actor,
19
+ createdAt: new Date().toISOString(),
20
+ id: Id.generate('aud'),
21
+ method: input.method,
22
+ path: input.path,
23
+ query: input.query ?? null,
24
+ requestId: input.requestId,
25
+ status: input.status,
26
+ })
27
+ .returningAll()
28
+ .executeTakeFirstOrThrow()
29
+ }
30
+
31
+ export declare namespace insert {
32
+ /** Fields captured for one administrator access. */
33
+ type Input = {
34
+ /** Verified administrator email. */
35
+ actor: string
36
+ /** HTTP method used for the access. */
37
+ method: string
38
+ /** Admin API path accessed, without query parameters. */
39
+ path: string
40
+ /** Query string used for the lookup. */
41
+ query?: string | undefined
42
+ /** Request id correlating the access with request logs. */
43
+ requestId: string
44
+ /** HTTP response status returned to the administrator. */
45
+ status: number
46
+ }
47
+ }
@@ -60,7 +60,7 @@ export declare namespace create {
60
60
  email: string
61
61
  /** When the invitation expires (ISO 8601). */
62
62
  expiresAt: string
63
- /** User id (`usr_…`) creating the invitation. */
63
+ /** User or API key id creating the invitation. */
64
64
  invitedBy: string
65
65
  /** Organization id (`org_…`) the invitation joins. */
66
66
  orgId: string
@@ -9,6 +9,9 @@ export type Table = db_Schema.Membership
9
9
  /** A stored membership row. */
10
10
  export type Record = Selectable<Table>
11
11
 
12
+ /** A membership with the member's resolved user identity. */
13
+ export type DetailedRecord = Record & { address: string | null; email: string | null }
14
+
12
15
  /** A membership role. */
13
16
  export type Role = Record['role']
14
17
 
@@ -93,20 +96,73 @@ export function listByOrg(db: Db.Db, orgId: string): Promise<Record[]> {
93
96
  * @param orgId - The organization id (`org_…`).
94
97
  * @returns The records with `address`/`email` from the member's user row.
95
98
  */
96
- export function listByOrgDetailed(
99
+ export function listByOrgDetailed(db: Db.Db, orgId: string): Promise<DetailedRecord[]> {
100
+ return db.kysely
101
+ .selectFrom('memberships')
102
+ .innerJoin('users', 'users.id', 'memberships.userId')
103
+ .where('memberships.orgId', '=', orgId)
104
+ .selectAll('memberships')
105
+ .select(['users.address', 'users.email'])
106
+ .orderBy('memberships.createdAt', 'asc')
107
+ .execute()
108
+ }
109
+
110
+ /**
111
+ * Lists a bounded page of an organization's memberships with member identity,
112
+ * oldest first.
113
+ *
114
+ * @param db - The database.
115
+ * @param orgId - The organization id.
116
+ * @param options - Keyset and page-size options.
117
+ * @returns At most `limit + 1` rows for next-cursor derivation.
118
+ */
119
+ export function listByOrgDetailedPage(
97
120
  db: Db.Db,
98
121
  orgId: string,
99
- ): Promise<(Record & { address: string | null; email: string | null })[]> {
100
- return db.kysely
122
+ options: listByOrgDetailedPage.Options,
123
+ ): Promise<DetailedRecord[]> {
124
+ const cursor = options.cursor
125
+ let query = db.kysely
101
126
  .selectFrom('memberships')
102
127
  .innerJoin('users', 'users.id', 'memberships.userId')
103
128
  .where('memberships.orgId', '=', orgId)
104
129
  .selectAll('memberships')
105
130
  .select(['users.address', 'users.email'])
131
+ if (cursor)
132
+ query = query.where((eb) =>
133
+ eb.or([
134
+ eb('memberships.createdAt', '>', cursor.createdAt),
135
+ eb.and([
136
+ eb('memberships.createdAt', '=', cursor.createdAt),
137
+ eb('memberships.userId', '>', cursor.userId),
138
+ ]),
139
+ ]),
140
+ )
141
+ return query
106
142
  .orderBy('memberships.createdAt', 'asc')
143
+ .orderBy('memberships.userId', 'asc')
144
+ .limit(options.limit + 1)
107
145
  .execute()
108
146
  }
109
147
 
148
+ export declare namespace listByOrgDetailedPage {
149
+ /** Cursor fields for the last membership on the previous page. */
150
+ type Cursor = {
151
+ /** Membership creation time. */
152
+ createdAt: string
153
+ /** Member user id, used as a deterministic tie-breaker. */
154
+ userId: string
155
+ }
156
+
157
+ /** Options for {@link listByOrgDetailedPage}. */
158
+ type Options = {
159
+ /** Last membership returned by the previous page. */
160
+ cursor?: Cursor | undefined
161
+ /** Requested page size. */
162
+ limit: number
163
+ }
164
+ }
165
+
110
166
  /**
111
167
  * Changes a member's role atomically, refusing to demote the last owner. The
112
168
  * org's owner rows are locked for the transaction, so a concurrent demotion or
@@ -1,4 +1,4 @@
1
- import type { Selectable } from 'kysely'
1
+ import { sql, type Selectable } from 'kysely'
2
2
 
3
3
  import * as Id from '../../internal/Id.js'
4
4
  import type * as Db from '../Db.js'
@@ -133,6 +133,39 @@ export function get(db: Db.Db, id: string): Promise<Record | undefined> {
133
133
  return db.kysely.selectFrom('organizations').selectAll().where('id', '=', id).executeTakeFirst()
134
134
  }
135
135
 
136
+ /**
137
+ * Reads an organization with current member and project counts.
138
+ *
139
+ * @param db - The database.
140
+ * @param id - The organization id.
141
+ * @returns The organization summary, or `undefined` when absent.
142
+ */
143
+ export async function getSummary(db: Db.Db, id: string): Promise<search.Result | undefined> {
144
+ const record = await db.kysely
145
+ .selectFrom('organizations')
146
+ .selectAll('organizations')
147
+ .select((eb) => [
148
+ eb
149
+ .selectFrom('memberships')
150
+ .select((builder) => builder.fn.countAll<number>().as('count'))
151
+ .whereRef('memberships.orgId', '=', 'organizations.id')
152
+ .as('memberCount'),
153
+ eb
154
+ .selectFrom('projects')
155
+ .select((builder) => builder.fn.countAll<number>().as('count'))
156
+ .whereRef('projects.orgId', '=', 'organizations.id')
157
+ .as('projectCount'),
158
+ ])
159
+ .where('organizations.id', '=', id)
160
+ .executeTakeFirst()
161
+ if (!record) return undefined
162
+ return {
163
+ ...record,
164
+ memberCount: Number(record.memberCount),
165
+ projectCount: Number(record.projectCount),
166
+ }
167
+ }
168
+
136
169
  /**
137
170
  * Lists organizations, newest first.
138
171
  *
@@ -143,6 +176,120 @@ export function list(db: Db.Db): Promise<Record[]> {
143
176
  return db.kysely.selectFrom('organizations').selectAll().orderBy('createdAt', 'desc').execute()
144
177
  }
145
178
 
179
+ /**
180
+ * Searches organizations for the admin lookup surface, newest first. Results
181
+ * include member and project counts and use a stable `(createdAt, id)` keyset.
182
+ *
183
+ * @param db - The database.
184
+ * @param options - Search, keyset, and page-size options.
185
+ * @returns At most `limit + 1` rows so the caller can derive a next cursor.
186
+ */
187
+ export async function search(db: Db.Db, options: search.Options): Promise<search.Result[]> {
188
+ const { cursor, limit, query: searchQuery } = options
189
+ let query = db.kysely
190
+ .selectFrom('organizations')
191
+ .selectAll('organizations')
192
+ .select((eb) => [
193
+ eb
194
+ .selectFrom('memberships')
195
+ .select((builder) => builder.fn.countAll<number>().as('count'))
196
+ .whereRef('memberships.orgId', '=', 'organizations.id')
197
+ .as('memberCount'),
198
+ eb
199
+ .selectFrom('projects')
200
+ .select((builder) => builder.fn.countAll<number>().as('count'))
201
+ .whereRef('projects.orgId', '=', 'organizations.id')
202
+ .as('projectCount'),
203
+ ])
204
+
205
+ if (searchQuery?.startsWith('org_')) query = query.where('organizations.id', '=', searchQuery)
206
+ else if (searchQuery?.startsWith('usr_'))
207
+ query = query.where((eb) =>
208
+ eb.or([
209
+ eb('organizations.userId', '=', searchQuery),
210
+ eb(
211
+ 'organizations.id',
212
+ 'in',
213
+ eb
214
+ .selectFrom('memberships')
215
+ .select('memberships.orgId')
216
+ .where('memberships.userId', '=', searchQuery),
217
+ ),
218
+ ]),
219
+ )
220
+ else if (searchQuery) {
221
+ const pattern = `${searchQuery
222
+ .toLowerCase()
223
+ .replaceAll('\\', '\\\\')
224
+ .replaceAll('%', '\\%')
225
+ .replaceAll('_', '\\_')}%`
226
+ query = query.where((eb) =>
227
+ eb.or([
228
+ sql<boolean>`lower(${sql.ref('organizations.name')}) LIKE ${pattern} ESCAPE '\\'`,
229
+ eb(
230
+ 'organizations.id',
231
+ 'in',
232
+ eb
233
+ .selectFrom('memberships')
234
+ .innerJoin('users', 'users.id', 'memberships.userId')
235
+ .select('memberships.orgId')
236
+ .where(sql<boolean>`lower(${sql.ref('users.email')}) LIKE ${pattern} ESCAPE '\\'`),
237
+ ),
238
+ ]),
239
+ )
240
+ }
241
+
242
+ if (cursor)
243
+ query = query.where((eb) =>
244
+ eb.or([
245
+ eb('organizations.createdAt', '<', cursor.createdAt),
246
+ eb.and([
247
+ eb('organizations.createdAt', '=', cursor.createdAt),
248
+ eb('organizations.id', '<', cursor.id),
249
+ ]),
250
+ ]),
251
+ )
252
+
253
+ const records = await query
254
+ .orderBy('organizations.createdAt', 'desc')
255
+ .orderBy('organizations.id', 'desc')
256
+ .limit(limit + 1)
257
+ .execute()
258
+ return records.map((record) => ({
259
+ ...record,
260
+ memberCount: Number(record.memberCount),
261
+ projectCount: Number(record.projectCount),
262
+ }))
263
+ }
264
+
265
+ export declare namespace search {
266
+ /** Cursor fields for the last organization returned by the previous page. */
267
+ type Cursor = {
268
+ /** Organization creation time. */
269
+ createdAt: string
270
+ /** Organization id, used as a deterministic tie-breaker. */
271
+ id: string
272
+ }
273
+
274
+ /** Options for {@link search}. */
275
+ type Options = {
276
+ /** Last organization returned by the previous page. */
277
+ cursor?: Cursor | undefined
278
+ /** Requested page size. */
279
+ limit: number
280
+ /** Exact resource id or case-insensitive name/email prefix search. */
281
+ query?: string | undefined
282
+ }
283
+
284
+ /** Organization summary returned by {@link search}. */
285
+ type Result = Record & {
286
+ /** Number of current organization members. */
287
+ memberCount: number
288
+ /** Number of projects owned by the organization. */
289
+ projectCount: number
290
+ }
291
+ }
292
+
146
293
  /**
147
294
  * Lists organizations owned by `userId`, newest first.
148
295
  *
@@ -81,6 +81,53 @@ export function listByOrg(db: Db.Db, orgId: string): Promise<Record[]> {
81
81
  .execute()
82
82
  }
83
83
 
84
+ /**
85
+ * Lists a bounded page of an organization's projects, newest first.
86
+ *
87
+ * @param db - The database.
88
+ * @param orgId - The organization id.
89
+ * @param options - Keyset and page-size options.
90
+ * @returns At most `limit + 1` rows for next-cursor derivation.
91
+ */
92
+ export function listByOrgPage(
93
+ db: Db.Db,
94
+ orgId: string,
95
+ options: listByOrgPage.Options,
96
+ ): Promise<Record[]> {
97
+ const cursor = options.cursor
98
+ let query = db.kysely.selectFrom('projects').selectAll().where('orgId', '=', orgId)
99
+ if (cursor)
100
+ query = query.where((eb) =>
101
+ eb.or([
102
+ eb('createdAt', '<', cursor.createdAt),
103
+ eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
104
+ ]),
105
+ )
106
+ return query
107
+ .orderBy('createdAt', 'desc')
108
+ .orderBy('id', 'desc')
109
+ .limit(options.limit + 1)
110
+ .execute()
111
+ }
112
+
113
+ export declare namespace listByOrgPage {
114
+ /** Cursor fields for the last project on the previous page. */
115
+ type Cursor = {
116
+ /** Project creation time. */
117
+ createdAt: string
118
+ /** Project id, used as a deterministic tie-breaker. */
119
+ id: string
120
+ }
121
+
122
+ /** Options for {@link listByOrgPage}. */
123
+ type Options = {
124
+ /** Last project returned by the previous page. */
125
+ cursor?: Cursor | undefined
126
+ /** Requested page size. */
127
+ limit: number
128
+ }
129
+ }
130
+
84
131
  /**
85
132
  * Renames a project, bumping `updatedAt`.
86
133
  *
@@ -10,6 +10,12 @@ export type Table = db_Schema.WebhookDelivery
10
10
  /** A stored webhook-delivery row. */
11
11
  export type Record = Selectable<Table>
12
12
 
13
+ /** A failed delivery joined to its organization-owned subscription. */
14
+ export type OrganizationFailure = Pick<
15
+ Record,
16
+ 'attempt' | 'createdAt' | 'error' | 'id' | 'responseStatus' | 'subscriptionId'
17
+ >
18
+
13
19
  /** Maps a stored row to the domain delivery (null columns → absent fields). */
14
20
  export function toDelivery(row: Record): Webhooks.Delivery {
15
21
  return {
@@ -85,6 +91,83 @@ export async function list(
85
91
  return (await query.execute()).map(toDelivery)
86
92
  }
87
93
 
94
+ /** Lists recent failed webhook deliveries across an organization. */
95
+ export function listFailuresByOrg(
96
+ db: Db.Db,
97
+ orgId: string,
98
+ options: listFailuresByOrg.Options,
99
+ ): Promise<OrganizationFailure[]> {
100
+ return db.kysely
101
+ .selectFrom('webhook_deliveries')
102
+ .innerJoin(
103
+ 'webhook_subscriptions',
104
+ 'webhook_subscriptions.id',
105
+ 'webhook_deliveries.subscriptionId',
106
+ )
107
+ .select([
108
+ 'webhook_deliveries.attempt',
109
+ 'webhook_deliveries.createdAt',
110
+ 'webhook_deliveries.error',
111
+ 'webhook_deliveries.id',
112
+ 'webhook_deliveries.responseStatus',
113
+ 'webhook_deliveries.subscriptionId',
114
+ ])
115
+ .where('webhook_subscriptions.ownerId', '=', orgId)
116
+ .where('webhook_subscriptions.ownerType', '=', 'api_key')
117
+ .where('webhook_deliveries.createdAt', '>=', options.from)
118
+ .where('webhook_deliveries.expiresAt', '>', options.now)
119
+ .where('webhook_deliveries.status', '=', 'failed')
120
+ .orderBy('webhook_deliveries.createdAt', 'desc')
121
+ .orderBy('webhook_deliveries.id', 'desc')
122
+ .limit(options.limit)
123
+ .execute()
124
+ }
125
+
126
+ export declare namespace listFailuresByOrg {
127
+ /** Bounds for the organization failure scan. */
128
+ type Options = {
129
+ /** Window start (ISO 8601), inclusive. */
130
+ from: string
131
+ /** Maximum failures to return. */
132
+ limit: number
133
+ /** ISO timestamp retention is evaluated against. */
134
+ now: string
135
+ }
136
+ }
137
+
138
+ /** Counts failed webhook deliveries across an organization in a time window. */
139
+ export async function countFailuresByOrg(
140
+ db: Db.Db,
141
+ orgId: string,
142
+ options: countFailuresByOrg.Options,
143
+ ): Promise<number> {
144
+ const row = await db.kysely
145
+ .selectFrom('webhook_deliveries')
146
+ .innerJoin(
147
+ 'webhook_subscriptions',
148
+ 'webhook_subscriptions.id',
149
+ 'webhook_deliveries.subscriptionId',
150
+ )
151
+ .select((eb) => eb.fn.countAll().as('count'))
152
+ .where('webhook_subscriptions.ownerId', '=', orgId)
153
+ .where('webhook_subscriptions.ownerType', '=', 'api_key')
154
+ .where('webhook_deliveries.createdAt', '>=', options.from)
155
+ .where('webhook_deliveries.expiresAt', '>', options.now)
156
+ .where('webhook_deliveries.status', '=', 'failed')
157
+ .executeTakeFirstOrThrow()
158
+ return Number(row.count)
159
+ }
160
+
161
+ export declare namespace countFailuresByOrg {
162
+ /** Bounds for the organization failure count. */
163
+ type Options = {
164
+ /** Window start (ISO 8601), inclusive. */
165
+ from: string
166
+ /** ISO timestamp retention is evaluated against. */
167
+ now: string
168
+ }
169
+ }
170
+
88
171
  export declare namespace list {
89
172
  /** Options for {@link list}. */
90
173
  type Options = {