@rdlabo/workers-hono-kit 0.2.0 → 0.3.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/README.md +126 -11
- package/dist/ai/gateway.d.ts +54 -16
- package/dist/ai/gateway.js +37 -12
- package/dist/aws/cloudfront.d.ts +23 -5
- package/dist/aws/cloudfront.js +45 -6
- package/dist/aws/secrets-manager.d.ts +38 -4
- package/dist/aws/secrets-manager.js +48 -3
- package/dist/cache/kv-cache.d.ts +173 -10
- package/dist/cache/kv-cache.js +139 -7
- package/dist/db/connection.d.ts +56 -14
- package/dist/db/connection.js +39 -13
- package/dist/db/database.d.ts +159 -23
- package/dist/db/database.js +49 -5
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.js +11 -2
- package/dist/db/jst.d.ts +89 -6
- package/dist/db/jst.js +89 -23
- package/dist/db/orm-config.d.ts +61 -19
- package/dist/db/orm-config.js +43 -14
- package/dist/db/retry.d.ts +25 -3
- package/dist/db/retry.js +25 -3
- package/dist/db/write-result.d.ts +27 -4
- package/dist/db/write-result.js +22 -1
- package/dist/firebase/firebase-verifier.d.ts +53 -4
- package/dist/firebase/identity-toolkit.d.ts +54 -5
- package/dist/firebase/identity-toolkit.js +51 -0
- package/dist/firebase/jose-firebase-verifier.d.ts +79 -7
- package/dist/firebase/jose-firebase-verifier.js +68 -7
- package/dist/firebase/remote-verifier.d.ts +42 -4
- package/dist/firebase/remote-verifier.js +58 -9
- package/dist/http/app-env.d.ts +41 -8
- package/dist/http/app-env.js +38 -8
- package/dist/http/app-info.d.ts +25 -3
- package/dist/http/app-info.js +16 -2
- package/dist/http/http-status.d.ts +12 -3
- package/dist/http/http-status.js +12 -3
- package/dist/http/nest-error.d.ts +90 -29
- package/dist/http/nest-error.js +59 -18
- package/dist/http/user-protocol.d.ts +23 -3
- package/dist/http/user-protocol.js +14 -2
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -3
- package/dist/middleware/auth.d.ts +74 -13
- package/dist/middleware/auth.js +30 -6
- package/dist/middleware/finalize-response.d.ts +30 -0
- package/dist/middleware/finalize-response.js +41 -12
- package/dist/middleware/validation.d.ts +83 -9
- package/dist/middleware/validation.js +52 -9
- package/dist/middleware/zod-coerce.d.ts +56 -2
- package/dist/middleware/zod-coerce.js +68 -9
- package/dist/queue/consumer.d.ts +112 -0
- package/dist/queue/consumer.js +80 -0
- package/dist/queue/send.d.ts +90 -0
- package/dist/queue/send.js +85 -0
- package/dist/stripe/client.d.ts +46 -9
- package/dist/stripe/client.js +41 -3
- package/dist/testing/auth.d.ts +58 -10
- package/dist/testing/auth.js +58 -10
- package/dist/testing/configurable-fake.d.ts +20 -9
- package/dist/testing/configurable-fake.js +23 -11
- package/dist/testing/db.d.ts +81 -12
- package/dist/testing/db.js +23 -1
- package/dist/testing/fakes.d.ts +77 -9
- package/dist/testing/fakes.js +69 -7
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +10 -5
- package/dist/testing/stripe-fixtures.d.ts +93 -3
- package/dist/testing/stripe-fixtures.js +93 -3
- package/package.json +3 -2
- package/scripts/check-subrequest-fanout.mjs +86 -0
- package/src/ai/gateway.ts +66 -27
- package/src/aws/cloudfront.ts +46 -6
- package/src/aws/secrets-manager.ts +56 -7
- package/src/cache/kv-cache.ts +194 -12
- package/src/db/connection.ts +56 -14
- package/src/db/database.ts +160 -24
- package/src/db/index.ts +11 -2
- package/src/db/jst.ts +89 -23
- package/src/db/orm-config.ts +61 -19
- package/src/db/retry.ts +25 -3
- package/src/db/write-result.ts +27 -4
- package/src/firebase/firebase-verifier.ts +53 -4
- package/src/firebase/identity-toolkit.ts +57 -5
- package/src/firebase/jose-firebase-verifier.ts +79 -9
- package/src/firebase/remote-verifier.ts +58 -9
- package/src/http/app-env.ts +41 -8
- package/src/http/app-info.ts +25 -3
- package/src/http/http-status.ts +12 -3
- package/src/http/nest-error.ts +106 -37
- package/src/http/user-protocol.ts +23 -3
- package/src/index.ts +17 -3
- package/src/middleware/auth.ts +77 -15
- package/src/middleware/finalize-response.ts +41 -12
- package/src/middleware/validation.ts +89 -15
- package/src/middleware/zod-coerce.ts +68 -9
- package/src/queue/consumer.ts +146 -0
- package/src/queue/send.ts +129 -0
- package/src/stripe/client.ts +46 -9
- package/src/testing/auth.ts +58 -10
- package/src/testing/configurable-fake.ts +23 -11
- package/src/testing/db.ts +82 -13
- package/src/testing/fakes.ts +77 -9
- package/src/testing/index.ts +10 -5
- package/src/testing/stripe-fixtures.ts +93 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer-side helper for fanning a large list of items into a Cloudflare Queue without letting the
|
|
3
|
+
* producer's own subrequest count scale linearly with the list.
|
|
4
|
+
*
|
|
5
|
+
* A Worker may issue at most 50 (free) / 1000 (paid) subrequests per invocation, and each
|
|
6
|
+
* {@link QueueLike.send} counts as one subrequest. Enqueuing `N` items with per-item `send()` calls
|
|
7
|
+
* therefore reintroduces the very unbounded fan-out that queues exist to remove. {@link sendInChunks}
|
|
8
|
+
* instead groups items into batches and issues one {@link QueueLike.sendBatch} per batch, so the
|
|
9
|
+
* producer spends `ceil(N / chunkSize)` subrequests regardless of how large `N` grows.
|
|
10
|
+
*
|
|
11
|
+
* The heavy per-item work (external API calls, etc.) is expected to run in the queue *consumer*,
|
|
12
|
+
* where each invocation processes only `max_batch_size` messages and thus enjoys its own bounded
|
|
13
|
+
* subrequest budget. See {@link processBatch} for the consumer side.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // In a Cron Trigger `scheduled` handler: enqueue every billing user, then let the consumer
|
|
18
|
+
* // re-derive payment state one user per message.
|
|
19
|
+
* const userIds = await query.getUserIdForReloadCustomer(); // DB read — not a subrequest
|
|
20
|
+
* await sendInChunks(env.PAYMENT_RELOAD_QUEUE, userIds); // ceil(N / 100) subrequests
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Minimal subset of `@cloudflare/workers-types`' `Queue` used by {@link sendInChunks}.
|
|
27
|
+
*
|
|
28
|
+
* Declared locally so consumers are not forced to depend on `@cloudflare/workers-types`. Only the
|
|
29
|
+
* batch-send operation the helper actually needs is modeled.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam Body - Type of each message body enqueued onto this queue.
|
|
32
|
+
*/
|
|
33
|
+
export interface QueueLike<Body = unknown> {
|
|
34
|
+
/**
|
|
35
|
+
* Enqueue up to 100 messages in a single operation (one subrequest).
|
|
36
|
+
*
|
|
37
|
+
* @param messages - The message envelopes to enqueue; at most 100 per call, 256 KB per batch.
|
|
38
|
+
* @param options - Optional batch-level options, e.g. a `delaySeconds` applied to every message.
|
|
39
|
+
* @returns A promise that resolves once the batch is accepted. The resolved value is ignored, so a
|
|
40
|
+
* real `Queue` binding (whose `sendBatch` resolves to a `QueueSendBatchResponse`) is assignable.
|
|
41
|
+
*/
|
|
42
|
+
sendBatch: (messages: Iterable<QueueSendMessage<Body>>, options?: {
|
|
43
|
+
delaySeconds?: number;
|
|
44
|
+
}) => Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A single message envelope passed to {@link QueueLike.sendBatch}.
|
|
48
|
+
*
|
|
49
|
+
* @typeParam Body - Type of the message body.
|
|
50
|
+
*/
|
|
51
|
+
export interface QueueSendMessage<Body = unknown> {
|
|
52
|
+
/** The message payload; structured-cloned by the Queues runtime. */
|
|
53
|
+
body: Body;
|
|
54
|
+
/** Optional content type hint (`'json'` by default for object bodies). */
|
|
55
|
+
contentType?: 'text' | 'bytes' | 'json' | 'v8';
|
|
56
|
+
/** Optional per-message delivery delay, in seconds. */
|
|
57
|
+
delaySeconds?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Enqueue every item in `items` using batched sends so the producer's subrequest count stays
|
|
61
|
+
* bounded at `ceil(items.length / chunkSize)` rather than growing per item.
|
|
62
|
+
*
|
|
63
|
+
* Each element becomes one queue message (`{ body: item }`); wrap or map your rows into small,
|
|
64
|
+
* self-describing payloads (e.g. an id plus a discriminator) before calling. Keep each batch under
|
|
65
|
+
* the Queues 256 KB limit — with `chunkSize <= 100` and small id-shaped payloads this is not a
|
|
66
|
+
* concern, but large bodies may require a smaller `chunkSize`.
|
|
67
|
+
*
|
|
68
|
+
* Batches are sent sequentially so a mid-list failure surfaces promptly (the already-sent batches
|
|
69
|
+
* are durably enqueued; the throw lets the caller decide whether to retry the remainder). An empty
|
|
70
|
+
* `items` is a no-op.
|
|
71
|
+
*
|
|
72
|
+
* @typeParam Body - Type of each message body.
|
|
73
|
+
* @param queue - The producer binding to send onto.
|
|
74
|
+
* @param items - The full list of message bodies to enqueue; may be arbitrarily large.
|
|
75
|
+
* @param options - Tuning options.
|
|
76
|
+
* @param options.chunkSize - Messages per `sendBatch` call. Defaults to and is capped at 100 (the
|
|
77
|
+
* Queues per-batch maximum); values below 1 are clamped to 1.
|
|
78
|
+
* @returns The number of `sendBatch` calls issued (i.e. subrequests spent), useful for asserting the
|
|
79
|
+
* fan-out stayed bounded in tests.
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const batches = await sendInChunks(env.MY_QUEUE, ids); // one send per 100 ids
|
|
83
|
+
* const batches = await sendInChunks(env.MY_QUEUE, rows, { // custom batch size for larger bodies
|
|
84
|
+
* chunkSize: 25,
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function sendInChunks<Body>(queue: QueueLike<Body>, items: readonly Body[], options?: {
|
|
89
|
+
chunkSize?: number;
|
|
90
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer-side helper for fanning a large list of items into a Cloudflare Queue without letting the
|
|
3
|
+
* producer's own subrequest count scale linearly with the list.
|
|
4
|
+
*
|
|
5
|
+
* A Worker may issue at most 50 (free) / 1000 (paid) subrequests per invocation, and each
|
|
6
|
+
* {@link QueueLike.send} counts as one subrequest. Enqueuing `N` items with per-item `send()` calls
|
|
7
|
+
* therefore reintroduces the very unbounded fan-out that queues exist to remove. {@link sendInChunks}
|
|
8
|
+
* instead groups items into batches and issues one {@link QueueLike.sendBatch} per batch, so the
|
|
9
|
+
* producer spends `ceil(N / chunkSize)` subrequests regardless of how large `N` grows.
|
|
10
|
+
*
|
|
11
|
+
* The heavy per-item work (external API calls, etc.) is expected to run in the queue *consumer*,
|
|
12
|
+
* where each invocation processes only `max_batch_size` messages and thus enjoys its own bounded
|
|
13
|
+
* subrequest budget. See {@link processBatch} for the consumer side.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // In a Cron Trigger `scheduled` handler: enqueue every billing user, then let the consumer
|
|
18
|
+
* // re-derive payment state one user per message.
|
|
19
|
+
* const userIds = await query.getUserIdForReloadCustomer(); // DB read — not a subrequest
|
|
20
|
+
* await sendInChunks(env.PAYMENT_RELOAD_QUEUE, userIds); // ceil(N / 100) subrequests
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The Cloudflare Queues hard limit on messages per {@link QueueLike.sendBatch} call.
|
|
27
|
+
*/
|
|
28
|
+
const MAX_BATCH_SIZE = 100;
|
|
29
|
+
/**
|
|
30
|
+
* Split a list into fixed-size chunks (order preserving).
|
|
31
|
+
*
|
|
32
|
+
* @typeParam T - Element type.
|
|
33
|
+
* @param items - Source list.
|
|
34
|
+
* @param size - Maximum chunk length (assumed `>= 1`).
|
|
35
|
+
* @returns An array of chunks, each at most `size` long.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function chunk(items, size) {
|
|
39
|
+
const result = [];
|
|
40
|
+
for (let i = 0; i < items.length; i += size) {
|
|
41
|
+
result.push(items.slice(i, i + size));
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Enqueue every item in `items` using batched sends so the producer's subrequest count stays
|
|
47
|
+
* bounded at `ceil(items.length / chunkSize)` rather than growing per item.
|
|
48
|
+
*
|
|
49
|
+
* Each element becomes one queue message (`{ body: item }`); wrap or map your rows into small,
|
|
50
|
+
* self-describing payloads (e.g. an id plus a discriminator) before calling. Keep each batch under
|
|
51
|
+
* the Queues 256 KB limit — with `chunkSize <= 100` and small id-shaped payloads this is not a
|
|
52
|
+
* concern, but large bodies may require a smaller `chunkSize`.
|
|
53
|
+
*
|
|
54
|
+
* Batches are sent sequentially so a mid-list failure surfaces promptly (the already-sent batches
|
|
55
|
+
* are durably enqueued; the throw lets the caller decide whether to retry the remainder). An empty
|
|
56
|
+
* `items` is a no-op.
|
|
57
|
+
*
|
|
58
|
+
* @typeParam Body - Type of each message body.
|
|
59
|
+
* @param queue - The producer binding to send onto.
|
|
60
|
+
* @param items - The full list of message bodies to enqueue; may be arbitrarily large.
|
|
61
|
+
* @param options - Tuning options.
|
|
62
|
+
* @param options.chunkSize - Messages per `sendBatch` call. Defaults to and is capped at 100 (the
|
|
63
|
+
* Queues per-batch maximum); values below 1 are clamped to 1.
|
|
64
|
+
* @returns The number of `sendBatch` calls issued (i.e. subrequests spent), useful for asserting the
|
|
65
|
+
* fan-out stayed bounded in tests.
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const batches = await sendInChunks(env.MY_QUEUE, ids); // one send per 100 ids
|
|
69
|
+
* const batches = await sendInChunks(env.MY_QUEUE, rows, { // custom batch size for larger bodies
|
|
70
|
+
* chunkSize: 25,
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export async function sendInChunks(queue, items, options) {
|
|
75
|
+
if (items.length === 0) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
const rawChunkSize = options?.chunkSize ?? MAX_BATCH_SIZE;
|
|
79
|
+
const chunkSize = Math.min(MAX_BATCH_SIZE, Math.max(1, Math.trunc(Number.isNaN(rawChunkSize) ? MAX_BATCH_SIZE : rawChunkSize)));
|
|
80
|
+
const batches = chunk(items, chunkSize);
|
|
81
|
+
for (const batch of batches) {
|
|
82
|
+
await queue.sendBatch(batch.map((body) => ({ body })));
|
|
83
|
+
}
|
|
84
|
+
return batches.length;
|
|
85
|
+
}
|
package/dist/stripe/client.d.ts
CHANGED
|
@@ -1,19 +1,56 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* http スタックが無いため、Stripe SDK の fetch ベース HttpClient を使う。
|
|
5
|
-
*
|
|
6
|
-
* `apiVersion` は **任意**: 各 repo の `/api`(NestJS)と挙動を一致させるため、固定したい repo は
|
|
7
|
-
* 渡し(例 tipsys は `'2024-04-10'`)、SDK 既定で良い repo は省く(例 receptray)。
|
|
3
|
+
* Options for {@link createStripeClient}.
|
|
8
4
|
*/
|
|
9
5
|
export interface CreateStripeClientOptions {
|
|
10
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Stripe API version to pin the client to. When omitted, the SDK's built-in default is used.
|
|
8
|
+
* Pin it when you need stable, reproducible API behavior independent of SDK upgrades.
|
|
9
|
+
*/
|
|
11
10
|
apiVersion?: string;
|
|
12
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a Stripe client configured to run on Cloudflare Workers.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Workers has no Node.js `http` stack, so the client is built with `Stripe.createFetchHttpClient()`
|
|
17
|
+
* (a `fetch`-based HTTP client) instead of the SDK's default Node transport.
|
|
18
|
+
*
|
|
19
|
+
* @param secret - Stripe secret API key.
|
|
20
|
+
* @param options - Optional client configuration; see {@link CreateStripeClientOptions}.
|
|
21
|
+
* @returns A configured {@link Stripe} instance.
|
|
22
|
+
* @throws Error when `secret` is empty.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const stripe = createStripeClient(env.STRIPE_SECRET, { apiVersion: '2024-04-10' });
|
|
26
|
+
* const customer = await stripe.customers.retrieve(customerId);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
13
29
|
export declare function createStripeClient(secret: string, options?: CreateStripeClientOptions): Stripe;
|
|
14
30
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
31
|
+
* Verify a Stripe webhook signature and return the parsed event.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Uses `constructEventAsync` together with `Stripe.createSubtleCryptoProvider()` because the Workers
|
|
35
|
+
* crypto API (SubtleCrypto) is asynchronous and the synchronous `constructEvent` is unavailable.
|
|
36
|
+
* The `secret` is not used by signature verification itself, but a client must be constructed to
|
|
37
|
+
* perform the check; no Stripe API call is made.
|
|
38
|
+
*
|
|
39
|
+
* @param secret - Stripe secret API key, used only to construct the verifying client.
|
|
40
|
+
* @param webhookSecret - Endpoint signing secret used to validate the signature.
|
|
41
|
+
* @param payload - Raw request body exactly as received (string or `ArrayBuffer`).
|
|
42
|
+
* @param signature - Value of the `Stripe-Signature` request header.
|
|
43
|
+
* @returns The verified {@link Stripe.Event}.
|
|
44
|
+
* @throws Error when `webhookSecret` is empty, or when `secret` is empty (the verifying client cannot be constructed).
|
|
45
|
+
* @throws Stripe.errors.StripeSignatureVerificationError when the signature does not match.
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const event = await verifyStripeWebhook(
|
|
49
|
+
* env.STRIPE_SECRET,
|
|
50
|
+
* env.STRIPE_WEBHOOK_SECRET,
|
|
51
|
+
* await request.text(),
|
|
52
|
+
* request.headers.get('stripe-signature')!,
|
|
53
|
+
* );
|
|
54
|
+
* ```
|
|
18
55
|
*/
|
|
19
56
|
export declare function verifyStripeWebhook(secret: string, webhookSecret: string, payload: string | ArrayBuffer, signature: string): Promise<Stripe.Event>;
|
package/dist/stripe/client.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
|
+
/**
|
|
3
|
+
* Create a Stripe client configured to run on Cloudflare Workers.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Workers has no Node.js `http` stack, so the client is built with `Stripe.createFetchHttpClient()`
|
|
7
|
+
* (a `fetch`-based HTTP client) instead of the SDK's default Node transport.
|
|
8
|
+
*
|
|
9
|
+
* @param secret - Stripe secret API key.
|
|
10
|
+
* @param options - Optional client configuration; see {@link CreateStripeClientOptions}.
|
|
11
|
+
* @returns A configured {@link Stripe} instance.
|
|
12
|
+
* @throws Error when `secret` is empty.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const stripe = createStripeClient(env.STRIPE_SECRET, { apiVersion: '2024-04-10' });
|
|
16
|
+
* const customer = await stripe.customers.retrieve(customerId);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
2
19
|
export function createStripeClient(secret, options = {}) {
|
|
3
20
|
if (!secret) {
|
|
4
21
|
throw new Error('Stripe secret is not set');
|
|
@@ -10,9 +27,30 @@ export function createStripeClient(secret, options = {}) {
|
|
|
10
27
|
return new Stripe(secret, config);
|
|
11
28
|
}
|
|
12
29
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
30
|
+
* Verify a Stripe webhook signature and return the parsed event.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Uses `constructEventAsync` together with `Stripe.createSubtleCryptoProvider()` because the Workers
|
|
34
|
+
* crypto API (SubtleCrypto) is asynchronous and the synchronous `constructEvent` is unavailable.
|
|
35
|
+
* The `secret` is not used by signature verification itself, but a client must be constructed to
|
|
36
|
+
* perform the check; no Stripe API call is made.
|
|
37
|
+
*
|
|
38
|
+
* @param secret - Stripe secret API key, used only to construct the verifying client.
|
|
39
|
+
* @param webhookSecret - Endpoint signing secret used to validate the signature.
|
|
40
|
+
* @param payload - Raw request body exactly as received (string or `ArrayBuffer`).
|
|
41
|
+
* @param signature - Value of the `Stripe-Signature` request header.
|
|
42
|
+
* @returns The verified {@link Stripe.Event}.
|
|
43
|
+
* @throws Error when `webhookSecret` is empty, or when `secret` is empty (the verifying client cannot be constructed).
|
|
44
|
+
* @throws Stripe.errors.StripeSignatureVerificationError when the signature does not match.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const event = await verifyStripeWebhook(
|
|
48
|
+
* env.STRIPE_SECRET,
|
|
49
|
+
* env.STRIPE_WEBHOOK_SECRET,
|
|
50
|
+
* await request.text(),
|
|
51
|
+
* request.headers.get('stripe-signature')!,
|
|
52
|
+
* );
|
|
53
|
+
* ```
|
|
16
54
|
*/
|
|
17
55
|
export function verifyStripeWebhook(secret, webhookSecret, payload, signature) {
|
|
18
56
|
if (!webhookSecret) {
|
package/dist/testing/auth.d.ts
CHANGED
|
@@ -2,11 +2,27 @@ import type { Pool } from 'mysql2/promise';
|
|
|
2
2
|
import type { DecodedIdToken } from '../firebase/firebase-verifier.js';
|
|
3
3
|
import type { FakeFirebaseVerifier } from './fakes.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Build authentication headers compatible with the client interceptor convention
|
|
6
|
+
* (`x-amz-security-token` + `x-amz-meta-*`).
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* Consolidates the identically-shaped header boilerplate that route specs tend to duplicate.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* `version` is persisted into an `app_version` column (`varchar(10)`), so keep it to 10 characters
|
|
12
|
+
* or fewer. Passing `contentType: null` omits the `content-type` header entirely (e.g. for GET requests).
|
|
13
|
+
*
|
|
14
|
+
* @param token - Security token placed in the `x-amz-security-token` header.
|
|
15
|
+
* @param opts - Optional overrides.
|
|
16
|
+
* @param opts.version - App version for `x-amz-meta-version` (defaults to `'1.0.0'`).
|
|
17
|
+
* @param opts.uuid - Device/client UUID for `x-amz-meta-uuid` (defaults to `'test-uuid'`).
|
|
18
|
+
* @param opts.contentType - Content type; defaults to `'application/json'`. Pass `null` to omit the header.
|
|
19
|
+
* @returns A plain header record suitable for `fetch`/`app.request` calls.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const res = await app.request('/me', { headers: authHeaders(token) });
|
|
23
|
+
* // GET without a content-type header:
|
|
24
|
+
* await app.request('/items', { headers: authHeaders(token, { contentType: null }) });
|
|
25
|
+
* ```
|
|
10
26
|
*/
|
|
11
27
|
export declare function authHeaders(token: string, opts?: {
|
|
12
28
|
version?: string;
|
|
@@ -14,15 +30,47 @@ export declare function authHeaders(token: string, opts?: {
|
|
|
14
30
|
contentType?: string | null;
|
|
15
31
|
}): Record<string, string>;
|
|
16
32
|
/**
|
|
17
|
-
* fake
|
|
18
|
-
*
|
|
33
|
+
* Register a token on a fake Firebase verifier without touching the database.
|
|
34
|
+
*
|
|
35
|
+
* Use this when {@link provisionUser} does not fit because the project's `users` table has a
|
|
36
|
+
* non-conventional shape (for example, keyed by email rather than `firebase_uid`); pair it with a
|
|
37
|
+
* project-specific provisioning step.
|
|
38
|
+
*
|
|
39
|
+
* @param firebase - In-memory verifier to register the token on.
|
|
40
|
+
* @param uid - Firebase UID associated with the token.
|
|
41
|
+
* @param record - Additional decoded-token fields to merge in (e.g. `email`).
|
|
42
|
+
* @param token - Token string to register (defaults to `` `tok-${uid}` ``).
|
|
43
|
+
* @returns The registered token string.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const token = registerFirebaseToken(firebase, 'uid-1', { email: 'a@example.com' });
|
|
47
|
+
* const res = await app.request('/me', { headers: authHeaders(token) });
|
|
48
|
+
* ```
|
|
19
49
|
*/
|
|
20
50
|
export declare function registerFirebaseToken(firebase: FakeFirebaseVerifier, uid: string, record?: Partial<DecodedIdToken>, token?: string): string;
|
|
21
51
|
/**
|
|
22
|
-
* fake
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
52
|
+
* Register a token on a fake Firebase verifier and ensure a matching `users` row exists, returning
|
|
53
|
+
* its id.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Assumes a conventional `users(id, firebase_uid, agree)` table. The operation is idempotent: if a
|
|
57
|
+
* row with the same `firebase_uid` already exists it is reused rather than re-inserted. For projects
|
|
58
|
+
* whose `users` table has a different shape, use {@link registerFirebaseToken} plus project-specific
|
|
59
|
+
* provisioning instead.
|
|
60
|
+
*
|
|
61
|
+
* @param pool - mysql2 pool connected to the test database.
|
|
62
|
+
* @param firebase - In-memory verifier to register the token on.
|
|
63
|
+
* @param opts - Provisioning options.
|
|
64
|
+
* @param opts.uid - Firebase UID for the user.
|
|
65
|
+
* @param opts.token - Token string to register (defaults to `` `tok-${uid}` ``).
|
|
66
|
+
* @param opts.agree - Value for the `agree` column on insert (defaults to `1`).
|
|
67
|
+
* @param opts.email - Optional email merged into the decoded token record.
|
|
68
|
+
* @returns The resolved `userId`, along with the `uid` and registered `token`.
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const { userId, token } = await provisionUser(pool, firebase, { uid: 'uid-1' });
|
|
72
|
+
* const res = await app.request('/me', { headers: authHeaders(token) });
|
|
73
|
+
* ```
|
|
26
74
|
*/
|
|
27
75
|
export declare function provisionUser(pool: Pool, firebase: FakeFirebaseVerifier, opts: {
|
|
28
76
|
uid: string;
|
package/dist/testing/auth.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Build authentication headers compatible with the client interceptor convention
|
|
3
|
+
* (`x-amz-security-token` + `x-amz-meta-*`).
|
|
4
4
|
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
5
|
+
* Consolidates the identically-shaped header boilerplate that route specs tend to duplicate.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `version` is persisted into an `app_version` column (`varchar(10)`), so keep it to 10 characters
|
|
9
|
+
* or fewer. Passing `contentType: null` omits the `content-type` header entirely (e.g. for GET requests).
|
|
10
|
+
*
|
|
11
|
+
* @param token - Security token placed in the `x-amz-security-token` header.
|
|
12
|
+
* @param opts - Optional overrides.
|
|
13
|
+
* @param opts.version - App version for `x-amz-meta-version` (defaults to `'1.0.0'`).
|
|
14
|
+
* @param opts.uuid - Device/client UUID for `x-amz-meta-uuid` (defaults to `'test-uuid'`).
|
|
15
|
+
* @param opts.contentType - Content type; defaults to `'application/json'`. Pass `null` to omit the header.
|
|
16
|
+
* @returns A plain header record suitable for `fetch`/`app.request` calls.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const res = await app.request('/me', { headers: authHeaders(token) });
|
|
20
|
+
* // GET without a content-type header:
|
|
21
|
+
* await app.request('/items', { headers: authHeaders(token, { contentType: null }) });
|
|
22
|
+
* ```
|
|
7
23
|
*/
|
|
8
24
|
export function authHeaders(token, opts = {}) {
|
|
9
25
|
const headers = {
|
|
@@ -17,18 +33,50 @@ export function authHeaders(token, opts = {}) {
|
|
|
17
33
|
return headers;
|
|
18
34
|
}
|
|
19
35
|
/**
|
|
20
|
-
* fake
|
|
21
|
-
*
|
|
36
|
+
* Register a token on a fake Firebase verifier without touching the database.
|
|
37
|
+
*
|
|
38
|
+
* Use this when {@link provisionUser} does not fit because the project's `users` table has a
|
|
39
|
+
* non-conventional shape (for example, keyed by email rather than `firebase_uid`); pair it with a
|
|
40
|
+
* project-specific provisioning step.
|
|
41
|
+
*
|
|
42
|
+
* @param firebase - In-memory verifier to register the token on.
|
|
43
|
+
* @param uid - Firebase UID associated with the token.
|
|
44
|
+
* @param record - Additional decoded-token fields to merge in (e.g. `email`).
|
|
45
|
+
* @param token - Token string to register (defaults to `` `tok-${uid}` ``).
|
|
46
|
+
* @returns The registered token string.
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const token = registerFirebaseToken(firebase, 'uid-1', { email: 'a@example.com' });
|
|
50
|
+
* const res = await app.request('/me', { headers: authHeaders(token) });
|
|
51
|
+
* ```
|
|
22
52
|
*/
|
|
23
53
|
export function registerFirebaseToken(firebase, uid, record = {}, token = `tok-${uid}`) {
|
|
24
54
|
firebase.register(token, { uid, ...record });
|
|
25
55
|
return token;
|
|
26
56
|
}
|
|
27
57
|
/**
|
|
28
|
-
* fake
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
58
|
+
* Register a token on a fake Firebase verifier and ensure a matching `users` row exists, returning
|
|
59
|
+
* its id.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* Assumes a conventional `users(id, firebase_uid, agree)` table. The operation is idempotent: if a
|
|
63
|
+
* row with the same `firebase_uid` already exists it is reused rather than re-inserted. For projects
|
|
64
|
+
* whose `users` table has a different shape, use {@link registerFirebaseToken} plus project-specific
|
|
65
|
+
* provisioning instead.
|
|
66
|
+
*
|
|
67
|
+
* @param pool - mysql2 pool connected to the test database.
|
|
68
|
+
* @param firebase - In-memory verifier to register the token on.
|
|
69
|
+
* @param opts - Provisioning options.
|
|
70
|
+
* @param opts.uid - Firebase UID for the user.
|
|
71
|
+
* @param opts.token - Token string to register (defaults to `` `tok-${uid}` ``).
|
|
72
|
+
* @param opts.agree - Value for the `agree` column on insert (defaults to `1`).
|
|
73
|
+
* @param opts.email - Optional email merged into the decoded token record.
|
|
74
|
+
* @returns The resolved `userId`, along with the `uid` and registered `token`.
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const { userId, token } = await provisionUser(pool, firebase, { uid: 'uid-1' });
|
|
78
|
+
* const res = await app.request('/me', { headers: authHeaders(token) });
|
|
79
|
+
* ```
|
|
32
80
|
*/
|
|
33
81
|
export async function provisionUser(pool, firebase, opts) {
|
|
34
82
|
const token = registerFirebaseToken(firebase, opts.uid, opts.email ? { email: opts.email } : {}, opts.token);
|
|
@@ -1,14 +1,25 @@
|
|
|
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 declare function configurableFake<T extends object>(impl: Partial<T>, name?: string): T;
|
|
@@ -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(impl, name = 'fake') {
|
|
15
26
|
return new Proxy(impl, {
|
|
@@ -17,8 +28,9 @@ export function configurableFake(impl, name = 'fake') {
|
|
|
17
28
|
if (prop in target) {
|
|
18
29
|
return target[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
|
}
|