@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.mjs CHANGED
@@ -167,6 +167,7 @@ function buildKeycloakCallbacks(config) {
167
167
  *
168
168
  * Compatible with Auth.js v5 JWT callback signature.
169
169
  */
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
171
  async jwt({
171
172
  token,
172
173
  user,
@@ -217,10 +218,8 @@ function buildKeycloakCallbacks(config) {
217
218
  *
218
219
  * Compatible with Auth.js v5 session callback signature.
219
220
  */
220
- async session({
221
- session,
222
- token
223
- }) {
221
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
+ async session({ session, token }) {
224
223
  const user = session.user;
225
224
  if (user) {
226
225
  user.id = token.id || token.sub;
@@ -344,9 +343,53 @@ var WrapperPresets = {
344
343
  import { z } from "zod";
345
344
 
346
345
  // src/security.ts
346
+ import { timingSafeEqual } from "crypto";
347
347
  var URL_PROTOCOL_PATTERN = /(https?:\/\/|ftp:\/\/|www\.)\S+/i;
348
348
  var URL_DOMAIN_PATTERN = /\b[\w.-]+\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\b/i;
349
349
  var HTML_TAG_PATTERN = /<[^>]*>/;
350
+ function escapeHtml(str) {
351
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
352
+ }
353
+ function containsUrls(str) {
354
+ return URL_PROTOCOL_PATTERN.test(str) || URL_DOMAIN_PATTERN.test(str);
355
+ }
356
+ function containsHtml(str) {
357
+ return HTML_TAG_PATTERN.test(str);
358
+ }
359
+ function stripHtml(str) {
360
+ return str.replace(/<[^>]*>/g, "");
361
+ }
362
+ function constantTimeEqual(a, b) {
363
+ try {
364
+ const aBuf = Buffer.from(a, "utf-8");
365
+ const bBuf = Buffer.from(b, "utf-8");
366
+ if (aBuf.length !== bBuf.length) return false;
367
+ return timingSafeEqual(aBuf, bBuf);
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+ function sanitizeApiError(error, statusCode, isDevelopment = false) {
373
+ if (statusCode >= 400 && statusCode < 500) {
374
+ const message = error instanceof Error ? error.message : String(error || "Bad request");
375
+ return { message };
376
+ }
377
+ const result = {
378
+ message: "An internal error occurred. Please try again later.",
379
+ code: "INTERNAL_ERROR"
380
+ };
381
+ if (isDevelopment && error instanceof Error) {
382
+ result.stack = error.stack;
383
+ }
384
+ return result;
385
+ }
386
+ function getCorrelationId(headers) {
387
+ const get = typeof headers === "function" ? headers : (name) => {
388
+ const val = headers[name] ?? headers[name.toLowerCase()];
389
+ return Array.isArray(val) ? val[0] : val;
390
+ };
391
+ return get("x-request-id") || get("X-Request-ID") || get("x-correlation-id") || get("X-Correlation-ID") || crypto.randomUUID();
392
+ }
350
393
 
351
394
  // src/auth/schemas.ts
352
395
  var EmailSchema = z.string().trim().toLowerCase().email("Invalid email address");
@@ -690,6 +733,44 @@ function resolveIdentifier(session, clientIp) {
690
733
  return { identifier: `ip:${clientIp ?? "unknown"}`, isAuthenticated: false };
691
734
  }
692
735
 
736
+ // src/auth/rate-limit-store-redis.ts
737
+ function createRedisRateLimitStore(redis, options = {}) {
738
+ const prefix = options.keyPrefix ?? "";
739
+ return {
740
+ async increment(key, windowMs, now) {
741
+ const fullKey = `${prefix}${key}`;
742
+ const windowStart = now - windowMs;
743
+ const windowSeconds = Math.ceil(windowMs / 1e3) + 60;
744
+ await redis.zremrangebyscore(fullKey, 0, windowStart);
745
+ const current = await redis.zcard(fullKey);
746
+ const member = `${now}:${Math.random().toString(36).slice(2, 10)}`;
747
+ await redis.zadd(fullKey, now, member);
748
+ await redis.expire(fullKey, windowSeconds);
749
+ return { count: current + 1 };
750
+ },
751
+ async isBlocked(key) {
752
+ const fullKey = `${prefix}${key}`;
753
+ const value = await redis.get(fullKey);
754
+ if (!value) {
755
+ return { blocked: false, ttlMs: 0 };
756
+ }
757
+ const ttlSeconds = await redis.ttl(fullKey);
758
+ if (ttlSeconds <= 0) {
759
+ return { blocked: false, ttlMs: 0 };
760
+ }
761
+ return { blocked: true, ttlMs: ttlSeconds * 1e3 };
762
+ },
763
+ async setBlock(key, durationSeconds) {
764
+ const fullKey = `${prefix}${key}`;
765
+ await redis.setex(fullKey, durationSeconds, "1");
766
+ },
767
+ async reset(key) {
768
+ const fullKey = `${prefix}${key}`;
769
+ await redis.del(fullKey);
770
+ }
771
+ };
772
+ }
773
+
693
774
  // src/auth/audit.ts
694
775
  var StandardAuditActions = {
695
776
  // Authentication
@@ -848,7 +929,277 @@ function createAuditLogger(options = {}) {
848
929
  }
849
930
  return { log, createTimedAudit };
850
931
  }
932
+
933
+ // src/api.ts
934
+ var ApiErrorCode = {
935
+ VALIDATION_ERROR: "VALIDATION_ERROR",
936
+ UNAUTHORIZED: "UNAUTHORIZED",
937
+ FORBIDDEN: "FORBIDDEN",
938
+ NOT_FOUND: "NOT_FOUND",
939
+ CONFLICT: "CONFLICT",
940
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
941
+ INTERNAL_ERROR: "INTERNAL_ERROR",
942
+ DATABASE_ERROR: "DATABASE_ERROR",
943
+ EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
944
+ CONFIGURATION_ERROR: "CONFIGURATION_ERROR"
945
+ };
946
+ var ApiError = class extends Error {
947
+ statusCode;
948
+ code;
949
+ details;
950
+ constructor(statusCode, message, code = ApiErrorCode.INTERNAL_ERROR, details) {
951
+ super(message);
952
+ this.name = "ApiError";
953
+ this.statusCode = statusCode;
954
+ this.code = code;
955
+ this.details = details;
956
+ }
957
+ };
958
+ function isApiError(error) {
959
+ return error instanceof ApiError;
960
+ }
961
+ var CommonApiErrors = {
962
+ unauthorized: (msg = "Unauthorized") => new ApiError(401, msg, ApiErrorCode.UNAUTHORIZED),
963
+ forbidden: (msg = "Forbidden") => new ApiError(403, msg, ApiErrorCode.FORBIDDEN),
964
+ notFound: (resource = "Resource") => new ApiError(404, `${resource} not found`, ApiErrorCode.NOT_FOUND),
965
+ conflict: (msg = "Resource already exists") => new ApiError(409, msg, ApiErrorCode.CONFLICT),
966
+ rateLimitExceeded: (msg = "Rate limit exceeded") => new ApiError(429, msg, ApiErrorCode.RATE_LIMIT_EXCEEDED),
967
+ validationError: (details) => new ApiError(
968
+ 400,
969
+ "Validation failed",
970
+ ApiErrorCode.VALIDATION_ERROR,
971
+ details
972
+ ),
973
+ internalError: (msg = "Internal server error") => new ApiError(500, msg, ApiErrorCode.INTERNAL_ERROR)
974
+ };
975
+ var PG_ERROR_MAP = {
976
+ "23505": {
977
+ status: 409,
978
+ code: ApiErrorCode.CONFLICT,
979
+ message: "Resource already exists"
980
+ },
981
+ "23503": {
982
+ status: 404,
983
+ code: ApiErrorCode.NOT_FOUND,
984
+ message: "Referenced resource not found"
985
+ },
986
+ PGRST116: {
987
+ status: 404,
988
+ code: ApiErrorCode.NOT_FOUND,
989
+ message: "Resource not found"
990
+ }
991
+ };
992
+ function classifyError(error, isDev = false) {
993
+ if (isApiError(error)) {
994
+ return {
995
+ status: error.statusCode,
996
+ body: { error: error.message, code: error.code, details: error.details }
997
+ };
998
+ }
999
+ if (error && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
1000
+ return {
1001
+ status: 400,
1002
+ body: {
1003
+ error: "Validation failed",
1004
+ code: ApiErrorCode.VALIDATION_ERROR,
1005
+ details: error.issues.map((i) => ({
1006
+ field: i.path?.join("."),
1007
+ message: i.message
1008
+ }))
1009
+ }
1010
+ };
1011
+ }
1012
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1013
+ const pgCode = error.code;
1014
+ const mapped = PG_ERROR_MAP[pgCode];
1015
+ if (mapped) {
1016
+ return {
1017
+ status: mapped.status,
1018
+ body: { error: mapped.message, code: mapped.code }
1019
+ };
1020
+ }
1021
+ return {
1022
+ status: 500,
1023
+ body: {
1024
+ error: "Database error",
1025
+ code: ApiErrorCode.DATABASE_ERROR,
1026
+ details: isDev ? error.message : void 0
1027
+ }
1028
+ };
1029
+ }
1030
+ if (error instanceof Error) {
1031
+ return {
1032
+ status: 500,
1033
+ body: {
1034
+ error: isDev ? error.message : "Internal server error",
1035
+ code: ApiErrorCode.INTERNAL_ERROR,
1036
+ details: isDev ? error.stack : void 0
1037
+ }
1038
+ };
1039
+ }
1040
+ return {
1041
+ status: 500,
1042
+ body: {
1043
+ error: "An unexpected error occurred",
1044
+ code: ApiErrorCode.INTERNAL_ERROR
1045
+ }
1046
+ };
1047
+ }
1048
+ function buildPagination(page, limit, total) {
1049
+ return {
1050
+ page,
1051
+ limit,
1052
+ total,
1053
+ totalPages: Math.ceil(total / limit),
1054
+ hasMore: page * limit < total
1055
+ };
1056
+ }
1057
+
1058
+ // src/auth/nextjs-api.ts
1059
+ async function enforceRateLimit(request, operation, rule, options) {
1060
+ const identifier = options?.identifier ?? (options?.userId ? `user:${options.userId}` : void 0) ?? `ip:${extractClientIp((name) => request.headers.get(name))}`;
1061
+ const isAuthenticated = !!options?.userId;
1062
+ const result = await checkRateLimit(operation, identifier, rule, {
1063
+ ...options?.rateLimitOptions,
1064
+ isAuthenticated
1065
+ });
1066
+ if (!result.allowed) {
1067
+ const headers = buildRateLimitResponseHeaders(result);
1068
+ return new Response(
1069
+ JSON.stringify({
1070
+ error: "Rate limit exceeded. Please try again later.",
1071
+ retryAfter: result.retryAfterSeconds
1072
+ }),
1073
+ {
1074
+ status: 429,
1075
+ headers: { "Content-Type": "application/json", ...headers }
1076
+ }
1077
+ );
1078
+ }
1079
+ return null;
1080
+ }
1081
+ function errorResponse(error, options) {
1082
+ const isDev = options?.isDevelopment ?? process.env.NODE_ENV === "development";
1083
+ const { status, body } = classifyError(error, isDev);
1084
+ return new Response(JSON.stringify(body), {
1085
+ status,
1086
+ headers: { "Content-Type": "application/json" }
1087
+ });
1088
+ }
1089
+ function zodErrorResponse(error) {
1090
+ const firstIssue = error.issues[0];
1091
+ const message = firstIssue ? `${firstIssue.path.join(".") || "input"}: ${firstIssue.message}` : "Validation error";
1092
+ return new Response(JSON.stringify({ error: message }), {
1093
+ status: 400,
1094
+ headers: { "Content-Type": "application/json" }
1095
+ });
1096
+ }
1097
+ function extractBearerToken(request) {
1098
+ const auth = request.headers.get("authorization");
1099
+ if (!auth?.startsWith("Bearer ")) return null;
1100
+ return auth.slice(7).trim() || null;
1101
+ }
1102
+ function isValidBearerToken(request, secret) {
1103
+ if (!secret) return false;
1104
+ const token = extractBearerToken(request);
1105
+ if (!token) return false;
1106
+ return constantTimeEqual(token, secret);
1107
+ }
1108
+
1109
+ // src/env.ts
1110
+ function getRequiredEnv(key) {
1111
+ const value = process.env[key];
1112
+ if (!value) {
1113
+ throw new Error(`Missing required environment variable: ${key}`);
1114
+ }
1115
+ return value;
1116
+ }
1117
+ function getOptionalEnv(key, defaultValue) {
1118
+ return process.env[key] || defaultValue;
1119
+ }
1120
+ function getBoolEnv(key, defaultValue = false) {
1121
+ const value = process.env[key];
1122
+ if (value === void 0 || value === "") return defaultValue;
1123
+ return value === "true" || value === "1";
1124
+ }
1125
+ function getIntEnv(key, defaultValue) {
1126
+ const value = process.env[key];
1127
+ if (value === void 0 || value === "") return defaultValue;
1128
+ const parsed = parseInt(value, 10);
1129
+ return isNaN(parsed) ? defaultValue : parsed;
1130
+ }
1131
+ function validateEnvVars(config) {
1132
+ const result = checkEnvVars(config);
1133
+ if (!result.valid) {
1134
+ const lines = [];
1135
+ if (result.missing.length > 0) {
1136
+ lines.push(
1137
+ "Missing required environment variables:",
1138
+ ...result.missing.map((v) => ` - ${v}`)
1139
+ );
1140
+ }
1141
+ if (result.missingOneOf.length > 0) {
1142
+ for (const group of result.missingOneOf) {
1143
+ lines.push(`Missing one of: ${group.join(" | ")}`);
1144
+ }
1145
+ }
1146
+ if (result.invalid.length > 0) {
1147
+ lines.push(
1148
+ "Invalid environment variables:",
1149
+ ...result.invalid.map((v) => ` - ${v.key}: ${v.reason}`)
1150
+ );
1151
+ }
1152
+ throw new Error(lines.join("\n"));
1153
+ }
1154
+ }
1155
+ function checkEnvVars(config) {
1156
+ const missing = [];
1157
+ const invalid = [];
1158
+ const missingOneOf = [];
1159
+ if (config.required) {
1160
+ for (const key of config.required) {
1161
+ if (!process.env[key]) {
1162
+ missing.push(key);
1163
+ }
1164
+ }
1165
+ }
1166
+ if (config.requireOneOf) {
1167
+ for (const group of config.requireOneOf) {
1168
+ const hasAny = group.some((key) => !!process.env[key]);
1169
+ if (!hasAny) {
1170
+ missingOneOf.push(group);
1171
+ }
1172
+ }
1173
+ }
1174
+ if (config.validators) {
1175
+ for (const [key, validator] of Object.entries(config.validators)) {
1176
+ const value = process.env[key];
1177
+ if (value) {
1178
+ const result = validator(value);
1179
+ if (result !== true) {
1180
+ invalid.push({ key, reason: result });
1181
+ }
1182
+ }
1183
+ }
1184
+ }
1185
+ return {
1186
+ valid: missing.length === 0 && invalid.length === 0 && missingOneOf.length === 0,
1187
+ missing,
1188
+ invalid,
1189
+ missingOneOf
1190
+ };
1191
+ }
1192
+ function getEnvSummary(keys) {
1193
+ const summary = {};
1194
+ for (const key of keys) {
1195
+ summary[key] = !!process.env[key];
1196
+ }
1197
+ return summary;
1198
+ }
851
1199
  export {
1200
+ ApiError,
1201
+ ApiErrorCode,
1202
+ CommonApiErrors,
852
1203
  CommonRateLimits,
853
1204
  DateRangeSchema,
854
1205
  EmailSchema,
@@ -868,33 +1219,56 @@ export {
868
1219
  buildAuthCookies,
869
1220
  buildErrorBody,
870
1221
  buildKeycloakCallbacks,
1222
+ buildPagination,
871
1223
  buildRateLimitHeaders,
872
1224
  buildRateLimitResponseHeaders,
873
1225
  buildRedirectCallback,
874
1226
  buildTokenRefreshParams,
1227
+ checkEnvVars,
875
1228
  checkRateLimit,
1229
+ classifyError,
1230
+ constantTimeEqual,
1231
+ containsHtml,
1232
+ containsUrls,
876
1233
  createAuditActor,
877
1234
  createAuditLogger,
878
1235
  createFeatureFlags,
879
1236
  createMemoryRateLimitStore,
1237
+ createRedisRateLimitStore,
880
1238
  createSafeTextSchema,
881
1239
  detectStage,
1240
+ enforceRateLimit,
1241
+ errorResponse,
1242
+ escapeHtml,
882
1243
  extractAuditIp,
883
1244
  extractAuditRequestId,
884
1245
  extractAuditUserAgent,
1246
+ extractBearerToken,
885
1247
  extractClientIp,
1248
+ getBoolEnv,
1249
+ getCorrelationId,
886
1250
  getEndSessionEndpoint,
1251
+ getEnvSummary,
1252
+ getIntEnv,
1253
+ getOptionalEnv,
887
1254
  getRateLimitStatus,
1255
+ getRequiredEnv,
888
1256
  getTokenEndpoint,
889
1257
  hasAllRoles,
890
1258
  hasAnyRole,
891
1259
  hasRole,
892
1260
  isAllowlisted,
1261
+ isApiError,
893
1262
  isTokenExpired,
1263
+ isValidBearerToken,
894
1264
  parseKeycloakRoles,
895
1265
  refreshKeycloakToken,
896
1266
  resetRateLimitForKey,
897
1267
  resolveIdentifier,
898
- resolveRateLimitIdentifier
1268
+ resolveRateLimitIdentifier,
1269
+ sanitizeApiError,
1270
+ stripHtml,
1271
+ validateEnvVars,
1272
+ zodErrorResponse
899
1273
  };
900
1274
  //# sourceMappingURL=auth.mjs.map