redis-otp-manager 0.1.0 → 0.2.2

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Vijay prakash
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vijay prakash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,18 +1,21 @@
1
1
  # redis-otp-manager
2
2
 
3
- Lightweight, Redis-backed OTP manager for Node.js apps.
3
+ Lightweight, Redis-backed OTP manager for Node.js and NestJS apps.
4
4
 
5
- ## Current MVP
5
+ ## Current Features
6
6
 
7
- This first implementation includes:
7
+ This package currently includes:
8
8
  - OTP generation
9
9
  - OTP verification
10
10
  - SHA-256 OTP hashing
11
11
  - Redis-compatible storage adapter
12
12
  - In-memory adapter for tests
13
13
  - Intent-aware key strategy
14
+ - Conservative identifier normalization
14
15
  - Rate limiting
16
+ - Optional resend cooldown
15
17
  - Max-attempt protection
18
+ - NestJS module integration via `redis-otp-manager/nest`
16
19
 
17
20
  ## Install
18
21
 
@@ -20,6 +23,12 @@ This first implementation includes:
20
23
  npm install redis-otp-manager
21
24
  ```
22
25
 
26
+ For NestJS apps, also install the Nest peer dependencies used by your app:
27
+
28
+ ```bash
29
+ npm install @nestjs/common @nestjs/core reflect-metadata rxjs
30
+ ```
31
+
23
32
  ## Quality Checks
24
33
 
25
34
  ```bash
@@ -37,11 +46,12 @@ const otp = new OTPManager({
37
46
  store: new RedisAdapter(redisClient),
38
47
  ttl: 300,
39
48
  maxAttempts: 5,
49
+ resendCooldown: 45,
40
50
  rateLimit: {
41
51
  window: 60,
42
52
  max: 3,
43
53
  },
44
- devMode: process.env.NODE_ENV !== "production",
54
+ devMode: false,
45
55
  });
46
56
 
47
57
  const generated = await otp.generate({
@@ -58,6 +68,64 @@ await otp.verify({
58
68
  });
59
69
  ```
60
70
 
71
+ ## NestJS
72
+
73
+ Import the Nest integration from the dedicated subpath so non-Nest users do not pull Nest dependencies.
74
+
75
+ ```ts
76
+ import { Module } from "@nestjs/common";
77
+ import { createClient } from "redis";
78
+ import { OTPManager, RedisAdapter } from "redis-otp-manager";
79
+ import { OTPModule, InjectOTPManager } from "redis-otp-manager/nest";
80
+
81
+ const redisClient = createClient({ url: process.env.REDIS_URL });
82
+
83
+ @Module({
84
+ imports: [
85
+ OTPModule.forRoot({
86
+ store: new RedisAdapter(redisClient),
87
+ ttl: 300,
88
+ maxAttempts: 5,
89
+ resendCooldown: 45,
90
+ rateLimit: {
91
+ window: 60,
92
+ max: 3,
93
+ },
94
+ isGlobal: true,
95
+ }),
96
+ ],
97
+ })
98
+ export class AppModule {}
99
+ ```
100
+
101
+ Async setup is also supported:
102
+
103
+ ```ts
104
+ OTPModule.forRootAsync({
105
+ isGlobal: true,
106
+ inject: [ConfigService],
107
+ useFactory: (config: ConfigService) => ({
108
+ store: new RedisAdapter(createClient({ url: config.getOrThrow("REDIS_URL") })),
109
+ ttl: 300,
110
+ maxAttempts: 5,
111
+ resendCooldown: 45,
112
+ }),
113
+ });
114
+ ```
115
+
116
+ Inject in services:
117
+
118
+ ```ts
119
+ import { Injectable } from "@nestjs/common";
120
+ import { OTPManager } from "redis-otp-manager";
121
+ import { InjectOTPManager } from "redis-otp-manager/nest";
122
+
123
+ @Injectable()
124
+ export class AuthService {
125
+ constructor(@InjectOTPManager() private readonly otpManager: OTPManager) {}
126
+ }
127
+ ```
128
+
61
129
  ## API
62
130
 
63
131
  ### `new OTPManager(options)`
@@ -67,15 +135,26 @@ type OTPManagerOptions = {
67
135
  store: StoreAdapter;
68
136
  ttl: number;
69
137
  maxAttempts: number;
138
+ resendCooldown?: number;
70
139
  rateLimit?: {
71
140
  window: number;
72
141
  max: number;
73
142
  };
74
143
  devMode?: boolean;
75
144
  otpLength?: number;
145
+ identifierNormalization?: {
146
+ trim?: boolean;
147
+ lowercase?: boolean;
148
+ preserveCaseFor?: string[];
149
+ };
76
150
  };
77
151
  ```
78
152
 
153
+ Normalization is backward-compatible and conservative by default:
154
+ - identifiers are trimmed
155
+ - identifiers are lowercased for most channels
156
+ - `sms` and `token` preserve case by default
157
+
79
158
  ### `generate(input)`
80
159
 
81
160
  ```ts
@@ -114,6 +193,15 @@ Returns `true` or throws a typed error.
114
193
  - `OTPExpiredError`
115
194
  - `OTPInvalidError`
116
195
  - `OTPMaxAttemptsExceededError`
196
+ - `OTPResendCooldownError`
197
+
198
+ ## Safer Production Defaults
199
+
200
+ - `devMode: false`
201
+ - `otpLength: 8` for higher-risk flows
202
+ - `maxAttempts: 3`
203
+ - `resendCooldown: 30` or higher to reduce abuse
204
+ - keep Redis private and behind authenticated network access
117
205
 
118
206
  ## Key Design
119
207
 
@@ -121,12 +209,27 @@ Returns `true` or throws a typed error.
121
209
  otp:{intent}:{type}:{identifier}
122
210
  attempts:{intent}:{type}:{identifier}
123
211
  rate:{type}:{identifier}
212
+ cooldown:{intent}:{type}:{identifier}
213
+ ```
214
+
215
+ ## Release Automation
216
+
217
+ Publishing on every `main` merge is not recommended for npm packages because npm versions are immutable. The safer setup is:
218
+ - merge to `main` runs CI only
219
+ - publish happens when you push a version tag like `v0.2.0`
220
+
221
+ Required GitHub secrets:
222
+ - `NPM_TOKEN`
223
+
224
+ Tag-based publish:
225
+
226
+ ```bash
227
+ git tag v0.2.0
228
+ git push origin v0.2.0
124
229
  ```
125
230
 
126
231
  ## Next Roadmap
127
232
 
128
- - NestJS module and decorator
129
- - alphanumeric tokens
233
+ - atomic Redis verification
130
234
  - hooks/events
131
- - email token helpers
132
-
235
+ - analytics and observability
@@ -1,7 +1,8 @@
1
1
  import { generateNumericOtp } from "./otp.generator.js";
2
2
  import { hashOtp, verifyOtpHash } from "./otp.hash.js";
3
- import { OTPExpiredError, OTPInvalidError, OTPMaxAttemptsExceededError, } from "../errors/otp.errors.js";
4
- import { buildAttemptsKey, buildOtpKey, buildRateLimitKey, } from "../utils/key-builder.js";
3
+ import { OTPExpiredError, OTPInvalidError, OTPMaxAttemptsExceededError, OTPResendCooldownError, } from "../errors/otp.errors.js";
4
+ import { buildAttemptsKey, buildCooldownKey, buildOtpKey, buildRateLimitKey, } from "../utils/key-builder.js";
5
+ import { normalizePayloadIdentifier } from "../utils/identifier-normalizer.js";
5
6
  import { assertWithinRateLimit } from "../utils/rate-limiter.js";
6
7
  export class OTPManager {
7
8
  options;
@@ -15,14 +16,25 @@ export class OTPManager {
15
16
  }
16
17
  async generate(input) {
17
18
  validatePayload(input);
18
- const otpKey = buildOtpKey(input);
19
- const attemptsKey = buildAttemptsKey(input);
20
- const rateLimitKey = buildRateLimitKey(input);
19
+ const normalizedInput = normalizePayloadIdentifier(input, this.options.identifierNormalization);
20
+ const otpKey = buildOtpKey(normalizedInput);
21
+ const attemptsKey = buildAttemptsKey(normalizedInput);
22
+ const rateLimitKey = buildRateLimitKey(normalizedInput);
23
+ const cooldownKey = buildCooldownKey(normalizedInput);
24
+ if (this.options.resendCooldown) {
25
+ const cooldownActive = await this.options.store.get(cooldownKey);
26
+ if (cooldownActive) {
27
+ throw new OTPResendCooldownError();
28
+ }
29
+ }
21
30
  await assertWithinRateLimit(this.options.store, rateLimitKey, this.options.rateLimit);
22
31
  const otp = generateNumericOtp(this.options.otpLength);
23
32
  const hashedOtp = hashOtp(otp);
24
33
  await this.options.store.set(otpKey, hashedOtp, this.options.ttl);
25
34
  await this.options.store.del(attemptsKey);
35
+ if (this.options.resendCooldown) {
36
+ await this.options.store.set(cooldownKey, "1", this.options.resendCooldown);
37
+ }
26
38
  return {
27
39
  expiresIn: this.options.ttl,
28
40
  otp: this.options.devMode ? otp : undefined,
@@ -30,11 +42,12 @@ export class OTPManager {
30
42
  }
31
43
  async verify(input) {
32
44
  validatePayload(input);
45
+ const normalizedInput = normalizePayloadIdentifier(input, this.options.identifierNormalization);
33
46
  if (!input.otp || !input.otp.trim()) {
34
47
  throw new TypeError("OTP must be a non-empty string.");
35
48
  }
36
- const otpKey = buildOtpKey(input);
37
- const attemptsKey = buildAttemptsKey(input);
49
+ const otpKey = buildOtpKey(normalizedInput);
50
+ const attemptsKey = buildAttemptsKey(normalizedInput);
38
51
  const storedHash = await this.options.store.get(otpKey);
39
52
  if (!storedHash) {
40
53
  throw new OTPExpiredError();
@@ -65,6 +78,10 @@ function validateManagerOptions(options) {
65
78
  (!Number.isInteger(options.otpLength) || options.otpLength <= 0)) {
66
79
  throw new TypeError("otpLength must be a positive integer when provided.");
67
80
  }
81
+ if (options.resendCooldown !== undefined &&
82
+ (!Number.isInteger(options.resendCooldown) || options.resendCooldown <= 0)) {
83
+ throw new TypeError("resendCooldown must be a positive integer when provided.");
84
+ }
68
85
  if (options.rateLimit) {
69
86
  if (!Number.isInteger(options.rateLimit.window) || options.rateLimit.window <= 0) {
70
87
  throw new TypeError("rateLimit.window must be a positive integer.");
@@ -73,6 +90,20 @@ function validateManagerOptions(options) {
73
90
  throw new TypeError("rateLimit.max must be a positive integer.");
74
91
  }
75
92
  }
93
+ if (options.identifierNormalization) {
94
+ const { trim, lowercase, preserveCaseFor } = options.identifierNormalization;
95
+ if (trim !== undefined && typeof trim !== "boolean") {
96
+ throw new TypeError("identifierNormalization.trim must be a boolean.");
97
+ }
98
+ if (lowercase !== undefined && typeof lowercase !== "boolean") {
99
+ throw new TypeError("identifierNormalization.lowercase must be a boolean.");
100
+ }
101
+ if (preserveCaseFor !== undefined &&
102
+ (!Array.isArray(preserveCaseFor) ||
103
+ preserveCaseFor.some((channel) => typeof channel !== "string" || !channel.trim()))) {
104
+ throw new TypeError("identifierNormalization.preserveCaseFor must be an array of non-empty strings.");
105
+ }
106
+ }
76
107
  }
77
108
  function validatePayload(input) {
78
109
  if (!input.type || !input.type.trim()) {
@@ -13,6 +13,11 @@ export interface RateLimitConfig {
13
13
  window: number;
14
14
  max: number;
15
15
  }
16
+ export interface IdentifierNormalizationConfig {
17
+ trim?: boolean;
18
+ lowercase?: boolean;
19
+ preserveCaseFor?: OTPChannel[];
20
+ }
16
21
  export interface OTPManagerOptions {
17
22
  store: StoreAdapter;
18
23
  ttl: number;
@@ -20,6 +25,8 @@ export interface OTPManagerOptions {
20
25
  rateLimit?: RateLimitConfig;
21
26
  devMode?: boolean;
22
27
  otpLength?: number;
28
+ resendCooldown?: number;
29
+ identifierNormalization?: IdentifierNormalizationConfig;
23
30
  }
24
31
  export interface GenerateOTPResult {
25
32
  expiresIn: number;
@@ -14,3 +14,6 @@ export declare class OTPInvalidError extends OTPError {
14
14
  export declare class OTPMaxAttemptsExceededError extends OTPError {
15
15
  constructor(message?: string);
16
16
  }
17
+ export declare class OTPResendCooldownError extends OTPError {
18
+ constructor(message?: string);
19
+ }
@@ -26,3 +26,8 @@ export class OTPMaxAttemptsExceededError extends OTPError {
26
26
  super(message, "OTP_MAX_ATTEMPTS_EXCEEDED");
27
27
  }
28
28
  }
29
+ export class OTPResendCooldownError extends OTPError {
30
+ constructor(message = "OTP resend cooldown is active.") {
31
+ super(message, "OTP_RESEND_COOLDOWN");
32
+ }
33
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { OTPManager } from "./core/otp.service.js";
2
2
  export { RedisAdapter } from "./adapters/redis.adapter.js";
3
3
  export { MemoryAdapter } from "./adapters/memory.adapter.js";
4
- export type { GenerateOTPInput, GenerateOTPResult, OTPManagerOptions, OTPPayload, RateLimitConfig, StoreAdapter, VerifyOTPInput, } from "./core/otp.types.js";
5
- export { OTPError, OTPExpiredError, OTPInvalidError, OTPMaxAttemptsExceededError, OTPRateLimitExceededError, } from "./errors/otp.errors.js";
4
+ export type { GenerateOTPInput, GenerateOTPResult, IdentifierNormalizationConfig, OTPManagerOptions, OTPPayload, RateLimitConfig, StoreAdapter, VerifyOTPInput, } from "./core/otp.types.js";
5
+ export { OTPError, OTPExpiredError, OTPInvalidError, OTPMaxAttemptsExceededError, OTPRateLimitExceededError, OTPResendCooldownError, } from "./errors/otp.errors.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { OTPManager } from "./core/otp.service.js";
2
2
  export { RedisAdapter } from "./adapters/redis.adapter.js";
3
3
  export { MemoryAdapter } from "./adapters/memory.adapter.js";
4
- export { OTPError, OTPExpiredError, OTPInvalidError, OTPMaxAttemptsExceededError, OTPRateLimitExceededError, } from "./errors/otp.errors.js";
4
+ export { OTPError, OTPExpiredError, OTPInvalidError, OTPMaxAttemptsExceededError, OTPRateLimitExceededError, OTPResendCooldownError, } from "./errors/otp.errors.js";
@@ -0,0 +1,3 @@
1
+ export { OTPModule, OTP_MANAGER_OPTIONS, } from "./otp.module.js";
2
+ export { InjectOTPManager } from "./otp.decorator.js";
3
+ export type { OTPModuleAsyncOptions, OTPModuleOptions, } from "./otp.module.js";
@@ -0,0 +1,2 @@
1
+ export { OTPModule, OTP_MANAGER_OPTIONS, } from "./otp.module.js";
2
+ export { InjectOTPManager } from "./otp.decorator.js";
@@ -0,0 +1,2 @@
1
+ import { Inject } from "@nestjs/common";
2
+ export declare function InjectOTPManager(): ReturnType<typeof Inject>;
@@ -0,0 +1,5 @@
1
+ import { Inject } from "@nestjs/common";
2
+ import { OTPManager } from "../../core/otp.service.js";
3
+ export function InjectOTPManager() {
4
+ return Inject(OTPManager);
5
+ }
@@ -0,0 +1,15 @@
1
+ import type { DynamicModule, ModuleMetadata } from "@nestjs/common";
2
+ import type { OTPManagerOptions } from "../../core/otp.types.js";
3
+ export declare const OTP_MANAGER_OPTIONS: unique symbol;
4
+ export interface OTPModuleOptions extends OTPManagerOptions {
5
+ isGlobal?: boolean;
6
+ }
7
+ export interface OTPModuleAsyncOptions extends Pick<ModuleMetadata, "imports"> {
8
+ inject?: Array<string | symbol | Function>;
9
+ isGlobal?: boolean;
10
+ useFactory: (...args: any[]) => Promise<OTPManagerOptions> | OTPManagerOptions;
11
+ }
12
+ export declare class OTPModule {
13
+ static forRoot(options: OTPModuleOptions): DynamicModule;
14
+ static forRootAsync(options: OTPModuleAsyncOptions): DynamicModule;
15
+ }
@@ -0,0 +1,89 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { Module } from "@nestjs/common";
36
+ import { OTPManager } from "../../core/otp.service.js";
37
+ export const OTP_MANAGER_OPTIONS = Symbol("OTP_MANAGER_OPTIONS");
38
+ let OTPModule = (() => {
39
+ let _classDecorators = [Module({})];
40
+ let _classDescriptor;
41
+ let _classExtraInitializers = [];
42
+ let _classThis;
43
+ var OTPModule = class {
44
+ static { _classThis = this; }
45
+ static {
46
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
47
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
48
+ OTPModule = _classThis = _classDescriptor.value;
49
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
50
+ __runInitializers(_classThis, _classExtraInitializers);
51
+ }
52
+ static forRoot(options) {
53
+ const managerProvider = createManagerProvider(options);
54
+ return {
55
+ module: OTPModule,
56
+ global: options.isGlobal ?? false,
57
+ providers: [managerProvider],
58
+ exports: [OTPManager],
59
+ };
60
+ }
61
+ static forRootAsync(options) {
62
+ const optionsProvider = {
63
+ provide: OTP_MANAGER_OPTIONS,
64
+ inject: options.inject ?? [],
65
+ useFactory: options.useFactory,
66
+ };
67
+ const managerProvider = {
68
+ provide: OTPManager,
69
+ inject: [OTP_MANAGER_OPTIONS],
70
+ useFactory: (resolvedOptions) => new OTPManager(resolvedOptions),
71
+ };
72
+ return {
73
+ module: OTPModule,
74
+ global: options.isGlobal ?? false,
75
+ imports: options.imports ?? [],
76
+ providers: [optionsProvider, managerProvider],
77
+ exports: [OTPManager],
78
+ };
79
+ }
80
+ };
81
+ return OTPModule = _classThis;
82
+ })();
83
+ export { OTPModule };
84
+ function createManagerProvider(options) {
85
+ return {
86
+ provide: OTPManager,
87
+ useValue: new OTPManager(options),
88
+ };
89
+ }
@@ -0,0 +1,2 @@
1
+ import type { IdentifierNormalizationConfig, OTPPayload } from "../core/otp.types.js";
2
+ export declare function normalizePayloadIdentifier(payload: OTPPayload, config?: IdentifierNormalizationConfig): OTPPayload;
@@ -0,0 +1,27 @@
1
+ const DEFAULT_IDENTIFIER_NORMALIZATION = {
2
+ trim: true,
3
+ lowercase: true,
4
+ preserveCaseFor: ["sms", "token"],
5
+ };
6
+ export function normalizePayloadIdentifier(payload, config) {
7
+ const resolvedConfig = resolveNormalizationConfig(config);
8
+ let identifier = payload.identifier;
9
+ if (resolvedConfig.trim) {
10
+ identifier = identifier.trim();
11
+ }
12
+ const preserveCase = resolvedConfig.preserveCaseFor.includes(payload.type);
13
+ if (resolvedConfig.lowercase && !preserveCase) {
14
+ identifier = identifier.toLowerCase();
15
+ }
16
+ return {
17
+ ...payload,
18
+ identifier,
19
+ };
20
+ }
21
+ function resolveNormalizationConfig(config) {
22
+ return {
23
+ ...DEFAULT_IDENTIFIER_NORMALIZATION,
24
+ ...config,
25
+ preserveCaseFor: config?.preserveCaseFor ?? DEFAULT_IDENTIFIER_NORMALIZATION.preserveCaseFor,
26
+ };
27
+ }
@@ -2,3 +2,4 @@ import type { OTPPayload } from "../core/otp.types.js";
2
2
  export declare function buildOtpKey(payload: OTPPayload): string;
3
3
  export declare function buildAttemptsKey(payload: OTPPayload): string;
4
4
  export declare function buildRateLimitKey(payload: OTPPayload): string;
5
+ export declare function buildCooldownKey(payload: OTPPayload): string;
@@ -10,3 +10,6 @@ export function buildAttemptsKey(payload) {
10
10
  export function buildRateLimitKey(payload) {
11
11
  return `rate:${payload.type}:${payload.identifier}`;
12
12
  }
13
+ export function buildCooldownKey(payload) {
14
+ return `cooldown:${normalizeIntent(payload.intent)}:${payload.type}:${payload.identifier}`;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redis-otp-manager",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "Lightweight, Redis-backed OTP manager for Node.js apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,10 @@
19
19
  ".": {
20
20
  "types": "./dist/index.d.ts",
21
21
  "import": "./dist/index.js"
22
+ },
23
+ "./nest": {
24
+ "types": "./dist/integrations/nest/index.d.ts",
25
+ "import": "./dist/integrations/nest/index.js"
22
26
  }
23
27
  },
24
28
  "files": [
@@ -37,6 +41,7 @@
37
41
  "otp",
38
42
  "redis",
39
43
  "nodejs",
44
+ "nestjs",
40
45
  "authentication",
41
46
  "security",
42
47
  "verification",
@@ -49,17 +54,36 @@
49
54
  "access": "public"
50
55
  },
51
56
  "peerDependencies": {
52
- "redis": "^4.0.0 || ^5.0.0"
57
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
58
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
59
+ "redis": "^4.0.0 || ^5.0.0",
60
+ "reflect-metadata": "^0.1.12 || ^0.2.0",
61
+ "rxjs": "^7.0.0"
53
62
  },
54
63
  "peerDependenciesMeta": {
64
+ "@nestjs/common": {
65
+ "optional": true
66
+ },
67
+ "@nestjs/core": {
68
+ "optional": true
69
+ },
55
70
  "redis": {
56
71
  "optional": true
72
+ },
73
+ "reflect-metadata": {
74
+ "optional": true
75
+ },
76
+ "rxjs": {
77
+ "optional": true
57
78
  }
58
79
  },
59
80
  "devDependencies": {
81
+ "@nestjs/common": "^11.1.17",
82
+ "@nestjs/core": "^11.1.17",
60
83
  "@types/node": "^24.0.0",
84
+ "reflect-metadata": "^0.2.2",
85
+ "rxjs": "^7.8.2",
61
86
  "tsx": "^4.19.0",
62
87
  "typescript": "^5.8.0"
63
88
  }
64
- }
65
-
89
+ }