tapimo 0.4.1 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +1990 -1442
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1304 -1304
- package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +10 -120
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/fee-amm.d.ts +20 -20
- package/dist/apps/data/routes/funding.js +7 -7
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/management/App.d.ts +808 -267
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +116 -100
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +182 -50
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +100 -40
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +122 -33
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +4 -18
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +38 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/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 +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/internal/Auth.d.ts +46 -40
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +118 -69
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/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/FundingRoutes.d.ts +15 -43
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +8 -17
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.js.map +1 -1
- package/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 +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/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +104 -6
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +17 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/FundingRouteProviders.test.ts +37 -93
- package/src/apps/data/FundingRouteProviders.ts +14 -161
- package/src/apps/data/routes/funding.test.ts +7 -42
- package/src/apps/data/routes/funding.ts +8 -8
- package/src/apps/data/routes/indexer.test.ts +2 -2
- package/src/apps/data/routes/webhooks.test.ts +2 -2
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +72 -7
- package/src/apps/mcp.ts +46 -4
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/cloudflare.ts +5 -0
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/internal/Auth.test.ts +571 -23
- package/src/internal/Auth.ts +166 -94
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +8 -8
- package/src/internal/FundingRoutes.ts +25 -29
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -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/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
package/src/internal/Auth.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { findTargetHandler } from 'hono/utils/handler'
|
|
|
9
9
|
import { decodeJwt } from 'jose'
|
|
10
10
|
import { 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,7 @@ 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 defaultMppRateLimit = { perMinute: 100 } satisfies RateLimit.Limit
|
|
33
35
|
|
|
34
36
|
/** Auth context consumed by Tempo API handlers. */
|
|
35
37
|
export type Context = {
|
|
@@ -47,7 +49,7 @@ export type Context = {
|
|
|
47
49
|
}
|
|
48
50
|
| undefined
|
|
49
51
|
/** MPP payment configuration. */
|
|
50
|
-
mpp?:
|
|
52
|
+
mpp?: MppHandler | undefined
|
|
51
53
|
/**
|
|
52
54
|
* Configured default anonymous public quota. Used as the throttle ceiling for
|
|
53
55
|
* sandbox keys without active billing on routes that expose no public lane
|
|
@@ -89,22 +91,27 @@ export type Payment = {
|
|
|
89
91
|
type: 'mpp'
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
/** MPP
|
|
93
|
-
export
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
/** MPP route-policy types. Distinct from the SIWE {@link Session} capability. */
|
|
95
|
+
export declare namespace Mpp {
|
|
96
|
+
/** MPP Session request options used by route access policies. */
|
|
97
|
+
type Session = Method.RequestDefaults<ReturnType<typeof tempo.session>> & {
|
|
98
|
+
/** Optional human-readable payment description. */
|
|
99
|
+
description?: string | undefined
|
|
100
|
+
/** Optional challenge expiration timestamp or date. */
|
|
101
|
+
expires?: Date | string | undefined
|
|
102
|
+
/** Optional server-defined correlation data. */
|
|
103
|
+
meta?: Record<string, string> | undefined
|
|
104
|
+
/** Optional route/resource scope bound to the challenge. */
|
|
105
|
+
scope?: string | undefined
|
|
106
|
+
}
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
/** MPP payment handler consumed by auth policies. */
|
|
105
|
-
type
|
|
106
|
-
/**
|
|
107
|
-
|
|
110
|
+
type MppHandler = {
|
|
111
|
+
/** Paid-request quota per access principal. */
|
|
112
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
113
|
+
/** Creates a Hono middleware for a Tempo Session payment. */
|
|
114
|
+
session(options: Mpp.Session): MiddlewareHandler
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
/**
|
|
@@ -127,11 +134,13 @@ export type PolicyOverride = {
|
|
|
127
134
|
}
|
|
128
135
|
| boolean
|
|
129
136
|
| undefined
|
|
130
|
-
/** MPP paid lane: object to override the
|
|
137
|
+
/** MPP paid lane: object to override the Session request, or boolean to enable/disable. */
|
|
131
138
|
mpp?:
|
|
132
139
|
| {
|
|
133
|
-
/**
|
|
134
|
-
|
|
140
|
+
/** Paid-request quota override for this route (a protective per-route cap). */
|
|
141
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
142
|
+
/** Tempo Session request override for this route. */
|
|
143
|
+
session?: Mpp.Session | undefined
|
|
135
144
|
}
|
|
136
145
|
| boolean
|
|
137
146
|
| undefined
|
|
@@ -160,7 +169,7 @@ export type Principal =
|
|
|
160
169
|
orgId: string
|
|
161
170
|
/** Payment metadata for paid overflow requests. */
|
|
162
171
|
payment?: Payment | undefined
|
|
163
|
-
/**
|
|
172
|
+
/** Attributed project id, when present. */
|
|
164
173
|
projectId?: string | undefined
|
|
165
174
|
/** Principal kind. */
|
|
166
175
|
type: 'api_key'
|
|
@@ -205,9 +214,9 @@ export type Principal =
|
|
|
205
214
|
export type Variables = {
|
|
206
215
|
/** Auth context consumed by protected handlers. */
|
|
207
216
|
auth: Context
|
|
208
|
-
/** Caller's membership resolved by {@link ensureOrg}; absent for `super_admin`. */
|
|
217
|
+
/** Caller's membership resolved by {@link ensureOrg} or {@link ensureProject}; absent for API keys and `super_admin`. */
|
|
209
218
|
membership?: Memberships.Record | undefined
|
|
210
|
-
/** Organization resolved by {@link ensureOrg} for the current request. */
|
|
219
|
+
/** Organization resolved by {@link ensureOrg} or {@link ensureProject} for the current request. */
|
|
211
220
|
org?: Organizations.Record | undefined
|
|
212
221
|
/** Authenticated or anonymous caller. */
|
|
213
222
|
principal?: Principal | undefined
|
|
@@ -221,13 +230,7 @@ export function getPrincipal<environment extends Environment>(c: hono_Context<en
|
|
|
221
230
|
}
|
|
222
231
|
|
|
223
232
|
/**
|
|
224
|
-
* Guards
|
|
225
|
-
* are members of; the super admin reaches any. Missing or foreign
|
|
226
|
-
* organizations return `404`, so foreign org ids are indistinguishable from
|
|
227
|
-
* absent ones; a member below the required role gets `403` (the resource is
|
|
228
|
-
* visible to them, the action is not). The resolved organization and
|
|
229
|
-
* membership land on context — read them with {@link org} and
|
|
230
|
-
* {@link membership}.
|
|
233
|
+
* Guards `:orgId` routes and resolves the organization. Sessions require membership, API keys require ownership, and the super admin bypasses ownership.
|
|
231
234
|
*/
|
|
232
235
|
export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
|
|
233
236
|
const { role: minimum = 'member' } = options
|
|
@@ -243,11 +246,17 @@ export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
|
|
|
243
246
|
const orgId = c.req.param('orgId' as never) as string | undefined
|
|
244
247
|
if (!orgId) return notFound()
|
|
245
248
|
const principal = getPrincipal(c)
|
|
246
|
-
if (
|
|
249
|
+
if (
|
|
250
|
+
principal?.type !== 'api_key' &&
|
|
251
|
+
principal?.type !== 'session' &&
|
|
252
|
+
principal?.type !== 'super_admin'
|
|
253
|
+
)
|
|
254
|
+
return notFound()
|
|
255
|
+
if (principal.type === 'api_key' && principal.orgId !== orgId) return notFound()
|
|
247
256
|
const db = Db.get(contextDb(c))
|
|
248
257
|
const record = await Organizations.get(db, orgId)
|
|
249
258
|
if (!record) return notFound()
|
|
250
|
-
if (principal.type
|
|
259
|
+
if (principal.type === 'session') {
|
|
251
260
|
const member = await Memberships.get(db, orgId, principal.id)
|
|
252
261
|
if (!member) return notFound()
|
|
253
262
|
if (Memberships.rank[member.role] < Memberships.rank[minimum])
|
|
@@ -267,16 +276,13 @@ export function ensureOrg(options: ensureOrg.Options = {}): MiddlewareHandler {
|
|
|
267
276
|
export declare namespace ensureOrg {
|
|
268
277
|
/** Options for the org guard. */
|
|
269
278
|
type Options = {
|
|
270
|
-
/** Minimum role
|
|
279
|
+
/** Minimum session role; defaults to `member`. */
|
|
271
280
|
role?: Memberships.Role | undefined
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
/**
|
|
276
|
-
* Guards project
|
|
277
|
-
* which must run first. Projects resolve only within the scoped organization;
|
|
278
|
-
* missing or cross-org projects return `404`. The resolved project lands on
|
|
279
|
-
* context — read it with {@link project}.
|
|
285
|
+
* Guards `:projectId` routes and resolves the project. Flat routes authorize against its organization; nested routes preserve the scope from {@link ensureOrg}.
|
|
280
286
|
*/
|
|
281
287
|
export function ensureProject(): MiddlewareHandler {
|
|
282
288
|
// Typed loosely (like {@link policy}) so route chains keep their inferred
|
|
@@ -290,10 +296,32 @@ export function ensureProject(): MiddlewareHandler {
|
|
|
290
296
|
})
|
|
291
297
|
const projectId = c.req.param('projectId' as never) as string | undefined
|
|
292
298
|
if (!projectId) return notFound()
|
|
299
|
+
const principal = getPrincipal(c)
|
|
300
|
+
if (
|
|
301
|
+
principal?.type !== 'api_key' &&
|
|
302
|
+
principal?.type !== 'session' &&
|
|
303
|
+
principal?.type !== 'super_admin'
|
|
304
|
+
)
|
|
305
|
+
return notFound()
|
|
306
|
+
const db = Db.get(contextDb(c))
|
|
307
|
+
const record = await Projects.get(db, projectId)
|
|
308
|
+
if (!record) return notFound()
|
|
293
309
|
const scoped = c.get('org')
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
310
|
+
if (scoped) {
|
|
311
|
+
if (record.orgId !== scoped.id) return notFound()
|
|
312
|
+
c.set('project', record)
|
|
313
|
+
await next()
|
|
314
|
+
return undefined
|
|
315
|
+
}
|
|
316
|
+
if (principal.type === 'api_key' && principal.orgId !== record.orgId) return notFound()
|
|
317
|
+
const organization = await Organizations.get(db, record.orgId)
|
|
318
|
+
if (!organization) return notFound()
|
|
319
|
+
if (principal.type === 'session') {
|
|
320
|
+
const member = await Memberships.get(db, record.orgId, principal.id)
|
|
321
|
+
if (!member) return notFound()
|
|
322
|
+
c.set('membership', member)
|
|
323
|
+
}
|
|
324
|
+
c.set('org', organization)
|
|
297
325
|
c.set('project', record)
|
|
298
326
|
await next()
|
|
299
327
|
return undefined
|
|
@@ -318,7 +346,7 @@ export function ensureOrgError(c: hono_Context) {
|
|
|
318
346
|
|
|
319
347
|
/** Typed project-scope error response for Hono client inference. Never reached at runtime. */
|
|
320
348
|
export function ensureProjectError(c: hono_Context) {
|
|
321
|
-
//
|
|
349
|
+
// Nested project routes run the org scope first, so either not-found can surface.
|
|
322
350
|
const code =
|
|
323
351
|
c.req.header('tempo-narrow-scope-code') === 'organization_not_found'
|
|
324
352
|
? ('organization_not_found' as const)
|
|
@@ -330,19 +358,20 @@ export function ensureProjectError(c: hono_Context) {
|
|
|
330
358
|
})
|
|
331
359
|
}
|
|
332
360
|
|
|
333
|
-
/** Reads the caller's membership resolved by
|
|
361
|
+
/** Reads the caller's membership resolved by the organization or project guard; `undefined` for API keys and `super_admin`. */
|
|
334
362
|
export function membership<environment extends Environment>(
|
|
335
363
|
c: hono_Context<environment>,
|
|
336
364
|
): Memberships.Record | undefined {
|
|
337
365
|
return c.get('membership')
|
|
338
366
|
}
|
|
339
367
|
|
|
340
|
-
/** Reads the organization resolved by
|
|
368
|
+
/** Reads the organization resolved by the organization or project guard; throws when neither middleware ran. */
|
|
341
369
|
export function org<environment extends Environment>(
|
|
342
370
|
c: hono_Context<environment>,
|
|
343
371
|
): Organizations.Record {
|
|
344
372
|
const record = c.get('org')
|
|
345
|
-
if (!record)
|
|
373
|
+
if (!record)
|
|
374
|
+
throw new Error('`ensureOrg` or `ensureProject` must run before reading the organization')
|
|
346
375
|
return record
|
|
347
376
|
}
|
|
348
377
|
|
|
@@ -351,7 +380,7 @@ export function project<environment extends Environment>(
|
|
|
351
380
|
c: hono_Context<environment>,
|
|
352
381
|
): Projects.Record {
|
|
353
382
|
const record = c.get('project')
|
|
354
|
-
if (!record) throw new Error('`
|
|
383
|
+
if (!record) throw new Error('`ensureProject` must run before reading the project')
|
|
355
384
|
return record
|
|
356
385
|
}
|
|
357
386
|
|
|
@@ -445,7 +474,11 @@ export function middleware<
|
|
|
445
474
|
endpoint extends string = string,
|
|
446
475
|
environment extends Environment = Environment,
|
|
447
476
|
>(options: middleware.Options<endpoint>): MiddlewareHandler<environment> {
|
|
448
|
-
const {
|
|
477
|
+
const {
|
|
478
|
+
rateLimit: mppRateLimit = defaultMppRateLimit,
|
|
479
|
+
session: session_ = {},
|
|
480
|
+
...mpp
|
|
481
|
+
} = options.mpp === false ? {} : (options.mpp ?? {})
|
|
449
482
|
const defaults = laneDefaults(options)
|
|
450
483
|
// Default-deny: no lanes open until a route grants them via `policy`.
|
|
451
484
|
// Otherwise the `super_admin` principal bypasses lane checks entirely.
|
|
@@ -460,7 +493,9 @@ export function middleware<
|
|
|
460
493
|
const resolve = (() => {
|
|
461
494
|
const uncached = (c: hono_Context, token: string) => {
|
|
462
495
|
const kv = contextKv(c)
|
|
463
|
-
return kv
|
|
496
|
+
return kv
|
|
497
|
+
? ApiKeys.resolve(kv.store, token, { scopeCatalog: options.scopeCatalog })
|
|
498
|
+
: Promise.resolve(null)
|
|
464
499
|
}
|
|
465
500
|
if (options.apiKey === false || options.apiKey?.cache === false) return uncached
|
|
466
501
|
return cachedResolve(uncached, options.apiKey?.cache ?? {})
|
|
@@ -481,11 +516,23 @@ export function middleware<
|
|
|
481
516
|
...(options.mpp === false
|
|
482
517
|
? {}
|
|
483
518
|
: {
|
|
484
|
-
mpp:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
519
|
+
mpp: {
|
|
520
|
+
rateLimit: mppRateLimit,
|
|
521
|
+
session: Mppx.create({
|
|
522
|
+
...mpp,
|
|
523
|
+
// Defaults only; global `session` options may override, including
|
|
524
|
+
// enabling same-route HEAD bootstrap.
|
|
525
|
+
methods: [
|
|
526
|
+
tempo.session({
|
|
527
|
+
amount: '0.0001',
|
|
528
|
+
bootstrap: false,
|
|
529
|
+
currency: Addresses.pathUsd,
|
|
530
|
+
unitType: 'request',
|
|
531
|
+
...session_,
|
|
532
|
+
}),
|
|
533
|
+
],
|
|
534
|
+
}).session,
|
|
535
|
+
},
|
|
489
536
|
}),
|
|
490
537
|
rateLimit: RateLimit.memory(options.rateLimit),
|
|
491
538
|
...(options.session ? { session: options.session } : {}),
|
|
@@ -554,6 +601,8 @@ export declare namespace middleware {
|
|
|
554
601
|
| undefined
|
|
555
602
|
/** Rate-limit store options. */
|
|
556
603
|
rateLimit?: RateLimit.memory.Options | undefined
|
|
604
|
+
/** Scope catalog accepted while resolving API-key records. */
|
|
605
|
+
scopeCatalog?: Scope.Catalog | undefined
|
|
557
606
|
/** Session lane resolution — the session surface's capability; omit to leave the session lane closed. */
|
|
558
607
|
session?: Session | undefined
|
|
559
608
|
/**
|
|
@@ -576,8 +625,10 @@ export declare namespace middleware {
|
|
|
576
625
|
|
|
577
626
|
/** MPP payment options used by auth middleware. */
|
|
578
627
|
type MppOptions = Omit<NonNullable<Parameters<typeof Mppx.create>[0]>, 'methods'> & {
|
|
579
|
-
/**
|
|
580
|
-
|
|
628
|
+
/** Paid-request quota per access principal. Defaults to 100 per minute. */
|
|
629
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
630
|
+
/** Global Tempo Session options passed to the MPP method instantiator. */
|
|
631
|
+
session?: NonNullable<Parameters<typeof tempo.session>[0]> | undefined
|
|
581
632
|
}
|
|
582
633
|
}
|
|
583
634
|
|
|
@@ -678,7 +729,7 @@ function handlerError(description: string) {
|
|
|
678
729
|
* disables both.
|
|
679
730
|
*/
|
|
680
731
|
export function install(options: install.Options) {
|
|
681
|
-
const { db, kv } = options
|
|
732
|
+
const { db, kv, scopeCatalog } = options
|
|
682
733
|
const { session: options_session, ...options_middleware } =
|
|
683
734
|
options.auth === false ? {} : (options.auth ?? {})
|
|
684
735
|
if (options_session && !kv)
|
|
@@ -894,7 +945,11 @@ export function install(options: install.Options) {
|
|
|
894
945
|
const middleware_ =
|
|
895
946
|
options.auth === false
|
|
896
947
|
? undefined
|
|
897
|
-
: middleware({
|
|
948
|
+
: middleware({
|
|
949
|
+
...options_middleware,
|
|
950
|
+
...(app ? { session: app.session } : {}),
|
|
951
|
+
scopeCatalog,
|
|
952
|
+
})
|
|
898
953
|
|
|
899
954
|
return { app, middleware: middleware_ }
|
|
900
955
|
}
|
|
@@ -903,7 +958,7 @@ export declare namespace install {
|
|
|
903
958
|
/** Options for building the built-in auth pieces. */
|
|
904
959
|
type Options = {
|
|
905
960
|
auth:
|
|
906
|
-
| (Omit<middleware.Options, 'session'> & {
|
|
961
|
+
| (Omit<middleware.Options, 'scopeCatalog' | 'session'> & {
|
|
907
962
|
/** 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
963
|
session?: Session | undefined
|
|
909
964
|
})
|
|
@@ -913,6 +968,8 @@ export declare namespace install {
|
|
|
913
968
|
db: Db.Source
|
|
914
969
|
/** KV state store, or undefined when key auth is closed. */
|
|
915
970
|
kv: { store: Store.State } | undefined
|
|
971
|
+
/** Scope catalog accepted while resolving API-key records. */
|
|
972
|
+
scopeCatalog: Scope.Catalog
|
|
916
973
|
}
|
|
917
974
|
|
|
918
975
|
/** Session sign-in surface configuration: `true` for defaults, or a config object. */
|
|
@@ -1013,7 +1070,7 @@ function laneDefaults(options: middleware.Options): LaneDefaults {
|
|
|
1013
1070
|
// catalog scopes via `Auth.policy`.
|
|
1014
1071
|
scopes: apiKey?.scopes ?? [Scope.wildcard],
|
|
1015
1072
|
},
|
|
1016
|
-
mpp: {
|
|
1073
|
+
mpp: { session: {} },
|
|
1017
1074
|
public: { rateLimit: public_?.rateLimit ?? { perMinute: 60 } },
|
|
1018
1075
|
session: true,
|
|
1019
1076
|
}
|
|
@@ -1042,8 +1099,8 @@ function resolvePolicy(
|
|
|
1042
1099
|
// The OpenAPI summary/description rides along as the payment-challenge
|
|
1043
1100
|
// description. It is metadata, not a policy override, so it only applies when
|
|
1044
1101
|
// the paid lane is actually enabled and has no explicit description already.
|
|
1045
|
-
if (mpp && description !== undefined && mpp.
|
|
1046
|
-
mpp = {
|
|
1102
|
+
if (mpp && description !== undefined && mpp.session.description === undefined)
|
|
1103
|
+
mpp = { ...mpp, session: { ...mpp.session, description } }
|
|
1047
1104
|
|
|
1048
1105
|
// A lane-less policy stays enforced for super admin; only an explicit
|
|
1049
1106
|
// `false` override opts a route out of auth entirely.
|
|
@@ -1079,7 +1136,10 @@ function resolveMppLane(
|
|
|
1079
1136
|
if (override === false) return undefined
|
|
1080
1137
|
if (override === true) return current ?? default_
|
|
1081
1138
|
const base = current ?? default_
|
|
1082
|
-
return {
|
|
1139
|
+
return {
|
|
1140
|
+
rateLimit: override.rateLimit ?? base.rateLimit,
|
|
1141
|
+
session: { ...base.session, ...override.session },
|
|
1142
|
+
}
|
|
1083
1143
|
}
|
|
1084
1144
|
|
|
1085
1145
|
function resolvePublicLane(
|
|
@@ -1229,12 +1289,20 @@ export function require<environment extends Environment = Environment>(
|
|
|
1229
1289
|
return createMiddleware<environment>(async (c, next) => {
|
|
1230
1290
|
const auth = c.get('auth')
|
|
1231
1291
|
const credential = getApiKeyCredential(c)
|
|
1232
|
-
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1292
|
+
const hasQueryApiKey = new URL(c.req.url).searchParams.has('key')
|
|
1293
|
+
// Auth owns `key`; strict route schemas must not validate it.
|
|
1294
|
+
stripApiKeyQuery(c)
|
|
1295
|
+
const proceed = async () => {
|
|
1296
|
+
await next()
|
|
1297
|
+
// Query credential responses must not enter shared caches.
|
|
1298
|
+
const cacheControl = c.res.headers.get('Cache-Control')?.toLowerCase()
|
|
1299
|
+
if (
|
|
1300
|
+
hasQueryApiKey &&
|
|
1301
|
+
!cacheControl?.includes('no-store') &&
|
|
1302
|
+
!cacheControl?.includes('private')
|
|
1303
|
+
)
|
|
1304
|
+
c.res.headers.set('Cache-Control', 'private')
|
|
1305
|
+
}
|
|
1238
1306
|
if (credential === false)
|
|
1239
1307
|
return jsonError(c, {
|
|
1240
1308
|
code: 'api_key_malformed',
|
|
@@ -1248,7 +1316,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
1248
1316
|
// It must never reach KV resolution or scope/quota checks.
|
|
1249
1317
|
if (auth.superAdmin && ApiKey.hash(credential.token) === auth.superAdmin.tokenHash) {
|
|
1250
1318
|
c.set('principal', { actor: 'super_admin', id: 'super_admin', type: 'super_admin' })
|
|
1251
|
-
return
|
|
1319
|
+
return proceed()
|
|
1252
1320
|
}
|
|
1253
1321
|
|
|
1254
1322
|
const apiKey = await auth.apiKey?.resolve(c, credential.token)
|
|
@@ -1260,7 +1328,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
1260
1328
|
const session = await resolveSession(c, auth)
|
|
1261
1329
|
if (session) {
|
|
1262
1330
|
c.set('principal', session)
|
|
1263
|
-
return
|
|
1331
|
+
return proceed()
|
|
1264
1332
|
}
|
|
1265
1333
|
}
|
|
1266
1334
|
return jsonError(c, {
|
|
@@ -1319,7 +1387,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
1319
1387
|
const throttled = apiKey.environment === 'sandbox' && apiKey.billingActive !== true
|
|
1320
1388
|
|
|
1321
1389
|
if (hasPayment && !throttled)
|
|
1322
|
-
return requirePayment(c,
|
|
1390
|
+
return requirePayment(c, proceed, {
|
|
1323
1391
|
auth,
|
|
1324
1392
|
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
1325
1393
|
policy,
|
|
@@ -1351,7 +1419,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
1351
1419
|
// A throttled sandbox key gets a plain 429, never an MPP challenge —
|
|
1352
1420
|
// it must activate billing to lift the ceiling, not pay per request.
|
|
1353
1421
|
if (throttled) return rateLimitExceeded(c, result)
|
|
1354
|
-
return requirePaymentOrRateLimit(c,
|
|
1422
|
+
return requirePaymentOrRateLimit(c, proceed, {
|
|
1355
1423
|
auth,
|
|
1356
1424
|
payment: { reason: 'api_key_over_quota', type: 'mpp' },
|
|
1357
1425
|
policy,
|
|
@@ -1361,7 +1429,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
1361
1429
|
}
|
|
1362
1430
|
}
|
|
1363
1431
|
|
|
1364
|
-
return
|
|
1432
|
+
return proceed()
|
|
1365
1433
|
}
|
|
1366
1434
|
|
|
1367
1435
|
// Cookie sessions carry no Authorization credential; resolve the session
|
|
@@ -1371,14 +1439,14 @@ export function require<environment extends Environment = Environment>(
|
|
|
1371
1439
|
const session = await resolveSession(c, auth)
|
|
1372
1440
|
if (session) {
|
|
1373
1441
|
c.set('principal', session)
|
|
1374
|
-
return
|
|
1442
|
+
return proceed()
|
|
1375
1443
|
}
|
|
1376
1444
|
}
|
|
1377
1445
|
|
|
1378
1446
|
const publicPrincipal = principalFromPublic(c)
|
|
1379
1447
|
if (hasPayment) {
|
|
1380
1448
|
c.set('principal', publicPrincipal)
|
|
1381
|
-
return requirePayment(c,
|
|
1449
|
+
return requirePayment(c, proceed, {
|
|
1382
1450
|
auth,
|
|
1383
1451
|
payment: { reason: 'public_over_quota', type: 'mpp' },
|
|
1384
1452
|
policy,
|
|
@@ -1395,11 +1463,11 @@ export function require<environment extends Environment = Environment>(
|
|
|
1395
1463
|
if (result) setRateLimitHeaders(c, result)
|
|
1396
1464
|
if (!result || result.allowed) {
|
|
1397
1465
|
c.set('principal', publicPrincipal)
|
|
1398
|
-
return
|
|
1466
|
+
return proceed()
|
|
1399
1467
|
}
|
|
1400
1468
|
|
|
1401
1469
|
c.set('principal', publicPrincipal)
|
|
1402
|
-
return requirePaymentOrRateLimit(c,
|
|
1470
|
+
return requirePaymentOrRateLimit(c, proceed, {
|
|
1403
1471
|
auth,
|
|
1404
1472
|
payment: { reason: 'public_over_quota', type: 'mpp' },
|
|
1405
1473
|
policy,
|
|
@@ -1410,7 +1478,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
1410
1478
|
|
|
1411
1479
|
if (isPaymentEnabled(auth, policy)) {
|
|
1412
1480
|
c.set('principal', publicPrincipal)
|
|
1413
|
-
return requirePayment(c,
|
|
1481
|
+
return requirePayment(c, proceed, {
|
|
1414
1482
|
auth,
|
|
1415
1483
|
payment: { reason: 'public_over_quota', type: 'mpp' },
|
|
1416
1484
|
policy,
|
|
@@ -1445,8 +1513,10 @@ export declare namespace require {
|
|
|
1445
1513
|
/** MPP payment access policy. */
|
|
1446
1514
|
mpp?:
|
|
1447
1515
|
| {
|
|
1448
|
-
/**
|
|
1449
|
-
|
|
1516
|
+
/** Paid-request quota override for this route (a protective per-route cap). */
|
|
1517
|
+
rateLimit?: RateLimit.Limit | undefined
|
|
1518
|
+
/** Tempo Session request policy. */
|
|
1519
|
+
session: Mpp.Session
|
|
1450
1520
|
}
|
|
1451
1521
|
| undefined
|
|
1452
1522
|
/** Public access policy. */
|
|
@@ -1574,10 +1644,8 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
|
|
|
1574
1644
|
const header = c.req.header('tempo-api-key')
|
|
1575
1645
|
if (header) return { token: header }
|
|
1576
1646
|
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
// carrying only a payment scheme falls through to the deprecated lanes below
|
|
1580
|
-
// (which find nothing) and resolves to no API-key credential.
|
|
1647
|
+
// Bearer supports clients unable to set the canonical header. Payment
|
|
1648
|
+
// credentials belong to MPP and must pass through untouched.
|
|
1581
1649
|
const authorization = c.req.header('authorization')
|
|
1582
1650
|
if (authorization && !Credential.extractPaymentScheme(authorization)) {
|
|
1583
1651
|
const match = /^Bearer\s+(.+)$/i.exec(authorization)
|
|
@@ -1585,26 +1653,18 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
|
|
|
1585
1653
|
return { token: match[1] }
|
|
1586
1654
|
}
|
|
1587
1655
|
|
|
1588
|
-
//
|
|
1589
|
-
// keep authenticating: the `x-api-key` header and the `key` query parameter.
|
|
1656
|
+
// `x-api-key` remains a deprecated compatibility header.
|
|
1590
1657
|
const legacyHeader = c.req.header('x-api-key')
|
|
1591
1658
|
if (legacyHeader) return { token: legacyHeader }
|
|
1592
1659
|
|
|
1593
|
-
const
|
|
1594
|
-
if (
|
|
1660
|
+
const apiKey = c.req.query('key')
|
|
1661
|
+
if (apiKey) return { token: apiKey }
|
|
1595
1662
|
|
|
1596
1663
|
return null
|
|
1597
1664
|
}
|
|
1598
1665
|
|
|
1599
|
-
/**
|
|
1600
|
-
|
|
1601
|
-
* been read for authentication. Route query schemas are strict
|
|
1602
|
-
* (`z.strictObject`), so leaving `key` in the query string would make the
|
|
1603
|
-
* downstream validator reject the request as `query_invalid`. Stripping it
|
|
1604
|
-
* keeps the legacy `?key=...` auth lane working on strict-query endpoints
|
|
1605
|
-
* (e.g. `GET /v1/tokens`).
|
|
1606
|
-
*/
|
|
1607
|
-
function stripLegacyAuthQuery(c: hono_Context) {
|
|
1666
|
+
/** Removes the reserved auth query parameter before route validation. */
|
|
1667
|
+
function stripApiKeyQuery(c: hono_Context) {
|
|
1608
1668
|
const url = new URL(c.req.url)
|
|
1609
1669
|
if (!url.searchParams.has('key')) return
|
|
1610
1670
|
url.searchParams.delete('key')
|
|
@@ -1701,7 +1761,19 @@ async function requirePayment<environment extends Environment>(
|
|
|
1701
1761
|
next: () => Promise<void>,
|
|
1702
1762
|
options: PaymentAccess,
|
|
1703
1763
|
) {
|
|
1704
|
-
|
|
1764
|
+
if (hasPaymentCredential(c)) {
|
|
1765
|
+
const result = await consumeRateLimit(options.auth, {
|
|
1766
|
+
key: `mpp:${options.principal.type}:${options.principal.id}`,
|
|
1767
|
+
limit: options.policy.mpp?.rateLimit ?? options.auth.mpp?.rateLimit ?? defaultMppRateLimit,
|
|
1768
|
+
})
|
|
1769
|
+
if (result) {
|
|
1770
|
+
setRateLimitHeaders(c, result)
|
|
1771
|
+
c.header('RateLimit-Scope', 'mpp')
|
|
1772
|
+
if (!result.allowed) return rateLimitExceeded(c, result)
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
const middleware = options.auth.mpp?.session(options.policy.mpp?.session ?? {})
|
|
1705
1777
|
if (!middleware)
|
|
1706
1778
|
return jsonError(c, {
|
|
1707
1779
|
code: 'payment_required',
|
|
@@ -18,6 +18,14 @@ describe('Cursor', () => {
|
|
|
18
18
|
])
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
+
test('round-trips a timestamp/id tuple', () => {
|
|
22
|
+
const token = Cursor.encode(['2026-01-01T00:00:00.000Z', 'org_abc123'])
|
|
23
|
+
expect(Cursor.decode(token, ['timestamp', 'id'])).toEqual([
|
|
24
|
+
'2026-01-01T00:00:00.000Z',
|
|
25
|
+
'org_abc123',
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
21
29
|
test('returns undefined for malformed, mis-typed, or wrong-arity tokens', () => {
|
|
22
30
|
expect(Cursor.decode('not-base64!!', ['int', 'int'])).toBeUndefined()
|
|
23
31
|
expect(Cursor.decode(btoa('{"oops":true}'), ['int', 'int'])).toBeUndefined()
|
|
@@ -27,6 +35,7 @@ describe('Cursor', () => {
|
|
|
27
35
|
expect(Cursor.decode(btoa('[-1,2]'), ['int', 'int'])).toBeUndefined()
|
|
28
36
|
expect(Cursor.decode(btoa('["x",2]'), ['uint', 'int'])).toBeUndefined()
|
|
29
37
|
expect(Cursor.decode(btoa('["0xnotanaddress",2]'), ['address', 'int'])).toBeUndefined()
|
|
38
|
+
expect(Cursor.decode(btoa('["org_abc\' OR 1=1"]'), ['id'])).toBeUndefined()
|
|
30
39
|
})
|
|
31
40
|
|
|
32
41
|
test('renders injection-safe SQL literals per field kind', () => {
|
|
@@ -35,6 +44,7 @@ describe('Cursor', () => {
|
|
|
35
44
|
expect(Cursor.literal('0xAbC0000000000000000000000000000000000000', 'address')).toBe(
|
|
36
45
|
"'0xabc0000000000000000000000000000000000000'",
|
|
37
46
|
)
|
|
47
|
+
expect(Cursor.literal('org_abc123', 'id')).toBe("'org_abc123'")
|
|
38
48
|
expect(Cursor.literal('2024-01-01T00:00:00Z', 'timestamp')).toBe("'2024-01-01T00:00:00Z'")
|
|
39
49
|
})
|
|
40
50
|
|
package/src/internal/Cursor.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
export type Cursor = (string | number)[]
|
|
17
17
|
|
|
18
18
|
/** The kind of a cursor field, determining its validation and SQL literal form. */
|
|
19
|
-
export type Field = '
|
|
19
|
+
export type Field = 'address' | 'id' | 'int' | 'timestamp' | 'uint'
|
|
20
20
|
|
|
21
21
|
/** Encodes a positional tuple as an opaque, URL-safe token. */
|
|
22
22
|
export function encode(values: Cursor): string {
|
|
@@ -76,6 +76,8 @@ export function literal(value: string | number, field: Field): string {
|
|
|
76
76
|
return String(value)
|
|
77
77
|
case 'address':
|
|
78
78
|
return `'${String(value).toLowerCase()}'`
|
|
79
|
+
case 'id':
|
|
80
|
+
return `'${String(value)}'`
|
|
79
81
|
case 'timestamp':
|
|
80
82
|
return `'${String(value)}'`
|
|
81
83
|
}
|
|
@@ -135,6 +137,8 @@ function isValid(value: unknown, field: Field): boolean {
|
|
|
135
137
|
return typeof value === 'string' && /^\d+$/.test(value)
|
|
136
138
|
case 'address':
|
|
137
139
|
return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)
|
|
140
|
+
case 'id':
|
|
141
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]+$/.test(value)
|
|
138
142
|
case 'timestamp':
|
|
139
143
|
return typeof value === 'string' && !Number.isNaN(Date.parse(value))
|
|
140
144
|
}
|
|
@@ -51,6 +51,17 @@ describe('middleware', () => {
|
|
|
51
51
|
expect(store.size).toMatchInlineSnapshot(`1`)
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
test('bypasses the cache for query API key credentials', async () => {
|
|
55
|
+
const { app, store, downstreamCalls } = createApp()
|
|
56
|
+
|
|
57
|
+
await app.request('/cached?key=opaque-secret')
|
|
58
|
+
await app.request('/cached?key=opaque-secret')
|
|
59
|
+
|
|
60
|
+
expect(downstreamCalls()).toMatchInlineSnapshot(`2`)
|
|
61
|
+
expect(store.gets).toMatchInlineSnapshot(`0`)
|
|
62
|
+
expect(store.size).toMatchInlineSnapshot(`0`)
|
|
63
|
+
})
|
|
64
|
+
|
|
54
65
|
test('refreshes the entry when the request sends `Cache-Control: no-cache`', async () => {
|
|
55
66
|
const { app, store, downstreamCalls } = createApp()
|
|
56
67
|
|
|
@@ -80,6 +80,8 @@ export function middleware<environment extends Env>(
|
|
|
80
80
|
store !== undefined &&
|
|
81
81
|
c.req.method === 'GET' &&
|
|
82
82
|
!c.req.raw.headers.has('range') &&
|
|
83
|
+
// Query credentials must reach auth and never enter cache keys.
|
|
84
|
+
!new URL(c.req.url).searchParams.has('key') &&
|
|
83
85
|
isCacheableRoute(c)
|
|
84
86
|
const key = eligible ? cacheKey(c.req.url) : undefined
|
|
85
87
|
|