@digilogiclabs/platform-core 1.6.0 → 1.7.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/dist/auth.d.mts +174 -1
- package/dist/auth.d.ts +174 -1
- package/dist/auth.js +108 -3
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +101 -2
- package/dist/auth.mjs.map +1 -1
- package/dist/{index-CkyVz0hQ.d.mts → env-DerQ7Da-.d.mts} +2 -2
- package/dist/{index-CkyVz0hQ.d.ts → env-DerQ7Da-.d.ts} +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/auth.d.mts
CHANGED
|
@@ -1,2 +1,175 @@
|
|
|
1
|
-
|
|
1
|
+
import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-DerQ7Da-.mjs';
|
|
2
|
+
export { as as AllowlistConfig, A as ApiError, k as ApiErrorCode, m as ApiErrorCodeType, o as ApiPaginatedResponse, _ as ApiSecurityConfig, a0 as ApiSecurityContext, n as ApiSuccessResponse, aK as AuditRequest, P as AuthCookiesConfig, W as AuthMethod, C as CommonApiErrors, ay as CommonRateLimits, aj as DateRangeInput, ad as DateRangeSchema, an as DeploymentStage, ah as EmailInput, a6 as EmailSchema, E as EnvValidationConfig, x as EnvValidationResult, ap as FlagDefinition, aq as FlagDefinitions, ao as FlagValue, z as KEYCLOAK_DEFAULT_ROLES, N as KeycloakCallbacksConfig, K as KeycloakConfig, O as KeycloakJwtFields, y as KeycloakTokenSet, al as LoginInput, af as LoginSchema, aF as OpsAuditActor, aH as OpsAuditEvent, aJ as OpsAuditLoggerOptions, aI as OpsAuditRecord, aG as OpsAuditResource, ai as PaginationInput, ac as PaginationSchema, a7 as PasswordSchema, aa as PersonNameSchema, a9 as PhoneSchema, ax as RateLimitCheckResult, Y as RateLimitPreset, S as RedirectCallbackConfig, ar as ResolvedFlags, X as RouteAuditConfig, ak as SearchQueryInput, ae as SearchQuerySchema, $ as SecuritySession, am as SignupInput, ag as SignupSchema, a8 as SlugSchema, aM as StandardAuditActionType, aL as StandardAuditActions, Z as StandardRateLimitPresets, T as TokenRefreshResult, a5 as WrapperPresets, av as buildAllowlist, Q as buildAuthCookies, a4 as buildErrorBody, V as buildKeycloakCallbacks, l as buildPagination, a3 as buildRateLimitHeaders, aD as buildRateLimitResponseHeaders, U as buildRedirectCallback, I as buildTokenRefreshParams, u as checkEnvVars, aA as checkRateLimit, i as classifyError, c as constantTimeEqual, f as containsHtml, d as containsUrls, aQ as createAuditActor, aR as createAuditLogger, au as createFeatureFlags, az as createMemoryRateLimitStore, ab as createSafeTextSchema, at as detectStage, e as escapeHtml, aN as extractAuditIp, aP as extractAuditRequestId, aO as extractAuditUserAgent, a2 as extractClientIp, r as getBoolEnv, h as getCorrelationId, L as getEndSessionEndpoint, w as getEnvSummary, t as getIntEnv, q as getOptionalEnv, aB as getRateLimitStatus, p as getRequiredEnv, J as getTokenEndpoint, G as hasAllRoles, F as hasAnyRole, D as hasRole, aw as isAllowlisted, j as isApiError, H as isTokenExpired, B as parseKeycloakRoles, M as refreshKeycloakToken, aC as resetRateLimitForKey, aE as resolveIdentifier, a1 as resolveRateLimitIdentifier, g as sanitizeApiError, s as stripHtml, v as validateEnvVars } from './env-DerQ7Da-.mjs';
|
|
2
3
|
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redis Rate Limit Store
|
|
7
|
+
*
|
|
8
|
+
* Production-ready `RateLimitStore` implementation using Redis sorted sets
|
|
9
|
+
* for accurate sliding window rate limiting. Works with ioredis or any
|
|
10
|
+
* Redis client that supports the required commands.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import Redis from 'ioredis'
|
|
15
|
+
* import {
|
|
16
|
+
* createRedisRateLimitStore,
|
|
17
|
+
* checkRateLimit,
|
|
18
|
+
* CommonRateLimits,
|
|
19
|
+
* } from '@digilogiclabs/platform-core/auth'
|
|
20
|
+
*
|
|
21
|
+
* const redis = new Redis(process.env.REDIS_URL)
|
|
22
|
+
* const store = createRedisRateLimitStore(redis, { keyPrefix: 'myapp:' })
|
|
23
|
+
*
|
|
24
|
+
* const result = await checkRateLimit('api-call', 'user:123', CommonRateLimits.apiGeneral, { store })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimal Redis client interface.
|
|
30
|
+
*
|
|
31
|
+
* Compatible with ioredis, node-redis v4, and @upstash/redis.
|
|
32
|
+
* Only the commands used by the rate limiter are required.
|
|
33
|
+
*/
|
|
34
|
+
interface RedisRateLimitClient {
|
|
35
|
+
zremrangebyscore(key: string, min: number | string, max: number | string): Promise<number>;
|
|
36
|
+
zadd(key: string, score: number, member: string): Promise<number>;
|
|
37
|
+
zcard(key: string): Promise<number>;
|
|
38
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
39
|
+
get(key: string): Promise<string | null>;
|
|
40
|
+
ttl(key: string): Promise<number>;
|
|
41
|
+
setex(key: string, seconds: number, value: string): Promise<string>;
|
|
42
|
+
del(...keys: string[]): Promise<number>;
|
|
43
|
+
}
|
|
44
|
+
interface RedisRateLimitStoreOptions {
|
|
45
|
+
/** Prefix for all Redis keys (e.g., 'myapp:') */
|
|
46
|
+
keyPrefix?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a Redis-backed rate limit store using sorted sets.
|
|
50
|
+
*
|
|
51
|
+
* Uses the sliding window algorithm:
|
|
52
|
+
* - Each request adds a timestamped entry to a sorted set
|
|
53
|
+
* - Old entries (outside the window) are pruned on each check
|
|
54
|
+
* - The set cardinality gives the request count
|
|
55
|
+
*
|
|
56
|
+
* Blocks use simple key-value with TTL.
|
|
57
|
+
*/
|
|
58
|
+
declare function createRedisRateLimitStore(redis: RedisRateLimitClient, options?: RedisRateLimitStoreOptions): RateLimitStore;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Next.js API Route Helpers
|
|
62
|
+
*
|
|
63
|
+
* Shared utilities for Next.js API routes that all apps need.
|
|
64
|
+
* These wrap platform-core's framework-agnostic primitives into
|
|
65
|
+
* Next.js-specific patterns (NextResponse, NextRequest).
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import {
|
|
70
|
+
* enforceRateLimit,
|
|
71
|
+
* zodErrorResponse,
|
|
72
|
+
* errorResponse,
|
|
73
|
+
* extractBearerToken,
|
|
74
|
+
* isValidBearerToken,
|
|
75
|
+
* } from '@digilogiclabs/platform-core/auth'
|
|
76
|
+
*
|
|
77
|
+
* export async function POST(request: NextRequest) {
|
|
78
|
+
* const rl = await enforceRateLimit(request, 'submit', CommonRateLimits.publicForm)
|
|
79
|
+
* if (rl) return rl
|
|
80
|
+
*
|
|
81
|
+
* const parsed = schema.safeParse(await request.json())
|
|
82
|
+
* if (!parsed.success) return zodErrorResponse(parsed.error)
|
|
83
|
+
*
|
|
84
|
+
* try { ... }
|
|
85
|
+
* catch (error) { return errorResponse(error) }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Enforce rate limiting on a Next.js API request.
|
|
92
|
+
*
|
|
93
|
+
* Returns a 429 NextResponse if the limit is exceeded, or `null` if allowed.
|
|
94
|
+
* Apps use this at the top of route handlers for early rejection.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const rateLimited = await enforceRateLimit(request, 'sync', CommonRateLimits.adminAction)
|
|
99
|
+
* if (rateLimited) return rateLimited
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare function enforceRateLimit(request: {
|
|
103
|
+
headers: {
|
|
104
|
+
get(name: string): string | null;
|
|
105
|
+
};
|
|
106
|
+
}, operation: string, rule: RateLimitRule, options?: {
|
|
107
|
+
/** Override identifier (default: extracted from x-forwarded-for) */
|
|
108
|
+
identifier?: string;
|
|
109
|
+
/** User ID for per-user limiting */
|
|
110
|
+
userId?: string;
|
|
111
|
+
/** Rate limit store and options */
|
|
112
|
+
rateLimitOptions?: RateLimitOptions;
|
|
113
|
+
}): Promise<Response | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Convert any error into a structured JSON response.
|
|
116
|
+
*
|
|
117
|
+
* Uses platform-core's `classifyError()` to handle ApiError,
|
|
118
|
+
* Zod errors, PostgreSQL errors, and generic errors consistently.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* catch (error) {
|
|
123
|
+
* return errorResponse(error)
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function errorResponse(error: unknown, options?: {
|
|
128
|
+
isDevelopment?: boolean;
|
|
129
|
+
}): Response;
|
|
130
|
+
/**
|
|
131
|
+
* Convert a Zod validation error into a user-friendly 400 response.
|
|
132
|
+
*
|
|
133
|
+
* Extracts the first issue and returns a clear error message
|
|
134
|
+
* with the field path (e.g., "email: Invalid email").
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const parsed = schema.safeParse(await request.json())
|
|
139
|
+
* if (!parsed.success) return zodErrorResponse(parsed.error)
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
declare function zodErrorResponse(error: {
|
|
143
|
+
issues: Array<{
|
|
144
|
+
path: (string | number)[];
|
|
145
|
+
message: string;
|
|
146
|
+
}>;
|
|
147
|
+
}): Response;
|
|
148
|
+
/**
|
|
149
|
+
* Extract a bearer token from the Authorization header.
|
|
150
|
+
*
|
|
151
|
+
* @returns The token string, or null if not present/malformed.
|
|
152
|
+
*/
|
|
153
|
+
declare function extractBearerToken(request: {
|
|
154
|
+
headers: {
|
|
155
|
+
get(name: string): string | null;
|
|
156
|
+
};
|
|
157
|
+
}): string | null;
|
|
158
|
+
/**
|
|
159
|
+
* Check if a request's bearer token matches a secret.
|
|
160
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* if (!isValidBearerToken(request, process.env.ADMIN_SECRET)) {
|
|
165
|
+
* return new Response('Unauthorized', { status: 401 })
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
declare function isValidBearerToken(request: {
|
|
170
|
+
headers: {
|
|
171
|
+
get(name: string): string | null;
|
|
172
|
+
};
|
|
173
|
+
}, secret: string | undefined): boolean;
|
|
174
|
+
|
|
175
|
+
export { RateLimitOptions, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, isValidBearerToken, zodErrorResponse };
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,2 +1,175 @@
|
|
|
1
|
-
|
|
1
|
+
import { R as RateLimitStore, a as RateLimitRule, b as RateLimitOptions } from './env-DerQ7Da-.js';
|
|
2
|
+
export { as as AllowlistConfig, A as ApiError, k as ApiErrorCode, m as ApiErrorCodeType, o as ApiPaginatedResponse, _ as ApiSecurityConfig, a0 as ApiSecurityContext, n as ApiSuccessResponse, aK as AuditRequest, P as AuthCookiesConfig, W as AuthMethod, C as CommonApiErrors, ay as CommonRateLimits, aj as DateRangeInput, ad as DateRangeSchema, an as DeploymentStage, ah as EmailInput, a6 as EmailSchema, E as EnvValidationConfig, x as EnvValidationResult, ap as FlagDefinition, aq as FlagDefinitions, ao as FlagValue, z as KEYCLOAK_DEFAULT_ROLES, N as KeycloakCallbacksConfig, K as KeycloakConfig, O as KeycloakJwtFields, y as KeycloakTokenSet, al as LoginInput, af as LoginSchema, aF as OpsAuditActor, aH as OpsAuditEvent, aJ as OpsAuditLoggerOptions, aI as OpsAuditRecord, aG as OpsAuditResource, ai as PaginationInput, ac as PaginationSchema, a7 as PasswordSchema, aa as PersonNameSchema, a9 as PhoneSchema, ax as RateLimitCheckResult, Y as RateLimitPreset, S as RedirectCallbackConfig, ar as ResolvedFlags, X as RouteAuditConfig, ak as SearchQueryInput, ae as SearchQuerySchema, $ as SecuritySession, am as SignupInput, ag as SignupSchema, a8 as SlugSchema, aM as StandardAuditActionType, aL as StandardAuditActions, Z as StandardRateLimitPresets, T as TokenRefreshResult, a5 as WrapperPresets, av as buildAllowlist, Q as buildAuthCookies, a4 as buildErrorBody, V as buildKeycloakCallbacks, l as buildPagination, a3 as buildRateLimitHeaders, aD as buildRateLimitResponseHeaders, U as buildRedirectCallback, I as buildTokenRefreshParams, u as checkEnvVars, aA as checkRateLimit, i as classifyError, c as constantTimeEqual, f as containsHtml, d as containsUrls, aQ as createAuditActor, aR as createAuditLogger, au as createFeatureFlags, az as createMemoryRateLimitStore, ab as createSafeTextSchema, at as detectStage, e as escapeHtml, aN as extractAuditIp, aP as extractAuditRequestId, aO as extractAuditUserAgent, a2 as extractClientIp, r as getBoolEnv, h as getCorrelationId, L as getEndSessionEndpoint, w as getEnvSummary, t as getIntEnv, q as getOptionalEnv, aB as getRateLimitStatus, p as getRequiredEnv, J as getTokenEndpoint, G as hasAllRoles, F as hasAnyRole, D as hasRole, aw as isAllowlisted, j as isApiError, H as isTokenExpired, B as parseKeycloakRoles, M as refreshKeycloakToken, aC as resetRateLimitForKey, aE as resolveIdentifier, a1 as resolveRateLimitIdentifier, g as sanitizeApiError, s as stripHtml, v as validateEnvVars } from './env-DerQ7Da-.js';
|
|
2
3
|
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redis Rate Limit Store
|
|
7
|
+
*
|
|
8
|
+
* Production-ready `RateLimitStore` implementation using Redis sorted sets
|
|
9
|
+
* for accurate sliding window rate limiting. Works with ioredis or any
|
|
10
|
+
* Redis client that supports the required commands.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import Redis from 'ioredis'
|
|
15
|
+
* import {
|
|
16
|
+
* createRedisRateLimitStore,
|
|
17
|
+
* checkRateLimit,
|
|
18
|
+
* CommonRateLimits,
|
|
19
|
+
* } from '@digilogiclabs/platform-core/auth'
|
|
20
|
+
*
|
|
21
|
+
* const redis = new Redis(process.env.REDIS_URL)
|
|
22
|
+
* const store = createRedisRateLimitStore(redis, { keyPrefix: 'myapp:' })
|
|
23
|
+
*
|
|
24
|
+
* const result = await checkRateLimit('api-call', 'user:123', CommonRateLimits.apiGeneral, { store })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimal Redis client interface.
|
|
30
|
+
*
|
|
31
|
+
* Compatible with ioredis, node-redis v4, and @upstash/redis.
|
|
32
|
+
* Only the commands used by the rate limiter are required.
|
|
33
|
+
*/
|
|
34
|
+
interface RedisRateLimitClient {
|
|
35
|
+
zremrangebyscore(key: string, min: number | string, max: number | string): Promise<number>;
|
|
36
|
+
zadd(key: string, score: number, member: string): Promise<number>;
|
|
37
|
+
zcard(key: string): Promise<number>;
|
|
38
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
39
|
+
get(key: string): Promise<string | null>;
|
|
40
|
+
ttl(key: string): Promise<number>;
|
|
41
|
+
setex(key: string, seconds: number, value: string): Promise<string>;
|
|
42
|
+
del(...keys: string[]): Promise<number>;
|
|
43
|
+
}
|
|
44
|
+
interface RedisRateLimitStoreOptions {
|
|
45
|
+
/** Prefix for all Redis keys (e.g., 'myapp:') */
|
|
46
|
+
keyPrefix?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a Redis-backed rate limit store using sorted sets.
|
|
50
|
+
*
|
|
51
|
+
* Uses the sliding window algorithm:
|
|
52
|
+
* - Each request adds a timestamped entry to a sorted set
|
|
53
|
+
* - Old entries (outside the window) are pruned on each check
|
|
54
|
+
* - The set cardinality gives the request count
|
|
55
|
+
*
|
|
56
|
+
* Blocks use simple key-value with TTL.
|
|
57
|
+
*/
|
|
58
|
+
declare function createRedisRateLimitStore(redis: RedisRateLimitClient, options?: RedisRateLimitStoreOptions): RateLimitStore;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Next.js API Route Helpers
|
|
62
|
+
*
|
|
63
|
+
* Shared utilities for Next.js API routes that all apps need.
|
|
64
|
+
* These wrap platform-core's framework-agnostic primitives into
|
|
65
|
+
* Next.js-specific patterns (NextResponse, NextRequest).
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import {
|
|
70
|
+
* enforceRateLimit,
|
|
71
|
+
* zodErrorResponse,
|
|
72
|
+
* errorResponse,
|
|
73
|
+
* extractBearerToken,
|
|
74
|
+
* isValidBearerToken,
|
|
75
|
+
* } from '@digilogiclabs/platform-core/auth'
|
|
76
|
+
*
|
|
77
|
+
* export async function POST(request: NextRequest) {
|
|
78
|
+
* const rl = await enforceRateLimit(request, 'submit', CommonRateLimits.publicForm)
|
|
79
|
+
* if (rl) return rl
|
|
80
|
+
*
|
|
81
|
+
* const parsed = schema.safeParse(await request.json())
|
|
82
|
+
* if (!parsed.success) return zodErrorResponse(parsed.error)
|
|
83
|
+
*
|
|
84
|
+
* try { ... }
|
|
85
|
+
* catch (error) { return errorResponse(error) }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Enforce rate limiting on a Next.js API request.
|
|
92
|
+
*
|
|
93
|
+
* Returns a 429 NextResponse if the limit is exceeded, or `null` if allowed.
|
|
94
|
+
* Apps use this at the top of route handlers for early rejection.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const rateLimited = await enforceRateLimit(request, 'sync', CommonRateLimits.adminAction)
|
|
99
|
+
* if (rateLimited) return rateLimited
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare function enforceRateLimit(request: {
|
|
103
|
+
headers: {
|
|
104
|
+
get(name: string): string | null;
|
|
105
|
+
};
|
|
106
|
+
}, operation: string, rule: RateLimitRule, options?: {
|
|
107
|
+
/** Override identifier (default: extracted from x-forwarded-for) */
|
|
108
|
+
identifier?: string;
|
|
109
|
+
/** User ID for per-user limiting */
|
|
110
|
+
userId?: string;
|
|
111
|
+
/** Rate limit store and options */
|
|
112
|
+
rateLimitOptions?: RateLimitOptions;
|
|
113
|
+
}): Promise<Response | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Convert any error into a structured JSON response.
|
|
116
|
+
*
|
|
117
|
+
* Uses platform-core's `classifyError()` to handle ApiError,
|
|
118
|
+
* Zod errors, PostgreSQL errors, and generic errors consistently.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* catch (error) {
|
|
123
|
+
* return errorResponse(error)
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function errorResponse(error: unknown, options?: {
|
|
128
|
+
isDevelopment?: boolean;
|
|
129
|
+
}): Response;
|
|
130
|
+
/**
|
|
131
|
+
* Convert a Zod validation error into a user-friendly 400 response.
|
|
132
|
+
*
|
|
133
|
+
* Extracts the first issue and returns a clear error message
|
|
134
|
+
* with the field path (e.g., "email: Invalid email").
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const parsed = schema.safeParse(await request.json())
|
|
139
|
+
* if (!parsed.success) return zodErrorResponse(parsed.error)
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
declare function zodErrorResponse(error: {
|
|
143
|
+
issues: Array<{
|
|
144
|
+
path: (string | number)[];
|
|
145
|
+
message: string;
|
|
146
|
+
}>;
|
|
147
|
+
}): Response;
|
|
148
|
+
/**
|
|
149
|
+
* Extract a bearer token from the Authorization header.
|
|
150
|
+
*
|
|
151
|
+
* @returns The token string, or null if not present/malformed.
|
|
152
|
+
*/
|
|
153
|
+
declare function extractBearerToken(request: {
|
|
154
|
+
headers: {
|
|
155
|
+
get(name: string): string | null;
|
|
156
|
+
};
|
|
157
|
+
}): string | null;
|
|
158
|
+
/**
|
|
159
|
+
* Check if a request's bearer token matches a secret.
|
|
160
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* if (!isValidBearerToken(request, process.env.ADMIN_SECRET)) {
|
|
165
|
+
* return new Response('Unauthorized', { status: 401 })
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
declare function isValidBearerToken(request: {
|
|
170
|
+
headers: {
|
|
171
|
+
get(name: string): string | null;
|
|
172
|
+
};
|
|
173
|
+
}, secret: string | undefined): boolean;
|
|
174
|
+
|
|
175
|
+
export { RateLimitOptions, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, isValidBearerToken, zodErrorResponse };
|
package/dist/auth.js
CHANGED
|
@@ -57,12 +57,16 @@ __export(auth_exports, {
|
|
|
57
57
|
createAuditLogger: () => createAuditLogger,
|
|
58
58
|
createFeatureFlags: () => createFeatureFlags,
|
|
59
59
|
createMemoryRateLimitStore: () => createMemoryRateLimitStore,
|
|
60
|
+
createRedisRateLimitStore: () => createRedisRateLimitStore,
|
|
60
61
|
createSafeTextSchema: () => createSafeTextSchema,
|
|
61
62
|
detectStage: () => detectStage,
|
|
63
|
+
enforceRateLimit: () => enforceRateLimit,
|
|
64
|
+
errorResponse: () => errorResponse,
|
|
62
65
|
escapeHtml: () => escapeHtml,
|
|
63
66
|
extractAuditIp: () => extractAuditIp,
|
|
64
67
|
extractAuditRequestId: () => extractAuditRequestId,
|
|
65
68
|
extractAuditUserAgent: () => extractAuditUserAgent,
|
|
69
|
+
extractBearerToken: () => extractBearerToken,
|
|
66
70
|
extractClientIp: () => extractClientIp,
|
|
67
71
|
getBoolEnv: () => getBoolEnv,
|
|
68
72
|
getCorrelationId: () => getCorrelationId,
|
|
@@ -79,6 +83,7 @@ __export(auth_exports, {
|
|
|
79
83
|
isAllowlisted: () => isAllowlisted,
|
|
80
84
|
isApiError: () => isApiError,
|
|
81
85
|
isTokenExpired: () => isTokenExpired,
|
|
86
|
+
isValidBearerToken: () => isValidBearerToken,
|
|
82
87
|
parseKeycloakRoles: () => parseKeycloakRoles,
|
|
83
88
|
refreshKeycloakToken: () => refreshKeycloakToken,
|
|
84
89
|
resetRateLimitForKey: () => resetRateLimitForKey,
|
|
@@ -86,7 +91,8 @@ __export(auth_exports, {
|
|
|
86
91
|
resolveRateLimitIdentifier: () => resolveRateLimitIdentifier,
|
|
87
92
|
sanitizeApiError: () => sanitizeApiError,
|
|
88
93
|
stripHtml: () => stripHtml,
|
|
89
|
-
validateEnvVars: () => validateEnvVars
|
|
94
|
+
validateEnvVars: () => validateEnvVars,
|
|
95
|
+
zodErrorResponse: () => zodErrorResponse
|
|
90
96
|
});
|
|
91
97
|
module.exports = __toCommonJS(auth_exports);
|
|
92
98
|
|
|
@@ -260,7 +266,11 @@ function buildKeycloakCallbacks(config) {
|
|
|
260
266
|
* Compatible with Auth.js v5 JWT callback signature.
|
|
261
267
|
*/
|
|
262
268
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
263
|
-
async jwt({
|
|
269
|
+
async jwt({
|
|
270
|
+
token,
|
|
271
|
+
user,
|
|
272
|
+
account
|
|
273
|
+
}) {
|
|
264
274
|
if (user) {
|
|
265
275
|
token.id = token.sub ?? user.id;
|
|
266
276
|
}
|
|
@@ -821,6 +831,44 @@ function resolveIdentifier(session, clientIp) {
|
|
|
821
831
|
return { identifier: `ip:${clientIp ?? "unknown"}`, isAuthenticated: false };
|
|
822
832
|
}
|
|
823
833
|
|
|
834
|
+
// src/auth/rate-limit-store-redis.ts
|
|
835
|
+
function createRedisRateLimitStore(redis, options = {}) {
|
|
836
|
+
const prefix = options.keyPrefix ?? "";
|
|
837
|
+
return {
|
|
838
|
+
async increment(key, windowMs, now) {
|
|
839
|
+
const fullKey = `${prefix}${key}`;
|
|
840
|
+
const windowStart = now - windowMs;
|
|
841
|
+
const windowSeconds = Math.ceil(windowMs / 1e3) + 60;
|
|
842
|
+
await redis.zremrangebyscore(fullKey, 0, windowStart);
|
|
843
|
+
const current = await redis.zcard(fullKey);
|
|
844
|
+
const member = `${now}:${Math.random().toString(36).slice(2, 10)}`;
|
|
845
|
+
await redis.zadd(fullKey, now, member);
|
|
846
|
+
await redis.expire(fullKey, windowSeconds);
|
|
847
|
+
return { count: current + 1 };
|
|
848
|
+
},
|
|
849
|
+
async isBlocked(key) {
|
|
850
|
+
const fullKey = `${prefix}${key}`;
|
|
851
|
+
const value = await redis.get(fullKey);
|
|
852
|
+
if (!value) {
|
|
853
|
+
return { blocked: false, ttlMs: 0 };
|
|
854
|
+
}
|
|
855
|
+
const ttlSeconds = await redis.ttl(fullKey);
|
|
856
|
+
if (ttlSeconds <= 0) {
|
|
857
|
+
return { blocked: false, ttlMs: 0 };
|
|
858
|
+
}
|
|
859
|
+
return { blocked: true, ttlMs: ttlSeconds * 1e3 };
|
|
860
|
+
},
|
|
861
|
+
async setBlock(key, durationSeconds) {
|
|
862
|
+
const fullKey = `${prefix}${key}`;
|
|
863
|
+
await redis.setex(fullKey, durationSeconds, "1");
|
|
864
|
+
},
|
|
865
|
+
async reset(key) {
|
|
866
|
+
const fullKey = `${prefix}${key}`;
|
|
867
|
+
await redis.del(fullKey);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
824
872
|
// src/auth/audit.ts
|
|
825
873
|
var StandardAuditActions = {
|
|
826
874
|
// Authentication
|
|
@@ -1105,6 +1153,57 @@ function buildPagination(page, limit, total) {
|
|
|
1105
1153
|
};
|
|
1106
1154
|
}
|
|
1107
1155
|
|
|
1156
|
+
// src/auth/nextjs-api.ts
|
|
1157
|
+
async function enforceRateLimit(request, operation, rule, options) {
|
|
1158
|
+
const identifier = options?.identifier ?? (options?.userId ? `user:${options.userId}` : void 0) ?? `ip:${extractClientIp((name) => request.headers.get(name))}`;
|
|
1159
|
+
const isAuthenticated = !!options?.userId;
|
|
1160
|
+
const result = await checkRateLimit(operation, identifier, rule, {
|
|
1161
|
+
...options?.rateLimitOptions,
|
|
1162
|
+
isAuthenticated
|
|
1163
|
+
});
|
|
1164
|
+
if (!result.allowed) {
|
|
1165
|
+
const headers = buildRateLimitResponseHeaders(result);
|
|
1166
|
+
return new Response(
|
|
1167
|
+
JSON.stringify({
|
|
1168
|
+
error: "Rate limit exceeded. Please try again later.",
|
|
1169
|
+
retryAfter: result.retryAfterSeconds
|
|
1170
|
+
}),
|
|
1171
|
+
{
|
|
1172
|
+
status: 429,
|
|
1173
|
+
headers: { "Content-Type": "application/json", ...headers }
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
function errorResponse(error, options) {
|
|
1180
|
+
const isDev = options?.isDevelopment ?? process.env.NODE_ENV === "development";
|
|
1181
|
+
const { status, body } = classifyError(error, isDev);
|
|
1182
|
+
return new Response(JSON.stringify(body), {
|
|
1183
|
+
status,
|
|
1184
|
+
headers: { "Content-Type": "application/json" }
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
function zodErrorResponse(error) {
|
|
1188
|
+
const firstIssue = error.issues[0];
|
|
1189
|
+
const message = firstIssue ? `${firstIssue.path.join(".") || "input"}: ${firstIssue.message}` : "Validation error";
|
|
1190
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
1191
|
+
status: 400,
|
|
1192
|
+
headers: { "Content-Type": "application/json" }
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
function extractBearerToken(request) {
|
|
1196
|
+
const auth = request.headers.get("authorization");
|
|
1197
|
+
if (!auth?.startsWith("Bearer ")) return null;
|
|
1198
|
+
return auth.slice(7).trim() || null;
|
|
1199
|
+
}
|
|
1200
|
+
function isValidBearerToken(request, secret) {
|
|
1201
|
+
if (!secret) return false;
|
|
1202
|
+
const token = extractBearerToken(request);
|
|
1203
|
+
if (!token) return false;
|
|
1204
|
+
return constantTimeEqual(token, secret);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1108
1207
|
// src/env.ts
|
|
1109
1208
|
function getRequiredEnv(key) {
|
|
1110
1209
|
const value = process.env[key];
|
|
@@ -1234,12 +1333,16 @@ function getEnvSummary(keys) {
|
|
|
1234
1333
|
createAuditLogger,
|
|
1235
1334
|
createFeatureFlags,
|
|
1236
1335
|
createMemoryRateLimitStore,
|
|
1336
|
+
createRedisRateLimitStore,
|
|
1237
1337
|
createSafeTextSchema,
|
|
1238
1338
|
detectStage,
|
|
1339
|
+
enforceRateLimit,
|
|
1340
|
+
errorResponse,
|
|
1239
1341
|
escapeHtml,
|
|
1240
1342
|
extractAuditIp,
|
|
1241
1343
|
extractAuditRequestId,
|
|
1242
1344
|
extractAuditUserAgent,
|
|
1345
|
+
extractBearerToken,
|
|
1243
1346
|
extractClientIp,
|
|
1244
1347
|
getBoolEnv,
|
|
1245
1348
|
getCorrelationId,
|
|
@@ -1256,6 +1359,7 @@ function getEnvSummary(keys) {
|
|
|
1256
1359
|
isAllowlisted,
|
|
1257
1360
|
isApiError,
|
|
1258
1361
|
isTokenExpired,
|
|
1362
|
+
isValidBearerToken,
|
|
1259
1363
|
parseKeycloakRoles,
|
|
1260
1364
|
refreshKeycloakToken,
|
|
1261
1365
|
resetRateLimitForKey,
|
|
@@ -1263,6 +1367,7 @@ function getEnvSummary(keys) {
|
|
|
1263
1367
|
resolveRateLimitIdentifier,
|
|
1264
1368
|
sanitizeApiError,
|
|
1265
1369
|
stripHtml,
|
|
1266
|
-
validateEnvVars
|
|
1370
|
+
validateEnvVars,
|
|
1371
|
+
zodErrorResponse
|
|
1267
1372
|
});
|
|
1268
1373
|
//# sourceMappingURL=auth.js.map
|