@rdlabo/workers-hono-kit 0.1.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 +160 -24
- package/dist/db/database.js +51 -7
- package/dist/db/index.d.ts +21 -10
- package/dist/db/index.js +17 -8
- 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 +81 -9
- package/dist/firebase/jose-firebase-verifier.js +68 -7
- package/dist/firebase/remote-verifier.d.ts +43 -5
- package/dist/firebase/remote-verifier.js +60 -11
- 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 +41 -30
- package/dist/index.js +29 -21
- package/dist/middleware/auth.d.ts +75 -14
- package/dist/middleware/auth.js +31 -7
- 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 +60 -12
- 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 +79 -11
- package/dist/testing/fakes.js +70 -8
- package/dist/testing/index.d.ts +15 -8
- package/dist/testing/index.js +15 -10
- package/dist/testing/stripe-fixtures.d.ts +93 -3
- package/dist/testing/stripe-fixtures.js +93 -3
- package/package.json +19 -9
- 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 +163 -27
- package/src/db/index.ts +21 -12
- 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 +81 -11
- package/src/firebase/remote-verifier.ts +61 -12
- 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 +47 -33
- package/src/middleware/auth.ts +79 -17
- 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 +60 -12
- package/src/testing/configurable-fake.ts +23 -11
- package/src/testing/db.ts +82 -13
- package/src/testing/fakes.ts +80 -12
- package/src/testing/index.ts +18 -13
- package/src/testing/stripe-fixtures.ts +93 -3
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
54
|
-
*
|
|
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());
|
package/src/stripe/client.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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,
|
package/src/testing/auth.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import type { Pool } from 'mysql2/promise';
|
|
2
|
-
import type { DecodedIdToken } from '../firebase/firebase-verifier';
|
|
3
|
-
import type { FakeFirebaseVerifier } from './fakes';
|
|
2
|
+
import type { DecodedIdToken } from '../firebase/firebase-verifier.js';
|
|
3
|
+
import type { FakeFirebaseVerifier } from './fakes.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Build authentication headers compatible with the client interceptor convention
|
|
7
|
+
* (`x-amz-security-token` + `x-amz-meta-*`).
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
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
|
|
29
|
-
*
|
|
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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
21
|
-
// thenable
|
|
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
|
}
|
package/src/testing/db.ts
CHANGED
|
@@ -4,40 +4,87 @@ import { createConnection, createPool } from 'mysql2/promise';
|
|
|
4
4
|
import type { Pool } from 'mysql2/promise';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* テストスキーマは「コミット済み Drizzle マイグレーション」を単一ソースとして構築する
|
|
9
|
-
* (手書き schema.sql ではなく `db:generate` 由来の ./drizzle)。
|
|
7
|
+
* Connection parameters for the test MySQL server.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
9
|
+
* @see {@link CreateTestDbOptions.connection} for how defaults are resolved.
|
|
12
10
|
*/
|
|
13
11
|
export interface TestDbConnection {
|
|
12
|
+
/** Server host. */
|
|
14
13
|
host: string;
|
|
14
|
+
/** Server port. */
|
|
15
15
|
port: number;
|
|
16
|
+
/** User name. */
|
|
16
17
|
user: string;
|
|
18
|
+
/** Password. */
|
|
17
19
|
password: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Options for {@link createTestDb}.
|
|
24
|
+
*/
|
|
20
25
|
export interface CreateTestDbOptions {
|
|
21
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Test database name (e.g. `'app_test'`). To isolate parallel runs per feature, resolve a per-run
|
|
28
|
+
* name on the caller side and pass it here.
|
|
29
|
+
*/
|
|
22
30
|
dbName: string;
|
|
23
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Absolute path to the Drizzle migrations folder. Resolve it on the caller side, e.g.
|
|
33
|
+
* `join(here, '..', 'drizzle')`.
|
|
34
|
+
*/
|
|
24
35
|
migrationsFolder: string;
|
|
25
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Connection overrides. Unspecified fields fall back to environment variables
|
|
38
|
+
* (`DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD`), then to `127.0.0.1`/`3306`/`root`/`root`.
|
|
39
|
+
*/
|
|
26
40
|
connection?: Partial<TestDbConnection>;
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Test database handle returned by {@link createTestDb}, bundling schema setup, pooling, and
|
|
45
|
+
* fixture helpers for a single test database.
|
|
46
|
+
*/
|
|
29
47
|
export interface TestDb {
|
|
48
|
+
/** The resolved test database name. */
|
|
30
49
|
readonly dbName: string;
|
|
50
|
+
/** The resolved connection parameters. */
|
|
31
51
|
readonly connection: TestDbConnection;
|
|
32
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Drop and recreate the database, then apply the committed Drizzle migrations to build the schema.
|
|
54
|
+
*
|
|
55
|
+
* @returns A promise that resolves once migrations have been applied.
|
|
56
|
+
*/
|
|
33
57
|
resetSchema(): Promise<void>;
|
|
34
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Create a mysql2 pool connected to the test database.
|
|
60
|
+
*
|
|
61
|
+
* @remarks Call `pool.end()` (e.g. in `afterAll`) to release connections.
|
|
62
|
+
* @returns A connection pool for the test database.
|
|
63
|
+
*/
|
|
35
64
|
createTestPool(): Pool;
|
|
36
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Truncate every base table in the database.
|
|
67
|
+
*
|
|
68
|
+
* @remarks Table names are discovered dynamically from `information_schema`; the
|
|
69
|
+
* `__drizzle_migrations` bookkeeping table is excluded. Foreign-key checks are disabled for the
|
|
70
|
+
* duration so truncation order does not matter.
|
|
71
|
+
* @param pool - Pool connected to the test database.
|
|
72
|
+
*/
|
|
37
73
|
truncateAll(pool: Pool): Promise<void>;
|
|
38
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Insert a single row, mapping column names to values — a generic fixture helper for specs.
|
|
76
|
+
*
|
|
77
|
+
* @param pool - Pool connected to the test database.
|
|
78
|
+
* @param table - Target table name.
|
|
79
|
+
* @param row - Column-name to value map. A no-op if empty.
|
|
80
|
+
*/
|
|
39
81
|
seed(pool: Pool, table: string, row: Record<string, unknown>): Promise<void>;
|
|
40
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Report whether the local MySQL server is reachable.
|
|
84
|
+
*
|
|
85
|
+
* @remarks Useful as a guard, e.g. `describe.skipIf(!(await mysqlReachable()))`.
|
|
86
|
+
* @returns `true` if a connection could be opened, otherwise `false`.
|
|
87
|
+
*/
|
|
41
88
|
mysqlReachable(): Promise<boolean>;
|
|
42
89
|
}
|
|
43
90
|
|
|
@@ -51,6 +98,28 @@ function resolveConnection(override?: Partial<TestDbConnection>): TestDbConnecti
|
|
|
51
98
|
};
|
|
52
99
|
}
|
|
53
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Create a {@link TestDb} handle for a single test database.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The test schema is built from the committed Drizzle migrations as the single source of truth
|
|
106
|
+
* (the `db:generate` output under `./drizzle`), rather than a hand-written `schema.sql`. This helper
|
|
107
|
+
* is Node-only test infrastructure (run under Vitest) and is unrelated to runtime behavior.
|
|
108
|
+
*
|
|
109
|
+
* @param options - Database name, migrations folder, and optional connection overrides. See
|
|
110
|
+
* {@link CreateTestDbOptions}.
|
|
111
|
+
* @returns A handle exposing schema setup, pooling, truncation, seeding, and a reachability probe.
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const testDb = createTestDb({ dbName: 'app_test', migrationsFolder: join(here, '..', 'drizzle') });
|
|
115
|
+
* beforeAll(async () => {
|
|
116
|
+
* await testDb.resetSchema();
|
|
117
|
+
* });
|
|
118
|
+
* const pool = testDb.createTestPool();
|
|
119
|
+
* beforeEach(() => testDb.truncateAll(pool));
|
|
120
|
+
* afterAll(() => pool.end());
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
54
123
|
export function createTestDb(options: CreateTestDbOptions): TestDb {
|
|
55
124
|
const { dbName, migrationsFolder } = options;
|
|
56
125
|
const connection = resolveConnection(options.connection);
|
|
@@ -80,7 +149,7 @@ export function createTestDb(options: CreateTestDbOptions): TestDb {
|
|
|
80
149
|
timezone: '+09:00',
|
|
81
150
|
});
|
|
82
151
|
// Pin ONLY_FULL_GROUP_BY on every pooled connection so GROUP BY violations surface in specs
|
|
83
|
-
// regardless of the server's my.cnf (
|
|
152
|
+
// regardless of the server's my.cnf (the policy is centralized here, not left to each server). CONCAT keeps
|
|
84
153
|
// the server's other sql_mode flags and is harmless if ONLY_FULL_GROUP_BY is already present.
|
|
85
154
|
// mysql2 queues this SET ahead of the consumer's first query on each new physical connection.
|
|
86
155
|
pool.on('connection', (conn) => {
|