@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.d.mts +1112 -0
- package/dist/auth.d.ts +1112 -0
- package/dist/auth.js +973 -0
- package/dist/auth.js.map +1 -0
- package/dist/auth.mjs +900 -0
- package/dist/auth.mjs.map +1 -0
- package/dist/index.d.mts +278 -5
- package/dist/index.d.ts +278 -5
- package/dist/index.js +1322 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1233 -24
- package/dist/index.mjs.map +1 -1
- package/dist/migrate.js +0 -0
- package/dist/security-headers.js.map +1 -1
- package/dist/security-headers.mjs.map +1 -1
- package/dist/testing.js +3 -1
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +9 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +16 -11
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
|