@rdlabo/workers-hono-kit 0.10.0 → 0.10.2
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 +17 -3
- package/dist/firebase/jose-firebase-verifier.d.ts +12 -0
- package/dist/firebase/jose-firebase-verifier.js +20 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/middleware/auth.d.ts +37 -3
- package/dist/middleware/auth.js +36 -7
- package/dist/stripe/subscription.d.ts +1 -0
- package/dist/stripe/subscription.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
106
106
|
| `sendInChunks(queue, messages, options?)` / `QueueLike` / `QueueSendMessage` | Send queue messages in bounded chunks to stay under the Workers subrequest cap per invocation. `options.chunkSize` sets the per-batch size (defaults to and is capped at 100). |
|
|
107
107
|
| `processBatch(batch, handler, options?)` / `isNonRetryableQueueError(error)` / `NonRetryableQueueErrorLike` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency. Errors explicitly tagged with `queueDisposition: 'discard'` are reported and acked as permanent failures; all other errors are retried. |
|
|
108
108
|
| `createQueueErrorHandler(options)` / `CreateQueueErrorHandlerOptions` | Factory for `processBatch`'s `onError`: logs every failure; optional Sentry capture with queue/message context; optional `maxRetries` gate (report only on final attempt, except permanent failures which are reported immediately). |
|
|
109
|
+
| `assertStripeCustomerUpdated(options)` | Preserve the shared Stripe UPDATE→existence-check algorithm. `createNotFoundError(customerId)` can supply a domain-specific error without forking the algorithm. |
|
|
109
110
|
| `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape used by lifecycle-compatible APIs and deferred work helpers. |
|
|
110
111
|
|
|
111
112
|
Permanent Queue failures must opt in with the Queue-specific marker; unrelated `retryable` fields are ignored:
|
|
@@ -480,8 +481,10 @@ Repos with a custom DB error classifier (e.g. odss-mobile) pass `classify` to
|
|
|
480
481
|
### Auth middleware
|
|
481
482
|
|
|
482
483
|
Encodes the shared skeleton (read token header → verify → `getAppInfo` → resolve user id →
|
|
483
|
-
set context, with
|
|
484
|
-
|
|
484
|
+
set context, with configurable reporting and response hooks). Inject your own verify/resolver,
|
|
485
|
+
context-variable names, and failure mode. By default a missing header remains backward compatible
|
|
486
|
+
and calls `verify('')`; set `rejectMissingToken: true` to reject missing/blank input first with
|
|
487
|
+
`AuthTokenMissingError`.
|
|
485
488
|
|
|
486
489
|
`createAuthMiddleware<Env, Verified, Id>` is generic over your Hono `Env`, so `c.set(...)` in
|
|
487
490
|
`setContext` is type-checked against your `Variables`.
|
|
@@ -491,6 +494,7 @@ import { createAuthMiddleware, createIdentityAuthFailureBody } from '@rdlabo/wor
|
|
|
491
494
|
|
|
492
495
|
// AuthGuard: verify + resolve (and provision) the DB user id.
|
|
493
496
|
const userAuth = createAuthMiddleware<AppEnv, UserRecord, number>({
|
|
497
|
+
rejectMissingToken: true,
|
|
494
498
|
verify: (token) => container.firebase.verifyIdToken(token),
|
|
495
499
|
resolveUserId: (record, _c, appInfo) =>
|
|
496
500
|
container.auth.getUserIdFromFirebase(record, appInfo).catch(() => container.auth.createUser(record)),
|
|
@@ -499,18 +503,28 @@ const userAuth = createAuthMiddleware<AppEnv, UserRecord, number>({
|
|
|
499
503
|
c.set('userId', userId);
|
|
500
504
|
c.set('appInfo', appInfo);
|
|
501
505
|
},
|
|
502
|
-
|
|
506
|
+
reportFailure: (error, context, { stage, tokenPresent }) => {
|
|
507
|
+
// Suppress expected credential rejection; report dependency/internal failures without tokens.
|
|
508
|
+
},
|
|
509
|
+
onFailure: (_error, context, { stage }) =>
|
|
503
510
|
context.json(createIdentityAuthFailureBody(), 401),
|
|
504
511
|
});
|
|
505
512
|
|
|
506
513
|
// TokenGuard (login): verify only — omit resolveUserId. Override the failure if needed.
|
|
507
514
|
const tokenAuth = createAuthMiddleware<AppEnv, UserRecord>({
|
|
515
|
+
rejectMissingToken: true,
|
|
508
516
|
verify: (token) => container.firebase.verifyIdToken(token),
|
|
509
517
|
setContext: (c, { verified }) => c.set('userRecord', verified),
|
|
510
518
|
onFailure: (_e, c) => c.json(createIdentityAuthFailureBody(), 401),
|
|
511
519
|
});
|
|
512
520
|
```
|
|
513
521
|
|
|
522
|
+
`reportFailure(error, context, details)` receives only the stage (`token`, `verify`, `appInfo`,
|
|
523
|
+
`resolveUserId`, or `setContext`) and a `tokenPresent` boolean; raw token data is never included in
|
|
524
|
+
`details`. If the hook is omitted, the historical `console.error(error)` behavior remains. A reporting
|
|
525
|
+
hook failure is logged but cannot change the authentication response. `onFailure` receives the same
|
|
526
|
+
details as its third argument and may be asynchronous.
|
|
527
|
+
|
|
514
528
|
Authentication failures use three explicit scopes. Only `identity` permits a client to purge its
|
|
515
529
|
global authenticated session, offline replica boundary, and outbox. `reauthentication` means the
|
|
516
530
|
identity remains valid but a recent sign-in is required; `credential` belongs to a domain feature
|
|
@@ -22,6 +22,18 @@ type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array | JWTVerifyGetKey;
|
|
|
22
22
|
* be verified against Google's rotating public keys.
|
|
23
23
|
*/
|
|
24
24
|
export declare const SECURETOKEN_JWK_URL = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
|
|
25
|
+
/** Expected rejection for Firebase-specific claims that passed JOSE signature/claim verification. */
|
|
26
|
+
export declare class FirebaseIdTokenValidationError extends Error {
|
|
27
|
+
readonly claim: 'subject' | 'exp' | 'iat' | 'auth_time';
|
|
28
|
+
/** Stable machine-readable code for authentication failure classifiers. */
|
|
29
|
+
readonly code = "ERR_FIREBASE_ID_TOKEN_INVALID";
|
|
30
|
+
/**
|
|
31
|
+
* Create a Firebase ID-token validation rejection.
|
|
32
|
+
*
|
|
33
|
+
* @param claim - Firebase-specific claim which failed validation.
|
|
34
|
+
*/
|
|
35
|
+
constructor(claim: 'subject' | 'exp' | 'iat' | 'auth_time');
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
38
|
* Verifies Firebase ID tokens with `jose` RS256 against Google's securetoken JWKS, and
|
|
27
39
|
* optionally looks up or deletes users via the Google Identity Toolkit REST API.
|
|
@@ -8,6 +8,22 @@ import { jwtVerify } from 'jose';
|
|
|
8
8
|
* be verified against Google's rotating public keys.
|
|
9
9
|
*/
|
|
10
10
|
export const SECURETOKEN_JWK_URL = 'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com';
|
|
11
|
+
/** Expected rejection for Firebase-specific claims that passed JOSE signature/claim verification. */
|
|
12
|
+
export class FirebaseIdTokenValidationError extends Error {
|
|
13
|
+
claim;
|
|
14
|
+
/** Stable machine-readable code for authentication failure classifiers. */
|
|
15
|
+
code = 'ERR_FIREBASE_ID_TOKEN_INVALID';
|
|
16
|
+
/**
|
|
17
|
+
* Create a Firebase ID-token validation rejection.
|
|
18
|
+
*
|
|
19
|
+
* @param claim - Firebase-specific claim which failed validation.
|
|
20
|
+
*/
|
|
21
|
+
constructor(claim) {
|
|
22
|
+
super(`Firebase ID token has an invalid ${claim}`);
|
|
23
|
+
this.claim = claim;
|
|
24
|
+
this.name = 'FirebaseIdTokenValidationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
/**
|
|
12
28
|
* Verifies Firebase ID tokens with `jose` RS256 against Google's securetoken JWKS, and
|
|
13
29
|
* optionally looks up or deletes users via the Google Identity Toolkit REST API.
|
|
@@ -74,18 +90,18 @@ export class JoseFirebaseVerifier {
|
|
|
74
90
|
const { payload } = typeof key === 'function' ? await jwtVerify(idToken, key, options) : await jwtVerify(idToken, key, options);
|
|
75
91
|
// Apply Firebase's documented checks beyond signature/iss/aud/exp.
|
|
76
92
|
if (!payload.sub || typeof payload.sub !== 'string' || payload.sub.length > 128) {
|
|
77
|
-
throw new
|
|
93
|
+
throw new FirebaseIdTokenValidationError('subject');
|
|
78
94
|
}
|
|
79
95
|
if (!Number.isFinite(payload.exp)) {
|
|
80
|
-
throw new
|
|
96
|
+
throw new FirebaseIdTokenValidationError('exp');
|
|
81
97
|
}
|
|
82
98
|
const issuedAt = payload.iat;
|
|
83
99
|
if (typeof issuedAt !== 'number' || !Number.isFinite(issuedAt) || issuedAt > now) {
|
|
84
|
-
throw new
|
|
100
|
+
throw new FirebaseIdTokenValidationError('iat');
|
|
85
101
|
}
|
|
86
102
|
const authTime = payload.auth_time;
|
|
87
103
|
if (typeof authTime !== 'number' || !Number.isFinite(authTime) || authTime > now) {
|
|
88
|
-
throw new
|
|
104
|
+
throw new FirebaseIdTokenValidationError('auth_time');
|
|
89
105
|
}
|
|
90
106
|
return { ...payload, uid: payload.sub, email: payload.email };
|
|
91
107
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export { validate, createValidate } from './middleware/validation.js';
|
|
|
14
14
|
export { createSentryValidate } from './middleware/validation.js';
|
|
15
15
|
export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, SentryScopeLike, } from './middleware/validation.js';
|
|
16
16
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
17
|
-
export { createAuthMiddleware } from './middleware/auth.js';
|
|
18
|
-
export type { AuthMiddlewareOptions } from './middleware/auth.js';
|
|
17
|
+
export { AuthTokenMissingError, createAuthMiddleware } from './middleware/auth.js';
|
|
18
|
+
export type { AuthMiddlewareFailureDetails, AuthMiddlewareFailureStage, AuthMiddlewareOptions, } from './middleware/auth.js';
|
|
19
19
|
export { perfLog } from './middleware/perf-log.js';
|
|
20
20
|
export type { PerfLogOptions, AnalyticsEngineDatasetLike } from './middleware/perf-log.js';
|
|
21
21
|
export { createMaintenanceMiddleware, createMaintenanceWaitHandler, isMaintenanceEnabled, MAINTENANCE_BODY, MAINTENANCE_CODE, MAINTENANCE_WAIT_PATH, } from './middleware/maintenance.js';
|
|
@@ -90,7 +90,7 @@ export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
|
|
|
90
90
|
export { getTemporaryCredentials } from './aws/sts.js';
|
|
91
91
|
export type { GetTemporaryCredentialsOptions, StsCredentials } from './aws/sts.js';
|
|
92
92
|
export type { DecodedIdToken, FirebaseVerifier } from './firebase/firebase-verifier.js';
|
|
93
|
-
export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
|
|
93
|
+
export { FirebaseIdTokenValidationError, JoseFirebaseVerifier, SECURETOKEN_JWK_URL, } from './firebase/jose-firebase-verifier.js';
|
|
94
94
|
export { IdentityToolkit } from './firebase/identity-toolkit.js';
|
|
95
95
|
export type { ServiceAccount } from './firebase/identity-toolkit.js';
|
|
96
96
|
export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier.js';
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { validate, createValidate } from './middleware/validation.js';
|
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional public re-export
|
|
17
17
|
export { createSentryValidate } from './middleware/validation.js';
|
|
18
18
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
19
|
-
export { createAuthMiddleware } from './middleware/auth.js';
|
|
19
|
+
export { AuthTokenMissingError, createAuthMiddleware } from './middleware/auth.js';
|
|
20
20
|
export { perfLog } from './middleware/perf-log.js';
|
|
21
21
|
export { createMaintenanceMiddleware, createMaintenanceWaitHandler, isMaintenanceEnabled, MAINTENANCE_BODY, MAINTENANCE_CODE, MAINTENANCE_WAIT_PATH, } from './middleware/maintenance.js';
|
|
22
22
|
export { createIsolateMemo } from './container/isolate-memo.js';
|
|
@@ -67,6 +67,6 @@ export { createAiGatewayProvider } from './ai/gateway.js';
|
|
|
67
67
|
export { getAuthenticationSecret } from './aws/secrets-manager.js';
|
|
68
68
|
export { getCloudFrontSignedUrl } from './aws/cloudfront.js';
|
|
69
69
|
export { getTemporaryCredentials } from './aws/sts.js';
|
|
70
|
-
export { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './firebase/jose-firebase-verifier.js';
|
|
70
|
+
export { FirebaseIdTokenValidationError, JoseFirebaseVerifier, SECURETOKEN_JWK_URL, } from './firebase/jose-firebase-verifier.js';
|
|
71
71
|
export { IdentityToolkit } from './firebase/identity-toolkit.js';
|
|
72
72
|
export { createRemoteFirebaseVerifier, createServiceAccountVerifier } from './firebase/remote-verifier.js';
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { Context, Env, MiddlewareHandler } from 'hono';
|
|
2
2
|
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
3
|
import type { AppInfo } from '../http/app-info.js';
|
|
4
|
+
/** Processing stage at which authentication middleware failed. */
|
|
5
|
+
export type AuthMiddlewareFailureStage = 'token' | 'verify' | 'appInfo' | 'resolveUserId' | 'setContext';
|
|
6
|
+
/** Safe metadata describing an authentication middleware failure without including the token. */
|
|
7
|
+
export interface AuthMiddlewareFailureDetails {
|
|
8
|
+
/** Stage which rejected or failed. */
|
|
9
|
+
stage: AuthMiddlewareFailureStage;
|
|
10
|
+
/** Whether the configured token header contained a non-blank value. */
|
|
11
|
+
tokenPresent: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Expected rejection raised when the configured authentication header is absent or blank. */
|
|
14
|
+
export declare class AuthTokenMissingError extends Error {
|
|
15
|
+
/** Stable machine-readable code for application classifiers. */
|
|
16
|
+
readonly code = "AUTH_TOKEN_MISSING";
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
4
19
|
/**
|
|
5
20
|
* Configuration for {@link createAuthMiddleware}.
|
|
6
21
|
*
|
|
@@ -11,10 +26,19 @@ import type { AppInfo } from '../http/app-info.js';
|
|
|
11
26
|
export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
12
27
|
/** Header carrying the ID token. Defaults to `'x-amz-security-token'`. */
|
|
13
28
|
tokenHeader?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Reject an absent or blank token before calling {@link AuthMiddlewareOptions.verify}.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Defaults to `false` for backward compatibility: existing consumers historically receive an
|
|
34
|
+
* empty string in `verify` when the header is absent. Enable this when the application wants a
|
|
35
|
+
* typed {@link AuthTokenMissingError} and does not use an empty token as custom input.
|
|
36
|
+
*/
|
|
37
|
+
rejectMissingToken?: boolean;
|
|
14
38
|
/**
|
|
15
39
|
* Verify the raw token and return the decoded value or user record.
|
|
16
40
|
*
|
|
17
|
-
* @param token - The raw token
|
|
41
|
+
* @param token - The raw token, or an empty string when absent unless `rejectMissingToken` is enabled.
|
|
18
42
|
* @param c - The current Hono context.
|
|
19
43
|
* @returns The verified value passed to {@link AuthMiddlewareOptions.resolveUserId}/{@link AuthMiddlewareOptions.setContext}.
|
|
20
44
|
* @throws If the token is invalid; rejecting/throwing triggers the failure path.
|
|
@@ -59,7 +83,17 @@ export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
|
59
83
|
* @param c - The current Hono context.
|
|
60
84
|
* @returns The failure response to send.
|
|
61
85
|
*/
|
|
62
|
-
onFailure?: (err: unknown, c: Context<E
|
|
86
|
+
onFailure?: (err: unknown, c: Context<E>, details: AuthMiddlewareFailureDetails) => Response | Promise<Response>;
|
|
87
|
+
/**
|
|
88
|
+
* Report a failed authentication attempt.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* When omitted, the historical behavior (`console.error(err)`) is preserved. Applications should
|
|
92
|
+
* provide this hook to suppress expected credential rejections while reporting dependency and
|
|
93
|
+
* internal failures through their normal observability path. The hook must never include raw
|
|
94
|
+
* authentication tokens in logs or telemetry.
|
|
95
|
+
*/
|
|
96
|
+
reportFailure?: (err: unknown, c: Context<E>, details: AuthMiddlewareFailureDetails) => void | Promise<void>;
|
|
63
97
|
/** Status used by the default `onFailure`. Defaults to `403`. */
|
|
64
98
|
failureStatus?: ContentfulStatusCode;
|
|
65
99
|
/** Message used by the default `onFailure`. Defaults to `'Forbidden resource'`. */
|
|
@@ -69,7 +103,7 @@ export interface AuthMiddlewareOptions<E extends Env, Verified, Id = unknown> {
|
|
|
69
103
|
* Create an authentication middleware equivalent to a NestJS `AuthGuard` / `TokenGuard`.
|
|
70
104
|
*
|
|
71
105
|
* The middleware runs a fixed skeleton — read the token header, `verify`, `getAppInfo`,
|
|
72
|
-
* `resolveUserId`, `setContext`, and on error `
|
|
106
|
+
* `resolveUserId`, `setContext`, and on error `reportFailure` then `onFailure` — while the
|
|
73
107
|
* application injects the variable parts (token verification, user-id resolution, context variable
|
|
74
108
|
* names, and the failure response). Omitting {@link AuthMiddlewareOptions.resolveUserId} yields a
|
|
75
109
|
* token-only middleware.
|
package/dist/middleware/auth.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { HTTPException } from 'hono/http-exception';
|
|
2
2
|
import { getAppInfo } from '../http/app-info.js';
|
|
3
|
+
/** Expected rejection raised when the configured authentication header is absent or blank. */
|
|
4
|
+
export class AuthTokenMissingError extends Error {
|
|
5
|
+
/** Stable machine-readable code for application classifiers. */
|
|
6
|
+
code = 'AUTH_TOKEN_MISSING';
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Authentication token is missing');
|
|
9
|
+
this.name = 'AuthTokenMissingError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* Create an authentication middleware equivalent to a NestJS `AuthGuard` / `TokenGuard`.
|
|
5
14
|
*
|
|
6
15
|
* The middleware runs a fixed skeleton — read the token header, `verify`, `getAppInfo`,
|
|
7
|
-
* `resolveUserId`, `setContext`, and on error `
|
|
16
|
+
* `resolveUserId`, `setContext`, and on error `reportFailure` then `onFailure` — while the
|
|
8
17
|
* application injects the variable parts (token verification, user-id resolution, context variable
|
|
9
18
|
* names, and the failure response). Omitting {@link AuthMiddlewareOptions.resolveUserId} yields a
|
|
10
19
|
* token-only middleware.
|
|
@@ -30,22 +39,42 @@ import { getAppInfo } from '../http/app-info.js';
|
|
|
30
39
|
* ```
|
|
31
40
|
*/
|
|
32
41
|
export function createAuthMiddleware(options) {
|
|
33
|
-
const { tokenHeader = 'x-amz-security-token', verify, resolveUserId, setContext, onFailure, failureStatus = 403, failureMessage = 'Forbidden resource', } = options;
|
|
42
|
+
const { tokenHeader = 'x-amz-security-token', rejectMissingToken = false, verify, resolveUserId, setContext, onFailure, reportFailure, failureStatus = 403, failureMessage = 'Forbidden resource', } = options;
|
|
34
43
|
return async (c, next) => {
|
|
44
|
+
let stage = 'token';
|
|
45
|
+
let tokenPresent = false;
|
|
35
46
|
try {
|
|
36
47
|
const token = c.req.header(tokenHeader) ?? '';
|
|
48
|
+
tokenPresent = token.trim().length > 0;
|
|
49
|
+
if (!tokenPresent && rejectMissingToken) {
|
|
50
|
+
throw new AuthTokenMissingError();
|
|
51
|
+
}
|
|
52
|
+
stage = 'verify';
|
|
37
53
|
const verified = await verify(token, c);
|
|
54
|
+
stage = 'appInfo';
|
|
38
55
|
const appInfo = getAppInfo(c);
|
|
56
|
+
stage = 'resolveUserId';
|
|
39
57
|
const userId = resolveUserId ? await resolveUserId(verified, c, appInfo) : undefined;
|
|
58
|
+
stage = 'setContext';
|
|
40
59
|
setContext(c, { verified, appInfo, userId });
|
|
41
60
|
}
|
|
42
61
|
catch (e) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
const details = { stage, tokenPresent };
|
|
63
|
+
if (reportFailure) {
|
|
64
|
+
try {
|
|
65
|
+
await reportFailure(e, c, details);
|
|
66
|
+
}
|
|
67
|
+
catch (reportingError) {
|
|
68
|
+
// Observability must never alter the authentication response.
|
|
69
|
+
console.error(reportingError);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Preserve the historical default for consumers that have not adopted classified reporting.
|
|
74
|
+
console.error(e);
|
|
75
|
+
}
|
|
47
76
|
if (onFailure) {
|
|
48
|
-
return onFailure(e, c);
|
|
77
|
+
return onFailure(e, c, details);
|
|
49
78
|
}
|
|
50
79
|
throw new HTTPException(failureStatus, { message: failureMessage });
|
|
51
80
|
}
|
|
@@ -49,6 +49,6 @@ export async function assertStripeCustomerUpdated(options) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
if ((await options.countCustomer(options.productId, options.customerId)) === 0) {
|
|
52
|
-
throw new Error(`Customer not found: ${options.customerId}`);
|
|
52
|
+
throw options.createNotFoundError?.(options.customerId) ?? new Error(`Customer not found: ${options.customerId}`);
|
|
53
53
|
}
|
|
54
54
|
}
|