@digilogiclabs/platform-core 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.js CHANGED
@@ -20,6 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/auth/index.ts
21
21
  var auth_exports = {};
22
22
  __export(auth_exports, {
23
+ ApiError: () => ApiError,
24
+ ApiErrorCode: () => ApiErrorCode,
25
+ CommonApiErrors: () => CommonApiErrors,
23
26
  CommonRateLimits: () => CommonRateLimits,
24
27
  DateRangeSchema: () => DateRangeSchema,
25
28
  EmailSchema: () => EmailSchema,
@@ -39,34 +42,57 @@ __export(auth_exports, {
39
42
  buildAuthCookies: () => buildAuthCookies,
40
43
  buildErrorBody: () => buildErrorBody,
41
44
  buildKeycloakCallbacks: () => buildKeycloakCallbacks,
45
+ buildPagination: () => buildPagination,
42
46
  buildRateLimitHeaders: () => buildRateLimitHeaders,
43
47
  buildRateLimitResponseHeaders: () => buildRateLimitResponseHeaders,
44
48
  buildRedirectCallback: () => buildRedirectCallback,
45
49
  buildTokenRefreshParams: () => buildTokenRefreshParams,
50
+ checkEnvVars: () => checkEnvVars,
46
51
  checkRateLimit: () => checkRateLimit,
52
+ classifyError: () => classifyError,
53
+ constantTimeEqual: () => constantTimeEqual,
54
+ containsHtml: () => containsHtml,
55
+ containsUrls: () => containsUrls,
47
56
  createAuditActor: () => createAuditActor,
48
57
  createAuditLogger: () => createAuditLogger,
49
58
  createFeatureFlags: () => createFeatureFlags,
50
59
  createMemoryRateLimitStore: () => createMemoryRateLimitStore,
60
+ createRedisRateLimitStore: () => createRedisRateLimitStore,
51
61
  createSafeTextSchema: () => createSafeTextSchema,
52
62
  detectStage: () => detectStage,
63
+ enforceRateLimit: () => enforceRateLimit,
64
+ errorResponse: () => errorResponse,
65
+ escapeHtml: () => escapeHtml,
53
66
  extractAuditIp: () => extractAuditIp,
54
67
  extractAuditRequestId: () => extractAuditRequestId,
55
68
  extractAuditUserAgent: () => extractAuditUserAgent,
69
+ extractBearerToken: () => extractBearerToken,
56
70
  extractClientIp: () => extractClientIp,
71
+ getBoolEnv: () => getBoolEnv,
72
+ getCorrelationId: () => getCorrelationId,
57
73
  getEndSessionEndpoint: () => getEndSessionEndpoint,
74
+ getEnvSummary: () => getEnvSummary,
75
+ getIntEnv: () => getIntEnv,
76
+ getOptionalEnv: () => getOptionalEnv,
58
77
  getRateLimitStatus: () => getRateLimitStatus,
78
+ getRequiredEnv: () => getRequiredEnv,
59
79
  getTokenEndpoint: () => getTokenEndpoint,
60
80
  hasAllRoles: () => hasAllRoles,
61
81
  hasAnyRole: () => hasAnyRole,
62
82
  hasRole: () => hasRole,
63
83
  isAllowlisted: () => isAllowlisted,
84
+ isApiError: () => isApiError,
64
85
  isTokenExpired: () => isTokenExpired,
86
+ isValidBearerToken: () => isValidBearerToken,
65
87
  parseKeycloakRoles: () => parseKeycloakRoles,
66
88
  refreshKeycloakToken: () => refreshKeycloakToken,
67
89
  resetRateLimitForKey: () => resetRateLimitForKey,
68
90
  resolveIdentifier: () => resolveIdentifier,
69
- resolveRateLimitIdentifier: () => resolveRateLimitIdentifier
91
+ resolveRateLimitIdentifier: () => resolveRateLimitIdentifier,
92
+ sanitizeApiError: () => sanitizeApiError,
93
+ stripHtml: () => stripHtml,
94
+ validateEnvVars: () => validateEnvVars,
95
+ zodErrorResponse: () => zodErrorResponse
70
96
  });
71
97
  module.exports = __toCommonJS(auth_exports);
72
98
 
@@ -239,6 +265,7 @@ function buildKeycloakCallbacks(config) {
239
265
  *
240
266
  * Compatible with Auth.js v5 JWT callback signature.
241
267
  */
268
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
242
269
  async jwt({
243
270
  token,
244
271
  user,
@@ -289,10 +316,8 @@ function buildKeycloakCallbacks(config) {
289
316
  *
290
317
  * Compatible with Auth.js v5 session callback signature.
291
318
  */
292
- async session({
293
- session,
294
- token
295
- }) {
319
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
+ async session({ session, token }) {
296
321
  const user = session.user;
297
322
  if (user) {
298
323
  user.id = token.id || token.sub;
@@ -416,9 +441,53 @@ var WrapperPresets = {
416
441
  var import_zod = require("zod");
417
442
 
418
443
  // src/security.ts
444
+ var import_crypto = require("crypto");
419
445
  var URL_PROTOCOL_PATTERN = /(https?:\/\/|ftp:\/\/|www\.)\S+/i;
420
446
  var URL_DOMAIN_PATTERN = /\b[\w.-]+\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\b/i;
421
447
  var HTML_TAG_PATTERN = /<[^>]*>/;
448
+ function escapeHtml(str) {
449
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
450
+ }
451
+ function containsUrls(str) {
452
+ return URL_PROTOCOL_PATTERN.test(str) || URL_DOMAIN_PATTERN.test(str);
453
+ }
454
+ function containsHtml(str) {
455
+ return HTML_TAG_PATTERN.test(str);
456
+ }
457
+ function stripHtml(str) {
458
+ return str.replace(/<[^>]*>/g, "");
459
+ }
460
+ function constantTimeEqual(a, b) {
461
+ try {
462
+ const aBuf = Buffer.from(a, "utf-8");
463
+ const bBuf = Buffer.from(b, "utf-8");
464
+ if (aBuf.length !== bBuf.length) return false;
465
+ return (0, import_crypto.timingSafeEqual)(aBuf, bBuf);
466
+ } catch {
467
+ return false;
468
+ }
469
+ }
470
+ function sanitizeApiError(error, statusCode, isDevelopment = false) {
471
+ if (statusCode >= 400 && statusCode < 500) {
472
+ const message = error instanceof Error ? error.message : String(error || "Bad request");
473
+ return { message };
474
+ }
475
+ const result = {
476
+ message: "An internal error occurred. Please try again later.",
477
+ code: "INTERNAL_ERROR"
478
+ };
479
+ if (isDevelopment && error instanceof Error) {
480
+ result.stack = error.stack;
481
+ }
482
+ return result;
483
+ }
484
+ function getCorrelationId(headers) {
485
+ const get = typeof headers === "function" ? headers : (name) => {
486
+ const val = headers[name] ?? headers[name.toLowerCase()];
487
+ return Array.isArray(val) ? val[0] : val;
488
+ };
489
+ return get("x-request-id") || get("X-Request-ID") || get("x-correlation-id") || get("X-Correlation-ID") || crypto.randomUUID();
490
+ }
422
491
 
423
492
  // src/auth/schemas.ts
424
493
  var EmailSchema = import_zod.z.string().trim().toLowerCase().email("Invalid email address");
@@ -762,6 +831,44 @@ function resolveIdentifier(session, clientIp) {
762
831
  return { identifier: `ip:${clientIp ?? "unknown"}`, isAuthenticated: false };
763
832
  }
764
833
 
834
+ // src/auth/rate-limit-store-redis.ts
835
+ function createRedisRateLimitStore(redis, options = {}) {
836
+ const prefix = options.keyPrefix ?? "";
837
+ return {
838
+ async increment(key, windowMs, now) {
839
+ const fullKey = `${prefix}${key}`;
840
+ const windowStart = now - windowMs;
841
+ const windowSeconds = Math.ceil(windowMs / 1e3) + 60;
842
+ await redis.zremrangebyscore(fullKey, 0, windowStart);
843
+ const current = await redis.zcard(fullKey);
844
+ const member = `${now}:${Math.random().toString(36).slice(2, 10)}`;
845
+ await redis.zadd(fullKey, now, member);
846
+ await redis.expire(fullKey, windowSeconds);
847
+ return { count: current + 1 };
848
+ },
849
+ async isBlocked(key) {
850
+ const fullKey = `${prefix}${key}`;
851
+ const value = await redis.get(fullKey);
852
+ if (!value) {
853
+ return { blocked: false, ttlMs: 0 };
854
+ }
855
+ const ttlSeconds = await redis.ttl(fullKey);
856
+ if (ttlSeconds <= 0) {
857
+ return { blocked: false, ttlMs: 0 };
858
+ }
859
+ return { blocked: true, ttlMs: ttlSeconds * 1e3 };
860
+ },
861
+ async setBlock(key, durationSeconds) {
862
+ const fullKey = `${prefix}${key}`;
863
+ await redis.setex(fullKey, durationSeconds, "1");
864
+ },
865
+ async reset(key) {
866
+ const fullKey = `${prefix}${key}`;
867
+ await redis.del(fullKey);
868
+ }
869
+ };
870
+ }
871
+
765
872
  // src/auth/audit.ts
766
873
  var StandardAuditActions = {
767
874
  // Authentication
@@ -920,8 +1027,278 @@ function createAuditLogger(options = {}) {
920
1027
  }
921
1028
  return { log, createTimedAudit };
922
1029
  }
1030
+
1031
+ // src/api.ts
1032
+ var ApiErrorCode = {
1033
+ VALIDATION_ERROR: "VALIDATION_ERROR",
1034
+ UNAUTHORIZED: "UNAUTHORIZED",
1035
+ FORBIDDEN: "FORBIDDEN",
1036
+ NOT_FOUND: "NOT_FOUND",
1037
+ CONFLICT: "CONFLICT",
1038
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
1039
+ INTERNAL_ERROR: "INTERNAL_ERROR",
1040
+ DATABASE_ERROR: "DATABASE_ERROR",
1041
+ EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
1042
+ CONFIGURATION_ERROR: "CONFIGURATION_ERROR"
1043
+ };
1044
+ var ApiError = class extends Error {
1045
+ statusCode;
1046
+ code;
1047
+ details;
1048
+ constructor(statusCode, message, code = ApiErrorCode.INTERNAL_ERROR, details) {
1049
+ super(message);
1050
+ this.name = "ApiError";
1051
+ this.statusCode = statusCode;
1052
+ this.code = code;
1053
+ this.details = details;
1054
+ }
1055
+ };
1056
+ function isApiError(error) {
1057
+ return error instanceof ApiError;
1058
+ }
1059
+ var CommonApiErrors = {
1060
+ unauthorized: (msg = "Unauthorized") => new ApiError(401, msg, ApiErrorCode.UNAUTHORIZED),
1061
+ forbidden: (msg = "Forbidden") => new ApiError(403, msg, ApiErrorCode.FORBIDDEN),
1062
+ notFound: (resource = "Resource") => new ApiError(404, `${resource} not found`, ApiErrorCode.NOT_FOUND),
1063
+ conflict: (msg = "Resource already exists") => new ApiError(409, msg, ApiErrorCode.CONFLICT),
1064
+ rateLimitExceeded: (msg = "Rate limit exceeded") => new ApiError(429, msg, ApiErrorCode.RATE_LIMIT_EXCEEDED),
1065
+ validationError: (details) => new ApiError(
1066
+ 400,
1067
+ "Validation failed",
1068
+ ApiErrorCode.VALIDATION_ERROR,
1069
+ details
1070
+ ),
1071
+ internalError: (msg = "Internal server error") => new ApiError(500, msg, ApiErrorCode.INTERNAL_ERROR)
1072
+ };
1073
+ var PG_ERROR_MAP = {
1074
+ "23505": {
1075
+ status: 409,
1076
+ code: ApiErrorCode.CONFLICT,
1077
+ message: "Resource already exists"
1078
+ },
1079
+ "23503": {
1080
+ status: 404,
1081
+ code: ApiErrorCode.NOT_FOUND,
1082
+ message: "Referenced resource not found"
1083
+ },
1084
+ PGRST116: {
1085
+ status: 404,
1086
+ code: ApiErrorCode.NOT_FOUND,
1087
+ message: "Resource not found"
1088
+ }
1089
+ };
1090
+ function classifyError(error, isDev = false) {
1091
+ if (isApiError(error)) {
1092
+ return {
1093
+ status: error.statusCode,
1094
+ body: { error: error.message, code: error.code, details: error.details }
1095
+ };
1096
+ }
1097
+ if (error && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
1098
+ return {
1099
+ status: 400,
1100
+ body: {
1101
+ error: "Validation failed",
1102
+ code: ApiErrorCode.VALIDATION_ERROR,
1103
+ details: error.issues.map((i) => ({
1104
+ field: i.path?.join("."),
1105
+ message: i.message
1106
+ }))
1107
+ }
1108
+ };
1109
+ }
1110
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1111
+ const pgCode = error.code;
1112
+ const mapped = PG_ERROR_MAP[pgCode];
1113
+ if (mapped) {
1114
+ return {
1115
+ status: mapped.status,
1116
+ body: { error: mapped.message, code: mapped.code }
1117
+ };
1118
+ }
1119
+ return {
1120
+ status: 500,
1121
+ body: {
1122
+ error: "Database error",
1123
+ code: ApiErrorCode.DATABASE_ERROR,
1124
+ details: isDev ? error.message : void 0
1125
+ }
1126
+ };
1127
+ }
1128
+ if (error instanceof Error) {
1129
+ return {
1130
+ status: 500,
1131
+ body: {
1132
+ error: isDev ? error.message : "Internal server error",
1133
+ code: ApiErrorCode.INTERNAL_ERROR,
1134
+ details: isDev ? error.stack : void 0
1135
+ }
1136
+ };
1137
+ }
1138
+ return {
1139
+ status: 500,
1140
+ body: {
1141
+ error: "An unexpected error occurred",
1142
+ code: ApiErrorCode.INTERNAL_ERROR
1143
+ }
1144
+ };
1145
+ }
1146
+ function buildPagination(page, limit, total) {
1147
+ return {
1148
+ page,
1149
+ limit,
1150
+ total,
1151
+ totalPages: Math.ceil(total / limit),
1152
+ hasMore: page * limit < total
1153
+ };
1154
+ }
1155
+
1156
+ // src/auth/nextjs-api.ts
1157
+ async function enforceRateLimit(request, operation, rule, options) {
1158
+ const identifier = options?.identifier ?? (options?.userId ? `user:${options.userId}` : void 0) ?? `ip:${extractClientIp((name) => request.headers.get(name))}`;
1159
+ const isAuthenticated = !!options?.userId;
1160
+ const result = await checkRateLimit(operation, identifier, rule, {
1161
+ ...options?.rateLimitOptions,
1162
+ isAuthenticated
1163
+ });
1164
+ if (!result.allowed) {
1165
+ const headers = buildRateLimitResponseHeaders(result);
1166
+ return new Response(
1167
+ JSON.stringify({
1168
+ error: "Rate limit exceeded. Please try again later.",
1169
+ retryAfter: result.retryAfterSeconds
1170
+ }),
1171
+ {
1172
+ status: 429,
1173
+ headers: { "Content-Type": "application/json", ...headers }
1174
+ }
1175
+ );
1176
+ }
1177
+ return null;
1178
+ }
1179
+ function errorResponse(error, options) {
1180
+ const isDev = options?.isDevelopment ?? process.env.NODE_ENV === "development";
1181
+ const { status, body } = classifyError(error, isDev);
1182
+ return new Response(JSON.stringify(body), {
1183
+ status,
1184
+ headers: { "Content-Type": "application/json" }
1185
+ });
1186
+ }
1187
+ function zodErrorResponse(error) {
1188
+ const firstIssue = error.issues[0];
1189
+ const message = firstIssue ? `${firstIssue.path.join(".") || "input"}: ${firstIssue.message}` : "Validation error";
1190
+ return new Response(JSON.stringify({ error: message }), {
1191
+ status: 400,
1192
+ headers: { "Content-Type": "application/json" }
1193
+ });
1194
+ }
1195
+ function extractBearerToken(request) {
1196
+ const auth = request.headers.get("authorization");
1197
+ if (!auth?.startsWith("Bearer ")) return null;
1198
+ return auth.slice(7).trim() || null;
1199
+ }
1200
+ function isValidBearerToken(request, secret) {
1201
+ if (!secret) return false;
1202
+ const token = extractBearerToken(request);
1203
+ if (!token) return false;
1204
+ return constantTimeEqual(token, secret);
1205
+ }
1206
+
1207
+ // src/env.ts
1208
+ function getRequiredEnv(key) {
1209
+ const value = process.env[key];
1210
+ if (!value) {
1211
+ throw new Error(`Missing required environment variable: ${key}`);
1212
+ }
1213
+ return value;
1214
+ }
1215
+ function getOptionalEnv(key, defaultValue) {
1216
+ return process.env[key] || defaultValue;
1217
+ }
1218
+ function getBoolEnv(key, defaultValue = false) {
1219
+ const value = process.env[key];
1220
+ if (value === void 0 || value === "") return defaultValue;
1221
+ return value === "true" || value === "1";
1222
+ }
1223
+ function getIntEnv(key, defaultValue) {
1224
+ const value = process.env[key];
1225
+ if (value === void 0 || value === "") return defaultValue;
1226
+ const parsed = parseInt(value, 10);
1227
+ return isNaN(parsed) ? defaultValue : parsed;
1228
+ }
1229
+ function validateEnvVars(config) {
1230
+ const result = checkEnvVars(config);
1231
+ if (!result.valid) {
1232
+ const lines = [];
1233
+ if (result.missing.length > 0) {
1234
+ lines.push(
1235
+ "Missing required environment variables:",
1236
+ ...result.missing.map((v) => ` - ${v}`)
1237
+ );
1238
+ }
1239
+ if (result.missingOneOf.length > 0) {
1240
+ for (const group of result.missingOneOf) {
1241
+ lines.push(`Missing one of: ${group.join(" | ")}`);
1242
+ }
1243
+ }
1244
+ if (result.invalid.length > 0) {
1245
+ lines.push(
1246
+ "Invalid environment variables:",
1247
+ ...result.invalid.map((v) => ` - ${v.key}: ${v.reason}`)
1248
+ );
1249
+ }
1250
+ throw new Error(lines.join("\n"));
1251
+ }
1252
+ }
1253
+ function checkEnvVars(config) {
1254
+ const missing = [];
1255
+ const invalid = [];
1256
+ const missingOneOf = [];
1257
+ if (config.required) {
1258
+ for (const key of config.required) {
1259
+ if (!process.env[key]) {
1260
+ missing.push(key);
1261
+ }
1262
+ }
1263
+ }
1264
+ if (config.requireOneOf) {
1265
+ for (const group of config.requireOneOf) {
1266
+ const hasAny = group.some((key) => !!process.env[key]);
1267
+ if (!hasAny) {
1268
+ missingOneOf.push(group);
1269
+ }
1270
+ }
1271
+ }
1272
+ if (config.validators) {
1273
+ for (const [key, validator] of Object.entries(config.validators)) {
1274
+ const value = process.env[key];
1275
+ if (value) {
1276
+ const result = validator(value);
1277
+ if (result !== true) {
1278
+ invalid.push({ key, reason: result });
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ return {
1284
+ valid: missing.length === 0 && invalid.length === 0 && missingOneOf.length === 0,
1285
+ missing,
1286
+ invalid,
1287
+ missingOneOf
1288
+ };
1289
+ }
1290
+ function getEnvSummary(keys) {
1291
+ const summary = {};
1292
+ for (const key of keys) {
1293
+ summary[key] = !!process.env[key];
1294
+ }
1295
+ return summary;
1296
+ }
923
1297
  // Annotate the CommonJS export names for ESM import in node:
924
1298
  0 && (module.exports = {
1299
+ ApiError,
1300
+ ApiErrorCode,
1301
+ CommonApiErrors,
925
1302
  CommonRateLimits,
926
1303
  DateRangeSchema,
927
1304
  EmailSchema,
@@ -941,33 +1318,56 @@ function createAuditLogger(options = {}) {
941
1318
  buildAuthCookies,
942
1319
  buildErrorBody,
943
1320
  buildKeycloakCallbacks,
1321
+ buildPagination,
944
1322
  buildRateLimitHeaders,
945
1323
  buildRateLimitResponseHeaders,
946
1324
  buildRedirectCallback,
947
1325
  buildTokenRefreshParams,
1326
+ checkEnvVars,
948
1327
  checkRateLimit,
1328
+ classifyError,
1329
+ constantTimeEqual,
1330
+ containsHtml,
1331
+ containsUrls,
949
1332
  createAuditActor,
950
1333
  createAuditLogger,
951
1334
  createFeatureFlags,
952
1335
  createMemoryRateLimitStore,
1336
+ createRedisRateLimitStore,
953
1337
  createSafeTextSchema,
954
1338
  detectStage,
1339
+ enforceRateLimit,
1340
+ errorResponse,
1341
+ escapeHtml,
955
1342
  extractAuditIp,
956
1343
  extractAuditRequestId,
957
1344
  extractAuditUserAgent,
1345
+ extractBearerToken,
958
1346
  extractClientIp,
1347
+ getBoolEnv,
1348
+ getCorrelationId,
959
1349
  getEndSessionEndpoint,
1350
+ getEnvSummary,
1351
+ getIntEnv,
1352
+ getOptionalEnv,
960
1353
  getRateLimitStatus,
1354
+ getRequiredEnv,
961
1355
  getTokenEndpoint,
962
1356
  hasAllRoles,
963
1357
  hasAnyRole,
964
1358
  hasRole,
965
1359
  isAllowlisted,
1360
+ isApiError,
966
1361
  isTokenExpired,
1362
+ isValidBearerToken,
967
1363
  parseKeycloakRoles,
968
1364
  refreshKeycloakToken,
969
1365
  resetRateLimitForKey,
970
1366
  resolveIdentifier,
971
- resolveRateLimitIdentifier
1367
+ resolveRateLimitIdentifier,
1368
+ sanitizeApiError,
1369
+ stripHtml,
1370
+ validateEnvVars,
1371
+ zodErrorResponse
972
1372
  });
973
1373
  //# sourceMappingURL=auth.js.map