@voyant-travel/hono 0.128.0 → 0.128.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +0 -6
- package/dist/index.d.ts +1 -1
- package/dist/middleware/auth.js +37 -2
- package/dist/middleware/index.d.ts +1 -2
- package/dist/middleware/index.js +1 -2
- package/dist/middleware/metrics.d.ts +8 -19
- package/dist/middleware/metrics.js +3 -8
- package/dist/middleware/rate-limit.d.ts +7 -50
- package/dist/middleware/rate-limit.js +3 -75
- package/dist/middleware/require-actor.js +4 -2
- package/dist/types.d.ts +4 -17
- package/package.json +6 -7
package/dist/app.js
CHANGED
|
@@ -19,7 +19,6 @@ import { cors } from "./middleware/cors.js";
|
|
|
19
19
|
import { db } from "./middleware/db.js";
|
|
20
20
|
import { handleApiError, requestId } from "./middleware/error-boundary.js";
|
|
21
21
|
import { logger } from "./middleware/logger.js";
|
|
22
|
-
import { metrics } from "./middleware/metrics.js";
|
|
23
22
|
import { publicResponseCache } from "./middleware/public-cache.js";
|
|
24
23
|
import { rateLimit, resolveRateLimitStore, } from "./middleware/rate-limit.js";
|
|
25
24
|
import { requireActor } from "./middleware/require-actor.js";
|
|
@@ -372,11 +371,6 @@ export function mountApp(config) {
|
|
|
372
371
|
app.use("*", requestId);
|
|
373
372
|
// Structured logger
|
|
374
373
|
app.use("*", logger(config.logger));
|
|
375
|
-
// Per-request metrics → Analytics Engine (no-op without the binding).
|
|
376
|
-
// Mounted before the cache middleware so cache hits are measured too.
|
|
377
|
-
if (config.metrics !== false) {
|
|
378
|
-
app.use("*", metrics());
|
|
379
|
-
}
|
|
380
374
|
// CORS (allowlist via env CORS_ALLOWLIST)
|
|
381
375
|
app.use("*", cors());
|
|
382
376
|
if (config.securityHeaders !== false) {
|
package/dist/index.d.ts
CHANGED
|
@@ -20,5 +20,5 @@ export { stampOpenApiRegistryApiId } from "./openapi-ownership.js";
|
|
|
20
20
|
export { openApiValidationHook } from "./openapi-validation.js";
|
|
21
21
|
export type { CreatePublicCapabilityOptions, PublicCapabilityCookieOptions, PublicCapabilityPayload, VerifyPublicCapabilityOptions, } from "./public-capability.js";
|
|
22
22
|
export { createPublicCapabilityToken, extractPublicCapabilityToken, serializePublicCapabilityCookie, verifyPublicCapabilityToken, } from "./public-capability.js";
|
|
23
|
-
export type { DbFactory, DbFactorySelector, DbSource, DbSurfaceSelection, LogEntry, LoggerProvider, VoyantAppConfig, VoyantAuthIntegration, VoyantAuthPermissionArgs, VoyantAuthResolveArgs, VoyantBindings, VoyantDb, VoyantExecutionContext, VoyantQueryRuntime, VoyantRequestAuthContext, VoyantRouteHandler, VoyantVariables, } from "./types.js";
|
|
23
|
+
export type { DbFactory, DbFactorySelector, DbSource, DbSurfaceSelection, LogEntry, LoggerProvider, VoyantAppConfig, VoyantAuthAppTokenResolveArgs, VoyantAuthIntegration, VoyantAuthPermissionArgs, VoyantAuthResolveArgs, VoyantBindings, VoyantDb, VoyantExecutionContext, VoyantQueryRuntime, VoyantRequestAuthContext, VoyantRouteHandler, VoyantVariables, } from "./types.js";
|
|
24
24
|
export { ApiHttpError, ForbiddenApiError, normalizeValidationError, parseJsonBody, parseOptionalJsonBody, parseQuery, RequestValidationError, UnauthorizedApiError, } from "./validation.js";
|
package/dist/middleware/auth.js
CHANGED
|
@@ -139,6 +139,19 @@ function applyAuthContext(c, auth) {
|
|
|
139
139
|
c.set("apiTokenId", auth.apiTokenId);
|
|
140
140
|
if (auth.apiKeyId)
|
|
141
141
|
c.set("apiKeyId", auth.apiKeyId);
|
|
142
|
+
if (auth.appId)
|
|
143
|
+
c.set("appId", auth.appId);
|
|
144
|
+
if (auth.appInstallationId)
|
|
145
|
+
c.set("appInstallationId", auth.appInstallationId);
|
|
146
|
+
if (auth.appReleaseId)
|
|
147
|
+
c.set("appReleaseId", auth.appReleaseId);
|
|
148
|
+
if (auth.appCredentialGeneration !== undefined) {
|
|
149
|
+
c.set("appCredentialGeneration", auth.appCredentialGeneration);
|
|
150
|
+
}
|
|
151
|
+
if (auth.appTokenMode)
|
|
152
|
+
c.set("appTokenMode", auth.appTokenMode);
|
|
153
|
+
if (auth.appViewerId)
|
|
154
|
+
c.set("appViewerId", auth.appViewerId);
|
|
142
155
|
}
|
|
143
156
|
export function requireAuth(dbSource, opts) {
|
|
144
157
|
const publicPaths = opts?.publicPaths ?? [];
|
|
@@ -270,7 +283,29 @@ export function requireAuth(dbSource, opts) {
|
|
|
270
283
|
await lease.release();
|
|
271
284
|
}
|
|
272
285
|
}
|
|
273
|
-
// Strategy 3:
|
|
286
|
+
// Strategy 3: Remote app access token support
|
|
287
|
+
if (token && opts?.auth?.resolveAppToken) {
|
|
288
|
+
const lease = acquireRequestDb(c, dbFactory);
|
|
289
|
+
try {
|
|
290
|
+
const resolved = await opts.auth.resolveAppToken({
|
|
291
|
+
request: c.req.raw,
|
|
292
|
+
env: c.env,
|
|
293
|
+
db: lease.db,
|
|
294
|
+
// Guarded: Hono throws on `executionCtx` access outside Workers.
|
|
295
|
+
ctx: tryGetExecutionCtx(c),
|
|
296
|
+
token,
|
|
297
|
+
});
|
|
298
|
+
if (resolved?.callerType === "app" && resolved.appInstallationId) {
|
|
299
|
+
applyAuthContext(c, resolved);
|
|
300
|
+
// `await` is load-bearing — see strategy 2.
|
|
301
|
+
return await next();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
await lease.release();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Strategy 4: App-provided auth resolution (cookies, provider tokens, etc.)
|
|
274
309
|
if (opts?.auth?.resolve) {
|
|
275
310
|
const lease = acquireRequestDb(c, dbFactory);
|
|
276
311
|
try {
|
|
@@ -293,7 +328,7 @@ export function requireAuth(dbSource, opts) {
|
|
|
293
328
|
await lease.release();
|
|
294
329
|
}
|
|
295
330
|
}
|
|
296
|
-
// Strategy
|
|
331
|
+
// Strategy 5: Generic session-claims bearer token support
|
|
297
332
|
const sessionSecret = c.env.SESSION_CLAIMS_SECRET;
|
|
298
333
|
if (token && sessionSecret && token.includes(".")) {
|
|
299
334
|
try {
|
|
@@ -5,9 +5,8 @@ export { db } from "./db.js";
|
|
|
5
5
|
export { errorBoundary, type HandleApiErrorOptions, handleApiError, requestId, } from "./error-boundary.js";
|
|
6
6
|
export { DEFAULT_IDEMPOTENCY_TTL_MS, type IdempotencyKeyOptions, idempotencyKey, purgeExpiredIdempotencyKeys, } from "./idempotency-key.js";
|
|
7
7
|
export { consoleLoggerProvider, logger } from "./logger.js";
|
|
8
|
-
export { type AnalyticsEngineDatasetLike, DB_METRICS_CONTEXT_KEY, type MetricsMiddlewareOptions, metrics, type RequestDbMetrics, withQueryCounting, } from "./metrics.js";
|
|
9
8
|
export { type PublicCacheOptions, publicResponseCache, resetPublicCacheStateForTests, } from "./public-cache.js";
|
|
10
|
-
export {
|
|
9
|
+
export { clientIpKey, createMemoryRateLimitStore, createRedisRateLimitStore, enforceRateLimit, LIVE_LIMITS, type RateLimitConfig, type RateLimitPolicy, type RateLimitRequestContext, type RateLimitResult, type RateLimitRule, type RateLimitStore, type RedisRateLimitStoreOptions, rateLimit, resolveRateLimitStore, } from "./rate-limit.js";
|
|
11
10
|
export { isStaffRbacEnforced, requireActor } from "./require-actor.js";
|
|
12
11
|
export { requirePermission } from "./require-permission.js";
|
|
13
12
|
export { type SecurityHeadersOptions, securityHeaders } from "./security-headers.js";
|
package/dist/middleware/index.js
CHANGED
|
@@ -5,9 +5,8 @@ export { db } from "./db.js";
|
|
|
5
5
|
export { errorBoundary, handleApiError, requestId, } from "./error-boundary.js";
|
|
6
6
|
export { DEFAULT_IDEMPOTENCY_TTL_MS, idempotencyKey, purgeExpiredIdempotencyKeys, } from "./idempotency-key.js";
|
|
7
7
|
export { consoleLoggerProvider, logger } from "./logger.js";
|
|
8
|
-
export { DB_METRICS_CONTEXT_KEY, metrics, withQueryCounting, } from "./metrics.js";
|
|
9
8
|
export { publicResponseCache, resetPublicCacheStateForTests, } from "./public-cache.js";
|
|
10
|
-
export { clientIpKey,
|
|
9
|
+
export { clientIpKey, createMemoryRateLimitStore, createRedisRateLimitStore, enforceRateLimit, LIVE_LIMITS, rateLimit, resolveRateLimitStore, } from "./rate-limit.js";
|
|
11
10
|
export { isStaffRbacEnforced, requireActor } from "./require-actor.js";
|
|
12
11
|
export { requirePermission } from "./require-permission.js";
|
|
13
12
|
export { securityHeaders } from "./security-headers.js";
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from "hono";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Structural shape of a Workers Analytics Engine dataset binding —
|
|
5
|
-
* declared locally so `@voyant-travel/hono` needs no `@cloudflare/workers-types`
|
|
6
|
-
* dependency.
|
|
7
|
-
*/
|
|
2
|
+
/** Structural sink contract for request metrics. */
|
|
8
3
|
export interface AnalyticsEngineDatasetLike {
|
|
9
4
|
writeDataPoint(point: {
|
|
10
5
|
blobs?: string[];
|
|
@@ -18,19 +13,11 @@ export interface RequestDbMetrics {
|
|
|
18
13
|
}
|
|
19
14
|
export declare const DB_METRICS_CONTEXT_KEY = "__voyantDbMetrics";
|
|
20
15
|
export interface MetricsMiddlewareOptions {
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
* `env.METRICS`. Returning undefined makes the middleware a no-op for
|
|
24
|
-
* that request (deployments without the binding pay ~nothing).
|
|
25
|
-
*/
|
|
26
|
-
dataset?: (env: unknown) => AnalyticsEngineDatasetLike | undefined;
|
|
16
|
+
/** Resolve the metrics sink for a request. Returning undefined is a no-op. */
|
|
17
|
+
dataset: (env: unknown) => AnalyticsEngineDatasetLike | undefined;
|
|
27
18
|
}
|
|
28
19
|
/**
|
|
29
|
-
* Per-request metrics
|
|
30
|
-
* 3.4, the in-worker half — the platform dispatcher's DISPATCH_METRICS
|
|
31
|
-
* dataset already records hostname/plan/cache/duration per dispatch;
|
|
32
|
-
* this adds what only the worker can see: the matched route pattern,
|
|
33
|
-
* the db query count, and in-worker cache hits).
|
|
20
|
+
* Per-request metrics for an explicitly supplied deployment sink.
|
|
34
21
|
*
|
|
35
22
|
* One data point per request:
|
|
36
23
|
* - blobs: [method, routePattern, surface, cacheStatus]
|
|
@@ -40,8 +27,10 @@ export interface MetricsMiddlewareOptions {
|
|
|
40
27
|
* `writeDataPoint` is fire-and-forget and never throws into the
|
|
41
28
|
* request; a missing binding short-circuits before timing overhead.
|
|
42
29
|
*/
|
|
43
|
-
export declare function metrics(options
|
|
44
|
-
Variables:
|
|
30
|
+
export declare function metrics(options: MetricsMiddlewareOptions): MiddlewareHandler<{
|
|
31
|
+
Variables: {
|
|
32
|
+
[DB_METRICS_CONTEXT_KEY]?: RequestDbMetrics;
|
|
33
|
+
};
|
|
45
34
|
}>;
|
|
46
35
|
/**
|
|
47
36
|
* Wrap a drizzle client so top-level query-initiating calls increment
|
|
@@ -9,11 +9,7 @@ function surfaceOf(path) {
|
|
|
9
9
|
return "other";
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Per-request metrics
|
|
13
|
-
* 3.4, the in-worker half — the platform dispatcher's DISPATCH_METRICS
|
|
14
|
-
* dataset already records hostname/plan/cache/duration per dispatch;
|
|
15
|
-
* this adds what only the worker can see: the matched route pattern,
|
|
16
|
-
* the db query count, and in-worker cache hits).
|
|
12
|
+
* Per-request metrics for an explicitly supplied deployment sink.
|
|
17
13
|
*
|
|
18
14
|
* One data point per request:
|
|
19
15
|
* - blobs: [method, routePattern, surface, cacheStatus]
|
|
@@ -23,9 +19,8 @@ function surfaceOf(path) {
|
|
|
23
19
|
* `writeDataPoint` is fire-and-forget and never throws into the
|
|
24
20
|
* request; a missing binding short-circuits before timing overhead.
|
|
25
21
|
*/
|
|
26
|
-
export function metrics(options
|
|
27
|
-
const resolveDataset = options.dataset
|
|
28
|
-
((env) => env?.METRICS);
|
|
22
|
+
export function metrics(options) {
|
|
23
|
+
const resolveDataset = options.dataset;
|
|
29
24
|
return async (c, next) => {
|
|
30
25
|
const dataset = resolveDataset(c.env);
|
|
31
26
|
if (!dataset || typeof dataset.writeDataPoint !== "function") {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { KVStore } from "@voyant-travel/utils/cache";
|
|
2
1
|
import { type LazyRedisClient } from "@voyant-travel/utils/redis-client";
|
|
3
2
|
import type { MiddlewareHandler } from "hono";
|
|
4
3
|
/**
|
|
@@ -6,11 +5,9 @@ import type { MiddlewareHandler } from "hono";
|
|
|
6
5
|
*
|
|
7
6
|
* The limiter is split into two halves:
|
|
8
7
|
*
|
|
9
|
-
* - a {@link RateLimitStore} — the counting backend.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* fixed-window counter (best-effort — see {@link createKvRateLimitStore}),
|
|
13
|
-
* and an in-memory Map for Node/dev/tests (per-isolate).
|
|
8
|
+
* - a {@link RateLimitStore} — the counting backend. Composition injects the
|
|
9
|
+
* selected provider; an in-memory Map remains the zero-configuration
|
|
10
|
+
* fallback for Node/dev/tests.
|
|
14
11
|
* - the enforcement surface — the {@link rateLimit} Hono middleware and the
|
|
15
12
|
* imperative {@link enforceRateLimit} helper that route packages
|
|
16
13
|
* (storefront, bookings, …) call directly inside handlers.
|
|
@@ -73,9 +70,8 @@ export interface RateLimitPolicy {
|
|
|
73
70
|
export interface RateLimitConfig {
|
|
74
71
|
/**
|
|
75
72
|
* Explicit store, or a function-of-bindings for stores built from env
|
|
76
|
-
* bindings
|
|
77
|
-
*
|
|
78
|
-
* to `c.env.RATE_LIMITER` (CF binding) → `c.env.RATE_LIMIT` (KV) →
|
|
73
|
+
* bindings. When omitted — or when the function returns `undefined` —
|
|
74
|
+
* resolution falls through to the injected `RATE_LIMIT_STORE`, then
|
|
79
75
|
* in-memory.
|
|
80
76
|
*/
|
|
81
77
|
store?: RateLimitStore | ((env: unknown) => RateLimitStore | undefined);
|
|
@@ -92,21 +88,6 @@ export interface RateLimitRule {
|
|
|
92
88
|
max: number;
|
|
93
89
|
windowSeconds: number;
|
|
94
90
|
}
|
|
95
|
-
/**
|
|
96
|
-
* The Cloudflare native Rate Limiting binding shape (wrangler
|
|
97
|
-
* `[[ratelimits]]`). The binding's `limit`/`period` are fixed in wrangler
|
|
98
|
-
* config — the policy's `max`/`windowSeconds` are advisory for headers
|
|
99
|
-
* only. One shared binding works across policies because the bucket is
|
|
100
|
-
* part of the key; deployments wanting different limits per policy bind
|
|
101
|
-
* one ratelimiter per policy and pass it via `policy.store`.
|
|
102
|
-
*/
|
|
103
|
-
export interface CloudflareRateLimiterBinding {
|
|
104
|
-
limit(opts: {
|
|
105
|
-
key: string;
|
|
106
|
-
}): Promise<{
|
|
107
|
-
success: boolean;
|
|
108
|
-
}>;
|
|
109
|
-
}
|
|
110
91
|
/**
|
|
111
92
|
* Minimal structural view of a Hono context — enough for key derivation,
|
|
112
93
|
* store resolution, and response headers — so route packages can call
|
|
@@ -142,30 +123,6 @@ export declare function clientIpKey(c: {
|
|
|
142
123
|
export declare function createMemoryRateLimitStore(options?: {
|
|
143
124
|
maxEntries?: number;
|
|
144
125
|
}): RateLimitStore;
|
|
145
|
-
/**
|
|
146
|
-
* KV-backed fixed-window store.
|
|
147
|
-
*
|
|
148
|
-
* **Best-effort by construction**: KV is eventually consistent and the
|
|
149
|
-
* counter is a non-atomic read-modify-write, so concurrent requests
|
|
150
|
-
* across PoPs undercount — a determined attacker gets somewhat more than
|
|
151
|
-
* `max` through before the window converges. With per-client keys this
|
|
152
|
-
* is still a real brake on brute force and write floods; deployments
|
|
153
|
-
* needing exact distributed limits should bind the Cloudflare Rate
|
|
154
|
-
* Limiting binding (`RATE_LIMITER`) instead.
|
|
155
|
-
*
|
|
156
|
-
* Stored keys are `lim:<bucket>:<clientKey>:<windowKey>` (the window
|
|
157
|
-
* suffix is appended here) with a TTL of `max(60, windowSeconds * 2)` —
|
|
158
|
-
* KV's TTL floor is 60s.
|
|
159
|
-
*/
|
|
160
|
-
export declare function createKvRateLimitStore(kv: KVStore): RateLimitStore;
|
|
161
|
-
/**
|
|
162
|
-
* Adapter for the Cloudflare native Rate Limiting binding. Truly
|
|
163
|
-
* distributed and atomic; the actual limit/period are fixed in wrangler
|
|
164
|
-
* config, so the policy's `max`/`windowSeconds` only inform the
|
|
165
|
-
* `Retry-After` hint. `remaining` is never reported (the binding does
|
|
166
|
-
* not expose it).
|
|
167
|
-
*/
|
|
168
|
-
export declare function createCloudflareRateLimitStore(binding: CloudflareRateLimiterBinding): RateLimitStore;
|
|
169
126
|
export interface RedisRateLimitStoreOptions {
|
|
170
127
|
client?: LazyRedisClient;
|
|
171
128
|
}
|
|
@@ -174,8 +131,8 @@ export declare function createRedisRateLimitStore(redisUrl: string, options?: Re
|
|
|
174
131
|
export declare function resetRateLimitWarningsForTests(): void;
|
|
175
132
|
/**
|
|
176
133
|
* Resolve the best available store from the environment:
|
|
177
|
-
* `c.env.
|
|
178
|
-
*
|
|
134
|
+
* Injected `c.env.RATE_LIMIT_STORE` → in-memory fallback. **Fails open into
|
|
135
|
+
* the memory store** —
|
|
179
136
|
* rate limiting must never break Node/headless deployments that bind
|
|
180
137
|
* neither — but warns once per isolate outside dev/test so a
|
|
181
138
|
* production deploy without a distributed backend is visible in logs.
|
|
@@ -67,58 +67,6 @@ export function createMemoryRateLimitStore(options) {
|
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
* KV-backed fixed-window store.
|
|
72
|
-
*
|
|
73
|
-
* **Best-effort by construction**: KV is eventually consistent and the
|
|
74
|
-
* counter is a non-atomic read-modify-write, so concurrent requests
|
|
75
|
-
* across PoPs undercount — a determined attacker gets somewhat more than
|
|
76
|
-
* `max` through before the window converges. With per-client keys this
|
|
77
|
-
* is still a real brake on brute force and write floods; deployments
|
|
78
|
-
* needing exact distributed limits should bind the Cloudflare Rate
|
|
79
|
-
* Limiting binding (`RATE_LIMITER`) instead.
|
|
80
|
-
*
|
|
81
|
-
* Stored keys are `lim:<bucket>:<clientKey>:<windowKey>` (the window
|
|
82
|
-
* suffix is appended here) with a TTL of `max(60, windowSeconds * 2)` —
|
|
83
|
-
* KV's TTL floor is 60s.
|
|
84
|
-
*/
|
|
85
|
-
export function createKvRateLimitStore(kv) {
|
|
86
|
-
return {
|
|
87
|
-
async limit(key, { max, windowSeconds }) {
|
|
88
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
89
|
-
const windowKey = Math.floor(nowSeconds / windowSeconds);
|
|
90
|
-
const storageKey = `${key}:${windowKey}`;
|
|
91
|
-
const raw = await kv.get(storageKey);
|
|
92
|
-
const current = raw ? Number(raw) || 0 : 0;
|
|
93
|
-
const next = current + 1;
|
|
94
|
-
await kv.put(storageKey, String(next), {
|
|
95
|
-
expirationTtl: Math.max(60, windowSeconds * 2),
|
|
96
|
-
});
|
|
97
|
-
return {
|
|
98
|
-
allowed: next <= max,
|
|
99
|
-
remaining: Math.max(0, max - next),
|
|
100
|
-
retryAfterSeconds: Math.max(1, windowSeconds - (nowSeconds % windowSeconds)),
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Adapter for the Cloudflare native Rate Limiting binding. Truly
|
|
107
|
-
* distributed and atomic; the actual limit/period are fixed in wrangler
|
|
108
|
-
* config, so the policy's `max`/`windowSeconds` only inform the
|
|
109
|
-
* `Retry-After` hint. `remaining` is never reported (the binding does
|
|
110
|
-
* not expose it).
|
|
111
|
-
*/
|
|
112
|
-
export function createCloudflareRateLimitStore(binding) {
|
|
113
|
-
return {
|
|
114
|
-
async limit(key, { windowSeconds }) {
|
|
115
|
-
const { success } = await binding.limit({ key });
|
|
116
|
-
return success
|
|
117
|
-
? { allowed: true }
|
|
118
|
-
: { allowed: false, retryAfterSeconds: Math.max(1, windowSeconds) };
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
70
|
export function createRedisRateLimitStore(redisUrl, options = {}) {
|
|
123
71
|
const lazyClient = options.client ?? createLazyRedisClient(redisUrl);
|
|
124
72
|
return {
|
|
@@ -140,9 +88,7 @@ export function createRedisRateLimitStore(redisUrl, options = {}) {
|
|
|
140
88
|
};
|
|
141
89
|
}
|
|
142
90
|
// ---- Store resolution ----
|
|
143
|
-
const bindingStoreCache = new WeakMap();
|
|
144
91
|
const explicitStoreCache = new WeakMap();
|
|
145
|
-
const kvStoreCache = new WeakMap();
|
|
146
92
|
const sharedMemoryStore = createMemoryRateLimitStore();
|
|
147
93
|
let warnedNoDistributedStore = false;
|
|
148
94
|
function isDevLikeEnv() {
|
|
@@ -156,23 +102,14 @@ export function resetRateLimitWarningsForTests() {
|
|
|
156
102
|
}
|
|
157
103
|
/**
|
|
158
104
|
* Resolve the best available store from the environment:
|
|
159
|
-
* `c.env.
|
|
160
|
-
*
|
|
105
|
+
* Injected `c.env.RATE_LIMIT_STORE` → in-memory fallback. **Fails open into
|
|
106
|
+
* the memory store** —
|
|
161
107
|
* rate limiting must never break Node/headless deployments that bind
|
|
162
108
|
* neither — but warns once per isolate outside dev/test so a
|
|
163
109
|
* production deploy without a distributed backend is visible in logs.
|
|
164
110
|
*/
|
|
165
111
|
export function resolveRateLimitStore(c, memoryFallback = sharedMemoryStore) {
|
|
166
112
|
const env = (c.env ?? {});
|
|
167
|
-
const binding = env.RATE_LIMITER;
|
|
168
|
-
if (binding && typeof binding.limit === "function") {
|
|
169
|
-
let store = bindingStoreCache.get(binding);
|
|
170
|
-
if (!store) {
|
|
171
|
-
store = createCloudflareRateLimitStore(binding);
|
|
172
|
-
bindingStoreCache.set(binding, store);
|
|
173
|
-
}
|
|
174
|
-
return store;
|
|
175
|
-
}
|
|
176
113
|
const explicit = env.RATE_LIMIT_STORE;
|
|
177
114
|
if (explicit && typeof explicit.limit === "function") {
|
|
178
115
|
let store = explicitStoreCache.get(explicit);
|
|
@@ -182,19 +119,10 @@ export function resolveRateLimitStore(c, memoryFallback = sharedMemoryStore) {
|
|
|
182
119
|
}
|
|
183
120
|
return store;
|
|
184
121
|
}
|
|
185
|
-
const kv = env.RATE_LIMIT;
|
|
186
|
-
if (kv && typeof kv.get === "function" && typeof kv.put === "function") {
|
|
187
|
-
let store = kvStoreCache.get(kv);
|
|
188
|
-
if (!store) {
|
|
189
|
-
store = createKvRateLimitStore(kv);
|
|
190
|
-
kvStoreCache.set(kv, store);
|
|
191
|
-
}
|
|
192
|
-
return store;
|
|
193
|
-
}
|
|
194
122
|
if (!warnedNoDistributedStore && !isDevLikeEnv()) {
|
|
195
123
|
warnedNoDistributedStore = true;
|
|
196
124
|
console.warn("[voyant] rate-limit: no distributed store available (inject RATE_LIMIT_STORE " +
|
|
197
|
-
"
|
|
125
|
+
"). " +
|
|
198
126
|
"Falling back to a per-isolate in-memory limiter — limits apply " +
|
|
199
127
|
"per instance, not fleet-wide.");
|
|
200
128
|
}
|
|
@@ -98,7 +98,9 @@ export function requireActor(...args) {
|
|
|
98
98
|
return async (c, next) => {
|
|
99
99
|
if (c.req.method === "OPTIONS")
|
|
100
100
|
return next();
|
|
101
|
-
if (c.get("callerType") === "api_key" ||
|
|
101
|
+
if (c.get("callerType") === "api_key" ||
|
|
102
|
+
c.get("callerType") === "app" ||
|
|
103
|
+
c.get("callerType") === "internal") {
|
|
102
104
|
const pathname = normalizePathname(new URL(c.req.url).pathname, {
|
|
103
105
|
basePath: options.basePath,
|
|
104
106
|
});
|
|
@@ -116,7 +118,7 @@ export function requireActor(...args) {
|
|
|
116
118
|
hasAnyApiKeyPermission(c.get("scopes"), resource, actions, options.accessCatalog)) {
|
|
117
119
|
return next();
|
|
118
120
|
}
|
|
119
|
-
return c.json({ error: "Forbidden:
|
|
121
|
+
return c.json({ error: "Forbidden: token missing required permission" }, 403);
|
|
120
122
|
}
|
|
121
123
|
const actor = c.get("actor");
|
|
122
124
|
if (!actor) {
|
package/dist/types.d.ts
CHANGED
|
@@ -24,17 +24,9 @@ export interface VoyantBindings {
|
|
|
24
24
|
APP_URL?: string;
|
|
25
25
|
DASH_BASE_URL?: string;
|
|
26
26
|
API_BASE_URL?: string;
|
|
27
|
-
RATE_LIMIT?: KVStore;
|
|
28
27
|
RATE_LIMIT_STORE?: import("./middleware/rate-limit.js").RateLimitStore;
|
|
29
28
|
CACHE?: KVStore;
|
|
30
29
|
SHARED_STATE?: KVStore;
|
|
31
|
-
RATE_LIMITER?: import("./middleware/rate-limit.js").CloudflareRateLimiterBinding;
|
|
32
|
-
/**
|
|
33
|
-
* Workers Analytics Engine dataset receiving per-request metrics
|
|
34
|
-
* (see the `metrics` middleware). Optional — without it the
|
|
35
|
-
* middleware is a no-op.
|
|
36
|
-
*/
|
|
37
|
-
METRICS?: import("./middleware/metrics.js").AnalyticsEngineDatasetLike;
|
|
38
30
|
}
|
|
39
31
|
export type VoyantDb = PostgresJsDatabase | NeonHttpDatabase | NeonWsDatabase;
|
|
40
32
|
export type VoyantQueryRuntime = QueryRunner;
|
|
@@ -55,8 +47,6 @@ export type VoyantVariables = CoreVoyantVariables & {
|
|
|
55
47
|
query?: VoyantQueryRuntime;
|
|
56
48
|
/** Optional workflow driver surfaced to HTTP routes after lazy app bootstrap. */
|
|
57
49
|
workflowDriver?: import("@voyant-travel/workflows/driver").WorkflowDriver;
|
|
58
|
-
/** Per-request db metrics counter populated by the metrics middleware. */
|
|
59
|
-
__voyantDbMetrics?: import("./middleware/metrics.js").RequestDbMetrics;
|
|
60
50
|
};
|
|
61
51
|
/** Handler contract for application-authored Hono API routes. */
|
|
62
52
|
export type VoyantRouteHandler<TBindings extends VoyantBindings = VoyantBindings> = Handler<{
|
|
@@ -144,6 +134,9 @@ export interface VoyantAuthResolveArgs<TBindings extends VoyantBindings = Voyant
|
|
|
144
134
|
db: VoyantDb;
|
|
145
135
|
ctx?: VoyantExecutionContext;
|
|
146
136
|
}
|
|
137
|
+
export interface VoyantAuthAppTokenResolveArgs<TBindings extends VoyantBindings = VoyantBindings> extends VoyantAuthResolveArgs<TBindings> {
|
|
138
|
+
token: string;
|
|
139
|
+
}
|
|
147
140
|
export interface VoyantAuthPermissionArgs<TBindings extends VoyantBindings = VoyantBindings> extends VoyantAuthResolveArgs<TBindings> {
|
|
148
141
|
permission: VoyantPermission;
|
|
149
142
|
auth: VoyantRequestAuthContext;
|
|
@@ -170,6 +163,7 @@ export interface VoyantAuthIntegration<TBindings extends VoyantBindings = Voyant
|
|
|
170
163
|
* so `/v1/public/*` route guards work.
|
|
171
164
|
*/
|
|
172
165
|
resolve?: (args: VoyantAuthResolveArgs<TBindings>) => Promise<VoyantRequestAuthContext | null> | VoyantRequestAuthContext | null;
|
|
166
|
+
resolveAppToken?: (args: VoyantAuthAppTokenResolveArgs<TBindings>) => Promise<VoyantAuthContext | null> | VoyantAuthContext | null;
|
|
173
167
|
hasPermission?: (args: VoyantAuthPermissionArgs<TBindings>) => Promise<boolean> | boolean;
|
|
174
168
|
validateApiKey?: (args: VoyantAuthApiKeyValidationArgs<TBindings>) => Promise<boolean> | boolean;
|
|
175
169
|
onUnauthorized?: (args: VoyantAuthUnauthorizedArgs<TBindings>) => Promise<Response | null> | Response | null;
|
|
@@ -260,13 +254,6 @@ export interface VoyantAppConfig<TBindings extends VoyantBindings = VoyantBindin
|
|
|
260
254
|
* `insertOutboxEvents(tx, ...)`. Default off.
|
|
261
255
|
*/
|
|
262
256
|
outbox?: boolean;
|
|
263
|
-
/**
|
|
264
|
-
* Per-request metrics to the `env.METRICS` Analytics Engine dataset
|
|
265
|
-
* (method, route pattern, surface, cache status, duration, status,
|
|
266
|
-
* db query count). Enabled by default and inert without the binding;
|
|
267
|
-
* set `false` to disable entirely.
|
|
268
|
-
*/
|
|
269
|
-
metrics?: boolean;
|
|
270
257
|
/**
|
|
271
258
|
* Default request body limit enforced before route handlers parse
|
|
272
259
|
* JSON/form data. Content-type-aware: JSON bodies are capped at 10 MiB
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/hono",
|
|
3
|
-
"version": "0.128.
|
|
3
|
+
"version": "0.128.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -130,18 +130,17 @@
|
|
|
130
130
|
"drizzle-orm": "^0.45.2",
|
|
131
131
|
"hono": "^4.12.27",
|
|
132
132
|
"zod": "^4.4.3",
|
|
133
|
-
"@voyant-travel/core": "^0.
|
|
134
|
-
"@voyant-travel/db": "^0.114.
|
|
135
|
-
"@voyant-travel/types": "^0.109.
|
|
133
|
+
"@voyant-travel/core": "^0.125.1",
|
|
134
|
+
"@voyant-travel/db": "^0.114.10",
|
|
135
|
+
"@voyant-travel/types": "^0.109.3",
|
|
136
136
|
"@voyant-travel/utils": "^0.107.1",
|
|
137
|
-
"@voyant-travel/workflows": "^0.122.
|
|
137
|
+
"@voyant-travel/workflows": "^0.122.3"
|
|
138
138
|
},
|
|
139
139
|
"devDependencies": {
|
|
140
|
-
"@cloudflare/workers-types": "^4.20260702.1",
|
|
141
140
|
"typescript": "^6.0.3",
|
|
142
141
|
"vitest": "^4.1.9",
|
|
143
142
|
"@voyant-travel/voyant-typescript-config": "^0.1.0",
|
|
144
|
-
"@voyant-travel/workflows-orchestrator": "^0.122.
|
|
143
|
+
"@voyant-travel/workflows-orchestrator": "^0.122.3"
|
|
145
144
|
},
|
|
146
145
|
"files": [
|
|
147
146
|
"dist"
|