@rdlabo/workers-hono-kit 0.3.7 → 0.4.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.
Files changed (57) hide show
  1. package/README.md +34 -1
  2. package/dist/business-time/index.d.ts +49 -0
  3. package/dist/business-time/index.js +149 -0
  4. package/dist/business-time/types.d.ts +9 -0
  5. package/dist/business-time/types.js +5 -0
  6. package/dist/db/columns.d.ts +46 -0
  7. package/dist/db/columns.js +36 -0
  8. package/dist/db/connection.js +2 -1
  9. package/dist/db/decimal.d.ts +27 -0
  10. package/dist/db/decimal.js +50 -0
  11. package/dist/db/index.d.ts +4 -1
  12. package/dist/db/index.js +3 -1
  13. package/dist/db/jst.d.ts +10 -72
  14. package/dist/db/jst.js +10 -82
  15. package/dist/testing/index.d.ts +2 -0
  16. package/dist/testing/index.js +2 -0
  17. package/dist/testing/workers-bindings.d.ts +49 -0
  18. package/dist/testing/workers-bindings.js +62 -0
  19. package/package.json +7 -3
  20. package/scripts/db-baseline.mjs +0 -0
  21. package/src/ai/gateway.ts +0 -120
  22. package/src/aws/cloudfront.ts +0 -105
  23. package/src/aws/secrets-manager.ts +0 -112
  24. package/src/cache/kv-cache.ts +0 -316
  25. package/src/db/connection.ts +0 -107
  26. package/src/db/database.ts +0 -269
  27. package/src/db/index.ts +0 -39
  28. package/src/db/jst.ts +0 -122
  29. package/src/db/migrate.ts +0 -155
  30. package/src/db/orm-config.ts +0 -171
  31. package/src/db/retry.ts +0 -43
  32. package/src/db/write-result.ts +0 -46
  33. package/src/firebase/firebase-verifier.ts +0 -76
  34. package/src/firebase/identity-toolkit.ts +0 -179
  35. package/src/firebase/jose-firebase-verifier.ts +0 -159
  36. package/src/firebase/remote-verifier.ts +0 -98
  37. package/src/http/app-env.ts +0 -53
  38. package/src/http/app-info.ts +0 -38
  39. package/src/http/execution-context.ts +0 -11
  40. package/src/http/http-status.ts +0 -71
  41. package/src/http/nest-error.ts +0 -207
  42. package/src/http/trailing-slash.ts +0 -28
  43. package/src/http/user-protocol.ts +0 -36
  44. package/src/index.ts +0 -77
  45. package/src/middleware/auth.ts +0 -129
  46. package/src/middleware/finalize-response.ts +0 -90
  47. package/src/middleware/validation.ts +0 -158
  48. package/src/middleware/zod-coerce.ts +0 -124
  49. package/src/queue/consumer.ts +0 -146
  50. package/src/queue/send.ts +0 -129
  51. package/src/stripe/client.ts +0 -85
  52. package/src/testing/auth.ts +0 -110
  53. package/src/testing/configurable-fake.ts +0 -45
  54. package/src/testing/db.ts +0 -194
  55. package/src/testing/fakes.ts +0 -153
  56. package/src/testing/index.ts +0 -31
  57. package/src/testing/stripe-fixtures.ts +0 -175
@@ -1,207 +0,0 @@
1
- import type { Context, Env } from 'hono';
2
- import type { ContentfulStatusCode } from 'hono/utils/http-status';
3
-
4
- /**
5
- * Reason phrases attached by the NestJS default exception filter, keyed by HTTP status code.
6
- *
7
- * @remarks
8
- * Mirrors the `error` field values NestJS produces for common client-error statuses, so a Hono app can
9
- * return byte-identical error bodies. Used as the default `reasonPhrases` map by {@link createNestErrorHandler}.
10
- */
11
- export const NEST_REASON_PHRASES: Record<number, string> = {
12
- 400: 'Bad Request',
13
- 401: 'Unauthorized',
14
- 403: 'Forbidden',
15
- 404: 'Not Found',
16
- };
17
-
18
- /**
19
- * Contextual metadata passed to an {@link ErrorReporter} when reporting an unexpected error.
20
- */
21
- export interface ErrorReportContext {
22
- /** Correlation id for the failing request, if one is tracked. */
23
- requestId?: string;
24
- }
25
-
26
- /**
27
- * Signature of a function that reports an unexpected (non-HTTP) error to an external sink such as Sentry.
28
- *
29
- * @remarks
30
- * Wire it into {@link createNestErrorHandler} via `onUnhandledError`, e.g.
31
- * `(err, c) => reporter(err, { requestId: c.get('requestId') })`. The reporting client itself is
32
- * intentionally kept out of this kit; the consumer supplies the implementation.
33
- *
34
- * @param error - The thrown value being reported.
35
- * @param context - Optional correlation context for the failing request.
36
- */
37
- export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
38
-
39
- /**
40
- * Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
41
- *
42
- * @internal
43
- */
44
- interface HttpErrorLike {
45
- /** HTTP status code to respond with. */
46
- status: ContentfulStatusCode;
47
- /** Human-readable error message placed in the response body. */
48
- message: string;
49
- /**
50
- * Escape hatch for a fully custom response body. When present, it is rendered verbatim instead of
51
- * the NestJS-shaped body.
52
- */
53
- body?: unknown;
54
- }
55
-
56
- /**
57
- * Options controlling how {@link createNestErrorHandler} shapes error responses.
58
- *
59
- * @typeParam E - The Hono environment type, so `onUnhandledError` receives a correctly typed context.
60
- */
61
- export interface NestErrorHandlerOptions<E extends Env = Env> {
62
- /** Status-to-reason-phrase map for the `error` field. Defaults to {@link NEST_REASON_PHRASES}. */
63
- reasonPhrases?: Record<number, string>;
64
- /**
65
- * Statuses that return only `{ statusCode, message }`, omitting the `error` field. Defaults to `[401]`,
66
- * matching NestJS where a generic `HttpException(msg, 401)` carries no `error`.
67
- */
68
- bareStatuses?: readonly number[];
69
- /**
70
- * Field order of the non-bare error body. Defaults to `'statusCode-first'` (the NestJS canonical order).
71
- * Use `'message-first'` to emit `{ message, error, statusCode }` when byte parity requires it.
72
- */
73
- fieldOrder?: 'statusCode-first' | 'message-first';
74
- /**
75
- * Fallback `error` value for statuses that are neither bare nor present in `reasonPhrases`. Defaults to
76
- * `undefined`, meaning the `error` field is omitted when no reason phrase is known. Set to a string such
77
- * as `'Error'` to always include an `error` field, faithfully reproducing the NestJS default exception
78
- * filter behavior where `error` is always present.
79
- */
80
- fallbackReason?: string;
81
- /**
82
- * Predicate identifying which thrown values are HTTP errors. Defaults to detecting Hono's `HTTPException`.
83
- * Override it (e.g. `(e) => e instanceof MyHttpError`) when the app throws a custom HTTP error type.
84
- */
85
- isHttpError?: (err: unknown) => err is HttpErrorLike;
86
- /**
87
- * Hook invoked before an unexpected (non-HTTP) error is returned as a 500, typically used to report the
88
- * error (e.g. to Sentry). Any exception thrown by this hook is swallowed so reporting cannot alter the
89
- * error response.
90
- */
91
- onUnhandledError?: (err: unknown, c: Context<E>) => void;
92
- /**
93
- * Response body for unexpected errors returned as 500. Defaults to
94
- * `{ statusCode: 500, message: 'Internal server error' }`.
95
- */
96
- internalServerErrorBody?: unknown;
97
- }
98
-
99
- /**
100
- * Structurally detect Hono's `HTTPException` without relying on `instanceof`.
101
- *
102
- * @remarks
103
- * When this kit is symlinked into a consumer, the `hono` instance it resolves can differ from the
104
- * consumer's `hono`, so an `HTTPException` from one copy fails an `instanceof` check against the other.
105
- * Detecting the presence of a `getResponse()` method and a numeric `status` is stable across module
106
- * boundaries and production bundles.
107
- *
108
- * @param err - The thrown value to test.
109
- * @returns `true` when `err` looks like a Hono `HTTPException`.
110
- *
111
- * @internal
112
- */
113
- const isHTTPException = (err: unknown): err is HttpErrorLike =>
114
- err instanceof Error &&
115
- typeof (err as { getResponse?: unknown }).getResponse === 'function' &&
116
- typeof (err as { status?: unknown }).status === 'number';
117
-
118
- /**
119
- * Create a Hono `onError` handler that maps thrown errors to NestJS-shaped error JSON.
120
- *
121
- * @remarks
122
- * Reproduces the NestJS default exception filter so a Hono app returns byte-identical error bodies:
123
- * - HTTP errors (by default `HTTPException`) are mapped to a NestJS-shaped body; if the error carries a
124
- * custom `body`, that body is returned verbatim.
125
- * - Statuses listed in `bareStatuses` (default `[401]`) omit the `error` field.
126
- * - Any other (unexpected) error triggers `onUnhandledError`, is logged via `console.error`, and returns 500.
127
- *
128
- * Per-app differences in body field order, HTTP error type, and reporting hook are absorbed through
129
- * {@link NestErrorHandlerOptions}, while the branching logic stays shared.
130
- *
131
- * @typeParam E - The Hono environment type propagated to `onUnhandledError`.
132
- * @param options - Overrides for reason phrases, bare statuses, field order, error detection, and reporting.
133
- * @returns A handler suitable for `app.onError(...)`.
134
- *
135
- * @example
136
- * ```ts
137
- * app.onError(
138
- * createNestErrorHandler({
139
- * fieldOrder: 'message-first',
140
- * fallbackReason: 'Error',
141
- * onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
142
- * }),
143
- * );
144
- * ```
145
- */
146
- export function createNestErrorHandler<E extends Env = Env>(options: NestErrorHandlerOptions<E> = {}) {
147
- const {
148
- reasonPhrases = NEST_REASON_PHRASES,
149
- bareStatuses = [401],
150
- fieldOrder = 'statusCode-first',
151
- isHttpError = isHTTPException,
152
- onUnhandledError,
153
- internalServerErrorBody = { statusCode: 500, message: 'Internal server error' },
154
- fallbackReason,
155
- } = options;
156
-
157
- return (err: Error, c: Context<E>): Response => {
158
- if (isHttpError(err)) {
159
- // Escape hatch for a custom error body: render it verbatim.
160
- if (err.body !== undefined) {
161
- return c.json(err.body as object, err.status);
162
- }
163
- // reasonPhrases[status] is typed as string, but with noUncheckedIndexedAccess disabled it can be
164
- // undefined at runtime. The fallbackReason fallback for unregistered statuses is intentional.
165
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
166
- const reason = bareStatuses.includes(err.status) ? undefined : (reasonPhrases[err.status] ?? fallbackReason);
167
- if (reason === undefined) {
168
- return c.json({ statusCode: err.status, message: err.message }, err.status);
169
- }
170
- const body =
171
- fieldOrder === 'message-first'
172
- ? { message: err.message, error: reason, statusCode: err.status }
173
- : { statusCode: err.status, message: err.message, error: reason };
174
- return c.json(body, err.status);
175
- }
176
-
177
- try {
178
- onUnhandledError?.(err, c);
179
- } catch {
180
- // Reporting must never change the behavior of the error response.
181
- }
182
- console.error(err);
183
- return c.json(internalServerErrorBody as object, 500);
184
- };
185
- }
186
-
187
- /**
188
- * Hono `notFound` handler that returns the canonical Express/NestJS unmatched-route 404 body.
189
- *
190
- * @remarks
191
- * Produces `{ message: "Cannot <METHOD> <path>", error: 'Not Found', statusCode: 404 }`, matching the
192
- * NestJS default 404 response so unmatched routes stay byte-identical.
193
- *
194
- * @param c - The Hono request context for the unmatched route.
195
- * @returns A 404 JSON response.
196
- *
197
- * @example
198
- * ```ts
199
- * app.notFound(nestNotFoundHandler);
200
- * ```
201
- */
202
- export function nestNotFoundHandler(c: Context): Response {
203
- return c.json(
204
- { message: `Cannot ${c.req.method} ${new URL(c.req.url).pathname}`, error: 'Not Found', statusCode: 404 },
205
- 404,
206
- );
207
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Trailing-slash normalization for NestJS(Express) → Hono parity.
3
- *
4
- * Express runs with strict routing off by default, so `/x` and `/x/` resolve to the same handler.
5
- * Hono distinguishes them, so a client that sends e.g. `GET /functions/timeline/` 404s against a
6
- * Hono worker that only registered `/functions/timeline`. Apply this at the Worker `fetch` entry to
7
- * strip a trailing slash before routing, preserving method / headers / body.
8
- *
9
- * We intentionally do NOT use `hono/trailing-slash`'s `trimTrailingSlash`, which responds with a 301
10
- * redirect and thus breaks non-GET methods (POST/PUT/DELETE) and clients that don't follow redirects.
11
- *
12
- * @example
13
- * export default {
14
- * fetch: (request, env, ctx) => app.fetch(normalizeTrailingSlash(request), env, ctx),
15
- * };
16
- *
17
- * @param request - The incoming request.
18
- * @returns The same request when the path has no trailing slash (or is exactly `/`), otherwise a new
19
- * Request with the trailing slash(es) removed.
20
- */
21
- export const normalizeTrailingSlash = (request: Request): Request => {
22
- const url = new URL(request.url);
23
- if (url.pathname.length > 1 && url.pathname.endsWith('/')) {
24
- url.pathname = url.pathname.replace(/\/+$/, '');
25
- return new Request(url, request);
26
- }
27
- return request;
28
- };
@@ -1,36 +0,0 @@
1
- import type { Context } from 'hono';
2
-
3
- /**
4
- * The client's network identity: IP address and user agent.
5
- *
6
- * @remarks
7
- * Equivalent to the data a NestJS `@UserProtocol` decorator would expose, so a Hono app can persist the
8
- * same client metadata. Both fields are nullable to map directly onto nullable database columns.
9
- */
10
- export interface IUserProtocol {
11
- /** Client IP address, or `null` when no source header is present. */
12
- ipAddress: string | null;
13
- /** Client user-agent string, or `null` when the `User-Agent` header is absent. */
14
- userAgent: string | null;
15
- }
16
-
17
- /**
18
- * Read the client's IP address and user agent from the Hono request context.
19
- *
20
- * @remarks
21
- * On Cloudflare the real client IP is provided in `CF-Connecting-IP`, with `X-Forwarded-For` used as a
22
- * fallback. Missing values resolve to `null` so they map cleanly onto nullable storage.
23
- *
24
- * @param c - The Hono request context to read headers from.
25
- * @returns The client's IP address and user agent for the current request.
26
- *
27
- * @example
28
- * ```ts
29
- * const { ipAddress, userAgent } = getUserProtocol(c);
30
- * await auditLog.insert({ ipAddress, userAgent });
31
- * ```
32
- */
33
- export const getUserProtocol = (c: Context): IUserProtocol => ({
34
- ipAddress: c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for') ?? null,
35
- userAgent: c.req.header('user-agent') ?? null,
36
- });
package/src/index.ts DELETED
@@ -1,77 +0,0 @@
1
- /**
2
- * `@rdlabo/workers-hono-kit` — infrastructure-layer helpers for Hono on Cloudflare Workers.
3
- *
4
- * This package collects reusable, configuration-injected building blocks (HTTP middleware,
5
- * caching, Stripe, Drizzle helpers, AI Gateway, AWS/Firebase integrations) that can be shared
6
- * across services. Domain logic, database schemas, and application-specific behavior are
7
- * intentionally left to the consuming application; only generic infrastructure that can be made
8
- * reusable through dependency/configuration injection lives here.
9
- *
10
- * @packageDocumentation
11
- */
12
-
13
- // middleware
14
- export { finalizeResponse } from './middleware/finalize-response.js';
15
- export { validate, createSentryValidate } from './middleware/validation.js';
16
- export type {
17
- ValidateOptions,
18
- ValidationTarget,
19
- ZodErrorLike,
20
- SentryLike,
21
- SentryScopeLike,
22
- } from './middleware/validation.js';
23
- export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
24
- export { createAuthMiddleware } from './middleware/auth.js';
25
- export type { AuthMiddlewareOptions } from './middleware/auth.js';
26
-
27
- // http
28
- export { getUserProtocol } from './http/user-protocol.js';
29
- export type { IUserProtocol } from './http/user-protocol.js';
30
- export { getAppInfo } from './http/app-info.js';
31
- export type { AppInfo } from './http/app-info.js';
32
- export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
33
- export type { AppEnv } from './http/app-env.js';
34
- export { HttpStatus } from './http/http-status.js';
35
- export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
36
- export type { NestErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/nest-error.js';
37
- export { normalizeTrailingSlash } from './http/trailing-slash.js';
38
- export type { ExecutionContextLike } from './http/execution-context.js';
39
-
40
- // cache
41
- export { KVCache } from './cache/kv-cache.js';
42
- export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
43
-
44
- // stripe
45
- export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
46
- export type { CreateStripeClientOptions } from './stripe/client.js';
47
-
48
- // db
49
- export { retryWhenDeadlock } from './db/retry.js';
50
-
51
- // queue
52
- export { sendInChunks } from './queue/send.js';
53
- export type { QueueLike, QueueSendMessage } from './queue/send.js';
54
- export { processBatch } from './queue/consumer.js';
55
- export type { QueueMessageLike, MessageBatchLike, ProcessBatchOptions, ProcessBatchResult } from './queue/consumer.js';
56
-
57
- // ai
58
- export { createAiGatewayProvider } from './ai/gateway.js';
59
- export type {
60
- AiGatewayConfig,
61
- AiGatewayProvider,
62
- AiGatewayBinding,
63
- AiGateway,
64
- AiGatewayOptions,
65
- } from './ai/gateway.js';
66
-
67
- // aws
68
- export { getAuthenticationSecret } from './aws/secrets-manager.js';
69
- export type { AwsSecretsOptions } from './aws/secrets-manager.js';
70
- export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
71
-
72
- // firebase
73
- export type { DecodedIdToken, FirebaseVerifier } from './firebase/firebase-verifier.js';
74
- export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
75
- export { IdentityToolkit } from './firebase/identity-toolkit.js';
76
- export type { ServiceAccount } from './firebase/identity-toolkit.js';
77
- export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier.js';
@@ -1,129 +0,0 @@
1
- import type { Context, Env, MiddlewareHandler } from 'hono';
2
- import { HTTPException } from 'hono/http-exception';
3
- import type { ContentfulStatusCode } from 'hono/utils/http-status';
4
- import { getAppInfo } from '../http/app-info.js';
5
- import type { AppInfo } from '../http/app-info.js';
6
-
7
- /**
8
- * Configuration for {@link createAuthMiddleware}.
9
- *
10
- * @typeParam E - The Hono `Env` (bindings/variables) of the application.
11
- * @typeParam Verified - The value produced by {@link AuthMiddlewareOptions.verify} (e.g. a decoded token or user record).
12
- * @typeParam Id - The resolved user identifier type.
13
- */
14
- export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
15
- /** Header carrying the ID token. Defaults to `'x-amz-security-token'`. */
16
- tokenHeader?: string;
17
- /**
18
- * Verify the raw token and return the decoded value or user record.
19
- *
20
- * @param token - The raw token read from {@link AuthMiddlewareOptions.tokenHeader} (empty string if absent).
21
- * @param c - The current Hono context.
22
- * @returns The verified value passed to {@link AuthMiddlewareOptions.resolveUserId}/{@link AuthMiddlewareOptions.setContext}.
23
- * @throws If the token is invalid; rejecting/throwing triggers the failure path.
24
- */
25
- verify: (token: string, c: Context<E>) => Promise<Verified>;
26
- /**
27
- * Resolve the database user id (creating the user if necessary).
28
- *
29
- * @remarks
30
- * Omit this to run in **token-only** mode (verification only, e.g. for login). Create-on-miss
31
- * behavior (such as `getUserId(...).catch(() => createUser(...))`) should be composed here by the
32
- * caller.
33
- *
34
- * @param verified - The value returned by {@link AuthMiddlewareOptions.verify}.
35
- * @param c - The current Hono context.
36
- * @param appInfo - The resolved application info for the request.
37
- * @returns The resolved user id.
38
- */
39
- resolveUserId?: (verified: Verified, c: Context<E>, appInfo: AppInfo) => Promise<Id>;
40
- /**
41
- * Store the verification result on the context variables.
42
- *
43
- * @remarks
44
- * Inject the application-specific variable names here (e.g. `decodedToken`, `userRecord`, `userProtocol`).
45
- *
46
- * @param c - The current Hono context.
47
- * @param data - The verified value, resolved app info, and (when available) the user id.
48
- */
49
- setContext: (c: Context<E>, data: { verified: Verified; appInfo: AppInfo; userId?: Id }) => void;
50
- /**
51
- * Override the failure behavior.
52
- *
53
- * @remarks
54
- * Defaults to `throw new HTTPException(failureStatus, { message: failureMessage })`. Provide this to
55
- * return a custom `Response` instead (e.g. `c.json(body, status)`).
56
- *
57
- * @param err - The error thrown during verification/resolution.
58
- * @param c - The current Hono context.
59
- * @returns The failure response to send.
60
- */
61
- onFailure?: (err: unknown, c: Context<E>) => Response;
62
- /** Status used by the default `onFailure`. Defaults to `403`. */
63
- failureStatus?: ContentfulStatusCode;
64
- /** Message used by the default `onFailure`. Defaults to `'Forbidden resource'`. */
65
- failureMessage?: string;
66
- }
67
-
68
- /**
69
- * Create an authentication middleware equivalent to a NestJS `AuthGuard` / `TokenGuard`.
70
- *
71
- * The middleware runs a fixed skeleton — read the token header, `verify`, `getAppInfo`,
72
- * `resolveUserId`, `setContext`, and on error `console.error` then `onFailure` — while the
73
- * application injects the variable parts (token verification, user-id resolution, context variable
74
- * names, and the failure response). Omitting {@link AuthMiddlewareOptions.resolveUserId} yields a
75
- * token-only middleware.
76
- *
77
- * @typeParam E - The Hono `Env` of the application.
78
- * @typeParam Verified - The value produced by `verify`.
79
- * @typeParam Id - The resolved user identifier type.
80
- * @param options - The verification, resolution, and failure hooks; see {@link AuthMiddlewareOptions}.
81
- * @returns A {@link MiddlewareHandler} that authenticates the request and populates the context.
82
- * @throws HTTPException From the default failure handler when `onFailure` is not supplied.
83
- *
84
- * @example
85
- * ```ts
86
- * const auth = createAuthMiddleware({
87
- * verify: (token, c) => verifier.verifyIdToken(token),
88
- * resolveUserId: (decoded) => findUserId(decoded.uid),
89
- * setContext: (c, { verified, userId }) => {
90
- * c.set('decodedToken', verified);
91
- * c.set('userId', userId);
92
- * },
93
- * });
94
- * app.use('/api/*', auth);
95
- * ```
96
- */
97
- export function createAuthMiddleware<E extends Env = Env, Verified = unknown, Id = unknown>(
98
- options: AuthMiddlewareOptions<E, Verified, Id>,
99
- ): MiddlewareHandler<E> {
100
- const {
101
- tokenHeader = 'x-amz-security-token',
102
- verify,
103
- resolveUserId,
104
- setContext,
105
- onFailure,
106
- failureStatus = 403,
107
- failureMessage = 'Forbidden resource',
108
- } = options;
109
-
110
- return async (c, next) => {
111
- try {
112
- const token = c.req.header(tokenHeader) ?? '';
113
- const verified = await verify(token, c);
114
- const appInfo = getAppInfo(c);
115
- const userId = resolveUserId ? await resolveUserId(verified, c, appInfo) : undefined;
116
- setContext(c, { verified, appInfo, userId });
117
- } catch (e) {
118
- // Equivalent to a guard returning false → ForbiddenException('Forbidden resource'). Log the
119
- // cause and, by default, throw so the app's onError renders the error body (callers can
120
- // override with onFailure to return a custom response instead).
121
- console.error(e);
122
- if (onFailure) {
123
- return onFailure(e, c);
124
- }
125
- throw new HTTPException(failureStatus, { message: failureMessage });
126
- }
127
- await next();
128
- };
129
- }
@@ -1,90 +0,0 @@
1
- import type { MiddlewareHandler } from 'hono';
2
-
3
- /**
4
- * Compute an Express `etag`-package compatible weak ETag for a response body.
5
- *
6
- * The format is `W/"<byteLength-in-hex>-<first 27 chars of base64(sha1(body))>"`, byte-for-byte
7
- * identical to the weak ETag produced by the Express `etag` package. This deliberately differs
8
- * from `hono/etag`'s own format so responses match an Express/Nest backend exactly.
9
- *
10
- * @param body - The raw response body bytes to hash.
11
- * @returns The weak ETag header value (e.g. `W/"1a-Qwerty..."`).
12
- * @internal
13
- */
14
- async function weakEtag(body: ArrayBuffer): Promise<string> {
15
- const digest = await crypto.subtle.digest('SHA-1', body);
16
- const bytes = new Uint8Array(digest);
17
- let bin = '';
18
- for (const b of bytes) {
19
- bin += String.fromCharCode(b);
20
- }
21
- const b64 = btoa(bin).substring(0, 27);
22
- return `W/"${body.byteLength.toString(16)}-${b64}"`;
23
- }
24
-
25
- /**
26
- * Create a Hono middleware that finalizes responses for byte-parity with an Express/Nest backend.
27
- *
28
- * After the downstream handler runs, it performs two adjustments:
29
- *
30
- * 1. **JSON charset**: Express's `res.json` emits `application/json; charset=utf-8`, whereas Hono's
31
- * `c.json` emits a bare `application/json`. A bare `application/json` content type is rewritten
32
- * to include `; charset=utf-8`.
33
- * 2. **Weak ETag**: An Express `etag`-package compatible weak ETag is added, matching the format an
34
- * Express/Nest backend applies to responses by default. See {@link weakEtag} for the exact format.
35
- *
36
- * Server-Sent Events (`text/event-stream`) are skipped entirely because the stream cannot be
37
- * buffered. ETag generation is also skipped for `204`/`304` responses, responses that already carry
38
- * an `etag` header, and responses without a body.
39
- *
40
- * @returns A {@link MiddlewareHandler} that rewrites the response headers (and body, when an ETag
41
- * must be computed) in place.
42
- *
43
- * @example
44
- * ```ts
45
- * import { Hono } from 'hono';
46
- * import { finalizeResponse } from '@rdlabo/workers-hono-kit';
47
- *
48
- * const app = new Hono();
49
- * app.use('*', finalizeResponse());
50
- * app.get('/users', (c) => c.json({ ok: true }));
51
- * // → Content-Type: application/json; charset=utf-8
52
- * // → ETag: W/"b-..." (b = 0xb = 11 bytes, the length of `{"ok":true}`)
53
- * ```
54
- */
55
- export function finalizeResponse(): MiddlewareHandler {
56
- return async (c, next) => {
57
- await next();
58
-
59
- const status = c.res.status;
60
- const contentType = c.res.headers.get('content-type') ?? '';
61
-
62
- // Leave SSE / streaming responses untouched.
63
- if (contentType.includes('text/event-stream')) {
64
- return;
65
- }
66
-
67
- // Charset target: add the charset when a JSON response leaves it unspecified.
68
- const needsCharset = contentType === 'application/json';
69
- // ETag target: Express omits it on 204/304 and respects an existing ETag.
70
- const needsEtag = status !== 204 && status !== 304 && !c.res.headers.has('etag') && !!c.res.body;
71
-
72
- if (!needsCharset && !needsEtag) {
73
- return;
74
- }
75
-
76
- const headers = new Headers(c.res.headers);
77
- if (needsCharset) {
78
- headers.set('content-type', 'application/json; charset=utf-8');
79
- }
80
-
81
- if (needsEtag) {
82
- const buf = await c.res.clone().arrayBuffer();
83
- headers.set('ETag', await weakEtag(buf));
84
- c.res = new Response(buf, { status, statusText: c.res.statusText, headers });
85
- } else {
86
- // Swap only the headers without reading the body.
87
- c.res = new Response(c.res.body, { status, statusText: c.res.statusText, headers });
88
- }
89
- };
90
- }