tapimo 0.4.3 → 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 +7222 -6316
- 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 +116 -116
- package/dist/apps/data/App.d.ts +2541 -2607
- 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/FundingQuoteProviders.d.ts +36 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingQuoteProviders.js +190 -0
- 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/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +92 -92
- package/dist/apps/data/routes/funding.d.ts +91 -864
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +138 -450
- 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 +36 -36
- 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 +1843 -861
- 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 +121 -121
- 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 +2 -2
- 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 +13 -13
- package/dist/apps/management/routes/members.d.ts +77 -77
- package/dist/apps/management/routes/orgs.d.ts +96 -96
- package/dist/apps/management/routes/projects.d.ts +62 -62
- 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 +79 -79
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +3 -1
- package/dist/apps/mcp.js.map +1 -1
- 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/cloudflare.d.ts +2 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +4 -0
- package/dist/cloudflare.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 +42 -23
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +169 -20
- 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/Errors.d.ts +6 -0
- package/dist/internal/Errors.d.ts.map +1 -0
- package/dist/internal/Errors.js +16 -0
- package/dist/internal/Errors.js.map +1 -0
- package/dist/internal/FundingQuotes.d.ts +644 -0
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -289
- package/dist/internal/FundingQuotes.js.map +1 -0
- package/dist/internal/Store.d.ts +61 -16
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +72 -17
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +2 -0
- 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 +27 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +27 -0
- package/dist/internal/index.js.map +1 -0
- package/package.json +8 -3
- package/src/ApiKeys.test.ts +23 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.test.ts +60 -4
- package/src/App.ts +6 -0
- package/src/Client.test-d.ts +16 -54
- 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/FundingQuoteProviders.ts +251 -0
- 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 +149 -306
- package/src/apps/data/routes/funding.ts +154 -604
- package/src/apps/data/routes/indexer.test.ts +99 -3
- 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 +23 -2
- 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/mcp.test.ts +55 -14
- package/src/apps/mcp.ts +3 -1
- package/src/apps/relay/App.test.ts +324 -4
- package/src/apps/relay/App.ts +40 -4
- package/src/cloudflare.ts +5 -0
- 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 +528 -37
- package/src/internal/Auth.ts +246 -45
- 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/FundingQuotes.ts +735 -0
- package/src/internal/Log.test.ts +4 -4
- package/src/internal/Store.test-d.ts +15 -0
- package/src/internal/Store.test.ts +153 -2
- package/src/internal/Store.ts +157 -41
- package/src/internal/Viem.ts +14 -5
- package/src/internal/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
- package/dist/apps/data/FundingRouteProviders.d.ts +0 -143
- package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingRouteProviders.js +0 -546
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts +0 -1085
- 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 -485
- package/src/apps/data/FundingRouteProviders.ts +0 -744
- package/src/internal/FundingRoutes.test.ts +0 -239
- package/src/internal/FundingRoutes.ts +0 -1067
package/src/internal/Auth.ts
CHANGED
|
@@ -7,8 +7,9 @@ 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
|
+
import { Addresses } from 'viem/tempo'
|
|
12
13
|
import * as z from 'zod/mini'
|
|
13
14
|
|
|
14
15
|
import type * as App from '../App.js'
|
|
@@ -30,6 +31,8 @@ import * as Users from '../db/tables/users.js'
|
|
|
30
31
|
import * as Viem from './Viem.js'
|
|
31
32
|
|
|
32
33
|
const policySymbol = Symbol('tempo-api.auth.policy')
|
|
34
|
+
const mppManagementSymbol = Symbol('tempo-api.auth.mpp-management')
|
|
35
|
+
const defaultMppRateLimit = { perMinute: 100 } satisfies RateLimit.Limit
|
|
33
36
|
|
|
34
37
|
/** Auth context consumed by Tempo API handlers. */
|
|
35
38
|
export type Context = {
|
|
@@ -47,7 +50,7 @@ export type Context = {
|
|
|
47
50
|
}
|
|
48
51
|
| undefined
|
|
49
52
|
/** MPP payment configuration. */
|
|
50
|
-
mpp?:
|
|
53
|
+
mpp?: MppHandler | undefined
|
|
51
54
|
/**
|
|
52
55
|
* Configured default anonymous public quota. Used as the throttle ceiling for
|
|
53
56
|
* sandbox keys without active billing on routes that expose no public lane
|
|
@@ -89,22 +92,29 @@ export type Payment = {
|
|
|
89
92
|
type: 'mpp'
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
/** MPP
|
|
93
|
-
export
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
/** MPP route-policy types. Distinct from the SIWE {@link Session} capability. */
|
|
96
|
+
export declare namespace Mpp {
|
|
97
|
+
/** MPP Session request options used by route access policies. */
|
|
98
|
+
type Session = Method.RequestDefaults<ReturnType<typeof tempo.session>> & {
|
|
99
|
+
/** Optional human-readable payment description. */
|
|
100
|
+
description?: string | undefined
|
|
101
|
+
/** Optional challenge expiration timestamp or date. */
|
|
102
|
+
expires?: Date | string | undefined
|
|
103
|
+
/** Optional server-defined correlation data. */
|
|
104
|
+
meta?: Record<string, string> | undefined
|
|
105
|
+
/** Optional route/resource scope bound to the challenge. */
|
|
106
|
+
scope?: string | undefined
|
|
107
|
+
}
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
/** MPP payment handler consumed by auth policies. */
|
|
105
|
-
type
|
|
106
|
-
/**
|
|
107
|
-
|
|
111
|
+
type MppHandler = {
|
|
112
|
+
/** Paid-request quota per access principal. */
|
|
113
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
114
|
+
/** Creates a Hono middleware for a Tempo Session payment. */
|
|
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
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
/**
|
|
@@ -127,11 +137,13 @@ export type PolicyOverride = {
|
|
|
127
137
|
}
|
|
128
138
|
| boolean
|
|
129
139
|
| undefined
|
|
130
|
-
/** MPP paid lane: object to override the
|
|
140
|
+
/** MPP paid lane: object to override the Session request, or boolean to enable/disable. */
|
|
131
141
|
mpp?:
|
|
132
142
|
| {
|
|
133
|
-
/**
|
|
134
|
-
|
|
143
|
+
/** Paid-request quota override for this route (a protective per-route cap). */
|
|
144
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
145
|
+
/** Tempo Session request override for this route. */
|
|
146
|
+
session?: Mpp.Session | undefined
|
|
135
147
|
}
|
|
136
148
|
| boolean
|
|
137
149
|
| undefined
|
|
@@ -273,7 +285,7 @@ export declare namespace ensureOrg {
|
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
/**
|
|
276
|
-
*
|
|
288
|
+
* Resolves `:projectId`, preserving nested organization scope. Sessions require membership; API keys require organization ownership and matching project attribution when present.
|
|
277
289
|
*/
|
|
278
290
|
export function ensureProject(): MiddlewareHandler {
|
|
279
291
|
// Typed loosely (like {@link policy}) so route chains keep their inferred
|
|
@@ -297,6 +309,12 @@ export function ensureProject(): MiddlewareHandler {
|
|
|
297
309
|
const db = Db.get(contextDb(c))
|
|
298
310
|
const record = await Projects.get(db, projectId)
|
|
299
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()
|
|
300
318
|
const scoped = c.get('org')
|
|
301
319
|
if (scoped) {
|
|
302
320
|
if (record.orgId !== scoped.id) return notFound()
|
|
@@ -465,7 +483,12 @@ export function middleware<
|
|
|
465
483
|
endpoint extends string = string,
|
|
466
484
|
environment extends Environment = Environment,
|
|
467
485
|
>(options: middleware.Options<endpoint>): MiddlewareHandler<environment> {
|
|
468
|
-
const {
|
|
486
|
+
const {
|
|
487
|
+
rateLimit: mppRateLimit = defaultMppRateLimit,
|
|
488
|
+
session: session_ = {},
|
|
489
|
+
...mpp
|
|
490
|
+
} = options.mpp === false ? {} : (options.mpp ?? {})
|
|
491
|
+
const secretKey = resolveMppSecretKey(mpp.secretKey)
|
|
469
492
|
const defaults = laneDefaults(options)
|
|
470
493
|
// Default-deny: no lanes open until a route grants them via `policy`.
|
|
471
494
|
// Otherwise the `super_admin` principal bypasses lane checks entirely.
|
|
@@ -503,11 +526,25 @@ export function middleware<
|
|
|
503
526
|
...(options.mpp === false
|
|
504
527
|
? {}
|
|
505
528
|
: {
|
|
506
|
-
mpp:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
529
|
+
mpp: {
|
|
530
|
+
rateLimit: mppRateLimit,
|
|
531
|
+
session: Mppx.create({
|
|
532
|
+
...mpp,
|
|
533
|
+
...(secretKey ? { secretKey } : {}),
|
|
534
|
+
// Defaults only; global `session` options may override, including
|
|
535
|
+
// enabling same-route HEAD bootstrap.
|
|
536
|
+
methods: [
|
|
537
|
+
tempo.session({
|
|
538
|
+
amount: '0.0001',
|
|
539
|
+
bootstrap: false,
|
|
540
|
+
currency: Addresses.pathUsd,
|
|
541
|
+
unitType: 'request',
|
|
542
|
+
...session_,
|
|
543
|
+
}),
|
|
544
|
+
],
|
|
545
|
+
}).session,
|
|
546
|
+
sessionChainId: (c) => paymentSessionChainId(c, secretKey),
|
|
547
|
+
},
|
|
511
548
|
}),
|
|
512
549
|
rateLimit: RateLimit.memory(options.rateLimit),
|
|
513
550
|
...(options.session ? { session: options.session } : {}),
|
|
@@ -520,17 +557,19 @@ export function middleware<
|
|
|
520
557
|
const route = openApiRoute(c)
|
|
521
558
|
if (!route) return next()
|
|
522
559
|
|
|
523
|
-
const { key, description } = route
|
|
560
|
+
const { key, description, management, scope } = route
|
|
524
561
|
const overrides = options.overrides as
|
|
525
562
|
| Record<string, PolicyOverride | false | undefined>
|
|
526
563
|
| undefined
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
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,
|
|
532
570
|
)
|
|
533
571
|
if (!endpointPolicy) return next()
|
|
572
|
+
if (management && (!auth.mpp || !endpointPolicy.mpp)) return next()
|
|
534
573
|
|
|
535
574
|
c.set('auth', auth)
|
|
536
575
|
return require<environment>(endpointPolicy)(c, next)
|
|
@@ -600,8 +639,10 @@ export declare namespace middleware {
|
|
|
600
639
|
|
|
601
640
|
/** MPP payment options used by auth middleware. */
|
|
602
641
|
type MppOptions = Omit<NonNullable<Parameters<typeof Mppx.create>[0]>, 'methods'> & {
|
|
603
|
-
/**
|
|
604
|
-
|
|
642
|
+
/** Paid-request quota per access principal. Defaults to 100 per minute. */
|
|
643
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
644
|
+
/** Global Tempo Session options passed to the MPP method instantiator. */
|
|
645
|
+
session?: NonNullable<Parameters<typeof tempo.session>[0]> | undefined
|
|
605
646
|
}
|
|
606
647
|
}
|
|
607
648
|
|
|
@@ -975,6 +1016,55 @@ export function policy(options: PolicyOverride): MiddlewareHandler {
|
|
|
975
1016
|
return middleware
|
|
976
1017
|
}
|
|
977
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
|
+
|
|
978
1068
|
export declare namespace endpoint {
|
|
979
1069
|
/**
|
|
980
1070
|
* HTTP endpoint key inferred from a Hono app schema. Distributes over the
|
|
@@ -1021,12 +1111,35 @@ type OpenApiHandler = {
|
|
|
1021
1111
|
type OpenApiRoute = {
|
|
1022
1112
|
key: string
|
|
1023
1113
|
description: string | undefined
|
|
1114
|
+
management: boolean
|
|
1115
|
+
scope: string
|
|
1024
1116
|
}
|
|
1025
1117
|
|
|
1026
1118
|
type PolicyHandler = {
|
|
1027
1119
|
[policySymbol]?: PolicyOverride | undefined
|
|
1028
1120
|
}
|
|
1029
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
|
+
|
|
1030
1143
|
function laneDefaults(options: middleware.Options): LaneDefaults {
|
|
1031
1144
|
const apiKey = options.apiKey === false ? undefined : options.apiKey
|
|
1032
1145
|
const public_ = options.public === false ? undefined : options.public
|
|
@@ -1043,7 +1156,7 @@ function laneDefaults(options: middleware.Options): LaneDefaults {
|
|
|
1043
1156
|
// catalog scopes via `Auth.policy`.
|
|
1044
1157
|
scopes: apiKey?.scopes ?? [Scope.wildcard],
|
|
1045
1158
|
},
|
|
1046
|
-
mpp: {
|
|
1159
|
+
mpp: { session: {} },
|
|
1047
1160
|
public: { rateLimit: public_?.rateLimit ?? { perMinute: 60 } },
|
|
1048
1161
|
session: true,
|
|
1049
1162
|
}
|
|
@@ -1072,8 +1185,8 @@ function resolvePolicy(
|
|
|
1072
1185
|
// The OpenAPI summary/description rides along as the payment-challenge
|
|
1073
1186
|
// description. It is metadata, not a policy override, so it only applies when
|
|
1074
1187
|
// the paid lane is actually enabled and has no explicit description already.
|
|
1075
|
-
if (mpp && description !== undefined && mpp.
|
|
1076
|
-
mpp = {
|
|
1188
|
+
if (mpp && description !== undefined && mpp.session.description === undefined)
|
|
1189
|
+
mpp = { ...mpp, session: { ...mpp.session, description } }
|
|
1077
1190
|
|
|
1078
1191
|
// A lane-less policy stays enforced for super admin; only an explicit
|
|
1079
1192
|
// `false` override opts a route out of auth entirely.
|
|
@@ -1085,6 +1198,17 @@ function resolvePolicy(
|
|
|
1085
1198
|
}
|
|
1086
1199
|
}
|
|
1087
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
|
+
|
|
1088
1212
|
function resolveApiKeyLane(
|
|
1089
1213
|
current: require.Policy['apiKey'],
|
|
1090
1214
|
override: PolicyOverride['apiKey'],
|
|
@@ -1109,7 +1233,10 @@ function resolveMppLane(
|
|
|
1109
1233
|
if (override === false) return undefined
|
|
1110
1234
|
if (override === true) return current ?? default_
|
|
1111
1235
|
const base = current ?? default_
|
|
1112
|
-
return {
|
|
1236
|
+
return {
|
|
1237
|
+
rateLimit: override.rateLimit ?? base.rateLimit,
|
|
1238
|
+
session: { ...base.session, ...override.session },
|
|
1239
|
+
}
|
|
1113
1240
|
}
|
|
1114
1241
|
|
|
1115
1242
|
function resolvePublicLane(
|
|
@@ -1135,9 +1262,19 @@ function resolveSessionLane(
|
|
|
1135
1262
|
}
|
|
1136
1263
|
|
|
1137
1264
|
function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
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))
|
|
1141
1278
|
if (!route) return undefined
|
|
1142
1279
|
|
|
1143
1280
|
const spec = openApiSpec(route)
|
|
@@ -1147,6 +1284,8 @@ function openApiRoute(c: hono_Context): OpenApiRoute | undefined {
|
|
|
1147
1284
|
return {
|
|
1148
1285
|
key: endpointKey(c, route),
|
|
1149
1286
|
description: typeof description === 'string' ? description : undefined,
|
|
1287
|
+
management: false,
|
|
1288
|
+
scope: `${route.method.toUpperCase()} ${route.path}`,
|
|
1150
1289
|
}
|
|
1151
1290
|
}
|
|
1152
1291
|
|
|
@@ -1169,6 +1308,11 @@ function policyFromRoute(route: RouterRoute) {
|
|
|
1169
1308
|
return handler[policySymbol]
|
|
1170
1309
|
}
|
|
1171
1310
|
|
|
1311
|
+
function mppManagementFromRoute(route: RouterRoute) {
|
|
1312
|
+
const handler = findTargetHandler(route.handler) as MppManagementHandler
|
|
1313
|
+
return handler[mppManagementSymbol]
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1172
1316
|
/** Resolved access lanes for one documented operation. */
|
|
1173
1317
|
export type OperationAccess = {
|
|
1174
1318
|
/** Whether API-key access is allowed. */
|
|
@@ -1223,8 +1367,8 @@ export function describeAccess(app: Hono<any, any>): Record<string, OperationAcc
|
|
|
1223
1367
|
return access
|
|
1224
1368
|
}
|
|
1225
1369
|
|
|
1226
|
-
function endpointKey(c: hono_Context, route: RouterRoute) {
|
|
1227
|
-
return `${
|
|
1370
|
+
function endpointKey(c: hono_Context, route: RouterRoute, method = route.method) {
|
|
1371
|
+
return `${method.toUpperCase()} ${stripBasePath(normalizeRoutePath(route.path), basePath(c))}`
|
|
1228
1372
|
}
|
|
1229
1373
|
|
|
1230
1374
|
function normalizeRoutePath(path: string) {
|
|
@@ -1483,8 +1627,10 @@ export declare namespace require {
|
|
|
1483
1627
|
/** MPP payment access policy. */
|
|
1484
1628
|
mpp?:
|
|
1485
1629
|
| {
|
|
1486
|
-
/**
|
|
1487
|
-
|
|
1630
|
+
/** Paid-request quota override for this route (a protective per-route cap). */
|
|
1631
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
1632
|
+
/** Tempo Session request policy. */
|
|
1633
|
+
session: Mpp.Session
|
|
1488
1634
|
}
|
|
1489
1635
|
| undefined
|
|
1490
1636
|
/** Public access policy. */
|
|
@@ -1663,6 +1809,40 @@ function hasPaymentCredential(c: hono_Context) {
|
|
|
1663
1809
|
return !!Credential.extractPaymentScheme(authorization)
|
|
1664
1810
|
}
|
|
1665
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
|
+
|
|
1666
1846
|
function isPaymentEnabled(auth: Context, policy: require.Policy) {
|
|
1667
1847
|
return !!auth.mpp && !!policy.mpp
|
|
1668
1848
|
}
|
|
@@ -1729,7 +1909,28 @@ async function requirePayment<environment extends Environment>(
|
|
|
1729
1909
|
next: () => Promise<void>,
|
|
1730
1910
|
options: PaymentAccess,
|
|
1731
1911
|
) {
|
|
1732
|
-
|
|
1912
|
+
if (hasPaymentCredential(c)) {
|
|
1913
|
+
const result = await consumeRateLimit(options.auth, {
|
|
1914
|
+
key: `mpp:${options.principal.type}:${options.principal.id}`,
|
|
1915
|
+
limit: options.policy.mpp?.rateLimit ?? options.auth.mpp?.rateLimit ?? defaultMppRateLimit,
|
|
1916
|
+
})
|
|
1917
|
+
if (result) {
|
|
1918
|
+
setRateLimitHeaders(c, result)
|
|
1919
|
+
c.header('RateLimit-Scope', 'mpp')
|
|
1920
|
+
if (!result.allowed) return rateLimitExceeded(c, result)
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
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
|
+
})
|
|
1733
1934
|
if (!middleware)
|
|
1734
1935
|
return jsonError(c, {
|
|
1735
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 {
|