tapimo 0.2.0 → 0.4.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/README.md +36 -8
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7532 -4850
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +11 -3
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +63 -53
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +1049 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +54 -0
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingRouteProviders.js +355 -0
- package/dist/apps/data/FundingRouteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -0
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +1 -0
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +843 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +443 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- 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/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +5 -3
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +28 -96
- package/dist/apps/docs/App.js.map +1 -1
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +3 -2
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +3 -2
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/{platform → management}/App.d.ts +3015 -738
- package/dist/apps/management/App.d.ts.map +1 -0
- package/dist/apps/{platform → management}/App.js +20 -10
- package/dist/apps/management/App.js.map +1 -0
- package/dist/apps/management/Billing.d.ts +279 -0
- package/dist/apps/management/Billing.d.ts.map +1 -0
- package/dist/apps/management/Billing.js +477 -0
- package/dist/apps/management/Billing.js.map +1 -0
- package/dist/apps/management/index.d.ts +3 -0
- package/dist/apps/management/index.d.ts.map +1 -0
- package/dist/apps/management/index.js +3 -0
- package/dist/apps/management/index.js.map +1 -0
- package/dist/apps/{platform → management}/routes/api-keys.d.ts +341 -49
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/api-keys.js +116 -14
- package/dist/apps/management/routes/api-keys.js.map +1 -0
- package/dist/apps/management/routes/billing.d.ts +1520 -0
- package/dist/apps/management/routes/billing.d.ts.map +1 -0
- package/dist/apps/management/routes/billing.js +795 -0
- package/dist/apps/management/routes/billing.js.map +1 -0
- package/dist/apps/{platform → management}/routes/invitations.d.ts +202 -84
- package/dist/apps/management/routes/invitations.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/invitations.js +59 -1
- package/dist/apps/management/routes/invitations.js.map +1 -0
- package/dist/apps/{platform → management}/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/me.d.ts.map +1 -0
- package/dist/apps/management/routes/me.js.map +1 -0
- package/dist/apps/management/routes/members.d.ts.map +1 -0
- package/dist/apps/management/routes/members.js.map +1 -0
- package/dist/apps/{platform → management}/routes/orgs.d.ts +50 -36
- package/dist/apps/management/routes/orgs.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/orgs.js +14 -3
- package/dist/apps/management/routes/orgs.js.map +1 -0
- package/dist/apps/{platform → management}/routes/projects.d.ts +97 -97
- package/dist/apps/{platform → management}/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js.map +1 -0
- package/dist/apps/management/routes/scopes.d.ts +107 -0
- package/dist/apps/management/routes/scopes.d.ts.map +1 -0
- package/dist/apps/management/routes/scopes.js +66 -0
- package/dist/apps/management/routes/scopes.js.map +1 -0
- package/dist/apps/management/routes/usage.d.ts +588 -0
- package/dist/apps/management/routes/usage.d.ts.map +1 -0
- package/dist/apps/management/routes/usage.js +391 -0
- package/dist/apps/management/routes/usage.js.map +1 -0
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/relay/App.d.ts +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +149 -77
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +915 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +625 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +8 -7
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +26 -8
- package/src/Client.test-d.ts +353 -2
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/composition.test.ts +26 -13
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +37 -0
- package/src/apps/data/FundingRouteProviders.test.ts +281 -0
- package/src/apps/data/FundingRouteProviders.ts +459 -0
- package/src/apps/data/index.ts +1 -0
- package/src/apps/data/routes/funding.test.ts +314 -0
- package/src/apps/data/routes/funding.ts +698 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +30 -101
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +3 -2
- package/src/apps/{platform → management}/App.ts +56 -17
- package/src/apps/management/Billing.test.ts +530 -0
- package/src/apps/management/Billing.ts +682 -0
- package/src/apps/management/index.ts +2 -0
- package/src/apps/{platform → management}/routes/api-keys.test.ts +172 -9
- package/src/apps/{platform → management}/routes/api-keys.ts +137 -21
- package/src/apps/management/routes/billing.test.ts +1100 -0
- package/src/apps/management/routes/billing.ts +966 -0
- package/src/apps/{platform → management}/routes/invitations.test.ts +55 -7
- package/src/apps/{platform → management}/routes/invitations.ts +69 -1
- package/src/apps/management/routes/me.test.ts +351 -0
- package/src/apps/{platform → management}/routes/orgs.ts +26 -4
- package/src/apps/management/routes/scopes.test.ts +29 -0
- package/src/apps/management/routes/scopes.ts +85 -0
- package/src/apps/management/routes/usage.test.ts +422 -0
- package/src/apps/management/routes/usage.ts +527 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/relay/App.test.ts +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +176 -87
- package/src/internal/Cache.ts +5 -1
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.test.ts +213 -0
- package/src/internal/FundingRoutes.ts +898 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
- package/dist/apps/platform/App.d.ts.map +0 -1
- package/dist/apps/platform/App.js.map +0 -1
- package/dist/apps/platform/index.d.ts +0 -2
- package/dist/apps/platform/index.d.ts.map +0 -1
- package/dist/apps/platform/index.js +0 -2
- package/dist/apps/platform/index.js.map +0 -1
- package/dist/apps/platform/routes/api-keys.d.ts.map +0 -1
- package/dist/apps/platform/routes/api-keys.js.map +0 -1
- package/dist/apps/platform/routes/invitations.d.ts.map +0 -1
- package/dist/apps/platform/routes/invitations.js.map +0 -1
- package/dist/apps/platform/routes/me.d.ts.map +0 -1
- package/dist/apps/platform/routes/me.js.map +0 -1
- package/dist/apps/platform/routes/members.d.ts.map +0 -1
- package/dist/apps/platform/routes/members.js.map +0 -1
- package/dist/apps/platform/routes/orgs.d.ts.map +0 -1
- package/dist/apps/platform/routes/orgs.js.map +0 -1
- package/dist/apps/platform/routes/projects.js.map +0 -1
- package/src/apps/platform/index.ts +0 -1
- package/src/apps/platform/routes/me.test.ts +0 -121
- /package/dist/apps/{platform → management}/routes/me.js +0 -0
- /package/dist/apps/{platform → management}/routes/members.d.ts +0 -0
- /package/dist/apps/{platform → management}/routes/members.js +0 -0
- /package/dist/apps/{platform → management}/routes/projects.js +0 -0
- /package/src/apps/{platform → management}/routes/me.ts +0 -0
- /package/src/apps/{platform → management}/routes/members.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/members.ts +0 -0
- /package/src/apps/{platform → management}/routes/orgs.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/projects.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/projects.ts +0 -0
package/src/internal/Auth.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createMiddleware } from 'hono/factory'
|
|
|
6
6
|
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
|
+
import { decodeJwt } from 'jose'
|
|
9
10
|
import { Credential, type Method } from 'mppx'
|
|
10
11
|
import { Mppx, tempo } from 'mppx/hono'
|
|
11
12
|
import * as z from 'zod/mini'
|
|
@@ -15,6 +16,7 @@ import * as ApiKey from '../ApiKey.js'
|
|
|
15
16
|
import * as ApiKeys from '../ApiKeys.js'
|
|
16
17
|
import * as Db from '../db/Db.js'
|
|
17
18
|
import type * as Store from './Store.js'
|
|
19
|
+
import * as Invitations from '../db/tables/invitations.js'
|
|
18
20
|
import * as Memberships from '../db/tables/memberships.js'
|
|
19
21
|
import * as Metadata from '../apps/metadata.js'
|
|
20
22
|
import * as OpenApi from './OpenApi.js'
|
|
@@ -46,6 +48,13 @@ export type Context = {
|
|
|
46
48
|
| undefined
|
|
47
49
|
/** MPP payment configuration. */
|
|
48
50
|
mpp?: Mpp | undefined
|
|
51
|
+
/**
|
|
52
|
+
* Configured default anonymous public quota. Used as the throttle ceiling for
|
|
53
|
+
* sandbox keys without active billing on routes that expose no public lane
|
|
54
|
+
* (where the per-route `policy.public` is absent). Omit to fall back to the
|
|
55
|
+
* framework default.
|
|
56
|
+
*/
|
|
57
|
+
publicRateLimit?: RateLimit.Limit | undefined
|
|
49
58
|
/** Rate-limit store used by API handlers. */
|
|
50
59
|
rateLimit?: RateLimit.Store | undefined
|
|
51
60
|
/** Session lane resolution (the session surface's capability), or undefined when sessions are unmounted. */
|
|
@@ -465,6 +474,10 @@ export function middleware<
|
|
|
465
474
|
resolve,
|
|
466
475
|
},
|
|
467
476
|
}),
|
|
477
|
+
// The throttle ceiling for sandbox keys without active billing, so
|
|
478
|
+
// API-key-only routes (no per-route `policy.public`) still fall back to the
|
|
479
|
+
// deployment's configured public quota.
|
|
480
|
+
publicRateLimit: defaults.public.rateLimit,
|
|
468
481
|
...(options.mpp === false
|
|
469
482
|
? {}
|
|
470
483
|
: {
|
|
@@ -677,11 +690,19 @@ export function install(options: install.Options) {
|
|
|
677
690
|
// mounted sub-app, and the public lane rate-limits challenge-store writes.
|
|
678
691
|
const app = (() => {
|
|
679
692
|
if (!options_session || !kv) return undefined
|
|
680
|
-
const { issuer, origin, ttl } =
|
|
693
|
+
const { issuer, origin, requireEmail, ttl } =
|
|
681
694
|
(typeof options_session === 'object' ? options_session.wallet : undefined) ?? {}
|
|
695
|
+
// Case-insensitive exact-match email-domain allowlist for new-user sign-ups.
|
|
696
|
+
const emailAllowlist = (
|
|
697
|
+
typeof options_session === 'object' ? options_session.emailAllowlist : undefined
|
|
698
|
+
)
|
|
699
|
+
?.map((domain) => domain.trim().toLowerCase())
|
|
700
|
+
.filter(Boolean)
|
|
682
701
|
|
|
683
702
|
const handler = Accounts.Handler.auth({
|
|
684
|
-
...(issuer
|
|
703
|
+
...(issuer || requireEmail
|
|
704
|
+
? { identity: { ...(issuer ? { issuer } : {}), ...(requireEmail ? { required: true } : {}) } } // prettier-ignore
|
|
705
|
+
: {}),
|
|
685
706
|
// Sign-in upserts the `users` row keyed by address.
|
|
686
707
|
onAuthenticate: async ({ address }) => {
|
|
687
708
|
await Users.upsertByAddress(Db.get(db), { address })
|
|
@@ -738,7 +759,41 @@ export function install(options: install.Options) {
|
|
|
738
759
|
}
|
|
739
760
|
|
|
740
761
|
const policy_public = policy({ public: true })
|
|
762
|
+
const policy_session = policy({ public: true, session: true })
|
|
741
763
|
const describe = OpenApi.describeRoute({ hide: true })
|
|
764
|
+
|
|
765
|
+
// Sign-up gate: new addresses need an allowlisted identity email domain
|
|
766
|
+
// or a pending invitation; existing users pass. The claim decodes
|
|
767
|
+
// unverified; the mounted handler verifies the token before any session
|
|
768
|
+
// or user is created, so a forged claim never authenticates.
|
|
769
|
+
const gate_signup = createMiddleware<App.Environment>(async (c, next) => {
|
|
770
|
+
if (!emailAllowlist?.length) return next()
|
|
771
|
+
const { address, idToken } = (await c.req.json()) as z.output<typeof schema.verify.Body>
|
|
772
|
+
const database = Db.get(db)
|
|
773
|
+
const user = await Users.getByAddress(database, address)
|
|
774
|
+
if (!user) {
|
|
775
|
+
const email = (() => {
|
|
776
|
+
if (!idToken) return undefined
|
|
777
|
+
try {
|
|
778
|
+
const { email } = decodeJwt<{ email?: string }>(idToken)
|
|
779
|
+
return typeof email === 'string' ? email.toLowerCase() : undefined
|
|
780
|
+
} catch {
|
|
781
|
+
return undefined
|
|
782
|
+
}
|
|
783
|
+
})()
|
|
784
|
+
const allowed = await (async () => {
|
|
785
|
+
if (!email) return false
|
|
786
|
+
const at = email.lastIndexOf('@')
|
|
787
|
+
if (at > 0 && emailAllowlist.includes(email.slice(at + 1))) return true
|
|
788
|
+
// A pending invitation admits the email regardless of domain.
|
|
789
|
+
return (await Invitations.listByEmail(database, email)).length > 0
|
|
790
|
+
})()
|
|
791
|
+
if (!allowed)
|
|
792
|
+
return c.json({ error: 'sign-ups are restricted to approved email domains or invited emails' }, 403) // prettier-ignore
|
|
793
|
+
}
|
|
794
|
+
return next()
|
|
795
|
+
})
|
|
796
|
+
|
|
742
797
|
const app = new Hono<App.Environment>()
|
|
743
798
|
.use('/v1/auth/siwe', policy_public, describe)
|
|
744
799
|
.use('/v1/auth/siwe/*', policy_public, describe)
|
|
@@ -779,6 +834,7 @@ export function install(options: install.Options) {
|
|
|
779
834
|
code: 'body_invalid',
|
|
780
835
|
message: 'Invalid request body',
|
|
781
836
|
}),
|
|
837
|
+
gate_signup,
|
|
782
838
|
OpenApi.describeRoute({
|
|
783
839
|
description:
|
|
784
840
|
'Verifies the signed challenge and establishes a session. Sets a session cookie by default; pass `returnToken` to receive a bearer token instead.',
|
|
@@ -796,8 +852,11 @@ export function install(options: install.Options) {
|
|
|
796
852
|
},
|
|
797
853
|
},
|
|
798
854
|
},
|
|
799
|
-
400: handlerError('Invalid, expired, or mismatched challenge message.'),
|
|
855
|
+
400: handlerError('Invalid, expired, or mismatched challenge message; or a required identity token is missing.'), // prettier-ignore
|
|
800
856
|
401: handlerError('Signature or identity verification failed.'),
|
|
857
|
+
...(emailAllowlist?.length
|
|
858
|
+
? { 403: handlerError('Sign-up rejected: the identity email is neither on an allowlisted domain nor pending an invitation.') } // prettier-ignore
|
|
859
|
+
: {}),
|
|
801
860
|
409: handlerError('Challenge nonce already used or unknown.'),
|
|
802
861
|
429: OpenApi.standardError(429, 'Rate limited.'),
|
|
803
862
|
500: OpenApi.standardError(500, 'Internal server error.'),
|
|
@@ -808,7 +867,7 @@ export function install(options: install.Options) {
|
|
|
808
867
|
)
|
|
809
868
|
app.post(
|
|
810
869
|
'/v1/auth/siwe/logout',
|
|
811
|
-
|
|
870
|
+
policy_session,
|
|
812
871
|
OpenApi.describeRoute({
|
|
813
872
|
description: 'Revokes the current session and clears the session cookie.',
|
|
814
873
|
hide: false,
|
|
@@ -823,7 +882,7 @@ export function install(options: install.Options) {
|
|
|
823
882
|
}),
|
|
824
883
|
)
|
|
825
884
|
return Object.assign(
|
|
826
|
-
// The tag joins the `
|
|
885
|
+
// The tag joins the `Management API` group at `management()`'s chosen slot;
|
|
827
886
|
// group listings filter to defined tags, so it drops out when unmounted.
|
|
828
887
|
Metadata.attach(app.route('/v1/auth/siwe', handler), {
|
|
829
888
|
tags: [{ name: 'Authentication', description: 'Authenticate into the Tempo Platform.' }],
|
|
@@ -845,8 +904,8 @@ export declare namespace install {
|
|
|
845
904
|
type Options = {
|
|
846
905
|
auth:
|
|
847
906
|
| (Omit<middleware.Options, 'session'> & {
|
|
848
|
-
/** Session sign-in surface: mounts `/v1/auth/siwe` (challenge/verify/logout) and enables the session lane. Pass true for defaults or a
|
|
849
|
-
session?:
|
|
907
|
+
/** Session sign-in surface: mounts `/v1/auth/siwe` (challenge/verify/logout) and enables the session lane. Pass true for defaults or a config object. Requires `kv`. */
|
|
908
|
+
session?: Session | undefined
|
|
850
909
|
})
|
|
851
910
|
| false
|
|
852
911
|
| undefined
|
|
@@ -856,12 +915,24 @@ export declare namespace install {
|
|
|
856
915
|
kv: { store: Store.State } | undefined
|
|
857
916
|
}
|
|
858
917
|
|
|
918
|
+
/** Session sign-in surface configuration: `true` for defaults, or a config object. */
|
|
919
|
+
type Session =
|
|
920
|
+
| boolean
|
|
921
|
+
| {
|
|
922
|
+
/** Restrict new-user sign-ups to verified emails on these domains (case-insensitive exact match); invited emails and existing users sign in unaffected. */
|
|
923
|
+
emailAllowlist?: readonly string[] | undefined
|
|
924
|
+
/** SIWE wallet sign-in configuration. */
|
|
925
|
+
wallet?: Wallet | undefined
|
|
926
|
+
}
|
|
927
|
+
|
|
859
928
|
/** SIWE wallet sign-in configuration. */
|
|
860
929
|
type Wallet = {
|
|
861
930
|
/** Identity (verified email) issuer override; defaults to the Tempo wallet's production OIDC mount. */
|
|
862
931
|
issuer?: string | undefined
|
|
863
932
|
/** Pinned absolute origin for SIWE domain binding. Required off-Workers; on Workers the edge proxy headers are trusted by default. */
|
|
864
933
|
origin?: string | undefined
|
|
934
|
+
/** Reject sign-ins that carry no valid identity token, so every session holds a verified email. */
|
|
935
|
+
requireEmail?: boolean | undefined
|
|
865
936
|
/** Session and challenge TTL overrides, in seconds. */
|
|
866
937
|
ttl?: { challenge?: number | undefined; session?: number | undefined } | undefined
|
|
867
938
|
}
|
|
@@ -1199,84 +1270,98 @@ export function require<environment extends Environment = Environment>(
|
|
|
1199
1270
|
})
|
|
1200
1271
|
}
|
|
1201
1272
|
|
|
1202
|
-
// Sandbox keys are
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1273
|
+
// Sandbox keys are testnet-only. A sandbox key that explicitly targets
|
|
1274
|
+
// mainnet (`?chainId`) is rejected with a clear message rather than
|
|
1275
|
+
// steered or downgraded to anonymous, so the caller learns their key does
|
|
1276
|
+
// not cover mainnet. The no-`chainId` default (mainnet) is enforced by
|
|
1277
|
+
// the data group's chain guard, which knows the route is chain-scoped;
|
|
1278
|
+
// the relay resolves its chain from the request body and enforces the
|
|
1279
|
+
// same rule in its sponsor validation.
|
|
1207
1280
|
const explicitChain = explicitChainId(c)
|
|
1208
|
-
|
|
1281
|
+
|
|
1282
|
+
const principal = principalFromApiKey(apiKey)
|
|
1283
|
+
c.set('principal', principal)
|
|
1284
|
+
|
|
1285
|
+
if (
|
|
1209
1286
|
apiKey.environment === 'sandbox' &&
|
|
1210
1287
|
explicitChain !== undefined &&
|
|
1211
1288
|
Viem.isMainnet(explicitChain)
|
|
1289
|
+
)
|
|
1290
|
+
return jsonError(c, {
|
|
1291
|
+
code: 'api_key_forbidden',
|
|
1292
|
+
message: 'Sandbox API keys only support testnet. Pass a testnet `chainId`.',
|
|
1293
|
+
status: 403,
|
|
1294
|
+
})
|
|
1212
1295
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
message: 'API key not permitted for this route',
|
|
1231
|
-
status: 403,
|
|
1232
|
-
})
|
|
1233
|
-
const missing = apiKey.scopes.includes(Scope.wildcard)
|
|
1234
|
-
? []
|
|
1235
|
-
: (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? [])
|
|
1236
|
-
if (missing.length > 0)
|
|
1237
|
-
return jsonError(c, {
|
|
1238
|
-
code: 'api_key_forbidden',
|
|
1239
|
-
message: 'API key missing required scope',
|
|
1240
|
-
status: 403,
|
|
1241
|
-
})
|
|
1296
|
+
// Default-deny: routes with no API-key lane allow only the super admin.
|
|
1297
|
+
// A valid key is authenticated but forbidden.
|
|
1298
|
+
if (!policy.apiKey)
|
|
1299
|
+
return jsonError(c, {
|
|
1300
|
+
code: 'forbidden',
|
|
1301
|
+
message: 'API key not permitted for this route',
|
|
1302
|
+
status: 403,
|
|
1303
|
+
})
|
|
1304
|
+
const missing = apiKey.scopes.includes(Scope.wildcard)
|
|
1305
|
+
? []
|
|
1306
|
+
: (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? [])
|
|
1307
|
+
if (missing.length > 0)
|
|
1308
|
+
return jsonError(c, {
|
|
1309
|
+
code: 'api_key_forbidden',
|
|
1310
|
+
message: 'API key missing required scope',
|
|
1311
|
+
status: 403,
|
|
1312
|
+
})
|
|
1242
1313
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1314
|
+
// Sandbox keys without active billing are throttled to the public quota.
|
|
1315
|
+
// `billingActive` is snapshotted on the key record (stamped at mint,
|
|
1316
|
+
// re-synced when billing changes), so this needs no billing read on the
|
|
1317
|
+
// request path. Resolved before the paid lane so a payment credential
|
|
1318
|
+
// can't buy past the throttle: sandbox is non-billable testnet.
|
|
1319
|
+
const throttled = apiKey.environment === 'sandbox' && apiKey.billingActive !== true
|
|
1320
|
+
|
|
1321
|
+
if (hasPayment && !throttled)
|
|
1322
|
+
return requirePayment(c, next, {
|
|
1323
|
+
auth,
|
|
1324
|
+
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
1325
|
+
policy,
|
|
1326
|
+
principal,
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
if (policy.apiKey) {
|
|
1330
|
+
// A route consumes exactly one quota bucket: its single required scope,
|
|
1331
|
+
// else `'*'` (zero scopes). Routes today require at most one scope; a
|
|
1332
|
+
// future multi-scope route buckets against its first scope.
|
|
1333
|
+
const scope = policy.apiKey.scopes[0] ?? Scope.wildcard
|
|
1334
|
+
// A throttled sandbox key shares the key's bucket but at the public
|
|
1335
|
+
// ceiling, so activating billing lifts the limit on the same counter.
|
|
1336
|
+
const limit = throttled
|
|
1337
|
+
? (policy.public?.rateLimit ?? auth.publicRateLimit ?? { perMinute: 60 })
|
|
1338
|
+
: resolveApiKeyLimit({ apiKey, auth, policy: policy.apiKey, scope })
|
|
1339
|
+
const result = await consumeRateLimit(auth, {
|
|
1340
|
+
key: `api_key:${apiKey.id}:scope:${scope}`,
|
|
1341
|
+
limit,
|
|
1342
|
+
})
|
|
1343
|
+
if (result) {
|
|
1344
|
+
setRateLimitHeaders(c, result)
|
|
1345
|
+
// Surface the bucket so callers can correlate a 429 with the exhausted
|
|
1346
|
+
// quota. Set before the over-quota branch so it rides the 429 (an MPP
|
|
1347
|
+
// 402 challenge is owned by mppx and drops these headers — by design).
|
|
1348
|
+
c.header('RateLimit-Scope', scope)
|
|
1349
|
+
}
|
|
1350
|
+
if (result && !result.allowed) {
|
|
1351
|
+
// A throttled sandbox key gets a plain 429, never an MPP challenge —
|
|
1352
|
+
// it must activate billing to lift the ceiling, not pay per request.
|
|
1353
|
+
if (throttled) return rateLimitExceeded(c, result)
|
|
1354
|
+
return requirePaymentOrRateLimit(c, next, {
|
|
1245
1355
|
auth,
|
|
1246
1356
|
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
1247
1357
|
policy,
|
|
1248
1358
|
principal,
|
|
1359
|
+
rateLimit: result,
|
|
1249
1360
|
})
|
|
1250
|
-
|
|
1251
|
-
if (policy.apiKey) {
|
|
1252
|
-
// A route consumes exactly one quota bucket: its single required scope,
|
|
1253
|
-
// else `'*'` (zero scopes). Routes today require at most one scope; a
|
|
1254
|
-
// future multi-scope route buckets against its first scope.
|
|
1255
|
-
const scope = policy.apiKey.scopes[0] ?? Scope.wildcard
|
|
1256
|
-
const limit = resolveApiKeyLimit({ apiKey, auth, policy: policy.apiKey, scope })
|
|
1257
|
-
const result = await consumeRateLimit(auth, {
|
|
1258
|
-
key: `api_key:${apiKey.id}:scope:${scope}`,
|
|
1259
|
-
limit,
|
|
1260
|
-
})
|
|
1261
|
-
if (result) {
|
|
1262
|
-
setRateLimitHeaders(c, result)
|
|
1263
|
-
// Surface the bucket so callers can correlate a 429 with the exhausted
|
|
1264
|
-
// quota. Set before the over-quota branch so it rides the 429 (an MPP
|
|
1265
|
-
// 402 challenge is owned by mppx and drops these headers — by design).
|
|
1266
|
-
c.header('RateLimit-Scope', scope)
|
|
1267
|
-
}
|
|
1268
|
-
if (result && !result.allowed)
|
|
1269
|
-
return requirePaymentOrRateLimit(c, next, {
|
|
1270
|
-
auth,
|
|
1271
|
-
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
1272
|
-
policy,
|
|
1273
|
-
principal,
|
|
1274
|
-
rateLimit: result,
|
|
1275
|
-
})
|
|
1276
1361
|
}
|
|
1277
|
-
|
|
1278
|
-
return next()
|
|
1279
1362
|
}
|
|
1363
|
+
|
|
1364
|
+
return next()
|
|
1280
1365
|
}
|
|
1281
1366
|
|
|
1282
1367
|
// Cookie sessions carry no Authorization credential; resolve the session
|
|
@@ -1410,23 +1495,26 @@ function consumeRateLimit(
|
|
|
1410
1495
|
// or a stall past `rateLimitConsumeTimeout` degrades to unenforced quota
|
|
1411
1496
|
// instead of failing — or blocking — the request.
|
|
1412
1497
|
return withTimeout(auth.rateLimit.consume(options), rateLimitConsumeTimeout).catch((error) => {
|
|
1413
|
-
// A timeout or
|
|
1414
|
-
//
|
|
1415
|
-
//
|
|
1416
|
-
if (error instanceof RateLimitTimeoutError ||
|
|
1498
|
+
// A timeout or transient Durable Object fault is an expected degradation,
|
|
1499
|
+
// logged at warn to stay out of error dashboards; an unexpected store
|
|
1500
|
+
// throw is a genuine error.
|
|
1501
|
+
if (error instanceof RateLimitTimeoutError || isDurableObjectFault(error))
|
|
1417
1502
|
console.warn('[auth] rate-limit consume degraded; failing open', error)
|
|
1418
1503
|
else console.error('[auth] rate-limit consume failed; failing open', error)
|
|
1419
1504
|
return null
|
|
1420
1505
|
})
|
|
1421
1506
|
}
|
|
1422
1507
|
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
)
|
|
1508
|
+
function isDurableObjectFault(error: unknown): boolean {
|
|
1509
|
+
const { message, overloaded, retryable } = (error ?? {}) as {
|
|
1510
|
+
message?: string | undefined
|
|
1511
|
+
overloaded?: boolean | undefined
|
|
1512
|
+
retryable?: boolean | undefined
|
|
1513
|
+
}
|
|
1514
|
+
if (overloaded === true || retryable === true) return true
|
|
1515
|
+
// Cloudflare redacts internal Durable Object faults to "internal error;
|
|
1516
|
+
// reference = <token>", with no marker property to detect them by.
|
|
1517
|
+
return typeof message === 'string' && message.startsWith('internal error')
|
|
1430
1518
|
}
|
|
1431
1519
|
|
|
1432
1520
|
/**
|
|
@@ -1640,11 +1728,12 @@ function requirePaymentOrRateLimit<environment extends Environment>(
|
|
|
1640
1728
|
options: RateLimitAccess,
|
|
1641
1729
|
) {
|
|
1642
1730
|
if (isPaymentEnabled(options.auth, options.policy)) return requirePayment(c, next, options)
|
|
1731
|
+
return rateLimitExceeded(c, options.rateLimit)
|
|
1732
|
+
}
|
|
1643
1733
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
)
|
|
1734
|
+
/** Emits a `429` with a `Retry-After` derived from the window reset. */
|
|
1735
|
+
function rateLimitExceeded(c: hono_Context, result: RateLimit.Result) {
|
|
1736
|
+
c.header('Retry-After', String(Math.max(result.reset - Math.ceil(Date.now() / 1_000), 1)))
|
|
1648
1737
|
return jsonError(c, {
|
|
1649
1738
|
code: 'rate_limit_exceeded',
|
|
1650
1739
|
message: 'Rate limit exceeded',
|
package/src/internal/Cache.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function response<environment extends Env>(
|
|
|
74
74
|
})
|
|
75
75
|
const etag = hono_etag()
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
const middleware: MiddlewareHandler<environment> = async (c, next) => {
|
|
78
78
|
const values = response.preservedHeaders.map((name) => [name, c.res.headers.get(name)] as const)
|
|
79
79
|
|
|
80
80
|
// Honor the standard request `Cache-Control` directives (plus the HTTP/1.0
|
|
@@ -155,6 +155,10 @@ export function response<environment extends Env>(
|
|
|
155
155
|
if (result instanceof Response) return res
|
|
156
156
|
return undefined
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
// `markCacheable` lets the edge layer pay its store lookup only on routes
|
|
160
|
+
// that mount this middleware; everything else skips the read.
|
|
161
|
+
return EdgeCache.markCacheable(middleware)
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
export declare namespace response {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
+
import { createMiddleware } from 'hono/factory'
|
|
2
3
|
import * as EdgeCache from './EdgeCache.js'
|
|
3
4
|
import type * as Store from './Store.js'
|
|
4
5
|
|
|
@@ -128,6 +129,18 @@ describe('middleware', () => {
|
|
|
128
129
|
expect(store.size).toMatchInlineSnapshot(`0`)
|
|
129
130
|
})
|
|
130
131
|
|
|
132
|
+
test('skips the store lookup for routes that did not opt in', async () => {
|
|
133
|
+
const { app, store } = createApp()
|
|
134
|
+
|
|
135
|
+
await app.request('/uncached')
|
|
136
|
+
await app.request('/missing-route')
|
|
137
|
+
expect(store.gets).toMatchInlineSnapshot(`0`)
|
|
138
|
+
|
|
139
|
+
// Marked routes still pay (and use) the lookup.
|
|
140
|
+
await app.request('/cached')
|
|
141
|
+
expect(store.gets).toMatchInlineSnapshot(`1`)
|
|
142
|
+
})
|
|
143
|
+
|
|
131
144
|
test('answers conditional requests from a cached ETag with 304', async () => {
|
|
132
145
|
const { app } = createApp()
|
|
133
146
|
|
|
@@ -172,13 +185,18 @@ type Environment = { Variables: EdgeCache.Variables }
|
|
|
172
185
|
// In-memory `Store.Store` so the edge cache and origin cache share one shape.
|
|
173
186
|
function memoryStore() {
|
|
174
187
|
const map = new Map<string, string>()
|
|
188
|
+
let gets = 0
|
|
175
189
|
return {
|
|
176
190
|
async delete(key: string) {
|
|
177
191
|
map.delete(key)
|
|
178
192
|
},
|
|
179
193
|
async get(key: string) {
|
|
194
|
+
gets++
|
|
180
195
|
return map.get(key) ?? null
|
|
181
196
|
},
|
|
197
|
+
get gets() {
|
|
198
|
+
return gets
|
|
199
|
+
},
|
|
182
200
|
async list() {
|
|
183
201
|
return { keys: [...map.keys()].map((name) => ({ name })) }
|
|
184
202
|
},
|
|
@@ -189,7 +207,7 @@ function memoryStore() {
|
|
|
189
207
|
return map.size
|
|
190
208
|
},
|
|
191
209
|
type: 'state',
|
|
192
|
-
} satisfies Store.Store & { readonly size: number }
|
|
210
|
+
} satisfies Store.Store & { readonly gets: number; readonly size: number }
|
|
193
211
|
}
|
|
194
212
|
|
|
195
213
|
function createApp(options: { store?: Store.Store | undefined } = {}) {
|
|
@@ -203,7 +221,12 @@ function createApp(options: { store?: Store.Store | undefined } = {}) {
|
|
|
203
221
|
downstream++
|
|
204
222
|
await next()
|
|
205
223
|
})
|
|
206
|
-
|
|
224
|
+
// Routes publishing a policy mount a marked middleware, as `Cache.response`
|
|
225
|
+
// routes do; unmarked routes must skip the lookup entirely.
|
|
226
|
+
const cacheable = EdgeCache.markCacheable(
|
|
227
|
+
createMiddleware<Environment>(async (_c, next) => next()),
|
|
228
|
+
)
|
|
229
|
+
app.get('/cached', cacheable, (c) => {
|
|
207
230
|
c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
|
|
208
231
|
c.header('Age', '123')
|
|
209
232
|
c.header('CF-Cache-Status', 'HIT')
|
|
@@ -212,11 +235,11 @@ function createApp(options: { store?: Store.Store | undefined } = {}) {
|
|
|
212
235
|
c.header('RateLimit-Remaining', '9')
|
|
213
236
|
return c.json({ ok: true })
|
|
214
237
|
})
|
|
215
|
-
app.get('/error', (c) => {
|
|
238
|
+
app.get('/error', cacheable, (c) => {
|
|
216
239
|
c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
|
|
217
240
|
return c.json({ ok: false }, 500)
|
|
218
241
|
})
|
|
219
|
-
app.get('/binary', (c) => {
|
|
242
|
+
app.get('/binary', cacheable, (c) => {
|
|
220
243
|
c.set('edgeCache', { maxAge: 30, staleWhileRevalidate: 300 })
|
|
221
244
|
// Bytes that are not valid UTF-8 (PNG signature) — corrupted if stored via
|
|
222
245
|
// `.text()` instead of base64.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Env, MiddlewareHandler } from 'hono'
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from 'hono'
|
|
2
|
+
import { matchedRoutes } from 'hono/route'
|
|
2
3
|
import { Base64 } from 'ox'
|
|
3
4
|
|
|
4
5
|
import type * as Store from './Store.js'
|
|
@@ -53,6 +54,10 @@ const volatileHeaders = [
|
|
|
53
54
|
* is that cache hits bypass origin metering: they are not counted against
|
|
54
55
|
* rate-limit quotas.
|
|
55
56
|
*
|
|
57
|
+
* Lookups are equally scoped: only requests whose matched route carries the
|
|
58
|
+
* {@link markCacheable} marker (applied by `Cache.response`) pay the store
|
|
59
|
+
* read, so GETs to non-cacheable routes skip the round trip entirely.
|
|
60
|
+
*
|
|
56
61
|
* The cache is a plain `Store.Store` — the same store the origin read cache uses
|
|
57
62
|
* — holding the response serialized to a string under a URL key. It is a no-op
|
|
58
63
|
* when no store is supplied.
|
|
@@ -68,8 +73,14 @@ export function middleware<environment extends Env>(
|
|
|
68
73
|
): MiddlewareHandler<environment> {
|
|
69
74
|
return async (c, next) => {
|
|
70
75
|
const { store } = options
|
|
76
|
+
// Pay the store lookup only when a matched handler is marked cacheable
|
|
77
|
+
// (mounted `Cache.response`); other GETs (authed management routes, docs,
|
|
78
|
+
// 404s) can never hit, so the read would be a wasted round trip.
|
|
71
79
|
const eligible =
|
|
72
|
-
store !== undefined &&
|
|
80
|
+
store !== undefined &&
|
|
81
|
+
c.req.method === 'GET' &&
|
|
82
|
+
!c.req.raw.headers.has('range') &&
|
|
83
|
+
isCacheableRoute(c)
|
|
73
84
|
const key = eligible ? cacheKey(c.req.url) : undefined
|
|
74
85
|
|
|
75
86
|
// Honor the standard request `Cache-Control` directives so a caller can
|
|
@@ -104,16 +115,41 @@ export function middleware<environment extends Env>(
|
|
|
104
115
|
if (c.res.headers.get('content-type')?.includes('text/event-stream')) return
|
|
105
116
|
|
|
106
117
|
// Store a public, sanitized copy; the client still receives the original
|
|
107
|
-
// (private, metered) response for this miss.
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
// (private, metered) response for this miss. Clone synchronously — the
|
|
119
|
+
// original body locks once it streams to the client — then serialize and
|
|
120
|
+
// write behind `waitUntil` so the miss response is not held open on the
|
|
121
|
+
// body read.
|
|
122
|
+
const copy = c.res.clone()
|
|
123
|
+
const persist = serialize(copy, policy).then((value) =>
|
|
124
|
+
store.put(key, value, { ttl: policy.maxAge * 1_000 }),
|
|
125
|
+
)
|
|
110
126
|
const waitUntil = getWaitUntil(c)
|
|
111
|
-
if (waitUntil) waitUntil(
|
|
112
|
-
else await
|
|
127
|
+
if (waitUntil) waitUntil(persist)
|
|
128
|
+
else await persist.catch(() => {})
|
|
113
129
|
return
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
// Marks handlers whose route publishes an edge-cache policy; the middleware
|
|
134
|
+
// pays the store lookup only when a matched handler carries it.
|
|
135
|
+
const cacheableSymbol = Symbol('tempoApi.edgeCacheable')
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Marks a route handler/middleware as publishing an edge-cache policy, so
|
|
139
|
+
* {@link middleware} looks its route up in the edge cache. `Cache.response`
|
|
140
|
+
* applies it; routes composed any other way never enter the shared cache.
|
|
141
|
+
*/
|
|
142
|
+
export function markCacheable<const handler extends (...args: never[]) => unknown>(
|
|
143
|
+
handler: handler,
|
|
144
|
+
): handler {
|
|
145
|
+
return Object.assign(handler, { [cacheableSymbol]: true })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// True when any handler matched for this request is marked cacheable.
|
|
149
|
+
function isCacheableRoute(c: Context): boolean {
|
|
150
|
+
return matchedRoutes(c).some((route) => cacheableSymbol in route.handler)
|
|
151
|
+
}
|
|
152
|
+
|
|
117
153
|
export declare namespace middleware {
|
|
118
154
|
/** Options for the edge-cache middleware. */
|
|
119
155
|
type Options = {
|
|
@@ -134,6 +170,8 @@ type Stored = {
|
|
|
134
170
|
status: number
|
|
135
171
|
}
|
|
136
172
|
|
|
173
|
+
// Consumes `response` (callers pass a dedicated clone; the client streams the
|
|
174
|
+
// original).
|
|
137
175
|
async function serialize(response: Response, policy: Policy): Promise<string> {
|
|
138
176
|
const headers = new Headers(response.headers)
|
|
139
177
|
for (const name of volatileHeaders) headers.delete(name)
|
|
@@ -145,8 +183,8 @@ async function serialize(response: Response, policy: Policy): Promise<string> {
|
|
|
145
183
|
const textual = isTextual(headers.get('content-type'))
|
|
146
184
|
const stored: Stored = {
|
|
147
185
|
body: textual
|
|
148
|
-
? await response.
|
|
149
|
-
: Base64.fromBytes(new Uint8Array(await response.
|
|
186
|
+
? await response.text()
|
|
187
|
+
: Base64.fromBytes(new Uint8Array(await response.arrayBuffer())),
|
|
150
188
|
...(textual ? {} : { encoding: 'base64' as const }),
|
|
151
189
|
headers: [...headers],
|
|
152
190
|
status: response.status,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Fees from './Fees.js'
|
|
2
|
+
|
|
3
|
+
describe('fromGas', () => {
|
|
4
|
+
test('behavior: scales attodollar gas math to token units with ceiling division', () => {
|
|
5
|
+
// 100k gas × 1e6 atto = 1e11 atto rounds up to one token unit.
|
|
6
|
+
expect(Fees.fromGas(100_000n, 1_000_000n)).toBe(1n)
|
|
7
|
+
// 1M gas × 5e11 atto = 5e17 atto = $0.50 exactly.
|
|
8
|
+
expect(Fees.fromGas(1_000_000n, 500_000_000_000n)).toBe(500_000n)
|
|
9
|
+
// Any remainder rounds up, mirroring `calc_gas_balance_spending`.
|
|
10
|
+
expect(Fees.fromGas(1n, 1_000_000_000_001n)).toBe(2n)
|
|
11
|
+
expect(Fees.fromGas(0n, 0n)).toBe(0n)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('maxOf', () => {
|
|
16
|
+
test('behavior: computes the signed cap from bigint or hex quantity fields', () => {
|
|
17
|
+
expect(Fees.maxOf({ gas: 1_000_000n, maxFeePerGas: 500_000_000_000n })).toBe(500_000n)
|
|
18
|
+
// Fills carry hex quantities: 0xf4240 = 1M gas, 0x746a528800 = 5e11 atto.
|
|
19
|
+
expect(Fees.maxOf({ gas: '0xf4240', maxFeePerGas: '0x746a528800' })).toBe(500_000n)
|
|
20
|
+
// Zero fields are computable (a zero cap), not absent.
|
|
21
|
+
expect(Fees.maxOf({ gas: 0n, maxFeePerGas: 0n })).toBe(0n)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('behavior: yields undefined when either fee field is missing or unparseable', () => {
|
|
25
|
+
expect(Fees.maxOf({})).toBeUndefined()
|
|
26
|
+
expect(Fees.maxOf({ gas: 21_000n })).toBeUndefined()
|
|
27
|
+
expect(Fees.maxOf({ maxFeePerGas: 1_000_000n })).toBeUndefined()
|
|
28
|
+
// Decimal strings are not hex quantities; callers fail closed.
|
|
29
|
+
expect(Fees.maxOf({ gas: '21000', maxFeePerGas: 1_000_000n })).toBeUndefined()
|
|
30
|
+
expect(Fees.maxOf({ gas: null, maxFeePerGas: 1_000_000n })).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
})
|