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
@@ -8,6 +8,31 @@ export type BillableCount = RequestEvents.BillableCount
8
8
  /** Options for {@link readBillableRequestCounts}. */
9
9
  export type BillableCountsOptions = RequestEvents.BillableCountsOptions
10
10
 
11
+ /** One organization's request usage and error breakdown. */
12
+ export type ProjectUsage = RequestEvents.ProjectUsage
13
+
14
+ /** Options for reading one organization's request usage. */
15
+ export type ProjectUsageOptions = RequestEvents.ProjectUsageOptions
16
+
17
+ /** API errors grouped by their stable error code. */
18
+ export type ErrorBreakdown = RequestEvents.ErrorBreakdown
19
+
20
+ /** Reads stable API error-code counts for one organization attribution window. */
21
+ export function readErrorBreakdown(
22
+ source: Source,
23
+ options: ProjectUsageOptions,
24
+ ): Promise<ErrorBreakdown[]> {
25
+ return RequestEvents.readErrorBreakdown(get(source), options)
26
+ }
27
+
28
+ /** Reads request usage for an organization and optional attribution terms. */
29
+ export function readProjectUsage(
30
+ source: Source,
31
+ options: ProjectUsageOptions,
32
+ ): Promise<ProjectUsage> {
33
+ return RequestEvents.readProjectUsage(get(source), options)
34
+ }
35
+
11
36
  /**
12
37
  * Reads deduped billable request counts grouped by `(org, UTC hour)` from
13
38
  * `request_events` — the request-metered billing source. Resolves the source
@@ -59,6 +59,14 @@ export type Attribution = {
59
59
  /** API-key environment for request usage. */
60
60
  export type Environment = 'production' | 'sandbox'
61
61
 
62
+ /** API errors grouped by their stable error code. */
63
+ export type ErrorBreakdown = {
64
+ /** Stable API error code, or `unknown` when none was recorded. */
65
+ code: string
66
+ /** Requests returning this error. */
67
+ requests: number
68
+ }
69
+
62
70
  /** API key usage grouped for one project. */
63
71
  export type KeyBreakdown = {
64
72
  /** API key id (`key_…`). */
@@ -331,6 +339,26 @@ export async function readProjectUsage(
331
339
  }
332
340
  }
333
341
 
342
+ /** Reads stable API error-code counts for one organization attribution window. */
343
+ export async function readErrorBreakdown(
344
+ analytics: Analytics.Analytics,
345
+ options: ProjectUsageOptions,
346
+ ): Promise<ErrorBreakdown[]> {
347
+ const rows = await analytics.query<ErrorRow>(
348
+ `
349
+ SELECT
350
+ coalesce(error_code, 'unknown') AS code,
351
+ count() AS requests
352
+ FROM ${table}
353
+ WHERE ${usageWhere(options)} AND status >= 400
354
+ GROUP BY error_code
355
+ ORDER BY requests DESC, code ASC
356
+ LIMIT 25
357
+ `,
358
+ )
359
+ return rows.map((row) => ({ code: String(row.code), requests: number(row.requests) }))
360
+ }
361
+
334
362
  /** One org's deduped billable request count for a single UTC hour. */
335
363
  export type BillableCount = {
336
364
  /** UTC hour start (ISO 8601). */
@@ -406,6 +434,11 @@ type KeyRow = {
406
434
  requests: number | string
407
435
  }
408
436
 
437
+ type ErrorRow = {
438
+ code: string
439
+ requests: number | string
440
+ }
441
+
409
442
  type RouteRow = {
410
443
  averageDurationMs: number | string
411
444
  errors: number | string
@@ -1,5 +1,7 @@
1
1
  import { App, Store } from 'tapimo'
2
2
  import { data, management, relay } from 'tapimo/apps'
3
+ import { Hono } from 'hono'
4
+ import { describeRoute } from 'hono-openapi'
3
5
  import * as TestApp from '../../test/App.js'
4
6
 
5
7
  // Each test mounts one group and asserts its routes exist while omitted groups
@@ -10,7 +12,10 @@ import * as TestApp from '../../test/App.js'
10
12
  async function document(app: { request: (path: string) => Promise<Response> | Response }) {
11
13
  const res = await app.request('/openapi.json')
12
14
  return (await res.json()) as {
15
+ components: { securitySchemes: Record<string, unknown> }
13
16
  paths: Record<string, unknown>
17
+ tags: readonly { description: string; name: string }[]
18
+ webhooks?: Record<string, unknown> | undefined
14
19
  'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
15
20
  }
16
21
  }
@@ -20,6 +25,77 @@ async function paths(app: { request: (path: string) => Promise<Response> | Respo
20
25
  return Object.keys((await document(app)).paths)
21
26
  }
22
27
 
28
+ describe('App.from()', () => {
29
+ test('contributes external route-group metadata', async () => {
30
+ const group = new Hono().get(
31
+ '/v1/external/webhooks',
32
+ describeRoute({
33
+ responses: { 200: { description: 'External webhook status.' } },
34
+ summary: 'Get webhook status',
35
+ tags: ['External'],
36
+ }),
37
+ (c) => c.json({ status: 'ok' }, 200),
38
+ )
39
+ const external = App.from(group, {
40
+ docs: ({ spec }) =>
41
+ new Hono().get('/external-docs', async (c) => c.json({ paths: (await spec()).paths })),
42
+ securitySchemes: {
43
+ externalKey: { in: 'header', name: 'external-key', type: 'apiKey' },
44
+ },
45
+ tags: [{ description: 'Externally defined routes.', name: 'External' }],
46
+ webhooks: () => ({ externalEvent: { post: { responses: { 200: { description: 'OK' } } } } }),
47
+ 'x-tagGroups': [{ name: 'External API', tags: ['External'] }],
48
+ })
49
+ expectTypeOf(external).toEqualTypeOf<typeof group>()
50
+
51
+ const app = App.create({ auth: false, db: TestApp.database() }).route('/', external)
52
+ const spec = await document(app)
53
+
54
+ expect({
55
+ securityScheme: spec.components.securitySchemes['externalKey'],
56
+ tags: spec.tags,
57
+ tagGroups: spec['x-tagGroups'],
58
+ webhooks: spec.webhooks,
59
+ }).toMatchInlineSnapshot(`
60
+ {
61
+ "securityScheme": {
62
+ "in": "header",
63
+ "name": "external-key",
64
+ "type": "apiKey",
65
+ },
66
+ "tagGroups": [
67
+ {
68
+ "name": "External API",
69
+ "tags": [
70
+ "External",
71
+ ],
72
+ },
73
+ ],
74
+ "tags": [
75
+ {
76
+ "description": "Externally defined routes.",
77
+ "name": "External",
78
+ },
79
+ ],
80
+ "webhooks": {
81
+ "externalEvent": {
82
+ "post": {
83
+ "responses": {
84
+ "200": {
85
+ "description": "OK",
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ }
92
+ `)
93
+
94
+ const docs = await app.request('/external-docs')
95
+ expect((await docs.json()).paths['/v1/external/webhooks']).toBeDefined()
96
+ })
97
+ })
98
+
23
99
  describe('data()', () => {
24
100
  test('mounts data routes and omits management routes', async () => {
25
101
  const app = App.create({ db: TestApp.database() }).route('/', data())
@@ -42,13 +118,17 @@ describe('relay()', () => {
42
118
 
43
119
  // Mounted but default-closed and hidden: unauthenticated requests are
44
120
  // refused, and the path never appears in the OpenAPI document.
45
- const mounted = await composed.request('/rpc/relay')
46
- expect(mounted.status).toBe(401)
121
+ const relayMounted = await composed.request('/rpc/relay')
122
+ const sponsorMounted = await composed.request('/rpc/sponsor')
123
+ expect(relayMounted.status).toBe(401)
124
+ expect(sponsorMounted.status).toBe(401)
47
125
  expect((await paths(composed)).some((path) => path.startsWith('/rpc/relay'))).toBe(false)
126
+ expect((await paths(composed)).some((path) => path.startsWith('/rpc/sponsor'))).toBe(false)
48
127
 
49
128
  // Without the relay group, the data group no longer serves the path.
50
129
  const dataOnly = App.create({ db: TestApp.database() }).route('/', data())
51
130
  expect((await dataOnly.request('/rpc/relay')).status).toBe(404)
131
+ expect((await dataOnly.request('/rpc/sponsor')).status).toBe(404)
52
132
  })
53
133
  })
54
134
 
@@ -87,7 +87,11 @@ export function data(options: data.Options = {}) {
87
87
  // Reject a well-formed but unsupported `chainId` before it reaches an
88
88
  // upstream, which would otherwise return an opaque `upstream_error`.
89
89
  .use('*', async (c, next) => {
90
- const raw = c.req.query('chainId') ?? c.req.query('chain_id')
90
+ const raw = resolveChainSelector({
91
+ basePath: c.get('basePath') ?? '',
92
+ path: c.req.path,
93
+ query: c.req.query('chainId') ?? c.req.query('chain_id'),
94
+ })
91
95
  if (raw === undefined) return next()
92
96
  // Only reject a well-formed-but-unsupported chain here; malformed values
93
97
  // (e.g. `abc`, `0`) fall through to the route's own query validation.
@@ -101,13 +105,18 @@ export function data(options: data.Options = {}) {
101
105
  // `chainId`. The relay resolves its chain from the request body, not the
102
106
  // query, so it is skipped and enforces the rule in its sponsor validation.
103
107
  .use('*', async (c, next) => {
104
- const relay = Path.join(c.get('basePath') ?? '', 'rpc/relay')
108
+ const rpc = Path.join(c.get('basePath') ?? '', 'rpc')
109
+ const relay = Path.join(rpc, 'relay')
105
110
  if (c.req.path === relay || c.req.path.startsWith(`${relay}/`)) return next()
106
111
 
107
112
  const principal = Auth.getPrincipal(c)
108
113
  if (principal?.type !== 'api_key' || principal.environment !== 'sandbox') return next()
109
114
 
110
- const raw = c.req.query('chainId') ?? c.req.query('chain_id')
115
+ const raw = resolveChainSelector({
116
+ basePath: c.get('basePath') ?? '',
117
+ path: c.req.path,
118
+ query: c.req.query('chainId') ?? c.req.query('chain_id'),
119
+ })
111
120
  // Absent falls back to the group default (mainnet); a malformed value
112
121
  // stays with the route's own validator rather than the sandbox message.
113
122
  const chainId = (() => {
@@ -156,6 +165,18 @@ export function data(options: data.Options = {}) {
156
165
  return Metadata.attach(app, openapi)
157
166
  }
158
167
 
168
+ function resolveChainSelector(options: {
169
+ basePath: string
170
+ path: string
171
+ query: string | undefined
172
+ }) {
173
+ const rpc = Path.join(options.basePath, 'rpc')
174
+ if (options.path === rpc) return undefined
175
+ if (options.path.startsWith(`${rpc}/`))
176
+ return options.path.slice(rpc.length + 1).split('/')[0] || undefined
177
+ return options.query
178
+ }
179
+
159
180
  export declare namespace data {
160
181
  /** Options for the data route group. */
161
182
  type Options = {
@@ -93,6 +93,160 @@ describe('FundingRouteProviders', () => {
93
93
  })
94
94
  })
95
95
 
96
+ test('builds an authenticated exact-input Across quote request', async () => {
97
+ const route = await staticRoute('across')
98
+ const request = FundingRouteProviders.createAcrossQuoteRequest({
99
+ apiKey: 'across-api-key',
100
+ baseUrl: 'https://across.test/api/',
101
+ depositor: '0x1111111111111111111111111111111111111111',
102
+ integratorId: '0xdead',
103
+ recipient: '0x2222222222222222222222222222222222222222',
104
+ route,
105
+ })
106
+
107
+ expect(request.url.pathname).toBe('/api/swap/approval')
108
+ expect(request.url.searchParams.get('originChainId')).toBe('8453')
109
+ expect(request.url.searchParams.get('destinationChainId')).toBe('4217')
110
+ expect(request.url.searchParams.get('amount')).toBe('1000000')
111
+ expect(request.url.searchParams.get('integratorId')).toBe('0xdead')
112
+ expect(request.url.searchParams.get('tradeType')).toBe('exactInput')
113
+ expect(request.url.searchParams.get('skipOriginTxEstimation')).toBe('true')
114
+ expect(request.url.searchParams.get('strictTradeType')).toBe('true')
115
+ expect(request.url.searchParams.get('depositor')).toBe(
116
+ '0x1111111111111111111111111111111111111111',
117
+ )
118
+ expect(request.url.searchParams.get('recipient')).toBe(
119
+ '0x2222222222222222222222222222222222222222',
120
+ )
121
+ expect(new Headers(request.headers).get('authorization')).toBe('Bearer across-api-key')
122
+ })
123
+
124
+ test('calls the runtime fetch method with its global receiver', async () => {
125
+ const route = await staticRoute('across')
126
+ vi.useFakeTimers()
127
+ vi.setSystemTime(now())
128
+ const fetch = vi.fn(function (this: unknown) {
129
+ expect(this).toBe(globalThis)
130
+ return Promise.resolve(jsonResponse(acrossQuoteResponse(route)))
131
+ })
132
+ vi.stubGlobal('fetch', fetch)
133
+
134
+ try {
135
+ const quote = FundingRouteProviders.acrossQuote({
136
+ apiKey: 'across-api-key',
137
+ integratorId: '0xdead',
138
+ })
139
+ const result = await quote(
140
+ { depositor: '0x1111111111111111111111111111111111111111', route },
141
+ new AbortController().signal,
142
+ )
143
+
144
+ expect(result.outputAmount).toBe('990000')
145
+ expect(fetch).toHaveBeenCalledOnce()
146
+ } finally {
147
+ vi.unstubAllGlobals()
148
+ vi.useRealTimers()
149
+ }
150
+ })
151
+
152
+ test('normalizes an Across quote payload', async () => {
153
+ const route = await staticRoute('across')
154
+ const result = FundingRouteProviders.parseAcrossQuote({
155
+ body: acrossQuoteResponse(route),
156
+ now: now(),
157
+ route,
158
+ })
159
+
160
+ expect(result).toMatchObject({
161
+ estimatedSeconds: 2,
162
+ expiresAt: '2026-01-01T00:01:00.000Z',
163
+ fees: {
164
+ total: {
165
+ amount: '1200',
166
+ amountUsd: '0.0012',
167
+ bps: 12,
168
+ token: { chainId: 'eip155:8453', symbol: 'USDC' },
169
+ },
170
+ totalMax: {
171
+ amount: '1800',
172
+ amountUsd: '0.0018',
173
+ bps: 18,
174
+ token: { chainId: 'eip155:8453', symbol: 'USDC' },
175
+ },
176
+ },
177
+ minimumOutputAmount: '988000',
178
+ outputAmount: '990000',
179
+ sampledAt: '2026-01-01T00:00:00.000Z',
180
+ })
181
+ })
182
+
183
+ test('maps Across HTTP errors into stable provider errors', () => {
184
+ expect(
185
+ FundingRouteProviders.mapAcrossQuoteError({ body: { code: 'RATE_LIMITED' }, status: 429 }),
186
+ ).toBeInstanceOf(FundingRoutes.ProviderRateLimitError)
187
+ expect(
188
+ FundingRouteProviders.mapAcrossQuoteError({ body: { code: 'NO_ROUTE' }, status: 400 }),
189
+ ).toBeInstanceOf(FundingRoutes.ProviderQuoteUnavailableError)
190
+ const error = FundingRouteProviders.mapAcrossQuoteError({
191
+ body: { code: 'NOT_FOUND' },
192
+ status: 404,
193
+ })
194
+ expect(error).toMatchObject({ name: 'FundingRouteProviders.ProviderResponseError' })
195
+ expect(FundingRouteProviders.acrossQuoteFailure({ cause: error })).toEqual({
196
+ code: 'NOT_FOUND',
197
+ failure: 'http',
198
+ id: 'across',
199
+ operation: 'swap/approval',
200
+ status: 404,
201
+ })
202
+ })
203
+
204
+ test('rejects invalid Across configuration during adapter construction', () => {
205
+ expect(() =>
206
+ FundingRouteProviders.acrossQuote({ apiKey: 'across-api-key', integratorId: 'tempo' }),
207
+ ).toThrowErrorMatchingInlineSnapshot(`[FundingRouteProviders.ProviderConfigurationError]`)
208
+ })
209
+
210
+ test('rejects malformed Across quote payloads', async () => {
211
+ const route = await staticRoute('across')
212
+ const failure = (() => {
213
+ try {
214
+ FundingRouteProviders.parseAcrossQuote({
215
+ body: { expectedOutputAmount: '990000' },
216
+ now: now(),
217
+ route,
218
+ })
219
+ throw new Error('Expected malformed payload to fail')
220
+ } catch (cause) {
221
+ return cause
222
+ }
223
+ })()
224
+ expect(failure).toMatchObject({ name: 'FundingRouteProviders.ProviderPayloadError' })
225
+ expect(FundingRouteProviders.acrossQuoteFailure({ cause: failure })).toEqual({
226
+ failure: 'payload',
227
+ id: 'across',
228
+ operation: 'swap/approval',
229
+ })
230
+ })
231
+
232
+ test('rejects inconsistent Across fee summaries', async () => {
233
+ const route = await staticRoute('across')
234
+ const body = acrossQuoteResponse(route)
235
+ body.fees.totalMax.amount = '1000'
236
+ expect(() => FundingRouteProviders.parseAcrossQuote({ body, now: now(), route })).toThrow(
237
+ expect.objectContaining({ name: 'FundingRouteProviders.ProviderPayloadError' }),
238
+ )
239
+ })
240
+
241
+ test('rejects mismatched Across fee tokens', async () => {
242
+ const route = await staticRoute('across')
243
+ const body = acrossQuoteResponse(route)
244
+ body.fees.total.token.address = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
245
+ expect(() => FundingRouteProviders.parseAcrossQuote({ body, now: now(), route })).toThrow(
246
+ expect.objectContaining({ name: 'FundingRouteProviders.ProviderPayloadError' }),
247
+ )
248
+ })
249
+
96
250
  test('maps Relay liquidity and quote responses into quote probes', async () => {
97
251
  const requests: RecordedRequest[] = []
98
252
  const route = await staticRoute('relay')
@@ -252,6 +406,56 @@ function probeInputFromRoute(
252
406
  }
253
407
  }
254
408
 
409
+ function acrossQuoteResponse(route: FundingRoutes.QuoteProbeInput['route']) {
410
+ return {
411
+ amountType: 'exactInput',
412
+ approvalTxns: [{ data: '0x1234', to: '0x2222222222222222222222222222222222222222' }],
413
+ checks: { balance: { actual: '1000000', expected: '1000000' } },
414
+ expectedFillTime: 2,
415
+ expectedOutputAmount: '990000',
416
+ fees: {
417
+ total: {
418
+ amount: '1200',
419
+ amountUsd: '0.0012',
420
+ pct: '1200000000000000',
421
+ token: {
422
+ address: route.sourceToken.address,
423
+ chainId: 8453,
424
+ decimals: 6,
425
+ symbol: 'USDC',
426
+ },
427
+ },
428
+ totalMax: {
429
+ amount: '1800',
430
+ amountUsd: '0.0018',
431
+ pct: '1800000000000000',
432
+ token: {
433
+ address: route.sourceToken.address,
434
+ chainId: 8453,
435
+ decimals: 6,
436
+ symbol: 'USDC',
437
+ },
438
+ },
439
+ },
440
+ inputAmount: '1000000',
441
+ inputToken: {
442
+ address: route.sourceToken.address,
443
+ chainId: 8453,
444
+ decimals: 6,
445
+ symbol: 'USDC',
446
+ },
447
+ minOutputAmount: '988000',
448
+ outputToken: {
449
+ address: route.destinationToken.address,
450
+ chainId: 4217,
451
+ decimals: 6,
452
+ symbol: 'USDC.e',
453
+ },
454
+ quoteExpiryTimestamp: 1767225660,
455
+ swapTx: { data: '0xabcd', to: '0x3333333333333333333333333333333333333333' },
456
+ }
457
+ }
458
+
255
459
  function recordingFetch(
256
460
  requests: RecordedRequest[],
257
461
  respond: (request: RecordedRequest) => Response | Promise<Response>,