tapimo 0.4.1 → 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 (243) 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/FundingRouteProviders.d.ts +94 -5
  55. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.js +196 -5
  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/exchanges.d.ts +453 -453
  62. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  63. package/dist/apps/data/routes/funding.d.ts +730 -138
  64. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  65. package/dist/apps/data/routes/funding.js +234 -0
  66. package/dist/apps/data/routes/funding.js.map +1 -1
  67. package/dist/apps/data/routes/indexer.d.ts +5 -5
  68. package/dist/apps/data/routes/transactions.d.ts +281 -281
  69. package/dist/apps/data/routes/transfers.d.ts +58 -58
  70. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  71. package/dist/apps/management/App.d.ts +1156 -615
  72. package/dist/apps/management/App.d.ts.map +1 -1
  73. package/dist/apps/management/App.js +2 -2
  74. package/dist/apps/management/Billing.d.ts +3 -0
  75. package/dist/apps/management/Billing.d.ts.map +1 -1
  76. package/dist/apps/management/Billing.js +7 -2
  77. package/dist/apps/management/Billing.js.map +1 -1
  78. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  79. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  80. package/dist/apps/management/routes/api-keys.js +41 -30
  81. package/dist/apps/management/routes/api-keys.js.map +1 -1
  82. package/dist/apps/management/routes/billing.d.ts +278 -146
  83. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  84. package/dist/apps/management/routes/billing.js +14 -14
  85. package/dist/apps/management/routes/billing.js.map +1 -1
  86. package/dist/apps/management/routes/invitations.d.ts +93 -20
  87. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  88. package/dist/apps/management/routes/invitations.js +12 -15
  89. package/dist/apps/management/routes/invitations.js.map +1 -1
  90. package/dist/apps/management/routes/me.d.ts +6 -6
  91. package/dist/apps/management/routes/members.d.ts +89 -18
  92. package/dist/apps/management/routes/members.d.ts.map +1 -1
  93. package/dist/apps/management/routes/members.js +10 -10
  94. package/dist/apps/management/routes/members.js.map +1 -1
  95. package/dist/apps/management/routes/orgs.d.ts +130 -70
  96. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  97. package/dist/apps/management/routes/orgs.js +19 -19
  98. package/dist/apps/management/routes/orgs.js.map +1 -1
  99. package/dist/apps/management/routes/projects.d.ts +137 -48
  100. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  101. package/dist/apps/management/routes/projects.js +12 -13
  102. package/dist/apps/management/routes/projects.js.map +1 -1
  103. package/dist/apps/management/routes/scopes.d.ts +4 -18
  104. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  105. package/dist/apps/management/routes/scopes.js +6 -7
  106. package/dist/apps/management/routes/scopes.js.map +1 -1
  107. package/dist/apps/management/routes/usage.d.ts +63 -19
  108. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  109. package/dist/apps/management/routes/usage.js +9 -10
  110. package/dist/apps/management/routes/usage.js.map +1 -1
  111. package/dist/apps/mcp.d.ts.map +1 -1
  112. package/dist/apps/mcp.js +35 -3
  113. package/dist/apps/mcp.js.map +1 -1
  114. package/dist/apps/relay/App.d.ts +26 -4
  115. package/dist/apps/relay/App.d.ts.map +1 -1
  116. package/dist/apps/relay/App.js +17 -14
  117. package/dist/apps/relay/App.js.map +1 -1
  118. package/dist/db/Db.d.ts +5 -3
  119. package/dist/db/Db.d.ts.map +1 -1
  120. package/dist/db/Db.js +52 -0
  121. package/dist/db/Db.js.map +1 -1
  122. package/dist/db/Schema.d.ts +13 -0
  123. package/dist/db/Schema.d.ts.map +1 -1
  124. package/dist/db/Schema.js +16 -1
  125. package/dist/db/Schema.js.map +1 -1
  126. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  127. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  128. package/dist/db/tables/adminAuditLogs.js +19 -0
  129. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  130. package/dist/db/tables/invitations.d.ts +1 -1
  131. package/dist/db/tables/invitations.d.ts.map +1 -1
  132. package/dist/db/tables/memberships.d.ts +32 -4
  133. package/dist/db/tables/memberships.d.ts.map +1 -1
  134. package/dist/db/tables/memberships.js +31 -0
  135. package/dist/db/tables/memberships.js.map +1 -1
  136. package/dist/db/tables/organizations.d.ts +43 -1
  137. package/dist/db/tables/organizations.d.ts.map +1 -1
  138. package/dist/db/tables/organizations.js +103 -0
  139. package/dist/db/tables/organizations.js.map +1 -1
  140. package/dist/db/tables/projects.d.ts +25 -0
  141. package/dist/db/tables/projects.d.ts.map +1 -1
  142. package/dist/db/tables/projects.js +22 -0
  143. package/dist/db/tables/projects.js.map +1 -1
  144. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  145. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  146. package/dist/db/tables/webhookDeliveries.js +37 -0
  147. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  148. package/dist/internal/Auth.d.ts +13 -18
  149. package/dist/internal/Auth.d.ts.map +1 -1
  150. package/dist/internal/Auth.js +78 -58
  151. package/dist/internal/Auth.js.map +1 -1
  152. package/dist/internal/Cursor.d.ts +1 -1
  153. package/dist/internal/Cursor.d.ts.map +1 -1
  154. package/dist/internal/Cursor.js +4 -0
  155. package/dist/internal/Cursor.js.map +1 -1
  156. package/dist/internal/EdgeCache.d.ts.map +1 -1
  157. package/dist/internal/EdgeCache.js +2 -0
  158. package/dist/internal/EdgeCache.js.map +1 -1
  159. package/dist/internal/FundingRoutes.d.ts +170 -0
  160. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  161. package/dist/internal/FundingRoutes.js +68 -6
  162. package/dist/internal/FundingRoutes.js.map +1 -1
  163. package/dist/internal/Log.d.ts +16 -1
  164. package/dist/internal/Log.d.ts.map +1 -1
  165. package/dist/internal/Log.js +5 -3
  166. package/dist/internal/Log.js.map +1 -1
  167. package/package.json +2 -2
  168. package/src/ApiKey.ts +2 -3
  169. package/src/ApiKeys.ts +95 -17
  170. package/src/App.test.ts +44 -2
  171. package/src/App.ts +29 -9
  172. package/src/Assets.ts +8 -0
  173. package/src/Auth.test-d.ts +36 -0
  174. package/src/Auth.ts +20 -1
  175. package/src/Client.test-d.ts +64 -17
  176. package/src/Scope.test.ts +34 -0
  177. package/src/Scope.ts +53 -17
  178. package/src/admin/App.test.ts +33 -3
  179. package/src/admin/App.ts +49 -14
  180. package/src/admin/apps/api-keys.ts +10 -3
  181. package/src/admin/apps/organizations.test.ts +455 -0
  182. package/src/admin/apps/organizations.ts +953 -0
  183. package/src/admin/apps/scopes.test.ts +10 -0
  184. package/src/admin/ui/plugin.ts +1 -1
  185. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  186. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  187. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  188. package/src/admin/ui/src/lib/config.ts +2 -3
  189. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  190. package/src/admin/ui/src/lib/organizations.ts +262 -0
  191. package/src/admin/ui/src/lib/query-client.ts +3 -1
  192. package/src/admin/ui/src/lib/theme.ts +57 -0
  193. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  194. package/src/admin/ui/src/routes/__root.tsx +18 -3
  195. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  196. package/src/admin/ui/src/routes/index.tsx +1 -1
  197. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  198. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  199. package/src/admin/ui/src/start.ts +6 -0
  200. package/src/admin/ui.generated.ts +63 -13
  201. package/src/analytics/Analytics.ts +25 -0
  202. package/src/analytics/tables/requestEvents.ts +33 -0
  203. package/src/apps/composition.test.ts +82 -2
  204. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  205. package/src/apps/data/FundingRouteProviders.ts +294 -9
  206. package/src/apps/data/routes/funding.test.ts +90 -23
  207. package/src/apps/data/routes/funding.ts +315 -0
  208. package/src/apps/management/App.ts +2 -2
  209. package/src/apps/management/Billing.ts +10 -2
  210. package/src/apps/management/routes/api-keys.test.ts +137 -1
  211. package/src/apps/management/routes/api-keys.ts +63 -38
  212. package/src/apps/management/routes/billing.ts +14 -14
  213. package/src/apps/management/routes/invitations.test.ts +102 -1
  214. package/src/apps/management/routes/invitations.ts +13 -15
  215. package/src/apps/management/routes/members.ts +10 -10
  216. package/src/apps/management/routes/orgs.test.ts +78 -16
  217. package/src/apps/management/routes/orgs.ts +21 -20
  218. package/src/apps/management/routes/projects.ts +12 -13
  219. package/src/apps/management/routes/scopes.test.ts +16 -0
  220. package/src/apps/management/routes/scopes.ts +6 -7
  221. package/src/apps/management/routes/usage.ts +9 -14
  222. package/src/apps/mcp.test.ts +25 -1
  223. package/src/apps/mcp.ts +43 -3
  224. package/src/apps/relay/App.test.ts +23 -18
  225. package/src/apps/relay/App.ts +35 -14
  226. package/src/db/Db.ts +59 -3
  227. package/src/db/Schema.ts +20 -1
  228. package/src/db/tables/adminAuditLogs.ts +47 -0
  229. package/src/db/tables/invitations.ts +1 -1
  230. package/src/db/tables/memberships.ts +59 -3
  231. package/src/db/tables/organizations.ts +148 -1
  232. package/src/db/tables/projects.ts +47 -0
  233. package/src/db/tables/webhookDeliveries.ts +83 -0
  234. package/src/internal/Auth.test.ts +174 -7
  235. package/src/internal/Auth.ts +90 -62
  236. package/src/internal/Cursor.test.ts +10 -0
  237. package/src/internal/Cursor.ts +5 -1
  238. package/src/internal/EdgeCache.test.ts +11 -0
  239. package/src/internal/EdgeCache.ts +2 -0
  240. package/src/internal/FundingRoutes.test.ts +26 -0
  241. package/src/internal/FundingRoutes.ts +187 -18
  242. package/src/internal/Log.test.ts +41 -3
  243. package/src/internal/Log.ts +23 -4
@@ -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 = {
@@ -1,8 +1,13 @@
1
- import { Hono } from 'hono'
1
+ import { Hono, type Context } from 'hono'
2
2
  import { requestId } from 'hono/request-id'
3
3
  import { Mppx, tempo } from 'mppx/hono'
4
4
  import { RateLimit } from 'tapimo'
5
5
  import * as ApiKey from '../ApiKey.js'
6
+ import type * as Db from '../db/Db.js'
7
+ import * as Memberships from '../db/tables/memberships.js'
8
+ import * as Organizations from '../db/tables/organizations.js'
9
+ import * as Projects from '../db/tables/projects.js'
10
+ import * as Users from '../db/tables/users.js'
6
11
  import * as Auth from './Auth.js'
7
12
  import * as Log from './Log.js'
8
13
  import * as Scope from '../Scope.js'
@@ -75,12 +80,13 @@ describe('require', () => {
75
80
  `)
76
81
  })
77
82
 
78
- test('accepts API key from deprecated key query param', async () => {
83
+ test('accepts API key from key query param', async () => {
79
84
  const app = createApp()
80
85
  const response = await app.request(`/protected?key=${token}`)
81
86
  const body = await response.json()
82
87
 
83
88
  expect(response.status).toMatchInlineSnapshot(`200`)
89
+ expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
84
90
  expect(body).toMatchInlineSnapshot(`
85
91
  {
86
92
  "id": "key_test",
@@ -90,11 +96,30 @@ describe('require', () => {
90
96
  `)
91
97
  })
92
98
 
93
- test('strips the deprecated key query param before route validation', async () => {
94
- // Regression: route query schemas are strict (`z.strictObject`), so the
95
- // `key` auth param must be removed from the URL once consumed — otherwise
96
- // the downstream validator rejects it as `query_invalid` and the legacy
97
- // `?key=...` lane breaks on every strict-query endpoint.
99
+ test('keeps query-authenticated responses private after downstream caching', async () => {
100
+ const auth = {
101
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
102
+ rateLimit: RateLimit.memory(),
103
+ }
104
+ const app = new Hono<Auth.Environment>()
105
+ app.use('*', async (c, next) => {
106
+ c.set('auth', auth)
107
+ await next()
108
+ })
109
+ app.use('/cached', Auth.require({ apiKey: { scopes: ['data:read'] } }))
110
+ app.get('/cached', (c) => {
111
+ c.header('Cache-Control', 'public, max-age=86400')
112
+ return c.json({ ok: true })
113
+ })
114
+
115
+ const response = await app.request(`/cached?key=${token}`)
116
+
117
+ expect(response.status).toMatchInlineSnapshot(`200`)
118
+ expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
119
+ })
120
+
121
+ test('strips the key query param before route validation', async () => {
122
+ // Auth owns `key`, so strict route schemas must not see it.
98
123
  const auth = {
99
124
  apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
100
125
  rateLimit: RateLimit.memory(),
@@ -1298,6 +1323,87 @@ describe('require', () => {
1298
1323
  })
1299
1324
  })
1300
1325
 
1326
+ describe('ensureProject', () => {
1327
+ test('authorizes a flat project route against the API key organization', async () => {
1328
+ const db = TestApp.database()
1329
+ const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
1330
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1331
+ const response = await createProjectGuardApp(db, apiKeyPrincipal(organization.id)).request(
1332
+ `/v1/projects/${project.id}/resource`,
1333
+ )
1334
+
1335
+ expect(response.status).toMatchInlineSnapshot(`200`)
1336
+ expect(await response.json()).toEqual({
1337
+ membership: null,
1338
+ orgId: organization.id,
1339
+ projectId: project.id,
1340
+ })
1341
+ })
1342
+
1343
+ test('hides a flat project from an API key in another organization', async () => {
1344
+ const db = TestApp.database()
1345
+ const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
1346
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1347
+ const response = await createProjectGuardApp(db, apiKeyPrincipal('org_foreign')).request(
1348
+ `/v1/projects/${project.id}/resource`,
1349
+ )
1350
+
1351
+ expect(response.status).toMatchInlineSnapshot(`404`)
1352
+ expect(await response.json()).toMatchInlineSnapshot(`
1353
+ {
1354
+ "error": {
1355
+ "code": "project_not_found",
1356
+ "message": "Project not found",
1357
+ },
1358
+ }
1359
+ `)
1360
+ })
1361
+
1362
+ test('resolves session membership for a flat project route', async () => {
1363
+ const db = TestApp.database()
1364
+ const user = await Users.upsertByAddress(db, {
1365
+ address: '0x0000000000000000000000000000000000000001',
1366
+ })
1367
+ const organization = await Organizations.create(db, { name: 'Test org' })
1368
+ await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
1369
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1370
+ const response = await createProjectGuardApp(db, sessionPrincipal(user.id)).request(
1371
+ `/v1/projects/${project.id}/resource`,
1372
+ )
1373
+ const body = (await response.json()) as Record<string, unknown>
1374
+
1375
+ expect(response.status).toMatchInlineSnapshot(`200`)
1376
+ expect(body).toEqual({
1377
+ membership: 'member',
1378
+ orgId: organization.id,
1379
+ projectId: project.id,
1380
+ })
1381
+ })
1382
+
1383
+ test('preserves nested organization role authorization', async () => {
1384
+ const db = TestApp.database()
1385
+ const user = await Users.upsertByAddress(db, {
1386
+ address: '0x0000000000000000000000000000000000000001',
1387
+ })
1388
+ const organization = await Organizations.create(db, { name: 'Test org' })
1389
+ await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
1390
+ const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
1391
+ const response = await createNestedProjectGuardApp(db, sessionPrincipal(user.id)).request(
1392
+ `/v1/orgs/${organization.id}/projects/${project.id}/resource`,
1393
+ )
1394
+
1395
+ expect(response.status).toMatchInlineSnapshot(`403`)
1396
+ expect(await response.json()).toMatchInlineSnapshot(`
1397
+ {
1398
+ "error": {
1399
+ "code": "forbidden",
1400
+ "message": "Requires the admin role",
1401
+ },
1402
+ }
1403
+ `)
1404
+ })
1405
+ })
1406
+
1301
1407
  function createScopedApp(options: {
1302
1408
  auth?: Auth.Context | undefined
1303
1409
  routes: readonly { path: string; policy: Auth.require.Policy }[]
@@ -1322,6 +1428,67 @@ function createScopedApp(options: {
1322
1428
  return app
1323
1429
  }
1324
1430
 
1431
+ type GuardEnvironment = {
1432
+ Variables: Auth.Variables & { db: Db.Source }
1433
+ }
1434
+
1435
+ function createProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
1436
+ const app = new Hono<GuardEnvironment>()
1437
+ app.use('*', async (c, next) => {
1438
+ c.set('db', db)
1439
+ c.set('principal', principal)
1440
+ await next()
1441
+ })
1442
+ app.get('/v1/projects/:projectId/resource', Auth.ensureProject(), projectGuardHandler)
1443
+ return app
1444
+ }
1445
+
1446
+ function createNestedProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
1447
+ const app = new Hono<GuardEnvironment>()
1448
+ app.use('*', async (c, next) => {
1449
+ c.set('db', db)
1450
+ c.set('principal', principal)
1451
+ await next()
1452
+ })
1453
+ app.get(
1454
+ '/v1/orgs/:orgId/projects/:projectId/resource',
1455
+ Auth.ensureOrg({ role: 'admin' }),
1456
+ Auth.ensureProject(),
1457
+ projectGuardHandler,
1458
+ )
1459
+ return app
1460
+ }
1461
+
1462
+ function projectGuardHandler(c: Context<GuardEnvironment>) {
1463
+ return c.json({
1464
+ membership: Auth.membership(c)?.role ?? null,
1465
+ orgId: Auth.org(c).id,
1466
+ projectId: Auth.project(c).id,
1467
+ })
1468
+ }
1469
+
1470
+ function apiKeyPrincipal(orgId: string): Auth.Principal {
1471
+ const apiKey = { ...key, orgId }
1472
+ return {
1473
+ apiKey,
1474
+ environment: apiKey.environment,
1475
+ id: apiKey.id,
1476
+ orgId,
1477
+ type: 'api_key',
1478
+ }
1479
+ }
1480
+
1481
+ function sessionPrincipal(id: string): Auth.Principal {
1482
+ return {
1483
+ id,
1484
+ identity: {
1485
+ provider: 'wallet',
1486
+ subject: '0x0000000000000000000000000000000000000001',
1487
+ },
1488
+ type: 'session',
1489
+ }
1490
+ }
1491
+
1325
1492
  type Options = {
1326
1493
  auth?: Auth.Context | undefined
1327
1494
  logger?: Log.Emit | undefined