@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.
Files changed (97) hide show
  1. package/README.md +126 -11
  2. package/dist/ai/gateway.d.ts +54 -16
  3. package/dist/ai/gateway.js +37 -12
  4. package/dist/aws/cloudfront.d.ts +23 -5
  5. package/dist/aws/cloudfront.js +45 -6
  6. package/dist/aws/secrets-manager.d.ts +38 -4
  7. package/dist/aws/secrets-manager.js +48 -3
  8. package/dist/cache/kv-cache.d.ts +173 -10
  9. package/dist/cache/kv-cache.js +139 -7
  10. package/dist/db/connection.d.ts +56 -14
  11. package/dist/db/connection.js +39 -13
  12. package/dist/db/database.d.ts +159 -23
  13. package/dist/db/database.js +49 -5
  14. package/dist/db/index.d.ts +11 -0
  15. package/dist/db/index.js +11 -2
  16. package/dist/db/jst.d.ts +89 -6
  17. package/dist/db/jst.js +89 -23
  18. package/dist/db/orm-config.d.ts +61 -19
  19. package/dist/db/orm-config.js +43 -14
  20. package/dist/db/retry.d.ts +25 -3
  21. package/dist/db/retry.js +25 -3
  22. package/dist/db/write-result.d.ts +27 -4
  23. package/dist/db/write-result.js +22 -1
  24. package/dist/firebase/firebase-verifier.d.ts +53 -4
  25. package/dist/firebase/identity-toolkit.d.ts +54 -5
  26. package/dist/firebase/identity-toolkit.js +51 -0
  27. package/dist/firebase/jose-firebase-verifier.d.ts +79 -7
  28. package/dist/firebase/jose-firebase-verifier.js +68 -7
  29. package/dist/firebase/remote-verifier.d.ts +42 -4
  30. package/dist/firebase/remote-verifier.js +58 -9
  31. package/dist/http/app-env.d.ts +41 -8
  32. package/dist/http/app-env.js +38 -8
  33. package/dist/http/app-info.d.ts +25 -3
  34. package/dist/http/app-info.js +16 -2
  35. package/dist/http/http-status.d.ts +12 -3
  36. package/dist/http/http-status.js +12 -3
  37. package/dist/http/nest-error.d.ts +90 -29
  38. package/dist/http/nest-error.js +59 -18
  39. package/dist/http/user-protocol.d.ts +23 -3
  40. package/dist/http/user-protocol.js +14 -2
  41. package/dist/index.d.ts +11 -0
  42. package/dist/index.js +11 -3
  43. package/dist/middleware/auth.d.ts +74 -13
  44. package/dist/middleware/auth.js +30 -6
  45. package/dist/middleware/finalize-response.d.ts +30 -0
  46. package/dist/middleware/finalize-response.js +41 -12
  47. package/dist/middleware/validation.d.ts +83 -9
  48. package/dist/middleware/validation.js +52 -9
  49. package/dist/middleware/zod-coerce.d.ts +56 -2
  50. package/dist/middleware/zod-coerce.js +68 -9
  51. package/dist/stripe/client.d.ts +46 -9
  52. package/dist/stripe/client.js +41 -3
  53. package/dist/testing/auth.d.ts +58 -10
  54. package/dist/testing/auth.js +58 -10
  55. package/dist/testing/configurable-fake.d.ts +20 -9
  56. package/dist/testing/configurable-fake.js +23 -11
  57. package/dist/testing/db.d.ts +81 -12
  58. package/dist/testing/db.js +23 -1
  59. package/dist/testing/fakes.d.ts +77 -9
  60. package/dist/testing/fakes.js +69 -7
  61. package/dist/testing/index.d.ts +7 -0
  62. package/dist/testing/index.js +10 -5
  63. package/dist/testing/stripe-fixtures.d.ts +93 -3
  64. package/dist/testing/stripe-fixtures.js +93 -3
  65. package/package.json +1 -1
  66. package/src/ai/gateway.ts +66 -27
  67. package/src/aws/cloudfront.ts +46 -6
  68. package/src/aws/secrets-manager.ts +56 -7
  69. package/src/cache/kv-cache.ts +194 -12
  70. package/src/db/connection.ts +56 -14
  71. package/src/db/database.ts +160 -24
  72. package/src/db/index.ts +11 -2
  73. package/src/db/jst.ts +89 -23
  74. package/src/db/orm-config.ts +61 -19
  75. package/src/db/retry.ts +25 -3
  76. package/src/db/write-result.ts +27 -4
  77. package/src/firebase/firebase-verifier.ts +53 -4
  78. package/src/firebase/identity-toolkit.ts +57 -5
  79. package/src/firebase/jose-firebase-verifier.ts +79 -9
  80. package/src/firebase/remote-verifier.ts +58 -9
  81. package/src/http/app-env.ts +41 -8
  82. package/src/http/app-info.ts +25 -3
  83. package/src/http/http-status.ts +12 -3
  84. package/src/http/nest-error.ts +106 -37
  85. package/src/http/user-protocol.ts +23 -3
  86. package/src/index.ts +11 -3
  87. package/src/middleware/auth.ts +77 -15
  88. package/src/middleware/finalize-response.ts +41 -12
  89. package/src/middleware/validation.ts +89 -15
  90. package/src/middleware/zod-coerce.ts +68 -9
  91. package/src/stripe/client.ts +46 -9
  92. package/src/testing/auth.ts +58 -10
  93. package/src/testing/configurable-fake.ts +23 -11
  94. package/src/testing/db.ts +82 -13
  95. package/src/testing/fakes.ts +77 -9
  96. package/src/testing/index.ts +10 -5
  97. package/src/testing/stripe-fixtures.ts +93 -3
@@ -1,16 +1,15 @@
1
1
  import type { MiddlewareHandler } from 'hono';
2
2
 
3
3
  /**
4
- * レスポンス最終化ミドルウェア。Express/Nest(移植元 `../api`)との byte 一致のため 2 点を行う
5
- * (フリート共通仕様 = receptray/winecode hono と同一):
4
+ * Compute an Express `etag`-package compatible weak ETag for a response body.
6
5
  *
7
- * 1. **JSON charset**: Express res.json `application/json; charset=utf-8` を返すが、
8
- * Hono c.json `application/json`(charset 無し)なので合わせる。
9
- * 2. **weak ETag**: Express/`etag` パッケージ互換の
10
- * `W/"<byteLength(16進)>-<sha1(body)をbase64して先頭27文字>"`。Express(=Nest) は GET 等のレスポンスに
11
- * 既定で付与するため、hono/etag の独自形式ではなく Express の算法に厳密一致させる。
6
+ * The format is `W/"<byteLength-in-hex>-<first 27 chars of base64(sha1(body))>"`, byte-for-byte
7
+ * identical to the weak ETag produced by the Express `etag` package. This deliberately differs
8
+ * from `hono/etag`'s own format so responses match an Express/Nest backend exactly.
12
9
  *
13
- * SSE(text/event-stream)はストリームを buffer できないため両方スキップ。
10
+ * @param body - The raw response body bytes to hash.
11
+ * @returns The weak ETag header value (e.g. `W/"1a-Qwerty..."`).
12
+ * @internal
14
13
  */
15
14
  async function weakEtag(body: ArrayBuffer): Promise<string> {
16
15
  const digest = await crypto.subtle.digest('SHA-1', body);
@@ -23,6 +22,36 @@ async function weakEtag(body: ArrayBuffer): Promise<string> {
23
22
  return `W/"${body.byteLength.toString(16)}-${b64}"`;
24
23
  }
25
24
 
25
+ /**
26
+ * Create a Hono middleware that finalizes responses for byte-parity with an Express/Nest backend.
27
+ *
28
+ * After the downstream handler runs, it performs two adjustments:
29
+ *
30
+ * 1. **JSON charset**: Express's `res.json` emits `application/json; charset=utf-8`, whereas Hono's
31
+ * `c.json` emits a bare `application/json`. A bare `application/json` content type is rewritten
32
+ * to include `; charset=utf-8`.
33
+ * 2. **Weak ETag**: An Express `etag`-package compatible weak ETag is added, matching the format an
34
+ * Express/Nest backend applies to responses by default. See {@link weakEtag} for the exact format.
35
+ *
36
+ * Server-Sent Events (`text/event-stream`) are skipped entirely because the stream cannot be
37
+ * buffered. ETag generation is also skipped for `204`/`304` responses, responses that already carry
38
+ * an `etag` header, and responses without a body.
39
+ *
40
+ * @returns A {@link MiddlewareHandler} that rewrites the response headers (and body, when an ETag
41
+ * must be computed) in place.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { Hono } from 'hono';
46
+ * import { finalizeResponse } from '@rdlabo/workers-hono-kit';
47
+ *
48
+ * const app = new Hono();
49
+ * app.use('*', finalizeResponse());
50
+ * app.get('/users', (c) => c.json({ ok: true }));
51
+ * // → Content-Type: application/json; charset=utf-8
52
+ * // → ETag: W/"b-..." (b = 0xb = 11 bytes, the length of `{"ok":true}`)
53
+ * ```
54
+ */
26
55
  export function finalizeResponse(): MiddlewareHandler {
27
56
  return async (c, next) => {
28
57
  await next();
@@ -30,14 +59,14 @@ export function finalizeResponse(): MiddlewareHandler {
30
59
  const status = c.res.status;
31
60
  const contentType = c.res.headers.get('content-type') ?? '';
32
61
 
33
- // SSE / ストリームは触らない。
62
+ // Leave SSE / streaming responses untouched.
34
63
  if (contentType.includes('text/event-stream')) {
35
64
  return;
36
65
  }
37
66
 
38
- // charset 補正対象(JSON charset 未指定なら付与)。
67
+ // Charset target: add the charset when a JSON response leaves it unspecified.
39
68
  const needsCharset = contentType === 'application/json';
40
- // ETag 対象(Express 204/304 では付けない・既存 ETag は尊重)。
69
+ // ETag target: Express omits it on 204/304 and respects an existing ETag.
41
70
  const needsEtag = status !== 204 && status !== 304 && !c.res.headers.has('etag') && !!c.res.body;
42
71
 
43
72
  if (!needsCharset && !needsEtag) {
@@ -54,7 +83,7 @@ export function finalizeResponse(): MiddlewareHandler {
54
83
  headers.set('ETag', await weakEtag(buf));
55
84
  c.res = new Response(buf, { status, statusText: c.res.statusText, headers });
56
85
  } else {
57
- // body を読まずヘッダだけ差し替え。
86
+ // Swap only the headers without reading the body.
58
87
  c.res = new Response(c.res.body, { status, statusText: c.res.statusText, headers });
59
88
  }
60
89
  };
@@ -2,31 +2,45 @@ import { zValidator } from '@hono/zod-validator';
2
2
  import type { Context } from 'hono';
3
3
  import type { ZodType } from 'zod';
4
4
 
5
- /** zod v3(ZodError) / v4(core $ZodError) どちらの error でも受けられる最小形 */
5
+ /**
6
+ * Minimal structural shape that accepts either a zod v3 `ZodError` or a zod v4 core `$ZodError`.
7
+ *
8
+ * @remarks
9
+ * Declaring only the `issues` array (with `path` and `message`) keeps the kit independent of a
10
+ * specific zod major version while still exposing enough to format human-readable messages.
11
+ */
6
12
  export interface ZodErrorLike {
13
+ /** The validation issues reported by zod, each with a property path and a message. */
7
14
  issues: readonly { path: PropertyKey[]; message: string }[];
8
15
  }
9
16
 
17
+ /** Request part that a validator inspects, mirroring the targets supported by `@hono/zod-validator`. */
10
18
  export type ValidationTarget = 'json' | 'query' | 'param' | 'header' | 'cookie' | 'form';
11
19
 
20
+ /** Options controlling the optional side effects of {@link validate}. */
12
21
  export interface ValidateOptions {
13
22
  /**
14
- * 検証失敗時のフック(Sentry 通報など)。**検証の挙動は変えない** レスポンスは常に
15
- * NestJS ValidationPipe 同形の 400 を返す。例外を投げても握り潰す。
16
- * 既定は no-op(receptray 互換 = 4xx を通報しない)。foodlabel は Sentry 実通報を差し込む。
23
+ * Hook invoked when validation fails (e.g. to report to Sentry).
24
+ *
25
+ * @remarks
26
+ * This never changes validation behavior — the response is always a NestJS `ValidationPipe`-shaped
27
+ * 400. Exceptions thrown by the hook are swallowed. The default is a no-op (4xx errors are not
28
+ * reported); pass a hook to forward failures to an error tracker.
29
+ *
30
+ * @param error - The zod error describing the failed validation.
31
+ * @param c - The Hono context for the failing request.
17
32
  */
18
33
  onValidationError?: (error: ZodErrorLike, c: Context) => void;
19
34
  }
20
35
 
21
36
  /**
22
- * Zod 検証ミドルウェア(フリート共通 = receptray/winecode hono と同一仕様)。
23
- * 失敗時は NestJS の ValidationPipe と同形の body を返す:
24
- * { statusCode: 400, message: string[], error: 'Bad Request' }
37
+ * Convert a {@link ZodErrorLike} into NestJS `ValidationPipe`-style message strings.
25
38
  *
26
- * NOTE(parity): message の文字列内容は class-validator Zod で異なる。各 repo の固定 app の
27
- * 正常系では DTO 検証 400 は発生しない前提(ビジネス 400 は各エンドポイントで HttpError 忠実再現)。
39
+ * Each issue becomes `"<dotted.path>: <message>"`, or just `"<message>"` when the path is empty.
40
+ *
41
+ * @param error - The zod error to flatten.
42
+ * @returns One message string per issue.
28
43
  */
29
-
30
44
  function zodToMessages(error: ZodErrorLike): string[] {
31
45
  return error.issues.map((issue) => {
32
46
  const path = issue.path.map(String).join('.');
@@ -34,6 +48,35 @@ function zodToMessages(error: ZodErrorLike): string[] {
34
48
  });
35
49
  }
36
50
 
51
+ /**
52
+ * Create a zod validation middleware that returns a NestJS `ValidationPipe`-shaped 400 on failure.
53
+ *
54
+ * On success the parsed value is made available through `@hono/zod-validator` as usual. On failure
55
+ * the response body is `{ statusCode: 400, message: string[], error: 'Bad Request' }`, the failing
56
+ * field paths are logged via `console.warn` (so 400s are visible in `wrangler dev`/Workers logs),
57
+ * and {@link ValidateOptions.onValidationError} is invoked if provided.
58
+ *
59
+ * @remarks
60
+ * The exact message strings differ from class-validator because they are produced by zod, but the
61
+ * envelope shape matches NestJS so clients can parse failures identically.
62
+ *
63
+ * @typeParam T - The type produced by the zod schema.
64
+ * @param target - The request part to validate.
65
+ * @param schema - The zod schema to validate the target against.
66
+ * @param options - Optional hooks; see {@link ValidateOptions}.
67
+ * @returns A Hono middleware that validates `target` and short-circuits with a 400 on failure.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { z } from 'zod';
72
+ * import { validate } from '@rdlabo/workers-hono-kit';
73
+ *
74
+ * app.post('/users', validate('json', z.object({ name: z.string() })), (c) => {
75
+ * const { name } = c.req.valid('json');
76
+ * return c.json({ name });
77
+ * });
78
+ * ```
79
+ */
37
80
  export function validate<T>(target: ValidationTarget, schema: ZodType<T>, options?: ValidateOptions) {
38
81
  return zValidator(target, schema, (result, c) => {
39
82
  if (!result.success) {
@@ -55,21 +98,52 @@ export function validate<T>(target: ValidationTarget, schema: ZodType<T>, option
55
98
  });
56
99
  }
57
100
 
58
- /** Sentry の最小形(`@sentry/cloudflare` 等への直接依存を避けるための構造型)。 */
101
+ /**
102
+ * Minimal structural shape of a Sentry scope, covering only the methods this kit calls.
103
+ *
104
+ * @remarks
105
+ * Declaring the shape structurally avoids a direct dependency on `@sentry/cloudflare`; any object
106
+ * exposing these methods (such as the real Sentry scope) is accepted.
107
+ */
59
108
  export interface SentryScopeLike {
109
+ /** Attach a string tag to the current scope. */
60
110
  setTag(key: string, value: string): void;
111
+ /** Attach (or clear, with `null`) a structured context entry on the current scope. */
61
112
  setContext(key: string, context: Record<string, unknown> | null): void;
62
113
  }
114
+
115
+ /**
116
+ * Minimal structural shape of the Sentry client, covering only the methods this kit calls.
117
+ *
118
+ * @remarks
119
+ * Like {@link SentryScopeLike}, this avoids a hard dependency on `@sentry/cloudflare`.
120
+ */
63
121
  export interface SentryLike {
122
+ /** Run a callback with an isolated scope, restoring the previous scope afterward. */
64
123
  withScope(callback: (scope: SentryScopeLike) => void): void;
124
+ /** Report an exception to Sentry. */
65
125
  captureException(error: unknown): void;
66
126
  }
67
127
 
68
128
  /**
69
- * DTO 検証 400 Sentry に通報する `validate` を返すファクトリ(foodlabel/tipsys/winecode で重複していた
70
- * reportValidationToSentry を集約)。kit は `@sentry/cloudflare` に依存せず、Sentry モジュールを注入する。
71
- * 通報は `tag error.type=dto_validation` `context validation={errorCount, errors}`(各 /api
72
- * sentry-validation.pipe.ts 準拠)。検証の挙動・レスポンスは `validate` と同一(通報は副作用のみ)。
129
+ * Create a {@link validate}-like factory that additionally reports DTO validation 400s to Sentry.
130
+ *
131
+ * The returned function has the same signature and behavior as {@link validate}; reporting is a pure
132
+ * side effect that does not alter validation behavior or the response. Each report is tagged with
133
+ * `error.type=dto_validation` and carries a `validation` context of `{ errorCount, errors }`.
134
+ *
135
+ * @param sentry - A Sentry-like client used to capture validation failures; see {@link SentryLike}.
136
+ * @returns A function `(target, schema) => MiddlewareHandler` mirroring {@link validate}.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * import * as Sentry from '@sentry/cloudflare';
141
+ * import { z } from 'zod';
142
+ * import { createSentryValidate } from '@rdlabo/workers-hono-kit';
143
+ *
144
+ * const validate = createSentryValidate(Sentry);
145
+ * app.post('/users', validate('json', z.object({ name: z.string() })), handler);
146
+ * ```
73
147
  */
74
148
  export function createSentryValidate(sentry: SentryLike) {
75
149
  const onValidationError = (error: ZodErrorLike): void => {
@@ -1,17 +1,22 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  /**
4
- * `/api` class-transformer @Transform(number-transform.util.ts)を Zod preprocess で忠実再現する。
5
- * path/query は常に文字列で来るため数値強制が必要。空白のみ文字列は NaN にして後段の数値スキーマで弾く
6
- * (class-validator の @IsInt/@IsNumber が NaN を拒否する挙動と一致。zod v4 は z.number() が NaN を既定拒否)。
4
+ * Zod preprocessors that coerce string path/query parameters into numbers.
5
+ *
6
+ * @remarks
7
+ * These mirror a class-transformer `@Transform(() => Number)` coercion: path and query parameters
8
+ * always arrive as strings, so they must be forced to numbers before number validation. Whitespace-only
9
+ * strings are mapped to `NaN` so the downstream number schema rejects them (matching class-validator's
10
+ * `@IsInt`/`@IsNumber`, which reject `NaN`; zod v4's `z.number()` also rejects `NaN` by default).
7
11
  */
8
12
 
13
+ /** Return `true` when `value` is a string that is empty or contains only whitespace. */
9
14
  const isBlankString = (value: unknown): value is string => typeof value === 'string' && value.trim() === '';
10
15
 
11
- // toNumber: 空白文字列 → NaN、それ以外 → Number(value)
16
+ // toNumber: blank string → NaN, otherwise → Number(value).
12
17
  const rawToNumber = (value: unknown): unknown => (isBlankString(value) ? Number(undefined) : Number(value));
13
18
 
14
- // toNumberWithDefault: undefined/'' → default、空白 → NaNelse Number
19
+ // toNumberWithDefault: undefined/'' → default, blank → NaN, else Number.
15
20
  const rawToNumberWithDefault =
16
21
  (defaultValue: number) =>
17
22
  (value: unknown): unknown => {
@@ -24,7 +29,7 @@ const rawToNumberWithDefault =
24
29
  return Number(value);
25
30
  };
26
31
 
27
- // toOptionalNumber: undefined/null/'' → undefined、空白 → NaNelse Number
32
+ // toOptionalNumber: undefined/null/'' → undefined, blank → NaN, else Number.
28
33
  const rawToOptionalNumber = (value: unknown): unknown => {
29
34
  if (value === undefined || value === null || value === '') {
30
35
  return undefined;
@@ -35,7 +40,7 @@ const rawToOptionalNumber = (value: unknown): unknown => {
35
40
  return Number(value);
36
41
  };
37
42
 
38
- // toNullableNumber: null/undefined → passthrough'' → undefined、空白 → NaNelse Number
43
+ // toNullableNumber: null/undefined → passthrough, '' → undefined, blank → NaN, else Number.
39
44
  const rawToNullableNumber = (value: unknown): unknown => {
40
45
  if (value === null || value === undefined) {
41
46
  return value;
@@ -50,16 +55,70 @@ const rawToNullableNumber = (value: unknown): unknown => {
50
55
  };
51
56
 
52
57
  /**
53
- * 数値強制スキーマ。`inner` `z.number().int()` 等を渡して制約を足せる(既定は z.number())。
54
- * 例: zNum(z.number().int()) で整数必須。
58
+ * Build a required number schema that coerces string input to a number.
59
+ *
60
+ * @param inner - The inner number schema to apply after coercion; pass e.g. `z.number().int()` to add
61
+ * constraints. Defaults to `z.number()`.
62
+ * @returns A zod schema yielding a `number`.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * // Require an integer path param:
67
+ * const schema = z.object({ id: zNum(z.number().int()) });
68
+ * // '42' → 42, '' / ' ' → NaN (rejected)
69
+ * ```
55
70
  */
56
71
  export const zNum = (inner: z.ZodNumber = z.number()): z.ZodType<number> => z.preprocess(rawToNumber, inner);
57
72
 
73
+ /**
74
+ * Build a number schema that coerces string input and substitutes a default for missing values.
75
+ *
76
+ * `undefined` or an empty string yields `defaultValue`; a whitespace-only string yields `NaN`
77
+ * (rejected by the inner schema); anything else is passed through `Number`.
78
+ *
79
+ * @param defaultValue - The value used when the input is `undefined` or an empty string.
80
+ * @param inner - The inner number schema applied after coercion. Defaults to `z.number()`.
81
+ * @returns A zod schema yielding a `number`.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // Default page to 1 when the query param is absent:
86
+ * const schema = z.object({ page: zNumWithDefault(1) });
87
+ * ```
88
+ */
58
89
  export const zNumWithDefault = (defaultValue: number, inner: z.ZodNumber = z.number()): z.ZodType<number> =>
59
90
  z.preprocess(rawToNumberWithDefault(defaultValue), inner);
60
91
 
92
+ /**
93
+ * Build an optional number schema that coerces string input.
94
+ *
95
+ * `undefined`, `null`, or an empty string yields `undefined`; a whitespace-only string yields `NaN`
96
+ * (rejected); anything else is passed through `Number`.
97
+ *
98
+ * @param inner - The inner number schema applied after coercion. Defaults to `z.number()`.
99
+ * @returns A zod schema yielding `number | undefined`.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const schema = z.object({ limit: zNumOptional(z.number().int()) });
104
+ * ```
105
+ */
61
106
  export const zNumOptional = (inner: z.ZodNumber = z.number()): z.ZodType<number | undefined> =>
62
107
  z.preprocess(rawToOptionalNumber, inner.optional());
63
108
 
109
+ /**
110
+ * Build a nullable, optional number schema that coerces string input.
111
+ *
112
+ * `null`/`undefined` pass through unchanged; an empty string yields `undefined`; a whitespace-only
113
+ * string yields `NaN` (rejected); anything else is passed through `Number`.
114
+ *
115
+ * @param inner - The inner number schema applied after coercion. Defaults to `z.number()`.
116
+ * @returns A zod schema yielding `number | null | undefined`.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const schema = z.object({ parentId: zNumNullable(z.number().int()) });
121
+ * ```
122
+ */
64
123
  export const zNumNullable = (inner: z.ZodNumber = z.number()): z.ZodType<number | null | undefined> =>
65
124
  z.preprocess(rawToNullableNumber, inner.nullish());
@@ -1,17 +1,33 @@
1
1
  import Stripe from 'stripe';
2
2
 
3
3
  /**
4
- * Stripe クライアント生成(フリート共通 = receptray/tipsys hono)。Cloudflare Workers には Node の
5
- * http スタックが無いため、Stripe SDK の fetch ベース HttpClient を使う。
6
- *
7
- * `apiVersion` は **任意**: 各 repo の `/api`(NestJS)と挙動を一致させるため、固定したい repo は
8
- * 渡し(例 tipsys は `'2024-04-10'`)、SDK 既定で良い repo は省く(例 receptray)。
4
+ * Options for {@link createStripeClient}.
9
5
  */
10
6
  export interface CreateStripeClientOptions {
11
- /** 固定する Stripe API バージョン。省略すると SDK 既定。 */
7
+ /**
8
+ * Stripe API version to pin the client to. When omitted, the SDK's built-in default is used.
9
+ * Pin it when you need stable, reproducible API behavior independent of SDK upgrades.
10
+ */
12
11
  apiVersion?: string;
13
12
  }
14
13
 
14
+ /**
15
+ * Create a Stripe client configured to run on Cloudflare Workers.
16
+ *
17
+ * @remarks
18
+ * Workers has no Node.js `http` stack, so the client is built with `Stripe.createFetchHttpClient()`
19
+ * (a `fetch`-based HTTP client) instead of the SDK's default Node transport.
20
+ *
21
+ * @param secret - Stripe secret API key.
22
+ * @param options - Optional client configuration; see {@link CreateStripeClientOptions}.
23
+ * @returns A configured {@link Stripe} instance.
24
+ * @throws Error when `secret` is empty.
25
+ * @example
26
+ * ```ts
27
+ * const stripe = createStripeClient(env.STRIPE_SECRET, { apiVersion: '2024-04-10' });
28
+ * const customer = await stripe.customers.retrieve(customerId);
29
+ * ```
30
+ */
15
31
  export function createStripeClient(secret: string, options: CreateStripeClientOptions = {}): Stripe {
16
32
  if (!secret) {
17
33
  throw new Error('Stripe secret is not set');
@@ -24,9 +40,30 @@ export function createStripeClient(secret: string, options: CreateStripeClientOp
24
40
  }
25
41
 
26
42
  /**
27
- * Webhook 署名の検証。Workers では同期版 `constructEvent` が使えない(SubtleCrypto が非同期)ため
28
- * `constructEventAsync` + `SubtleCryptoProvider` を使う。`secret` は署名検証には無関係だが、検証用の
29
- * クライアント生成に必要(API コールはしない)。
43
+ * Verify a Stripe webhook signature and return the parsed event.
44
+ *
45
+ * @remarks
46
+ * Uses `constructEventAsync` together with `Stripe.createSubtleCryptoProvider()` because the Workers
47
+ * crypto API (SubtleCrypto) is asynchronous and the synchronous `constructEvent` is unavailable.
48
+ * The `secret` is not used by signature verification itself, but a client must be constructed to
49
+ * perform the check; no Stripe API call is made.
50
+ *
51
+ * @param secret - Stripe secret API key, used only to construct the verifying client.
52
+ * @param webhookSecret - Endpoint signing secret used to validate the signature.
53
+ * @param payload - Raw request body exactly as received (string or `ArrayBuffer`).
54
+ * @param signature - Value of the `Stripe-Signature` request header.
55
+ * @returns The verified {@link Stripe.Event}.
56
+ * @throws Error when `webhookSecret` is empty, or when `secret` is empty (the verifying client cannot be constructed).
57
+ * @throws Stripe.errors.StripeSignatureVerificationError when the signature does not match.
58
+ * @example
59
+ * ```ts
60
+ * const event = await verifyStripeWebhook(
61
+ * env.STRIPE_SECRET,
62
+ * env.STRIPE_WEBHOOK_SECRET,
63
+ * await request.text(),
64
+ * request.headers.get('stripe-signature')!,
65
+ * );
66
+ * ```
30
67
  */
31
68
  export function verifyStripeWebhook(
32
69
  secret: string,
@@ -3,11 +3,27 @@ import type { DecodedIdToken } from '../firebase/firebase-verifier.js';
3
3
  import type { FakeFirebaseVerifier } from './fakes.js';
4
4
 
5
5
  /**
6
- * app interceptor 互換の認証ヘッダ(`x-amz-security-token` + `x-amz-meta-*`)を組む。
7
- * fleet hono repo の route spec で同形に重複していたものを集約。
6
+ * Build authentication headers compatible with the client interceptor convention
7
+ * (`x-amz-security-token` + `x-amz-meta-*`).
8
8
  *
9
- * - `version` `app_version`(varchar(10)) に入るため 10 文字以内。
10
- * - `contentType: null` を渡すと content-type を付けない(GET 等)。
9
+ * Consolidates the identically-shaped header boilerplate that route specs tend to duplicate.
10
+ *
11
+ * @remarks
12
+ * `version` is persisted into an `app_version` column (`varchar(10)`), so keep it to 10 characters
13
+ * or fewer. Passing `contentType: null` omits the `content-type` header entirely (e.g. for GET requests).
14
+ *
15
+ * @param token - Security token placed in the `x-amz-security-token` header.
16
+ * @param opts - Optional overrides.
17
+ * @param opts.version - App version for `x-amz-meta-version` (defaults to `'1.0.0'`).
18
+ * @param opts.uuid - Device/client UUID for `x-amz-meta-uuid` (defaults to `'test-uuid'`).
19
+ * @param opts.contentType - Content type; defaults to `'application/json'`. Pass `null` to omit the header.
20
+ * @returns A plain header record suitable for `fetch`/`app.request` calls.
21
+ * @example
22
+ * ```ts
23
+ * const res = await app.request('/me', { headers: authHeaders(token) });
24
+ * // GET without a content-type header:
25
+ * await app.request('/items', { headers: authHeaders(token, { contentType: null }) });
26
+ * ```
11
27
  */
12
28
  export function authHeaders(
13
29
  token: string,
@@ -25,8 +41,22 @@ export function authHeaders(
25
41
  }
26
42
 
27
43
  /**
28
- * fake firebase にトークンを登録するだけの薄いヘルパ(DB を触らない)。戻り値はトークン。
29
- * `users` テーブル形が repo 固有(例: airlec は email 主)で provisionUser が合わない場合に使う。
44
+ * Register a token on a fake Firebase verifier without touching the database.
45
+ *
46
+ * Use this when {@link provisionUser} does not fit because the project's `users` table has a
47
+ * non-conventional shape (for example, keyed by email rather than `firebase_uid`); pair it with a
48
+ * project-specific provisioning step.
49
+ *
50
+ * @param firebase - In-memory verifier to register the token on.
51
+ * @param uid - Firebase UID associated with the token.
52
+ * @param record - Additional decoded-token fields to merge in (e.g. `email`).
53
+ * @param token - Token string to register (defaults to `` `tok-${uid}` ``).
54
+ * @returns The registered token string.
55
+ * @example
56
+ * ```ts
57
+ * const token = registerFirebaseToken(firebase, 'uid-1', { email: 'a@example.com' });
58
+ * const res = await app.request('/me', { headers: authHeaders(token) });
59
+ * ```
30
60
  */
31
61
  export function registerFirebaseToken(
32
62
  firebase: FakeFirebaseVerifier,
@@ -39,10 +69,28 @@ export function registerFirebaseToken(
39
69
  }
40
70
 
41
71
  /**
42
- * fake firebase にトークンを登録し、`users(firebase_uid)` 行を用意して userId を返す。
43
- * `users(id, firebase_uid, agree)` の fleet 共通形を前提(foodlabel/receptray など)。同 uid の
44
- * 既存行があれば再利用する(冪等)。users テーブル形が異なる repo は registerFirebaseToken + 独自
45
- * provision を使う。
72
+ * Register a token on a fake Firebase verifier and ensure a matching `users` row exists, returning
73
+ * its id.
74
+ *
75
+ * @remarks
76
+ * Assumes a conventional `users(id, firebase_uid, agree)` table. The operation is idempotent: if a
77
+ * row with the same `firebase_uid` already exists it is reused rather than re-inserted. For projects
78
+ * whose `users` table has a different shape, use {@link registerFirebaseToken} plus project-specific
79
+ * provisioning instead.
80
+ *
81
+ * @param pool - mysql2 pool connected to the test database.
82
+ * @param firebase - In-memory verifier to register the token on.
83
+ * @param opts - Provisioning options.
84
+ * @param opts.uid - Firebase UID for the user.
85
+ * @param opts.token - Token string to register (defaults to `` `tok-${uid}` ``).
86
+ * @param opts.agree - Value for the `agree` column on insert (defaults to `1`).
87
+ * @param opts.email - Optional email merged into the decoded token record.
88
+ * @returns The resolved `userId`, along with the `uid` and registered `token`.
89
+ * @example
90
+ * ```ts
91
+ * const { userId, token } = await provisionUser(pool, firebase, { uid: 'uid-1' });
92
+ * const res = await app.request('/me', { headers: authHeaders(token) });
93
+ * ```
46
94
  */
47
95
  export async function provisionUser(
48
96
  pool: Pool,
@@ -1,15 +1,26 @@
1
1
  /**
2
- * 部分実装から test double を作る。設定済みメソッドはそのまま、未設定メソッドを呼ぶと
3
- * `${name}.${method} not configured` で明示的に失敗する。
2
+ * Build a test double from a partial implementation: configured members are returned as-is, while
3
+ * calling any unconfigured member fails explicitly with `` `${name}.${method} not configured` ``.
4
4
  *
5
- * 各 repo の Fake*Gateway に散っていた「`Partial<impl>` を受け取り、未設定なら throw する手書き
6
- * クラス」の定型を一本化する。interface がドメインごとに異なる gateway(Stripe 等)でも、これで
7
- * 1 行で必要メソッドだけ差した fake を作れる:
5
+ * @remarks
6
+ * Replaces the hand-written "accept a `Partial<impl>` and throw on anything unset" fake classes that
7
+ * tend to proliferate per gateway. Because gateway interfaces differ by domain (Stripe, etc.), this
8
+ * lets you stub only the members a given test exercises in a single line.
8
9
  *
9
- * const stripe = configurableFake<StripeGateway>(
10
- * { listPaymentIntents: async () => fakeApiList([fakePaymentIntent()]) },
11
- * 'FakeStripeGateway',
12
- * );
10
+ * @typeParam T - The interface being faked.
11
+ * @param impl - Partial implementation; only the members the test needs.
12
+ * @param name - Label used in the "not configured" error message (defaults to `'fake'`).
13
+ * @returns A proxy typed as `T` that delegates to `impl` and throws on unconfigured members.
14
+ * @throws Error `` `${name}.${method} not configured` `` when an unconfigured string-keyed member is called.
15
+ * @example
16
+ * ```ts
17
+ * const stripe = configurableFake<StripeGateway>(
18
+ * { listPaymentIntents: async () => fakeApiList([fakePaymentIntent()]) },
19
+ * 'FakeStripeGateway',
20
+ * );
21
+ * await stripe.listPaymentIntents(); // ok
22
+ * await stripe.cancelPaymentIntent('pi_1'); // throws: FakeStripeGateway.cancelPaymentIntent not configured
23
+ * ```
13
24
  */
14
25
  export function configurableFake<T extends object>(impl: Partial<T>, name = 'fake'): T {
15
26
  return new Proxy(impl, {
@@ -17,8 +28,9 @@ export function configurableFake<T extends object>(impl: Partial<T>, name = 'fak
17
28
  if (prop in target) {
18
29
  return (target as Record<string | symbol, unknown>)[prop];
19
30
  }
20
- // Promise インターロップ用プロパティには「未設定メソッド」関数を返さない。返すと fake 自身が
21
- // thenable 扱いされ、誤って await / Promise.resolve した瞬間に then() が呼ばれて throw する罠になる。
31
+ // Never return the "unconfigured member" function for Promise-interop properties. Doing so would
32
+ // make the fake itself look thenable, so accidentally awaiting it (or passing it to
33
+ // Promise.resolve) would invoke then() and throw — a subtle footgun.
22
34
  if (prop === 'then' || prop === 'catch' || prop === 'finally') {
23
35
  return undefined;
24
36
  }