@digilogiclabs/platform-core 1.4.0 → 1.5.1

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,973 @@
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
+ CommonRateLimits: () => CommonRateLimits,
24
+ DateRangeSchema: () => DateRangeSchema,
25
+ EmailSchema: () => EmailSchema,
26
+ KEYCLOAK_DEFAULT_ROLES: () => KEYCLOAK_DEFAULT_ROLES,
27
+ LoginSchema: () => LoginSchema,
28
+ PaginationSchema: () => PaginationSchema,
29
+ PasswordSchema: () => PasswordSchema,
30
+ PersonNameSchema: () => PersonNameSchema,
31
+ PhoneSchema: () => PhoneSchema,
32
+ SearchQuerySchema: () => SearchQuerySchema,
33
+ SignupSchema: () => SignupSchema,
34
+ SlugSchema: () => SlugSchema,
35
+ StandardAuditActions: () => StandardAuditActions,
36
+ StandardRateLimitPresets: () => StandardRateLimitPresets,
37
+ WrapperPresets: () => WrapperPresets,
38
+ buildAllowlist: () => buildAllowlist,
39
+ buildAuthCookies: () => buildAuthCookies,
40
+ buildErrorBody: () => buildErrorBody,
41
+ buildKeycloakCallbacks: () => buildKeycloakCallbacks,
42
+ buildRateLimitHeaders: () => buildRateLimitHeaders,
43
+ buildRateLimitResponseHeaders: () => buildRateLimitResponseHeaders,
44
+ buildRedirectCallback: () => buildRedirectCallback,
45
+ buildTokenRefreshParams: () => buildTokenRefreshParams,
46
+ checkRateLimit: () => checkRateLimit,
47
+ createAuditActor: () => createAuditActor,
48
+ createAuditLogger: () => createAuditLogger,
49
+ createFeatureFlags: () => createFeatureFlags,
50
+ createMemoryRateLimitStore: () => createMemoryRateLimitStore,
51
+ createSafeTextSchema: () => createSafeTextSchema,
52
+ detectStage: () => detectStage,
53
+ extractAuditIp: () => extractAuditIp,
54
+ extractAuditRequestId: () => extractAuditRequestId,
55
+ extractAuditUserAgent: () => extractAuditUserAgent,
56
+ extractClientIp: () => extractClientIp,
57
+ getEndSessionEndpoint: () => getEndSessionEndpoint,
58
+ getRateLimitStatus: () => getRateLimitStatus,
59
+ getTokenEndpoint: () => getTokenEndpoint,
60
+ hasAllRoles: () => hasAllRoles,
61
+ hasAnyRole: () => hasAnyRole,
62
+ hasRole: () => hasRole,
63
+ isAllowlisted: () => isAllowlisted,
64
+ isTokenExpired: () => isTokenExpired,
65
+ parseKeycloakRoles: () => parseKeycloakRoles,
66
+ refreshKeycloakToken: () => refreshKeycloakToken,
67
+ resetRateLimitForKey: () => resetRateLimitForKey,
68
+ resolveIdentifier: () => resolveIdentifier,
69
+ resolveRateLimitIdentifier: () => resolveRateLimitIdentifier
70
+ });
71
+ module.exports = __toCommonJS(auth_exports);
72
+
73
+ // src/auth/keycloak.ts
74
+ var KEYCLOAK_DEFAULT_ROLES = [
75
+ "offline_access",
76
+ "uma_authorization"
77
+ ];
78
+ function parseKeycloakRoles(accessToken, additionalDefaultRoles = []) {
79
+ if (!accessToken) return [];
80
+ try {
81
+ const parts = accessToken.split(".");
82
+ if (parts.length !== 3) return [];
83
+ const payload = parts[1];
84
+ const decoded = JSON.parse(atob(payload));
85
+ const realmRoles = decoded.realm_roles ?? decoded.realm_access?.roles;
86
+ if (!Array.isArray(realmRoles)) return [];
87
+ const filterSet = /* @__PURE__ */ new Set([
88
+ ...KEYCLOAK_DEFAULT_ROLES,
89
+ ...additionalDefaultRoles
90
+ ]);
91
+ return realmRoles.filter(
92
+ (role) => typeof role === "string" && !filterSet.has(role)
93
+ );
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+ function hasRole(roles, role) {
99
+ return roles?.includes(role) ?? false;
100
+ }
101
+ function hasAnyRole(roles, requiredRoles) {
102
+ if (!roles || roles.length === 0) return false;
103
+ return requiredRoles.some((role) => roles.includes(role));
104
+ }
105
+ function hasAllRoles(roles, requiredRoles) {
106
+ if (!roles || roles.length === 0) return false;
107
+ return requiredRoles.every((role) => roles.includes(role));
108
+ }
109
+ function isTokenExpired(expiresAt, bufferMs = 6e4) {
110
+ if (!expiresAt) return true;
111
+ return Date.now() >= expiresAt - bufferMs;
112
+ }
113
+ function buildTokenRefreshParams(config, refreshToken) {
114
+ return new URLSearchParams({
115
+ grant_type: "refresh_token",
116
+ client_id: config.clientId,
117
+ client_secret: config.clientSecret,
118
+ refresh_token: refreshToken
119
+ });
120
+ }
121
+ function getTokenEndpoint(issuer) {
122
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
123
+ return `${base}/protocol/openid-connect/token`;
124
+ }
125
+ function getEndSessionEndpoint(issuer) {
126
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
127
+ return `${base}/protocol/openid-connect/logout`;
128
+ }
129
+ async function refreshKeycloakToken(config, refreshToken, additionalDefaultRoles) {
130
+ try {
131
+ const endpoint = getTokenEndpoint(config.issuer);
132
+ const params = buildTokenRefreshParams(config, refreshToken);
133
+ const response = await fetch(endpoint, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
136
+ body: params
137
+ });
138
+ if (!response.ok) {
139
+ const body = await response.text().catch(() => "");
140
+ return {
141
+ ok: false,
142
+ error: `Token refresh failed: HTTP ${response.status} - ${body}`
143
+ };
144
+ }
145
+ const data = await response.json();
146
+ return {
147
+ ok: true,
148
+ tokens: {
149
+ accessToken: data.access_token,
150
+ refreshToken: data.refresh_token ?? refreshToken,
151
+ idToken: data.id_token,
152
+ expiresAt: Date.now() + data.expires_in * 1e3,
153
+ roles: parseKeycloakRoles(data.access_token, additionalDefaultRoles)
154
+ }
155
+ };
156
+ } catch (error) {
157
+ return {
158
+ ok: false,
159
+ error: error instanceof Error ? error.message : "Token refresh failed"
160
+ };
161
+ }
162
+ }
163
+
164
+ // src/auth/nextjs-keycloak.ts
165
+ function buildAuthCookies(config = {}) {
166
+ const { domain, sessionToken = true, callbackUrl = true } = config;
167
+ const isProduction = process.env.NODE_ENV === "production";
168
+ const cookieDomain = isProduction ? domain : void 0;
169
+ const baseOptions = {
170
+ httpOnly: true,
171
+ sameSite: "lax",
172
+ path: "/",
173
+ secure: isProduction,
174
+ domain: cookieDomain
175
+ };
176
+ const cookies = {
177
+ pkceCodeVerifier: {
178
+ name: "authjs.pkce.code_verifier",
179
+ options: { ...baseOptions }
180
+ },
181
+ state: {
182
+ name: "authjs.state",
183
+ options: { ...baseOptions }
184
+ }
185
+ };
186
+ if (sessionToken) {
187
+ cookies.sessionToken = {
188
+ name: isProduction ? "__Secure-authjs.session-token" : "authjs.session-token",
189
+ options: { ...baseOptions }
190
+ };
191
+ }
192
+ if (callbackUrl) {
193
+ cookies.callbackUrl = {
194
+ name: isProduction ? "__Secure-authjs.callback-url" : "authjs.callback-url",
195
+ options: { ...baseOptions }
196
+ };
197
+ }
198
+ return cookies;
199
+ }
200
+ function buildRedirectCallback(config = {}) {
201
+ const { allowWwwVariant = false } = config;
202
+ return async ({ url, baseUrl }) => {
203
+ if (url.startsWith("/")) return `${baseUrl}${url}`;
204
+ try {
205
+ if (new URL(url).origin === baseUrl) return url;
206
+ } catch {
207
+ return baseUrl;
208
+ }
209
+ if (allowWwwVariant) {
210
+ try {
211
+ const urlHost = new URL(url).hostname;
212
+ const baseHost = new URL(baseUrl).hostname;
213
+ if (urlHost === `www.${baseHost}` || baseHost === `www.${urlHost}`) {
214
+ return url;
215
+ }
216
+ } catch {
217
+ }
218
+ }
219
+ return baseUrl;
220
+ };
221
+ }
222
+ function buildKeycloakCallbacks(config) {
223
+ const {
224
+ issuer,
225
+ clientId,
226
+ clientSecret,
227
+ defaultRoles = [],
228
+ debug = process.env.NODE_ENV === "development"
229
+ } = config;
230
+ const kcConfig = { issuer, clientId, clientSecret };
231
+ function log(message, meta) {
232
+ if (debug) {
233
+ console.log(`[Auth] ${message}`, meta ? JSON.stringify(meta) : "");
234
+ }
235
+ }
236
+ return {
237
+ /**
238
+ * JWT callback — stores Keycloak tokens and handles refresh.
239
+ *
240
+ * Compatible with Auth.js v5 JWT callback signature.
241
+ */
242
+ async jwt({
243
+ token,
244
+ user,
245
+ account
246
+ }) {
247
+ if (user) {
248
+ token.id = token.sub ?? user.id;
249
+ }
250
+ if (account?.provider === "keycloak") {
251
+ token.accessToken = account.access_token;
252
+ token.refreshToken = account.refresh_token;
253
+ token.idToken = account.id_token;
254
+ token.roles = parseKeycloakRoles(
255
+ account.access_token,
256
+ defaultRoles
257
+ );
258
+ token.accessTokenExpires = account.expires_at ? account.expires_at * 1e3 : Date.now() + 3e5;
259
+ return token;
260
+ }
261
+ if (!isTokenExpired(token.accessTokenExpires)) {
262
+ return token;
263
+ }
264
+ if (token.refreshToken) {
265
+ log("Token expired, attempting refresh...");
266
+ const result = await refreshKeycloakToken(
267
+ kcConfig,
268
+ token.refreshToken,
269
+ defaultRoles
270
+ );
271
+ if (result.ok) {
272
+ token.accessToken = result.tokens.accessToken;
273
+ token.idToken = result.tokens.idToken ?? token.idToken;
274
+ token.refreshToken = result.tokens.refreshToken ?? token.refreshToken;
275
+ token.accessTokenExpires = result.tokens.expiresAt;
276
+ token.roles = result.tokens.roles;
277
+ delete token.error;
278
+ log("Token refreshed OK");
279
+ return token;
280
+ }
281
+ log("Token refresh failed", { error: result.error });
282
+ return { ...token, error: "RefreshTokenError" };
283
+ }
284
+ log("Token expired but no refresh token available");
285
+ return { ...token, error: "RefreshTokenError" };
286
+ },
287
+ /**
288
+ * Session callback — maps JWT fields to the session object.
289
+ *
290
+ * Compatible with Auth.js v5 session callback signature.
291
+ */
292
+ async session({
293
+ session,
294
+ token
295
+ }) {
296
+ const user = session.user;
297
+ if (user) {
298
+ user.id = token.id || token.sub;
299
+ user.roles = token.roles || [];
300
+ }
301
+ session.idToken = token.idToken;
302
+ session.accessToken = token.accessToken;
303
+ if (token.error) {
304
+ session.error = token.error;
305
+ }
306
+ return session;
307
+ }
308
+ };
309
+ }
310
+
311
+ // src/auth/api-security.ts
312
+ var StandardRateLimitPresets = {
313
+ /** General API: 100/min, 200/min authenticated */
314
+ apiGeneral: {
315
+ limit: 100,
316
+ windowSeconds: 60,
317
+ authenticatedLimit: 200
318
+ },
319
+ /** Admin operations: 100/min (admins are trusted) */
320
+ adminAction: {
321
+ limit: 100,
322
+ windowSeconds: 60
323
+ },
324
+ /** AI/expensive operations: 20/hour, 50/hour authenticated */
325
+ aiRequest: {
326
+ limit: 20,
327
+ windowSeconds: 3600,
328
+ authenticatedLimit: 50
329
+ },
330
+ /** Auth attempts: 5/15min with 15min block */
331
+ authAttempt: {
332
+ limit: 5,
333
+ windowSeconds: 900,
334
+ blockDurationSeconds: 900
335
+ },
336
+ /** Contact/public forms: 10/hour */
337
+ publicForm: {
338
+ limit: 10,
339
+ windowSeconds: 3600,
340
+ blockDurationSeconds: 1800
341
+ },
342
+ /** Checkout/billing: 10/hour with 1hr block */
343
+ checkout: {
344
+ limit: 10,
345
+ windowSeconds: 3600,
346
+ blockDurationSeconds: 3600
347
+ }
348
+ };
349
+ function resolveRateLimitIdentifier(session, clientIp) {
350
+ if (session?.user?.id) {
351
+ return { identifier: `user:${session.user.id}`, isAuthenticated: true };
352
+ }
353
+ if (session?.user?.email) {
354
+ return {
355
+ identifier: `email:${session.user.email}`,
356
+ isAuthenticated: true
357
+ };
358
+ }
359
+ return { identifier: `ip:${clientIp}`, isAuthenticated: false };
360
+ }
361
+ function extractClientIp(getHeader) {
362
+ return getHeader("cf-connecting-ip") || getHeader("x-real-ip") || getHeader("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
363
+ }
364
+ function buildRateLimitHeaders(limit, remaining, resetAtMs) {
365
+ return {
366
+ "X-RateLimit-Limit": String(limit),
367
+ "X-RateLimit-Remaining": String(Math.max(0, remaining)),
368
+ "X-RateLimit-Reset": String(Math.ceil(resetAtMs / 1e3))
369
+ };
370
+ }
371
+ function buildErrorBody(error, extra) {
372
+ return { error, ...extra };
373
+ }
374
+ var WrapperPresets = {
375
+ /** Public route: no auth, rate limited */
376
+ public: {
377
+ requireAuth: false,
378
+ requireAdmin: false,
379
+ rateLimit: "apiGeneral"
380
+ },
381
+ /** Authenticated route: requires session */
382
+ authenticated: {
383
+ requireAuth: true,
384
+ requireAdmin: false,
385
+ rateLimit: "apiGeneral"
386
+ },
387
+ /** Admin route: requires session with admin role */
388
+ admin: {
389
+ requireAuth: true,
390
+ requireAdmin: true,
391
+ rateLimit: "adminAction"
392
+ },
393
+ /** Legacy admin: accepts session OR bearer token */
394
+ legacyAdmin: {
395
+ requireAuth: true,
396
+ requireAdmin: true,
397
+ allowBearerToken: true,
398
+ rateLimit: "adminAction"
399
+ },
400
+ /** AI/expensive: requires auth, strict rate limit */
401
+ ai: {
402
+ requireAuth: true,
403
+ requireAdmin: false,
404
+ rateLimit: "aiRequest"
405
+ },
406
+ /** Cron: no rate limit, admin-level access */
407
+ cron: {
408
+ requireAuth: true,
409
+ requireAdmin: false,
410
+ skipRateLimit: true,
411
+ skipAudit: false
412
+ }
413
+ };
414
+
415
+ // src/auth/schemas.ts
416
+ var import_zod = require("zod");
417
+
418
+ // src/security.ts
419
+ var URL_PROTOCOL_PATTERN = /(https?:\/\/|ftp:\/\/|www\.)\S+/i;
420
+ var URL_DOMAIN_PATTERN = /\b[\w.-]+\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\b/i;
421
+ var HTML_TAG_PATTERN = /<[^>]*>/;
422
+
423
+ // src/auth/schemas.ts
424
+ var EmailSchema = import_zod.z.string().trim().toLowerCase().email("Invalid email address");
425
+ var PasswordSchema = import_zod.z.string().min(8, "Password must be at least 8 characters").max(100, "Password must be less than 100 characters");
426
+ var SlugSchema = import_zod.z.string().min(1, "Slug is required").max(100, "Slug must be less than 100 characters").regex(
427
+ /^[a-z0-9-]+$/,
428
+ "Slug can only contain lowercase letters, numbers, and hyphens"
429
+ );
430
+ var PhoneSchema = import_zod.z.string().regex(/^[\d\s()+.\-]{7,20}$/, "Invalid phone number format");
431
+ 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(
432
+ /^[a-zA-Z\s\-']+$/,
433
+ "Name can only contain letters, spaces, hyphens and apostrophes"
434
+ );
435
+ function createSafeTextSchema(options) {
436
+ const {
437
+ min,
438
+ max,
439
+ allowHtml = false,
440
+ allowUrls = false,
441
+ fieldName = "Text"
442
+ } = options ?? {};
443
+ let schema = import_zod.z.string();
444
+ if (min !== void 0)
445
+ schema = schema.min(min, `${fieldName} must be at least ${min} characters`);
446
+ if (max !== void 0)
447
+ schema = schema.max(
448
+ max,
449
+ `${fieldName} must be less than ${max} characters`
450
+ );
451
+ if (!allowHtml && !allowUrls) {
452
+ return schema.refine((val) => !HTML_TAG_PATTERN.test(val), "HTML tags are not allowed").refine(
453
+ (val) => !URL_PROTOCOL_PATTERN.test(val),
454
+ "Links are not allowed for security reasons"
455
+ ).refine(
456
+ (val) => !URL_DOMAIN_PATTERN.test(val),
457
+ "Links are not allowed for security reasons"
458
+ );
459
+ }
460
+ if (!allowHtml) {
461
+ return schema.refine(
462
+ (val) => !HTML_TAG_PATTERN.test(val),
463
+ "HTML tags are not allowed"
464
+ );
465
+ }
466
+ if (!allowUrls) {
467
+ return schema.refine(
468
+ (val) => !URL_PROTOCOL_PATTERN.test(val),
469
+ "Links are not allowed for security reasons"
470
+ ).refine(
471
+ (val) => !URL_DOMAIN_PATTERN.test(val),
472
+ "Links are not allowed for security reasons"
473
+ );
474
+ }
475
+ return schema;
476
+ }
477
+ var PaginationSchema = import_zod.z.object({
478
+ page: import_zod.z.coerce.number().int().positive().default(1),
479
+ limit: import_zod.z.coerce.number().int().positive().max(100).default(20),
480
+ sortBy: import_zod.z.string().optional(),
481
+ sortOrder: import_zod.z.enum(["asc", "desc"]).default("desc")
482
+ });
483
+ var DateRangeSchema = import_zod.z.object({
484
+ startDate: import_zod.z.string().datetime(),
485
+ endDate: import_zod.z.string().datetime()
486
+ }).refine((data) => new Date(data.startDate) <= new Date(data.endDate), {
487
+ message: "Start date must be before end date"
488
+ });
489
+ var SearchQuerySchema = import_zod.z.object({
490
+ query: import_zod.z.string().min(1).max(200).trim(),
491
+ page: import_zod.z.coerce.number().int().positive().default(1),
492
+ limit: import_zod.z.coerce.number().int().positive().max(50).default(10)
493
+ });
494
+ var LoginSchema = import_zod.z.object({
495
+ email: EmailSchema,
496
+ password: PasswordSchema
497
+ });
498
+ var SignupSchema = import_zod.z.object({
499
+ email: EmailSchema,
500
+ password: PasswordSchema,
501
+ name: import_zod.z.string().min(2).max(100).optional()
502
+ });
503
+
504
+ // src/auth/feature-flags.ts
505
+ function detectStage() {
506
+ const stage = process.env.DEPLOYMENT_STAGE;
507
+ if (stage === "staging" || stage === "preview") return stage;
508
+ if (process.env.NODE_ENV === "production") return "production";
509
+ return "development";
510
+ }
511
+ function resolveFlagValue(value) {
512
+ if (typeof value === "boolean") return value;
513
+ const envValue = process.env[value.envVar];
514
+ if (envValue === void 0 || envValue === "") {
515
+ return value.default ?? false;
516
+ }
517
+ return envValue === "true" || envValue === "1";
518
+ }
519
+ function createFeatureFlags(definitions) {
520
+ return {
521
+ /**
522
+ * Resolve all flags for the current environment.
523
+ * Call this once at startup or per-request for dynamic flags.
524
+ */
525
+ resolve(stage) {
526
+ const currentStage = stage ?? detectStage();
527
+ const resolved = {};
528
+ for (const [key, def] of Object.entries(definitions)) {
529
+ const stageKey = currentStage === "preview" ? "staging" : currentStage;
530
+ resolved[key] = resolveFlagValue(def[stageKey]);
531
+ }
532
+ return resolved;
533
+ },
534
+ /**
535
+ * Check if a single flag is enabled.
536
+ */
537
+ isEnabled(flag, stage) {
538
+ const currentStage = stage ?? detectStage();
539
+ const def = definitions[flag];
540
+ const stageKey = currentStage === "preview" ? "staging" : currentStage;
541
+ return resolveFlagValue(def[stageKey]);
542
+ },
543
+ /**
544
+ * Get the flag definitions (for introspection/admin UI).
545
+ */
546
+ definitions
547
+ };
548
+ }
549
+ function buildAllowlist(config) {
550
+ const fromEnv = config.envVar ? process.env[config.envVar]?.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean) ?? [] : [];
551
+ const fallback = config.fallback?.map((e) => e.toLowerCase()) ?? [];
552
+ return [.../* @__PURE__ */ new Set([...fromEnv, ...fallback])];
553
+ }
554
+ function isAllowlisted(email, allowlist) {
555
+ return allowlist.includes(email.toLowerCase());
556
+ }
557
+
558
+ // src/auth/rate-limiter.ts
559
+ var CommonRateLimits = {
560
+ /** General API: 100/min, 200/min authenticated */
561
+ apiGeneral: {
562
+ limit: 100,
563
+ windowSeconds: 60,
564
+ authenticatedLimit: 200
565
+ },
566
+ /** Admin actions: 100/min */
567
+ adminAction: {
568
+ limit: 100,
569
+ windowSeconds: 60
570
+ },
571
+ /** Auth attempts: 10/15min with 30min block */
572
+ authAttempt: {
573
+ limit: 10,
574
+ windowSeconds: 900,
575
+ blockDurationSeconds: 1800
576
+ },
577
+ /** AI/expensive requests: 20/hour, 50/hour authenticated */
578
+ aiRequest: {
579
+ limit: 20,
580
+ windowSeconds: 3600,
581
+ authenticatedLimit: 50
582
+ },
583
+ /** Public form submissions: 5/hour with 1hr block */
584
+ publicForm: {
585
+ limit: 5,
586
+ windowSeconds: 3600,
587
+ blockDurationSeconds: 3600
588
+ },
589
+ /** Checkout/billing: 10/hour with 1hr block */
590
+ checkout: {
591
+ limit: 10,
592
+ windowSeconds: 3600,
593
+ blockDurationSeconds: 3600
594
+ }
595
+ };
596
+ function createMemoryRateLimitStore() {
597
+ const windows = /* @__PURE__ */ new Map();
598
+ const blocks = /* @__PURE__ */ new Map();
599
+ const cleanupInterval = setInterval(() => {
600
+ const now = Date.now();
601
+ for (const [key, entry] of windows) {
602
+ if (entry.expiresAt < now) windows.delete(key);
603
+ }
604
+ for (const [key, expiry] of blocks) {
605
+ if (expiry < now) blocks.delete(key);
606
+ }
607
+ }, 60 * 1e3);
608
+ if (cleanupInterval.unref) {
609
+ cleanupInterval.unref();
610
+ }
611
+ return {
612
+ async increment(key, windowMs, now) {
613
+ const windowStart = now - windowMs;
614
+ let entry = windows.get(key);
615
+ if (!entry) {
616
+ entry = { timestamps: [], expiresAt: now + windowMs + 6e4 };
617
+ windows.set(key, entry);
618
+ }
619
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
620
+ entry.timestamps.push(now);
621
+ entry.expiresAt = now + windowMs + 6e4;
622
+ return { count: entry.timestamps.length };
623
+ },
624
+ async isBlocked(key) {
625
+ const expiry = blocks.get(key);
626
+ if (!expiry || expiry < Date.now()) {
627
+ blocks.delete(key);
628
+ return { blocked: false, ttlMs: 0 };
629
+ }
630
+ return { blocked: true, ttlMs: expiry - Date.now() };
631
+ },
632
+ async setBlock(key, durationSeconds) {
633
+ blocks.set(key, Date.now() + durationSeconds * 1e3);
634
+ },
635
+ async reset(key) {
636
+ windows.delete(key);
637
+ blocks.delete(`block:${key}`);
638
+ blocks.delete(key);
639
+ }
640
+ };
641
+ }
642
+ var defaultStore;
643
+ function getDefaultStore() {
644
+ if (!defaultStore) {
645
+ defaultStore = createMemoryRateLimitStore();
646
+ }
647
+ return defaultStore;
648
+ }
649
+ async function checkRateLimit(operation, identifier, rule, options = {}) {
650
+ const store = options.store ?? getDefaultStore();
651
+ const limit = options.isAuthenticated && rule.authenticatedLimit ? rule.authenticatedLimit : rule.limit;
652
+ const now = Date.now();
653
+ const windowMs = rule.windowSeconds * 1e3;
654
+ const resetAt = now + windowMs;
655
+ const key = `ratelimit:${operation}:${identifier}`;
656
+ const blockKey = `ratelimit:block:${operation}:${identifier}`;
657
+ try {
658
+ if (rule.blockDurationSeconds) {
659
+ const blockStatus = await store.isBlocked(blockKey);
660
+ if (blockStatus.blocked) {
661
+ return {
662
+ allowed: false,
663
+ remaining: 0,
664
+ resetAt: now + blockStatus.ttlMs,
665
+ current: limit + 1,
666
+ limit,
667
+ retryAfterSeconds: Math.ceil(blockStatus.ttlMs / 1e3)
668
+ };
669
+ }
670
+ }
671
+ const { count } = await store.increment(key, windowMs, now);
672
+ if (count > limit) {
673
+ options.logger?.warn("Rate limit exceeded", {
674
+ operation,
675
+ identifier,
676
+ current: count,
677
+ limit
678
+ });
679
+ if (rule.blockDurationSeconds) {
680
+ await store.setBlock(blockKey, rule.blockDurationSeconds);
681
+ }
682
+ return {
683
+ allowed: false,
684
+ remaining: 0,
685
+ resetAt,
686
+ current: count,
687
+ limit,
688
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
689
+ };
690
+ }
691
+ return {
692
+ allowed: true,
693
+ remaining: limit - count,
694
+ resetAt,
695
+ current: count,
696
+ limit,
697
+ retryAfterSeconds: 0
698
+ };
699
+ } catch (error) {
700
+ options.logger?.error("Rate limit check failed, allowing request", {
701
+ error: error instanceof Error ? error.message : String(error),
702
+ operation,
703
+ identifier
704
+ });
705
+ return {
706
+ allowed: true,
707
+ remaining: limit,
708
+ resetAt,
709
+ current: 0,
710
+ limit,
711
+ retryAfterSeconds: 0
712
+ };
713
+ }
714
+ }
715
+ async function getRateLimitStatus(operation, identifier, rule, store) {
716
+ const s = store ?? getDefaultStore();
717
+ const key = `ratelimit:${operation}:${identifier}`;
718
+ const now = Date.now();
719
+ const windowMs = rule.windowSeconds * 1e3;
720
+ try {
721
+ const { count } = await s.increment(key, windowMs, now);
722
+ return {
723
+ allowed: count <= rule.limit,
724
+ remaining: Math.max(0, rule.limit - count),
725
+ resetAt: now + windowMs,
726
+ current: count,
727
+ limit: rule.limit,
728
+ retryAfterSeconds: count > rule.limit ? Math.ceil(windowMs / 1e3) : 0
729
+ };
730
+ } catch {
731
+ return null;
732
+ }
733
+ }
734
+ async function resetRateLimitForKey(operation, identifier, store) {
735
+ const s = store ?? getDefaultStore();
736
+ const key = `ratelimit:${operation}:${identifier}`;
737
+ const blockKey = `ratelimit:block:${operation}:${identifier}`;
738
+ await s.reset(key);
739
+ await s.reset(blockKey);
740
+ }
741
+ function buildRateLimitResponseHeaders(result) {
742
+ const headers = {
743
+ "X-RateLimit-Limit": String(result.limit),
744
+ "X-RateLimit-Remaining": String(result.remaining),
745
+ "X-RateLimit-Reset": String(Math.ceil(result.resetAt / 1e3))
746
+ };
747
+ if (!result.allowed) {
748
+ headers["Retry-After"] = String(result.retryAfterSeconds);
749
+ }
750
+ return headers;
751
+ }
752
+ function resolveIdentifier(session, clientIp) {
753
+ if (session?.user?.id) {
754
+ return { identifier: `user:${session.user.id}`, isAuthenticated: true };
755
+ }
756
+ if (session?.user?.email) {
757
+ return {
758
+ identifier: `email:${session.user.email}`,
759
+ isAuthenticated: true
760
+ };
761
+ }
762
+ return { identifier: `ip:${clientIp ?? "unknown"}`, isAuthenticated: false };
763
+ }
764
+
765
+ // src/auth/audit.ts
766
+ var StandardAuditActions = {
767
+ // Authentication
768
+ LOGIN_SUCCESS: "auth.login.success",
769
+ LOGIN_FAILURE: "auth.login.failure",
770
+ LOGOUT: "auth.logout",
771
+ SESSION_REFRESH: "auth.session.refresh",
772
+ PASSWORD_CHANGE: "auth.password.change",
773
+ PASSWORD_RESET: "auth.password.reset",
774
+ // Billing
775
+ CHECKOUT_START: "billing.checkout.start",
776
+ CHECKOUT_COMPLETE: "billing.checkout.complete",
777
+ SUBSCRIPTION_CREATE: "billing.subscription.create",
778
+ SUBSCRIPTION_CANCEL: "billing.subscription.cancel",
779
+ SUBSCRIPTION_UPDATE: "billing.subscription.update",
780
+ PAYMENT_FAILED: "billing.payment.failed",
781
+ // Admin
782
+ ADMIN_LOGIN: "admin.login",
783
+ ADMIN_USER_VIEW: "admin.user.view",
784
+ ADMIN_USER_UPDATE: "admin.user.update",
785
+ ADMIN_CONFIG_CHANGE: "admin.config.change",
786
+ // Security Events
787
+ RATE_LIMIT_EXCEEDED: "security.rate_limit.exceeded",
788
+ INVALID_INPUT: "security.input.invalid",
789
+ UNAUTHORIZED_ACCESS: "security.access.unauthorized",
790
+ OWNERSHIP_VIOLATION: "security.ownership.violation",
791
+ WEBHOOK_SIGNATURE_INVALID: "security.webhook.signature_invalid",
792
+ // Data
793
+ DATA_EXPORT: "data.export",
794
+ DATA_DELETE: "data.delete",
795
+ DATA_UPDATE: "data.update"
796
+ };
797
+ function extractAuditIp(request) {
798
+ if (!request) return void 0;
799
+ const headers = [
800
+ "cf-connecting-ip",
801
+ // Cloudflare
802
+ "x-real-ip",
803
+ // Nginx
804
+ "x-forwarded-for",
805
+ // Standard proxy
806
+ "x-client-ip"
807
+ // Apache
808
+ ];
809
+ for (const header of headers) {
810
+ const value = request.headers.get(header);
811
+ if (value) {
812
+ return value.split(",")[0]?.trim();
813
+ }
814
+ }
815
+ return void 0;
816
+ }
817
+ function extractAuditUserAgent(request) {
818
+ return request?.headers.get("user-agent") ?? void 0;
819
+ }
820
+ function extractAuditRequestId(request) {
821
+ return request?.headers.get("x-request-id") ?? crypto.randomUUID();
822
+ }
823
+ function createAuditActor(session) {
824
+ if (!session?.user) {
825
+ return { id: "anonymous", type: "anonymous" };
826
+ }
827
+ return {
828
+ id: session.user.id ?? session.user.email ?? "unknown",
829
+ email: session.user.email ?? void 0,
830
+ type: "user"
831
+ };
832
+ }
833
+ var defaultLogger = {
834
+ info: (msg, meta) => console.log(msg, meta ? JSON.stringify(meta) : ""),
835
+ warn: (msg, meta) => console.warn(msg, meta ? JSON.stringify(meta) : ""),
836
+ error: (msg, meta) => console.error(msg, meta ? JSON.stringify(meta) : "")
837
+ };
838
+ function createAuditLogger(options = {}) {
839
+ const { persist, logger = defaultLogger } = options;
840
+ async function log(event, request) {
841
+ const record = {
842
+ id: crypto.randomUUID(),
843
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
844
+ ip: extractAuditIp(request),
845
+ userAgent: extractAuditUserAgent(request),
846
+ requestId: extractAuditRequestId(request),
847
+ ...event
848
+ };
849
+ const logFn = event.outcome === "failure" || event.outcome === "blocked" ? logger.warn : logger.info;
850
+ logFn(`[AUDIT] ${event.action}`, {
851
+ auditId: record.id,
852
+ actor: record.actor,
853
+ action: record.action,
854
+ resource: record.resource,
855
+ outcome: record.outcome,
856
+ ip: record.ip,
857
+ metadata: record.metadata,
858
+ reason: record.reason
859
+ });
860
+ if (persist) {
861
+ try {
862
+ await persist(record);
863
+ } catch (error) {
864
+ logger.error("Failed to persist audit log", {
865
+ error: error instanceof Error ? error.message : String(error),
866
+ auditId: record.id
867
+ });
868
+ }
869
+ }
870
+ return record;
871
+ }
872
+ function createTimedAudit(event, request) {
873
+ const startTime = Date.now();
874
+ return {
875
+ success: async (metadata) => {
876
+ return log(
877
+ {
878
+ ...event,
879
+ outcome: "success",
880
+ metadata: {
881
+ ...event.metadata,
882
+ ...metadata,
883
+ durationMs: Date.now() - startTime
884
+ }
885
+ },
886
+ request
887
+ );
888
+ },
889
+ failure: async (reason, metadata) => {
890
+ return log(
891
+ {
892
+ ...event,
893
+ outcome: "failure",
894
+ reason,
895
+ metadata: {
896
+ ...event.metadata,
897
+ ...metadata,
898
+ durationMs: Date.now() - startTime
899
+ }
900
+ },
901
+ request
902
+ );
903
+ },
904
+ blocked: async (reason, metadata) => {
905
+ return log(
906
+ {
907
+ ...event,
908
+ outcome: "blocked",
909
+ reason,
910
+ metadata: {
911
+ ...event.metadata,
912
+ ...metadata,
913
+ durationMs: Date.now() - startTime
914
+ }
915
+ },
916
+ request
917
+ );
918
+ }
919
+ };
920
+ }
921
+ return { log, createTimedAudit };
922
+ }
923
+ // Annotate the CommonJS export names for ESM import in node:
924
+ 0 && (module.exports = {
925
+ CommonRateLimits,
926
+ DateRangeSchema,
927
+ EmailSchema,
928
+ KEYCLOAK_DEFAULT_ROLES,
929
+ LoginSchema,
930
+ PaginationSchema,
931
+ PasswordSchema,
932
+ PersonNameSchema,
933
+ PhoneSchema,
934
+ SearchQuerySchema,
935
+ SignupSchema,
936
+ SlugSchema,
937
+ StandardAuditActions,
938
+ StandardRateLimitPresets,
939
+ WrapperPresets,
940
+ buildAllowlist,
941
+ buildAuthCookies,
942
+ buildErrorBody,
943
+ buildKeycloakCallbacks,
944
+ buildRateLimitHeaders,
945
+ buildRateLimitResponseHeaders,
946
+ buildRedirectCallback,
947
+ buildTokenRefreshParams,
948
+ checkRateLimit,
949
+ createAuditActor,
950
+ createAuditLogger,
951
+ createFeatureFlags,
952
+ createMemoryRateLimitStore,
953
+ createSafeTextSchema,
954
+ detectStage,
955
+ extractAuditIp,
956
+ extractAuditRequestId,
957
+ extractAuditUserAgent,
958
+ extractClientIp,
959
+ getEndSessionEndpoint,
960
+ getRateLimitStatus,
961
+ getTokenEndpoint,
962
+ hasAllRoles,
963
+ hasAnyRole,
964
+ hasRole,
965
+ isAllowlisted,
966
+ isTokenExpired,
967
+ parseKeycloakRoles,
968
+ refreshKeycloakToken,
969
+ resetRateLimitForKey,
970
+ resolveIdentifier,
971
+ resolveRateLimitIdentifier
972
+ });
973
+ //# sourceMappingURL=auth.js.map