@open-mercato/checkout 0.6.8-develop.6962.1.bd029a2bb2 → 0.6.8-develop.6964.1.36b364cfd8
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/dist/modules/checkout/api/pay/[slug]/route.js +10 -9
- package/dist/modules/checkout/api/pay/[slug]/route.js.map +2 -2
- package/dist/modules/checkout/api/pay/[slug]/status/[transactionId]/route.js +10 -9
- package/dist/modules/checkout/api/pay/[slug]/status/[transactionId]/route.js.map +2 -2
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js +20 -10
- package/dist/modules/checkout/api/pay/[slug]/submit/route.js.map +2 -2
- package/dist/modules/checkout/api/pay/[slug]/verify-password/route.js +20 -10
- package/dist/modules/checkout/api/pay/[slug]/verify-password/route.js.map +2 -2
- package/dist/modules/checkout/lib/rateLimiter.js +45 -2
- package/dist/modules/checkout/lib/rateLimiter.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/checkout/api/pay/[slug]/route.ts +11 -11
- package/src/modules/checkout/api/pay/[slug]/status/[transactionId]/route.ts +11 -11
- package/src/modules/checkout/api/pay/[slug]/submit/__tests__/route.test.ts +99 -0
- package/src/modules/checkout/api/pay/[slug]/submit/route.ts +20 -11
- package/src/modules/checkout/api/pay/[slug]/verify-password/__tests__/route.test.ts +121 -0
- package/src/modules/checkout/api/pay/[slug]/verify-password/route.ts +20 -11
- package/src/modules/checkout/api/pay/__tests__/openapi-rate-limit-responses.test.ts +41 -0
- package/src/modules/checkout/lib/__tests__/rateLimiter.test.ts +117 -1
- package/src/modules/checkout/lib/rateLimiter.ts +85 -1
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
1
3
|
import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
checkRateLimit,
|
|
6
|
+
getClientIp,
|
|
7
|
+
RATE_LIMIT_FALLBACK_KEY,
|
|
8
|
+
RATE_LIMIT_UNAVAILABLE_FALLBACK,
|
|
9
|
+
} from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
3
10
|
import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
|
|
11
|
+
import type { RateLimitConfig } from '@open-mercato/shared/lib/ratelimit/types'
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('checkout')
|
|
4
14
|
|
|
5
15
|
export function buildCheckoutRateLimitKey(
|
|
6
16
|
req: Request,
|
|
@@ -11,6 +21,80 @@ export function buildCheckoutRateLimitKey(
|
|
|
11
21
|
return `${namespace}:${clientKey}`
|
|
12
22
|
}
|
|
13
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `fail-closed` rejects with 503 when the limiter cannot decide, for endpoints where an
|
|
26
|
+
* unenforced limit is worse than a rejected request. `fail-open` lets the request through
|
|
27
|
+
* and only logs, for read-only endpoints.
|
|
28
|
+
*/
|
|
29
|
+
export type CheckoutRateLimitPosture = 'fail-open' | 'fail-closed'
|
|
30
|
+
|
|
31
|
+
type RateLimiterContainer = {
|
|
32
|
+
resolve: (name: string) => unknown
|
|
33
|
+
hasRegistration?: (name: string) => boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `rateLimiterService` is registered conditionally — `getCachedRateLimiterService()` returns
|
|
38
|
+
* null when the rate-limit configuration itself is invalid, and then the DI key never exists.
|
|
39
|
+
* That is a deployment-time misconfiguration rather than a transient outage, so it is kept
|
|
40
|
+
* distinct from a limiter that is registered but cannot decide.
|
|
41
|
+
*/
|
|
42
|
+
function resolveOptionalRateLimiter(container: RateLimiterContainer): RateLimiterService | null {
|
|
43
|
+
if (typeof container.hasRegistration === 'function' && !container.hasRegistration('rateLimiterService')) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return (container.resolve('rateLimiterService') as RateLimiterService | undefined) ?? null
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run the checkout rate-limit guard for one request and return the response that should be
|
|
55
|
+
* sent instead of handling it, or null to continue.
|
|
56
|
+
*
|
|
57
|
+
* Three outcomes are deliberately distinguished:
|
|
58
|
+
* - the limiter is not configured at all → logged as a configuration error and the check is
|
|
59
|
+
* skipped, matching `sales/quotes/accept`; the endpoint is never reported as temporarily
|
|
60
|
+
* unavailable for a condition that lasts until an operator fixes the environment
|
|
61
|
+
* - the limiter is configured but could not decide (degraded, or the check threw) → 503 under
|
|
62
|
+
* `fail-closed`, a logged warning under `fail-open`
|
|
63
|
+
* - the limiter decided the quota is exhausted → the 429 from `checkRateLimit`
|
|
64
|
+
*/
|
|
65
|
+
export async function enforceCheckoutRateLimit(options: {
|
|
66
|
+
req: Request
|
|
67
|
+
container: RateLimiterContainer
|
|
68
|
+
config: RateLimitConfig
|
|
69
|
+
namespace: string
|
|
70
|
+
errorMessage: string
|
|
71
|
+
posture: CheckoutRateLimitPosture
|
|
72
|
+
}): Promise<NextResponse | null> {
|
|
73
|
+
const { req, container, config, namespace, errorMessage, posture } = options
|
|
74
|
+
const rateLimiter = resolveOptionalRateLimiter(container)
|
|
75
|
+
if (!rateLimiter) {
|
|
76
|
+
logger.error('Rate limiter service is not registered — check RATE_LIMIT_* configuration; checkout rate limiting is not enforced', {
|
|
77
|
+
namespace,
|
|
78
|
+
posture,
|
|
79
|
+
})
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const key = buildCheckoutRateLimitKey(req, rateLimiter, namespace)
|
|
85
|
+
return await checkRateLimit(rateLimiter, config, key, errorMessage, {
|
|
86
|
+
failClosed: posture === 'fail-closed',
|
|
87
|
+
})
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (posture === 'fail-closed') {
|
|
90
|
+
logger.error('Checkout rate limit check failed, rejecting request', { err: error, namespace })
|
|
91
|
+
return NextResponse.json({ error: RATE_LIMIT_UNAVAILABLE_FALLBACK }, { status: 503 })
|
|
92
|
+
}
|
|
93
|
+
logger.warn('Checkout rate limit check failed, allowing request', { err: error, namespace })
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
14
98
|
export const checkoutPublicViewRateLimitConfig = readEndpointRateLimitConfig('CHECKOUT_PUBLIC_VIEW', {
|
|
15
99
|
points: 60,
|
|
16
100
|
duration: 60,
|