@sentinel-atl/hardening 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 +102 -0
- package/dist/audit-rotation.d.ts +33 -0
- package/dist/audit-rotation.d.ts.map +1 -0
- package/dist/audit-rotation.js +120 -0
- package/dist/audit-rotation.js.map +1 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +117 -0
- package/dist/auth.js.map +1 -0
- package/dist/cors.d.ts +34 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +86 -0
- package/dist/cors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/nonce-store.d.ts +50 -0
- package/dist/nonce-store.d.ts.map +1 -0
- package/dist/nonce-store.js +88 -0
- package/dist/nonce-store.js.map +1 -0
- package/dist/rate-limit.d.ts +55 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +116 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/security-headers.d.ts +36 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +48 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/tls.d.ts +33 -0
- package/dist/tls.d.ts.map +1 -0
- package/dist/tls.js +41 -0
- package/dist/tls.js.map +1 -0
- package/package.json +43 -0
- package/src/__tests__/hardening.test.ts +472 -0
- package/src/audit-rotation.ts +149 -0
- package/src/auth.ts +162 -0
- package/src/cors.ts +118 -0
- package/src/index.ts +62 -0
- package/src/nonce-store.ts +111 -0
- package/src/rate-limit.ts +141 -0
- package/src/security-headers.ts +79 -0
- package/src/tls.ts +66 -0
- package/tsconfig.json +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @sentinel-atl/hardening
|
|
2
|
+
|
|
3
|
+
Production hardening middleware for Sentinel services — API key auth, CORS, TLS, rate limiting, nonce replay protection, audit log rotation, and security headers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sentinel-atl/hardening
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Modules
|
|
12
|
+
|
|
13
|
+
### Authentication
|
|
14
|
+
|
|
15
|
+
API key auth with scoped access control and constant-time comparison.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { authenticate, hasScope, authConfigFromEnv } from '@sentinel-atl/hardening';
|
|
19
|
+
|
|
20
|
+
const config = authConfigFromEnv();
|
|
21
|
+
// Or manual: { enabled: true, keys: [{ key: 'secret', scopes: ['read', 'write'] }] }
|
|
22
|
+
|
|
23
|
+
const result = authenticate(req, config);
|
|
24
|
+
if (!result.authenticated) sendUnauthorized(res);
|
|
25
|
+
if (!hasScope(result, 'write')) sendForbidden(res);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Key extraction order: `Authorization: Bearer <key>` → `X-API-Key` header → `?apiKey=` query param.
|
|
29
|
+
|
|
30
|
+
**Environment**: `SENTINEL_API_KEYS=key1:read,write;key2:admin`
|
|
31
|
+
|
|
32
|
+
### CORS
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { applyCors, corsConfigFromEnv } from '@sentinel-atl/hardening';
|
|
36
|
+
|
|
37
|
+
const config = corsConfigFromEnv();
|
|
38
|
+
const isPreflight = applyCors(req, res, config);
|
|
39
|
+
if (isPreflight) return; // Already responded to OPTIONS
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Environment**: `SENTINEL_CORS_ORIGINS=https://example.com,https://app.example.com`
|
|
43
|
+
|
|
44
|
+
### TLS
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createSecureServer, tlsConfigFromEnv } from '@sentinel-atl/hardening';
|
|
48
|
+
|
|
49
|
+
const tls = tlsConfigFromEnv();
|
|
50
|
+
const server = createSecureServer(tls, handler);
|
|
51
|
+
// Returns https.Server when TLS is configured, http.Server otherwise
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Environment**: `SENTINEL_TLS_CERT_PATH`, `SENTINEL_TLS_KEY_PATH`
|
|
55
|
+
|
|
56
|
+
### Rate Limiting
|
|
57
|
+
|
|
58
|
+
RFC 6585 compliant, in-memory rate limiter.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { RateLimiter, parseRateLimit } from '@sentinel-atl/hardening';
|
|
62
|
+
|
|
63
|
+
const { max, windowMs } = parseRateLimit('100/min');
|
|
64
|
+
const limiter = new RateLimiter(max, windowMs);
|
|
65
|
+
|
|
66
|
+
const info = limiter.check(clientId);
|
|
67
|
+
setRateLimitHeaders(res, info);
|
|
68
|
+
if (!info.allowed) sendRateLimited(res, info);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Nonce Store (Replay Protection)
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { NonceStore } from '@sentinel-atl/hardening';
|
|
75
|
+
|
|
76
|
+
const nonces = new NonceStore({ backend: store, ttl: 300 });
|
|
77
|
+
const isNew = await nonces.consume(nonce);
|
|
78
|
+
if (!isNew) return res.end('Replay detected');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Audit Log Rotation
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { rotateIfNeeded, cleanupRotatedFiles } from '@sentinel-atl/hardening';
|
|
85
|
+
|
|
86
|
+
await rotateIfNeeded('./audit.jsonl', { maxSize: 10_000_000 }); // 10MB
|
|
87
|
+
await cleanupRotatedFiles('./audit.jsonl', { maxAge: 30 * 86400_000 }); // 30 days
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Security Headers
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { applySecurityHeaders } from '@sentinel-atl/hardening';
|
|
94
|
+
|
|
95
|
+
applySecurityHeaders(res, { hsts: true });
|
|
96
|
+
// Sets: Content-Security-Policy, X-Content-Type-Options, X-Frame-Options,
|
|
97
|
+
// X-XSS-Protection, Referrer-Policy, Permissions-Policy, HSTS
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log rotation — prevents unbounded log file growth.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Size-based rotation: rotate when file exceeds maxSizeBytes
|
|
6
|
+
* - Time-based rotation: rotate daily/hourly
|
|
7
|
+
* - Retention: keep N rotated files, delete older ones
|
|
8
|
+
* - Compression-ready: rotated files get .N suffix (gzip can be added later)
|
|
9
|
+
*/
|
|
10
|
+
export interface RotationConfig {
|
|
11
|
+
/** Path to the active log file */
|
|
12
|
+
logPath: string;
|
|
13
|
+
/** Maximum size in bytes before rotation (default: 10MB) */
|
|
14
|
+
maxSizeBytes?: number;
|
|
15
|
+
/** Maximum number of rotated files to keep (default: 10) */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
/** Rotation interval: 'size' | 'daily' | 'hourly' (default: 'size') */
|
|
18
|
+
interval?: 'size' | 'daily' | 'hourly';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if the log file needs rotation and rotate if so.
|
|
22
|
+
* Returns true if rotation occurred.
|
|
23
|
+
*/
|
|
24
|
+
export declare function rotateIfNeeded(config: RotationConfig): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Clean up rotated files beyond the retention limit.
|
|
27
|
+
*/
|
|
28
|
+
export declare function cleanupRotatedFiles(config: RotationConfig): Promise<number>;
|
|
29
|
+
/**
|
|
30
|
+
* Get total size of all log files (active + rotated).
|
|
31
|
+
*/
|
|
32
|
+
export declare function totalLogSize(config: RotationConfig): Promise<number>;
|
|
33
|
+
//# sourceMappingURL=audit-rotation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-rotation.d.ts","sourceRoot":"","sources":["../src/audit-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;CACxC;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CA+B7E;AA8BD;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBjF;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB1E"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log rotation — prevents unbounded log file growth.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Size-based rotation: rotate when file exceeds maxSizeBytes
|
|
6
|
+
* - Time-based rotation: rotate daily/hourly
|
|
7
|
+
* - Retention: keep N rotated files, delete older ones
|
|
8
|
+
* - Compression-ready: rotated files get .N suffix (gzip can be added later)
|
|
9
|
+
*/
|
|
10
|
+
import { stat, rename, unlink, readdir } from 'node:fs/promises';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { dirname, basename, join } from 'node:path';
|
|
13
|
+
// ─── Rotation ────────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Check if the log file needs rotation and rotate if so.
|
|
16
|
+
* Returns true if rotation occurred.
|
|
17
|
+
*/
|
|
18
|
+
export async function rotateIfNeeded(config) {
|
|
19
|
+
const { logPath } = config;
|
|
20
|
+
const maxSize = config.maxSizeBytes ?? 10_485_760; // 10 MB
|
|
21
|
+
const maxFiles = config.maxFiles ?? 10;
|
|
22
|
+
if (!existsSync(logPath))
|
|
23
|
+
return false;
|
|
24
|
+
const interval = config.interval ?? 'size';
|
|
25
|
+
let shouldRotate = false;
|
|
26
|
+
if (interval === 'size') {
|
|
27
|
+
const fileStat = await stat(logPath);
|
|
28
|
+
shouldRotate = fileStat.size >= maxSize;
|
|
29
|
+
}
|
|
30
|
+
else if (interval === 'daily' || interval === 'hourly') {
|
|
31
|
+
const fileStat = await stat(logPath);
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const fileTime = new Date(fileStat.mtime);
|
|
34
|
+
if (interval === 'daily') {
|
|
35
|
+
shouldRotate = now.toDateString() !== fileTime.toDateString();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
shouldRotate = now.getHours() !== fileTime.getHours() ||
|
|
39
|
+
now.toDateString() !== fileTime.toDateString();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!shouldRotate)
|
|
43
|
+
return false;
|
|
44
|
+
await rotate(logPath, maxFiles);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Perform the rotation: shift existing files and rename current log.
|
|
49
|
+
*
|
|
50
|
+
* log.jsonl → log.jsonl.1
|
|
51
|
+
* log.jsonl.1 → log.jsonl.2
|
|
52
|
+
* ...
|
|
53
|
+
* log.jsonl.N → deleted (if N > maxFiles)
|
|
54
|
+
*/
|
|
55
|
+
async function rotate(logPath, maxFiles) {
|
|
56
|
+
// Delete the oldest file if it exists
|
|
57
|
+
const oldest = `${logPath}.${maxFiles}`;
|
|
58
|
+
if (existsSync(oldest)) {
|
|
59
|
+
await unlink(oldest);
|
|
60
|
+
}
|
|
61
|
+
// Shift existing rotated files
|
|
62
|
+
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
63
|
+
const from = `${logPath}.${i}`;
|
|
64
|
+
const to = `${logPath}.${i + 1}`;
|
|
65
|
+
if (existsSync(from)) {
|
|
66
|
+
await rename(from, to);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Rotate current log
|
|
70
|
+
await rename(logPath, `${logPath}.1`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Clean up rotated files beyond the retention limit.
|
|
74
|
+
*/
|
|
75
|
+
export async function cleanupRotatedFiles(config) {
|
|
76
|
+
const maxFiles = config.maxFiles ?? 10;
|
|
77
|
+
const dir = dirname(config.logPath);
|
|
78
|
+
const base = basename(config.logPath);
|
|
79
|
+
const files = await readdir(dir);
|
|
80
|
+
const rotatedFiles = files
|
|
81
|
+
.filter(f => f.startsWith(base + '.') && /\.\d+$/.test(f))
|
|
82
|
+
.sort((a, b) => {
|
|
83
|
+
const numA = parseInt(a.split('.').pop());
|
|
84
|
+
const numB = parseInt(b.split('.').pop());
|
|
85
|
+
return numA - numB;
|
|
86
|
+
});
|
|
87
|
+
let removed = 0;
|
|
88
|
+
for (const file of rotatedFiles) {
|
|
89
|
+
const num = parseInt(file.split('.').pop());
|
|
90
|
+
if (num > maxFiles) {
|
|
91
|
+
await unlink(join(dir, file));
|
|
92
|
+
removed++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return removed;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get total size of all log files (active + rotated).
|
|
99
|
+
*/
|
|
100
|
+
export async function totalLogSize(config) {
|
|
101
|
+
const dir = dirname(config.logPath);
|
|
102
|
+
const base = basename(config.logPath);
|
|
103
|
+
let total = 0;
|
|
104
|
+
if (existsSync(config.logPath)) {
|
|
105
|
+
total += (await stat(config.logPath)).size;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const files = await readdir(dir);
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
if (file.startsWith(base + '.') && /\.\d+$/.test(file)) {
|
|
111
|
+
total += (await stat(join(dir, file))).size;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Directory might not exist
|
|
117
|
+
}
|
|
118
|
+
return total;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=audit-rotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-rotation.js","sourceRoot":"","sources":["../src/audit-rotation.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Authentication middleware.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Bearer token in Authorization header
|
|
6
|
+
* - X-API-Key header
|
|
7
|
+
* - ?apiKey query parameter
|
|
8
|
+
*
|
|
9
|
+
* Keys are validated using constant-time comparison to prevent timing attacks.
|
|
10
|
+
* Multiple keys can be configured with different scopes (read, write, admin).
|
|
11
|
+
*/
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
13
|
+
export type AuthScope = 'read' | 'write' | 'admin';
|
|
14
|
+
export interface ApiKey {
|
|
15
|
+
/** The key value (plaintext — in production, store hashed) */
|
|
16
|
+
key: string;
|
|
17
|
+
/** Human-readable label */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Scopes this key grants */
|
|
20
|
+
scopes: AuthScope[];
|
|
21
|
+
}
|
|
22
|
+
export interface AuthConfig {
|
|
23
|
+
/** Whether auth is enabled (default: false — opt-in) */
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
/** Registered API keys */
|
|
26
|
+
keys: ApiKey[];
|
|
27
|
+
/** Paths that don't require auth (e.g., /health, badge endpoints) */
|
|
28
|
+
publicPaths?: string[];
|
|
29
|
+
/** Custom realm for WWW-Authenticate header */
|
|
30
|
+
realm?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface AuthResult {
|
|
33
|
+
authenticated: boolean;
|
|
34
|
+
key?: ApiKey;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Authenticate an incoming request.
|
|
39
|
+
*/
|
|
40
|
+
export declare function authenticate(req: IncomingMessage, config: AuthConfig): AuthResult;
|
|
41
|
+
/**
|
|
42
|
+
* Check if request has the required scope.
|
|
43
|
+
*/
|
|
44
|
+
export declare function hasScope(result: AuthResult, required: AuthScope): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Send a 401 Unauthorized response.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sendUnauthorized(res: ServerResponse, config: AuthConfig, error?: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Send a 403 Forbidden response.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sendForbidden(res: ServerResponse, error?: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Create a default auth config from environment variables.
|
|
55
|
+
*
|
|
56
|
+
* SENTINEL_API_KEYS=key1:read,write;key2:admin
|
|
57
|
+
*/
|
|
58
|
+
export declare function authConfigFromEnv(): AuthConfig;
|
|
59
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAKjE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,MAAM;IACrB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAoCD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CAyBjF;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,GAAG,OAAO,CAKzE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAO9F;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAGvE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,CAiB9C"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Authentication middleware.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Bearer token in Authorization header
|
|
6
|
+
* - X-API-Key header
|
|
7
|
+
* - ?apiKey query parameter
|
|
8
|
+
*
|
|
9
|
+
* Keys are validated using constant-time comparison to prevent timing attacks.
|
|
10
|
+
* Multiple keys can be configured with different scopes (read, write, admin).
|
|
11
|
+
*/
|
|
12
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
13
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
14
|
+
function constantTimeCompare(a, b) {
|
|
15
|
+
if (a.length !== b.length)
|
|
16
|
+
return false;
|
|
17
|
+
const bufA = Buffer.from(a, 'utf-8');
|
|
18
|
+
const bufB = Buffer.from(b, 'utf-8');
|
|
19
|
+
return timingSafeEqual(bufA, bufB);
|
|
20
|
+
}
|
|
21
|
+
function extractApiKey(req) {
|
|
22
|
+
// 1. Authorization: Bearer <key>
|
|
23
|
+
const authHeader = req.headers['authorization'];
|
|
24
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
25
|
+
return authHeader.slice(7);
|
|
26
|
+
}
|
|
27
|
+
// 2. X-API-Key header
|
|
28
|
+
const xApiKey = req.headers['x-api-key'];
|
|
29
|
+
if (typeof xApiKey === 'string' && xApiKey) {
|
|
30
|
+
return xApiKey;
|
|
31
|
+
}
|
|
32
|
+
// 3. Query parameter
|
|
33
|
+
const url = new URL(req.url ?? '/', `http://localhost`);
|
|
34
|
+
const queryKey = url.searchParams.get('apiKey');
|
|
35
|
+
if (queryKey) {
|
|
36
|
+
return queryKey;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// ─── Auth Check ──────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Authenticate an incoming request.
|
|
43
|
+
*/
|
|
44
|
+
export function authenticate(req, config) {
|
|
45
|
+
if (!config.enabled) {
|
|
46
|
+
return { authenticated: true };
|
|
47
|
+
}
|
|
48
|
+
// Check public paths
|
|
49
|
+
const url = new URL(req.url ?? '/', `http://localhost`);
|
|
50
|
+
const path = url.pathname;
|
|
51
|
+
if (config.publicPaths?.some(p => path === p || path.startsWith(p + '/'))) {
|
|
52
|
+
return { authenticated: true };
|
|
53
|
+
}
|
|
54
|
+
const providedKey = extractApiKey(req);
|
|
55
|
+
if (!providedKey) {
|
|
56
|
+
return { authenticated: false, error: 'Missing API key' };
|
|
57
|
+
}
|
|
58
|
+
// Find matching key (constant-time comparison)
|
|
59
|
+
for (const registeredKey of config.keys) {
|
|
60
|
+
if (constantTimeCompare(providedKey, registeredKey.key)) {
|
|
61
|
+
return { authenticated: true, key: registeredKey };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { authenticated: false, error: 'Invalid API key' };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if request has the required scope.
|
|
68
|
+
*/
|
|
69
|
+
export function hasScope(result, required) {
|
|
70
|
+
if (!result.authenticated)
|
|
71
|
+
return false;
|
|
72
|
+
if (!result.key)
|
|
73
|
+
return true; // Auth disabled or public path
|
|
74
|
+
if (result.key.scopes.includes('admin'))
|
|
75
|
+
return true;
|
|
76
|
+
return result.key.scopes.includes(required);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Send a 401 Unauthorized response.
|
|
80
|
+
*/
|
|
81
|
+
export function sendUnauthorized(res, config, error) {
|
|
82
|
+
const realm = config.realm ?? 'Sentinel';
|
|
83
|
+
res.writeHead(401, {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'WWW-Authenticate': `Bearer realm="${realm}"`,
|
|
86
|
+
});
|
|
87
|
+
res.end(JSON.stringify({ error: error ?? 'Unauthorized' }));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Send a 403 Forbidden response.
|
|
91
|
+
*/
|
|
92
|
+
export function sendForbidden(res, error) {
|
|
93
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ error: error ?? 'Forbidden: insufficient scope' }));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a default auth config from environment variables.
|
|
98
|
+
*
|
|
99
|
+
* SENTINEL_API_KEYS=key1:read,write;key2:admin
|
|
100
|
+
*/
|
|
101
|
+
export function authConfigFromEnv() {
|
|
102
|
+
const envKeys = process.env['SENTINEL_API_KEYS'];
|
|
103
|
+
if (!envKeys) {
|
|
104
|
+
return { enabled: false, keys: [] };
|
|
105
|
+
}
|
|
106
|
+
const keys = envKeys.split(';').filter(Boolean).map((entry, i) => {
|
|
107
|
+
const [key, scopeStr] = entry.split(':');
|
|
108
|
+
const scopes = (scopeStr ?? 'read').split(',').map(s => s.trim());
|
|
109
|
+
return { key: key.trim(), label: `key-${i}`, scopes };
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
enabled: keys.length > 0,
|
|
113
|
+
keys,
|
|
114
|
+
publicPaths: ['/health'],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/cors.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS middleware — configurable origin allowlist.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the `Access-Control-Allow-Origin: *` wildcard with
|
|
5
|
+
* explicit origin checking and proper Vary headers.
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
8
|
+
export interface CorsConfig {
|
|
9
|
+
/** Allowed origins. Use ['*'] to allow all (not recommended for production). */
|
|
10
|
+
allowedOrigins: string[];
|
|
11
|
+
/** Allowed HTTP methods */
|
|
12
|
+
allowedMethods?: string[];
|
|
13
|
+
/** Allowed request headers */
|
|
14
|
+
allowedHeaders?: string[];
|
|
15
|
+
/** Headers to expose to the browser */
|
|
16
|
+
exposedHeaders?: string[];
|
|
17
|
+
/** Whether to allow credentials (cookies, auth headers) */
|
|
18
|
+
allowCredentials?: boolean;
|
|
19
|
+
/** Max age for preflight cache (seconds) */
|
|
20
|
+
maxAge?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function defaultCorsConfig(): CorsConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Apply CORS headers to a response.
|
|
25
|
+
* Returns true if this was a preflight request (caller should end the response).
|
|
26
|
+
*/
|
|
27
|
+
export declare function applyCors(req: IncomingMessage, res: ServerResponse, config: CorsConfig): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Create a CORS config from environment variable.
|
|
30
|
+
*
|
|
31
|
+
* SENTINEL_CORS_ORIGINS=https://example.com,https://app.example.com
|
|
32
|
+
*/
|
|
33
|
+
export declare function corsConfigFromEnv(): CorsConfig;
|
|
34
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIjE,MAAM,WAAW,UAAU;IACzB,gFAAgF;IAChF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,2BAA2B;IAC3B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,wBAAgB,iBAAiB,IAAI,UAAU,CAQ9C;AAID;;;GAGG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,UAAU,GACjB,OAAO,CAoDT;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,CAU9C"}
|
package/dist/cors.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS middleware — configurable origin allowlist.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the `Access-Control-Allow-Origin: *` wildcard with
|
|
5
|
+
* explicit origin checking and proper Vary headers.
|
|
6
|
+
*/
|
|
7
|
+
// ─── Default Config ──────────────────────────────────────────────────
|
|
8
|
+
export function defaultCorsConfig() {
|
|
9
|
+
return {
|
|
10
|
+
allowedOrigins: ['*'],
|
|
11
|
+
allowedMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
12
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Caller-Id', 'X-Server-Name'],
|
|
13
|
+
allowCredentials: false,
|
|
14
|
+
maxAge: 86400, // 24 hours
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// ─── CORS Application ───────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Apply CORS headers to a response.
|
|
20
|
+
* Returns true if this was a preflight request (caller should end the response).
|
|
21
|
+
*/
|
|
22
|
+
export function applyCors(req, res, config) {
|
|
23
|
+
const origin = req.headers['origin'];
|
|
24
|
+
// Determine if origin is allowed
|
|
25
|
+
let allowedOrigin;
|
|
26
|
+
if (config.allowedOrigins.includes('*')) {
|
|
27
|
+
// Wildcard — but if credentials are enabled, must echo specific origin
|
|
28
|
+
allowedOrigin = config.allowCredentials && origin ? origin : '*';
|
|
29
|
+
}
|
|
30
|
+
else if (origin && config.allowedOrigins.includes(origin)) {
|
|
31
|
+
allowedOrigin = origin;
|
|
32
|
+
}
|
|
33
|
+
else if (!origin) {
|
|
34
|
+
// Same-origin or non-browser request — no CORS header needed
|
|
35
|
+
allowedOrigin = '';
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Origin not allowed — don't set any CORS headers
|
|
39
|
+
allowedOrigin = '';
|
|
40
|
+
}
|
|
41
|
+
if (allowedOrigin) {
|
|
42
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
43
|
+
// Vary: Origin — critical for caching when origin-specific
|
|
44
|
+
if (allowedOrigin !== '*') {
|
|
45
|
+
res.setHeader('Vary', 'Origin');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (config.allowCredentials) {
|
|
49
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
50
|
+
}
|
|
51
|
+
if (config.exposedHeaders?.length) {
|
|
52
|
+
res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
|
|
53
|
+
}
|
|
54
|
+
// Handle preflight
|
|
55
|
+
if (req.method === 'OPTIONS') {
|
|
56
|
+
if (config.allowedMethods?.length) {
|
|
57
|
+
res.setHeader('Access-Control-Allow-Methods', config.allowedMethods.join(', '));
|
|
58
|
+
}
|
|
59
|
+
if (config.allowedHeaders?.length) {
|
|
60
|
+
res.setHeader('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
|
|
61
|
+
}
|
|
62
|
+
if (config.maxAge !== undefined) {
|
|
63
|
+
res.setHeader('Access-Control-Max-Age', String(config.maxAge));
|
|
64
|
+
}
|
|
65
|
+
res.writeHead(204);
|
|
66
|
+
res.end();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a CORS config from environment variable.
|
|
73
|
+
*
|
|
74
|
+
* SENTINEL_CORS_ORIGINS=https://example.com,https://app.example.com
|
|
75
|
+
*/
|
|
76
|
+
export function corsConfigFromEnv() {
|
|
77
|
+
const envOrigins = process.env['SENTINEL_CORS_ORIGINS'];
|
|
78
|
+
const origins = envOrigins
|
|
79
|
+
? envOrigins.split(',').map(o => o.trim()).filter(Boolean)
|
|
80
|
+
: ['*'];
|
|
81
|
+
return {
|
|
82
|
+
...defaultCorsConfig(),
|
|
83
|
+
allowedOrigins: origins,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=cors.js.map
|
package/dist/cors.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.js","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sentinel-atl/hardening — Production Hardening Middleware
|
|
3
|
+
*
|
|
4
|
+
* Reusable security middleware for all Sentinel HTTP servers:
|
|
5
|
+
* - API key authentication with scoped access
|
|
6
|
+
* - CORS with configurable origin allowlist
|
|
7
|
+
* - TLS/HTTPS support
|
|
8
|
+
* - Rate limit headers (RFC 6585)
|
|
9
|
+
* - Persistent nonce replay protection
|
|
10
|
+
* - Audit log rotation
|
|
11
|
+
*/
|
|
12
|
+
export { authenticate, hasScope, sendUnauthorized, sendForbidden, authConfigFromEnv, type AuthConfig, type AuthScope, type ApiKey, type AuthResult, } from './auth.js';
|
|
13
|
+
export { applyCors, defaultCorsConfig, corsConfigFromEnv, type CorsConfig, } from './cors.js';
|
|
14
|
+
export { createSecureServer, tlsConfigFromEnv, type TlsConfig, } from './tls.js';
|
|
15
|
+
export { RateLimiter, setRateLimitHeaders, sendRateLimited, parseRateLimit, type RateLimitInfo, } from './rate-limit.js';
|
|
16
|
+
export { NonceStore, type NonceStoreConfig, } from './nonce-store.js';
|
|
17
|
+
export { rotateIfNeeded, cleanupRotatedFiles, totalLogSize, type RotationConfig, } from './audit-rotation.js';
|
|
18
|
+
export { applySecurityHeaders, securityHeadersConfigFromEnv, type SecurityHeadersConfig, } from './security-headers.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,SAAS,GACf,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,UAAU,EACV,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,YAAY,EACZ,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sentinel-atl/hardening — Production Hardening Middleware
|
|
3
|
+
*
|
|
4
|
+
* Reusable security middleware for all Sentinel HTTP servers:
|
|
5
|
+
* - API key authentication with scoped access
|
|
6
|
+
* - CORS with configurable origin allowlist
|
|
7
|
+
* - TLS/HTTPS support
|
|
8
|
+
* - Rate limit headers (RFC 6585)
|
|
9
|
+
* - Persistent nonce replay protection
|
|
10
|
+
* - Audit log rotation
|
|
11
|
+
*/
|
|
12
|
+
export { authenticate, hasScope, sendUnauthorized, sendForbidden, authConfigFromEnv, } from './auth.js';
|
|
13
|
+
export { applyCors, defaultCorsConfig, corsConfigFromEnv, } from './cors.js';
|
|
14
|
+
export { createSecureServer, tlsConfigFromEnv, } from './tls.js';
|
|
15
|
+
export { RateLimiter, setRateLimitHeaders, sendRateLimited, parseRateLimit, } from './rate-limit.js';
|
|
16
|
+
export { NonceStore, } from './nonce-store.js';
|
|
17
|
+
export { rotateIfNeeded, cleanupRotatedFiles, totalLogSize, } from './audit-rotation.js';
|
|
18
|
+
export { applySecurityHeaders, securityHeadersConfigFromEnv, } from './security-headers.js';
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAKlB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,iBAAiB,GAElB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,kBAAkB,EAClB,gBAAgB,GAEjB,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,cAAc,GAEf,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,UAAU,GAEX,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,YAAY,GAEb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,oBAAoB,EACpB,4BAA4B,GAE7B,MAAM,uBAAuB,CAAC"}
|