@rdlabo/workers-hono-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/ai/gateway.d.ts +40 -0
- package/dist/ai/gateway.js +36 -0
- package/dist/aws/cloudfront.d.ts +9 -0
- package/dist/aws/cloudfront.js +43 -0
- package/dist/aws/secrets-manager.d.ts +14 -0
- package/dist/aws/secrets-manager.js +43 -0
- package/dist/cache/kv-cache.d.ts +51 -0
- package/dist/cache/kv-cache.js +88 -0
- package/dist/db/connection.d.ts +41 -0
- package/dist/db/connection.js +48 -0
- package/dist/db/database.d.ts +60 -0
- package/dist/db/database.js +63 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +8 -0
- package/dist/db/jst.d.ts +19 -0
- package/dist/db/jst.js +48 -0
- package/dist/db/orm-config.d.ts +62 -0
- package/dist/db/orm-config.js +42 -0
- package/dist/db/retry.d.ts +6 -0
- package/dist/db/retry.js +22 -0
- package/dist/db/write-result.d.ts +16 -0
- package/dist/db/write-result.js +13 -0
- package/dist/firebase/firebase-verifier.d.ts +17 -0
- package/dist/firebase/firebase-verifier.js +1 -0
- package/dist/firebase/identity-toolkit.d.ts +24 -0
- package/dist/firebase/identity-toolkit.js +64 -0
- package/dist/firebase/jose-firebase-verifier.d.ts +32 -0
- package/dist/firebase/jose-firebase-verifier.js +52 -0
- package/dist/firebase/remote-verifier.d.ts +9 -0
- package/dist/firebase/remote-verifier.js +44 -0
- package/dist/http/app-env.d.ts +19 -0
- package/dist/http/app-env.js +16 -0
- package/dist/http/app-info.d.ts +11 -0
- package/dist/http/app-info.js +8 -0
- package/dist/http/http-status.d.ts +62 -0
- package/dist/http/http-status.js +63 -0
- package/dist/http/nest-error.d.ts +72 -0
- package/dist/http/nest-error.js +65 -0
- package/dist/http/user-protocol.d.ts +11 -0
- package/dist/http/user-protocol.js +8 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +28 -0
- package/dist/middleware/auth.d.ts +36 -0
- package/dist/middleware/auth.js +30 -0
- package/dist/middleware/finalize-response.d.ts +2 -0
- package/dist/middleware/finalize-response.js +53 -0
- package/dist/middleware/validation.d.ts +77 -0
- package/dist/middleware/validation.js +53 -0
- package/dist/middleware/zod-coerce.d.ts +9 -0
- package/dist/middleware/zod-coerce.js +50 -0
- package/dist/stripe/client.d.ts +19 -0
- package/dist/stripe/client.js +23 -0
- package/dist/testing/auth.d.ts +36 -0
- package/dist/testing/auth.js +42 -0
- package/dist/testing/configurable-fake.d.ts +14 -0
- package/dist/testing/configurable-fake.js +33 -0
- package/dist/testing/db.d.ts +37 -0
- package/dist/testing/db.js +74 -0
- package/dist/testing/fakes.d.ts +35 -0
- package/dist/testing/fakes.js +56 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/stripe-fixtures.d.ts +13 -0
- package/dist/testing/stripe-fixtures.js +76 -0
- package/package.json +113 -0
- package/scripts/sync-dev-aws.mjs +59 -0
- package/src/ai/gateway.ts +81 -0
- package/src/aws/cloudfront.ts +65 -0
- package/src/aws/secrets-manager.ts +63 -0
- package/src/cache/kv-cache.ts +134 -0
- package/src/db/connection.ts +73 -0
- package/src/db/database.ts +133 -0
- package/src/db/index.ts +27 -0
- package/src/db/jst.ts +56 -0
- package/src/db/orm-config.ts +71 -0
- package/src/db/retry.ts +21 -0
- package/src/db/write-result.ts +23 -0
- package/src/firebase/firebase-verifier.ts +15 -0
- package/src/firebase/identity-toolkit.ts +82 -0
- package/src/firebase/jose-firebase-verifier.ts +71 -0
- package/src/firebase/remote-verifier.ts +49 -0
- package/src/http/app-env.ts +20 -0
- package/src/http/app-info.ts +16 -0
- package/src/http/http-status.ts +62 -0
- package/src/http/nest-error.ts +138 -0
- package/src/http/user-protocol.ts +16 -0
- package/src/index.ts +55 -0
- package/src/middleware/auth.ts +67 -0
- package/src/middleware/finalize-response.ts +61 -0
- package/src/middleware/validation.ts +84 -0
- package/src/middleware/zod-coerce.ts +65 -0
- package/src/stripe/client.ts +48 -0
- package/src/testing/auth.ts +62 -0
- package/src/testing/configurable-fake.ts +33 -0
- package/src/testing/db.ts +125 -0
- package/src/testing/fakes.ts +75 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/stripe-fixtures.ts +85 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
import { IdentityToolkit } from './identity-toolkit';
|
|
3
|
+
import { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './jose-firebase-verifier';
|
|
4
|
+
/**
|
|
5
|
+
* 本番用の便宜ファクトリ。`createRemoteJWKSet` で Google securetoken の公開鍵を取り、
|
|
6
|
+
* `JoseFirebaseVerifier` を返す。トークン検証のみ用途(getUser/deleteUser は不要 = Identity Toolkit 無し)。
|
|
7
|
+
*
|
|
8
|
+
* JWKS は URL 固定なので isolate 内で 1 度だけ生成して共有し(jose が内部メモリにキャッシュ)、
|
|
9
|
+
* verifier は projectId ごとにメモ化する。winecode の旧 `verifyFirebaseIdToken`(module-level JWKS)の
|
|
10
|
+
* キャッシュ挙動を保つための置換。
|
|
11
|
+
*/
|
|
12
|
+
let jwks;
|
|
13
|
+
const verifiers = new Map();
|
|
14
|
+
export function createRemoteFirebaseVerifier(projectId) {
|
|
15
|
+
jwks ??= createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL));
|
|
16
|
+
let verifier = verifiers.get(projectId);
|
|
17
|
+
if (!verifier) {
|
|
18
|
+
verifier = new JoseFirebaseVerifier({ projectId, keyResolver: jwks });
|
|
19
|
+
verifiers.set(projectId, verifier);
|
|
20
|
+
}
|
|
21
|
+
return verifier;
|
|
22
|
+
}
|
|
23
|
+
let saVerifierCache = null;
|
|
24
|
+
/**
|
|
25
|
+
* サービスアカウント JSON から検証器を作る便宜ファクトリ(receptray/tipsys hono の `firebaseFor` 相当)。
|
|
26
|
+
* `getUser`/`deleteUser` のため `IdentityToolkit` を内包する点が `createRemoteFirebaseVerifier` との違い。
|
|
27
|
+
* SA JSON 文字列をキーに isolate 内で 1 つだけキャッシュ(秘密が変わったときだけ再生成)し、
|
|
28
|
+
* JWKS は `createRemoteFirebaseVerifier` と共有する。
|
|
29
|
+
*/
|
|
30
|
+
export function createServiceAccountVerifier(serviceAccountJson) {
|
|
31
|
+
if (saVerifierCache?.key !== serviceAccountJson) {
|
|
32
|
+
jwks ??= createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL));
|
|
33
|
+
const sa = JSON.parse(serviceAccountJson);
|
|
34
|
+
saVerifierCache = {
|
|
35
|
+
key: serviceAccountJson,
|
|
36
|
+
verifier: new JoseFirebaseVerifier({
|
|
37
|
+
projectId: sa.project_id,
|
|
38
|
+
keyResolver: jwks,
|
|
39
|
+
identity: new IdentityToolkit(sa),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return saVerifierCache.verifier;
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AppEnv = 'development' | 'production';
|
|
2
|
+
/**
|
|
3
|
+
* 実行環境(development / production)を解決する。フリート共通の判定。
|
|
4
|
+
*
|
|
5
|
+
* NestJS `/api` は実行時 FS の `.git` 有無で判定し「git が throw → catch → 本番」としていた。
|
|
6
|
+
* Workers は実行時に FS / child_process が無いため同じ手は使えない。代わりに dev シグナルを
|
|
7
|
+
* **コミット済みの起動コマンド**に置く: `wrangler dev --var APP_ENV:development`(`deploy` は無注入)。
|
|
8
|
+
* よって `env.APP_ENV === 'development'` の時だけ development、それ以外(注入無し=本番デプロイ)は
|
|
9
|
+
* production に倒す。これは `/api` の「absence/catch = 本番(安全側)」と同じ意味論。
|
|
10
|
+
*
|
|
11
|
+
* `env` 由来なので fetch / scheduled どちらの文脈でも使え、リクエストヘッダ由来でないため詐称されない。
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveAppEnv(env: {
|
|
14
|
+
APP_ENV?: string;
|
|
15
|
+
} | null | undefined): AppEnv;
|
|
16
|
+
/** production 判定のショートハンド。 */
|
|
17
|
+
export declare const isProductionEnv: (env: {
|
|
18
|
+
APP_ENV?: string;
|
|
19
|
+
} | null | undefined) => boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 実行環境(development / production)を解決する。フリート共通の判定。
|
|
3
|
+
*
|
|
4
|
+
* NestJS `/api` は実行時 FS の `.git` 有無で判定し「git が throw → catch → 本番」としていた。
|
|
5
|
+
* Workers は実行時に FS / child_process が無いため同じ手は使えない。代わりに dev シグナルを
|
|
6
|
+
* **コミット済みの起動コマンド**に置く: `wrangler dev --var APP_ENV:development`(`deploy` は無注入)。
|
|
7
|
+
* よって `env.APP_ENV === 'development'` の時だけ development、それ以外(注入無し=本番デプロイ)は
|
|
8
|
+
* production に倒す。これは `/api` の「absence/catch = 本番(安全側)」と同じ意味論。
|
|
9
|
+
*
|
|
10
|
+
* `env` 由来なので fetch / scheduled どちらの文脈でも使え、リクエストヘッダ由来でないため詐称されない。
|
|
11
|
+
*/
|
|
12
|
+
export function resolveAppEnv(env) {
|
|
13
|
+
return env?.APP_ENV === 'development' ? 'development' : 'production';
|
|
14
|
+
}
|
|
15
|
+
/** production 判定のショートハンド。 */
|
|
16
|
+
export const isProductionEnv = (env) => resolveAppEnv(env) === 'production';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
/** クライアントアプリのメタ情報(NestJS の x-amz-meta-* ヘッダ由来。3 repo 共通)。 */
|
|
3
|
+
export interface AppInfo {
|
|
4
|
+
version: string | null;
|
|
5
|
+
uuid: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `x-amz-meta-version` / `x-amz-meta-uuid` ヘッダから AppInfo を読む。
|
|
9
|
+
* auth middleware が per-request で c.set('appInfo', ...) する値(3 repo で同一仕様)。
|
|
10
|
+
*/
|
|
11
|
+
export declare const getAppInfo: (c: Context) => AppInfo;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x-amz-meta-version` / `x-amz-meta-uuid` ヘッダから AppInfo を読む。
|
|
3
|
+
* auth middleware が per-request で c.set('appInfo', ...) する値(3 repo で同一仕様)。
|
|
4
|
+
*/
|
|
5
|
+
export const getAppInfo = (c) => ({
|
|
6
|
+
version: c.req.header('x-amz-meta-version') ?? null,
|
|
7
|
+
uuid: c.req.header('x-amz-meta-uuid') ?? null,
|
|
8
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS `@nestjs/common` の HttpStatus enum と同一。フリート(NestJS → Hono 移植)で
|
|
3
|
+
* ステータスコードを Nest と同じ名前で参照するための共通定数。`/api` のレスポンス status と
|
|
4
|
+
* バイト一致させる際の単一の参照元にする。
|
|
5
|
+
*/
|
|
6
|
+
export declare enum HttpStatus {
|
|
7
|
+
CONTINUE = 100,
|
|
8
|
+
SWITCHING_PROTOCOLS = 101,
|
|
9
|
+
PROCESSING = 102,
|
|
10
|
+
EARLYHINTS = 103,
|
|
11
|
+
OK = 200,
|
|
12
|
+
CREATED = 201,
|
|
13
|
+
ACCEPTED = 202,
|
|
14
|
+
NON_AUTHORITATIVE_INFORMATION = 203,
|
|
15
|
+
NO_CONTENT = 204,
|
|
16
|
+
RESET_CONTENT = 205,
|
|
17
|
+
PARTIAL_CONTENT = 206,
|
|
18
|
+
MULTI_STATUS = 207,
|
|
19
|
+
ALREADY_REPORTED = 208,
|
|
20
|
+
CONTENT_DIFFERENT = 210,
|
|
21
|
+
AMBIGUOUS = 300,
|
|
22
|
+
MOVED_PERMANENTLY = 301,
|
|
23
|
+
FOUND = 302,
|
|
24
|
+
SEE_OTHER = 303,
|
|
25
|
+
NOT_MODIFIED = 304,
|
|
26
|
+
TEMPORARY_REDIRECT = 307,
|
|
27
|
+
PERMANENT_REDIRECT = 308,
|
|
28
|
+
BAD_REQUEST = 400,
|
|
29
|
+
UNAUTHORIZED = 401,
|
|
30
|
+
PAYMENT_REQUIRED = 402,
|
|
31
|
+
FORBIDDEN = 403,
|
|
32
|
+
NOT_FOUND = 404,
|
|
33
|
+
METHOD_NOT_ALLOWED = 405,
|
|
34
|
+
NOT_ACCEPTABLE = 406,
|
|
35
|
+
PROXY_AUTHENTICATION_REQUIRED = 407,
|
|
36
|
+
REQUEST_TIMEOUT = 408,
|
|
37
|
+
CONFLICT = 409,
|
|
38
|
+
GONE = 410,
|
|
39
|
+
LENGTH_REQUIRED = 411,
|
|
40
|
+
PRECONDITION_FAILED = 412,
|
|
41
|
+
PAYLOAD_TOO_LARGE = 413,
|
|
42
|
+
URI_TOO_LONG = 414,
|
|
43
|
+
UNSUPPORTED_MEDIA_TYPE = 415,
|
|
44
|
+
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
|
|
45
|
+
EXPECTATION_FAILED = 417,
|
|
46
|
+
I_AM_A_TEAPOT = 418,
|
|
47
|
+
MISDIRECTED = 421,
|
|
48
|
+
UNPROCESSABLE_ENTITY = 422,
|
|
49
|
+
LOCKED = 423,
|
|
50
|
+
FAILED_DEPENDENCY = 424,
|
|
51
|
+
PRECONDITION_REQUIRED = 428,
|
|
52
|
+
TOO_MANY_REQUESTS = 429,
|
|
53
|
+
UNRECOVERABLE_ERROR = 456,
|
|
54
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
55
|
+
NOT_IMPLEMENTED = 501,
|
|
56
|
+
BAD_GATEWAY = 502,
|
|
57
|
+
SERVICE_UNAVAILABLE = 503,
|
|
58
|
+
GATEWAY_TIMEOUT = 504,
|
|
59
|
+
HTTP_VERSION_NOT_SUPPORTED = 505,
|
|
60
|
+
INSUFFICIENT_STORAGE = 507,
|
|
61
|
+
LOOP_DETECTED = 508
|
|
62
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS `@nestjs/common` の HttpStatus enum と同一。フリート(NestJS → Hono 移植)で
|
|
3
|
+
* ステータスコードを Nest と同じ名前で参照するための共通定数。`/api` のレスポンス status と
|
|
4
|
+
* バイト一致させる際の単一の参照元にする。
|
|
5
|
+
*/
|
|
6
|
+
export var HttpStatus;
|
|
7
|
+
(function (HttpStatus) {
|
|
8
|
+
HttpStatus[HttpStatus["CONTINUE"] = 100] = "CONTINUE";
|
|
9
|
+
HttpStatus[HttpStatus["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS";
|
|
10
|
+
HttpStatus[HttpStatus["PROCESSING"] = 102] = "PROCESSING";
|
|
11
|
+
HttpStatus[HttpStatus["EARLYHINTS"] = 103] = "EARLYHINTS";
|
|
12
|
+
HttpStatus[HttpStatus["OK"] = 200] = "OK";
|
|
13
|
+
HttpStatus[HttpStatus["CREATED"] = 201] = "CREATED";
|
|
14
|
+
HttpStatus[HttpStatus["ACCEPTED"] = 202] = "ACCEPTED";
|
|
15
|
+
HttpStatus[HttpStatus["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION";
|
|
16
|
+
HttpStatus[HttpStatus["NO_CONTENT"] = 204] = "NO_CONTENT";
|
|
17
|
+
HttpStatus[HttpStatus["RESET_CONTENT"] = 205] = "RESET_CONTENT";
|
|
18
|
+
HttpStatus[HttpStatus["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT";
|
|
19
|
+
HttpStatus[HttpStatus["MULTI_STATUS"] = 207] = "MULTI_STATUS";
|
|
20
|
+
HttpStatus[HttpStatus["ALREADY_REPORTED"] = 208] = "ALREADY_REPORTED";
|
|
21
|
+
HttpStatus[HttpStatus["CONTENT_DIFFERENT"] = 210] = "CONTENT_DIFFERENT";
|
|
22
|
+
HttpStatus[HttpStatus["AMBIGUOUS"] = 300] = "AMBIGUOUS";
|
|
23
|
+
HttpStatus[HttpStatus["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY";
|
|
24
|
+
HttpStatus[HttpStatus["FOUND"] = 302] = "FOUND";
|
|
25
|
+
HttpStatus[HttpStatus["SEE_OTHER"] = 303] = "SEE_OTHER";
|
|
26
|
+
HttpStatus[HttpStatus["NOT_MODIFIED"] = 304] = "NOT_MODIFIED";
|
|
27
|
+
HttpStatus[HttpStatus["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT";
|
|
28
|
+
HttpStatus[HttpStatus["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT";
|
|
29
|
+
HttpStatus[HttpStatus["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
30
|
+
HttpStatus[HttpStatus["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
|
31
|
+
HttpStatus[HttpStatus["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
|
|
32
|
+
HttpStatus[HttpStatus["FORBIDDEN"] = 403] = "FORBIDDEN";
|
|
33
|
+
HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
34
|
+
HttpStatus[HttpStatus["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
|
|
35
|
+
HttpStatus[HttpStatus["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE";
|
|
36
|
+
HttpStatus[HttpStatus["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED";
|
|
37
|
+
HttpStatus[HttpStatus["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
|
|
38
|
+
HttpStatus[HttpStatus["CONFLICT"] = 409] = "CONFLICT";
|
|
39
|
+
HttpStatus[HttpStatus["GONE"] = 410] = "GONE";
|
|
40
|
+
HttpStatus[HttpStatus["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED";
|
|
41
|
+
HttpStatus[HttpStatus["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED";
|
|
42
|
+
HttpStatus[HttpStatus["PAYLOAD_TOO_LARGE"] = 413] = "PAYLOAD_TOO_LARGE";
|
|
43
|
+
HttpStatus[HttpStatus["URI_TOO_LONG"] = 414] = "URI_TOO_LONG";
|
|
44
|
+
HttpStatus[HttpStatus["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
|
|
45
|
+
HttpStatus[HttpStatus["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE";
|
|
46
|
+
HttpStatus[HttpStatus["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED";
|
|
47
|
+
HttpStatus[HttpStatus["I_AM_A_TEAPOT"] = 418] = "I_AM_A_TEAPOT";
|
|
48
|
+
HttpStatus[HttpStatus["MISDIRECTED"] = 421] = "MISDIRECTED";
|
|
49
|
+
HttpStatus[HttpStatus["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
|
|
50
|
+
HttpStatus[HttpStatus["LOCKED"] = 423] = "LOCKED";
|
|
51
|
+
HttpStatus[HttpStatus["FAILED_DEPENDENCY"] = 424] = "FAILED_DEPENDENCY";
|
|
52
|
+
HttpStatus[HttpStatus["PRECONDITION_REQUIRED"] = 428] = "PRECONDITION_REQUIRED";
|
|
53
|
+
HttpStatus[HttpStatus["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
|
|
54
|
+
HttpStatus[HttpStatus["UNRECOVERABLE_ERROR"] = 456] = "UNRECOVERABLE_ERROR";
|
|
55
|
+
HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
|
56
|
+
HttpStatus[HttpStatus["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
|
|
57
|
+
HttpStatus[HttpStatus["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
|
|
58
|
+
HttpStatus[HttpStatus["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
|
|
59
|
+
HttpStatus[HttpStatus["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
|
|
60
|
+
HttpStatus[HttpStatus["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED";
|
|
61
|
+
HttpStatus[HttpStatus["INSUFFICIENT_STORAGE"] = 507] = "INSUFFICIENT_STORAGE";
|
|
62
|
+
HttpStatus[HttpStatus["LOOP_DETECTED"] = 508] = "LOOP_DETECTED";
|
|
63
|
+
})(HttpStatus || (HttpStatus = {}));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Context, Env } from 'hono';
|
|
2
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
|
+
/**
|
|
4
|
+
* NestJS の既定例外フィルタが付ける reason phrase(フリート共通 = receptray/winecode/foodlabel の
|
|
5
|
+
* REASON_PHRASE / DEFAULT_MESSAGES と同一)。3 repo がそれぞれ手書きしていたものを一本化する。
|
|
6
|
+
*/
|
|
7
|
+
export declare const NEST_REASON_PHRASES: Record<number, string>;
|
|
8
|
+
/** 想定外エラーの通報先に渡す文脈(request id 相関など)。フリート共通の最小形。 */
|
|
9
|
+
export interface ErrorReportContext {
|
|
10
|
+
requestId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 想定外エラーの通報関数(Sentry 等)の型。各 repo の container.reportError がこの形。
|
|
14
|
+
* `createNestErrorHandler({ onUnhandledError })` に `(err, c) => reporter(err, { requestId: c.get('requestId') })`
|
|
15
|
+
* の形で差し込む。Sentry 呼び出し自体は各 repo(@sentry/cloudflare は workers-hono-kit に持ち込まない)。
|
|
16
|
+
*/
|
|
17
|
+
export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
|
|
18
|
+
/** http エラーとみなされた値から status / message / body を読むための最小形。 */
|
|
19
|
+
interface HttpErrorLike {
|
|
20
|
+
status: ContentfulStatusCode;
|
|
21
|
+
message: string;
|
|
22
|
+
/** repo 固有 body の脱出口(winecode の HttpError.body 相当)。あればそのまま render する。 */
|
|
23
|
+
body?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface NestErrorHandlerOptions<E extends Env = Env> {
|
|
26
|
+
/** reason phrase map。既定 `NEST_REASON_PHRASES`。 */
|
|
27
|
+
reasonPhrases?: Record<number, string>;
|
|
28
|
+
/**
|
|
29
|
+
* `error` フィールドを省いて `{ statusCode, message }` のみ返す status。既定 `[401]`
|
|
30
|
+
* (NestJS の generic `HttpException(msg, 401)` は `error` を持たない)。
|
|
31
|
+
*/
|
|
32
|
+
bareStatuses?: readonly number[];
|
|
33
|
+
/**
|
|
34
|
+
* 非 bare body のフィールド順序。既定 `'statusCode-first'`(= NestJS canonical / receptray・winecode)。
|
|
35
|
+
* **foodlabel は `'message-first'`** を指定して `{ message, error, statusCode }` の byte-parity を保つ。
|
|
36
|
+
*/
|
|
37
|
+
fieldOrder?: 'statusCode-first' | 'message-first';
|
|
38
|
+
/**
|
|
39
|
+
* reasonPhrases に無い(かつ bare でない)status の `error` フォールバック。既定 `undefined`
|
|
40
|
+
* (= reason 無しなら `error` を省く)。**winecode は `'Error'`** を指定し、全 status で `error` を必ず出す
|
|
41
|
+
* (NestJS 既定例外フィルタの「error は常に存在」を忠実再現)。
|
|
42
|
+
*/
|
|
43
|
+
fallbackReason?: string;
|
|
44
|
+
/**
|
|
45
|
+
* http エラー判定。既定は hono の `HTTPException`。
|
|
46
|
+
* **winecode は独自 `HttpError` を使う**ため `(e) => e instanceof HttpError` を渡す。
|
|
47
|
+
*/
|
|
48
|
+
isHttpError?: (err: unknown) => err is HttpErrorLike;
|
|
49
|
+
/**
|
|
50
|
+
* http エラーでない(= 想定外)エラーを 500 で返す前に呼ぶフック(Sentry 通報など)。
|
|
51
|
+
* **receptray は `container.reportError?.(err, { requestId })`** を差し込む。例外は握り潰す。
|
|
52
|
+
*/
|
|
53
|
+
onUnhandledError?: (err: unknown, c: Context<E>) => void;
|
|
54
|
+
/** 想定外エラー時の 500 body。既定 `{ statusCode: 500, message: 'Internal server error' }`。 */
|
|
55
|
+
internalServerErrorBody?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* NestJS の例外フィルタ相当の Hono `onError` ハンドラを作る(フリート共通)。
|
|
59
|
+
* - http エラー(既定 `HTTPException`)→ Nest 形 body にマップ。`body` を持つ場合はそれを verbatim で返す。
|
|
60
|
+
* - bareStatuses(既定 401)は `error` フィールド無し。
|
|
61
|
+
* - それ以外(想定外エラー)→ `onUnhandledError` 通報 + `console.error` + 500。
|
|
62
|
+
*
|
|
63
|
+
* `app.onError(createNestErrorHandler(...))` の形で使う。各 repo の parity 差異(body 順序・
|
|
64
|
+
* エラー型・通報フック)は options で吸収し、本体の分岐ロジックは共有する。
|
|
65
|
+
*/
|
|
66
|
+
export declare function createNestErrorHandler<E extends Env = Env>(options?: NestErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
|
|
67
|
+
/**
|
|
68
|
+
* Express/Nest 既定の未マッチルート 404 body を返す `notFound` ハンドラ。
|
|
69
|
+
* `app.notFound(nestNotFoundHandler)` で使う(receptray/winecode は未実装の parity ギャップ)。
|
|
70
|
+
*/
|
|
71
|
+
export declare function nestNotFoundHandler(c: Context): Response;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS の既定例外フィルタが付ける reason phrase(フリート共通 = receptray/winecode/foodlabel の
|
|
3
|
+
* REASON_PHRASE / DEFAULT_MESSAGES と同一)。3 repo がそれぞれ手書きしていたものを一本化する。
|
|
4
|
+
*/
|
|
5
|
+
export const NEST_REASON_PHRASES = {
|
|
6
|
+
400: 'Bad Request',
|
|
7
|
+
401: 'Unauthorized',
|
|
8
|
+
403: 'Forbidden',
|
|
9
|
+
404: 'Not Found',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* hono の `HTTPException` を **構造的に**判定する(`instanceof` ではない)。workers-hono-kit は consumer に
|
|
13
|
+
* symlink 同梱されるため、workers-hono-kit が解決する `hono` と consumer の `hono` が別インスタンスになり得る
|
|
14
|
+
* (別コピーの HTTPException は `instanceof` で一致しない)。`getResponse()` と数値 `status` を持つかで
|
|
15
|
+
* 判定すればモジュール境界をまたいでも、prod バンドルでも安定する。
|
|
16
|
+
*/
|
|
17
|
+
const isHTTPException = (err) => err instanceof Error &&
|
|
18
|
+
typeof err.getResponse === 'function' &&
|
|
19
|
+
typeof err.status === 'number';
|
|
20
|
+
/**
|
|
21
|
+
* NestJS の例外フィルタ相当の Hono `onError` ハンドラを作る(フリート共通)。
|
|
22
|
+
* - http エラー(既定 `HTTPException`)→ Nest 形 body にマップ。`body` を持つ場合はそれを verbatim で返す。
|
|
23
|
+
* - bareStatuses(既定 401)は `error` フィールド無し。
|
|
24
|
+
* - それ以外(想定外エラー)→ `onUnhandledError` 通報 + `console.error` + 500。
|
|
25
|
+
*
|
|
26
|
+
* `app.onError(createNestErrorHandler(...))` の形で使う。各 repo の parity 差異(body 順序・
|
|
27
|
+
* エラー型・通報フック)は options で吸収し、本体の分岐ロジックは共有する。
|
|
28
|
+
*/
|
|
29
|
+
export function createNestErrorHandler(options = {}) {
|
|
30
|
+
const { reasonPhrases = NEST_REASON_PHRASES, bareStatuses = [401], fieldOrder = 'statusCode-first', isHttpError = isHTTPException, onUnhandledError, internalServerErrorBody = { statusCode: 500, message: 'Internal server error' }, fallbackReason, } = options;
|
|
31
|
+
return (err, c) => {
|
|
32
|
+
if (isHttpError(err)) {
|
|
33
|
+
// repo 固有 body の脱出口(winecode)。
|
|
34
|
+
if (err.body !== undefined) {
|
|
35
|
+
return c.json(err.body, err.status);
|
|
36
|
+
}
|
|
37
|
+
// reasonPhrases[status] は型上 string だが noUncheckedIndexedAccess 無効のため実際は未定義になり得る。
|
|
38
|
+
// 未登録 status の fallbackReason フォールバックは意図的。
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
40
|
+
const reason = bareStatuses.includes(err.status) ? undefined : (reasonPhrases[err.status] ?? fallbackReason);
|
|
41
|
+
if (reason === undefined) {
|
|
42
|
+
return c.json({ statusCode: err.status, message: err.message }, err.status);
|
|
43
|
+
}
|
|
44
|
+
const body = fieldOrder === 'message-first'
|
|
45
|
+
? { message: err.message, error: reason, statusCode: err.status }
|
|
46
|
+
: { statusCode: err.status, message: err.message, error: reason };
|
|
47
|
+
return c.json(body, err.status);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
onUnhandledError?.(err, c);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// 通報はエラーレスポンスの挙動を変えてはならない。
|
|
54
|
+
}
|
|
55
|
+
console.error(err);
|
|
56
|
+
return c.json(internalServerErrorBody, 500);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Express/Nest 既定の未マッチルート 404 body を返す `notFound` ハンドラ。
|
|
61
|
+
* `app.notFound(nestNotFoundHandler)` で使う(receptray/winecode は未実装の parity ギャップ)。
|
|
62
|
+
*/
|
|
63
|
+
export function nestNotFoundHandler(c) {
|
|
64
|
+
return c.json({ message: `Cannot ${c.req.method} ${new URL(c.req.url).pathname}`, error: 'Not Found', statusCode: 404 }, 404);
|
|
65
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
/** クライアントの IP / UA(NestJS の @UserProtocol デコレータ相当)。 */
|
|
3
|
+
export interface IUserProtocol {
|
|
4
|
+
ipAddress: string | null;
|
|
5
|
+
userAgent: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Hono Context からクライアント IP / UA を取得する。Cloudflare は実 IP を `CF-Connecting-IP` に入れる
|
|
9
|
+
* (`X-Forwarded-For` はフォールバック)。未取得は null(DB の nullable カラムにそのまま入る)。
|
|
10
|
+
*/
|
|
11
|
+
export declare const getUserProtocol: (c: Context) => IUserProtocol;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono Context からクライアント IP / UA を取得する。Cloudflare は実 IP を `CF-Connecting-IP` に入れる
|
|
3
|
+
* (`X-Forwarded-For` はフォールバック)。未取得は null(DB の nullable カラムにそのまま入る)。
|
|
4
|
+
*/
|
|
5
|
+
export const getUserProtocol = (c) => ({
|
|
6
|
+
ipAddress: c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for') ?? null,
|
|
7
|
+
userAgent: c.req.header('user-agent') ?? null,
|
|
8
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { finalizeResponse } from './middleware/finalize-response';
|
|
2
|
+
export { validate, createSentryValidate } from './middleware/validation';
|
|
3
|
+
export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, SentryScopeLike, } from './middleware/validation';
|
|
4
|
+
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce';
|
|
5
|
+
export { createAuthMiddleware } from './middleware/auth';
|
|
6
|
+
export type { AuthMiddlewareOptions } from './middleware/auth';
|
|
7
|
+
export { getUserProtocol } from './http/user-protocol';
|
|
8
|
+
export type { IUserProtocol } from './http/user-protocol';
|
|
9
|
+
export { getAppInfo } from './http/app-info';
|
|
10
|
+
export type { AppInfo } from './http/app-info';
|
|
11
|
+
export { resolveAppEnv, isProductionEnv } from './http/app-env';
|
|
12
|
+
export type { AppEnv } from './http/app-env';
|
|
13
|
+
export { HttpStatus } from './http/http-status';
|
|
14
|
+
export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error';
|
|
15
|
+
export type { NestErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/nest-error';
|
|
16
|
+
export { KVCache } from './cache/kv-cache';
|
|
17
|
+
export type { KVNamespace, KVCacheOptions } from './cache/kv-cache';
|
|
18
|
+
export { createStripeClient, verifyStripeWebhook } from './stripe/client';
|
|
19
|
+
export type { CreateStripeClientOptions } from './stripe/client';
|
|
20
|
+
export { retryWhenDeadlock } from './db/retry';
|
|
21
|
+
export { createAiGatewayProvider } from './ai/gateway';
|
|
22
|
+
export type { AiGatewayConfig, AiGatewayProvider, AiGatewayBinding, AiGateway, AiGatewayOptions } from './ai/gateway';
|
|
23
|
+
export { getAuthenticationSecret } from './aws/secrets-manager';
|
|
24
|
+
export type { AwsSecretsOptions } from './aws/secrets-manager';
|
|
25
|
+
export { getCloudFrontSignedUrl } from './aws/cloudfront';
|
|
26
|
+
export type { DecodedIdToken, FirebaseVerifier } from './firebase/firebase-verifier';
|
|
27
|
+
export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier';
|
|
28
|
+
export { IdentityToolkit } from './firebase/identity-toolkit';
|
|
29
|
+
export type { ServiceAccount } from './firebase/identity-toolkit';
|
|
30
|
+
export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @rdlabo/workers-hono-kit — フリート共通のインフラ層ヘルパ(receptray / winecode / foodlabel)。
|
|
2
|
+
// ドメイン・DB・各 repo 固有の parity 差異(auth エラー status/body、secretId、Secret スキーマ)は
|
|
3
|
+
// 各 repo 側に残し、ここには「設定注入で汎用化できるインフラ」だけを置く。
|
|
4
|
+
// middleware
|
|
5
|
+
export { finalizeResponse } from './middleware/finalize-response';
|
|
6
|
+
export { validate, createSentryValidate } from './middleware/validation';
|
|
7
|
+
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce';
|
|
8
|
+
export { createAuthMiddleware } from './middleware/auth';
|
|
9
|
+
// http
|
|
10
|
+
export { getUserProtocol } from './http/user-protocol';
|
|
11
|
+
export { getAppInfo } from './http/app-info';
|
|
12
|
+
export { resolveAppEnv, isProductionEnv } from './http/app-env';
|
|
13
|
+
export { HttpStatus } from './http/http-status';
|
|
14
|
+
export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error';
|
|
15
|
+
// cache
|
|
16
|
+
export { KVCache } from './cache/kv-cache';
|
|
17
|
+
// stripe
|
|
18
|
+
export { createStripeClient, verifyStripeWebhook } from './stripe/client';
|
|
19
|
+
// db
|
|
20
|
+
export { retryWhenDeadlock } from './db/retry';
|
|
21
|
+
// ai
|
|
22
|
+
export { createAiGatewayProvider } from './ai/gateway';
|
|
23
|
+
// aws
|
|
24
|
+
export { getAuthenticationSecret } from './aws/secrets-manager';
|
|
25
|
+
export { getCloudFrontSignedUrl } from './aws/cloudfront';
|
|
26
|
+
export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier';
|
|
27
|
+
export { IdentityToolkit } from './firebase/identity-toolkit';
|
|
28
|
+
export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
|
+
import type { AppInfo } from '../http/app-info';
|
|
4
|
+
export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
5
|
+
/** ID トークンを載せるヘッダ。既定 `'x-amz-security-token'`(フリート共通)。 */
|
|
6
|
+
tokenHeader?: string;
|
|
7
|
+
/** 生トークンを検証して record/decoded を返す。無効なら throw / reject すること。 */
|
|
8
|
+
verify: (token: string, c: Context<E>) => Promise<Verified>;
|
|
9
|
+
/**
|
|
10
|
+
* DB userId を解決(必要なら新規作成)する。**省略すると token-only**(検証のみ・login 用)になる。
|
|
11
|
+
* create-on-miss(`getUserIdFromFirebase(...).catch(() => createUser(...))`)は repo 側でここに合成する。
|
|
12
|
+
*/
|
|
13
|
+
resolveUserId?: (verified: Verified, c: Context<E>, appInfo: AppInfo) => Promise<Id>;
|
|
14
|
+
/** 検証結果を c.var に載せる。repo 固有の var 名(`decodedToken` / `userRecord` / `userProtocol` 等)を注入する。 */
|
|
15
|
+
setContext: (c: Context<E>, data: {
|
|
16
|
+
verified: Verified;
|
|
17
|
+
appInfo: AppInfo;
|
|
18
|
+
userId?: Id;
|
|
19
|
+
}) => void;
|
|
20
|
+
/**
|
|
21
|
+
* 失敗時の挙動。既定は `throw new HTTPException(failureStatus, { message: failureMessage })`
|
|
22
|
+
* (foodlabel/receptray と同形)。**winecode は `c.json(BODY, n)` を返す**ため上書きする。
|
|
23
|
+
*/
|
|
24
|
+
onFailure?: (err: unknown, c: Context<E>) => Response;
|
|
25
|
+
/** 既定 onFailure の status。既定 `403`(token-only の 401 等は repo が上書き)。 */
|
|
26
|
+
failureStatus?: ContentfulStatusCode;
|
|
27
|
+
/** 既定 onFailure の message。既定 `'Forbidden resource'`。 */
|
|
28
|
+
failureMessage?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* NestJS の AuthGuard / TokenGuard 相当の認証 middleware を作る(フリート共通)。
|
|
32
|
+
* スケルトン(ヘッダ読取 → verify → getAppInfo → resolveUserId → setContext、失敗で console.error +
|
|
33
|
+
* onFailure)を共有し、repo 固有部分(verify / userId 解決 / var 名 / 失敗レスポンス)だけ注入させる。
|
|
34
|
+
* `resolveUserId` を省けば token-only middleware になる。
|
|
35
|
+
*/
|
|
36
|
+
export declare function createAuthMiddleware<E extends Env = Env, Verified = unknown, Id = unknown>(options: AuthMiddlewareOptions<E, Verified, Id>): MiddlewareHandler<E>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HTTPException } from 'hono/http-exception';
|
|
2
|
+
import { getAppInfo } from '../http/app-info';
|
|
3
|
+
/**
|
|
4
|
+
* NestJS の AuthGuard / TokenGuard 相当の認証 middleware を作る(フリート共通)。
|
|
5
|
+
* スケルトン(ヘッダ読取 → verify → getAppInfo → resolveUserId → setContext、失敗で console.error +
|
|
6
|
+
* onFailure)を共有し、repo 固有部分(verify / userId 解決 / var 名 / 失敗レスポンス)だけ注入させる。
|
|
7
|
+
* `resolveUserId` を省けば token-only middleware になる。
|
|
8
|
+
*/
|
|
9
|
+
export function createAuthMiddleware(options) {
|
|
10
|
+
const { tokenHeader = 'x-amz-security-token', verify, resolveUserId, setContext, onFailure, failureStatus = 403, failureMessage = 'Forbidden resource', } = options;
|
|
11
|
+
return async (c, next) => {
|
|
12
|
+
try {
|
|
13
|
+
const token = c.req.header(tokenHeader) ?? '';
|
|
14
|
+
const verified = await verify(token, c);
|
|
15
|
+
const appInfo = getAppInfo(c);
|
|
16
|
+
const userId = resolveUserId ? await resolveUserId(verified, c, appInfo) : undefined;
|
|
17
|
+
setContext(c, { verified, appInfo, userId });
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
// Nest guard が false → ForbiddenException('Forbidden resource')。原因をログし、既定では throw して
|
|
21
|
+
// app.onError に Nest 形 body を描かせる(repo は onFailure で return 形に上書き可能)。
|
|
22
|
+
console.error(e);
|
|
23
|
+
if (onFailure) {
|
|
24
|
+
return onFailure(e, c);
|
|
25
|
+
}
|
|
26
|
+
throw new HTTPException(failureStatus, { message: failureMessage });
|
|
27
|
+
}
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* レスポンス最終化ミドルウェア。Express/Nest(移植元 `../api`)との byte 一致のため 2 点を行う
|
|
3
|
+
* (フリート共通仕様 = receptray/winecode hono と同一):
|
|
4
|
+
*
|
|
5
|
+
* 1. **JSON の charset**: Express の res.json は `application/json; charset=utf-8` を返すが、
|
|
6
|
+
* Hono の c.json は `application/json`(charset 無し)なので合わせる。
|
|
7
|
+
* 2. **weak ETag**: Express/`etag` パッケージ互換の
|
|
8
|
+
* `W/"<byteLength(16進)>-<sha1(body)をbase64して先頭27文字>"`。Express(=Nest) は GET 等のレスポンスに
|
|
9
|
+
* 既定で付与するため、hono/etag の独自形式ではなく Express の算法に厳密一致させる。
|
|
10
|
+
*
|
|
11
|
+
* SSE(text/event-stream)はストリームを buffer できないため両方スキップ。
|
|
12
|
+
*/
|
|
13
|
+
async function weakEtag(body) {
|
|
14
|
+
const digest = await crypto.subtle.digest('SHA-1', body);
|
|
15
|
+
const bytes = new Uint8Array(digest);
|
|
16
|
+
let bin = '';
|
|
17
|
+
for (const b of bytes) {
|
|
18
|
+
bin += String.fromCharCode(b);
|
|
19
|
+
}
|
|
20
|
+
const b64 = btoa(bin).substring(0, 27);
|
|
21
|
+
return `W/"${body.byteLength.toString(16)}-${b64}"`;
|
|
22
|
+
}
|
|
23
|
+
export function finalizeResponse() {
|
|
24
|
+
return async (c, next) => {
|
|
25
|
+
await next();
|
|
26
|
+
const status = c.res.status;
|
|
27
|
+
const contentType = c.res.headers.get('content-type') ?? '';
|
|
28
|
+
// SSE / ストリームは触らない。
|
|
29
|
+
if (contentType.includes('text/event-stream')) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// charset 補正対象(JSON で charset 未指定なら付与)。
|
|
33
|
+
const needsCharset = contentType === 'application/json';
|
|
34
|
+
// ETag 対象(Express は 204/304 では付けない・既存 ETag は尊重)。
|
|
35
|
+
const needsEtag = status !== 204 && status !== 304 && !c.res.headers.has('etag') && !!c.res.body;
|
|
36
|
+
if (!needsCharset && !needsEtag) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const headers = new Headers(c.res.headers);
|
|
40
|
+
if (needsCharset) {
|
|
41
|
+
headers.set('content-type', 'application/json; charset=utf-8');
|
|
42
|
+
}
|
|
43
|
+
if (needsEtag) {
|
|
44
|
+
const buf = await c.res.clone().arrayBuffer();
|
|
45
|
+
headers.set('ETag', await weakEtag(buf));
|
|
46
|
+
c.res = new Response(buf, { status, statusText: c.res.statusText, headers });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// body を読まずヘッダだけ差し替え。
|
|
50
|
+
c.res = new Response(c.res.body, { status, statusText: c.res.statusText, headers });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|