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.
- package/README.md +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +2451 -1533
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1354 -984
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +20 -3
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +196 -5
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +453 -453
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +730 -138
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +234 -0
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
- package/dist/apps/management/App.d.ts +1156 -615
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +106 -90
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +278 -146
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +13 -13
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +130 -70
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +137 -48
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +14 -28
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +35 -3
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/handlers/relay.d.ts +13 -10
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +46 -16
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +13 -18
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +78 -58
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/FundingRoutes.d.ts +170 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +68 -6
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.js.map +1 -1
- package/package.json +2 -2
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +44 -2
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +64 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/App.ts +24 -3
- package/src/apps/data/FundingRouteProviders.test.ts +204 -0
- package/src/apps/data/FundingRouteProviders.ts +294 -9
- package/src/apps/data/routes/funding.test.ts +90 -23
- package/src/apps/data/routes/funding.ts +315 -0
- package/src/apps/data/routes/rpc.test.ts +33 -0
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +25 -1
- package/src/apps/mcp.ts +43 -3
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/handlers/relay.test-d.ts +8 -0
- package/src/handlers/relay.test.ts +2 -2
- package/src/handlers/relay.ts +62 -22
- package/src/internal/Auth.test.ts +174 -7
- package/src/internal/Auth.ts +90 -62
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +26 -0
- package/src/internal/FundingRoutes.ts +187 -18
- package/src/internal/Log.test.ts +41 -3
- 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
|
|
46
|
-
|
|
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
|
|
package/src/apps/data/App.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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 =
|
|
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>,
|