@rdlabo/workers-hono-kit 0.6.1 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,9 +5,9 @@ Infrastructure toolkit for building APIs on [Hono](https://hono.dev) + [Cloudfla
5
5
  It provides the building blocks a NestJS-style API needs but that don't run on `workerd` (no Node.js AWS SDK, no `firebase-admin`), plus middleware that matches Express / NestJS response semantics byte-for-byte:
6
6
 
7
7
  - **Firebase ID-token verification** on Workers via [`jose`](https://github.com/panva/jose) (RS256 against Google's securetoken JWKS), with optional Identity Toolkit REST for `getUser` / `deleteUser`.
8
- - **AWS Secrets Manager** via SigV4-signed `fetch` ([`aws4fetch`](https://github.com/mhart/aws4fetch)) — no AWS SDK.
8
+ - **AWS Secrets Manager / STS AssumeRole / CloudFront signed URLs** via SigV4-signed `fetch` ([`aws4fetch`](https://github.com/mhart/aws4fetch)) or Web Crypto — no AWS SDK.
9
9
  - **Middleware**: `finalizeResponse` (Express-compatible weak ETag + JSON charset), `validate` (NestJS `ValidationPipe`-shaped 400), and zod number-coercion helpers.
10
- - **NestJS-shaped errors**: `createNestErrorHandler` / `nestNotFoundHandler` / `HttpStatus`.
10
+ - **Standard API errors**: `createHttpErrorHandler` / `notFoundHandler` / `HttpStatus`.
11
11
  - **Deadlock retry** (`ER_LOCK_DEADLOCK` exponential backoff) and an optional **MySQL data layer** (`@rdlabo/workers-hono-kit/db`) for Hyperdrive + Drizzle.
12
12
  - **AI Gateway**: route `@ai-sdk` models through the Cloudflare AI Gateway.
13
13
  - **Stripe** Workers-native client + async webhook verification.
@@ -56,6 +56,7 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
56
56
  | `createSentryValidate(sentry)` | **Deprecated** — use `createValidate({ sentry })`. |
57
57
  | `zNum` / `zNumWithDefault` / `zNumOptional` / `zNumNullable` | Number-coercion zod schemas (mirror class-transformer `@Transform`). |
58
58
  | `getAuthenticationSecret<T>(options, secretId)` / `AwsSecretsOptions` | Fetch a secret from AWS Secrets Manager (SigV4 `fetch`, per-isolate cache). |
59
+ | `getTemporaryCredentials(options)` / `GetTemporaryCredentialsOptions` / `StsCredentials` | STS `AssumeRole` via SigV4 `fetch` (global `sts.amazonaws.com`); returns temporary credentials for browser S3 uploads. |
59
60
  | `getCloudFrontSignedUrl(url, privateKeyPem, keyPairId, dateLessThan)` | CloudFront signed URL (canned policy, RSA-SHA1, URL-safe base64) — Web Crypto reimpl of `@aws-sdk/cloudfront-signer`, byte-identical query order. |
60
61
  | `JoseFirebaseVerifier` / `FirebaseVerifier` / `DecodedIdToken` | Firebase ID-token verification (`verifyIdToken`, `getUser`, `deleteUser`). |
61
62
  | `createRemoteFirebaseVerifier(projectId)` | Convenience factory: production verifier with a cached remote JWKS (verification only). |
@@ -66,17 +67,17 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
66
67
  | `getAppInfo(c)` / `AppInfo` | Read `x-amz-meta-version` / `x-amz-meta-uuid`. |
67
68
  | `resolveAppEnv(env)` / `isProductionEnv(env)` / `AppEnv` | Resolve `'development'` / `'production'` from `env.APP_ENV` (defaults to `'production'` for safety). |
68
69
  | `HttpStatus` | HTTP status enum identical to NestJS `@nestjs/common`. |
69
- | `createNestErrorHandler(options?)` / `NestErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to the NestJS exception-filter body (`{ statusCode, message, error? }`; `401` omits `error`). Configurable field order, reason phrases, error predicate, and unhandled-error report hook. Unhandled errors log via `console.error` (mysql2 errors include `sqlMessage` / `errno` when detectable). |
70
- | `createAppErrorHandler(options?)` / `CreateAppErrorHandlerOptions` | Standard `app.onError`: {@link createQueryFailedNestErrorHandler} + default {@link classifyGenericMysqlDriverError} + optional `sentry` (Sentry apps), `getReportError` / `reportError` (tests / container), or neither (no external reporting). |
71
- | `createQueryFailedNestErrorHandler(options)` / `QueryFailedClassifier` / `ClassifiedDbError` | Lower-level compose when you need full control over `classify` + `onUnhandledError` without defaults. |
72
- | `classifyGenericMysqlDriverError(err)` | Default classifier for apps without Nest filter parity: any mysql2 driver error → `{ statusCode: 500, message: 'Internal server error' }`; non-DB errors → `null`. |
73
- | `findMysqlDriverError(err)` / `logMysqlDriverError(err, statusCode)` | Low-level mysql2 driver-error detection (follows `err.cause`) and structured logging. For custom classifiers (e.g. odss parity). |
74
- | `nestNotFoundHandler(c)` | `app.notFound()` handler with the Express/Nest default `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
70
+ | `createHttpErrorHandler(options?)` / `HttpErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to `{ statusCode, message, error? }` (`401` omits `error`). Optional custom error predicate and unhandled-error report hook. Unhandled errors log via `console.error` (mysql2 errors include `sqlMessage` / `errno` when detectable). |
71
+ | `createAppErrorHandler(options?)` / `CreateAppErrorHandlerOptions` | Standard `app.onError`: {@link createQueryFailedErrorHandler} + default {@link classifyGenericMysqlDriverError} + optional `sentry` (Sentry apps), `getReportError` / `reportError` (tests / container), or neither (no external reporting). |
72
+ | `createQueryFailedErrorHandler(options)` / `QueryFailedClassifier` / `ClassifiedDbError` | Lower-level compose when you need full control over `classify` + `onUnhandledError` without defaults. |
73
+ | `classifyGenericMysqlDriverError(err)` | Default classifier: any mysql2 driver error → `{ statusCode: 500, message: 'Internal server error' }`; non-DB errors → `null`. |
74
+ | `findMysqlDriverError(err)` / `logMysqlDriverError(err, statusCode)` | Low-level mysql2 driver-error detection (follows `err.cause`) and structured logging. For custom classifiers (e.g. odss). |
75
+ | `notFoundHandler(c)` | `app.notFound()` handler with `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
75
76
  | `normalizeTrailingSlash(request)` | Strip trailing slash(es) from the request URL before routing (Express/Nest parity). Does **not** 301-redirect — preserves POST/PUT/DELETE bodies. |
76
- | `NEST_REASON_PHRASES` | `{ 400, 401, 403, 404 }` → NestJS reason phrases. |
77
+ | `HTTP_ERROR_PHRASES` | `{ 400, 401, 403, 404 }` → standard `error` field phrases. |
77
78
  | `createAuthMiddleware(options)` / `AuthMiddlewareOptions` | Factory for a Firebase-token auth middleware: reads the token header, verifies, resolves the DB user id, and stashes the result on the context. Omit `resolveUserId` for a token-only (login) guard. |
78
79
  | `perfLog(options?)` / `PerfLogOptions` / `AnalyticsEngineDatasetLike` | Middleware that records one per-request latency data point (`t_app`, colo, cold/warm, route, status) and emits it to **Workers Logs** (`console.log`) and/or **Workers Analytics Engine** (`writeDataPoint`). Lets you measure low-traffic Workers without a live `wrangler tail`. |
79
- | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
80
+ | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createHttpErrorHandler`'s `onUnhandledError`. |
80
81
  | `createSentryErrorReporter(sentry)` / `SentryExceptionReporterLike` | Build an `ErrorReporter` that forwards to Sentry with an optional `request_id` tag (no hard `@sentry/cloudflare` dependency). |
81
82
  | `DeferExecutor` / `defaultDefer` / `createWaitUntilDefer(ctx)` | Fire-and-forget executor for Workers: `defaultDefer` swallows rejections (tests); `createWaitUntilDefer` registers work via `ctx.waitUntil`. |
82
83
  | `createAiGatewayProvider(config)` / `AiGatewayConfig` / `AiGatewayProvider` | Route `@ai-sdk` models through the Cloudflare AI Gateway, via either a Workers `AI` binding or REST credentials (`accountId` / `gateway` / `token`). |
@@ -271,6 +272,20 @@ const secret = await getAuthenticationSecret<MySecret>(
271
272
  );
272
273
  ```
273
274
 
275
+ ### STS AssumeRole (browser S3 uploads)
276
+
277
+ ```ts
278
+ import { getTemporaryCredentials } from '@rdlabo/workers-hono-kit';
279
+
280
+ const credentials = await getTemporaryCredentials({
281
+ accessKeyId: env.AWS_ACCESS_KEY_ID,
282
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
283
+ roleArn: 'arn:aws:iam::123456789012:role/s3-put-app-only-role',
284
+ roleSessionName: `session-${userId}-${Date.now()}`,
285
+ });
286
+ // Return credentials to the browser; PutObject uses @aws-sdk/client-s3 with AccessKeyId / …
287
+ ```
288
+
274
289
  ### Deadlock retry & HTTP helpers
275
290
 
276
291
  ```ts
@@ -283,12 +298,10 @@ const appInfo = getAppInfo(c);
283
298
  return c.json(body, HttpStatus.CREATED);
284
299
  ```
285
300
 
286
- ### NestJS-shaped error / 404 handlers
301
+ ### HTTP error / 404 handlers
287
302
 
288
- `createNestErrorHandler()` renders a thrown `HTTPException` as the NestJS exception-filter
289
- body, and `nestNotFoundHandler` gives the Express/Nest default 404. The defaults match the
290
- NestJS canonical shape (`{ statusCode, message, error? }`, `401` omits `error`); the options
291
- let you reproduce any byte-for-byte variation an existing API expects.
303
+ `createHttpErrorHandler()` renders a thrown `HTTPException` as standard API error JSON,
304
+ and `notFoundHandler` gives the default unmatched-route 404 body.
292
305
 
293
306
  #### App entry (fleet standard)
294
307
 
@@ -314,7 +327,7 @@ app.onError(
314
327
  );
315
328
 
316
329
  // odss-mobile: add classify: classifyQueryFailed (repo parity)
317
- // winecode: sentry + isHttpError / reasonPhrases in errors.ts (no container middleware)
330
+ // winecode: sentry + isHttpError in errors.ts (no container middleware)
318
331
  // foodlabel: sentry + reportError: container.reportError (per-request container closure)
319
332
  ```
320
333
 
@@ -341,19 +354,19 @@ const { middleware: containerMiddleware, withContainer } = createContainerRuntim
341
354
  Use `withContainer` from `scheduled` / `queue` handlers; use `containerMiddleware` in `createApp`.
342
355
 
343
356
  ```ts
344
- import { createNestErrorHandler, nestNotFoundHandler } from '@rdlabo/workers-hono-kit';
357
+ import { createHttpErrorHandler, notFoundHandler } from '@rdlabo/workers-hono-kit';
345
358
 
346
- app.notFound(nestNotFoundHandler);
359
+ app.notFound(notFoundHandler);
347
360
 
348
361
  // Prefer createAppErrorHandler (see "App entry" above). Lower-level only when needed:
349
- app.onError(createNestErrorHandler());
362
+ app.onError(createHttpErrorHandler());
350
363
  ```
351
364
 
352
365
  **Important:** `Sentry.withSentry` does **not** capture errors handled by `app.onError`. Pass `sentry`
353
366
  to `createAppErrorHandler` (or wire `getReportError` / `reportError` for tests and scheduled paths).
354
367
 
355
- Repos with a Nest `QueryFailedExceptionFilter` parity layer (e.g. odss-mobile) pass `classify` to
356
- `createAppErrorHandler` — do not call `createQueryFailedNestErrorHandler` directly unless you need full control.
368
+ Repos with a custom DB error classifier (e.g. odss-mobile) pass `classify` to
369
+ `createAppErrorHandler` — do not call `createQueryFailedErrorHandler` directly unless you need full control.
357
370
 
358
371
  ### Auth middleware
359
372
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * AWS credentials used to sign an STS `AssumeRole` request.
3
+ *
4
+ * @remarks
5
+ * Same shape as {@link AwsSecretsOptions} minus the required Secrets Manager region; STS defaults to
6
+ * the global endpoint (`us-east-1`) unless {@link GetTemporaryCredentialsOptions.region} is set.
7
+ */
8
+ export interface GetTemporaryCredentialsOptions {
9
+ /** AWS access key ID of the caller principal that may AssumeRole. */
10
+ accessKeyId: string;
11
+ /** AWS secret access key of the caller principal. */
12
+ secretAccessKey: string;
13
+ /** Optional STS session token when the caller already holds temporary credentials. */
14
+ sessionToken?: string;
15
+ /** ARN of the role to assume (e.g. `arn:aws:iam::123:role/s3-put-app-only-role`). */
16
+ roleArn: string;
17
+ /** Session name recorded in CloudTrail (often `session-${userId}-${Date.now()}`). */
18
+ roleSessionName: string;
19
+ /**
20
+ * Credential lifetime in seconds.
21
+ * @defaultValue 900
22
+ */
23
+ durationSeconds?: number;
24
+ /**
25
+ * STS SigV4 signing region.
26
+ * @defaultValue us-east-1
27
+ */
28
+ region?: string;
29
+ /**
30
+ * STS endpoint URL.
31
+ * @defaultValue https://sts.amazonaws.com/
32
+ */
33
+ endpoint?: string;
34
+ }
35
+ /**
36
+ * Temporary credentials returned by STS `AssumeRole`.
37
+ *
38
+ * @remarks
39
+ * Field names match the STS XML response / `@aws-sdk/client-sts` `Credentials` shape so browser
40
+ * apps can pass them straight into `@aws-sdk/client-s3` (`AccessKeyId` → `accessKeyId`, etc.).
41
+ */
42
+ export interface StsCredentials {
43
+ AccessKeyId?: string;
44
+ SecretAccessKey?: string;
45
+ SessionToken?: string;
46
+ Expiration?: Date;
47
+ }
48
+ /**
49
+ * Call STS `AssumeRole` via SigV4-signed `fetch` (aws4fetch) and return temporary credentials.
50
+ *
51
+ * Port of winecode / airlec browser-upload credential issuance — no AWS SDK. The consuming app
52
+ * supplies `roleArn` and `roleSessionName`; the kit only performs the signed STS request and XML parse.
53
+ *
54
+ * @param options - Caller AWS keys plus assume-role parameters.
55
+ * @returns Temporary credentials for browser or edge PutObject / GetObject.
56
+ * @throws Error When the STS response is not OK.
57
+ * @example
58
+ * ```ts
59
+ * const credentials = await getTemporaryCredentials({
60
+ * accessKeyId: env.AWS_ACCESS_KEY_ID,
61
+ * secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
62
+ * roleArn: 'arn:aws:iam::123:role/s3-put-app-only-role',
63
+ * roleSessionName: `session-${userId}-${Date.now()}`,
64
+ * });
65
+ * ```
66
+ */
67
+ export declare function getTemporaryCredentials(options: GetTemporaryCredentialsOptions): Promise<StsCredentials>;
@@ -0,0 +1,66 @@
1
+ import { AwsClient } from 'aws4fetch';
2
+ /** STS SigV4 signing region for the global endpoint (`sts.amazonaws.com`). */
3
+ const DEFAULT_STS_REGION = 'us-east-1';
4
+ /** winecode / airlec default — 15 minutes. */
5
+ const DEFAULT_DURATION_SECONDS = 900;
6
+ const STS_API_VERSION = '2011-06-15';
7
+ /** Global STS endpoint (unchanged from winecode AwsService). */
8
+ const DEFAULT_STS_ENDPOINT = 'https://sts.amazonaws.com/';
9
+ /**
10
+ * Call STS `AssumeRole` via SigV4-signed `fetch` (aws4fetch) and return temporary credentials.
11
+ *
12
+ * Port of winecode / airlec browser-upload credential issuance — no AWS SDK. The consuming app
13
+ * supplies `roleArn` and `roleSessionName`; the kit only performs the signed STS request and XML parse.
14
+ *
15
+ * @param options - Caller AWS keys plus assume-role parameters.
16
+ * @returns Temporary credentials for browser or edge PutObject / GetObject.
17
+ * @throws Error When the STS response is not OK.
18
+ * @example
19
+ * ```ts
20
+ * const credentials = await getTemporaryCredentials({
21
+ * accessKeyId: env.AWS_ACCESS_KEY_ID,
22
+ * secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
23
+ * roleArn: 'arn:aws:iam::123:role/s3-put-app-only-role',
24
+ * roleSessionName: `session-${userId}-${Date.now()}`,
25
+ * });
26
+ * ```
27
+ */
28
+ export async function getTemporaryCredentials(options) {
29
+ const region = options.region ?? DEFAULT_STS_REGION;
30
+ const durationSeconds = options.durationSeconds ?? DEFAULT_DURATION_SECONDS;
31
+ const endpoint = options.endpoint ?? DEFAULT_STS_ENDPOINT;
32
+ const params = new URLSearchParams({
33
+ Action: 'AssumeRole',
34
+ Version: STS_API_VERSION,
35
+ RoleArn: options.roleArn,
36
+ RoleSessionName: options.roleSessionName,
37
+ DurationSeconds: String(durationSeconds),
38
+ });
39
+ const aws = new AwsClient({
40
+ accessKeyId: options.accessKeyId,
41
+ secretAccessKey: options.secretAccessKey,
42
+ sessionToken: options.sessionToken,
43
+ region,
44
+ service: 'sts',
45
+ });
46
+ const response = await aws.fetch(endpoint, {
47
+ method: 'POST',
48
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
49
+ body: params.toString(),
50
+ });
51
+ const xml = await response.text();
52
+ if (!response.ok) {
53
+ throw new Error(`STS AssumeRole failed: ${response.status} ${xml}`);
54
+ }
55
+ const pick = (tag) => {
56
+ const m = new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml);
57
+ return m ? m[1] : undefined;
58
+ };
59
+ const expiration = pick('Expiration');
60
+ return {
61
+ AccessKeyId: pick('AccessKeyId'),
62
+ SecretAccessKey: pick('SecretAccessKey'),
63
+ SessionToken: pick('SessionToken'),
64
+ Expiration: expiration ? new Date(expiration) : undefined,
65
+ };
66
+ }
@@ -1,16 +1,15 @@
1
1
  import type { Context, Env } from 'hono';
2
- import type { ErrorReporter, NestErrorHandlerOptions, SentryExceptionReporterLike } from './nest-error.js';
2
+ import type { ErrorReporter, HttpErrorHandlerOptions, SentryExceptionReporterLike } from './http-error.js';
3
3
  import type { QueryFailedClassifier } from './query-failed-error.js';
4
4
  /**
5
5
  * Options for {@link createAppErrorHandler}.
6
6
  *
7
7
  * @remarks
8
- * Wires `createQueryFailedNestErrorHandler` with fleet defaults (`fieldOrder: 'message-first'`,
9
- * {@link classifyGenericMysqlDriverError}) and optional error reporting.
8
+ * Wires `createQueryFailedErrorHandler` with fleet defaults ({@link classifyGenericMysqlDriverError}) and optional error reporting.
10
9
  * Pass `sentry` for Sentry-backed apps; omit it (or pass `undefined`) when not used.
11
10
  * `getReportError` / `reportError` override `sentry` (tests, container injection, scheduled paths).
12
11
  */
13
- export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<NestErrorHandlerOptions<E>, 'onUnhandledError'> {
12
+ export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<HttpErrorHandlerOptions<E>, 'onUnhandledError'> {
14
13
  /** mysql2 driver error classifier. Defaults to {@link classifyGenericMysqlDriverError}. */
15
14
  classify?: QueryFailedClassifier;
16
15
  /** Optional Sentry client (`@sentry/cloudflare`). Omitted on repos without Sentry. */
@@ -23,6 +22,6 @@ export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<
23
22
  onUnhandledError?: (err: unknown, c: Context<E>) => void;
24
23
  }
25
24
  /**
26
- * Standard `app.onError` factory: QueryFailed filter → Nest default filter, with optional error reporting.
25
+ * Standard `app.onError` factory: QueryFailed filter → HTTP error handler, with optional error reporting.
27
26
  */
28
27
  export declare function createAppErrorHandler<E extends Env = Env>(options?: CreateAppErrorHandlerOptions<E>): (err: Error, c: Context<E, any, {}>) => Response;
@@ -1,10 +1,10 @@
1
- import { createSentryErrorReporter } from './nest-error.js';
2
- import { classifyGenericMysqlDriverError, createQueryFailedNestErrorHandler } from './query-failed-error.js';
1
+ import { createSentryErrorReporter } from './http-error.js';
2
+ import { classifyGenericMysqlDriverError, createQueryFailedErrorHandler } from './query-failed-error.js';
3
3
  /**
4
- * Standard `app.onError` factory: QueryFailed filter → Nest default filter, with optional error reporting.
4
+ * Standard `app.onError` factory: QueryFailed filter → HTTP error handler, with optional error reporting.
5
5
  */
6
6
  export function createAppErrorHandler(options = {}) {
7
- const { classify = classifyGenericMysqlDriverError, sentry, reportError, getReportError, onUnhandledError, fieldOrder = 'message-first', ...nestOptions } = options;
7
+ const { classify = classifyGenericMysqlDriverError, sentry, reportError, getReportError, onUnhandledError, ...httpOptions } = options;
8
8
  const sentryReporter = sentry ? createSentryErrorReporter(sentry) : undefined;
9
9
  const resolvedOnUnhandled = onUnhandledError ??
10
10
  ((err, c) => {
@@ -12,9 +12,8 @@ export function createAppErrorHandler(options = {}) {
12
12
  const requestId = c.get('requestId');
13
13
  reporter?.(err, { requestId });
14
14
  });
15
- return createQueryFailedNestErrorHandler({
16
- fieldOrder,
17
- ...nestOptions,
15
+ return createQueryFailedErrorHandler({
16
+ ...httpOptions,
18
17
  classify,
19
18
  onUnhandledError: resolvedOnUnhandled,
20
19
  });
@@ -0,0 +1,113 @@
1
+ import type { Context, Env } from 'hono';
2
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
3
+ /**
4
+ * Standard `error` field phrases for common HTTP status codes.
5
+ *
6
+ * @remarks
7
+ * Used by {@link createHttpErrorHandler} for the `error` field on client-error statuses.
8
+ */
9
+ export declare const HTTP_ERROR_PHRASES: Record<number, string>;
10
+ /**
11
+ * Contextual metadata passed to an {@link ErrorReporter} when reporting an unexpected error.
12
+ */
13
+ export interface ErrorReportContext {
14
+ /** Correlation id for the failing request, if one is tracked. */
15
+ requestId?: string;
16
+ }
17
+ /**
18
+ * Signature of a function that reports an unexpected (non-HTTP) error to an external sink such as Sentry.
19
+ *
20
+ * @remarks
21
+ * Wire it into {@link createHttpErrorHandler} via `onUnhandledError`, e.g.
22
+ * `(err, c) => reporter(err, { requestId: c.get('requestId') })`. The reporting client itself is
23
+ * intentionally kept out of this kit; the consumer supplies the implementation.
24
+ *
25
+ * @param error - The thrown value being reported.
26
+ * @param context - Optional correlation context for the failing request.
27
+ */
28
+ export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
29
+ /**
30
+ * Minimal Sentry-like client for {@link createSentryErrorReporter} and {@link createQueueErrorHandler}.
31
+ *
32
+ * @remarks
33
+ * Declared structurally to avoid a hard dependency on `@sentry/cloudflare`.
34
+ */
35
+ export interface SentryExceptionReporterLike {
36
+ captureException(exception: unknown, captureContext?: {
37
+ tags?: Record<string, string>;
38
+ extra?: Record<string, unknown>;
39
+ }): void;
40
+ }
41
+ /**
42
+ * Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
43
+ */
44
+ export declare function createSentryErrorReporter(sentry: SentryExceptionReporterLike): ErrorReporter;
45
+ /**
46
+ * Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
47
+ *
48
+ * @internal
49
+ */
50
+ interface HttpErrorLike {
51
+ /** HTTP status code to respond with. */
52
+ status: ContentfulStatusCode;
53
+ /** Human-readable error message placed in the response body. */
54
+ message: string;
55
+ /**
56
+ * Escape hatch for a fully custom response body. When present, it is rendered verbatim instead of
57
+ * the standard error JSON shape.
58
+ */
59
+ body?: unknown;
60
+ }
61
+ /**
62
+ * Options controlling how {@link createHttpErrorHandler} shapes error responses.
63
+ *
64
+ * @typeParam E - The Hono environment type, so `onUnhandledError` receives a correctly typed context.
65
+ */
66
+ export interface HttpErrorHandlerOptions<E extends Env = Env> {
67
+ /**
68
+ * Predicate identifying which thrown values are HTTP errors. Defaults to detecting Hono's `HTTPException`.
69
+ * Override it (e.g. `(e) => e instanceof MyHttpError`) when the app throws a custom HTTP error type.
70
+ */
71
+ isHttpError?: (err: unknown) => err is HttpErrorLike;
72
+ /**
73
+ * Hook invoked before an unexpected (non-HTTP) error is returned as a 500, typically used to report the
74
+ * error (e.g. to Sentry). Any exception thrown by this hook is swallowed so reporting cannot alter the
75
+ * error response.
76
+ */
77
+ onUnhandledError?: (err: unknown, c: Context<E>) => void;
78
+ }
79
+ /**
80
+ * Create a Hono `onError` handler that maps thrown errors to standard API error JSON.
81
+ *
82
+ * @remarks
83
+ * - HTTP errors (by default `HTTPException`) map to `{ statusCode, message, error? }`; `401` omits `error`.
84
+ * - Errors with a custom `body` are returned verbatim.
85
+ * - Unexpected errors trigger `onUnhandledError`, are logged, and return generic 500.
86
+ *
87
+ * @typeParam E - The Hono environment type propagated to `onUnhandledError`.
88
+ * @param options - Optional custom HTTP error detection and reporting hook.
89
+ * @returns A handler suitable for `app.onError(...)`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * app.onError(
94
+ * createHttpErrorHandler({
95
+ * onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
96
+ * }),
97
+ * );
98
+ * ```
99
+ */
100
+ export declare function createHttpErrorHandler<E extends Env = Env>(options?: HttpErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
101
+ /**
102
+ * Hono `notFound` handler for unmatched routes.
103
+ *
104
+ * @param c - The Hono request context for the unmatched route.
105
+ * @returns A 404 JSON response: `{ message: 'Cannot METHOD path', error, statusCode }`.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * app.notFound(notFoundHandler);
110
+ * ```
111
+ */
112
+ export declare function notFoundHandler(c: Context): Response;
113
+ export {};
@@ -0,0 +1,106 @@
1
+ import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
2
+ /** Statuses that return only `{ statusCode, message }` (no `error` field). */
3
+ const BARE_STATUSES = [401];
4
+ const INTERNAL_SERVER_ERROR_BODY = { statusCode: 500, message: 'Internal server error' };
5
+ /**
6
+ * Standard `error` field phrases for common HTTP status codes.
7
+ *
8
+ * @remarks
9
+ * Used by {@link createHttpErrorHandler} for the `error` field on client-error statuses.
10
+ */
11
+ export const HTTP_ERROR_PHRASES = {
12
+ 400: 'Bad Request',
13
+ 401: 'Unauthorized',
14
+ 403: 'Forbidden',
15
+ 404: 'Not Found',
16
+ };
17
+ /**
18
+ * Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
19
+ */
20
+ export function createSentryErrorReporter(sentry) {
21
+ return (error, context) => {
22
+ sentry.captureException(error, context?.requestId ? { tags: { request_id: context.requestId } } : undefined);
23
+ };
24
+ }
25
+ /**
26
+ * Structurally detect Hono's `HTTPException` without relying on `instanceof`.
27
+ *
28
+ * @remarks
29
+ * When this kit is symlinked into a consumer, the `hono` instance it resolves can differ from the
30
+ * consumer's `hono`, so an `HTTPException` from one copy fails an `instanceof` check against the other.
31
+ * Detecting the presence of a `getResponse()` method and a numeric `status` is stable across module
32
+ * boundaries and production bundles.
33
+ *
34
+ * @param err - The thrown value to test.
35
+ * @returns `true` when `err` looks like a Hono `HTTPException`.
36
+ *
37
+ * @internal
38
+ */
39
+ const isHTTPException = (err) => err instanceof Error &&
40
+ typeof err.getResponse === 'function' &&
41
+ typeof err.status === 'number';
42
+ /**
43
+ * Create a Hono `onError` handler that maps thrown errors to standard API error JSON.
44
+ *
45
+ * @remarks
46
+ * - HTTP errors (by default `HTTPException`) map to `{ statusCode, message, error? }`; `401` omits `error`.
47
+ * - Errors with a custom `body` are returned verbatim.
48
+ * - Unexpected errors trigger `onUnhandledError`, are logged, and return generic 500.
49
+ *
50
+ * @typeParam E - The Hono environment type propagated to `onUnhandledError`.
51
+ * @param options - Optional custom HTTP error detection and reporting hook.
52
+ * @returns A handler suitable for `app.onError(...)`.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * app.onError(
57
+ * createHttpErrorHandler({
58
+ * onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
59
+ * }),
60
+ * );
61
+ * ```
62
+ */
63
+ export function createHttpErrorHandler(options = {}) {
64
+ const { isHttpError = isHTTPException, onUnhandledError } = options;
65
+ return (err, c) => {
66
+ if (isHttpError(err)) {
67
+ if (err.body !== undefined) {
68
+ return c.json(err.body, err.status);
69
+ }
70
+ const reason = BARE_STATUSES.includes(err.status)
71
+ ? undefined
72
+ : HTTP_ERROR_PHRASES[err.status];
73
+ if (reason === undefined) {
74
+ return c.json({ statusCode: err.status, message: err.message }, err.status);
75
+ }
76
+ return c.json({ statusCode: err.status, message: err.message, error: reason }, err.status);
77
+ }
78
+ try {
79
+ onUnhandledError?.(err, c);
80
+ }
81
+ catch {
82
+ // Reporting must never change the behavior of the error response.
83
+ }
84
+ if (findMysqlDriverError(err)) {
85
+ logMysqlDriverError(err, 500);
86
+ }
87
+ else {
88
+ console.error(err);
89
+ }
90
+ return c.json(INTERNAL_SERVER_ERROR_BODY, 500);
91
+ };
92
+ }
93
+ /**
94
+ * Hono `notFound` handler for unmatched routes.
95
+ *
96
+ * @param c - The Hono request context for the unmatched route.
97
+ * @returns A 404 JSON response: `{ message: 'Cannot METHOD path', error, statusCode }`.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * app.notFound(notFoundHandler);
102
+ * ```
103
+ */
104
+ export function notFoundHandler(c) {
105
+ return c.json({ message: `Cannot ${c.req.method} ${new URL(c.req.url).pathname}`, error: 'Not Found', statusCode: 404 }, 404);
106
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Context, Env } from 'hono';
2
- import type { NestErrorHandlerOptions } from './nest-error.js';
3
- /** Nest QueryFailedExceptionFilter が返す `{ statusCode, message }` 形(error フィールド無し)。 */
2
+ import type { HttpErrorHandlerOptions } from './http-error.js';
3
+ /** DB エラー分類結果: `{ statusCode, message }` 形(error フィールド無し)。 */
4
4
  export interface ClassifiedDbError {
5
5
  statusCode: 400 | 500;
6
6
  message: string;
@@ -8,22 +8,21 @@ export interface ClassifiedDbError {
8
8
  /** mysql2 / Drizzle 由来の DB エラーを HTTP 応答用に分類する。非 DB エラーは null。 */
9
9
  export type QueryFailedClassifier = (err: unknown) => ClassifiedDbError | null;
10
10
  /**
11
- * Default classifier for apps without a NestJS `QueryFailedExceptionFilter` parity layer.
12
- * Maps any mysql2 driver error to generic 500 `{ statusCode, message: 'Internal server error' }`.
11
+ * Default classifier: any mysql2 driver error generic 500 `{ statusCode, message: 'Internal server error' }`.
13
12
  */
14
13
  export declare function classifyGenericMysqlDriverError(err: unknown): ClassifiedDbError | null;
15
- export interface QueryFailedNestErrorHandlerOptions<E extends Env = Env> extends NestErrorHandlerOptions<E> {
16
- /** アプリ固有の分類(parity-critical な日本語メッセージ等は consumer 側で定義)。 */
14
+ export interface QueryFailedErrorHandlerOptions<E extends Env = Env> extends HttpErrorHandlerOptions<E> {
15
+ /** アプリ固有の分類(日本語メッセージ等は consumer 側で定義)。 */
17
16
  classify: QueryFailedClassifier;
18
17
  }
19
18
  /**
20
- * QueryFailedExceptionFilterNest 既定 exception filter の合成 onError。
19
+ * DB エラー分類 標準 HTTP エラーハンドラの合成 onError。
21
20
  *
22
21
  * @remarks
23
- * classify が non-null のときは parity 用 body を返しつつログ(+ 500 は onUnhandledError)を残す。
24
- * 非 DB エラーは {@link createNestErrorHandler} に委譲する。
22
+ * classify が non-null のときは分類結果の body を返しつつログ(+ 500 は onUnhandledError)を残す。
23
+ * 非 DB エラーは {@link createHttpErrorHandler} に委譲する。
25
24
  *
26
25
  * `Sentry.withSentry` だけでは onError 握りエラーは capture されないため、
27
26
  * `onUnhandledError: (err, c) => container.reportError?.(err, { requestId: c.get('requestId') })` を必ず配線する。
28
27
  */
29
- export declare function createQueryFailedNestErrorHandler<E extends Env = Env>(options: QueryFailedNestErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
28
+ export declare function createQueryFailedErrorHandler<E extends Env = Env>(options: QueryFailedErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
@@ -1,8 +1,7 @@
1
+ import { createHttpErrorHandler } from './http-error.js';
1
2
  import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
2
- import { createNestErrorHandler } from './nest-error.js';
3
3
  /**
4
- * Default classifier for apps without a NestJS `QueryFailedExceptionFilter` parity layer.
5
- * Maps any mysql2 driver error to generic 500 `{ statusCode, message: 'Internal server error' }`.
4
+ * Default classifier: any mysql2 driver error generic 500 `{ statusCode, message: 'Internal server error' }`.
6
5
  */
7
6
  export function classifyGenericMysqlDriverError(err) {
8
7
  if (!findMysqlDriverError(err)) {
@@ -11,7 +10,7 @@ export function classifyGenericMysqlDriverError(err) {
11
10
  return { statusCode: 500, message: 'Internal server error' };
12
11
  }
13
12
  /**
14
- * @internal Used by {@link createQueryFailedNestErrorHandler} only.
13
+ * @internal Used by {@link createQueryFailedErrorHandler} only.
15
14
  */
16
15
  function reportClassifiedDbError(err, classified, reportError, requestId) {
17
16
  logMysqlDriverError(err, classified.statusCode);
@@ -20,19 +19,19 @@ function reportClassifiedDbError(err, classified, reportError, requestId) {
20
19
  }
21
20
  }
22
21
  /**
23
- * QueryFailedExceptionFilterNest 既定 exception filter の合成 onError。
22
+ * DB エラー分類 標準 HTTP エラーハンドラの合成 onError。
24
23
  *
25
24
  * @remarks
26
- * classify が non-null のときは parity 用 body を返しつつログ(+ 500 は onUnhandledError)を残す。
27
- * 非 DB エラーは {@link createNestErrorHandler} に委譲する。
25
+ * classify が non-null のときは分類結果の body を返しつつログ(+ 500 は onUnhandledError)を残す。
26
+ * 非 DB エラーは {@link createHttpErrorHandler} に委譲する。
28
27
  *
29
28
  * `Sentry.withSentry` だけでは onError 握りエラーは capture されないため、
30
29
  * `onUnhandledError: (err, c) => container.reportError?.(err, { requestId: c.get('requestId') })` を必ず配線する。
31
30
  */
32
- export function createQueryFailedNestErrorHandler(options) {
33
- const { classify, ...nestOptions } = options;
34
- const nestErrorHandler = createNestErrorHandler(nestOptions);
35
- const { onUnhandledError } = nestOptions;
31
+ export function createQueryFailedErrorHandler(options) {
32
+ const { classify, ...httpOptions } = options;
33
+ const httpErrorHandler = createHttpErrorHandler(httpOptions);
34
+ const { onUnhandledError } = httpOptions;
36
35
  return (err, c) => {
37
36
  const classified = classify(err);
38
37
  if (classified) {
@@ -47,6 +46,6 @@ export function createQueryFailedNestErrorHandler(options) {
47
46
  }
48
47
  return c.json({ statusCode: classified.statusCode, message: classified.message }, classified.statusCode);
49
48
  }
50
- return nestErrorHandler(err, c);
49
+ return httpErrorHandler(err, c);
51
50
  };
52
51
  }
package/dist/index.d.ts CHANGED
@@ -29,20 +29,20 @@ export type { AppInfo } from './http/app-info.js';
29
29
  export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
30
30
  export type { AppEnv } from './http/app-env.js';
31
31
  export { HttpStatus } from './http/http-status.js';
32
- export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
33
- export type { NestErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/nest-error.js';
32
+ export { createHttpErrorHandler, notFoundHandler, HTTP_ERROR_PHRASES } from './http/http-error.js';
33
+ export type { HttpErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/http-error.js';
34
34
  export { findMysqlDriverError, logMysqlDriverError } from './http/mysql-driver-error.js';
35
35
  export type { MysqlDriverErrorLike } from './http/mysql-driver-error.js';
36
- export { createQueryFailedNestErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
37
- export type { ClassifiedDbError, QueryFailedClassifier, QueryFailedNestErrorHandlerOptions, } from './http/query-failed-error.js';
36
+ export { createQueryFailedErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
37
+ export type { ClassifiedDbError, QueryFailedClassifier, QueryFailedErrorHandlerOptions, } from './http/query-failed-error.js';
38
38
  export { createAppErrorHandler } from './http/app-error-handler.js';
39
39
  export type { CreateAppErrorHandlerOptions } from './http/app-error-handler.js';
40
40
  export { normalizeTrailingSlash } from './http/trailing-slash.js';
41
41
  export type { ExecutionContextLike } from './http/execution-context.js';
42
42
  export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
43
43
  export type { DeferExecutor } from './http/defer.js';
44
- export { createSentryErrorReporter } from './http/nest-error.js';
45
- export type { SentryExceptionReporterLike } from './http/nest-error.js';
44
+ export { createSentryErrorReporter } from './http/http-error.js';
45
+ export type { SentryExceptionReporterLike } from './http/http-error.js';
46
46
  export { KVCache } from './cache/kv-cache.js';
47
47
  export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
48
48
  export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
@@ -59,6 +59,8 @@ export type { AiGatewayConfig, AiGatewayProvider, AiGatewayBinding, AiGateway, A
59
59
  export { getAuthenticationSecret } from './aws/secrets-manager.js';
60
60
  export type { AwsSecretsOptions } from './aws/secrets-manager.js';
61
61
  export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
62
+ export { getTemporaryCredentials } from './aws/sts.js';
63
+ export type { GetTemporaryCredentialsOptions, StsCredentials } from './aws/sts.js';
62
64
  export type { DecodedIdToken, FirebaseVerifier } from './firebase/firebase-verifier.js';
63
65
  export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
64
66
  export { IdentityToolkit } from './firebase/identity-toolkit.js';
package/dist/index.js CHANGED
@@ -25,13 +25,13 @@ export { getUserProtocol } from './http/user-protocol.js';
25
25
  export { getAppInfo } from './http/app-info.js';
26
26
  export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
27
27
  export { HttpStatus } from './http/http-status.js';
28
- export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
28
+ export { createHttpErrorHandler, notFoundHandler, HTTP_ERROR_PHRASES } from './http/http-error.js';
29
29
  export { findMysqlDriverError, logMysqlDriverError } from './http/mysql-driver-error.js';
30
- export { createQueryFailedNestErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
30
+ export { createQueryFailedErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
31
31
  export { createAppErrorHandler } from './http/app-error-handler.js';
32
32
  export { normalizeTrailingSlash } from './http/trailing-slash.js';
33
33
  export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
34
- export { createSentryErrorReporter } from './http/nest-error.js';
34
+ export { createSentryErrorReporter } from './http/http-error.js';
35
35
  // cache
36
36
  export { KVCache } from './cache/kv-cache.js';
37
37
  // stripe
@@ -47,6 +47,7 @@ export { createAiGatewayProvider } from './ai/gateway.js';
47
47
  // aws
48
48
  export { getAuthenticationSecret } from './aws/secrets-manager.js';
49
49
  export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
50
+ export { getTemporaryCredentials } from './aws/sts.js';
50
51
  export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
51
52
  export { IdentityToolkit } from './firebase/identity-toolkit.js';
52
53
  export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier.js';
@@ -1,4 +1,4 @@
1
- import type { SentryExceptionReporterLike } from '../http/nest-error.js';
1
+ import type { SentryExceptionReporterLike } from '../http/http-error.js';
2
2
  import type { QueueMessageLike } from './consumer.js';
3
3
  /**
4
4
  * Options for {@link createQueueErrorHandler}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,149 +0,0 @@
1
- import type { Context, Env } from 'hono';
2
- import type { ContentfulStatusCode } from 'hono/utils/http-status';
3
- /**
4
- * Reason phrases attached by the NestJS default exception filter, keyed by HTTP status code.
5
- *
6
- * @remarks
7
- * Mirrors the `error` field values NestJS produces for common client-error statuses, so a Hono app can
8
- * return byte-identical error bodies. Used as the default `reasonPhrases` map by {@link createNestErrorHandler}.
9
- */
10
- export declare const NEST_REASON_PHRASES: Record<number, string>;
11
- /**
12
- * Contextual metadata passed to an {@link ErrorReporter} when reporting an unexpected error.
13
- */
14
- export interface ErrorReportContext {
15
- /** Correlation id for the failing request, if one is tracked. */
16
- requestId?: string;
17
- }
18
- /**
19
- * Signature of a function that reports an unexpected (non-HTTP) error to an external sink such as Sentry.
20
- *
21
- * @remarks
22
- * Wire it into {@link createNestErrorHandler} via `onUnhandledError`, e.g.
23
- * `(err, c) => reporter(err, { requestId: c.get('requestId') })`. The reporting client itself is
24
- * intentionally kept out of this kit; the consumer supplies the implementation.
25
- *
26
- * @param error - The thrown value being reported.
27
- * @param context - Optional correlation context for the failing request.
28
- */
29
- export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
30
- /**
31
- * Minimal Sentry-like client for {@link createSentryErrorReporter} and {@link createQueueErrorHandler}.
32
- *
33
- * @remarks
34
- * Declared structurally to avoid a hard dependency on `@sentry/cloudflare`.
35
- */
36
- export interface SentryExceptionReporterLike {
37
- captureException(exception: unknown, captureContext?: {
38
- tags?: Record<string, string>;
39
- extra?: Record<string, unknown>;
40
- }): void;
41
- }
42
- /**
43
- * Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
44
- */
45
- export declare function createSentryErrorReporter(sentry: SentryExceptionReporterLike): ErrorReporter;
46
- /**
47
- * Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
48
- *
49
- * @internal
50
- */
51
- interface HttpErrorLike {
52
- /** HTTP status code to respond with. */
53
- status: ContentfulStatusCode;
54
- /** Human-readable error message placed in the response body. */
55
- message: string;
56
- /**
57
- * Escape hatch for a fully custom response body. When present, it is rendered verbatim instead of
58
- * the NestJS-shaped body.
59
- */
60
- body?: unknown;
61
- }
62
- /**
63
- * Options controlling how {@link createNestErrorHandler} shapes error responses.
64
- *
65
- * @typeParam E - The Hono environment type, so `onUnhandledError` receives a correctly typed context.
66
- */
67
- export interface NestErrorHandlerOptions<E extends Env = Env> {
68
- /** Status-to-reason-phrase map for the `error` field. Defaults to {@link NEST_REASON_PHRASES}. */
69
- reasonPhrases?: Record<number, string>;
70
- /**
71
- * Statuses that return only `{ statusCode, message }`, omitting the `error` field. Defaults to `[401]`,
72
- * matching NestJS where a generic `HttpException(msg, 401)` carries no `error`.
73
- */
74
- bareStatuses?: readonly number[];
75
- /**
76
- * Field order of the non-bare error body. Defaults to `'statusCode-first'` (the NestJS canonical order).
77
- * Use `'message-first'` to emit `{ message, error, statusCode }` when byte parity requires it.
78
- */
79
- fieldOrder?: 'statusCode-first' | 'message-first';
80
- /**
81
- * Fallback `error` value for statuses that are neither bare nor present in `reasonPhrases`. Defaults to
82
- * `undefined`, meaning the `error` field is omitted when no reason phrase is known. Set to a string such
83
- * as `'Error'` to always include an `error` field, faithfully reproducing the NestJS default exception
84
- * filter behavior where `error` is always present.
85
- */
86
- fallbackReason?: string;
87
- /**
88
- * Predicate identifying which thrown values are HTTP errors. Defaults to detecting Hono's `HTTPException`.
89
- * Override it (e.g. `(e) => e instanceof MyHttpError`) when the app throws a custom HTTP error type.
90
- */
91
- isHttpError?: (err: unknown) => err is HttpErrorLike;
92
- /**
93
- * Hook invoked before an unexpected (non-HTTP) error is returned as a 500, typically used to report the
94
- * error (e.g. to Sentry). Any exception thrown by this hook is swallowed so reporting cannot alter the
95
- * error response.
96
- */
97
- onUnhandledError?: (err: unknown, c: Context<E>) => void;
98
- /**
99
- * Response body for unexpected errors returned as 500. Defaults to
100
- * `{ statusCode: 500, message: 'Internal server error' }`.
101
- */
102
- internalServerErrorBody?: unknown;
103
- }
104
- /**
105
- * Create a Hono `onError` handler that maps thrown errors to NestJS-shaped error JSON.
106
- *
107
- * @remarks
108
- * Reproduces the NestJS default exception filter so a Hono app returns byte-identical error bodies:
109
- * - HTTP errors (by default `HTTPException`) are mapped to a NestJS-shaped body; if the error carries a
110
- * custom `body`, that body is returned verbatim.
111
- * - Statuses listed in `bareStatuses` (default `[401]`) omit the `error` field.
112
- * - Any other (unexpected) error triggers `onUnhandledError`, is logged via `console.error`, and returns 500.
113
- *
114
- * Per-app differences in body field order, HTTP error type, and reporting hook are absorbed through
115
- * {@link NestErrorHandlerOptions}, while the branching logic stays shared.
116
- *
117
- * @typeParam E - The Hono environment type propagated to `onUnhandledError`.
118
- * @param options - Overrides for reason phrases, bare statuses, field order, error detection, and reporting.
119
- * @returns A handler suitable for `app.onError(...)`.
120
- *
121
- * @example
122
- * ```ts
123
- * app.onError(
124
- * createNestErrorHandler({
125
- * fieldOrder: 'message-first',
126
- * fallbackReason: 'Error',
127
- * onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
128
- * }),
129
- * );
130
- * ```
131
- */
132
- export declare function createNestErrorHandler<E extends Env = Env>(options?: NestErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
133
- /**
134
- * Hono `notFound` handler that returns the canonical Express/NestJS unmatched-route 404 body.
135
- *
136
- * @remarks
137
- * Produces `{ message: "Cannot <METHOD> <path>", error: 'Not Found', statusCode: 404 }`, matching the
138
- * NestJS default 404 response so unmatched routes stay byte-identical.
139
- *
140
- * @param c - The Hono request context for the unmatched route.
141
- * @returns A 404 JSON response.
142
- *
143
- * @example
144
- * ```ts
145
- * app.notFound(nestNotFoundHandler);
146
- * ```
147
- */
148
- export declare function nestNotFoundHandler(c: Context): Response;
149
- export {};
@@ -1,120 +0,0 @@
1
- import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
2
- /**
3
- * Reason phrases attached by the NestJS default exception filter, keyed by HTTP status code.
4
- *
5
- * @remarks
6
- * Mirrors the `error` field values NestJS produces for common client-error statuses, so a Hono app can
7
- * return byte-identical error bodies. Used as the default `reasonPhrases` map by {@link createNestErrorHandler}.
8
- */
9
- export const NEST_REASON_PHRASES = {
10
- 400: 'Bad Request',
11
- 401: 'Unauthorized',
12
- 403: 'Forbidden',
13
- 404: 'Not Found',
14
- };
15
- /**
16
- * Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
17
- */
18
- export function createSentryErrorReporter(sentry) {
19
- return (error, context) => {
20
- sentry.captureException(error, context?.requestId ? { tags: { request_id: context.requestId } } : undefined);
21
- };
22
- }
23
- /**
24
- * Structurally detect Hono's `HTTPException` without relying on `instanceof`.
25
- *
26
- * @remarks
27
- * When this kit is symlinked into a consumer, the `hono` instance it resolves can differ from the
28
- * consumer's `hono`, so an `HTTPException` from one copy fails an `instanceof` check against the other.
29
- * Detecting the presence of a `getResponse()` method and a numeric `status` is stable across module
30
- * boundaries and production bundles.
31
- *
32
- * @param err - The thrown value to test.
33
- * @returns `true` when `err` looks like a Hono `HTTPException`.
34
- *
35
- * @internal
36
- */
37
- const isHTTPException = (err) => err instanceof Error &&
38
- typeof err.getResponse === 'function' &&
39
- typeof err.status === 'number';
40
- /**
41
- * Create a Hono `onError` handler that maps thrown errors to NestJS-shaped error JSON.
42
- *
43
- * @remarks
44
- * Reproduces the NestJS default exception filter so a Hono app returns byte-identical error bodies:
45
- * - HTTP errors (by default `HTTPException`) are mapped to a NestJS-shaped body; if the error carries a
46
- * custom `body`, that body is returned verbatim.
47
- * - Statuses listed in `bareStatuses` (default `[401]`) omit the `error` field.
48
- * - Any other (unexpected) error triggers `onUnhandledError`, is logged via `console.error`, and returns 500.
49
- *
50
- * Per-app differences in body field order, HTTP error type, and reporting hook are absorbed through
51
- * {@link NestErrorHandlerOptions}, while the branching logic stays shared.
52
- *
53
- * @typeParam E - The Hono environment type propagated to `onUnhandledError`.
54
- * @param options - Overrides for reason phrases, bare statuses, field order, error detection, and reporting.
55
- * @returns A handler suitable for `app.onError(...)`.
56
- *
57
- * @example
58
- * ```ts
59
- * app.onError(
60
- * createNestErrorHandler({
61
- * fieldOrder: 'message-first',
62
- * fallbackReason: 'Error',
63
- * onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
64
- * }),
65
- * );
66
- * ```
67
- */
68
- export function createNestErrorHandler(options = {}) {
69
- const { reasonPhrases = NEST_REASON_PHRASES, bareStatuses = [401], fieldOrder = 'statusCode-first', isHttpError = isHTTPException, onUnhandledError, internalServerErrorBody = { statusCode: 500, message: 'Internal server error' }, fallbackReason, } = options;
70
- return (err, c) => {
71
- if (isHttpError(err)) {
72
- // Escape hatch for a custom error body: render it verbatim.
73
- if (err.body !== undefined) {
74
- return c.json(err.body, err.status);
75
- }
76
- // reasonPhrases[status] is typed as string, but with noUncheckedIndexedAccess disabled it can be
77
- // undefined at runtime. The fallbackReason fallback for unregistered statuses is intentional.
78
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
79
- const reason = bareStatuses.includes(err.status) ? undefined : (reasonPhrases[err.status] ?? fallbackReason);
80
- if (reason === undefined) {
81
- return c.json({ statusCode: err.status, message: err.message }, err.status);
82
- }
83
- const body = fieldOrder === 'message-first'
84
- ? { message: err.message, error: reason, statusCode: err.status }
85
- : { statusCode: err.status, message: err.message, error: reason };
86
- return c.json(body, err.status);
87
- }
88
- try {
89
- onUnhandledError?.(err, c);
90
- }
91
- catch {
92
- // Reporting must never change the behavior of the error response.
93
- }
94
- if (findMysqlDriverError(err)) {
95
- logMysqlDriverError(err, 500);
96
- }
97
- else {
98
- console.error(err);
99
- }
100
- return c.json(internalServerErrorBody, 500);
101
- };
102
- }
103
- /**
104
- * Hono `notFound` handler that returns the canonical Express/NestJS unmatched-route 404 body.
105
- *
106
- * @remarks
107
- * Produces `{ message: "Cannot <METHOD> <path>", error: 'Not Found', statusCode: 404 }`, matching the
108
- * NestJS default 404 response so unmatched routes stay byte-identical.
109
- *
110
- * @param c - The Hono request context for the unmatched route.
111
- * @returns A 404 JSON response.
112
- *
113
- * @example
114
- * ```ts
115
- * app.notFound(nestNotFoundHandler);
116
- * ```
117
- */
118
- export function nestNotFoundHandler(c) {
119
- return c.json({ message: `Cannot ${c.req.method} ${new URL(c.req.url).pathname}`, error: 'Not Found', statusCode: 404 }, 404);
120
- }