@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/THIRD-PARTY-NOTICES.md +75 -0
  4. package/dist/cli/generate-wrangler-types/index.d.ts +1 -0
  5. package/dist/cli/generate-wrangler-types/index.js +329 -0
  6. package/dist/cli/generate-wrangler-types/index.js.map +1 -0
  7. package/dist/error-CLYcAvBM.d.ts +28 -0
  8. package/dist/errors/index.d.ts +2 -0
  9. package/dist/errors/index.js +3 -0
  10. package/dist/errors-Ciipq_zr.js +58 -0
  11. package/dist/errors-Ciipq_zr.js.map +1 -0
  12. package/dist/factory-BI5gVL_P.js +220 -0
  13. package/dist/factory-BI5gVL_P.js.map +1 -0
  14. package/dist/generators-D8WWEHa1.js +165 -0
  15. package/dist/generators-D8WWEHa1.js.map +1 -0
  16. package/dist/guards/index.d.ts +2 -0
  17. package/dist/guards/index.js +2 -0
  18. package/dist/guards-6K1CVAr5.js +61 -0
  19. package/dist/guards-6K1CVAr5.js.map +1 -0
  20. package/dist/hono/index.d.ts +347 -0
  21. package/dist/hono/index.js +307 -0
  22. package/dist/hono/index.js.map +1 -0
  23. package/dist/index-434HN8jN.d.ts +43 -0
  24. package/dist/index-Byl-ZrCy.d.ts +138 -0
  25. package/dist/index-CUICemFw.d.ts +129 -0
  26. package/dist/index-DRIhR-Xn.d.ts +81 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +8 -0
  29. package/dist/jwt-BvuKtvby.js +328 -0
  30. package/dist/jwt-BvuKtvby.js.map +1 -0
  31. package/dist/logging/index.d.ts +3 -0
  32. package/dist/logging/index.js +3 -0
  33. package/dist/logging-CGHjOVLM.js +24 -0
  34. package/dist/logging-CGHjOVLM.js.map +1 -0
  35. package/dist/policy-CvS6AvvD.js +20 -0
  36. package/dist/policy-CvS6AvvD.js.map +1 -0
  37. package/dist/problem-details/index.d.ts +4 -0
  38. package/dist/problem-details/index.js +3 -0
  39. package/dist/problem-details-CuRsLy3Q.js +37 -0
  40. package/dist/problem-details-CuRsLy3Q.js.map +1 -0
  41. package/dist/silent-CWpHE65X.js +652 -0
  42. package/dist/silent-CWpHE65X.js.map +1 -0
  43. package/dist/testing/index.d.ts +44 -0
  44. package/dist/testing/index.js +2 -0
  45. package/dist/types-Cx6NNILW.d.ts +44 -0
  46. package/dist/types-DCSMb1cp.d.ts +195 -0
  47. package/dist/types-DXboaWOT.d.ts +29 -0
  48. package/dist/vite/index.d.ts +79 -0
  49. package/dist/vite/index.js +417 -0
  50. package/dist/vite/index.js.map +1 -0
  51. 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