strapi-plugin-oidc 1.8.3 → 1.8.5

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.
@@ -345,9 +345,149 @@ function clearAuthCookies(strapi2, ctx) {
345
345
  ctx.cookies.set(name, "", rootPathOptions);
346
346
  }
347
347
  }
348
- const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
349
- function isValidEmail(email) {
350
- return EMAIL_REGEX.test(email);
348
+ class OidcError extends Error {
349
+ kind;
350
+ cause;
351
+ constructor(kind, message, cause) {
352
+ super(message);
353
+ this.name = "OidcError";
354
+ this.kind = kind;
355
+ this.cause = cause;
356
+ }
357
+ }
358
+ const OIDC_ERROR_DISPATCH = {
359
+ nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
360
+ token_exchange_failed: {
361
+ action: "token_exchange_failed",
362
+ code: errorCodes.TOKEN_EXCHANGE_FAILED
363
+ },
364
+ id_token_parse_failed: {
365
+ action: "login_failure",
366
+ code: errorCodes.ID_TOKEN_PARSE_FAILED,
367
+ key: "id_token_parse_failed"
368
+ },
369
+ userinfo_fetch_failed: {
370
+ action: "login_failure",
371
+ code: errorCodes.USERINFO_FETCH_FAILED,
372
+ key: "userinfo_fetch_failed"
373
+ },
374
+ user_creation_failed: {
375
+ action: "login_failure",
376
+ code: errorCodes.USER_CREATION_FAILED,
377
+ key: "user_creation_failed"
378
+ },
379
+ whitelist_rejected: {
380
+ action: "whitelist_rejected",
381
+ code: errorCodes.WHITELIST_CHECK_FAILED,
382
+ key: "whitelist_rejected"
383
+ },
384
+ invalid_email: {
385
+ action: "login_failure",
386
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
387
+ key: "sign_in_unknown"
388
+ },
389
+ email_not_verified: {
390
+ action: "email_not_verified",
391
+ code: errorCodes.EMAIL_NOT_VERIFIED,
392
+ key: "email_not_verified"
393
+ },
394
+ id_token_invalid: {
395
+ action: "id_token_invalid",
396
+ code: errorCodes.ID_TOKEN_INVALID,
397
+ key: "id_token_invalid"
398
+ },
399
+ unknown: {
400
+ action: "login_failure",
401
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
402
+ key: "sign_in_unknown"
403
+ }
404
+ };
405
+ function toMessage(e) {
406
+ return e instanceof Error ? e.message : String(e);
407
+ }
408
+ const REQUIRED_CONFIG_KEYS = [
409
+ "OIDC_DISCOVERY_URL",
410
+ "OIDC_CLIENT_ID",
411
+ "OIDC_CLIENT_SECRET",
412
+ "OIDC_REDIRECT_URI",
413
+ "OIDC_SCOPE",
414
+ "OIDC_FAMILY_NAME_FIELD",
415
+ "OIDC_GIVEN_NAME_FIELD",
416
+ // Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
417
+ "OIDC_TOKEN_ENDPOINT",
418
+ "OIDC_USERINFO_ENDPOINT",
419
+ "OIDC_AUTHORIZATION_ENDPOINT"
420
+ ];
421
+ const jwksCache = /* @__PURE__ */ new Map();
422
+ let jwksDisabledWarned = false;
423
+ function getJwks(uri) {
424
+ let jwks = jwksCache.get(uri);
425
+ if (!jwks) {
426
+ jwks = jose.createRemoteJWKSet(new URL(uri));
427
+ jwksCache.set(uri, jwks);
428
+ }
429
+ return jwks;
430
+ }
431
+ async function verifyIdToken(idToken, config2) {
432
+ const jwksUri = config2.OIDC_JWKS_URI;
433
+ const issuer = config2.OIDC_ISSUER;
434
+ if (!jwksUri) {
435
+ if (!jwksDisabledWarned) {
436
+ jwksDisabledWarned = true;
437
+ strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
438
+ }
439
+ return null;
440
+ }
441
+ try {
442
+ const jwks = getJwks(jwksUri);
443
+ const { payload } = await jose.jwtVerify(idToken, jwks, {
444
+ issuer: issuer || void 0,
445
+ audience: config2.OIDC_CLIENT_ID
446
+ });
447
+ return payload;
448
+ } catch (e) {
449
+ if (e instanceof jose.errors.JWTClaimValidationFailed || e instanceof jose.errors.JWSSignatureVerificationFailed || e instanceof jose.errors.JWTExpired || e instanceof jose.errors.JWTInvalid || e instanceof jose.errors.JWSInvalid) {
450
+ const msg = toMessage(e);
451
+ throw new OidcError("id_token_invalid", msg, e);
452
+ }
453
+ throw e;
454
+ }
455
+ }
456
+ function configValidation() {
457
+ const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
458
+ const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
459
+ if (missing.length === 0) {
460
+ return config2;
461
+ }
462
+ throw new Error(errorMessages.MISSING_CONFIG(missing.join(", ")));
463
+ }
464
+ async function oidcSignIn(ctx) {
465
+ const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
466
+ const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
467
+ const state = node_crypto.randomBytes(32).toString("base64url");
468
+ const nonce = node_crypto.randomBytes(32).toString("base64url");
469
+ const cookieOptions = {
470
+ httpOnly: true,
471
+ maxAge: 6e5,
472
+ secure: shouldMarkSecure(strapi, ctx),
473
+ sameSite: "lax"
474
+ };
475
+ ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
476
+ ctx.cookies.set("oidc_state", state, cookieOptions);
477
+ ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
478
+ const params = new URLSearchParams({
479
+ response_type: "code",
480
+ client_id: OIDC_CLIENT_ID,
481
+ redirect_uri: OIDC_REDIRECT_URI,
482
+ scope: OIDC_SCOPE,
483
+ code_challenge: codeChallenge,
484
+ code_challenge_method: "S256",
485
+ state,
486
+ nonce
487
+ });
488
+ const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
489
+ ctx.set("Location", authorizationUrl);
490
+ return ctx.send({}, 302);
351
491
  }
352
492
  const en = {
353
493
  "global.plugins.strapi-plugin-oidc": "OIDC Plugin",
@@ -510,70 +650,20 @@ const authPageMessages = (locale) => ({
510
650
  errorTitle: t(locale, "auth.page.error.title", "Authentication Failed"),
511
651
  returnToLogin: t(locale, "auth.page.error.returnToLogin", "Return to Login")
512
652
  });
513
- class OidcError extends Error {
514
- kind;
515
- cause;
516
- constructor(kind, message, cause) {
517
- super(message);
518
- this.name = "OidcError";
519
- this.kind = kind;
520
- this.cause = cause;
521
- }
522
- }
523
- const OIDC_ERROR_DISPATCH = {
524
- nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
525
- token_exchange_failed: {
526
- action: "token_exchange_failed",
527
- code: errorCodes.TOKEN_EXCHANGE_FAILED
528
- },
529
- id_token_parse_failed: {
530
- action: "login_failure",
531
- code: errorCodes.ID_TOKEN_PARSE_FAILED,
532
- key: "id_token_parse_failed"
533
- },
534
- userinfo_fetch_failed: {
535
- action: "login_failure",
536
- code: errorCodes.USERINFO_FETCH_FAILED,
537
- key: "userinfo_fetch_failed"
538
- },
539
- user_creation_failed: {
540
- action: "login_failure",
541
- code: errorCodes.USER_CREATION_FAILED,
542
- key: "user_creation_failed"
543
- },
544
- whitelist_rejected: {
545
- action: "whitelist_rejected",
546
- code: errorCodes.WHITELIST_CHECK_FAILED,
547
- key: "whitelist_rejected"
548
- },
549
- invalid_email: {
550
- action: "login_failure",
551
- code: errorCodes.TOKEN_EXCHANGE_FAILED,
552
- key: "sign_in_unknown"
553
- },
554
- email_not_verified: {
555
- action: "email_not_verified",
556
- code: errorCodes.EMAIL_NOT_VERIFIED,
557
- key: "email_not_verified"
558
- },
559
- id_token_invalid: {
560
- action: "id_token_invalid",
561
- code: errorCodes.ID_TOKEN_INVALID,
562
- key: "id_token_invalid"
563
- },
564
- unknown: {
565
- action: "login_failure",
566
- code: errorCodes.TOKEN_EXCHANGE_FAILED,
567
- key: "sign_in_unknown"
568
- }
569
- };
570
- const TRUSTED_IP_HEADER = "cf-connecting-ip";
653
+ const TRUSTED_IP_HEADERS = /* @__PURE__ */ new Set([
654
+ "cf-connecting-ip",
655
+ "true-client-ip",
656
+ "x-real-ip",
657
+ "fastly-client-ip",
658
+ "fly-client-ip",
659
+ "x-nf-client-connection-ip"
660
+ ]);
571
661
  function getTrustedHeaderName() {
572
662
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
573
663
  const raw = config2.OIDC_TRUSTED_IP_HEADER;
574
664
  if (typeof raw !== "string" || !raw) return void 0;
575
665
  const normalized = raw.trim().toLowerCase();
576
- return normalized === TRUSTED_IP_HEADER ? normalized : void 0;
666
+ return TRUSTED_IP_HEADERS.has(normalized) ? normalized : void 0;
577
667
  }
578
668
  function getClientIp(ctx) {
579
669
  const proxyTrusted = ctx.app?.proxy === true;
@@ -590,128 +680,9 @@ function getClientIp(ctx) {
590
680
  }
591
681
  return ctx.ip;
592
682
  }
593
- function toMessage(e) {
594
- return e instanceof Error ? e.message : String(e);
595
- }
596
- const REQUIRED_CONFIG_KEYS = [
597
- "OIDC_DISCOVERY_URL",
598
- "OIDC_CLIENT_ID",
599
- "OIDC_CLIENT_SECRET",
600
- "OIDC_REDIRECT_URI",
601
- "OIDC_SCOPE",
602
- "OIDC_FAMILY_NAME_FIELD",
603
- "OIDC_GIVEN_NAME_FIELD",
604
- // Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
605
- "OIDC_TOKEN_ENDPOINT",
606
- "OIDC_USERINFO_ENDPOINT",
607
- "OIDC_AUTHORIZATION_ENDPOINT"
608
- ];
609
- const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
610
- const jwksCache = /* @__PURE__ */ new Map();
611
- let jwksDisabledWarned = false;
612
- function getJwks(uri) {
613
- let jwks = jwksCache.get(uri);
614
- if (!jwks) {
615
- jwks = jose.createRemoteJWKSet(new URL(uri));
616
- jwksCache.set(uri, jwks);
617
- }
618
- return jwks;
619
- }
620
- async function verifyIdToken(idToken, config2) {
621
- const jwksUri = config2.OIDC_JWKS_URI;
622
- const issuer = config2.OIDC_ISSUER;
623
- if (!jwksUri) {
624
- if (!jwksDisabledWarned) {
625
- jwksDisabledWarned = true;
626
- strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
627
- }
628
- return null;
629
- }
630
- try {
631
- const jwks = getJwks(jwksUri);
632
- const { payload } = await jose.jwtVerify(idToken, jwks, {
633
- issuer: issuer || void 0,
634
- audience: config2.OIDC_CLIENT_ID
635
- });
636
- return payload;
637
- } catch (e) {
638
- if (e instanceof jose.errors.JWTClaimValidationFailed || e instanceof jose.errors.JWSSignatureVerificationFailed || e instanceof jose.errors.JWTExpired || e instanceof jose.errors.JWTInvalid || e instanceof jose.errors.JWSInvalid) {
639
- const msg = toMessage(e);
640
- throw new OidcError("id_token_invalid", msg, e);
641
- }
642
- throw e;
643
- }
644
- }
645
- function configValidation() {
646
- const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
647
- const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
648
- if (missing.length === 0) {
649
- return config2;
650
- }
651
- throw new Error(errorMessages.MISSING_CONFIG(missing.join(", ")));
652
- }
653
- async function oidcSignIn(ctx) {
654
- const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPE, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
655
- const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
656
- const state = node_crypto.randomBytes(32).toString("base64url");
657
- const nonce = node_crypto.randomBytes(32).toString("base64url");
658
- const cookieOptions = {
659
- httpOnly: true,
660
- maxAge: 6e5,
661
- secure: shouldMarkSecure(strapi, ctx),
662
- sameSite: "lax"
663
- };
664
- ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
665
- ctx.cookies.set("oidc_state", state, cookieOptions);
666
- ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
667
- const params = new URLSearchParams({
668
- response_type: "code",
669
- client_id: OIDC_CLIENT_ID,
670
- redirect_uri: OIDC_REDIRECT_URI,
671
- scope: OIDC_SCOPE,
672
- code_challenge: codeChallenge,
673
- code_challenge_method: "S256",
674
- state,
675
- nonce
676
- });
677
- const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
678
- ctx.set("Location", authorizationUrl);
679
- return ctx.send({}, 302);
680
- }
681
- async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
682
- const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
683
- method: "POST",
684
- body: params,
685
- headers: {
686
- "Content-Type": "application/x-www-form-urlencoded"
687
- }
688
- });
689
- if (!response.ok) {
690
- throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
691
- }
692
- const tokenData = await response.json();
693
- if (tokenData.id_token) {
694
- const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
695
- try {
696
- const idTokenPayload = verifiedPayload ?? JSON.parse(
697
- Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
698
- );
699
- if (idTokenPayload.nonce !== expectedNonce) {
700
- throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
701
- }
702
- } catch (e) {
703
- if (e instanceof OidcError) throw e;
704
- throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
705
- }
706
- }
707
- const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
708
- headers: { Authorization: `Bearer ${tokenData.access_token}` }
709
- });
710
- if (!userResponse.ok) {
711
- throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
712
- }
713
- const userInfo = await userResponse.json();
714
- return { userInfo, accessToken: tokenData.access_token };
683
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
684
+ function isValidEmail(email) {
685
+ return EMAIL_REGEX.test(email);
715
686
  }
716
687
  function collectGroupMapRoleNames(userInfo, config2) {
717
688
  const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
@@ -891,6 +862,61 @@ function classifyOidcError(e, userInfo) {
891
862
  params
892
863
  };
893
864
  }
865
+ async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
866
+ const errorInfo = classifyOidcError(e, userInfo);
867
+ const message = toMessage(e);
868
+ await auditLog2.log({
869
+ action: errorInfo.action,
870
+ email: userInfo?.email,
871
+ ip: getClientIp(ctx),
872
+ detailsKey: errorInfo.action,
873
+ detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
874
+ });
875
+ strapi.log.error({
876
+ code: errorInfo.code,
877
+ phase: "oidc_callback",
878
+ message: e instanceof Error ? e.message : "Unknown sign-in error",
879
+ detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
880
+ email: userInfo?.email
881
+ });
882
+ const locale = negotiateLocale(ctx.request.headers["accept-language"]);
883
+ ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
884
+ }
885
+ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
886
+ const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
887
+ method: "POST",
888
+ body: params,
889
+ headers: {
890
+ "Content-Type": "application/x-www-form-urlencoded"
891
+ }
892
+ });
893
+ if (!response.ok) {
894
+ throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
895
+ }
896
+ const tokenData = await response.json();
897
+ if (tokenData.id_token) {
898
+ const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
899
+ try {
900
+ const idTokenPayload = verifiedPayload ?? JSON.parse(
901
+ Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
902
+ );
903
+ if (idTokenPayload.nonce !== expectedNonce) {
904
+ throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
905
+ }
906
+ } catch (e) {
907
+ if (e instanceof OidcError) throw e;
908
+ throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
909
+ }
910
+ }
911
+ const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
912
+ headers: { Authorization: `Bearer ${tokenData.access_token}` }
913
+ });
914
+ if (!userResponse.ok) {
915
+ throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
916
+ }
917
+ const userInfo = await userResponse.json();
918
+ return { userInfo, accessToken: tokenData.access_token };
919
+ }
894
920
  function readAndClearPkceCookies(ctx) {
895
921
  const oidcState = ctx.cookies.get("oidc_state");
896
922
  const codeVerifier = ctx.cookies.get("oidc_code_verifier");
@@ -924,26 +950,6 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
924
950
  }
925
951
  await Promise.all(entries);
926
952
  }
927
- async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
928
- const errorInfo = classifyOidcError(e, userInfo);
929
- const message = toMessage(e);
930
- await auditLog2.log({
931
- action: errorInfo.action,
932
- email: userInfo?.email,
933
- ip: getClientIp(ctx),
934
- detailsKey: errorInfo.action,
935
- detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
936
- });
937
- strapi.log.error({
938
- code: errorInfo.code,
939
- phase: "oidc_callback",
940
- message: e instanceof Error ? e.message : "Unknown sign-in error",
941
- detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
942
- email: userInfo?.email
943
- });
944
- const locale = negotiateLocale(ctx.request.headers["accept-language"]);
945
- ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
946
- }
947
953
  async function oidcSignInCallback(ctx) {
948
954
  const config2 = configValidation();
949
955
  const oauthService2 = getOauthService();
@@ -1011,6 +1017,7 @@ async function oidcSignInCallback(ctx) {
1011
1017
  await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
1012
1018
  }
1013
1019
  }
1020
+ const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
1014
1021
  async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
1015
1022
  try {
1016
1023
  const response = await fetch(userinfoEndpoint, {
@@ -2054,6 +2061,7 @@ function translateDetails(key, params) {
2054
2061
  if (!translation) return null;
2055
2062
  return interpolate(translation, params);
2056
2063
  }
2064
+ const DAY_MS = 864e5;
2057
2065
  const STRING_OP_MAP = {
2058
2066
  $eq: (v) => v,
2059
2067
  $contains: (v) => ({ $containsi: v }),
@@ -2066,9 +2074,11 @@ const DATE_OP_MAP = {
2066
2074
  $lt: (v) => ({ $lt: v }),
2067
2075
  $lte: (v) => ({ $lte: v }),
2068
2076
  $between: (v) => ({ $between: v })
2069
- // $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
2070
2077
  };
2071
- const DAY_MS = 864e5;
2078
+ const ACTION_OP_MAP = {
2079
+ $eq: (v) => v,
2080
+ $in: (v) => ({ $in: v })
2081
+ };
2072
2082
  function nextDayIso(iso) {
2073
2083
  return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
2074
2084
  }
@@ -2076,10 +2086,6 @@ function expandCreatedAtInToDayRanges(days) {
2076
2086
  const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
2077
2087
  return ranges.length === 1 ? ranges[0] : { $or: ranges };
2078
2088
  }
2079
- const ACTION_OP_MAP = {
2080
- $eq: (v) => v,
2081
- $in: (v) => ({ $in: v })
2082
- };
2083
2089
  function mapFieldFilter(conditions, field, filter, opMap) {
2084
2090
  for (const [op, value] of Object.entries(filter)) {
2085
2091
  const transform = opMap[op];
@@ -2088,20 +2094,26 @@ function mapFieldFilter(conditions, field, filter, opMap) {
2088
2094
  if (result !== void 0) conditions.push({ [field]: result });
2089
2095
  }
2090
2096
  }
2097
+ function buildDateConditions(conditions, createdAt) {
2098
+ if (!createdAt) return;
2099
+ const { $in: inDays, ...rest } = createdAt;
2100
+ if (Array.isArray(inDays) && inDays.length > 0) {
2101
+ conditions.push(expandCreatedAtInToDayRanges(inDays));
2102
+ }
2103
+ for (const [op, value] of Object.entries(rest)) {
2104
+ const transform = DATE_OP_MAP[op];
2105
+ if (transform) {
2106
+ const result = transform(value);
2107
+ if (result !== void 0) conditions.push({ createdAt: result });
2108
+ }
2109
+ }
2110
+ }
2091
2111
  function buildWhereClause(filters) {
2092
2112
  const conditions = [];
2093
2113
  if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
2094
2114
  if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
2095
2115
  if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
2096
- if (filters.createdAt) {
2097
- const { $in: inDays, ...rest } = filters.createdAt;
2098
- if (Array.isArray(inDays) && inDays.length > 0) {
2099
- conditions.push(expandCreatedAtInToDayRanges(inDays));
2100
- }
2101
- if (Object.keys(rest).length > 0) {
2102
- mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
2103
- }
2104
- }
2116
+ if (filters.createdAt) buildDateConditions(conditions, filters.createdAt);
2105
2117
  if (conditions.length === 0) return {};
2106
2118
  if (conditions.length === 1) return conditions[0];
2107
2119
  return { $and: conditions };