@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 CHANGED
@@ -1,2 +1,175 @@
1
- export { aq as AllowlistConfig, j as ApiError, A as ApiErrorCode, n as ApiErrorCodeType, p as ApiPaginatedResponse, Z as ApiSecurityConfig, $ as ApiSecurityContext, o as ApiSuccessResponse, aN as AuditRequest, L as AuthCookiesConfig, V as AuthMethod, C as CommonApiErrors, ax as CommonRateLimits, ad as DateRangeInput, a6 as DateRangeSchema, al as DeploymentStage, ab as EmailInput, a0 as EmailSchema, aW as EnvValidationConfig, aX as EnvValidationResult, an as FlagDefinition, ao as FlagDefinitions, am as FlagValue, K as KEYCLOAK_DEFAULT_ROLES, I as KeycloakCallbacksConfig, B as KeycloakConfig, J as KeycloakJwtFields, D as KeycloakTokenSet, af as LoginInput, a8 as LoginSchema, aI as OpsAuditActor, aK as OpsAuditEvent, aM as OpsAuditLoggerOptions, aL as OpsAuditRecord, aJ as OpsAuditResource, ac as PaginationInput, a5 as PaginationSchema, a1 as PasswordSchema, a4 as PersonNameSchema, a3 as PhoneSchema, az as RateLimitCheckResult, aB as RateLimitOptions, Y as RateLimitPreset, ay as RateLimitRule, aA as RateLimitStore, R as RedirectCallbackConfig, ap as ResolvedFlags, X as RouteAuditConfig, ae as SearchQueryInput, a7 as SearchQuerySchema, _ as SecuritySession, ag as SignupInput, a9 as SignupSchema, a2 as SlugSchema, aO as StandardAuditActionType, aH as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, W as WrapperPresets, aj as buildAllowlist, F as buildAuthCookies, Q as buildErrorBody, E as buildKeycloakCallbacks, m as buildPagination, O as buildRateLimitHeaders, av as buildRateLimitResponseHeaders, G as buildRedirectCallback, w as buildTokenRefreshParams, aU as checkEnvVars, ar as checkRateLimit, l as classifyError, f as constantTimeEqual, a as containsHtml, c as containsUrls, aD as createAuditActor, aC as createAuditLogger, ah as createFeatureFlags, au as createMemoryRateLimitStore, aa as createSafeTextSchema, ai as detectStage, e as escapeHtml, aE as extractAuditIp, aG as extractAuditRequestId, aF as extractAuditUserAgent, N as extractClientIp, aR as getBoolEnv, h as getCorrelationId, y as getEndSessionEndpoint, aV as getEnvSummary, aS as getIntEnv, aQ as getOptionalEnv, as as getRateLimitStatus, aP as getRequiredEnv, x as getTokenEndpoint, u as hasAllRoles, t as hasAnyRole, r as hasRole, ak as isAllowlisted, k as isApiError, v as isTokenExpired, q as parseKeycloakRoles, z as refreshKeycloakToken, at as resetRateLimitForKey, aw as resolveIdentifier, M as resolveRateLimitIdentifier, g as sanitizeApiError, s as stripHtml, aT as validateEnvVars } from './index-CkyVz0hQ.mjs';
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
- export { aq as AllowlistConfig, j as ApiError, A as ApiErrorCode, n as ApiErrorCodeType, p as ApiPaginatedResponse, Z as ApiSecurityConfig, $ as ApiSecurityContext, o as ApiSuccessResponse, aN as AuditRequest, L as AuthCookiesConfig, V as AuthMethod, C as CommonApiErrors, ax as CommonRateLimits, ad as DateRangeInput, a6 as DateRangeSchema, al as DeploymentStage, ab as EmailInput, a0 as EmailSchema, aW as EnvValidationConfig, aX as EnvValidationResult, an as FlagDefinition, ao as FlagDefinitions, am as FlagValue, K as KEYCLOAK_DEFAULT_ROLES, I as KeycloakCallbacksConfig, B as KeycloakConfig, J as KeycloakJwtFields, D as KeycloakTokenSet, af as LoginInput, a8 as LoginSchema, aI as OpsAuditActor, aK as OpsAuditEvent, aM as OpsAuditLoggerOptions, aL as OpsAuditRecord, aJ as OpsAuditResource, ac as PaginationInput, a5 as PaginationSchema, a1 as PasswordSchema, a4 as PersonNameSchema, a3 as PhoneSchema, az as RateLimitCheckResult, aB as RateLimitOptions, Y as RateLimitPreset, ay as RateLimitRule, aA as RateLimitStore, R as RedirectCallbackConfig, ap as ResolvedFlags, X as RouteAuditConfig, ae as SearchQueryInput, a7 as SearchQuerySchema, _ as SecuritySession, ag as SignupInput, a9 as SignupSchema, a2 as SlugSchema, aO as StandardAuditActionType, aH as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, W as WrapperPresets, aj as buildAllowlist, F as buildAuthCookies, Q as buildErrorBody, E as buildKeycloakCallbacks, m as buildPagination, O as buildRateLimitHeaders, av as buildRateLimitResponseHeaders, G as buildRedirectCallback, w as buildTokenRefreshParams, aU as checkEnvVars, ar as checkRateLimit, l as classifyError, f as constantTimeEqual, a as containsHtml, c as containsUrls, aD as createAuditActor, aC as createAuditLogger, ah as createFeatureFlags, au as createMemoryRateLimitStore, aa as createSafeTextSchema, ai as detectStage, e as escapeHtml, aE as extractAuditIp, aG as extractAuditRequestId, aF as extractAuditUserAgent, N as extractClientIp, aR as getBoolEnv, h as getCorrelationId, y as getEndSessionEndpoint, aV as getEnvSummary, aS as getIntEnv, aQ as getOptionalEnv, as as getRateLimitStatus, aP as getRequiredEnv, x as getTokenEndpoint, u as hasAllRoles, t as hasAnyRole, r as hasRole, ak as isAllowlisted, k as isApiError, v as isTokenExpired, q as parseKeycloakRoles, z as refreshKeycloakToken, at as resetRateLimitForKey, aw as resolveIdentifier, M as resolveRateLimitIdentifier, g as sanitizeApiError, s as stripHtml, aT as validateEnvVars } from './index-CkyVz0hQ.js';
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({ token, user, account }) {
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