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
@@ -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
 
@@ -4,95 +4,6 @@ import * as FundingRouteProviders from './FundingRouteProviders.js'
4
4
  const now = () => new Date('2026-01-01T00:00:00.000Z')
5
5
 
6
6
  describe('FundingRouteProviders', () => {
7
- test('maps Across suggested-fees responses into quote probes', async () => {
8
- const requests: RecordedRequest[] = []
9
- const fetch = recordingFetch(requests, async () =>
10
- jsonResponse({
11
- estimatedFillTimeSec: 2,
12
- limits: {
13
- maxDeposit: '2000000',
14
- maxDepositInstant: '1500000',
15
- minDeposit: '500000',
16
- },
17
- outputAmount: '990000',
18
- relayFeePct: '1200000000000000',
19
- timestamp: '1767225600',
20
- }),
21
- )
22
- const adapter = FundingRouteProviders.across({
23
- apiKey: 'across-api-key',
24
- baseUrl: 'https://across.test/api/',
25
- fetch,
26
- integratorId: 'tempo',
27
- now,
28
- })
29
-
30
- const result = await adapter.probeQuote(
31
- await probeInput('across'),
32
- new AbortController().signal,
33
- )
34
-
35
- expect(requests).toHaveLength(1)
36
- expect(requests[0]?.url.pathname).toBe('/api/suggested-fees')
37
- expect(requests[0]?.url.searchParams.get('originChainId')).toBe('8453')
38
- expect(requests[0]?.url.searchParams.get('destinationChainId')).toBe('4217')
39
- expect(requests[0]?.url.searchParams.get('amount')).toBe('1000000')
40
- expect(requests[0]?.url.searchParams.get('integratorId')).toBe('tempo')
41
- expect(requests[0]?.url.searchParams.get('allowUnmatchedDecimals')).toBe('true')
42
- expect(new Headers(requests[0]?.init?.headers).get('authorization')).toBe(
43
- 'Bearer across-api-key',
44
- )
45
- expect(result).toMatchObject({
46
- expiresAt: '2026-01-01T00:10:00.000Z',
47
- limits: {
48
- instantSourceAmount: '1500000',
49
- maxSourceAmount: '2000000',
50
- minSourceAmount: '500000',
51
- },
52
- outputAmount: '990000',
53
- outputAmountFormatted: '0.99',
54
- quality: {
55
- availableVolumeUsd: '2',
56
- estimatedSeconds: 2,
57
- feeBps: 12,
58
- instantVolumeUsd: '1.5',
59
- liquiditySource: 'providerLimit',
60
- sourceDetail: 'across:suggested-fees',
61
- tier: 'liquid',
62
- },
63
- sampledAt: '2026-01-01T00:00:00.000Z',
64
- status: 'available',
65
- })
66
- })
67
-
68
- test('maps recognized Across quote errors to unavailable probes', async () => {
69
- const fetch = recordingFetch([], async () =>
70
- jsonResponse({ code: 'AMOUNT_TOO_HIGH', message: 'Amount exceeds max deposit' }, 400),
71
- )
72
- const adapter = FundingRouteProviders.across({
73
- apiKey: 'across-api-key',
74
- fetch,
75
- integratorId: 'tempo',
76
- now,
77
- })
78
-
79
- const result = await adapter.probeQuote(
80
- await probeInput('across'),
81
- new AbortController().signal,
82
- )
83
-
84
- expect(result).toMatchObject({
85
- quality: {
86
- liquiditySource: 'providerLimit',
87
- score: 0,
88
- sourceDetail: 'across:suggested-fees:AMOUNT_TOO_HIGH',
89
- tier: 'unavailable',
90
- },
91
- sampledAt: '2026-01-01T00:00:00.000Z',
92
- status: 'unavailable',
93
- })
94
- })
95
-
96
7
  test('maps Relay liquidity and quote responses into quote probes', async () => {
97
8
  const requests: RecordedRequest[] = []
98
9
  const route = await staticRoute('relay')
@@ -165,6 +76,43 @@ describe('FundingRouteProviders', () => {
165
76
  })
166
77
  })
167
78
 
79
+ test('calls the runtime fetch method with its global receiver', async () => {
80
+ const route = await staticRoute('relay')
81
+ const fetch = vi.fn(function (this: unknown, input: Parameters<typeof globalThis.fetch>[0]) {
82
+ expect(this).toBe(globalThis)
83
+ const url = toUrl(input)
84
+ return Promise.resolve(
85
+ jsonResponse(
86
+ url.pathname === '/quote/v2'
87
+ ? {
88
+ details: {
89
+ currencyOut: { amount: '990000', minimumAmount: '980000' },
90
+ timeEstimate: 15,
91
+ },
92
+ }
93
+ : { liquidity: [] },
94
+ ),
95
+ )
96
+ })
97
+ vi.stubGlobal('fetch', fetch)
98
+
99
+ try {
100
+ const adapter = FundingRouteProviders.relay({
101
+ now,
102
+ userAddress: '0x1111111111111111111111111111111111111111',
103
+ })
104
+ const result = await adapter.probeQuote(
105
+ probeInputFromRoute(route),
106
+ new AbortController().signal,
107
+ )
108
+
109
+ expect(result.outputAmount).toBe('990000')
110
+ expect(fetch).toHaveBeenCalledTimes(2)
111
+ } finally {
112
+ vi.unstubAllGlobals()
113
+ }
114
+ })
115
+
168
116
  test('keeps Relay quotes when the liquidity probe fails', async () => {
169
117
  const route = await staticRoute('relay')
170
118
  const fetch = recordingFetch([], async (request) => {
@@ -224,10 +172,6 @@ async function staticRoute(
224
172
  return route as FundingRoutes.QuoteProbeInput['route']
225
173
  }
226
174
 
227
- async function probeInput(provider: FundingRoutes.ProviderId) {
228
- return probeInputFromRoute(await staticRoute(provider))
229
- }
230
-
231
175
  function probeInputFromRoute(
232
176
  route: FundingRoutes.QuoteProbeInput['route'],
233
177
  ): FundingRoutes.QuoteProbeInput {
@@ -1,90 +1,30 @@
1
- import type * as FundingRoutes from '../../internal/FundingRoutes.js'
1
+ import * as FundingRoutes from '../../internal/FundingRoutes.js'
2
2
 
3
3
  type Fetch = typeof globalThis.fetch
4
4
 
5
- const defaultAcrossBaseUrl = 'https://app.across.to/api'
5
+ const defaultFetch: Fetch = (input, init) => globalThis.fetch(input, init)
6
+
6
7
  const defaultRelayBaseUrl = 'https://api.relay.link'
7
8
 
8
9
  /** Creates the live funding route adapters configured for this deployment. */
9
10
  export function createAdapters(options: createAdapters.Options = {}) {
10
- return {
11
- ...(options.across ? { across: across(options.across) } : {}),
12
- ...(options.relay ? { relay: relay(options.relay) } : {}),
13
- } satisfies FundingRoutes.ProviderAdapters
11
+ return (
12
+ options.relay ? { relay: relay(options.relay) } : {}
13
+ ) satisfies FundingRoutes.ProviderAdapters
14
14
  }
15
15
 
16
16
  export declare namespace createAdapters {
17
17
  /** Provider adapter factory options. Omit a provider to keep its routes static. */
18
18
  type Options = {
19
- /** Across quote adapter options. */
20
- across?: across.Options | false | undefined
21
19
  /** Relay quote adapter options. */
22
20
  relay?: relay.Options | false | undefined
23
21
  }
24
22
  }
25
23
 
26
- /** Creates an Across quote adapter backed by `/suggested-fees`. */
27
- export function across(options: across.Options): FundingRoutes.ProviderAdapter {
28
- const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultAcrossBaseUrl)
29
- const fetcher = options.fetch ?? globalThis.fetch
30
- const now = options.now ?? (() => new Date())
31
-
32
- return {
33
- async probeQuote({ route }, signal) {
34
- const sourceChainId = providerChainId(route.sourceChain, 'across')
35
- const destinationChainId = eip155ChainId(route.destinationChain)
36
- const url = new URL(`${baseUrl}/suggested-fees`)
37
- url.search = new URLSearchParams({
38
- allowUnmatchedDecimals: 'true',
39
- amount: route.sourceAmount.amount,
40
- destinationChainId,
41
- inputToken: route.sourceToken.address,
42
- integratorId: options.integratorId,
43
- originChainId: sourceChainId,
44
- outputToken: route.destinationToken.address,
45
- }).toString()
46
-
47
- try {
48
- const body = await requestJson(url, {
49
- fetcher,
50
- headers: { Authorization: `Bearer ${options.apiKey}` },
51
- signal,
52
- })
53
- return formatAcrossResult(route, body, now())
54
- } catch (cause) {
55
- const unavailable = providerUnavailable(cause, ['AMOUNT_TOO_HIGH', 'AMOUNT_TOO_LOW'])
56
- if (!unavailable) throw cause
57
- return unavailableResult({
58
- detail: 'across:suggested-fees',
59
- message: unavailable,
60
- now: now(),
61
- source: 'providerLimit',
62
- })
63
- }
64
- },
65
- }
66
- }
67
-
68
- export declare namespace across {
69
- /** Across quote adapter options. */
70
- type Options = {
71
- /** Across API key sent as a bearer token. */
72
- apiKey: string
73
- /** Across integrator id sent as the required query parameter. */
74
- integratorId: string
75
- /** Override for tests or non-production Across environments. */
76
- baseUrl?: string | undefined
77
- /** Fetch implementation override for tests. */
78
- fetch?: Fetch | undefined
79
- /** Clock override for deterministic tests. */
80
- now?: (() => Date) | undefined
81
- }
82
- }
83
-
84
24
  /** Creates a Relay quote adapter backed by `/chains/liquidity` and `/quote/v2`. */
85
25
  export function relay(options: relay.Options): FundingRoutes.ProviderAdapter {
86
26
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultRelayBaseUrl)
87
- const fetcher = options.fetch ?? globalThis.fetch
27
+ const fetcher = options.fetch ?? defaultFetch
88
28
  const now = options.now ?? (() => new Date())
89
29
 
90
30
  return {
@@ -152,45 +92,6 @@ export declare namespace relay {
152
92
  }
153
93
  }
154
94
 
155
- function formatAcrossResult(
156
- route: FundingRoutes.RouteCandidate,
157
- body: unknown,
158
- now: Date,
159
- ): FundingRoutes.QuoteProbeResult {
160
- const response = record(body)
161
- const timestamp = integerString(response['timestamp'])
162
- const sampledAt = timestamp ? isoFromUnixSeconds(timestamp) : now.toISOString()
163
- const limits = acrossLimits(response['limits'])
164
- const amountStatus = statusFromLimits(route.sourceAmount.amount, limits)
165
- const status = response['isAmountTooLow'] === true ? 'unavailable' : amountStatus.status
166
- const tier = response['isAmountTooLow'] === true ? 'unavailable' : amountStatus.tier
167
- const outputAmount = decimalString(response['outputAmount'])
168
-
169
- return {
170
- expiresAt: timestamp ? isoFromUnixSeconds((BigInt(timestamp) + 600n).toString()) : null,
171
- limits,
172
- outputAmount,
173
- outputAmountFormatted: outputAmount
174
- ? decimalFromBaseUnits(outputAmount, route.destinationToken.decimals)
175
- : null,
176
- quality: {
177
- availableVolumeUsd: stablecoinUsdCapacity(route, limits?.maxSourceAmount),
178
- estimatedSeconds: nonnegativeInteger(response['estimatedFillTimeSec']),
179
- feeBps: scaledPctToBps(
180
- nestedString(response['totalRelayFee'], 'pct') ?? decimalString(response['relayFeePct']),
181
- ),
182
- historicalVolumeUsd24h: null,
183
- instantVolumeUsd: stablecoinUsdCapacity(route, limits?.instantSourceAmount),
184
- liquiditySource: 'providerLimit',
185
- score: scoreFor(tier),
186
- sourceDetail: 'across:suggested-fees',
187
- tier,
188
- },
189
- sampledAt,
190
- status,
191
- }
192
- }
193
-
194
95
  function formatRelayResult(
195
96
  route: FundingRoutes.RouteCandidate,
196
97
  quote: unknown,
@@ -262,31 +163,6 @@ function unavailableResult(options: {
262
163
  }
263
164
  }
264
165
 
265
- function acrossLimits(input: unknown): FundingRoutes.QuoteProbeLimits | null {
266
- const limits = record(input)
267
- const minSourceAmount = decimalString(limits['minDeposit'])
268
- const maxSourceAmount = decimalString(limits['maxDeposit'])
269
- const instantSourceAmount =
270
- decimalString(limits['maxDepositInstant']) ?? decimalString(limits['recommendedDepositInstant'])
271
- return minSourceAmount || maxSourceAmount || instantSourceAmount
272
- ? { instantSourceAmount, maxSourceAmount, minSourceAmount }
273
- : null
274
- }
275
-
276
- function statusFromLimits(
277
- amount: string,
278
- limits: FundingRoutes.QuoteProbeLimits | null,
279
- ): { status: FundingRoutes.RouteStatus; tier: FundingRoutes.QualityTier } {
280
- if (!limits) return { status: 'available', tier: 'liquid' }
281
- if (limits.minSourceAmount && compareIntegerStrings(amount, limits.minSourceAmount) < 0)
282
- return { status: 'unavailable', tier: 'unavailable' }
283
- if (limits.maxSourceAmount && compareIntegerStrings(amount, limits.maxSourceAmount) > 0)
284
- return { status: 'unavailable', tier: 'unavailable' }
285
- if (limits.instantSourceAmount && compareIntegerStrings(amount, limits.instantSourceAmount) > 0)
286
- return { status: 'thin', tier: 'thin' }
287
- return { status: 'available', tier: 'liquid' }
288
- }
289
-
290
166
  function relayLiquidity(input: unknown, route: FundingRoutes.RouteCandidate) {
291
167
  const response = record(input)
292
168
  const rows = Array.isArray(response['liquidity']) ? response['liquidity'] : []
@@ -333,12 +209,17 @@ function relayHeaders(apiKey: string | undefined): Record<string, string> | unde
333
209
  function providerUnavailable(cause: unknown, codes: readonly string[]) {
334
210
  if (!(cause instanceof ProviderResponseError) || cause.status < 400 || cause.status >= 500)
335
211
  return undefined
336
- const body = record(cause.body)
337
- const code = stringValue(body['code']) ?? stringValue(body['errorCode'])
212
+ const code = providerErrorCode(cause.body)
338
213
  if (code && codes.includes(code)) return code
339
214
  return undefined
340
215
  }
341
216
 
217
+ function providerErrorCode(input: unknown) {
218
+ const body = record(input)
219
+ const code = stringValue(body['code']) ?? stringValue(body['errorCode'])
220
+ return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
221
+ }
222
+
342
223
  class ProviderResponseError extends Error {
343
224
  override name = 'FundingRouteProviders.ProviderResponseError'
344
225
 
@@ -406,10 +287,6 @@ function integerString(input: unknown) {
406
287
  return undefined
407
288
  }
408
289
 
409
- function nestedString(input: unknown, key: string) {
410
- return decimalString(record(input)[key])
411
- }
412
-
413
290
  function nonnegativeInteger(input: unknown) {
414
291
  if (typeof input === 'number' && Number.isSafeInteger(input) && input >= 0) return input
415
292
  if (typeof input === 'string' && /^\d+$/.test(input)) return Number(input)
@@ -427,30 +304,6 @@ function compareIntegerStrings(left: string, right: string) {
427
304
  return BigInt(left) === BigInt(right) ? 0 : BigInt(left) > BigInt(right) ? 1 : -1
428
305
  }
429
306
 
430
- function isoFromUnixSeconds(input: string) {
431
- return new Date(Number(input) * 1_000).toISOString()
432
- }
433
-
434
- function stablecoinUsdCapacity(
435
- route: FundingRoutes.RouteCandidate,
436
- baseUnits: string | null | undefined,
437
- ) {
438
- if (!baseUnits || !isDollarStablecoin(route.sourceToken.symbol)) return null
439
- return decimalFromBaseUnits(baseUnits, route.sourceToken.decimals)
440
- }
441
-
442
- function isDollarStablecoin(symbol: string) {
443
- return ['DAI', 'FRXUSD', 'PYUSD', 'USDC', 'USDC.E', 'USDT', 'USDT0'].includes(
444
- symbol.toUpperCase(),
445
- )
446
- }
447
-
448
- function scaledPctToBps(input: string | undefined) {
449
- if (!input || !/^\d+$/.test(input)) return null
450
- const denominator = 1_000_000_000_000_000_000n
451
- return Number((BigInt(input) * 10_000n + denominator / 2n) / denominator)
452
- }
453
-
454
307
  function scoreFor(tier: FundingRoutes.QualityTier) {
455
308
  if (tier === 'liquid') return 90
456
309
  if (tier === 'thin') return 65
@@ -19,16 +19,11 @@ describe('GET /funding/routes', () => {
19
19
  expect(response.status).toBe(200)
20
20
  const body = await TestApp.json(response, Funding.schema.listFundingRoutes.Response)
21
21
 
22
- expect(body.data.map((route) => route.provider.id)).toEqual([
23
- 'across',
24
- 'relay',
25
- 'bridge',
26
- 'bungee',
27
- ])
22
+ expect(body.data.map((route) => route.provider.id)).toEqual(['relay', 'bridge', 'bungee'])
28
23
  expect(body.data[0]).toMatchObject({
29
24
  destinationChain: { id: 'eip155:4217', name: 'Tempo' },
30
25
  destinationToken: { symbol: 'USDC.e' },
31
- id: 'base-usdc-tempo-usdce-across',
26
+ id: 'base-usdc-tempo-usdce-relay',
32
27
  quality: { liquiditySource: 'staticInventory', tier: 'unknown' },
33
28
  sourceAmount: { amount: '1000000', formatted: '1' },
34
29
  sourceChain: { id: 'eip155:8453', name: 'Base' },
@@ -41,12 +36,12 @@ describe('GET /funding/routes', () => {
41
36
  const app = TestApp.create({
42
37
  fundingRoutes: {
43
38
  adapters: {
44
- across: {
39
+ relay: {
45
40
  async probeQuote(input, signal) {
46
41
  expect(signal.aborted).toBe(false)
47
42
  expect(input.cache.bypass).toBe(true)
48
43
  expect(input.include).toEqual(['quotes'])
49
- expect(input.route.id).toBe('base-usdc-tempo-usdce-across')
44
+ expect(input.route.id).toBe('base-usdc-tempo-usdce-relay')
50
45
  expect(input.sourceAmount.amount).toBe('1000000')
51
46
  expect(input.sourceChain.id).toBe('eip155:8453')
52
47
  expect(input.sourceToken.symbol).toBe('USDC')
@@ -81,7 +76,7 @@ describe('GET /funding/routes', () => {
81
76
  })
82
77
 
83
78
  const response = await app.request(
84
- '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000&provider=across&include=quotes',
79
+ '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000&provider=relay&include=quotes',
85
80
  { headers: { ...TestApp.auth.headers, 'Cache-Control': 'no-cache' } },
86
81
  )
87
82
  expect(response.status).toBe(200)
@@ -90,7 +85,7 @@ describe('GET /funding/routes', () => {
90
85
  expect(body.data).toHaveLength(1)
91
86
  expect(body.data[0]).toMatchObject({
92
87
  errorCode: null,
93
- id: 'base-usdc-tempo-usdce-across',
88
+ id: 'base-usdc-tempo-usdce-relay',
94
89
  quality: {
95
90
  availableVolumeUsd: '1000.00',
96
91
  liquiditySource: 'providerQuote',
@@ -261,13 +256,7 @@ describe('GET /providers', () => {
261
256
  expect(response.status).toBe(200)
262
257
  const body = await TestApp.json(response, Funding.schema.listProviders.Response)
263
258
 
264
- expect(body.data.map((provider) => provider.id)).toEqual([
265
- 'across',
266
- 'relay',
267
- 'bridge',
268
- 'bungee',
269
- 'lifi',
270
- ])
259
+ expect(body.data.map((provider) => provider.id)).toEqual(['relay', 'bridge', 'bungee', 'lifi'])
271
260
  expect(body.data.every((provider) => provider.capabilities.includes('fundingRoutes'))).toBe(
272
261
  true,
273
262
  )
@@ -275,34 +264,10 @@ describe('GET /providers', () => {
275
264
  'inventoryOnly',
276
265
  'inventoryOnly',
277
266
  'inventoryOnly',
278
- 'inventoryOnly',
279
267
  'planned',
280
268
  ])
281
269
  })
282
270
 
283
- test('marks configured live provider adapters', async () => {
284
- const app = TestApp.create({
285
- fundingRoutes: {
286
- adapters: {
287
- across: {
288
- async probeQuote() {
289
- throw new Error('not used')
290
- },
291
- },
292
- },
293
- },
294
- })
295
-
296
- const response = await app.request('/v1/providers?capability=fundingRoutes', {
297
- headers: TestApp.auth.headers,
298
- })
299
- expect(response.status).toBe(200)
300
- const body = await TestApp.json(response, Funding.schema.listProviders.Response)
301
-
302
- expect(body.data.find((provider) => provider.id === 'across')?.status).toBe('configured')
303
- expect(body.data.find((provider) => provider.id === 'relay')?.status).toBe('inventoryOnly')
304
- })
305
-
306
271
  test('rejects unsupported provider capabilities', async () => {
307
272
  const response = await TestApp.create().request('/v1/providers?capability=settlement', {
308
273
  headers: TestApp.auth.headers,