@rdlabo/workers-hono-kit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -11
- package/dist/ai/gateway.d.ts +54 -16
- package/dist/ai/gateway.js +37 -12
- package/dist/aws/cloudfront.d.ts +23 -5
- package/dist/aws/cloudfront.js +45 -6
- package/dist/aws/secrets-manager.d.ts +38 -4
- package/dist/aws/secrets-manager.js +48 -3
- package/dist/cache/kv-cache.d.ts +173 -10
- package/dist/cache/kv-cache.js +139 -7
- package/dist/db/connection.d.ts +56 -14
- package/dist/db/connection.js +39 -13
- package/dist/db/database.d.ts +159 -23
- package/dist/db/database.js +49 -5
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.js +11 -2
- package/dist/db/jst.d.ts +89 -6
- package/dist/db/jst.js +89 -23
- package/dist/db/orm-config.d.ts +61 -19
- package/dist/db/orm-config.js +43 -14
- package/dist/db/retry.d.ts +25 -3
- package/dist/db/retry.js +25 -3
- package/dist/db/write-result.d.ts +27 -4
- package/dist/db/write-result.js +22 -1
- package/dist/firebase/firebase-verifier.d.ts +53 -4
- package/dist/firebase/identity-toolkit.d.ts +54 -5
- package/dist/firebase/identity-toolkit.js +51 -0
- package/dist/firebase/jose-firebase-verifier.d.ts +79 -7
- package/dist/firebase/jose-firebase-verifier.js +68 -7
- package/dist/firebase/remote-verifier.d.ts +42 -4
- package/dist/firebase/remote-verifier.js +58 -9
- package/dist/http/app-env.d.ts +41 -8
- package/dist/http/app-env.js +38 -8
- package/dist/http/app-info.d.ts +25 -3
- package/dist/http/app-info.js +16 -2
- package/dist/http/http-status.d.ts +12 -3
- package/dist/http/http-status.js +12 -3
- package/dist/http/nest-error.d.ts +90 -29
- package/dist/http/nest-error.js +59 -18
- package/dist/http/user-protocol.d.ts +23 -3
- package/dist/http/user-protocol.js +14 -2
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -3
- package/dist/middleware/auth.d.ts +74 -13
- package/dist/middleware/auth.js +30 -6
- package/dist/middleware/finalize-response.d.ts +30 -0
- package/dist/middleware/finalize-response.js +41 -12
- package/dist/middleware/validation.d.ts +83 -9
- package/dist/middleware/validation.js +52 -9
- package/dist/middleware/zod-coerce.d.ts +56 -2
- package/dist/middleware/zod-coerce.js +68 -9
- package/dist/stripe/client.d.ts +46 -9
- package/dist/stripe/client.js +41 -3
- package/dist/testing/auth.d.ts +58 -10
- package/dist/testing/auth.js +58 -10
- package/dist/testing/configurable-fake.d.ts +20 -9
- package/dist/testing/configurable-fake.js +23 -11
- package/dist/testing/db.d.ts +81 -12
- package/dist/testing/db.js +23 -1
- package/dist/testing/fakes.d.ts +77 -9
- package/dist/testing/fakes.js +69 -7
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +10 -5
- package/dist/testing/stripe-fixtures.d.ts +93 -3
- package/dist/testing/stripe-fixtures.js +93 -3
- package/package.json +1 -1
- package/src/ai/gateway.ts +66 -27
- package/src/aws/cloudfront.ts +46 -6
- package/src/aws/secrets-manager.ts +56 -7
- package/src/cache/kv-cache.ts +194 -12
- package/src/db/connection.ts +56 -14
- package/src/db/database.ts +160 -24
- package/src/db/index.ts +11 -2
- package/src/db/jst.ts +89 -23
- package/src/db/orm-config.ts +61 -19
- package/src/db/retry.ts +25 -3
- package/src/db/write-result.ts +27 -4
- package/src/firebase/firebase-verifier.ts +53 -4
- package/src/firebase/identity-toolkit.ts +57 -5
- package/src/firebase/jose-firebase-verifier.ts +79 -9
- package/src/firebase/remote-verifier.ts +58 -9
- package/src/http/app-env.ts +41 -8
- package/src/http/app-info.ts +25 -3
- package/src/http/http-status.ts +12 -3
- package/src/http/nest-error.ts +106 -37
- package/src/http/user-protocol.ts +23 -3
- package/src/index.ts +11 -3
- package/src/middleware/auth.ts +77 -15
- package/src/middleware/finalize-response.ts +41 -12
- package/src/middleware/validation.ts +89 -15
- package/src/middleware/zod-coerce.ts +68 -9
- package/src/stripe/client.ts +46 -9
- package/src/testing/auth.ts +58 -10
- package/src/testing/configurable-fake.ts +23 -11
- package/src/testing/db.ts +82 -13
- package/src/testing/fakes.ts +77 -9
- package/src/testing/index.ts +10 -5
- package/src/testing/stripe-fixtures.ts +93 -3
|
@@ -4,16 +4,43 @@ import type { ServiceAccount } from './identity-toolkit.js';
|
|
|
4
4
|
import { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './jose-firebase-verifier.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* `JoseFirebaseVerifier` を返す。トークン検証のみ用途(getUser/deleteUser は不要 = Identity Toolkit 無し)。
|
|
7
|
+
* Shared remote JWKS for Google's securetoken keys.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The JWKS URL is fixed, so the set is created once per isolate and shared across verifiers
|
|
11
|
+
* (`jose` caches the fetched keys internally). Lazily initialised on first use.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
13
14
|
*/
|
|
14
15
|
let jwks: ReturnType<typeof createRemoteJWKSet> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Per-`projectId` cache of token-only verifiers, memoized for the lifetime of the isolate.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
15
21
|
const verifiers = new Map<string, JoseFirebaseVerifier>();
|
|
16
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Create a token-verification-only Firebase verifier for the given project.
|
|
25
|
+
*
|
|
26
|
+
* Uses `createRemoteJWKSet` to fetch Google's securetoken public keys and returns a
|
|
27
|
+
* {@link JoseFirebaseVerifier}. This factory is for token verification only; it configures no
|
|
28
|
+
* Identity Toolkit client, so `getUser` / `deleteUser` are unavailable.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* The remote JWKS is created once per isolate and shared, and the returned verifier is
|
|
32
|
+
* memoized per `projectId`. This preserves the caching behaviour of a module-level JWKS so
|
|
33
|
+
* repeated calls do not re-fetch keys or allocate new verifiers.
|
|
34
|
+
*
|
|
35
|
+
* @param projectId - The Firebase project id whose tokens will be verified.
|
|
36
|
+
* @returns A verifier that validates ID tokens for `projectId`.
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const verifier = createRemoteFirebaseVerifier('my-firebase-project');
|
|
40
|
+
* const decoded = await verifier.verifyIdToken(idToken);
|
|
41
|
+
* console.log(decoded.uid);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
17
44
|
export function createRemoteFirebaseVerifier(projectId: string): JoseFirebaseVerifier {
|
|
18
45
|
jwks ??= createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL));
|
|
19
46
|
let verifier = verifiers.get(projectId);
|
|
@@ -24,13 +51,35 @@ export function createRemoteFirebaseVerifier(projectId: string): JoseFirebaseVer
|
|
|
24
51
|
return verifier;
|
|
25
52
|
}
|
|
26
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Single-entry cache of the service-account verifier, keyed by the raw service-account JSON.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
27
59
|
let saVerifierCache: { key: string; verifier: JoseFirebaseVerifier } | null = null;
|
|
28
60
|
|
|
29
61
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
62
|
+
* Create a Firebase verifier from a service-account JSON string.
|
|
63
|
+
*
|
|
64
|
+
* Unlike {@link createRemoteFirebaseVerifier}, the returned {@link JoseFirebaseVerifier}
|
|
65
|
+
* embeds an {@link IdentityToolkit} client, enabling `getUser` and `deleteUser` in addition to
|
|
66
|
+
* token verification.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* The verifier is cached for the lifetime of the isolate, keyed by the service-account JSON
|
|
70
|
+
* string, and is only rebuilt when that secret changes. The remote JWKS is shared with
|
|
71
|
+
* {@link createRemoteFirebaseVerifier}.
|
|
72
|
+
*
|
|
73
|
+
* @param serviceAccountJson - The service-account key as a JSON string (parsed into {@link ServiceAccount}).
|
|
74
|
+
* @returns A verifier that validates ID tokens and can look up or delete users.
|
|
75
|
+
* @throws If `serviceAccountJson` is not valid JSON.
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const verifier = createServiceAccountVerifier(env.FIREBASE_SERVICE_ACCOUNT);
|
|
79
|
+
* const decoded = await verifier.verifyIdToken(idToken);
|
|
80
|
+
* const user = await verifier.getUser(decoded.uid);
|
|
81
|
+
* await verifier.deleteUser(decoded.uid);
|
|
82
|
+
* ```
|
|
34
83
|
*/
|
|
35
84
|
export function createServiceAccountVerifier(serviceAccountJson: string): JoseFirebaseVerifier {
|
|
36
85
|
if (saVerifierCache?.key !== serviceAccountJson) {
|
package/src/http/app-env.ts
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolved runtime environment of the Worker.
|
|
3
|
+
*/
|
|
1
4
|
export type AppEnv = 'development' | 'production';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
7
|
+
* Resolve the runtime environment (development or production) from the Worker `env` binding.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Cloudflare Workers have no filesystem or `child_process` at runtime, so the environment cannot be
|
|
11
|
+
* inferred from the presence of a `.git` directory the way a Node/NestJS process might. Instead, the
|
|
12
|
+
* development signal is carried in the committed launch command: `wrangler dev --var APP_ENV:development`
|
|
13
|
+
* injects `APP_ENV`, while `wrangler deploy` injects nothing. Only `env.APP_ENV === 'development'`
|
|
14
|
+
* resolves to `'development'`; every other case (including a missing binding, i.e. a production deploy)
|
|
15
|
+
* resolves to `'production'`. This "absence defaults to production" semantic keeps the safe side as the
|
|
16
|
+
* default.
|
|
17
|
+
*
|
|
18
|
+
* Because it reads from `env` rather than a request header, it works in both `fetch` and `scheduled`
|
|
19
|
+
* contexts and cannot be spoofed by an incoming request.
|
|
5
20
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* **コミット済みの起動コマンド**に置く: `wrangler dev --var APP_ENV:development`(`deploy` は無注入)。
|
|
9
|
-
* よって `env.APP_ENV === 'development'` の時だけ development、それ以外(注入無し=本番デプロイ)は
|
|
10
|
-
* production に倒す。これは `/api` の「absence/catch = 本番(安全側)」と同じ意味論。
|
|
21
|
+
* @param env - The Worker environment binding, or `null`/`undefined` when unavailable.
|
|
22
|
+
* @returns `'development'` only when `env.APP_ENV` is exactly `'development'`; otherwise `'production'`.
|
|
11
23
|
*
|
|
12
|
-
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* export default {
|
|
27
|
+
* fetch(req, env) {
|
|
28
|
+
* if (resolveAppEnv(env) === 'development') {
|
|
29
|
+
* // enable verbose logging
|
|
30
|
+
* }
|
|
31
|
+
* },
|
|
32
|
+
* };
|
|
33
|
+
* ```
|
|
13
34
|
*/
|
|
14
35
|
export function resolveAppEnv(env: { APP_ENV?: string } | null | undefined): AppEnv {
|
|
15
36
|
return env?.APP_ENV === 'development' ? 'development' : 'production';
|
|
16
37
|
}
|
|
17
38
|
|
|
18
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Shorthand for checking whether the resolved environment is production.
|
|
41
|
+
*
|
|
42
|
+
* @param env - The Worker environment binding, or `null`/`undefined` when unavailable.
|
|
43
|
+
* @returns `true` when {@link resolveAppEnv} resolves to `'production'`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* if (isProductionEnv(env)) {
|
|
48
|
+
* // skip dev-only diagnostics
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
19
52
|
export const isProductionEnv = (env: { APP_ENV?: string } | null | undefined): boolean =>
|
|
20
53
|
resolveAppEnv(env) === 'production';
|
package/src/http/app-info.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Client application metadata derived from the `x-amz-meta-*` request headers.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Reproduces the per-request app identity that a NestJS service would expose so a Hono app
|
|
8
|
+
* can read the same client-supplied version/uuid pair without changing the wire contract.
|
|
9
|
+
*/
|
|
4
10
|
export interface AppInfo {
|
|
11
|
+
/** Client application version from `x-amz-meta-version`, or `null` when the header is absent. */
|
|
5
12
|
version: string | null;
|
|
13
|
+
/** Client installation identifier from `x-amz-meta-uuid`, or `null` when the header is absent. */
|
|
6
14
|
uuid: string | null;
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
|
-
* `x-amz-meta-version` / `x-amz-meta-uuid`
|
|
11
|
-
*
|
|
18
|
+
* Read {@link AppInfo} from the `x-amz-meta-version` / `x-amz-meta-uuid` request headers.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Typically called by an auth middleware that stores the result per request via
|
|
22
|
+
* `c.set('appInfo', getAppInfo(c))`. Missing headers resolve to `null` rather than throwing.
|
|
23
|
+
*
|
|
24
|
+
* @param c - The Hono request context to read headers from.
|
|
25
|
+
* @returns The client application metadata for the current request.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* app.use(async (c, next) => {
|
|
30
|
+
* c.set('appInfo', getAppInfo(c));
|
|
31
|
+
* await next();
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
12
34
|
*/
|
|
13
35
|
export const getAppInfo = (c: Context): AppInfo => ({
|
|
14
36
|
version: c.req.header('x-amz-meta-version') ?? null,
|
package/src/http/http-status.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* HTTP status codes mirroring the `HttpStatus` enum from `@nestjs/common`.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Provides a single source of truth for referencing status codes by the same names NestJS uses, so a
|
|
6
|
+
* Hono app can emit responses whose status matches a NestJS service byte-for-byte. The member set and
|
|
7
|
+
* numeric values intentionally track `@nestjs/common` rather than the IANA registry, including a few
|
|
8
|
+
* non-standard codes that NestJS ships.
|
|
5
9
|
*/
|
|
6
10
|
export enum HttpStatus {
|
|
7
11
|
CONTINUE = 100,
|
|
@@ -17,7 +21,9 @@ export enum HttpStatus {
|
|
|
17
21
|
PARTIAL_CONTENT = 206,
|
|
18
22
|
MULTI_STATUS = 207,
|
|
19
23
|
ALREADY_REPORTED = 208,
|
|
24
|
+
/** Non-standard WebDAV extension carried over from NestJS. */
|
|
20
25
|
CONTENT_DIFFERENT = 210,
|
|
26
|
+
/** Multiple Choices (300). Named `AMBIGUOUS` to match NestJS. */
|
|
21
27
|
AMBIGUOUS = 300,
|
|
22
28
|
MOVED_PERMANENTLY = 301,
|
|
23
29
|
FOUND = 302,
|
|
@@ -43,13 +49,16 @@ export enum HttpStatus {
|
|
|
43
49
|
UNSUPPORTED_MEDIA_TYPE = 415,
|
|
44
50
|
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
|
|
45
51
|
EXPECTATION_FAILED = 417,
|
|
52
|
+
/** "I'm a teapot" (418), from RFC 2324. */
|
|
46
53
|
I_AM_A_TEAPOT = 418,
|
|
54
|
+
/** Misdirected Request (421). Named `MISDIRECTED` to match NestJS. */
|
|
47
55
|
MISDIRECTED = 421,
|
|
48
56
|
UNPROCESSABLE_ENTITY = 422,
|
|
49
57
|
LOCKED = 423,
|
|
50
58
|
FAILED_DEPENDENCY = 424,
|
|
51
59
|
PRECONDITION_REQUIRED = 428,
|
|
52
60
|
TOO_MANY_REQUESTS = 429,
|
|
61
|
+
/** Non-standard code carried over from NestJS. */
|
|
53
62
|
UNRECOVERABLE_ERROR = 456,
|
|
54
63
|
INTERNAL_SERVER_ERROR = 500,
|
|
55
64
|
NOT_IMPLEMENTED = 501,
|
package/src/http/nest-error.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { Context, Env } from 'hono';
|
|
|
2
2
|
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* NestJS
|
|
6
|
-
*
|
|
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}.
|
|
7
10
|
*/
|
|
8
11
|
export const NEST_REASON_PHRASES: Record<number, string> = {
|
|
9
12
|
400: 'Bad Request',
|
|
@@ -12,64 +15,100 @@ export const NEST_REASON_PHRASES: Record<number, string> = {
|
|
|
12
15
|
404: 'Not Found',
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Contextual metadata passed to an {@link ErrorReporter} when reporting an unexpected error.
|
|
20
|
+
*/
|
|
16
21
|
export interface ErrorReportContext {
|
|
22
|
+
/** Correlation id for the failing request, if one is tracked. */
|
|
17
23
|
requestId?: string;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.
|
|
24
36
|
*/
|
|
25
37
|
export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
|
|
26
38
|
|
|
27
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
28
44
|
interface HttpErrorLike {
|
|
45
|
+
/** HTTP status code to respond with. */
|
|
29
46
|
status: ContentfulStatusCode;
|
|
47
|
+
/** Human-readable error message placed in the response body. */
|
|
30
48
|
message: string;
|
|
31
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Escape hatch for a fully custom response body. When present, it is rendered verbatim instead of
|
|
51
|
+
* the NestJS-shaped body.
|
|
52
|
+
*/
|
|
32
53
|
body?: unknown;
|
|
33
54
|
}
|
|
34
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
|
+
*/
|
|
35
61
|
export interface NestErrorHandlerOptions<E extends Env = Env> {
|
|
36
|
-
/** reason
|
|
62
|
+
/** Status-to-reason-phrase map for the `error` field. Defaults to {@link NEST_REASON_PHRASES}. */
|
|
37
63
|
reasonPhrases?: Record<number, string>;
|
|
38
64
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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`.
|
|
41
67
|
*/
|
|
42
68
|
bareStatuses?: readonly number[];
|
|
43
69
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
46
72
|
*/
|
|
47
73
|
fieldOrder?: 'statusCode-first' | 'message-first';
|
|
48
74
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
52
79
|
*/
|
|
53
80
|
fallbackReason?: string;
|
|
54
81
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
84
|
*/
|
|
58
85
|
isHttpError?: (err: unknown) => err is HttpErrorLike;
|
|
59
86
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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.
|
|
62
90
|
*/
|
|
63
91
|
onUnhandledError?: (err: unknown, c: Context<E>) => void;
|
|
64
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Response body for unexpected errors returned as 500. Defaults to
|
|
94
|
+
* `{ statusCode: 500, message: 'Internal server error' }`.
|
|
95
|
+
*/
|
|
65
96
|
internalServerErrorBody?: unknown;
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
|
73
112
|
*/
|
|
74
113
|
const isHTTPException = (err: unknown): err is HttpErrorLike =>
|
|
75
114
|
err instanceof Error &&
|
|
@@ -77,13 +116,32 @@ const isHTTPException = (err: unknown): err is HttpErrorLike =>
|
|
|
77
116
|
typeof (err as { status?: unknown }).status === 'number';
|
|
78
117
|
|
|
79
118
|
/**
|
|
80
|
-
*
|
|
81
|
-
* - http エラー(既定 `HTTPException`)→ Nest 形 body にマップ。`body` を持つ場合はそれを verbatim で返す。
|
|
82
|
-
* - bareStatuses(既定 401)は `error` フィールド無し。
|
|
83
|
-
* - それ以外(想定外エラー)→ `onUnhandledError` 通報 + `console.error` + 500。
|
|
119
|
+
* Create a Hono `onError` handler that maps thrown errors to NestJS-shaped error JSON.
|
|
84
120
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
+
* ```
|
|
87
145
|
*/
|
|
88
146
|
export function createNestErrorHandler<E extends Env = Env>(options: NestErrorHandlerOptions<E> = {}) {
|
|
89
147
|
const {
|
|
@@ -98,12 +156,12 @@ export function createNestErrorHandler<E extends Env = Env>(options: NestErrorHa
|
|
|
98
156
|
|
|
99
157
|
return (err: Error, c: Context<E>): Response => {
|
|
100
158
|
if (isHttpError(err)) {
|
|
101
|
-
//
|
|
159
|
+
// Escape hatch for a custom error body: render it verbatim.
|
|
102
160
|
if (err.body !== undefined) {
|
|
103
161
|
return c.json(err.body as object, err.status);
|
|
104
162
|
}
|
|
105
|
-
// reasonPhrases[status]
|
|
106
|
-
//
|
|
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.
|
|
107
165
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
108
166
|
const reason = bareStatuses.includes(err.status) ? undefined : (reasonPhrases[err.status] ?? fallbackReason);
|
|
109
167
|
if (reason === undefined) {
|
|
@@ -119,7 +177,7 @@ export function createNestErrorHandler<E extends Env = Env>(options: NestErrorHa
|
|
|
119
177
|
try {
|
|
120
178
|
onUnhandledError?.(err, c);
|
|
121
179
|
} catch {
|
|
122
|
-
//
|
|
180
|
+
// Reporting must never change the behavior of the error response.
|
|
123
181
|
}
|
|
124
182
|
console.error(err);
|
|
125
183
|
return c.json(internalServerErrorBody as object, 500);
|
|
@@ -127,8 +185,19 @@ export function createNestErrorHandler<E extends Env = Env>(options: NestErrorHa
|
|
|
127
185
|
}
|
|
128
186
|
|
|
129
187
|
/**
|
|
130
|
-
* Express/
|
|
131
|
-
*
|
|
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
|
+
* ```
|
|
132
201
|
*/
|
|
133
202
|
export function nestNotFoundHandler(c: Context): Response {
|
|
134
203
|
return c.json(
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
2
|
|
|
3
|
-
/**
|
|
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
|
+
*/
|
|
4
10
|
export interface IUserProtocol {
|
|
11
|
+
/** Client IP address, or `null` when no source header is present. */
|
|
5
12
|
ipAddress: string | null;
|
|
13
|
+
/** Client user-agent string, or `null` when the `User-Agent` header is absent. */
|
|
6
14
|
userAgent: string | null;
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
+
* ```
|
|
12
32
|
*/
|
|
13
33
|
export const getUserProtocol = (c: Context): IUserProtocol => ({
|
|
14
34
|
ipAddress: c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for') ?? null,
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
*/
|
|
4
12
|
|
|
5
13
|
// middleware
|
|
6
14
|
export { finalizeResponse } from './middleware/finalize-response.js';
|
package/src/middleware/auth.ts
CHANGED
|
@@ -4,34 +4,95 @@ import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
|
4
4
|
import { getAppInfo } from '../http/app-info.js';
|
|
5
5
|
import type { AppInfo } from '../http/app-info.js';
|
|
6
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
|
+
*/
|
|
7
14
|
export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
8
|
-
/** ID
|
|
15
|
+
/** Header carrying the ID token. Defaults to `'x-amz-security-token'`. */
|
|
9
16
|
tokenHeader?: string;
|
|
10
|
-
/**
|
|
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
|
+
*/
|
|
11
25
|
verify: (token: string, c: Context<E>) => Promise<Verified>;
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
15
38
|
*/
|
|
16
39
|
resolveUserId?: (verified: Verified, c: Context<E>, appInfo: AppInfo) => Promise<Id>;
|
|
17
|
-
/**
|
|
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
|
+
*/
|
|
18
49
|
setContext: (c: Context<E>, data: { verified: Verified; appInfo: AppInfo; userId?: Id }) => void;
|
|
19
50
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
60
|
*/
|
|
23
61
|
onFailure?: (err: unknown, c: Context<E>) => Response;
|
|
24
|
-
/**
|
|
62
|
+
/** Status used by the default `onFailure`. Defaults to `403`. */
|
|
25
63
|
failureStatus?: ContentfulStatusCode;
|
|
26
|
-
/**
|
|
64
|
+
/** Message used by the default `onFailure`. Defaults to `'Forbidden resource'`. */
|
|
27
65
|
failureMessage?: string;
|
|
28
66
|
}
|
|
29
67
|
|
|
30
68
|
/**
|
|
31
|
-
* NestJS
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `resolveUserId`
|
|
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
|
+
* ```
|
|
35
96
|
*/
|
|
36
97
|
export function createAuthMiddleware<E extends Env = Env, Verified = unknown, Id = unknown>(
|
|
37
98
|
options: AuthMiddlewareOptions<E, Verified, Id>,
|
|
@@ -54,8 +115,9 @@ export function createAuthMiddleware<E extends Env = Env, Verified = unknown, Id
|
|
|
54
115
|
const userId = resolveUserId ? await resolveUserId(verified, c, appInfo) : undefined;
|
|
55
116
|
setContext(c, { verified, appInfo, userId });
|
|
56
117
|
} catch (e) {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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).
|
|
59
121
|
console.error(e);
|
|
60
122
|
if (onFailure) {
|
|
61
123
|
return onFailure(e, c);
|