@adrianhall/cloudflare-toolkit 0.0.1
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/LICENSE +21 -0
- package/README.md +59 -0
- package/THIRD-PARTY-NOTICES.md +75 -0
- package/dist/cli/generate-wrangler-types/index.d.ts +1 -0
- package/dist/cli/generate-wrangler-types/index.js +329 -0
- package/dist/cli/generate-wrangler-types/index.js.map +1 -0
- package/dist/error-CLYcAvBM.d.ts +28 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors-Ciipq_zr.js +58 -0
- package/dist/errors-Ciipq_zr.js.map +1 -0
- package/dist/factory-BI5gVL_P.js +220 -0
- package/dist/factory-BI5gVL_P.js.map +1 -0
- package/dist/generators-D8WWEHa1.js +165 -0
- package/dist/generators-D8WWEHa1.js.map +1 -0
- package/dist/guards/index.d.ts +2 -0
- package/dist/guards/index.js +2 -0
- package/dist/guards-6K1CVAr5.js +61 -0
- package/dist/guards-6K1CVAr5.js.map +1 -0
- package/dist/hono/index.d.ts +347 -0
- package/dist/hono/index.js +307 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/index-434HN8jN.d.ts +43 -0
- package/dist/index-Byl-ZrCy.d.ts +138 -0
- package/dist/index-CUICemFw.d.ts +129 -0
- package/dist/index-DRIhR-Xn.d.ts +81 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/jwt-BvuKtvby.js +328 -0
- package/dist/jwt-BvuKtvby.js.map +1 -0
- package/dist/logging/index.d.ts +3 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging-CGHjOVLM.js +24 -0
- package/dist/logging-CGHjOVLM.js.map +1 -0
- package/dist/policy-CvS6AvvD.js +20 -0
- package/dist/policy-CvS6AvvD.js.map +1 -0
- package/dist/problem-details/index.d.ts +4 -0
- package/dist/problem-details/index.js +3 -0
- package/dist/problem-details-CuRsLy3Q.js +37 -0
- package/dist/problem-details-CuRsLy3Q.js.map +1 -0
- package/dist/silent-CWpHE65X.js +652 -0
- package/dist/silent-CWpHE65X.js.map +1 -0
- package/dist/testing/index.d.ts +44 -0
- package/dist/testing/index.js +2 -0
- package/dist/types-Cx6NNILW.d.ts +44 -0
- package/dist/types-DCSMb1cp.d.ts +195 -0
- package/dist/types-DXboaWOT.d.ts +29 -0
- package/dist/vite/index.d.ts +79 -0
- package/dist/vite/index.js +417 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +137 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { n as ProblemDetailsInput, t as ProblemDetails } from "../types-Cx6NNILW.js";
|
|
2
|
+
import { a as Environment, l as Logger, p as Transport, s as LogLevel } from "../types-DCSMb1cp.js";
|
|
3
|
+
import { t as PathPolicy } from "../types-DXboaWOT.js";
|
|
4
|
+
import { Context, ErrorHandler, MiddlewareHandler, NotFoundHandler } from "hono";
|
|
5
|
+
//#region src/lib/hono/error-handler.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link problemDetailsErrorHandler}.
|
|
8
|
+
*/
|
|
9
|
+
interface ProblemDetailsErrorHandlerOptions {
|
|
10
|
+
/** Prefix for the `type` URI. When set, a status-derived slug is appended. */
|
|
11
|
+
typePrefix?: string;
|
|
12
|
+
/** Default `type` URI. Defaults to `"about:blank"`. */
|
|
13
|
+
defaultType?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Include the stack trace on unhandled `Error` responses (500). The stack is emitted as a
|
|
16
|
+
* top-level `stack` extension member per RFC 9457 §3.1 flattening, not in `detail`, to
|
|
17
|
+
* prevent leakage into UIs that render `detail` verbatim.
|
|
18
|
+
*
|
|
19
|
+
* Security-sensitive: development-only, must default to `false`.
|
|
20
|
+
*/
|
|
21
|
+
includeStack?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* When `true`, populate `instance` from the request path (`c.req.path`) if the thrown problem
|
|
24
|
+
* didn't specify one. Explicit values always win.
|
|
25
|
+
*
|
|
26
|
+
* Default: `false` — opt-in to avoid silently changing response shape.
|
|
27
|
+
*/
|
|
28
|
+
autoInstance?: boolean;
|
|
29
|
+
/** Custom error to {@link ProblemDetailsInput} mapping. */
|
|
30
|
+
mapError?: (error: Error) => ProblemDetailsInput | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Localize `title`/`detail` before sending the response. Returned fields are merged onto the
|
|
33
|
+
* original {@link ProblemDetails}, so callers may return a partial patch (e.g. `{ title: "..."
|
|
34
|
+
* }`) or omit the return entirely.
|
|
35
|
+
*/
|
|
36
|
+
localize?: (pd: ProblemDetails, c: Context) => Partial<ProblemDetails> | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create an `app.onError` handler that returns RFC 9457 Problem Details responses. Handles
|
|
40
|
+
* {@link ProblemDetailsError}, Hono `HTTPException`, and unhandled exceptions.
|
|
41
|
+
*
|
|
42
|
+
* @param options - Options controlling `type` URI construction, stack-trace exposure, `instance`
|
|
43
|
+
* auto-population, custom error mapping, and localization.
|
|
44
|
+
* @returns A Hono `ErrorHandler`.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { Hono } from "hono";
|
|
48
|
+
* import { problemDetailsErrorHandler } from "@adrianhall/cloudflare-toolkit/hono";
|
|
49
|
+
*
|
|
50
|
+
* const app = new Hono();
|
|
51
|
+
* app.onError(problemDetailsErrorHandler());
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function problemDetailsErrorHandler(options?: ProblemDetailsErrorHandlerOptions): ErrorHandler;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/lib/hono/not-found-handler.d.ts
|
|
57
|
+
/**
|
|
58
|
+
* Options for {@link notFoundHandler}.
|
|
59
|
+
*/
|
|
60
|
+
interface NotFoundHandlerOptions {
|
|
61
|
+
/** Prefix for the `type` URI. When set, a status-derived slug is appended. */
|
|
62
|
+
typePrefix?: string;
|
|
63
|
+
/** Default `type` URI when `typePrefix` is not set. Defaults to `"about:blank"`. */
|
|
64
|
+
defaultType?: string;
|
|
65
|
+
/**
|
|
66
|
+
* When `true`, populate `instance` from the request path (`c.req.path`).
|
|
67
|
+
*
|
|
68
|
+
* Default: `false`, matching `problemDetailsErrorHandler`'s own default.
|
|
69
|
+
*/
|
|
70
|
+
autoInstance?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create an `app.notFound` handler that returns an RFC 9457 Problem Details `404` response. This
|
|
74
|
+
* mimics what throwing `notFound()` (`@adrianhall/cloudflare-toolkit/errors`) through
|
|
75
|
+
* {@link problemDetailsErrorHandler} would produce, without requiring a request to actually
|
|
76
|
+
* throw — `app.notFound()` is wired independently of `app.onError()`.
|
|
77
|
+
*
|
|
78
|
+
* @param options - Options controlling the `type` URI and whether `instance` is auto-populated.
|
|
79
|
+
* @returns A Hono `NotFoundHandler`.
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { Hono } from "hono";
|
|
83
|
+
* import { notFoundHandler } from "@adrianhall/cloudflare-toolkit/hono";
|
|
84
|
+
*
|
|
85
|
+
* const app = new Hono();
|
|
86
|
+
* app.notFound(notFoundHandler());
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function notFoundHandler(options?: NotFoundHandlerOptions): NotFoundHandler;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/lib/hono/types.d.ts
|
|
92
|
+
/**
|
|
93
|
+
* Context variables set by {@link cloudflareLogger}. Intersect this with your own `Variables`
|
|
94
|
+
* when typing your `Hono` instance so that `c.get("LOGGER")`/`c.var.LOGGER` is statically known:
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* interface AppVariables extends LoggerVariables {
|
|
98
|
+
* // Custom variables go here
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* type AppContext = { Bindings: Env; Variables: AppVariables };
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
interface LoggerVariables {
|
|
105
|
+
/** The request-scoped `Logger` set by `cloudflareLogger`. */
|
|
106
|
+
LOGGER: Logger;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Context variables set by {@link cloudflareAccess} on a successfully authenticated request.
|
|
110
|
+
* Intersect this with your own `Variables` when typing your `Hono` instance so that
|
|
111
|
+
* `c.get("userEmail")`/`c.get("userSub")` are statically known:
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* interface AppVariables extends AuthVariables {
|
|
115
|
+
* // Custom variables go here
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* type AppContext = { Bindings: Env; Variables: AppVariables };
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
interface AuthVariables {
|
|
122
|
+
/** Authenticated user's email address (from the JWT `email` claim). */
|
|
123
|
+
userEmail: string;
|
|
124
|
+
/** Authenticated user's unique identifier (from the JWT `sub` claim). */
|
|
125
|
+
userSub: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Convenience alias for the common case of using {@link cloudflareAccess} and
|
|
129
|
+
* {@link cloudflareLogger} together:
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* interface AppVariables extends CloudflareToolkitVariables {
|
|
133
|
+
* // Custom variables go here
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* Exactly equal to `AuthVariables & LoggerVariables` — {@link AuthVariables} and
|
|
138
|
+
* {@link LoggerVariables} remain separate, independently composable types; this alias does not
|
|
139
|
+
* replace using either on its own.
|
|
140
|
+
*/
|
|
141
|
+
type CloudflareToolkitVariables = AuthVariables & LoggerVariables;
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/lib/hono/logger-middleware.d.ts
|
|
144
|
+
/**
|
|
145
|
+
* Options for {@link cloudflareLogger}.
|
|
146
|
+
*/
|
|
147
|
+
interface CloudflareLoggerOptions {
|
|
148
|
+
/**
|
|
149
|
+
* Environment hint passed to `resolveLoggerConfig`. When omitted, the middleware reads
|
|
150
|
+
* `c.env.ENVIRONMENT` at request time; when that is also absent, `resolveLoggerConfig` treats
|
|
151
|
+
* it as `"production"`.
|
|
152
|
+
*/
|
|
153
|
+
readonly environment?: Environment;
|
|
154
|
+
/**
|
|
155
|
+
* Minimum severity level to emit. Defaults to the level chosen by
|
|
156
|
+
* `resolveLoggerConfig(environment, "worker")`.
|
|
157
|
+
*/
|
|
158
|
+
readonly level?: LogLevel;
|
|
159
|
+
/**
|
|
160
|
+
* Transport to receive emitted records. Defaults to the transport chosen by
|
|
161
|
+
* `resolveLoggerConfig(environment, "worker")`. Inject a capture transport in tests to assert
|
|
162
|
+
* on emitted records.
|
|
163
|
+
*/
|
|
164
|
+
readonly transport?: Transport;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create Hono middleware that attaches a request-scoped {@link Logger} to the context as
|
|
168
|
+
* `LOGGER` (`LoggerVariables`, ./types.ts), so downstream middleware and handlers can log
|
|
169
|
+
* through it.
|
|
170
|
+
*
|
|
171
|
+
* The logger's level and transport are resolved via `resolveLoggerConfig(environment, "worker")`
|
|
172
|
+
* (`@adrianhall/cloudflare-toolkit/logging`) unless overridden by `options.level`/
|
|
173
|
+
* `options.transport`. This middleware is independently wireable — it has no dependency on
|
|
174
|
+
* `cloudflareAccess`.
|
|
175
|
+
*
|
|
176
|
+
* @param options - Options controlling the environment, level, and transport used to build the
|
|
177
|
+
* logger.
|
|
178
|
+
* @returns A Hono `MiddlewareHandler` parameterised with {@link LoggerVariables}, so
|
|
179
|
+
* `c.set("LOGGER", …)` inside this middleware — and `c.get("LOGGER")` in a consumer's own
|
|
180
|
+
* handlers once composed via `app.use(...)` — are statically checked against
|
|
181
|
+
* {@link LoggerVariables} rather than accepted as an untyped magic string.
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* import { Hono } from "hono";
|
|
185
|
+
* import { cloudflareLogger } from "@adrianhall/cloudflare-toolkit/hono";
|
|
186
|
+
*
|
|
187
|
+
* const app = new Hono();
|
|
188
|
+
* app.use(cloudflareLogger());
|
|
189
|
+
*
|
|
190
|
+
* app.get("/", (c) => {
|
|
191
|
+
* c.get("LOGGER").info("handling request");
|
|
192
|
+
* return c.text("ok");
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
declare function cloudflareLogger(options?: CloudflareLoggerOptions): MiddlewareHandler<{
|
|
197
|
+
Variables: LoggerVariables;
|
|
198
|
+
}>;
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/lib/hono/cloudflare-access.d.ts
|
|
201
|
+
/**
|
|
202
|
+
* Options for {@link cloudflareAccess}.
|
|
203
|
+
*/
|
|
204
|
+
interface CloudflareAccessOptions {
|
|
205
|
+
/**
|
|
206
|
+
* Path policies evaluated in order (first match wins).
|
|
207
|
+
*
|
|
208
|
+
* - `authenticate: false` — bypass JWT validation entirely.
|
|
209
|
+
* - `authenticate: true` — require a valid JWT (401 if missing/invalid).
|
|
210
|
+
* - No matching policy — behavior is controlled by
|
|
211
|
+
* {@link CloudflareAccessOptions.defaultAction}.
|
|
212
|
+
*
|
|
213
|
+
* When omitted, every path is subject to {@link CloudflareAccessOptions.defaultAction}.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* policies: [
|
|
218
|
+
* { pattern: /^\/api\/version$/, authenticate: false },
|
|
219
|
+
* { pattern: /^\/api\//, authenticate: true }
|
|
220
|
+
* ]
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
readonly policies?: PathPolicy[];
|
|
224
|
+
/**
|
|
225
|
+
* What to do when the request path does not match any policy.
|
|
226
|
+
*
|
|
227
|
+
* - `"block"` *(default)* — return 401 if no valid JWT is present.
|
|
228
|
+
* - `"bypass"` — allow the request through without authentication. If a valid JWT is present
|
|
229
|
+
* it is still verified and `AuthVariables` are still set; otherwise the request continues
|
|
230
|
+
* with no authenticated user.
|
|
231
|
+
*/
|
|
232
|
+
readonly defaultAction?: "block" | "bypass";
|
|
233
|
+
/**
|
|
234
|
+
* Cloudflare Access team domain used to fetch the public JWKS. When omitted, the middleware
|
|
235
|
+
* reads `c.env.CLOUDFLARE_TEAM_DOMAIN` at request time.
|
|
236
|
+
*/
|
|
237
|
+
readonly teamDomain?: string;
|
|
238
|
+
/**
|
|
239
|
+
* Application Audience Tag. When provided, the middleware verifies the JWT `aud` claim
|
|
240
|
+
* contains this value.
|
|
241
|
+
*
|
|
242
|
+
* **When omitted, audience validation is skipped** — the `aud` claim is not checked at all.
|
|
243
|
+
* Every Cloudflare Access application in the same team shares the same JWKS, so without an
|
|
244
|
+
* `aud` check, a JWT that is valid for *any other Access application in the team* is accepted
|
|
245
|
+
* here too (cross-application token replay).
|
|
246
|
+
*
|
|
247
|
+
* Unless {@link CloudflareAccessOptions.enableDevTokens} is `true` (local development),
|
|
248
|
+
* omitting `audience` logs a one-time warning at construction time — see
|
|
249
|
+
* {@link cloudflareAccess}'s security remarks.
|
|
250
|
+
*/
|
|
251
|
+
readonly audience?: string;
|
|
252
|
+
/**
|
|
253
|
+
* Enable HS256 developer-token verification.
|
|
254
|
+
*
|
|
255
|
+
* **Default `false` (fail-closed).** When `false`, {@link cloudflareAccess} verifies the JWT
|
|
256
|
+
* **only** against the Cloudflare Access JWKS — a developer-signed HS256 token (including one
|
|
257
|
+
* signed with the public `DEFAULT_DEV_SECRET`) is rejected. This prevents a deployed Worker
|
|
258
|
+
* from silently trusting forgeable dev tokens.
|
|
259
|
+
*
|
|
260
|
+
* Enable it only in local development, gated on a build-time signal that is statically
|
|
261
|
+
* `false` in production:
|
|
262
|
+
*
|
|
263
|
+
* ```ts
|
|
264
|
+
* app.use(cloudflareAccess({ policies, enableDevTokens: import.meta.env.DEV }));
|
|
265
|
+
* ```
|
|
266
|
+
*
|
|
267
|
+
* When enabled without an explicit {@link CloudflareAccessOptions.devSecret}, the middleware
|
|
268
|
+
* logs a one-time warning that it is verifying with the public default secret.
|
|
269
|
+
*/
|
|
270
|
+
readonly enableDevTokens?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* HMAC secret for validating developer-generated JWTs. Ignored unless
|
|
273
|
+
* {@link CloudflareAccessOptions.enableDevTokens} is `true`. When dev tokens are enabled and
|
|
274
|
+
* this is omitted, the well-known `DEFAULT_DEV_SECRET` is used and a one-time warning is
|
|
275
|
+
* logged — never rely on that for production security.
|
|
276
|
+
*/
|
|
277
|
+
readonly devSecret?: string;
|
|
278
|
+
/**
|
|
279
|
+
* Structured logger used for debug/info/warn/error diagnostics. Defaults to a silent logger
|
|
280
|
+
* (nothing is emitted) when omitted.
|
|
281
|
+
*/
|
|
282
|
+
readonly logger?: Logger;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Create a Hono middleware that validates a Cloudflare Access JWT and sets `AuthVariables`
|
|
286
|
+
* (`userEmail`, `userSub`, ./types.ts) on the Hono context.
|
|
287
|
+
*
|
|
288
|
+
* **Policy evaluation**:
|
|
289
|
+
*
|
|
290
|
+
* | Policy match | Behavior |
|
|
291
|
+
* | ----------------------- | -------------------------------------------- |
|
|
292
|
+
* | `authenticate: false` | Bypass — skip JWT validation entirely. |
|
|
293
|
+
* | `authenticate: true` | Require — valid JWT or 401. |
|
|
294
|
+
* | No matching policy | Controlled by `defaultAction` (see below). |
|
|
295
|
+
*
|
|
296
|
+
* Every `401` this middleware returns is an RFC 9457 `application/problem+json` response
|
|
297
|
+
* (`{ type, status, title, detail }`), matching `problemDetailsErrorHandler` and
|
|
298
|
+
* `notFoundHandler`'s conventions.
|
|
299
|
+
*
|
|
300
|
+
* **`defaultAction`** (applies when no policy matches):
|
|
301
|
+
*
|
|
302
|
+
* - `"block"` *(default)* — return 401 if no valid JWT is present.
|
|
303
|
+
* - `"bypass"` — allow the request through. If a JWT *is* present and valid, the context
|
|
304
|
+
* variables are still set; otherwise the request continues with no authenticated user.
|
|
305
|
+
*
|
|
306
|
+
* **Verification order** (when JWT validation is performed):
|
|
307
|
+
*
|
|
308
|
+
* 1. *(Opt-in)* When `enableDevTokens` is `true`, try HMAC verification with the dev secret
|
|
309
|
+
* (fast, in-process).
|
|
310
|
+
* 2. Verify against the remote JWKS endpoint for the team domain.
|
|
311
|
+
*
|
|
312
|
+
* Developer-token verification is **fail-closed**: it is disabled by default so a deployed
|
|
313
|
+
* Worker never silently trusts a forgeable HS256 token signed with the public
|
|
314
|
+
* `DEFAULT_DEV_SECRET`. Enable it only in local development.
|
|
315
|
+
*
|
|
316
|
+
* **Audience validation is opt-in, not fail-closed**: omitting
|
|
317
|
+
* {@link CloudflareAccessOptions.audience} skips the `aud` check and allows cross-application
|
|
318
|
+
* token replay within the same Cloudflare Access team (see that option's docs). To surface this
|
|
319
|
+
* without breaking existing deployments, {@link cloudflareAccess} logs a one-time warning at
|
|
320
|
+
* construction time whenever `audience` is omitted **and** `enableDevTokens` is not `true` —
|
|
321
|
+
* i.e. in the default, production-shaped configuration. The warning is intentionally silent
|
|
322
|
+
* when `enableDevTokens` is `true`, since that already signals a local-development posture.
|
|
323
|
+
*
|
|
324
|
+
* @remarks Security-critical: this fail-closed default must be preserved exactly — see the
|
|
325
|
+
* "fail-closed" describe block in `test/workers/hono/cloudflare-access.test.ts`.
|
|
326
|
+
* @param options - Options controlling path policies, the default action for unmatched paths,
|
|
327
|
+
* the Cloudflare Access team domain/audience, dev-token verification, and the logger.
|
|
328
|
+
* @returns A Hono `MiddlewareHandler` parameterised with {@link AuthVariables}, so
|
|
329
|
+
* `c.set("userEmail", …)`/`c.set("userSub", …)` inside this middleware — and `c.get("userEmail")`/
|
|
330
|
+
* `c.get("userSub")` in a consumer's own handlers once composed via `app.use(...)` — are
|
|
331
|
+
* statically checked against {@link AuthVariables} rather than accepted as untyped magic strings.
|
|
332
|
+
* @example
|
|
333
|
+
* ```ts
|
|
334
|
+
* import { Hono } from "hono";
|
|
335
|
+
* import { cloudflareAccess, type AuthVariables } from "@adrianhall/cloudflare-toolkit/hono";
|
|
336
|
+
*
|
|
337
|
+
* const app = new Hono<{ Variables: AuthVariables }>();
|
|
338
|
+
* app.use(cloudflareAccess({ policies: [{ pattern: /^\/api\/version$/, authenticate: false }] }));
|
|
339
|
+
* app.get("/api/*", (c) => c.json({ user: c.get("userEmail") }));
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
declare function cloudflareAccess(options?: CloudflareAccessOptions): MiddlewareHandler<{
|
|
343
|
+
Variables: AuthVariables;
|
|
344
|
+
}>;
|
|
345
|
+
//#endregion
|
|
346
|
+
export { type AuthVariables, type CloudflareAccessOptions, type CloudflareLoggerOptions, type CloudflareToolkitVariables, type LoggerVariables, type NotFoundHandlerOptions, type ProblemDetailsErrorHandlerOptions, cloudflareAccess, cloudflareLogger, notFoundHandler, problemDetailsErrorHandler };
|
|
347
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { a as statusToPhrase, i as normalizeProblemDetails, n as ProblemDetailsError, o as statusToSlug, r as buildProblemResponse } from "../factory-BI5gVL_P.js";
|
|
2
|
+
import { n as resolveLoggerConfig, s as createLogger, t as createSilentTransport } from "../silent-CWpHE65X.js";
|
|
3
|
+
import { l as verifyAccessJwt, s as parseCookie, u as verifyDevJwt } from "../jwt-BvuKtvby.js";
|
|
4
|
+
import { t as matchPolicy } from "../policy-CvS6AvvD.js";
|
|
5
|
+
import { HTTPException } from "hono/http-exception";
|
|
6
|
+
//#region src/lib/hono/error-handler.ts
|
|
7
|
+
function buildType$1(status, options) {
|
|
8
|
+
if (options.typePrefix) {
|
|
9
|
+
const slug = statusToSlug(status);
|
|
10
|
+
if (slug) return `${options.typePrefix}/${slug}`;
|
|
11
|
+
}
|
|
12
|
+
return options.defaultType ?? "about:blank";
|
|
13
|
+
}
|
|
14
|
+
function toResponse(input, c, options) {
|
|
15
|
+
let pd = normalizeProblemDetails(input);
|
|
16
|
+
if (options.autoInstance && pd.instance === void 0) pd = {
|
|
17
|
+
...pd,
|
|
18
|
+
instance: c.req.path
|
|
19
|
+
};
|
|
20
|
+
if (options.localize) try {
|
|
21
|
+
pd = {
|
|
22
|
+
...pd,
|
|
23
|
+
...options.localize(pd, c)
|
|
24
|
+
};
|
|
25
|
+
} catch {}
|
|
26
|
+
c.set("problemDetails", pd);
|
|
27
|
+
return buildProblemResponse(pd);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create an `app.onError` handler that returns RFC 9457 Problem Details responses. Handles
|
|
31
|
+
* {@link ProblemDetailsError}, Hono `HTTPException`, and unhandled exceptions.
|
|
32
|
+
*
|
|
33
|
+
* @param options - Options controlling `type` URI construction, stack-trace exposure, `instance`
|
|
34
|
+
* auto-population, custom error mapping, and localization.
|
|
35
|
+
* @returns A Hono `ErrorHandler`.
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { Hono } from "hono";
|
|
39
|
+
* import { problemDetailsErrorHandler } from "@adrianhall/cloudflare-toolkit/hono";
|
|
40
|
+
*
|
|
41
|
+
* const app = new Hono();
|
|
42
|
+
* app.onError(problemDetailsErrorHandler());
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function problemDetailsErrorHandler(options = {}) {
|
|
46
|
+
return (error, c) => {
|
|
47
|
+
if (error instanceof ProblemDetailsError) return toResponse(error.problemDetails, c, options);
|
|
48
|
+
if (options.mapError) {
|
|
49
|
+
const mapped = options.mapError(error);
|
|
50
|
+
if (mapped) return toResponse(mapped, c, options);
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof HTTPException) return toResponse({
|
|
53
|
+
status: error.status,
|
|
54
|
+
type: buildType$1(error.status, options),
|
|
55
|
+
title: statusToPhrase(error.status),
|
|
56
|
+
detail: error.message
|
|
57
|
+
}, c, options);
|
|
58
|
+
return toResponse({
|
|
59
|
+
status: 500,
|
|
60
|
+
type: buildType$1(500, options),
|
|
61
|
+
title: "Internal Server Error",
|
|
62
|
+
detail: "An unexpected error occurred",
|
|
63
|
+
extensions: options.includeStack ? { stack: error.stack } : void 0
|
|
64
|
+
}, c, options);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/lib/hono/not-found-handler.ts
|
|
69
|
+
const NOT_FOUND_STATUS = 404;
|
|
70
|
+
function buildType(options) {
|
|
71
|
+
if (options.typePrefix) return `${options.typePrefix}/${statusToSlug(NOT_FOUND_STATUS)}`;
|
|
72
|
+
return options.defaultType ?? "about:blank";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create an `app.notFound` handler that returns an RFC 9457 Problem Details `404` response. This
|
|
76
|
+
* mimics what throwing `notFound()` (`@adrianhall/cloudflare-toolkit/errors`) through
|
|
77
|
+
* {@link problemDetailsErrorHandler} would produce, without requiring a request to actually
|
|
78
|
+
* throw — `app.notFound()` is wired independently of `app.onError()`.
|
|
79
|
+
*
|
|
80
|
+
* @param options - Options controlling the `type` URI and whether `instance` is auto-populated.
|
|
81
|
+
* @returns A Hono `NotFoundHandler`.
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { Hono } from "hono";
|
|
85
|
+
* import { notFoundHandler } from "@adrianhall/cloudflare-toolkit/hono";
|
|
86
|
+
*
|
|
87
|
+
* const app = new Hono();
|
|
88
|
+
* app.notFound(notFoundHandler());
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
function notFoundHandler(options = {}) {
|
|
92
|
+
return (c) => {
|
|
93
|
+
let pd = {
|
|
94
|
+
type: buildType(options),
|
|
95
|
+
status: NOT_FOUND_STATUS,
|
|
96
|
+
title: statusToPhrase(NOT_FOUND_STATUS)
|
|
97
|
+
};
|
|
98
|
+
if (options.autoInstance) pd = {
|
|
99
|
+
...pd,
|
|
100
|
+
instance: c.req.path
|
|
101
|
+
};
|
|
102
|
+
return buildProblemResponse(pd);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/lib/hono/logger-middleware.ts
|
|
107
|
+
function resolveEnvironment(options, c) {
|
|
108
|
+
if (options.environment !== void 0) return options.environment;
|
|
109
|
+
return c.env?.ENVIRONMENT;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create Hono middleware that attaches a request-scoped {@link Logger} to the context as
|
|
113
|
+
* `LOGGER` (`LoggerVariables`, ./types.ts), so downstream middleware and handlers can log
|
|
114
|
+
* through it.
|
|
115
|
+
*
|
|
116
|
+
* The logger's level and transport are resolved via `resolveLoggerConfig(environment, "worker")`
|
|
117
|
+
* (`@adrianhall/cloudflare-toolkit/logging`) unless overridden by `options.level`/
|
|
118
|
+
* `options.transport`. This middleware is independently wireable — it has no dependency on
|
|
119
|
+
* `cloudflareAccess`.
|
|
120
|
+
*
|
|
121
|
+
* @param options - Options controlling the environment, level, and transport used to build the
|
|
122
|
+
* logger.
|
|
123
|
+
* @returns A Hono `MiddlewareHandler` parameterised with {@link LoggerVariables}, so
|
|
124
|
+
* `c.set("LOGGER", …)` inside this middleware — and `c.get("LOGGER")` in a consumer's own
|
|
125
|
+
* handlers once composed via `app.use(...)` — are statically checked against
|
|
126
|
+
* {@link LoggerVariables} rather than accepted as an untyped magic string.
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { Hono } from "hono";
|
|
130
|
+
* import { cloudflareLogger } from "@adrianhall/cloudflare-toolkit/hono";
|
|
131
|
+
*
|
|
132
|
+
* const app = new Hono();
|
|
133
|
+
* app.use(cloudflareLogger());
|
|
134
|
+
*
|
|
135
|
+
* app.get("/", (c) => {
|
|
136
|
+
* c.get("LOGGER").info("handling request");
|
|
137
|
+
* return c.text("ok");
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
function cloudflareLogger(options = {}) {
|
|
142
|
+
return async (c, next) => {
|
|
143
|
+
const base = resolveLoggerConfig(resolveEnvironment(options, c), "worker");
|
|
144
|
+
const logger = createLogger({
|
|
145
|
+
level: options.level ?? base.level,
|
|
146
|
+
transport: options.transport ?? base.transport
|
|
147
|
+
});
|
|
148
|
+
c.set("LOGGER", logger);
|
|
149
|
+
await next();
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/lib/hono/cloudflare-access.ts
|
|
154
|
+
/** Silent fallback used when `options.logger` is not supplied. */
|
|
155
|
+
function createDefaultLogger() {
|
|
156
|
+
return createLogger({ transport: createSilentTransport() });
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build a `401 Unauthorized` RFC 9457 `application/problem+json` response, matching the shape
|
|
160
|
+
* `problemDetailsErrorHandler` (`./error-handler.js`) and `notFoundHandler` (`./not-found-handler.js`)
|
|
161
|
+
* produce. Built directly via `../problem-details/utils.js` rather than by throwing
|
|
162
|
+
* `unauthorized()` (`@adrianhall/cloudflare-toolkit/errors`), so `cloudflareAccess` returns the
|
|
163
|
+
* correct shape even when a consumer hasn't wired `app.onError(problemDetailsErrorHandler())` —
|
|
164
|
+
* every piece of `/hono` middleware is wired independently (no combined/coordinator middleware).
|
|
165
|
+
*
|
|
166
|
+
* @param detail - Human-readable explanation for this specific 401 occurrence.
|
|
167
|
+
* @returns The resulting `Response`.
|
|
168
|
+
*/
|
|
169
|
+
function unauthorizedResponse(detail) {
|
|
170
|
+
return buildProblemResponse(normalizeProblemDetails({
|
|
171
|
+
status: 401,
|
|
172
|
+
detail
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Attempt to verify a JWT.
|
|
177
|
+
*
|
|
178
|
+
* When `enableDevTokens` is `true`, the dev (HS256) secret is tried first as a fast in-process
|
|
179
|
+
* path; otherwise that path is skipped entirely and only Cloudflare Access JWKS verification
|
|
180
|
+
* runs — the fail-closed default.
|
|
181
|
+
*
|
|
182
|
+
* Returns the verified claims or `null`.
|
|
183
|
+
*/
|
|
184
|
+
async function verifyToken(c, token, options) {
|
|
185
|
+
if (options.enableDevTokens) {
|
|
186
|
+
const devResult = await verifyDevJwt(token, options.devSecret);
|
|
187
|
+
if (devResult) return devResult;
|
|
188
|
+
}
|
|
189
|
+
const bindings = c.env;
|
|
190
|
+
const teamDomain = options.teamDomainOverride ?? bindings?.CLOUDFLARE_TEAM_DOMAIN;
|
|
191
|
+
if (!teamDomain) {
|
|
192
|
+
options.logger.error("No team domain configured - set CLOUDFLARE_TEAM_DOMAIN in env or pass teamDomain in options");
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return verifyAccessJwt(token, teamDomain, options.audience, options.logger);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create a Hono middleware that validates a Cloudflare Access JWT and sets `AuthVariables`
|
|
199
|
+
* (`userEmail`, `userSub`, ./types.ts) on the Hono context.
|
|
200
|
+
*
|
|
201
|
+
* **Policy evaluation**:
|
|
202
|
+
*
|
|
203
|
+
* | Policy match | Behavior |
|
|
204
|
+
* | ----------------------- | -------------------------------------------- |
|
|
205
|
+
* | `authenticate: false` | Bypass — skip JWT validation entirely. |
|
|
206
|
+
* | `authenticate: true` | Require — valid JWT or 401. |
|
|
207
|
+
* | No matching policy | Controlled by `defaultAction` (see below). |
|
|
208
|
+
*
|
|
209
|
+
* Every `401` this middleware returns is an RFC 9457 `application/problem+json` response
|
|
210
|
+
* (`{ type, status, title, detail }`), matching `problemDetailsErrorHandler` and
|
|
211
|
+
* `notFoundHandler`'s conventions.
|
|
212
|
+
*
|
|
213
|
+
* **`defaultAction`** (applies when no policy matches):
|
|
214
|
+
*
|
|
215
|
+
* - `"block"` *(default)* — return 401 if no valid JWT is present.
|
|
216
|
+
* - `"bypass"` — allow the request through. If a JWT *is* present and valid, the context
|
|
217
|
+
* variables are still set; otherwise the request continues with no authenticated user.
|
|
218
|
+
*
|
|
219
|
+
* **Verification order** (when JWT validation is performed):
|
|
220
|
+
*
|
|
221
|
+
* 1. *(Opt-in)* When `enableDevTokens` is `true`, try HMAC verification with the dev secret
|
|
222
|
+
* (fast, in-process).
|
|
223
|
+
* 2. Verify against the remote JWKS endpoint for the team domain.
|
|
224
|
+
*
|
|
225
|
+
* Developer-token verification is **fail-closed**: it is disabled by default so a deployed
|
|
226
|
+
* Worker never silently trusts a forgeable HS256 token signed with the public
|
|
227
|
+
* `DEFAULT_DEV_SECRET`. Enable it only in local development.
|
|
228
|
+
*
|
|
229
|
+
* **Audience validation is opt-in, not fail-closed**: omitting
|
|
230
|
+
* {@link CloudflareAccessOptions.audience} skips the `aud` check and allows cross-application
|
|
231
|
+
* token replay within the same Cloudflare Access team (see that option's docs). To surface this
|
|
232
|
+
* without breaking existing deployments, {@link cloudflareAccess} logs a one-time warning at
|
|
233
|
+
* construction time whenever `audience` is omitted **and** `enableDevTokens` is not `true` —
|
|
234
|
+
* i.e. in the default, production-shaped configuration. The warning is intentionally silent
|
|
235
|
+
* when `enableDevTokens` is `true`, since that already signals a local-development posture.
|
|
236
|
+
*
|
|
237
|
+
* @remarks Security-critical: this fail-closed default must be preserved exactly — see the
|
|
238
|
+
* "fail-closed" describe block in `test/workers/hono/cloudflare-access.test.ts`.
|
|
239
|
+
* @param options - Options controlling path policies, the default action for unmatched paths,
|
|
240
|
+
* the Cloudflare Access team domain/audience, dev-token verification, and the logger.
|
|
241
|
+
* @returns A Hono `MiddlewareHandler` parameterised with {@link AuthVariables}, so
|
|
242
|
+
* `c.set("userEmail", …)`/`c.set("userSub", …)` inside this middleware — and `c.get("userEmail")`/
|
|
243
|
+
* `c.get("userSub")` in a consumer's own handlers once composed via `app.use(...)` — are
|
|
244
|
+
* statically checked against {@link AuthVariables} rather than accepted as untyped magic strings.
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* import { Hono } from "hono";
|
|
248
|
+
* import { cloudflareAccess, type AuthVariables } from "@adrianhall/cloudflare-toolkit/hono";
|
|
249
|
+
*
|
|
250
|
+
* const app = new Hono<{ Variables: AuthVariables }>();
|
|
251
|
+
* app.use(cloudflareAccess({ policies: [{ pattern: /^\/api\/version$/, authenticate: false }] }));
|
|
252
|
+
* app.get("/api/*", (c) => c.json({ user: c.get("userEmail") }));
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
function cloudflareAccess(options = {}) {
|
|
256
|
+
const policies = options.policies;
|
|
257
|
+
const defaultAction = options.defaultAction ?? "block";
|
|
258
|
+
const enableDevTokens = options.enableDevTokens ?? false;
|
|
259
|
+
const devSecretProvided = typeof options.devSecret === "string";
|
|
260
|
+
const devSecret = options.devSecret ?? "cloudflare-access-dev-secret-do-not-use-in-production";
|
|
261
|
+
const audience = options.audience;
|
|
262
|
+
const teamDomainOverride = options.teamDomain;
|
|
263
|
+
const log = options.logger ?? createDefaultLogger();
|
|
264
|
+
if (enableDevTokens && !devSecretProvided) log.warn("enableDevTokens is true but no devSecret was provided; verifying HS256 dev tokens with the public DEFAULT_DEV_SECRET. This is only safe in local development.");
|
|
265
|
+
if (!enableDevTokens && audience === void 0) log.warn("No audience was provided; the JWT 'aud' claim will not be validated. Any Cloudflare Access application in the same team can mint a token accepted here (cross-application token replay). Set the 'audience' option to this app's Audience Tag, or set enableDevTokens to silence this warning in local development.");
|
|
266
|
+
return async (c, next) => {
|
|
267
|
+
const pathname = new URL(c.req.url).pathname;
|
|
268
|
+
const policyMatch = policies ? matchPolicy(pathname, policies) : void 0;
|
|
269
|
+
if (policyMatch?.authenticate === false) {
|
|
270
|
+
log.debug("Path is public - bypassing auth", { pathname });
|
|
271
|
+
return next();
|
|
272
|
+
}
|
|
273
|
+
const authRequired = policyMatch?.authenticate === true || policyMatch === void 0 && defaultAction === "block";
|
|
274
|
+
const token = c.req.header("cf-access-jwt-assertion") ?? parseCookie(c.req.header("cookie"));
|
|
275
|
+
if (!token) {
|
|
276
|
+
if (authRequired) {
|
|
277
|
+
log.warn("No JWT found in header or cookie");
|
|
278
|
+
return unauthorizedResponse("Authentication required");
|
|
279
|
+
}
|
|
280
|
+
log.debug("No JWT - continuing (bypass)", { pathname });
|
|
281
|
+
return next();
|
|
282
|
+
}
|
|
283
|
+
const result = await verifyToken(c, token, {
|
|
284
|
+
enableDevTokens,
|
|
285
|
+
devSecret,
|
|
286
|
+
audience,
|
|
287
|
+
teamDomainOverride,
|
|
288
|
+
logger: log
|
|
289
|
+
});
|
|
290
|
+
if (result) {
|
|
291
|
+
log.debug("Verified token", { email: result.email });
|
|
292
|
+
c.set("userEmail", result.email);
|
|
293
|
+
c.set("userSub", result.sub);
|
|
294
|
+
return next();
|
|
295
|
+
}
|
|
296
|
+
if (authRequired) {
|
|
297
|
+
log.warn("JWT verification failed");
|
|
298
|
+
return unauthorizedResponse("Invalid or expired token");
|
|
299
|
+
}
|
|
300
|
+
log.info("JWT invalid - continuing (bypass)", { pathname });
|
|
301
|
+
return next();
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
export { cloudflareAccess, cloudflareLogger, notFoundHandler, problemDetailsErrorHandler };
|
|
306
|
+
|
|
307
|
+
//# sourceMappingURL=index.js.map
|