@digilogiclabs/platform-core 1.5.0 → 1.6.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.js ADDED
@@ -0,0 +1,1268 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/auth/index.ts
21
+ var auth_exports = {};
22
+ __export(auth_exports, {
23
+ ApiError: () => ApiError,
24
+ ApiErrorCode: () => ApiErrorCode,
25
+ CommonApiErrors: () => CommonApiErrors,
26
+ CommonRateLimits: () => CommonRateLimits,
27
+ DateRangeSchema: () => DateRangeSchema,
28
+ EmailSchema: () => EmailSchema,
29
+ KEYCLOAK_DEFAULT_ROLES: () => KEYCLOAK_DEFAULT_ROLES,
30
+ LoginSchema: () => LoginSchema,
31
+ PaginationSchema: () => PaginationSchema,
32
+ PasswordSchema: () => PasswordSchema,
33
+ PersonNameSchema: () => PersonNameSchema,
34
+ PhoneSchema: () => PhoneSchema,
35
+ SearchQuerySchema: () => SearchQuerySchema,
36
+ SignupSchema: () => SignupSchema,
37
+ SlugSchema: () => SlugSchema,
38
+ StandardAuditActions: () => StandardAuditActions,
39
+ StandardRateLimitPresets: () => StandardRateLimitPresets,
40
+ WrapperPresets: () => WrapperPresets,
41
+ buildAllowlist: () => buildAllowlist,
42
+ buildAuthCookies: () => buildAuthCookies,
43
+ buildErrorBody: () => buildErrorBody,
44
+ buildKeycloakCallbacks: () => buildKeycloakCallbacks,
45
+ buildPagination: () => buildPagination,
46
+ buildRateLimitHeaders: () => buildRateLimitHeaders,
47
+ buildRateLimitResponseHeaders: () => buildRateLimitResponseHeaders,
48
+ buildRedirectCallback: () => buildRedirectCallback,
49
+ buildTokenRefreshParams: () => buildTokenRefreshParams,
50
+ checkEnvVars: () => checkEnvVars,
51
+ checkRateLimit: () => checkRateLimit,
52
+ classifyError: () => classifyError,
53
+ constantTimeEqual: () => constantTimeEqual,
54
+ containsHtml: () => containsHtml,
55
+ containsUrls: () => containsUrls,
56
+ createAuditActor: () => createAuditActor,
57
+ createAuditLogger: () => createAuditLogger,
58
+ createFeatureFlags: () => createFeatureFlags,
59
+ createMemoryRateLimitStore: () => createMemoryRateLimitStore,
60
+ createSafeTextSchema: () => createSafeTextSchema,
61
+ detectStage: () => detectStage,
62
+ escapeHtml: () => escapeHtml,
63
+ extractAuditIp: () => extractAuditIp,
64
+ extractAuditRequestId: () => extractAuditRequestId,
65
+ extractAuditUserAgent: () => extractAuditUserAgent,
66
+ extractClientIp: () => extractClientIp,
67
+ getBoolEnv: () => getBoolEnv,
68
+ getCorrelationId: () => getCorrelationId,
69
+ getEndSessionEndpoint: () => getEndSessionEndpoint,
70
+ getEnvSummary: () => getEnvSummary,
71
+ getIntEnv: () => getIntEnv,
72
+ getOptionalEnv: () => getOptionalEnv,
73
+ getRateLimitStatus: () => getRateLimitStatus,
74
+ getRequiredEnv: () => getRequiredEnv,
75
+ getTokenEndpoint: () => getTokenEndpoint,
76
+ hasAllRoles: () => hasAllRoles,
77
+ hasAnyRole: () => hasAnyRole,
78
+ hasRole: () => hasRole,
79
+ isAllowlisted: () => isAllowlisted,
80
+ isApiError: () => isApiError,
81
+ isTokenExpired: () => isTokenExpired,
82
+ parseKeycloakRoles: () => parseKeycloakRoles,
83
+ refreshKeycloakToken: () => refreshKeycloakToken,
84
+ resetRateLimitForKey: () => resetRateLimitForKey,
85
+ resolveIdentifier: () => resolveIdentifier,
86
+ resolveRateLimitIdentifier: () => resolveRateLimitIdentifier,
87
+ sanitizeApiError: () => sanitizeApiError,
88
+ stripHtml: () => stripHtml,
89
+ validateEnvVars: () => validateEnvVars
90
+ });
91
+ module.exports = __toCommonJS(auth_exports);
92
+
93
+ // src/auth/keycloak.ts
94
+ var KEYCLOAK_DEFAULT_ROLES = [
95
+ "offline_access",
96
+ "uma_authorization"
97
+ ];
98
+ function parseKeycloakRoles(accessToken, additionalDefaultRoles = []) {
99
+ if (!accessToken) return [];
100
+ try {
101
+ const parts = accessToken.split(".");
102
+ if (parts.length !== 3) return [];
103
+ const payload = parts[1];
104
+ const decoded = JSON.parse(atob(payload));
105
+ const realmRoles = decoded.realm_roles ?? decoded.realm_access?.roles;
106
+ if (!Array.isArray(realmRoles)) return [];
107
+ const filterSet = /* @__PURE__ */ new Set([
108
+ ...KEYCLOAK_DEFAULT_ROLES,
109
+ ...additionalDefaultRoles
110
+ ]);
111
+ return realmRoles.filter(
112
+ (role) => typeof role === "string" && !filterSet.has(role)
113
+ );
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+ function hasRole(roles, role) {
119
+ return roles?.includes(role) ?? false;
120
+ }
121
+ function hasAnyRole(roles, requiredRoles) {
122
+ if (!roles || roles.length === 0) return false;
123
+ return requiredRoles.some((role) => roles.includes(role));
124
+ }
125
+ function hasAllRoles(roles, requiredRoles) {
126
+ if (!roles || roles.length === 0) return false;
127
+ return requiredRoles.every((role) => roles.includes(role));
128
+ }
129
+ function isTokenExpired(expiresAt, bufferMs = 6e4) {
130
+ if (!expiresAt) return true;
131
+ return Date.now() >= expiresAt - bufferMs;
132
+ }
133
+ function buildTokenRefreshParams(config, refreshToken) {
134
+ return new URLSearchParams({
135
+ grant_type: "refresh_token",
136
+ client_id: config.clientId,
137
+ client_secret: config.clientSecret,
138
+ refresh_token: refreshToken
139
+ });
140
+ }
141
+ function getTokenEndpoint(issuer) {
142
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
143
+ return `${base}/protocol/openid-connect/token`;
144
+ }
145
+ function getEndSessionEndpoint(issuer) {
146
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
147
+ return `${base}/protocol/openid-connect/logout`;
148
+ }
149
+ async function refreshKeycloakToken(config, refreshToken, additionalDefaultRoles) {
150
+ try {
151
+ const endpoint = getTokenEndpoint(config.issuer);
152
+ const params = buildTokenRefreshParams(config, refreshToken);
153
+ const response = await fetch(endpoint, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
156
+ body: params
157
+ });
158
+ if (!response.ok) {
159
+ const body = await response.text().catch(() => "");
160
+ return {
161
+ ok: false,
162
+ error: `Token refresh failed: HTTP ${response.status} - ${body}`
163
+ };
164
+ }
165
+ const data = await response.json();
166
+ return {
167
+ ok: true,
168
+ tokens: {
169
+ accessToken: data.access_token,
170
+ refreshToken: data.refresh_token ?? refreshToken,
171
+ idToken: data.id_token,
172
+ expiresAt: Date.now() + data.expires_in * 1e3,
173
+ roles: parseKeycloakRoles(data.access_token, additionalDefaultRoles)
174
+ }
175
+ };
176
+ } catch (error) {
177
+ return {
178
+ ok: false,
179
+ error: error instanceof Error ? error.message : "Token refresh failed"
180
+ };
181
+ }
182
+ }
183
+
184
+ // src/auth/nextjs-keycloak.ts
185
+ function buildAuthCookies(config = {}) {
186
+ const { domain, sessionToken = true, callbackUrl = true } = config;
187
+ const isProduction = process.env.NODE_ENV === "production";
188
+ const cookieDomain = isProduction ? domain : void 0;
189
+ const baseOptions = {
190
+ httpOnly: true,
191
+ sameSite: "lax",
192
+ path: "/",
193
+ secure: isProduction,
194
+ domain: cookieDomain
195
+ };
196
+ const cookies = {
197
+ pkceCodeVerifier: {
198
+ name: "authjs.pkce.code_verifier",
199
+ options: { ...baseOptions }
200
+ },
201
+ state: {
202
+ name: "authjs.state",
203
+ options: { ...baseOptions }
204
+ }
205
+ };
206
+ if (sessionToken) {
207
+ cookies.sessionToken = {
208
+ name: isProduction ? "__Secure-authjs.session-token" : "authjs.session-token",
209
+ options: { ...baseOptions }
210
+ };
211
+ }
212
+ if (callbackUrl) {
213
+ cookies.callbackUrl = {
214
+ name: isProduction ? "__Secure-authjs.callback-url" : "authjs.callback-url",
215
+ options: { ...baseOptions }
216
+ };
217
+ }
218
+ return cookies;
219
+ }
220
+ function buildRedirectCallback(config = {}) {
221
+ const { allowWwwVariant = false } = config;
222
+ return async ({ url, baseUrl }) => {
223
+ if (url.startsWith("/")) return `${baseUrl}${url}`;
224
+ try {
225
+ if (new URL(url).origin === baseUrl) return url;
226
+ } catch {
227
+ return baseUrl;
228
+ }
229
+ if (allowWwwVariant) {
230
+ try {
231
+ const urlHost = new URL(url).hostname;
232
+ const baseHost = new URL(baseUrl).hostname;
233
+ if (urlHost === `www.${baseHost}` || baseHost === `www.${urlHost}`) {
234
+ return url;
235
+ }
236
+ } catch {
237
+ }
238
+ }
239
+ return baseUrl;
240
+ };
241
+ }
242
+ function buildKeycloakCallbacks(config) {
243
+ const {
244
+ issuer,
245
+ clientId,
246
+ clientSecret,
247
+ defaultRoles = [],
248
+ debug = process.env.NODE_ENV === "development"
249
+ } = config;
250
+ const kcConfig = { issuer, clientId, clientSecret };
251
+ function log(message, meta) {
252
+ if (debug) {
253
+ console.log(`[Auth] ${message}`, meta ? JSON.stringify(meta) : "");
254
+ }
255
+ }
256
+ return {
257
+ /**
258
+ * JWT callback — stores Keycloak tokens and handles refresh.
259
+ *
260
+ * Compatible with Auth.js v5 JWT callback signature.
261
+ */
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ async jwt({ token, user, account }) {
264
+ if (user) {
265
+ token.id = token.sub ?? user.id;
266
+ }
267
+ if (account?.provider === "keycloak") {
268
+ token.accessToken = account.access_token;
269
+ token.refreshToken = account.refresh_token;
270
+ token.idToken = account.id_token;
271
+ token.roles = parseKeycloakRoles(
272
+ account.access_token,
273
+ defaultRoles
274
+ );
275
+ token.accessTokenExpires = account.expires_at ? account.expires_at * 1e3 : Date.now() + 3e5;
276
+ return token;
277
+ }
278
+ if (!isTokenExpired(token.accessTokenExpires)) {
279
+ return token;
280
+ }
281
+ if (token.refreshToken) {
282
+ log("Token expired, attempting refresh...");
283
+ const result = await refreshKeycloakToken(
284
+ kcConfig,
285
+ token.refreshToken,
286
+ defaultRoles
287
+ );
288
+ if (result.ok) {
289
+ token.accessToken = result.tokens.accessToken;
290
+ token.idToken = result.tokens.idToken ?? token.idToken;
291
+ token.refreshToken = result.tokens.refreshToken ?? token.refreshToken;
292
+ token.accessTokenExpires = result.tokens.expiresAt;
293
+ token.roles = result.tokens.roles;
294
+ delete token.error;
295
+ log("Token refreshed OK");
296
+ return token;
297
+ }
298
+ log("Token refresh failed", { error: result.error });
299
+ return { ...token, error: "RefreshTokenError" };
300
+ }
301
+ log("Token expired but no refresh token available");
302
+ return { ...token, error: "RefreshTokenError" };
303
+ },
304
+ /**
305
+ * Session callback — maps JWT fields to the session object.
306
+ *
307
+ * Compatible with Auth.js v5 session callback signature.
308
+ */
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ async session({ session, token }) {
311
+ const user = session.user;
312
+ if (user) {
313
+ user.id = token.id || token.sub;
314
+ user.roles = token.roles || [];
315
+ }
316
+ session.idToken = token.idToken;
317
+ session.accessToken = token.accessToken;
318
+ if (token.error) {
319
+ session.error = token.error;
320
+ }
321
+ return session;
322
+ }
323
+ };
324
+ }
325
+
326
+ // src/auth/api-security.ts
327
+ var StandardRateLimitPresets = {
328
+ /** General API: 100/min, 200/min authenticated */
329
+ apiGeneral: {
330
+ limit: 100,
331
+ windowSeconds: 60,
332
+ authenticatedLimit: 200
333
+ },
334
+ /** Admin operations: 100/min (admins are trusted) */
335
+ adminAction: {
336
+ limit: 100,
337
+ windowSeconds: 60
338
+ },
339
+ /** AI/expensive operations: 20/hour, 50/hour authenticated */
340
+ aiRequest: {
341
+ limit: 20,
342
+ windowSeconds: 3600,
343
+ authenticatedLimit: 50
344
+ },
345
+ /** Auth attempts: 5/15min with 15min block */
346
+ authAttempt: {
347
+ limit: 5,
348
+ windowSeconds: 900,
349
+ blockDurationSeconds: 900
350
+ },
351
+ /** Contact/public forms: 10/hour */
352
+ publicForm: {
353
+ limit: 10,
354
+ windowSeconds: 3600,
355
+ blockDurationSeconds: 1800
356
+ },
357
+ /** Checkout/billing: 10/hour with 1hr block */
358
+ checkout: {
359
+ limit: 10,
360
+ windowSeconds: 3600,
361
+ blockDurationSeconds: 3600
362
+ }
363
+ };
364
+ function resolveRateLimitIdentifier(session, clientIp) {
365
+ if (session?.user?.id) {
366
+ return { identifier: `user:${session.user.id}`, isAuthenticated: true };
367
+ }
368
+ if (session?.user?.email) {
369
+ return {
370
+ identifier: `email:${session.user.email}`,
371
+ isAuthenticated: true
372
+ };
373
+ }
374
+ return { identifier: `ip:${clientIp}`, isAuthenticated: false };
375
+ }
376
+ function extractClientIp(getHeader) {
377
+ return getHeader("cf-connecting-ip") || getHeader("x-real-ip") || getHeader("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
378
+ }
379
+ function buildRateLimitHeaders(limit, remaining, resetAtMs) {
380
+ return {
381
+ "X-RateLimit-Limit": String(limit),
382
+ "X-RateLimit-Remaining": String(Math.max(0, remaining)),
383
+ "X-RateLimit-Reset": String(Math.ceil(resetAtMs / 1e3))
384
+ };
385
+ }
386
+ function buildErrorBody(error, extra) {
387
+ return { error, ...extra };
388
+ }
389
+ var WrapperPresets = {
390
+ /** Public route: no auth, rate limited */
391
+ public: {
392
+ requireAuth: false,
393
+ requireAdmin: false,
394
+ rateLimit: "apiGeneral"
395
+ },
396
+ /** Authenticated route: requires session */
397
+ authenticated: {
398
+ requireAuth: true,
399
+ requireAdmin: false,
400
+ rateLimit: "apiGeneral"
401
+ },
402
+ /** Admin route: requires session with admin role */
403
+ admin: {
404
+ requireAuth: true,
405
+ requireAdmin: true,
406
+ rateLimit: "adminAction"
407
+ },
408
+ /** Legacy admin: accepts session OR bearer token */
409
+ legacyAdmin: {
410
+ requireAuth: true,
411
+ requireAdmin: true,
412
+ allowBearerToken: true,
413
+ rateLimit: "adminAction"
414
+ },
415
+ /** AI/expensive: requires auth, strict rate limit */
416
+ ai: {
417
+ requireAuth: true,
418
+ requireAdmin: false,
419
+ rateLimit: "aiRequest"
420
+ },
421
+ /** Cron: no rate limit, admin-level access */
422
+ cron: {
423
+ requireAuth: true,
424
+ requireAdmin: false,
425
+ skipRateLimit: true,
426
+ skipAudit: false
427
+ }
428
+ };
429
+
430
+ // src/auth/schemas.ts
431
+ var import_zod = require("zod");
432
+
433
+ // src/security.ts
434
+ var import_crypto = require("crypto");
435
+ var URL_PROTOCOL_PATTERN = /(https?:\/\/|ftp:\/\/|www\.)\S+/i;
436
+ var URL_DOMAIN_PATTERN = /\b[\w.-]+\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\b/i;
437
+ var HTML_TAG_PATTERN = /<[^>]*>/;
438
+ function escapeHtml(str) {
439
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
440
+ }
441
+ function containsUrls(str) {
442
+ return URL_PROTOCOL_PATTERN.test(str) || URL_DOMAIN_PATTERN.test(str);
443
+ }
444
+ function containsHtml(str) {
445
+ return HTML_TAG_PATTERN.test(str);
446
+ }
447
+ function stripHtml(str) {
448
+ return str.replace(/<[^>]*>/g, "");
449
+ }
450
+ function constantTimeEqual(a, b) {
451
+ try {
452
+ const aBuf = Buffer.from(a, "utf-8");
453
+ const bBuf = Buffer.from(b, "utf-8");
454
+ if (aBuf.length !== bBuf.length) return false;
455
+ return (0, import_crypto.timingSafeEqual)(aBuf, bBuf);
456
+ } catch {
457
+ return false;
458
+ }
459
+ }
460
+ function sanitizeApiError(error, statusCode, isDevelopment = false) {
461
+ if (statusCode >= 400 && statusCode < 500) {
462
+ const message = error instanceof Error ? error.message : String(error || "Bad request");
463
+ return { message };
464
+ }
465
+ const result = {
466
+ message: "An internal error occurred. Please try again later.",
467
+ code: "INTERNAL_ERROR"
468
+ };
469
+ if (isDevelopment && error instanceof Error) {
470
+ result.stack = error.stack;
471
+ }
472
+ return result;
473
+ }
474
+ function getCorrelationId(headers) {
475
+ const get = typeof headers === "function" ? headers : (name) => {
476
+ const val = headers[name] ?? headers[name.toLowerCase()];
477
+ return Array.isArray(val) ? val[0] : val;
478
+ };
479
+ return get("x-request-id") || get("X-Request-ID") || get("x-correlation-id") || get("X-Correlation-ID") || crypto.randomUUID();
480
+ }
481
+
482
+ // src/auth/schemas.ts
483
+ var EmailSchema = import_zod.z.string().trim().toLowerCase().email("Invalid email address");
484
+ var PasswordSchema = import_zod.z.string().min(8, "Password must be at least 8 characters").max(100, "Password must be less than 100 characters");
485
+ var SlugSchema = import_zod.z.string().min(1, "Slug is required").max(100, "Slug must be less than 100 characters").regex(
486
+ /^[a-z0-9-]+$/,
487
+ "Slug can only contain lowercase letters, numbers, and hyphens"
488
+ );
489
+ var PhoneSchema = import_zod.z.string().regex(/^[\d\s()+.\-]{7,20}$/, "Invalid phone number format");
490
+ var PersonNameSchema = import_zod.z.string().min(2, "Name must be at least 2 characters").max(100, "Name must be less than 100 characters").regex(
491
+ /^[a-zA-Z\s\-']+$/,
492
+ "Name can only contain letters, spaces, hyphens and apostrophes"
493
+ );
494
+ function createSafeTextSchema(options) {
495
+ const {
496
+ min,
497
+ max,
498
+ allowHtml = false,
499
+ allowUrls = false,
500
+ fieldName = "Text"
501
+ } = options ?? {};
502
+ let schema = import_zod.z.string();
503
+ if (min !== void 0)
504
+ schema = schema.min(min, `${fieldName} must be at least ${min} characters`);
505
+ if (max !== void 0)
506
+ schema = schema.max(
507
+ max,
508
+ `${fieldName} must be less than ${max} characters`
509
+ );
510
+ if (!allowHtml && !allowUrls) {
511
+ return schema.refine((val) => !HTML_TAG_PATTERN.test(val), "HTML tags are not allowed").refine(
512
+ (val) => !URL_PROTOCOL_PATTERN.test(val),
513
+ "Links are not allowed for security reasons"
514
+ ).refine(
515
+ (val) => !URL_DOMAIN_PATTERN.test(val),
516
+ "Links are not allowed for security reasons"
517
+ );
518
+ }
519
+ if (!allowHtml) {
520
+ return schema.refine(
521
+ (val) => !HTML_TAG_PATTERN.test(val),
522
+ "HTML tags are not allowed"
523
+ );
524
+ }
525
+ if (!allowUrls) {
526
+ return schema.refine(
527
+ (val) => !URL_PROTOCOL_PATTERN.test(val),
528
+ "Links are not allowed for security reasons"
529
+ ).refine(
530
+ (val) => !URL_DOMAIN_PATTERN.test(val),
531
+ "Links are not allowed for security reasons"
532
+ );
533
+ }
534
+ return schema;
535
+ }
536
+ var PaginationSchema = import_zod.z.object({
537
+ page: import_zod.z.coerce.number().int().positive().default(1),
538
+ limit: import_zod.z.coerce.number().int().positive().max(100).default(20),
539
+ sortBy: import_zod.z.string().optional(),
540
+ sortOrder: import_zod.z.enum(["asc", "desc"]).default("desc")
541
+ });
542
+ var DateRangeSchema = import_zod.z.object({
543
+ startDate: import_zod.z.string().datetime(),
544
+ endDate: import_zod.z.string().datetime()
545
+ }).refine((data) => new Date(data.startDate) <= new Date(data.endDate), {
546
+ message: "Start date must be before end date"
547
+ });
548
+ var SearchQuerySchema = import_zod.z.object({
549
+ query: import_zod.z.string().min(1).max(200).trim(),
550
+ page: import_zod.z.coerce.number().int().positive().default(1),
551
+ limit: import_zod.z.coerce.number().int().positive().max(50).default(10)
552
+ });
553
+ var LoginSchema = import_zod.z.object({
554
+ email: EmailSchema,
555
+ password: PasswordSchema
556
+ });
557
+ var SignupSchema = import_zod.z.object({
558
+ email: EmailSchema,
559
+ password: PasswordSchema,
560
+ name: import_zod.z.string().min(2).max(100).optional()
561
+ });
562
+
563
+ // src/auth/feature-flags.ts
564
+ function detectStage() {
565
+ const stage = process.env.DEPLOYMENT_STAGE;
566
+ if (stage === "staging" || stage === "preview") return stage;
567
+ if (process.env.NODE_ENV === "production") return "production";
568
+ return "development";
569
+ }
570
+ function resolveFlagValue(value) {
571
+ if (typeof value === "boolean") return value;
572
+ const envValue = process.env[value.envVar];
573
+ if (envValue === void 0 || envValue === "") {
574
+ return value.default ?? false;
575
+ }
576
+ return envValue === "true" || envValue === "1";
577
+ }
578
+ function createFeatureFlags(definitions) {
579
+ return {
580
+ /**
581
+ * Resolve all flags for the current environment.
582
+ * Call this once at startup or per-request for dynamic flags.
583
+ */
584
+ resolve(stage) {
585
+ const currentStage = stage ?? detectStage();
586
+ const resolved = {};
587
+ for (const [key, def] of Object.entries(definitions)) {
588
+ const stageKey = currentStage === "preview" ? "staging" : currentStage;
589
+ resolved[key] = resolveFlagValue(def[stageKey]);
590
+ }
591
+ return resolved;
592
+ },
593
+ /**
594
+ * Check if a single flag is enabled.
595
+ */
596
+ isEnabled(flag, stage) {
597
+ const currentStage = stage ?? detectStage();
598
+ const def = definitions[flag];
599
+ const stageKey = currentStage === "preview" ? "staging" : currentStage;
600
+ return resolveFlagValue(def[stageKey]);
601
+ },
602
+ /**
603
+ * Get the flag definitions (for introspection/admin UI).
604
+ */
605
+ definitions
606
+ };
607
+ }
608
+ function buildAllowlist(config) {
609
+ const fromEnv = config.envVar ? process.env[config.envVar]?.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean) ?? [] : [];
610
+ const fallback = config.fallback?.map((e) => e.toLowerCase()) ?? [];
611
+ return [.../* @__PURE__ */ new Set([...fromEnv, ...fallback])];
612
+ }
613
+ function isAllowlisted(email, allowlist) {
614
+ return allowlist.includes(email.toLowerCase());
615
+ }
616
+
617
+ // src/auth/rate-limiter.ts
618
+ var CommonRateLimits = {
619
+ /** General API: 100/min, 200/min authenticated */
620
+ apiGeneral: {
621
+ limit: 100,
622
+ windowSeconds: 60,
623
+ authenticatedLimit: 200
624
+ },
625
+ /** Admin actions: 100/min */
626
+ adminAction: {
627
+ limit: 100,
628
+ windowSeconds: 60
629
+ },
630
+ /** Auth attempts: 10/15min with 30min block */
631
+ authAttempt: {
632
+ limit: 10,
633
+ windowSeconds: 900,
634
+ blockDurationSeconds: 1800
635
+ },
636
+ /** AI/expensive requests: 20/hour, 50/hour authenticated */
637
+ aiRequest: {
638
+ limit: 20,
639
+ windowSeconds: 3600,
640
+ authenticatedLimit: 50
641
+ },
642
+ /** Public form submissions: 5/hour with 1hr block */
643
+ publicForm: {
644
+ limit: 5,
645
+ windowSeconds: 3600,
646
+ blockDurationSeconds: 3600
647
+ },
648
+ /** Checkout/billing: 10/hour with 1hr block */
649
+ checkout: {
650
+ limit: 10,
651
+ windowSeconds: 3600,
652
+ blockDurationSeconds: 3600
653
+ }
654
+ };
655
+ function createMemoryRateLimitStore() {
656
+ const windows = /* @__PURE__ */ new Map();
657
+ const blocks = /* @__PURE__ */ new Map();
658
+ const cleanupInterval = setInterval(() => {
659
+ const now = Date.now();
660
+ for (const [key, entry] of windows) {
661
+ if (entry.expiresAt < now) windows.delete(key);
662
+ }
663
+ for (const [key, expiry] of blocks) {
664
+ if (expiry < now) blocks.delete(key);
665
+ }
666
+ }, 60 * 1e3);
667
+ if (cleanupInterval.unref) {
668
+ cleanupInterval.unref();
669
+ }
670
+ return {
671
+ async increment(key, windowMs, now) {
672
+ const windowStart = now - windowMs;
673
+ let entry = windows.get(key);
674
+ if (!entry) {
675
+ entry = { timestamps: [], expiresAt: now + windowMs + 6e4 };
676
+ windows.set(key, entry);
677
+ }
678
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
679
+ entry.timestamps.push(now);
680
+ entry.expiresAt = now + windowMs + 6e4;
681
+ return { count: entry.timestamps.length };
682
+ },
683
+ async isBlocked(key) {
684
+ const expiry = blocks.get(key);
685
+ if (!expiry || expiry < Date.now()) {
686
+ blocks.delete(key);
687
+ return { blocked: false, ttlMs: 0 };
688
+ }
689
+ return { blocked: true, ttlMs: expiry - Date.now() };
690
+ },
691
+ async setBlock(key, durationSeconds) {
692
+ blocks.set(key, Date.now() + durationSeconds * 1e3);
693
+ },
694
+ async reset(key) {
695
+ windows.delete(key);
696
+ blocks.delete(`block:${key}`);
697
+ blocks.delete(key);
698
+ }
699
+ };
700
+ }
701
+ var defaultStore;
702
+ function getDefaultStore() {
703
+ if (!defaultStore) {
704
+ defaultStore = createMemoryRateLimitStore();
705
+ }
706
+ return defaultStore;
707
+ }
708
+ async function checkRateLimit(operation, identifier, rule, options = {}) {
709
+ const store = options.store ?? getDefaultStore();
710
+ const limit = options.isAuthenticated && rule.authenticatedLimit ? rule.authenticatedLimit : rule.limit;
711
+ const now = Date.now();
712
+ const windowMs = rule.windowSeconds * 1e3;
713
+ const resetAt = now + windowMs;
714
+ const key = `ratelimit:${operation}:${identifier}`;
715
+ const blockKey = `ratelimit:block:${operation}:${identifier}`;
716
+ try {
717
+ if (rule.blockDurationSeconds) {
718
+ const blockStatus = await store.isBlocked(blockKey);
719
+ if (blockStatus.blocked) {
720
+ return {
721
+ allowed: false,
722
+ remaining: 0,
723
+ resetAt: now + blockStatus.ttlMs,
724
+ current: limit + 1,
725
+ limit,
726
+ retryAfterSeconds: Math.ceil(blockStatus.ttlMs / 1e3)
727
+ };
728
+ }
729
+ }
730
+ const { count } = await store.increment(key, windowMs, now);
731
+ if (count > limit) {
732
+ options.logger?.warn("Rate limit exceeded", {
733
+ operation,
734
+ identifier,
735
+ current: count,
736
+ limit
737
+ });
738
+ if (rule.blockDurationSeconds) {
739
+ await store.setBlock(blockKey, rule.blockDurationSeconds);
740
+ }
741
+ return {
742
+ allowed: false,
743
+ remaining: 0,
744
+ resetAt,
745
+ current: count,
746
+ limit,
747
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
748
+ };
749
+ }
750
+ return {
751
+ allowed: true,
752
+ remaining: limit - count,
753
+ resetAt,
754
+ current: count,
755
+ limit,
756
+ retryAfterSeconds: 0
757
+ };
758
+ } catch (error) {
759
+ options.logger?.error("Rate limit check failed, allowing request", {
760
+ error: error instanceof Error ? error.message : String(error),
761
+ operation,
762
+ identifier
763
+ });
764
+ return {
765
+ allowed: true,
766
+ remaining: limit,
767
+ resetAt,
768
+ current: 0,
769
+ limit,
770
+ retryAfterSeconds: 0
771
+ };
772
+ }
773
+ }
774
+ async function getRateLimitStatus(operation, identifier, rule, store) {
775
+ const s = store ?? getDefaultStore();
776
+ const key = `ratelimit:${operation}:${identifier}`;
777
+ const now = Date.now();
778
+ const windowMs = rule.windowSeconds * 1e3;
779
+ try {
780
+ const { count } = await s.increment(key, windowMs, now);
781
+ return {
782
+ allowed: count <= rule.limit,
783
+ remaining: Math.max(0, rule.limit - count),
784
+ resetAt: now + windowMs,
785
+ current: count,
786
+ limit: rule.limit,
787
+ retryAfterSeconds: count > rule.limit ? Math.ceil(windowMs / 1e3) : 0
788
+ };
789
+ } catch {
790
+ return null;
791
+ }
792
+ }
793
+ async function resetRateLimitForKey(operation, identifier, store) {
794
+ const s = store ?? getDefaultStore();
795
+ const key = `ratelimit:${operation}:${identifier}`;
796
+ const blockKey = `ratelimit:block:${operation}:${identifier}`;
797
+ await s.reset(key);
798
+ await s.reset(blockKey);
799
+ }
800
+ function buildRateLimitResponseHeaders(result) {
801
+ const headers = {
802
+ "X-RateLimit-Limit": String(result.limit),
803
+ "X-RateLimit-Remaining": String(result.remaining),
804
+ "X-RateLimit-Reset": String(Math.ceil(result.resetAt / 1e3))
805
+ };
806
+ if (!result.allowed) {
807
+ headers["Retry-After"] = String(result.retryAfterSeconds);
808
+ }
809
+ return headers;
810
+ }
811
+ function resolveIdentifier(session, clientIp) {
812
+ if (session?.user?.id) {
813
+ return { identifier: `user:${session.user.id}`, isAuthenticated: true };
814
+ }
815
+ if (session?.user?.email) {
816
+ return {
817
+ identifier: `email:${session.user.email}`,
818
+ isAuthenticated: true
819
+ };
820
+ }
821
+ return { identifier: `ip:${clientIp ?? "unknown"}`, isAuthenticated: false };
822
+ }
823
+
824
+ // src/auth/audit.ts
825
+ var StandardAuditActions = {
826
+ // Authentication
827
+ LOGIN_SUCCESS: "auth.login.success",
828
+ LOGIN_FAILURE: "auth.login.failure",
829
+ LOGOUT: "auth.logout",
830
+ SESSION_REFRESH: "auth.session.refresh",
831
+ PASSWORD_CHANGE: "auth.password.change",
832
+ PASSWORD_RESET: "auth.password.reset",
833
+ // Billing
834
+ CHECKOUT_START: "billing.checkout.start",
835
+ CHECKOUT_COMPLETE: "billing.checkout.complete",
836
+ SUBSCRIPTION_CREATE: "billing.subscription.create",
837
+ SUBSCRIPTION_CANCEL: "billing.subscription.cancel",
838
+ SUBSCRIPTION_UPDATE: "billing.subscription.update",
839
+ PAYMENT_FAILED: "billing.payment.failed",
840
+ // Admin
841
+ ADMIN_LOGIN: "admin.login",
842
+ ADMIN_USER_VIEW: "admin.user.view",
843
+ ADMIN_USER_UPDATE: "admin.user.update",
844
+ ADMIN_CONFIG_CHANGE: "admin.config.change",
845
+ // Security Events
846
+ RATE_LIMIT_EXCEEDED: "security.rate_limit.exceeded",
847
+ INVALID_INPUT: "security.input.invalid",
848
+ UNAUTHORIZED_ACCESS: "security.access.unauthorized",
849
+ OWNERSHIP_VIOLATION: "security.ownership.violation",
850
+ WEBHOOK_SIGNATURE_INVALID: "security.webhook.signature_invalid",
851
+ // Data
852
+ DATA_EXPORT: "data.export",
853
+ DATA_DELETE: "data.delete",
854
+ DATA_UPDATE: "data.update"
855
+ };
856
+ function extractAuditIp(request) {
857
+ if (!request) return void 0;
858
+ const headers = [
859
+ "cf-connecting-ip",
860
+ // Cloudflare
861
+ "x-real-ip",
862
+ // Nginx
863
+ "x-forwarded-for",
864
+ // Standard proxy
865
+ "x-client-ip"
866
+ // Apache
867
+ ];
868
+ for (const header of headers) {
869
+ const value = request.headers.get(header);
870
+ if (value) {
871
+ return value.split(",")[0]?.trim();
872
+ }
873
+ }
874
+ return void 0;
875
+ }
876
+ function extractAuditUserAgent(request) {
877
+ return request?.headers.get("user-agent") ?? void 0;
878
+ }
879
+ function extractAuditRequestId(request) {
880
+ return request?.headers.get("x-request-id") ?? crypto.randomUUID();
881
+ }
882
+ function createAuditActor(session) {
883
+ if (!session?.user) {
884
+ return { id: "anonymous", type: "anonymous" };
885
+ }
886
+ return {
887
+ id: session.user.id ?? session.user.email ?? "unknown",
888
+ email: session.user.email ?? void 0,
889
+ type: "user"
890
+ };
891
+ }
892
+ var defaultLogger = {
893
+ info: (msg, meta) => console.log(msg, meta ? JSON.stringify(meta) : ""),
894
+ warn: (msg, meta) => console.warn(msg, meta ? JSON.stringify(meta) : ""),
895
+ error: (msg, meta) => console.error(msg, meta ? JSON.stringify(meta) : "")
896
+ };
897
+ function createAuditLogger(options = {}) {
898
+ const { persist, logger = defaultLogger } = options;
899
+ async function log(event, request) {
900
+ const record = {
901
+ id: crypto.randomUUID(),
902
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
903
+ ip: extractAuditIp(request),
904
+ userAgent: extractAuditUserAgent(request),
905
+ requestId: extractAuditRequestId(request),
906
+ ...event
907
+ };
908
+ const logFn = event.outcome === "failure" || event.outcome === "blocked" ? logger.warn : logger.info;
909
+ logFn(`[AUDIT] ${event.action}`, {
910
+ auditId: record.id,
911
+ actor: record.actor,
912
+ action: record.action,
913
+ resource: record.resource,
914
+ outcome: record.outcome,
915
+ ip: record.ip,
916
+ metadata: record.metadata,
917
+ reason: record.reason
918
+ });
919
+ if (persist) {
920
+ try {
921
+ await persist(record);
922
+ } catch (error) {
923
+ logger.error("Failed to persist audit log", {
924
+ error: error instanceof Error ? error.message : String(error),
925
+ auditId: record.id
926
+ });
927
+ }
928
+ }
929
+ return record;
930
+ }
931
+ function createTimedAudit(event, request) {
932
+ const startTime = Date.now();
933
+ return {
934
+ success: async (metadata) => {
935
+ return log(
936
+ {
937
+ ...event,
938
+ outcome: "success",
939
+ metadata: {
940
+ ...event.metadata,
941
+ ...metadata,
942
+ durationMs: Date.now() - startTime
943
+ }
944
+ },
945
+ request
946
+ );
947
+ },
948
+ failure: async (reason, metadata) => {
949
+ return log(
950
+ {
951
+ ...event,
952
+ outcome: "failure",
953
+ reason,
954
+ metadata: {
955
+ ...event.metadata,
956
+ ...metadata,
957
+ durationMs: Date.now() - startTime
958
+ }
959
+ },
960
+ request
961
+ );
962
+ },
963
+ blocked: async (reason, metadata) => {
964
+ return log(
965
+ {
966
+ ...event,
967
+ outcome: "blocked",
968
+ reason,
969
+ metadata: {
970
+ ...event.metadata,
971
+ ...metadata,
972
+ durationMs: Date.now() - startTime
973
+ }
974
+ },
975
+ request
976
+ );
977
+ }
978
+ };
979
+ }
980
+ return { log, createTimedAudit };
981
+ }
982
+
983
+ // src/api.ts
984
+ var ApiErrorCode = {
985
+ VALIDATION_ERROR: "VALIDATION_ERROR",
986
+ UNAUTHORIZED: "UNAUTHORIZED",
987
+ FORBIDDEN: "FORBIDDEN",
988
+ NOT_FOUND: "NOT_FOUND",
989
+ CONFLICT: "CONFLICT",
990
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
991
+ INTERNAL_ERROR: "INTERNAL_ERROR",
992
+ DATABASE_ERROR: "DATABASE_ERROR",
993
+ EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
994
+ CONFIGURATION_ERROR: "CONFIGURATION_ERROR"
995
+ };
996
+ var ApiError = class extends Error {
997
+ statusCode;
998
+ code;
999
+ details;
1000
+ constructor(statusCode, message, code = ApiErrorCode.INTERNAL_ERROR, details) {
1001
+ super(message);
1002
+ this.name = "ApiError";
1003
+ this.statusCode = statusCode;
1004
+ this.code = code;
1005
+ this.details = details;
1006
+ }
1007
+ };
1008
+ function isApiError(error) {
1009
+ return error instanceof ApiError;
1010
+ }
1011
+ var CommonApiErrors = {
1012
+ unauthorized: (msg = "Unauthorized") => new ApiError(401, msg, ApiErrorCode.UNAUTHORIZED),
1013
+ forbidden: (msg = "Forbidden") => new ApiError(403, msg, ApiErrorCode.FORBIDDEN),
1014
+ notFound: (resource = "Resource") => new ApiError(404, `${resource} not found`, ApiErrorCode.NOT_FOUND),
1015
+ conflict: (msg = "Resource already exists") => new ApiError(409, msg, ApiErrorCode.CONFLICT),
1016
+ rateLimitExceeded: (msg = "Rate limit exceeded") => new ApiError(429, msg, ApiErrorCode.RATE_LIMIT_EXCEEDED),
1017
+ validationError: (details) => new ApiError(
1018
+ 400,
1019
+ "Validation failed",
1020
+ ApiErrorCode.VALIDATION_ERROR,
1021
+ details
1022
+ ),
1023
+ internalError: (msg = "Internal server error") => new ApiError(500, msg, ApiErrorCode.INTERNAL_ERROR)
1024
+ };
1025
+ var PG_ERROR_MAP = {
1026
+ "23505": {
1027
+ status: 409,
1028
+ code: ApiErrorCode.CONFLICT,
1029
+ message: "Resource already exists"
1030
+ },
1031
+ "23503": {
1032
+ status: 404,
1033
+ code: ApiErrorCode.NOT_FOUND,
1034
+ message: "Referenced resource not found"
1035
+ },
1036
+ PGRST116: {
1037
+ status: 404,
1038
+ code: ApiErrorCode.NOT_FOUND,
1039
+ message: "Resource not found"
1040
+ }
1041
+ };
1042
+ function classifyError(error, isDev = false) {
1043
+ if (isApiError(error)) {
1044
+ return {
1045
+ status: error.statusCode,
1046
+ body: { error: error.message, code: error.code, details: error.details }
1047
+ };
1048
+ }
1049
+ if (error && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
1050
+ return {
1051
+ status: 400,
1052
+ body: {
1053
+ error: "Validation failed",
1054
+ code: ApiErrorCode.VALIDATION_ERROR,
1055
+ details: error.issues.map((i) => ({
1056
+ field: i.path?.join("."),
1057
+ message: i.message
1058
+ }))
1059
+ }
1060
+ };
1061
+ }
1062
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1063
+ const pgCode = error.code;
1064
+ const mapped = PG_ERROR_MAP[pgCode];
1065
+ if (mapped) {
1066
+ return {
1067
+ status: mapped.status,
1068
+ body: { error: mapped.message, code: mapped.code }
1069
+ };
1070
+ }
1071
+ return {
1072
+ status: 500,
1073
+ body: {
1074
+ error: "Database error",
1075
+ code: ApiErrorCode.DATABASE_ERROR,
1076
+ details: isDev ? error.message : void 0
1077
+ }
1078
+ };
1079
+ }
1080
+ if (error instanceof Error) {
1081
+ return {
1082
+ status: 500,
1083
+ body: {
1084
+ error: isDev ? error.message : "Internal server error",
1085
+ code: ApiErrorCode.INTERNAL_ERROR,
1086
+ details: isDev ? error.stack : void 0
1087
+ }
1088
+ };
1089
+ }
1090
+ return {
1091
+ status: 500,
1092
+ body: {
1093
+ error: "An unexpected error occurred",
1094
+ code: ApiErrorCode.INTERNAL_ERROR
1095
+ }
1096
+ };
1097
+ }
1098
+ function buildPagination(page, limit, total) {
1099
+ return {
1100
+ page,
1101
+ limit,
1102
+ total,
1103
+ totalPages: Math.ceil(total / limit),
1104
+ hasMore: page * limit < total
1105
+ };
1106
+ }
1107
+
1108
+ // src/env.ts
1109
+ function getRequiredEnv(key) {
1110
+ const value = process.env[key];
1111
+ if (!value) {
1112
+ throw new Error(`Missing required environment variable: ${key}`);
1113
+ }
1114
+ return value;
1115
+ }
1116
+ function getOptionalEnv(key, defaultValue) {
1117
+ return process.env[key] || defaultValue;
1118
+ }
1119
+ function getBoolEnv(key, defaultValue = false) {
1120
+ const value = process.env[key];
1121
+ if (value === void 0 || value === "") return defaultValue;
1122
+ return value === "true" || value === "1";
1123
+ }
1124
+ function getIntEnv(key, defaultValue) {
1125
+ const value = process.env[key];
1126
+ if (value === void 0 || value === "") return defaultValue;
1127
+ const parsed = parseInt(value, 10);
1128
+ return isNaN(parsed) ? defaultValue : parsed;
1129
+ }
1130
+ function validateEnvVars(config) {
1131
+ const result = checkEnvVars(config);
1132
+ if (!result.valid) {
1133
+ const lines = [];
1134
+ if (result.missing.length > 0) {
1135
+ lines.push(
1136
+ "Missing required environment variables:",
1137
+ ...result.missing.map((v) => ` - ${v}`)
1138
+ );
1139
+ }
1140
+ if (result.missingOneOf.length > 0) {
1141
+ for (const group of result.missingOneOf) {
1142
+ lines.push(`Missing one of: ${group.join(" | ")}`);
1143
+ }
1144
+ }
1145
+ if (result.invalid.length > 0) {
1146
+ lines.push(
1147
+ "Invalid environment variables:",
1148
+ ...result.invalid.map((v) => ` - ${v.key}: ${v.reason}`)
1149
+ );
1150
+ }
1151
+ throw new Error(lines.join("\n"));
1152
+ }
1153
+ }
1154
+ function checkEnvVars(config) {
1155
+ const missing = [];
1156
+ const invalid = [];
1157
+ const missingOneOf = [];
1158
+ if (config.required) {
1159
+ for (const key of config.required) {
1160
+ if (!process.env[key]) {
1161
+ missing.push(key);
1162
+ }
1163
+ }
1164
+ }
1165
+ if (config.requireOneOf) {
1166
+ for (const group of config.requireOneOf) {
1167
+ const hasAny = group.some((key) => !!process.env[key]);
1168
+ if (!hasAny) {
1169
+ missingOneOf.push(group);
1170
+ }
1171
+ }
1172
+ }
1173
+ if (config.validators) {
1174
+ for (const [key, validator] of Object.entries(config.validators)) {
1175
+ const value = process.env[key];
1176
+ if (value) {
1177
+ const result = validator(value);
1178
+ if (result !== true) {
1179
+ invalid.push({ key, reason: result });
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ return {
1185
+ valid: missing.length === 0 && invalid.length === 0 && missingOneOf.length === 0,
1186
+ missing,
1187
+ invalid,
1188
+ missingOneOf
1189
+ };
1190
+ }
1191
+ function getEnvSummary(keys) {
1192
+ const summary = {};
1193
+ for (const key of keys) {
1194
+ summary[key] = !!process.env[key];
1195
+ }
1196
+ return summary;
1197
+ }
1198
+ // Annotate the CommonJS export names for ESM import in node:
1199
+ 0 && (module.exports = {
1200
+ ApiError,
1201
+ ApiErrorCode,
1202
+ CommonApiErrors,
1203
+ CommonRateLimits,
1204
+ DateRangeSchema,
1205
+ EmailSchema,
1206
+ KEYCLOAK_DEFAULT_ROLES,
1207
+ LoginSchema,
1208
+ PaginationSchema,
1209
+ PasswordSchema,
1210
+ PersonNameSchema,
1211
+ PhoneSchema,
1212
+ SearchQuerySchema,
1213
+ SignupSchema,
1214
+ SlugSchema,
1215
+ StandardAuditActions,
1216
+ StandardRateLimitPresets,
1217
+ WrapperPresets,
1218
+ buildAllowlist,
1219
+ buildAuthCookies,
1220
+ buildErrorBody,
1221
+ buildKeycloakCallbacks,
1222
+ buildPagination,
1223
+ buildRateLimitHeaders,
1224
+ buildRateLimitResponseHeaders,
1225
+ buildRedirectCallback,
1226
+ buildTokenRefreshParams,
1227
+ checkEnvVars,
1228
+ checkRateLimit,
1229
+ classifyError,
1230
+ constantTimeEqual,
1231
+ containsHtml,
1232
+ containsUrls,
1233
+ createAuditActor,
1234
+ createAuditLogger,
1235
+ createFeatureFlags,
1236
+ createMemoryRateLimitStore,
1237
+ createSafeTextSchema,
1238
+ detectStage,
1239
+ escapeHtml,
1240
+ extractAuditIp,
1241
+ extractAuditRequestId,
1242
+ extractAuditUserAgent,
1243
+ extractClientIp,
1244
+ getBoolEnv,
1245
+ getCorrelationId,
1246
+ getEndSessionEndpoint,
1247
+ getEnvSummary,
1248
+ getIntEnv,
1249
+ getOptionalEnv,
1250
+ getRateLimitStatus,
1251
+ getRequiredEnv,
1252
+ getTokenEndpoint,
1253
+ hasAllRoles,
1254
+ hasAnyRole,
1255
+ hasRole,
1256
+ isAllowlisted,
1257
+ isApiError,
1258
+ isTokenExpired,
1259
+ parseKeycloakRoles,
1260
+ refreshKeycloakToken,
1261
+ resetRateLimitForKey,
1262
+ resolveIdentifier,
1263
+ resolveRateLimitIdentifier,
1264
+ sanitizeApiError,
1265
+ stripHtml,
1266
+ validateEnvVars
1267
+ });
1268
+ //# sourceMappingURL=auth.js.map