tapimo 0.4.4 → 0.5.0
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/dist/App.d.ts +7403 -6127
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +4 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +24 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +115 -115
- package/dist/apps/data/App.d.ts +1838 -1534
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +135 -44
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/{FundingRouteProviders.d.ts → FundingQuoteProviders.d.ts} +7 -7
- package/dist/apps/data/{FundingRouteProviders.d.ts.map → FundingQuoteProviders.d.ts.map} +1 -1
- package/dist/apps/data/{FundingRouteProviders.js → FundingQuoteProviders.js} +36 -91
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -1
- package/dist/apps/data/index.js +1 -1
- package/dist/apps/data/routes/activities.d.ts +242 -242
- package/dist/apps/data/routes/addresses.d.ts +227 -227
- package/dist/apps/data/routes/balances.d.ts +12 -12
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +67 -149
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +125 -126
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +3 -5
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +55 -54
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +3 -8
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +207 -388
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +143 -221
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +86 -16
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +33 -33
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts +58 -58
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/data/routes/webhooks.d.ts +340 -0
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +13 -1
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/management/App.d.ts +1090 -108
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +4 -0
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +118 -118
- package/dist/apps/management/routes/api-keys.js +2 -2
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +207 -207
- package/dist/apps/management/routes/invite-links.d.ts +1100 -0
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
- package/dist/apps/management/routes/invite-links.js +411 -0
- package/dist/apps/management/routes/invite-links.js.map +1 -0
- package/dist/apps/management/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/members.d.ts +71 -71
- package/dist/apps/management/routes/orgs.d.ts +12 -12
- package/dist/apps/management/routes/projects.d.ts +70 -70
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +75 -75
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +42 -4
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +3 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +57 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +31 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +31 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +42 -0
- package/dist/db/tables/inviteLinks.d.ts.map +1 -0
- package/dist/db/tables/inviteLinks.js +148 -0
- package/dist/db/tables/inviteLinks.js.map +1 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +2 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +13 -3
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +9 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +130 -10
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +3 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +17 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +185 -428
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -218
- package/dist/internal/FundingQuotes.js.map +1 -0
- package/dist/internal/Viem.d.ts +3 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +11 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/package.json +1 -1
- package/src/ApiKeys.test.ts +23 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.ts +6 -0
- package/src/Client.test-d.ts +20 -11
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +30 -4
- package/src/apps/data/App.test.ts +354 -0
- package/src/apps/data/App.ts +159 -52
- package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
- package/src/apps/data/{FundingRouteProviders.ts → FundingQuoteProviders.ts} +40 -101
- package/src/apps/data/index.ts +1 -1
- package/src/apps/data/routes/balances.test.ts +64 -18
- package/src/apps/data/routes/balances.ts +76 -177
- package/src/apps/data/routes/blocks.ts +3 -5
- package/src/apps/data/routes/coingecko.ts +3 -8
- package/src/apps/data/routes/funding.test.ts +151 -206
- package/src/apps/data/routes/funding.ts +156 -291
- package/src/apps/data/routes/indexer.test.ts +97 -1
- package/src/apps/data/routes/rpc.test.ts +219 -0
- package/src/apps/data/routes/rpc.ts +92 -17
- package/src/apps/data/routes/webhooks.test.ts +21 -0
- package/src/apps/data/routes/webhooks.ts +13 -1
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +4 -0
- package/src/apps/management/routes/api-keys.test.ts +47 -3
- package/src/apps/management/routes/api-keys.ts +2 -2
- package/src/apps/management/routes/invite-links.test.ts +385 -0
- package/src/apps/management/routes/invite-links.ts +570 -0
- package/src/apps/management/routes/scopes.test.ts +3 -2
- package/src/apps/management/routes/scopes.ts +4 -2
- package/src/apps/relay/App.test.ts +324 -4
- package/src/apps/relay/App.ts +40 -4
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.ts +2 -0
- package/src/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +132 -22
- package/src/internal/Auth.ts +171 -14
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +174 -0
- package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +153 -312
- package/src/internal/Viem.ts +14 -5
- package/src/internal/index.test.ts +1 -1
- package/src/internal/index.ts +1 -1
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts.map +0 -1
- package/dist/internal/FundingRoutes.js.map +0 -1
- package/src/apps/data/FundingRouteProviders.test.ts +0 -225
- package/src/internal/FundingRoutes.test.ts +0 -213
package/src/internal/Auth.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { matchedRoutes } from 'hono/route'
|
|
|
7
7
|
import type { MergePath, RouterRoute, Schema as core_Schema } from 'hono/types'
|
|
8
8
|
import { findTargetHandler } from 'hono/utils/handler'
|
|
9
9
|
import { decodeJwt } from 'jose'
|
|
10
|
-
import { Credential, type Method } from 'mppx'
|
|
10
|
+
import { Challenge, Credential, type Method } from 'mppx'
|
|
11
11
|
import { Mppx, tempo } from 'mppx/hono'
|
|
12
12
|
import { Addresses } from 'viem/tempo'
|
|
13
13
|
import * as z from 'zod/mini'
|
|
@@ -31,6 +31,7 @@ import * as Users from '../db/tables/users.js'
|
|
|
31
31
|
import * as Viem from './Viem.js'
|
|
32
32
|
|
|
33
33
|
const policySymbol = Symbol('tempo-api.auth.policy')
|
|
34
|
+
const mppManagementSymbol = Symbol('tempo-api.auth.mpp-management')
|
|
34
35
|
const defaultMppRateLimit = { perMinute: 100 } satisfies RateLimit.Limit
|
|
35
36
|
|
|
36
37
|
/** Auth context consumed by Tempo API handlers. */
|
|
@@ -112,6 +113,8 @@ type MppHandler = {
|
|
|
112
113
|
rateLimit?: RateLimit.Limit | undefined
|
|
113
114
|
/** Creates a Hono middleware for a Tempo Session payment. */
|
|
114
115
|
session(options: Mpp.Session): MiddlewareHandler
|
|
116
|
+
/** Resolves the chain from a challenge issued by this server. */
|
|
117
|
+
sessionChainId?: ((c: hono_Context) => number | undefined) | undefined
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
@@ -282,7 +285,7 @@ export declare namespace ensureOrg {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
/**
|
|
285
|
-
*
|
|
288
|
+
* Resolves `:projectId`, preserving nested organization scope. Sessions require membership; API keys require organization ownership and matching project attribution when present.
|
|
286
289
|
*/
|
|
287
290
|
export function ensureProject(): MiddlewareHandler {
|
|
288
291
|
// Typed loosely (like {@link policy}) so route chains keep their inferred
|
|
@@ -306,6 +309,12 @@ export function ensureProject(): MiddlewareHandler {
|
|
|
306
309
|
const db = Db.get(contextDb(c))
|
|
307
310
|
const record = await Projects.get(db, projectId)
|
|
308
311
|
if (!record) return notFound()
|
|
312
|
+
if (
|
|
313
|
+
principal.type === 'api_key' &&
|
|
314
|
+
principal.projectId !== undefined &&
|
|
315
|
+
principal.projectId !== record.id
|
|
316
|
+
)
|
|
317
|
+
return notFound()
|
|
309
318
|
const scoped = c.get('org')
|
|
310
319
|
if (scoped) {
|
|
311
320
|
if (record.orgId !== scoped.id) return notFound()
|
|
@@ -479,6 +488,7 @@ export function middleware<
|
|
|
479
488
|
session: session_ = {},
|
|
480
489
|
...mpp
|
|
481
490
|
} = options.mpp === false ? {} : (options.mpp ?? {})
|
|
491
|
+
const secretKey = resolveMppSecretKey(mpp.secretKey)
|
|
482
492
|
const defaults = laneDefaults(options)
|
|
483
493
|
// Default-deny: no lanes open until a route grants them via `policy`.
|
|
484
494
|
// Otherwise the `super_admin` principal bypasses lane checks entirely.
|
|
@@ -520,6 +530,7 @@ export function middleware<
|
|
|
520
530
|
rateLimit: mppRateLimit,
|
|
521
531
|
session: Mppx.create({
|
|
522
532
|
...mpp,
|
|
533
|
+
...(secretKey ? { secretKey } : {}),
|
|
523
534
|
// Defaults only; global `session` options may override, including
|
|
524
535
|
// enabling same-route HEAD bootstrap.
|
|
525
536
|
methods: [
|
|
@@ -532,6 +543,7 @@ export function middleware<
|
|
|
532
543
|
}),
|
|
533
544
|
],
|
|
534
545
|
}).session,
|
|
546
|
+
sessionChainId: (c) => paymentSessionChainId(c, secretKey),
|
|
535
547
|
},
|
|
536
548
|
}),
|
|
537
549
|
rateLimit: RateLimit.memory(options.rateLimit),
|
|
@@ -545,17 +557,19 @@ export function middleware<
|
|
|
545
557
|
const route = openApiRoute(c)
|
|
546
558
|
if (!route) return next()
|
|
547
559
|
|
|
548
|
-
const { key, description } = route
|
|
560
|
+
const { key, description, management, scope } = route
|
|
549
561
|
const overrides = options.overrides as
|
|
550
562
|
| Record<string, PolicyOverride | false | undefined>
|
|
551
563
|
| undefined
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
description,
|
|
564
|
+
const configuredOverride = overrides?.[key]
|
|
565
|
+
const routeOverride =
|
|
566
|
+
management && configuredOverride ? { mpp: configuredOverride.mpp } : configuredOverride
|
|
567
|
+
const endpointPolicy = bindMppScope(
|
|
568
|
+
resolvePolicy(base, defaults, [...policies(c), routeOverride], description),
|
|
569
|
+
scope,
|
|
557
570
|
)
|
|
558
571
|
if (!endpointPolicy) return next()
|
|
572
|
+
if (management && (!auth.mpp || !endpointPolicy.mpp)) return next()
|
|
559
573
|
|
|
560
574
|
c.set('auth', auth)
|
|
561
575
|
return require<environment>(endpointPolicy)(c, next)
|
|
@@ -1002,6 +1016,55 @@ export function policy(options: PolicyOverride): MiddlewareHandler {
|
|
|
1002
1016
|
return middleware
|
|
1003
1017
|
}
|
|
1004
1018
|
|
|
1019
|
+
/** Adds management POSTs for OpenAPI-registered MPP GET resources. */
|
|
1020
|
+
export function installMppManagementRoutes(
|
|
1021
|
+
app: Hono<any, any>,
|
|
1022
|
+
options: MppManagementOptions = {},
|
|
1023
|
+
) {
|
|
1024
|
+
const routes = [...app.routes]
|
|
1025
|
+
const postPaths = new Set(
|
|
1026
|
+
routes.filter((route) => route.method === 'POST').map((route) => route.path),
|
|
1027
|
+
)
|
|
1028
|
+
const targets = new Map<string, MppManagementTarget>()
|
|
1029
|
+
|
|
1030
|
+
for (const route of routes) {
|
|
1031
|
+
if (route.method !== 'GET') continue
|
|
1032
|
+
const target = targets.get(route.path) ?? { path: route.path }
|
|
1033
|
+
const override = policyFromRoute(route)
|
|
1034
|
+
if (override?.mpp !== undefined)
|
|
1035
|
+
target.mpp = resolveMppLane(target.mpp, override.mpp, { session: {} })
|
|
1036
|
+
|
|
1037
|
+
const spec = openApiSpec(route) as MppManagementSpec | undefined
|
|
1038
|
+
if (spec) {
|
|
1039
|
+
target.registered = true
|
|
1040
|
+
const description = typeof spec.summary === 'string' ? spec.summary : spec.description
|
|
1041
|
+
if (typeof description === 'string') target.description = description
|
|
1042
|
+
}
|
|
1043
|
+
targets.set(route.path, target)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
for (const target of targets.values()) {
|
|
1047
|
+
const key = `GET ${stripBasePath(normalizeRoutePath(target.path), options.basePath)}`
|
|
1048
|
+
const override = options.overrides?.[key]
|
|
1049
|
+
if (override === false) continue
|
|
1050
|
+
const mpp =
|
|
1051
|
+
override?.mpp === undefined
|
|
1052
|
+
? target.mpp
|
|
1053
|
+
: resolveMppLane(target.mpp, override.mpp, { session: {} })
|
|
1054
|
+
if (!target.registered || !mpp) continue
|
|
1055
|
+
if (postPaths.has(target.path))
|
|
1056
|
+
throw new Error(`MPP management POST conflicts with an existing route: ${target.path}`)
|
|
1057
|
+
|
|
1058
|
+
const management = policy({ mpp }) as MiddlewareHandler & MppManagementHandler
|
|
1059
|
+
management[mppManagementSymbol] = { description: target.description }
|
|
1060
|
+
app.post(
|
|
1061
|
+
stripBasePath(target.path, options.basePath),
|
|
1062
|
+
management,
|
|
1063
|
+
OpenApi.describeRoute({ hide: true }),
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1005
1068
|
export declare namespace endpoint {
|
|
1006
1069
|
/**
|
|
1007
1070
|
* HTTP endpoint key inferred from a Hono app schema. Distributes over the
|
|
@@ -1048,12 +1111,35 @@ type OpenApiHandler = {
|
|
|
1048
1111
|
type OpenApiRoute = {
|
|
1049
1112
|
key: string
|
|
1050
1113
|
description: string | undefined
|
|
1114
|
+
management: boolean
|
|
1115
|
+
scope: string
|
|
1051
1116
|
}
|
|
1052
1117
|
|
|
1053
1118
|
type PolicyHandler = {
|
|
1054
1119
|
[policySymbol]?: PolicyOverride | undefined
|
|
1055
1120
|
}
|
|
1056
1121
|
|
|
1122
|
+
type MppManagementHandler = {
|
|
1123
|
+
[mppManagementSymbol]?: { description?: string | undefined } | undefined
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
type MppManagementOptions = {
|
|
1127
|
+
basePath?: string | undefined
|
|
1128
|
+
overrides?: Record<string, PolicyOverride | false | undefined> | undefined
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
type MppManagementSpec = {
|
|
1132
|
+
description?: unknown
|
|
1133
|
+
summary?: unknown
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
type MppManagementTarget = {
|
|
1137
|
+
description?: string | undefined
|
|
1138
|
+
mpp?: NonNullable<require.Policy['mpp']> | undefined
|
|
1139
|
+
path: string
|
|
1140
|
+
registered?: boolean | undefined
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1057
1143
|
function laneDefaults(options: middleware.Options): LaneDefaults {
|
|
1058
1144
|
const apiKey = options.apiKey === false ? undefined : options.apiKey
|
|
1059
1145
|
const public_ = options.public === false ? undefined : options.public
|
|
@@ -1112,6 +1198,17 @@ function resolvePolicy(
|
|
|
1112
1198
|
}
|
|
1113
1199
|
}
|
|
1114
1200
|
|
|
1201
|
+
function bindMppScope(policy: require.Policy | undefined, scope: string) {
|
|
1202
|
+
if (!policy?.mpp || policy.mpp.session.scope !== undefined) return policy
|
|
1203
|
+
return {
|
|
1204
|
+
...policy,
|
|
1205
|
+
mpp: {
|
|
1206
|
+
...policy.mpp,
|
|
1207
|
+
session: { ...policy.mpp.session, scope },
|
|
1208
|
+
},
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1115
1212
|
function resolveApiKeyLane(
|
|
1116
1213
|
current: require.Policy['apiKey'],
|
|
1117
1214
|
override: PolicyOverride['apiKey'],
|
|
@@ -1165,9 +1262,19 @@ function resolveSessionLane(
|
|
|
1165
1262
|
}
|
|
1166
1263
|
|
|
1167
1264
|
function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1265
|
+
const routes = matchedRoutes(c).slice(c.req.routeIndex + 1)
|
|
1266
|
+
const managementRoute = routes.find((route) => !!mppManagementFromRoute(route))
|
|
1267
|
+
if (managementRoute) {
|
|
1268
|
+
const management = mppManagementFromRoute(managementRoute)!
|
|
1269
|
+
return {
|
|
1270
|
+
key: endpointKey(c, managementRoute, 'GET'),
|
|
1271
|
+
description: management.description,
|
|
1272
|
+
management: true,
|
|
1273
|
+
scope: `GET ${managementRoute.path}`,
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const route = routes.find((route) => !!openApiSpec(route))
|
|
1171
1278
|
if (!route) return undefined
|
|
1172
1279
|
|
|
1173
1280
|
const spec = openApiSpec(route)
|
|
@@ -1177,6 +1284,8 @@ function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
|
|
|
1177
1284
|
return {
|
|
1178
1285
|
key: endpointKey(c, route),
|
|
1179
1286
|
description: typeof description === 'string' ? description : undefined,
|
|
1287
|
+
management: false,
|
|
1288
|
+
scope: `${route.method.toUpperCase()} ${route.path}`,
|
|
1180
1289
|
}
|
|
1181
1290
|
}
|
|
1182
1291
|
|
|
@@ -1199,6 +1308,11 @@ function policyFromRoute(route: RouterRoute) {
|
|
|
1199
1308
|
return handler[policySymbol]
|
|
1200
1309
|
}
|
|
1201
1310
|
|
|
1311
|
+
function mppManagementFromRoute(route: RouterRoute) {
|
|
1312
|
+
const handler = findTargetHandler(route.handler) as MppManagementHandler
|
|
1313
|
+
return handler[mppManagementSymbol]
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1202
1316
|
/** Resolved access lanes for one documented operation. */
|
|
1203
1317
|
export type OperationAccess = {
|
|
1204
1318
|
/** Whether API-key access is allowed. */
|
|
@@ -1253,8 +1367,8 @@ export function describeAccess(app: Hono<any, any>): Record<string, OperationAcc
|
|
|
1253
1367
|
return access
|
|
1254
1368
|
}
|
|
1255
1369
|
|
|
1256
|
-
function endpointKey(c: hono_Context, route: RouterRoute) {
|
|
1257
|
-
return `${
|
|
1370
|
+
function endpointKey(c: hono_Context, route: RouterRoute, method = route.method) {
|
|
1371
|
+
return `${method.toUpperCase()} ${stripBasePath(normalizeRoutePath(route.path), basePath(c))}`
|
|
1258
1372
|
}
|
|
1259
1373
|
|
|
1260
1374
|
function normalizeRoutePath(path: string) {
|
|
@@ -1695,6 +1809,40 @@ function hasPaymentCredential(c: hono_Context) {
|
|
|
1695
1809
|
return !!Credential.extractPaymentScheme(authorization)
|
|
1696
1810
|
}
|
|
1697
1811
|
|
|
1812
|
+
function paymentSessionChainId(c: hono_Context, secretKey: string | undefined): number | undefined {
|
|
1813
|
+
if (!secretKey) return undefined
|
|
1814
|
+
try {
|
|
1815
|
+
const credential = Credential.fromRequest(c.req.raw)
|
|
1816
|
+
if (!Challenge.verify(credential.challenge, { secretKey })) return undefined
|
|
1817
|
+
if (credential.challenge.method !== 'tempo' || credential.challenge.intent !== 'session')
|
|
1818
|
+
return undefined
|
|
1819
|
+
const details = credential.challenge.request['methodDetails']
|
|
1820
|
+
if (!details || typeof details !== 'object') return undefined
|
|
1821
|
+
const chainId = (details as Record<string, unknown>)['chainId']
|
|
1822
|
+
return typeof chainId === 'number' ? chainId : undefined
|
|
1823
|
+
} catch {
|
|
1824
|
+
return undefined
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function resolveMppSecretKey(explicit: string | undefined): string | undefined {
|
|
1829
|
+
if (explicit) return explicit
|
|
1830
|
+
try {
|
|
1831
|
+
const value = typeof process === 'undefined' ? undefined : process.env['MPP_SECRET_KEY']
|
|
1832
|
+
if (value) return value
|
|
1833
|
+
} catch {}
|
|
1834
|
+
try {
|
|
1835
|
+
const deno = (
|
|
1836
|
+
globalThis as typeof globalThis & {
|
|
1837
|
+
Deno?: { env?: { get?: (name: string) => string | undefined } | undefined } | undefined
|
|
1838
|
+
}
|
|
1839
|
+
).Deno
|
|
1840
|
+
return deno?.env?.get?.('MPP_SECRET_KEY') || undefined
|
|
1841
|
+
} catch {
|
|
1842
|
+
return undefined
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1698
1846
|
function isPaymentEnabled(auth: Context, policy: require.Policy) {
|
|
1699
1847
|
return !!auth.mpp && !!policy.mpp
|
|
1700
1848
|
}
|
|
@@ -1773,7 +1921,16 @@ async function requirePayment<environment extends Environment>(
|
|
|
1773
1921
|
}
|
|
1774
1922
|
}
|
|
1775
1923
|
|
|
1776
|
-
const
|
|
1924
|
+
const session = options.policy.mpp?.session ?? {}
|
|
1925
|
+
const chainId =
|
|
1926
|
+
explicitChainId(c) ??
|
|
1927
|
+
(c.req.method === 'POST' && session.scope?.startsWith('GET ')
|
|
1928
|
+
? options.auth.mpp?.sessionChainId?.(c)
|
|
1929
|
+
: undefined)
|
|
1930
|
+
const middleware = options.auth.mpp?.session({
|
|
1931
|
+
...session,
|
|
1932
|
+
...(chainId === undefined ? {} : { chainId }),
|
|
1933
|
+
})
|
|
1777
1934
|
if (!middleware)
|
|
1778
1935
|
return jsonError(c, {
|
|
1779
1936
|
code: 'payment_required',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hono } from 'hono'
|
|
1
|
+
import { type Context, Hono } from 'hono'
|
|
2
2
|
import { createMiddleware } from 'hono/factory'
|
|
3
3
|
import * as EdgeCache from './EdgeCache.js'
|
|
4
4
|
import type * as Store from './Store.js'
|
|
@@ -189,6 +189,19 @@ describe('middleware', () => {
|
|
|
189
189
|
|
|
190
190
|
expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
|
|
191
191
|
})
|
|
192
|
+
|
|
193
|
+
test('rechecks route eligibility before every cache lookup', async () => {
|
|
194
|
+
let eligible = true
|
|
195
|
+
const { app, downstreamCalls, store } = createApp({ eligibility: () => eligible })
|
|
196
|
+
|
|
197
|
+
await app.request('/cached')
|
|
198
|
+
eligible = false
|
|
199
|
+
await app.request('/cached')
|
|
200
|
+
|
|
201
|
+
expect(downstreamCalls()).toBe(2)
|
|
202
|
+
expect(store.gets).toBe(1)
|
|
203
|
+
expect(store.size).toBe(1)
|
|
204
|
+
})
|
|
192
205
|
})
|
|
193
206
|
|
|
194
207
|
type Environment = { Variables: EdgeCache.Variables }
|
|
@@ -221,7 +234,12 @@ function memoryStore() {
|
|
|
221
234
|
} satisfies Store.Store & { readonly gets: number; readonly size: number }
|
|
222
235
|
}
|
|
223
236
|
|
|
224
|
-
function createApp(
|
|
237
|
+
function createApp(
|
|
238
|
+
options: {
|
|
239
|
+
eligibility?: ((c: Context<Environment>) => boolean) | undefined
|
|
240
|
+
store?: Store.Store | undefined
|
|
241
|
+
} = {},
|
|
242
|
+
) {
|
|
225
243
|
const store = memoryStore()
|
|
226
244
|
const edgeStore = 'store' in options ? options.store : store
|
|
227
245
|
let downstream = 0
|
|
@@ -234,9 +252,12 @@ function createApp(options: { store?: Store.Store | undefined } = {}) {
|
|
|
234
252
|
})
|
|
235
253
|
// Routes publishing a policy mount a marked middleware, as `Cache.response`
|
|
236
254
|
// routes do; unmarked routes must skip the lookup entirely.
|
|
237
|
-
const
|
|
255
|
+
const cacheable_marked = EdgeCache.markCacheable(
|
|
238
256
|
createMiddleware<Environment>(async (_c, next) => next()),
|
|
239
257
|
)
|
|
258
|
+
const cacheable = options.eligibility
|
|
259
|
+
? EdgeCache.setEligibility(cacheable_marked, options.eligibility)
|
|
260
|
+
: cacheable_marked
|
|
240
261
|
app.get('/cached', cacheable, (c) => {
|
|
241
262
|
c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
|
|
242
263
|
c.header('Age', '123')
|
|
@@ -135,6 +135,7 @@ export function middleware<environment extends Env>(
|
|
|
135
135
|
// Marks handlers whose route publishes an edge-cache policy; the middleware
|
|
136
136
|
// pays the store lookup only when a matched handler carries it.
|
|
137
137
|
const cacheableSymbol = Symbol('tempoApi.edgeCacheable')
|
|
138
|
+
const eligibility = new WeakMap<object, (c: Context) => boolean>()
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* Marks a route handler/middleware as publishing an edge-cache policy, so
|
|
@@ -147,9 +148,24 @@ export function markCacheable<const handler extends (...args: never[]) => unknow
|
|
|
147
148
|
return Object.assign(handler, { [cacheableSymbol]: true })
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
/** Adds a request eligibility check to a cacheable route handler. */
|
|
152
|
+
export function setEligibility<const handler extends (...args: never[]) => unknown>(
|
|
153
|
+
handler: handler,
|
|
154
|
+
predicate: (c: Context) => boolean,
|
|
155
|
+
): handler {
|
|
156
|
+
if (cacheableSymbol in handler) eligibility.set(handler, predicate)
|
|
157
|
+
return handler
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// True when a marked handler matches and every marked handler permits caching.
|
|
151
161
|
function isCacheableRoute(c: Context): boolean {
|
|
152
|
-
|
|
162
|
+
let cacheable = false
|
|
163
|
+
for (const route of matchedRoutes(c)) {
|
|
164
|
+
if (!(cacheableSymbol in route.handler)) continue
|
|
165
|
+
cacheable = true
|
|
166
|
+
if (eligibility.get(route.handler)?.(c) === false) return false
|
|
167
|
+
}
|
|
168
|
+
return cacheable
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
export declare namespace middleware {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as FundingQuotes from './FundingQuotes.js'
|
|
2
|
+
|
|
3
|
+
const baseUsdcRequest = {
|
|
4
|
+
provider: 'relay',
|
|
5
|
+
sourceAmount: '1000000',
|
|
6
|
+
sourceAmountUnits: 'baseUnits',
|
|
7
|
+
sourceChain: 'base',
|
|
8
|
+
sourceToken: 'USDC',
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
describe('listQuotes', () => {
|
|
12
|
+
test('returns normalized successful provider quotes', async () => {
|
|
13
|
+
let seen: FundingQuotes.QuoteProbeInput | undefined
|
|
14
|
+
const quotes = await FundingQuotes.listQuotes({
|
|
15
|
+
...baseUsdcRequest,
|
|
16
|
+
adapters: {
|
|
17
|
+
relay: {
|
|
18
|
+
async probeQuote(input, signal) {
|
|
19
|
+
seen = input
|
|
20
|
+
expect(signal.aborted).toBe(false)
|
|
21
|
+
return {
|
|
22
|
+
minimumOutputAmount: '980000',
|
|
23
|
+
outputAmount: '990000',
|
|
24
|
+
quality: {
|
|
25
|
+
estimatedSeconds: 4,
|
|
26
|
+
liquiditySource: 'providerQuote',
|
|
27
|
+
sourceDetail: 'providerEndpoint',
|
|
28
|
+
tier: 'liquid',
|
|
29
|
+
},
|
|
30
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
31
|
+
status: 'available',
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(seen?.candidate.id).toBe('base-usdc-tempo-usdce-relay')
|
|
39
|
+
expect(quotes).toHaveLength(1)
|
|
40
|
+
expect(quotes[0]).toMatchObject({
|
|
41
|
+
id: 'base-usdc-tempo-usdce-relay',
|
|
42
|
+
quality: {
|
|
43
|
+
liquiditySource: 'providerQuote',
|
|
44
|
+
tier: 'liquid',
|
|
45
|
+
},
|
|
46
|
+
quote: {
|
|
47
|
+
inputAmount: '1000000',
|
|
48
|
+
minimumOutputAmount: '980000',
|
|
49
|
+
outputAmount: '990000',
|
|
50
|
+
outputAmountFormatted: '0.99',
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns no static candidates when no adapter is configured', async () => {
|
|
56
|
+
await expect(FundingQuotes.listQuotes(baseUsdcRequest)).resolves.toEqual([])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('omits provider-declared unavailable quotes', async () => {
|
|
60
|
+
const quotes = FundingQuotes.listQuotes({
|
|
61
|
+
...baseUsdcRequest,
|
|
62
|
+
adapters: {
|
|
63
|
+
relay: {
|
|
64
|
+
async probeQuote() {
|
|
65
|
+
return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await expect(quotes).resolves.toEqual([])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('rejects provider failures when no quote succeeds', async () => {
|
|
75
|
+
const quotes = FundingQuotes.listQuotes({
|
|
76
|
+
...baseUsdcRequest,
|
|
77
|
+
adapters: {
|
|
78
|
+
relay: {
|
|
79
|
+
async probeQuote() {
|
|
80
|
+
throw new Error('upstream unavailable')
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
87
|
+
`[FundingQuotes.ProviderQuoteError]`,
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('rejects successful provider responses without output amounts', async () => {
|
|
92
|
+
const quotes = FundingQuotes.listQuotes({
|
|
93
|
+
...baseUsdcRequest,
|
|
94
|
+
adapters: {
|
|
95
|
+
relay: {
|
|
96
|
+
async probeQuote() {
|
|
97
|
+
return { sampledAt: '2026-01-01T00:00:00Z', status: 'available' }
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
104
|
+
`[FundingQuotes.ProviderQuoteError]`,
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('aborts providers that exceed the quote deadline', async () => {
|
|
109
|
+
let aborted = false
|
|
110
|
+
const quotes = FundingQuotes.listQuotes({
|
|
111
|
+
...baseUsdcRequest,
|
|
112
|
+
adapterTimeoutMs: 1,
|
|
113
|
+
adapters: {
|
|
114
|
+
relay: {
|
|
115
|
+
probeQuote(_input, signal) {
|
|
116
|
+
signal.addEventListener('abort', () => {
|
|
117
|
+
aborted = true
|
|
118
|
+
})
|
|
119
|
+
return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
126
|
+
`[FundingQuotes.ProviderQuoteError]`,
|
|
127
|
+
)
|
|
128
|
+
expect(aborted).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('passes cache policy through to providers', async () => {
|
|
132
|
+
let seen: FundingQuotes.QuoteProbeInput | undefined
|
|
133
|
+
await FundingQuotes.listQuotes({
|
|
134
|
+
...baseUsdcRequest,
|
|
135
|
+
adapters: {
|
|
136
|
+
relay: {
|
|
137
|
+
async probeQuote(input) {
|
|
138
|
+
seen = input
|
|
139
|
+
return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
cache: { bypass: true },
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(seen?.cache.bypass).toBe(true)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('listChains', () => {
|
|
151
|
+
test('returns only chain and token inputs with quote inventory', () => {
|
|
152
|
+
expect(FundingQuotes.listChains().map((chain) => chain.id)).toEqual([
|
|
153
|
+
'eip155:1',
|
|
154
|
+
'eip155:8453',
|
|
155
|
+
'eip155:56',
|
|
156
|
+
])
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('listProviders', () => {
|
|
161
|
+
test('reports deployment configuration', () => {
|
|
162
|
+
expect(FundingQuotes.listProviders()).toMatchInlineSnapshot(`
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
"id": "relay",
|
|
166
|
+
"kind": "solverNetwork",
|
|
167
|
+
"name": "Relay",
|
|
168
|
+
"requiresApiKey": false,
|
|
169
|
+
"status": "unconfigured",
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
`)
|
|
173
|
+
})
|
|
174
|
+
})
|