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
|
@@ -81,6 +81,53 @@ export function listByOrg(db: Db.Db, orgId: string): Promise<Record[]> {
|
|
|
81
81
|
.execute()
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Lists a bounded page of an organization's projects, newest first.
|
|
86
|
+
*
|
|
87
|
+
* @param db - The database.
|
|
88
|
+
* @param orgId - The organization id.
|
|
89
|
+
* @param options - Keyset and page-size options.
|
|
90
|
+
* @returns At most `limit + 1` rows for next-cursor derivation.
|
|
91
|
+
*/
|
|
92
|
+
export function listByOrgPage(
|
|
93
|
+
db: Db.Db,
|
|
94
|
+
orgId: string,
|
|
95
|
+
options: listByOrgPage.Options,
|
|
96
|
+
): Promise<Record[]> {
|
|
97
|
+
const cursor = options.cursor
|
|
98
|
+
let query = db.kysely.selectFrom('projects').selectAll().where('orgId', '=', orgId)
|
|
99
|
+
if (cursor)
|
|
100
|
+
query = query.where((eb) =>
|
|
101
|
+
eb.or([
|
|
102
|
+
eb('createdAt', '<', cursor.createdAt),
|
|
103
|
+
eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
|
|
104
|
+
]),
|
|
105
|
+
)
|
|
106
|
+
return query
|
|
107
|
+
.orderBy('createdAt', 'desc')
|
|
108
|
+
.orderBy('id', 'desc')
|
|
109
|
+
.limit(options.limit + 1)
|
|
110
|
+
.execute()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export declare namespace listByOrgPage {
|
|
114
|
+
/** Cursor fields for the last project on the previous page. */
|
|
115
|
+
type Cursor = {
|
|
116
|
+
/** Project creation time. */
|
|
117
|
+
createdAt: string
|
|
118
|
+
/** Project id, used as a deterministic tie-breaker. */
|
|
119
|
+
id: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Options for {@link listByOrgPage}. */
|
|
123
|
+
type Options = {
|
|
124
|
+
/** Last project returned by the previous page. */
|
|
125
|
+
cursor?: Cursor | undefined
|
|
126
|
+
/** Requested page size. */
|
|
127
|
+
limit: number
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
84
131
|
/**
|
|
85
132
|
* Renames a project, bumping `updatedAt`.
|
|
86
133
|
*
|
|
@@ -10,6 +10,12 @@ export type Table = db_Schema.WebhookDelivery
|
|
|
10
10
|
/** A stored webhook-delivery row. */
|
|
11
11
|
export type Record = Selectable<Table>
|
|
12
12
|
|
|
13
|
+
/** A failed delivery joined to its organization-owned subscription. */
|
|
14
|
+
export type OrganizationFailure = Pick<
|
|
15
|
+
Record,
|
|
16
|
+
'attempt' | 'createdAt' | 'error' | 'id' | 'responseStatus' | 'subscriptionId'
|
|
17
|
+
>
|
|
18
|
+
|
|
13
19
|
/** Maps a stored row to the domain delivery (null columns → absent fields). */
|
|
14
20
|
export function toDelivery(row: Record): Webhooks.Delivery {
|
|
15
21
|
return {
|
|
@@ -85,6 +91,83 @@ export async function list(
|
|
|
85
91
|
return (await query.execute()).map(toDelivery)
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
/** Lists recent failed webhook deliveries across an organization. */
|
|
95
|
+
export function listFailuresByOrg(
|
|
96
|
+
db: Db.Db,
|
|
97
|
+
orgId: string,
|
|
98
|
+
options: listFailuresByOrg.Options,
|
|
99
|
+
): Promise<OrganizationFailure[]> {
|
|
100
|
+
return db.kysely
|
|
101
|
+
.selectFrom('webhook_deliveries')
|
|
102
|
+
.innerJoin(
|
|
103
|
+
'webhook_subscriptions',
|
|
104
|
+
'webhook_subscriptions.id',
|
|
105
|
+
'webhook_deliveries.subscriptionId',
|
|
106
|
+
)
|
|
107
|
+
.select([
|
|
108
|
+
'webhook_deliveries.attempt',
|
|
109
|
+
'webhook_deliveries.createdAt',
|
|
110
|
+
'webhook_deliveries.error',
|
|
111
|
+
'webhook_deliveries.id',
|
|
112
|
+
'webhook_deliveries.responseStatus',
|
|
113
|
+
'webhook_deliveries.subscriptionId',
|
|
114
|
+
])
|
|
115
|
+
.where('webhook_subscriptions.ownerId', '=', orgId)
|
|
116
|
+
.where('webhook_subscriptions.ownerType', '=', 'api_key')
|
|
117
|
+
.where('webhook_deliveries.createdAt', '>=', options.from)
|
|
118
|
+
.where('webhook_deliveries.expiresAt', '>', options.now)
|
|
119
|
+
.where('webhook_deliveries.status', '=', 'failed')
|
|
120
|
+
.orderBy('webhook_deliveries.createdAt', 'desc')
|
|
121
|
+
.orderBy('webhook_deliveries.id', 'desc')
|
|
122
|
+
.limit(options.limit)
|
|
123
|
+
.execute()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export declare namespace listFailuresByOrg {
|
|
127
|
+
/** Bounds for the organization failure scan. */
|
|
128
|
+
type Options = {
|
|
129
|
+
/** Window start (ISO 8601), inclusive. */
|
|
130
|
+
from: string
|
|
131
|
+
/** Maximum failures to return. */
|
|
132
|
+
limit: number
|
|
133
|
+
/** ISO timestamp retention is evaluated against. */
|
|
134
|
+
now: string
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Counts failed webhook deliveries across an organization in a time window. */
|
|
139
|
+
export async function countFailuresByOrg(
|
|
140
|
+
db: Db.Db,
|
|
141
|
+
orgId: string,
|
|
142
|
+
options: countFailuresByOrg.Options,
|
|
143
|
+
): Promise<number> {
|
|
144
|
+
const row = await db.kysely
|
|
145
|
+
.selectFrom('webhook_deliveries')
|
|
146
|
+
.innerJoin(
|
|
147
|
+
'webhook_subscriptions',
|
|
148
|
+
'webhook_subscriptions.id',
|
|
149
|
+
'webhook_deliveries.subscriptionId',
|
|
150
|
+
)
|
|
151
|
+
.select((eb) => eb.fn.countAll().as('count'))
|
|
152
|
+
.where('webhook_subscriptions.ownerId', '=', orgId)
|
|
153
|
+
.where('webhook_subscriptions.ownerType', '=', 'api_key')
|
|
154
|
+
.where('webhook_deliveries.createdAt', '>=', options.from)
|
|
155
|
+
.where('webhook_deliveries.expiresAt', '>', options.now)
|
|
156
|
+
.where('webhook_deliveries.status', '=', 'failed')
|
|
157
|
+
.executeTakeFirstOrThrow()
|
|
158
|
+
return Number(row.count)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export declare namespace countFailuresByOrg {
|
|
162
|
+
/** Bounds for the organization failure count. */
|
|
163
|
+
type Options = {
|
|
164
|
+
/** Window start (ISO 8601), inclusive. */
|
|
165
|
+
from: string
|
|
166
|
+
/** ISO timestamp retention is evaluated against. */
|
|
167
|
+
now: string
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
88
171
|
export declare namespace list {
|
|
89
172
|
/** Options for {@link list}. */
|
|
90
173
|
type Options = {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Handler } from 'tapimo'
|
|
2
|
+
|
|
3
|
+
describe('Handler.relay', () => {
|
|
4
|
+
test('accepts Tempo API defaults', () => {
|
|
5
|
+
expectTypeOf(Handler.relay()).toEqualTypeOf<Handler.Handler>()
|
|
6
|
+
expectTypeOf(Handler.relay({ apiKey: 'secret_api_key' })).toEqualTypeOf<Handler.Handler>()
|
|
7
|
+
})
|
|
8
|
+
})
|
|
@@ -16,8 +16,8 @@ const recipient = Relay.accounts[7]!
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Tokens the relay handler probes for fee-token resolution. The default
|
|
19
|
-
* `resolveTokens`
|
|
20
|
-
*
|
|
19
|
+
* `resolveTokens` uses the Tempo API for mainnet and testnet, so localnet
|
|
20
|
+
* tests inject this list explicitly.
|
|
21
21
|
*/
|
|
22
22
|
const localnetTokens = [
|
|
23
23
|
{
|
package/src/handlers/relay.ts
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
import type { LocalAccount } from 'viem/accounts'
|
|
17
17
|
import { simulateCalls } from 'viem/actions'
|
|
18
18
|
import { Abis, Actions, Addresses, Capabilities, VirtualAddress } from 'viem/tempo'
|
|
19
|
+
import { tempoMainnet, tempoTestnet } from 'viem/tempo/chains'
|
|
19
20
|
|
|
20
21
|
import * as ExecutionError from './internal/executionError.js'
|
|
22
|
+
import * as TempoApi from '../Client.js'
|
|
21
23
|
import type * as RpcSchema from '../internal/RpcSchema.js'
|
|
22
24
|
import { type Handler, from } from '../Handler.js'
|
|
23
25
|
import * as Store from '../internal/Store.js'
|
|
@@ -25,6 +27,8 @@ import * as Multisig from './internal/multisig.js'
|
|
|
25
27
|
import * as Sponsorship from './internal/sponsorship.js'
|
|
26
28
|
import * as Utils from './internal/utils.js'
|
|
27
29
|
|
|
30
|
+
const defaultChains = [tempoMainnet, tempoTestnet] as const
|
|
31
|
+
|
|
28
32
|
/**
|
|
29
33
|
* Instantiates a relay handler that proxies `eth_fillTransaction`
|
|
30
34
|
* with wallet-aware enrichment (fee token resolution, simulation,
|
|
@@ -32,9 +36,9 @@ import * as Utils from './internal/utils.js'
|
|
|
32
36
|
*
|
|
33
37
|
* @example
|
|
34
38
|
* ```ts
|
|
35
|
-
* import { Handler } from '
|
|
39
|
+
* import { Handler } from 'tapimo'
|
|
36
40
|
*
|
|
37
|
-
* const handler = Handler.relay({
|
|
41
|
+
* const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY })
|
|
38
42
|
*
|
|
39
43
|
* // Plug handler into your server framework of choice:
|
|
40
44
|
* createServer(handler.listener) // Node.js
|
|
@@ -51,10 +55,9 @@ import * as Utils from './internal/utils.js'
|
|
|
51
55
|
*
|
|
52
56
|
* ```ts
|
|
53
57
|
* import { privateKeyToAccount } from 'viem/accounts'
|
|
54
|
-
* import { Handler } from '
|
|
58
|
+
* import { Handler } from 'tapimo'
|
|
55
59
|
*
|
|
56
60
|
* const handler = Handler.relay({
|
|
57
|
-
* getClient,
|
|
58
61
|
* feePayer: {
|
|
59
62
|
* account: privateKeyToAccount('0x...'),
|
|
60
63
|
* },
|
|
@@ -64,26 +67,26 @@ import * as Utils from './internal/utils.js'
|
|
|
64
67
|
* @param options - Options.
|
|
65
68
|
* @returns Request handler.
|
|
66
69
|
*/
|
|
67
|
-
export function relay(options: relay.Options): Handler {
|
|
70
|
+
export function relay(options: relay.Options = {}): Handler {
|
|
68
71
|
const {
|
|
69
|
-
|
|
72
|
+
apiKey,
|
|
73
|
+
getClient: getClient_,
|
|
70
74
|
internal_allowUnsafeUrls = false,
|
|
71
75
|
onRequest,
|
|
72
76
|
path = '/',
|
|
73
|
-
resolveTokens,
|
|
77
|
+
resolveTokens: resolveTokens_,
|
|
74
78
|
...rest
|
|
75
79
|
} = options
|
|
80
|
+
const getClient = getClient_ ?? defaultGetClient({ apiKey })
|
|
81
|
+
const resolveTokens = resolveTokens_ ?? defaultResolveTokens({ apiKey })
|
|
76
82
|
const feePayerOptions = options.feePayer
|
|
77
83
|
// Adapt the state store into the multisig operation store once, up front.
|
|
78
84
|
const multisigOptions = options.multisig
|
|
79
85
|
? { ...options.multisig, store: Multisig.fromStore(options.multisig.store) }
|
|
80
86
|
: undefined
|
|
81
87
|
|
|
82
|
-
// Resolves the chain's fee-token candidates through the caller-provided
|
|
83
|
-
// resolver. An unconfigured resolver (or a chain it has no list for) yields
|
|
84
|
-
// an empty candidate set.
|
|
85
88
|
const getTokens = async (chainId: number | undefined): Promise<readonly Address[]> => {
|
|
86
|
-
if (
|
|
89
|
+
if (chainId === undefined) return []
|
|
87
90
|
return await resolveTokens(chainId)
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -94,7 +97,7 @@ export function relay(options: relay.Options): Handler {
|
|
|
94
97
|
|
|
95
98
|
const features = {
|
|
96
99
|
autoSwap: options.autoSwap ?? options.features === 'all',
|
|
97
|
-
feeTokenResolution: options.features === 'all' || Boolean(
|
|
100
|
+
feeTokenResolution: options.features === 'all' || Boolean(resolveTokens_),
|
|
98
101
|
simulate: options.features === 'all',
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -175,6 +178,7 @@ export function relay(options: relay.Options): Handler {
|
|
|
175
178
|
let filled: Awaited<ReturnType<typeof fill>>
|
|
176
179
|
let sponsored = false
|
|
177
180
|
let feeToken = sponsoredFeeToken
|
|
181
|
+
const tokens = getTokens(chainId)
|
|
178
182
|
|
|
179
183
|
// Lazily resolve a swap source token when autoSwap needs one.
|
|
180
184
|
const resolveFeeTokenForSwap = from
|
|
@@ -183,7 +187,7 @@ export function relay(options: relay.Options): Handler {
|
|
|
183
187
|
account: from,
|
|
184
188
|
feeToken: undefined,
|
|
185
189
|
store,
|
|
186
|
-
tokens: (await
|
|
190
|
+
tokens: (await tokens).filter(
|
|
187
191
|
(t) => t.toLowerCase() !== insufficientToken.toLowerCase(),
|
|
188
192
|
),
|
|
189
193
|
})
|
|
@@ -194,7 +198,7 @@ export function relay(options: relay.Options): Handler {
|
|
|
194
198
|
// the transaction is calling — typically the token being
|
|
195
199
|
// transferred — so a user transferring USDC.e can pay gas in
|
|
196
200
|
// USDC.e even when the configured list defaults to pathUSD.
|
|
197
|
-
const configuredTokens = await
|
|
201
|
+
const configuredTokens = await tokens
|
|
198
202
|
const unsponsoredTokens = [
|
|
199
203
|
...configuredTokens,
|
|
200
204
|
...callTargetTokens(baseTx).filter(
|
|
@@ -221,7 +225,7 @@ export function relay(options: relay.Options): Handler {
|
|
|
221
225
|
// `validate` only governs the wallet's own fee payer).
|
|
222
226
|
// Default to the chain's first token so the broadcast envelope
|
|
223
227
|
// always carries a feeToken the chain can charge.
|
|
224
|
-
if (!feeToken) feeToken =
|
|
228
|
+
if (!feeToken) feeToken = configuredTokens[0]
|
|
225
229
|
const transaction = {
|
|
226
230
|
...baseTx,
|
|
227
231
|
feePayer: true,
|
|
@@ -334,8 +338,7 @@ export function relay(options: relay.Options): Handler {
|
|
|
334
338
|
const transaction_filled = filled.transaction
|
|
335
339
|
const swap = 'swap' in filled ? filled.swap : undefined
|
|
336
340
|
if (!feeToken)
|
|
337
|
-
feeToken =
|
|
338
|
-
(transaction_filled.feeToken as Address | undefined) ?? (await getTokens(chainId))[0]
|
|
341
|
+
feeToken = (transaction_filled.feeToken as Address | undefined) ?? configuredTokens[0]
|
|
339
342
|
|
|
340
343
|
// Parallelize: simulate, fee payer signing, and autoSwap metadata.
|
|
341
344
|
const alreadySigned =
|
|
@@ -648,8 +651,46 @@ export function relay(options: relay.Options): Handler {
|
|
|
648
651
|
return router
|
|
649
652
|
}
|
|
650
653
|
|
|
654
|
+
function defaultGetClient(options: { apiKey?: string | undefined }) {
|
|
655
|
+
const clients = new Map<number, Client>()
|
|
656
|
+
for (const chain of defaultChains) {
|
|
657
|
+
const transport = options.apiKey
|
|
658
|
+
? http(new URL(`/rpc/${chain.id}`, TempoApi.defaultUrl).toString(), {
|
|
659
|
+
fetchOptions: { headers: { 'tempo-api-key': options.apiKey } },
|
|
660
|
+
})
|
|
661
|
+
: http()
|
|
662
|
+
clients.set(
|
|
663
|
+
chain.id,
|
|
664
|
+
createClient({ batch: { multicall: { deployless: true } }, chain, transport }),
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return (chainId: number = tempoMainnet.id): Client => {
|
|
669
|
+
const client = clients.get(chainId)
|
|
670
|
+
if (!client) throw new Error(`Chain ${chainId} not configured`)
|
|
671
|
+
return client
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function defaultResolveTokens(options: { apiKey?: string | undefined }) {
|
|
676
|
+
const client = TempoApi.create({ apiKey: options.apiKey })
|
|
677
|
+
|
|
678
|
+
return async (chainId: number): Promise<readonly Address[]> => {
|
|
679
|
+
if (!defaultChains.some((chain) => chain.id === chainId)) return []
|
|
680
|
+
const response = await client.v1.tokenlist.$get({ query: { chainId: String(chainId) } })
|
|
681
|
+
if (response.status !== 200) return []
|
|
682
|
+
const { tokens } = await response.json()
|
|
683
|
+
return tokens.map((token) => token.address)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
651
687
|
export namespace relay {
|
|
652
688
|
export type Options = from.Options & {
|
|
689
|
+
/**
|
|
690
|
+
* Tempo API key used by the default RPC clients and token resolver. When
|
|
691
|
+
* omitted, RPC clients use each chain's default transport.
|
|
692
|
+
*/
|
|
693
|
+
apiKey?: string | undefined
|
|
653
694
|
/**
|
|
654
695
|
* Auto-swap options.
|
|
655
696
|
*/
|
|
@@ -663,9 +704,9 @@ export namespace relay {
|
|
|
663
704
|
/**
|
|
664
705
|
* Resolves a viem client for a chain id. The handler resolves the client
|
|
665
706
|
* from the `chainId` in the incoming transaction (falling back to the
|
|
666
|
-
* client's default chain).
|
|
707
|
+
* client's default chain). Defaults to Tempo mainnet and testnet clients.
|
|
667
708
|
*/
|
|
668
|
-
getClient
|
|
709
|
+
getClient?: ((chainId?: number) => Client) | undefined
|
|
669
710
|
/**
|
|
670
711
|
* Fee payer / sponsor configuration. When provided, the relay will
|
|
671
712
|
* sign `feePayerSignature` on the filled transaction.
|
|
@@ -721,9 +762,8 @@ export namespace relay {
|
|
|
721
762
|
/** Path to use for the handler. @default "/" */
|
|
722
763
|
path?: string | undefined
|
|
723
764
|
/**
|
|
724
|
-
* Resolves the known fee-token candidates for a chain.
|
|
725
|
-
*
|
|
726
|
-
* fee-token resolution. Omit to disable fee-token resolution.
|
|
765
|
+
* Resolves the known fee-token candidates for a chain. Defaults to the
|
|
766
|
+
* Tempo API verified token list for mainnet and testnet.
|
|
727
767
|
*/
|
|
728
768
|
resolveTokens?:
|
|
729
769
|
| ((chainId: number) => readonly Address[] | Promise<readonly Address[]>)
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { Hono } from 'hono'
|
|
1
|
+
import { Hono, type Context } from 'hono'
|
|
2
2
|
import { requestId } from 'hono/request-id'
|
|
3
3
|
import { Mppx, tempo } from 'mppx/hono'
|
|
4
4
|
import { RateLimit } from 'tapimo'
|
|
5
5
|
import * as ApiKey from '../ApiKey.js'
|
|
6
|
+
import type * as Db from '../db/Db.js'
|
|
7
|
+
import * as Memberships from '../db/tables/memberships.js'
|
|
8
|
+
import * as Organizations from '../db/tables/organizations.js'
|
|
9
|
+
import * as Projects from '../db/tables/projects.js'
|
|
10
|
+
import * as Users from '../db/tables/users.js'
|
|
6
11
|
import * as Auth from './Auth.js'
|
|
7
12
|
import * as Log from './Log.js'
|
|
8
13
|
import * as Scope from '../Scope.js'
|
|
@@ -75,12 +80,13 @@ describe('require', () => {
|
|
|
75
80
|
`)
|
|
76
81
|
})
|
|
77
82
|
|
|
78
|
-
test('accepts API key from
|
|
83
|
+
test('accepts API key from key query param', async () => {
|
|
79
84
|
const app = createApp()
|
|
80
85
|
const response = await app.request(`/protected?key=${token}`)
|
|
81
86
|
const body = await response.json()
|
|
82
87
|
|
|
83
88
|
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
89
|
+
expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
|
|
84
90
|
expect(body).toMatchInlineSnapshot(`
|
|
85
91
|
{
|
|
86
92
|
"id": "key_test",
|
|
@@ -90,11 +96,30 @@ describe('require', () => {
|
|
|
90
96
|
`)
|
|
91
97
|
})
|
|
92
98
|
|
|
93
|
-
test('
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
test('keeps query-authenticated responses private after downstream caching', async () => {
|
|
100
|
+
const auth = {
|
|
101
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
102
|
+
rateLimit: RateLimit.memory(),
|
|
103
|
+
}
|
|
104
|
+
const app = new Hono<Auth.Environment>()
|
|
105
|
+
app.use('*', async (c, next) => {
|
|
106
|
+
c.set('auth', auth)
|
|
107
|
+
await next()
|
|
108
|
+
})
|
|
109
|
+
app.use('/cached', Auth.require({ apiKey: { scopes: ['data:read'] } }))
|
|
110
|
+
app.get('/cached', (c) => {
|
|
111
|
+
c.header('Cache-Control', 'public, max-age=86400')
|
|
112
|
+
return c.json({ ok: true })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const response = await app.request(`/cached?key=${token}`)
|
|
116
|
+
|
|
117
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
118
|
+
expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"private"`)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('strips the key query param before route validation', async () => {
|
|
122
|
+
// Auth owns `key`, so strict route schemas must not see it.
|
|
98
123
|
const auth = {
|
|
99
124
|
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
100
125
|
rateLimit: RateLimit.memory(),
|
|
@@ -1298,6 +1323,87 @@ describe('require', () => {
|
|
|
1298
1323
|
})
|
|
1299
1324
|
})
|
|
1300
1325
|
|
|
1326
|
+
describe('ensureProject', () => {
|
|
1327
|
+
test('authorizes a flat project route against the API key organization', async () => {
|
|
1328
|
+
const db = TestApp.database()
|
|
1329
|
+
const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
|
|
1330
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1331
|
+
const response = await createProjectGuardApp(db, apiKeyPrincipal(organization.id)).request(
|
|
1332
|
+
`/v1/projects/${project.id}/resource`,
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
1336
|
+
expect(await response.json()).toEqual({
|
|
1337
|
+
membership: null,
|
|
1338
|
+
orgId: organization.id,
|
|
1339
|
+
projectId: project.id,
|
|
1340
|
+
})
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
test('hides a flat project from an API key in another organization', async () => {
|
|
1344
|
+
const db = TestApp.database()
|
|
1345
|
+
const organization = await Organizations.create(db, { id: key.orgId, name: 'Test org' })
|
|
1346
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1347
|
+
const response = await createProjectGuardApp(db, apiKeyPrincipal('org_foreign')).request(
|
|
1348
|
+
`/v1/projects/${project.id}/resource`,
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
expect(response.status).toMatchInlineSnapshot(`404`)
|
|
1352
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
1353
|
+
{
|
|
1354
|
+
"error": {
|
|
1355
|
+
"code": "project_not_found",
|
|
1356
|
+
"message": "Project not found",
|
|
1357
|
+
},
|
|
1358
|
+
}
|
|
1359
|
+
`)
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
test('resolves session membership for a flat project route', async () => {
|
|
1363
|
+
const db = TestApp.database()
|
|
1364
|
+
const user = await Users.upsertByAddress(db, {
|
|
1365
|
+
address: '0x0000000000000000000000000000000000000001',
|
|
1366
|
+
})
|
|
1367
|
+
const organization = await Organizations.create(db, { name: 'Test org' })
|
|
1368
|
+
await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
|
|
1369
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1370
|
+
const response = await createProjectGuardApp(db, sessionPrincipal(user.id)).request(
|
|
1371
|
+
`/v1/projects/${project.id}/resource`,
|
|
1372
|
+
)
|
|
1373
|
+
const body = (await response.json()) as Record<string, unknown>
|
|
1374
|
+
|
|
1375
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
1376
|
+
expect(body).toEqual({
|
|
1377
|
+
membership: 'member',
|
|
1378
|
+
orgId: organization.id,
|
|
1379
|
+
projectId: project.id,
|
|
1380
|
+
})
|
|
1381
|
+
})
|
|
1382
|
+
|
|
1383
|
+
test('preserves nested organization role authorization', async () => {
|
|
1384
|
+
const db = TestApp.database()
|
|
1385
|
+
const user = await Users.upsertByAddress(db, {
|
|
1386
|
+
address: '0x0000000000000000000000000000000000000001',
|
|
1387
|
+
})
|
|
1388
|
+
const organization = await Organizations.create(db, { name: 'Test org' })
|
|
1389
|
+
await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
|
|
1390
|
+
const project = await Projects.create(db, { name: 'Test project', orgId: organization.id })
|
|
1391
|
+
const response = await createNestedProjectGuardApp(db, sessionPrincipal(user.id)).request(
|
|
1392
|
+
`/v1/orgs/${organization.id}/projects/${project.id}/resource`,
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
expect(response.status).toMatchInlineSnapshot(`403`)
|
|
1396
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
1397
|
+
{
|
|
1398
|
+
"error": {
|
|
1399
|
+
"code": "forbidden",
|
|
1400
|
+
"message": "Requires the admin role",
|
|
1401
|
+
},
|
|
1402
|
+
}
|
|
1403
|
+
`)
|
|
1404
|
+
})
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1301
1407
|
function createScopedApp(options: {
|
|
1302
1408
|
auth?: Auth.Context | undefined
|
|
1303
1409
|
routes: readonly { path: string; policy: Auth.require.Policy }[]
|
|
@@ -1322,6 +1428,67 @@ function createScopedApp(options: {
|
|
|
1322
1428
|
return app
|
|
1323
1429
|
}
|
|
1324
1430
|
|
|
1431
|
+
type GuardEnvironment = {
|
|
1432
|
+
Variables: Auth.Variables & { db: Db.Source }
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function createProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
|
|
1436
|
+
const app = new Hono<GuardEnvironment>()
|
|
1437
|
+
app.use('*', async (c, next) => {
|
|
1438
|
+
c.set('db', db)
|
|
1439
|
+
c.set('principal', principal)
|
|
1440
|
+
await next()
|
|
1441
|
+
})
|
|
1442
|
+
app.get('/v1/projects/:projectId/resource', Auth.ensureProject(), projectGuardHandler)
|
|
1443
|
+
return app
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function createNestedProjectGuardApp(db: Db.Source, principal: Auth.Principal) {
|
|
1447
|
+
const app = new Hono<GuardEnvironment>()
|
|
1448
|
+
app.use('*', async (c, next) => {
|
|
1449
|
+
c.set('db', db)
|
|
1450
|
+
c.set('principal', principal)
|
|
1451
|
+
await next()
|
|
1452
|
+
})
|
|
1453
|
+
app.get(
|
|
1454
|
+
'/v1/orgs/:orgId/projects/:projectId/resource',
|
|
1455
|
+
Auth.ensureOrg({ role: 'admin' }),
|
|
1456
|
+
Auth.ensureProject(),
|
|
1457
|
+
projectGuardHandler,
|
|
1458
|
+
)
|
|
1459
|
+
return app
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function projectGuardHandler(c: Context<GuardEnvironment>) {
|
|
1463
|
+
return c.json({
|
|
1464
|
+
membership: Auth.membership(c)?.role ?? null,
|
|
1465
|
+
orgId: Auth.org(c).id,
|
|
1466
|
+
projectId: Auth.project(c).id,
|
|
1467
|
+
})
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function apiKeyPrincipal(orgId: string): Auth.Principal {
|
|
1471
|
+
const apiKey = { ...key, orgId }
|
|
1472
|
+
return {
|
|
1473
|
+
apiKey,
|
|
1474
|
+
environment: apiKey.environment,
|
|
1475
|
+
id: apiKey.id,
|
|
1476
|
+
orgId,
|
|
1477
|
+
type: 'api_key',
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function sessionPrincipal(id: string): Auth.Principal {
|
|
1482
|
+
return {
|
|
1483
|
+
id,
|
|
1484
|
+
identity: {
|
|
1485
|
+
provider: 'wallet',
|
|
1486
|
+
subject: '0x0000000000000000000000000000000000000001',
|
|
1487
|
+
},
|
|
1488
|
+
type: 'session',
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1325
1492
|
type Options = {
|
|
1326
1493
|
auth?: Auth.Context | undefined
|
|
1327
1494
|
logger?: Log.Emit | undefined
|