@spring-systems/server 0.8.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/CHANGELOG.md +62 -0
- package/LICENSE +8 -0
- package/README.md +94 -0
- package/dist/api-route-handler.d.ts +49 -0
- package/dist/api-route-handler.js +19 -0
- package/dist/api-route-handler.js.map +1 -0
- package/dist/chunk-7IUSTA5W.js +113 -0
- package/dist/chunk-7IUSTA5W.js.map +1 -0
- package/dist/chunk-CLZU34DG.js +465 -0
- package/dist/chunk-CLZU34DG.js.map +1 -0
- package/dist/chunk-CP33WQ5Q.js +47 -0
- package/dist/chunk-CP33WQ5Q.js.map +1 -0
- package/dist/chunk-FEB3UZEG.js +407 -0
- package/dist/chunk-FEB3UZEG.js.map +1 -0
- package/dist/chunk-KA7RJCWA.js +24 -0
- package/dist/chunk-KA7RJCWA.js.map +1 -0
- package/dist/chunk-OYTV4D7E.js +159 -0
- package/dist/chunk-OYTV4D7E.js.map +1 -0
- package/dist/chunk-YV6DZVPI.js +43 -0
- package/dist/chunk-YV6DZVPI.js.map +1 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +14 -0
- package/dist/client.js.map +1 -0
- package/dist/handlers/index.d.ts +81 -0
- package/dist/handlers/index.js +48 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/next-adapters.d.ts +25 -0
- package/dist/next-adapters.js +14 -0
- package/dist/next-adapters.js.map +1 -0
- package/dist/proxy-middleware.d.ts +8 -0
- package/dist/proxy-middleware.js +10 -0
- package/dist/proxy-middleware.js.map +1 -0
- package/dist/rate-limiter.d.ts +67 -0
- package/dist/rate-limiter.js +15 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/runtime-env.d.ts +15 -0
- package/dist/runtime-env.js +9 -0
- package/dist/runtime-env.js.map +1 -0
- package/dist/security-headers.d.ts +8 -0
- package/dist/security-headers.js +11 -0
- package/dist/security-headers.js.map +1 -0
- package/package.json +114 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @spring-systems/server
|
|
2
|
+
|
|
3
|
+
## 0.7.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Restored backward-compatible default for `createNextUIAdapter()` dynamic imports (`ssr: true` by default).
|
|
8
|
+
- Added `createClientOnlyNextUIAdapter()` helper for client-only apps (`ssr: false` by default).
|
|
9
|
+
- Added a dev-time warning when `createNextUIAdapter()` is used without explicit `defaultDynamicSsr`, to prevent accidental SSR defaults in client-only apps.
|
|
10
|
+
- Restricted `@spring-systems/server/client` exports to client-safe factories only (`createNextRouteAdapter`, `createClientOnlyNextUIAdapter`).
|
|
11
|
+
- Applied CORS headers consistently for proxy error responses (including non-logout routes).
|
|
12
|
+
- Enforced server-only root entrypoint via `import "server-only"`; client-safe adapters remain available under `@spring-systems/server/client`.
|
|
13
|
+
- Updated docs to use explicit `defaultDynamicSsr` in SSR-oriented adapter examples.
|
|
14
|
+
- Added dedicated `@spring-systems/server/client` export for client-only adapter imports.
|
|
15
|
+
- Refactored API route internals by extracting shared utility helpers from `api-route-handler`.
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @spring-systems/core@0.7.5
|
|
18
|
+
- @spring-systems/ui@0.7.5
|
|
19
|
+
|
|
20
|
+
## 0.7.4
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Dependency updates and version alignment.
|
|
25
|
+
- Updated dependencies
|
|
26
|
+
- @spring-systems/core@0.7.4
|
|
27
|
+
- @spring-systems/ui@0.7.4
|
|
28
|
+
|
|
29
|
+
## 0.7.3
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies
|
|
34
|
+
- @spring-systems/ui@0.7.3
|
|
35
|
+
- @spring-systems/core@0.7.3
|
|
36
|
+
|
|
37
|
+
## 0.7.2
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- Improve package documentation, onboarding navigation, and npm-published docs coverage.
|
|
42
|
+
- Updated dependencies
|
|
43
|
+
- @spring-systems/core@0.7.2
|
|
44
|
+
- @spring-systems/ui@0.7.2
|
|
45
|
+
|
|
46
|
+
## 0.7.1
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- Align package documentation with repository docs and enforce docs quality checks in release workflow.
|
|
51
|
+
- Updated dependencies
|
|
52
|
+
- @spring-systems/core@0.7.1
|
|
53
|
+
- @spring-systems/ui@0.7.1
|
|
54
|
+
|
|
55
|
+
## 0.7.0
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Initial npm registry release
|
|
60
|
+
- Updated dependencies
|
|
61
|
+
- @spring-systems/core@0.7.0
|
|
62
|
+
- @spring-systems/ui@0.7.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2024-2026 The Authors
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation are proprietary and confidential.
|
|
6
|
+
No rights are granted except as expressly agreed in writing by the copyright holder(s).
|
|
7
|
+
Use, copying, modification, distribution, sublicensing, publication, or disclosure
|
|
8
|
+
without prior written permission is prohibited.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @spring-systems/server
|
|
2
|
+
|
|
3
|
+
Next.js server-side integration for the Spring Systems SPRING framework. This package keeps the browser-to-backend boundary explicit: proxying, runtime env exposure, security headers, and server adapters live here instead of leaking into app code.
|
|
4
|
+
|
|
5
|
+
## When To Use It
|
|
6
|
+
|
|
7
|
+
Use this package when the app should talk to the backend through Next.js rather than directly from the browser.
|
|
8
|
+
|
|
9
|
+
- use it for proxy routes, runtime environment bootstrap, server adapters, and security headers
|
|
10
|
+
- skip it for client-side rendering or reusable UI behavior
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @spring-systems/server @spring-systems/core @spring-systems/ui
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Three files wire the server layer into a Next.js 16 app:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// src/proxy.ts — replaces middleware.ts in Next.js 16
|
|
24
|
+
import "@/project-config";
|
|
25
|
+
export { proxy } from "@spring-systems/server/proxy";
|
|
26
|
+
|
|
27
|
+
export const config = {
|
|
28
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)"],
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// app/api/[...path]/route.ts
|
|
34
|
+
export { GET, POST, PUT, DELETE, PATCH, OPTIONS } from "@spring-systems/server/api-handler";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// app/layout.tsx
|
|
39
|
+
import { RuntimeEnvScript } from "@spring-systems/server/runtime-env";
|
|
40
|
+
import { createNextRouteAdapter, createNextUIAdapter } from "@spring-systems/server/adapters";
|
|
41
|
+
import { SpringProvider } from "@spring-systems/ui/components";
|
|
42
|
+
|
|
43
|
+
export default function RootLayout({ children }) {
|
|
44
|
+
return (
|
|
45
|
+
<html>
|
|
46
|
+
<body>
|
|
47
|
+
<RuntimeEnvScript />
|
|
48
|
+
<SpringProvider
|
|
49
|
+
config={{}}
|
|
50
|
+
adapters={{
|
|
51
|
+
route: createNextRouteAdapter(),
|
|
52
|
+
ui: createNextUIAdapter({ defaultDynamicSsr: true }),
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</SpringProvider>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `@/project-config` side-effect import must come first — it registers framework settings (CSP, blocked paths, capabilities) before the proxy reads them.
|
|
64
|
+
|
|
65
|
+
## Entry Points
|
|
66
|
+
|
|
67
|
+
| Import path | Use it for |
|
|
68
|
+
| ----------------------------------------- | ------------------------------- |
|
|
69
|
+
| `@spring-systems/server/api-handler` | API route handlers |
|
|
70
|
+
| `@spring-systems/server/runtime-env` | Runtime environment bootstrap |
|
|
71
|
+
| `@spring-systems/server/adapters` | Next.js adapter factories |
|
|
72
|
+
| `@spring-systems/server/client` | Client-safe adapter helpers |
|
|
73
|
+
| `@spring-systems/server/proxy` | Proxy middleware |
|
|
74
|
+
| `@spring-systems/server/rate-limiter` | Rate limiting helpers |
|
|
75
|
+
| `@spring-systems/server/handlers` | Shared request handlers |
|
|
76
|
+
| `@spring-systems/server/security-headers` | CSP and security header helpers |
|
|
77
|
+
|
|
78
|
+
## Compatibility
|
|
79
|
+
|
|
80
|
+
Requires **Next.js 16** and **React 19**. Peer dependencies: `@spring-systems/core`, `@spring-systems/ui`.
|
|
81
|
+
|
|
82
|
+
## Boundary Rules
|
|
83
|
+
|
|
84
|
+
- keep upstream API communication behind this package when deployment relies on cookie or CSRF protections
|
|
85
|
+
- do not move browser rendering concerns here just because the consuming app uses Next.js
|
|
86
|
+
- for runtime and deployment rules see [ENVIRONMENT_VARIABLES.md](https://bitbucket.org/springsystems-projects/spring-framework-frontend/blob/main/docs/ENVIRONMENT_VARIABLES.md) and [SECURITY.md](https://bitbucket.org/springsystems-projects/spring-framework-frontend/blob/main/docs/SECURITY.md) in the monorepo docs
|
|
87
|
+
|
|
88
|
+
## Changelog
|
|
89
|
+
|
|
90
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history and breaking changes.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
UNLICENSED
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API proxy route handler for Next.js App Router.
|
|
5
|
+
*
|
|
6
|
+
* Proxies requests from the frontend to the backend API, handling:
|
|
7
|
+
* - Session token management (cookie-based auth)
|
|
8
|
+
* - CSRF protection and CORS headers
|
|
9
|
+
* - Login rate limiting
|
|
10
|
+
* - Request body size limits
|
|
11
|
+
* - Path normalization and version deduplication
|
|
12
|
+
*
|
|
13
|
+
* @module api-route-handler
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Handle GET requests through the API proxy. */
|
|
17
|
+
declare function GET(request: NextRequest, context: {
|
|
18
|
+
params: Promise<{
|
|
19
|
+
path: string[];
|
|
20
|
+
}>;
|
|
21
|
+
}): Promise<NextResponse<unknown>>;
|
|
22
|
+
/** Handle POST requests through the API proxy. */
|
|
23
|
+
declare function POST(request: NextRequest, context: {
|
|
24
|
+
params: Promise<{
|
|
25
|
+
path: string[];
|
|
26
|
+
}>;
|
|
27
|
+
}): Promise<NextResponse<unknown>>;
|
|
28
|
+
/** Handle PUT requests through the API proxy. */
|
|
29
|
+
declare function PUT(request: NextRequest, context: {
|
|
30
|
+
params: Promise<{
|
|
31
|
+
path: string[];
|
|
32
|
+
}>;
|
|
33
|
+
}): Promise<NextResponse<unknown>>;
|
|
34
|
+
/** Handle DELETE requests through the API proxy. */
|
|
35
|
+
declare function DELETE(request: NextRequest, context: {
|
|
36
|
+
params: Promise<{
|
|
37
|
+
path: string[];
|
|
38
|
+
}>;
|
|
39
|
+
}): Promise<NextResponse<unknown>>;
|
|
40
|
+
/** Handle PATCH requests through the API proxy. */
|
|
41
|
+
declare function PATCH(request: NextRequest, context: {
|
|
42
|
+
params: Promise<{
|
|
43
|
+
path: string[];
|
|
44
|
+
}>;
|
|
45
|
+
}): Promise<NextResponse<unknown>>;
|
|
46
|
+
/** Handle CORS preflight requests. */
|
|
47
|
+
declare function OPTIONS(request: NextRequest): Promise<NextResponse<unknown>>;
|
|
48
|
+
|
|
49
|
+
export { DELETE, GET, OPTIONS, PATCH, POST, PUT };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DELETE,
|
|
3
|
+
GET,
|
|
4
|
+
OPTIONS,
|
|
5
|
+
PATCH,
|
|
6
|
+
POST,
|
|
7
|
+
PUT
|
|
8
|
+
} from "./chunk-CLZU34DG.js";
|
|
9
|
+
import "./chunk-FEB3UZEG.js";
|
|
10
|
+
import "./chunk-7IUSTA5W.js";
|
|
11
|
+
export {
|
|
12
|
+
DELETE,
|
|
13
|
+
GET,
|
|
14
|
+
OPTIONS,
|
|
15
|
+
PATCH,
|
|
16
|
+
POST,
|
|
17
|
+
PUT
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=api-route-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/rate-limiter.ts
|
|
2
|
+
var InMemoryRateLimiter = class {
|
|
3
|
+
ipStore = /* @__PURE__ */ new Map();
|
|
4
|
+
accountStore = /* @__PURE__ */ new Map();
|
|
5
|
+
getStore(store) {
|
|
6
|
+
return store === "ip" ? this.ipStore : this.accountStore;
|
|
7
|
+
}
|
|
8
|
+
get(store, key) {
|
|
9
|
+
return this.getStore(store).get(key) ?? null;
|
|
10
|
+
}
|
|
11
|
+
set(store, key, entry) {
|
|
12
|
+
this.getStore(store).set(key, entry);
|
|
13
|
+
}
|
|
14
|
+
delete(store, key) {
|
|
15
|
+
this.getStore(store).delete(key);
|
|
16
|
+
}
|
|
17
|
+
size(store) {
|
|
18
|
+
return this.getStore(store).size;
|
|
19
|
+
}
|
|
20
|
+
evictOldest(store, count) {
|
|
21
|
+
const map = this.getStore(store);
|
|
22
|
+
const entries = [...map.entries()].sort((a, b) => a[1].windowStartedAt - b[1].windowStartedAt);
|
|
23
|
+
for (let i = 0; i < count && i < entries.length; i++) {
|
|
24
|
+
const candidate = entries[i];
|
|
25
|
+
if (!candidate) break;
|
|
26
|
+
map.delete(candidate[0]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
sweepExpired(store, now, windowMs) {
|
|
30
|
+
const map = this.getStore(store);
|
|
31
|
+
for (const [key, entry] of map.entries()) {
|
|
32
|
+
const windowExpired = now - entry.windowStartedAt > windowMs;
|
|
33
|
+
const noActiveBlock = entry.blockedUntil <= now;
|
|
34
|
+
if (windowExpired && noActiveBlock) {
|
|
35
|
+
map.delete(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var adapter = new InMemoryRateLimiter();
|
|
41
|
+
var hasWarnedInMemory = false;
|
|
42
|
+
function setRateLimiterAdapter(custom) {
|
|
43
|
+
adapter = custom;
|
|
44
|
+
}
|
|
45
|
+
function getRateLimiterAdapter() {
|
|
46
|
+
return adapter;
|
|
47
|
+
}
|
|
48
|
+
function checkRateLimit(ipKey, accountKey, policy) {
|
|
49
|
+
if (!hasWarnedInMemory && adapter instanceof InMemoryRateLimiter && process.env.NODE_ENV === "production") {
|
|
50
|
+
hasWarnedInMemory = true;
|
|
51
|
+
console.warn("[server] Rate limiter is using in-memory storage in production. For multi-instance deployments, call setRateLimiterAdapter() with a shared-storage adapter.");
|
|
52
|
+
}
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const ipResult = checkSingleLimit(adapter, "ip", ipKey, policy.maxAttemptsByIpAndAccount, policy, now);
|
|
55
|
+
if (ipResult) return ipResult;
|
|
56
|
+
if (accountKey) {
|
|
57
|
+
const accountResult = checkSingleLimit(adapter, "account", accountKey, policy.maxAttemptsByAccount, policy, now);
|
|
58
|
+
if (accountResult) return accountResult;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function recordFailedAttempt(ipKey, accountKey, policy) {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
recordAttempt(adapter, "ip", ipKey, policy, now);
|
|
65
|
+
if (accountKey) {
|
|
66
|
+
recordAttempt(adapter, "account", accountKey, policy, now);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function clearRateLimitEntries(ipKey, accountKey) {
|
|
70
|
+
adapter.delete("ip", ipKey);
|
|
71
|
+
if (accountKey) {
|
|
72
|
+
adapter.delete("account", accountKey);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function checkSingleLimit(adap, store, key, maxAttempts, policy, now) {
|
|
76
|
+
const entry = adap.get(store, key);
|
|
77
|
+
if (!entry) return null;
|
|
78
|
+
if (entry.blockedUntil > now) {
|
|
79
|
+
const remainSec = Math.ceil((entry.blockedUntil - now) / 1e3);
|
|
80
|
+
return `Rate limited (${store}). Try again in ${remainSec}s.`;
|
|
81
|
+
}
|
|
82
|
+
if (now - entry.windowStartedAt > policy.windowMs) {
|
|
83
|
+
adap.delete(store, key);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (entry.count >= maxAttempts) {
|
|
87
|
+
entry.blockedUntil = now + policy.blockMs;
|
|
88
|
+
adap.set(store, key, entry);
|
|
89
|
+
return `Too many attempts (${store}). Blocked for ${Math.ceil(policy.blockMs / 1e3)}s.`;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function recordAttempt(adap, store, key, policy, now) {
|
|
94
|
+
if (adap.size(store) >= policy.maxKeys) {
|
|
95
|
+
adap.evictOldest(store, Math.floor(policy.maxKeys * 0.1));
|
|
96
|
+
}
|
|
97
|
+
const entry = adap.get(store, key);
|
|
98
|
+
if (!entry || now - entry.windowStartedAt > policy.windowMs) {
|
|
99
|
+
adap.set(store, key, { count: 1, windowStartedAt: now, blockedUntil: 0 });
|
|
100
|
+
} else {
|
|
101
|
+
entry.count++;
|
|
102
|
+
adap.set(store, key, entry);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
setRateLimiterAdapter,
|
|
108
|
+
getRateLimiterAdapter,
|
|
109
|
+
checkRateLimit,
|
|
110
|
+
recordFailedAttempt,
|
|
111
|
+
clearRateLimitEntries
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=chunk-7IUSTA5W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rate-limiter.ts"],"sourcesContent":["/**\n * Pluggable rate limiter for API proxy authentication.\n *\n * The default implementation uses in-memory Maps (suitable for single-instance deployments).\n * Multi-instance deployments can replace this with a custom adapter via `setRateLimiterAdapter()`.\n *\n * @example\n * ```ts\n * import { setRateLimiterAdapter } from \"@spring-systems/server/rate-limiter\";\n * import { createCustomRateLimiter } from \"./my-rate-limiter\";\n *\n * setRateLimiterAdapter(createCustomRateLimiter());\n * ```\n *\n * @module rate-limiter\n */\n\nexport interface RateLimitEntry {\n count: number;\n windowStartedAt: number;\n blockedUntil: number;\n}\n\nexport interface RateLimitPolicy {\n windowMs: number;\n blockMs: number;\n maxAttemptsByIpAndAccount: number;\n maxAttemptsByAccount: number;\n maxKeys: number;\n}\n\n/**\n * Rate limiter adapter interface. Implementations must be async-safe.\n */\nexport interface RateLimiterAdapter {\n /** Get the current rate limit entry for a key, or null if no entry exists. */\n get(store: \"ip\" | \"account\", key: string): RateLimitEntry | null;\n\n /** Set/update the rate limit entry for a key. */\n set(store: \"ip\" | \"account\", key: string, entry: RateLimitEntry): void;\n\n /** Delete an entry (e.g. on successful login). */\n delete(store: \"ip\" | \"account\", key: string): void;\n\n /** Get the total number of tracked keys (for eviction logic). */\n size(store: \"ip\" | \"account\"): number;\n\n /** Clear the oldest entries when maxKeys is exceeded. */\n evictOldest(store: \"ip\" | \"account\", count: number): void;\n\n /**\n * Remove expired entries for a store.\n * Optional to preserve compatibility with existing custom adapters.\n */\n sweepExpired?(store: \"ip\" | \"account\", now: number, windowMs: number): void;\n}\n\n// ---------------------------------------------------------------------------\n// Default in-memory implementation\n// ---------------------------------------------------------------------------\n\nclass InMemoryRateLimiter implements RateLimiterAdapter {\n private ipStore = new Map<string, RateLimitEntry>();\n private accountStore = new Map<string, RateLimitEntry>();\n\n private getStore(store: \"ip\" | \"account\"): Map<string, RateLimitEntry> {\n return store === \"ip\" ? this.ipStore : this.accountStore;\n }\n\n get(store: \"ip\" | \"account\", key: string): RateLimitEntry | null {\n return this.getStore(store).get(key) ?? null;\n }\n\n set(store: \"ip\" | \"account\", key: string, entry: RateLimitEntry): void {\n this.getStore(store).set(key, entry);\n }\n\n delete(store: \"ip\" | \"account\", key: string): void {\n this.getStore(store).delete(key);\n }\n\n size(store: \"ip\" | \"account\"): number {\n return this.getStore(store).size;\n }\n\n evictOldest(store: \"ip\" | \"account\", count: number): void {\n const map = this.getStore(store);\n const entries = [...map.entries()].sort((a, b) => a[1].windowStartedAt - b[1].windowStartedAt);\n for (let i = 0; i < count && i < entries.length; i++) {\n const candidate = entries[i];\n if (!candidate) break;\n map.delete(candidate[0]);\n }\n }\n\n sweepExpired(store: \"ip\" | \"account\", now: number, windowMs: number): void {\n const map = this.getStore(store);\n for (const [key, entry] of map.entries()) {\n const windowExpired = now - entry.windowStartedAt > windowMs;\n const noActiveBlock = entry.blockedUntil <= now;\n if (windowExpired && noActiveBlock) {\n map.delete(key);\n }\n }\n }\n}\n\n// Singleton adapter\nlet adapter: RateLimiterAdapter = new InMemoryRateLimiter();\nlet hasWarnedInMemory = false;\n\n/** Replace the default in-memory rate limiter with a custom adapter. */\nexport function setRateLimiterAdapter(custom: RateLimiterAdapter): void {\n adapter = custom;\n}\n\n/** Get the current rate limiter adapter. */\nexport function getRateLimiterAdapter(): RateLimiterAdapter {\n return adapter;\n}\n\n/**\n * Check if a login attempt should be rate-limited.\n * @returns A reason string if blocked, or null if allowed.\n */\nexport function checkRateLimit(\n ipKey: string,\n accountKey: string | null,\n policy: RateLimitPolicy,\n): string | null {\n if (!hasWarnedInMemory && adapter instanceof InMemoryRateLimiter && process.env.NODE_ENV === \"production\") {\n hasWarnedInMemory = true;\n console.warn(\"[server] Rate limiter is using in-memory storage in production. For multi-instance deployments, call setRateLimiterAdapter() with a shared-storage adapter.\");\n }\n const now = Date.now();\n\n // Check IP-based limit\n const ipResult = checkSingleLimit(adapter, \"ip\", ipKey, policy.maxAttemptsByIpAndAccount, policy, now);\n if (ipResult) return ipResult;\n\n // Check account-based limit\n if (accountKey) {\n const accountResult = checkSingleLimit(adapter, \"account\", accountKey, policy.maxAttemptsByAccount, policy, now);\n if (accountResult) return accountResult;\n }\n\n return null;\n}\n\n/**\n * Record a failed login attempt.\n */\nexport function recordFailedAttempt(\n ipKey: string,\n accountKey: string | null,\n policy: RateLimitPolicy,\n): void {\n const now = Date.now();\n recordAttempt(adapter, \"ip\", ipKey, policy, now);\n if (accountKey) {\n recordAttempt(adapter, \"account\", accountKey, policy, now);\n }\n}\n\n/**\n * Clear rate limit entries for a key (e.g. on successful login).\n */\nexport function clearRateLimitEntries(ipKey: string, accountKey: string | null): void {\n adapter.delete(\"ip\", ipKey);\n if (accountKey) {\n adapter.delete(\"account\", accountKey);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction checkSingleLimit(\n adap: RateLimiterAdapter,\n store: \"ip\" | \"account\",\n key: string,\n maxAttempts: number,\n policy: RateLimitPolicy,\n now: number,\n): string | null {\n const entry = adap.get(store, key);\n if (!entry) return null;\n\n if (entry.blockedUntil > now) {\n const remainSec = Math.ceil((entry.blockedUntil - now) / 1000);\n return `Rate limited (${store}). Try again in ${remainSec}s.`;\n }\n\n if (now - entry.windowStartedAt > policy.windowMs) {\n adap.delete(store, key);\n return null;\n }\n\n if (entry.count >= maxAttempts) {\n entry.blockedUntil = now + policy.blockMs;\n adap.set(store, key, entry);\n return `Too many attempts (${store}). Blocked for ${Math.ceil(policy.blockMs / 1000)}s.`;\n }\n\n return null;\n}\n\nfunction recordAttempt(\n adap: RateLimiterAdapter,\n store: \"ip\" | \"account\",\n key: string,\n policy: RateLimitPolicy,\n now: number,\n): void {\n // Evict old entries if needed\n if (adap.size(store) >= policy.maxKeys) {\n adap.evictOldest(store, Math.floor(policy.maxKeys * 0.1));\n }\n\n const entry = adap.get(store, key);\n if (!entry || now - entry.windowStartedAt > policy.windowMs) {\n adap.set(store, key, { count: 1, windowStartedAt: now, blockedUntil: 0 });\n } else {\n entry.count++;\n adap.set(store, key, entry);\n }\n}\n"],"mappings":"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","names":[]}
|