onehitter 2.0.10 → 2.0.11

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/README.md CHANGED
@@ -43,9 +43,9 @@ npm run build
43
43
 
44
44
  2) Configure minimal env
45
45
  ```env
46
- MONGO_CONNECTION=mongodb+srv://...
47
- MONGO_DATABASE=myapp
48
- MONGO_COLLECTION=otps
46
+ OTP_MONGO_CONNECTION=mongodb+srv://...
47
+ OTP_MONGO_DATABASE=myapp
48
+ OTP_MONGO_COLLECTION=otps
49
49
 
50
50
  OTP_MESSAGE_FROM=noreply@example.com
51
51
  OTP_MESSAGE_SUBJECT=Your verification code
@@ -61,7 +61,7 @@ OTP_DIGITS=true
61
61
  const { MongoClient, ServerApiVersion } = require('mongodb')
62
62
  const OneHitter = require('onehitter').default
63
63
 
64
- const client = new MongoClient(process.env.MONGO_CONNECTION, {
64
+ const client = new MongoClient(process.env.OTP_MONGO_CONNECTION, {
65
65
  serverApi: { version: ServerApiVersion.v1, strict: true, deprecationErrors: true },
66
66
  })
67
67
  await client.connect()
@@ -79,7 +79,7 @@ await client.close()
79
79
  - For detailed validation outcomes (expired/not_found/blocked), use `validateStatus()` (see examples/validate-status.md).
80
80
 
81
81
  ## API at a glance
82
- - `make(): string` — generate an OTP according to env flags (`OTP_LENGTH`, `OTP_*`)
82
+ - `make(): string` — generate an OTP according to env flags (`OTP_LENGTH`, `OTP_*`); values greater than 64 are capped at 64 characters
83
83
  - `create(client, { contact, otp, createdAt }): Promise<InsertOneResult>` — MongoDB
84
84
  - `create({ contact, otp, createdAt }): Promise<InsertOneResult>` — SQLite (no client)
85
85
  - `send(to, otp): Promise<void>` — emails via SES; template customizable
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OtpAuthService = void 0;
7
+ const events_1 = require("events");
8
+ const onehitter_1 = __importDefault(require("./onehitter"));
9
+ /**
10
+ * The core OTP service. Extends EventEmitter to provide fan-out capabilities
11
+ * via the 'auth:success' and 'auth:failure' events.
12
+ *
13
+ * This class delegates OTP validation to the OneHitter service. On successful
14
+ * validation it emits a typed AUTH_SUCCESS event, and on failure it emits a
15
+ * typed AUTH_FAILURE event, allowing multiple listeners (logging, sessions,
16
+ * metrics, etc.) to react without coupling them to the underlying
17
+ * authentication logic.
18
+ */
19
+ class OtpAuthService extends events_1.EventEmitter {
20
+ constructor(deps) {
21
+ super();
22
+ this.oneHitter = deps?.oneHitter ?? new onehitter_1.default();
23
+ this.buildPayload =
24
+ deps?.buildPayload ??
25
+ ((userId, extra) => ({
26
+ userId,
27
+ authTime: new Date(),
28
+ ...(extra ?? {}),
29
+ }));
30
+ this.buildFailurePayload =
31
+ deps?.buildFailurePayload ??
32
+ ((userId, reason, extra) => ({
33
+ userId,
34
+ authTime: new Date(),
35
+ reason,
36
+ ...(extra ?? {}),
37
+ }));
38
+ }
39
+ /**
40
+ * Authenticates the user and broadcasts the 'auth:success' event upon success.
41
+ *
42
+ * This method calls into the OneHitter `validate` API, using the provided
43
+ * `userId` as the OTP contact identifier (for example, an email address or
44
+ * phone number). If validation succeeds, a typed payload is emitted via the
45
+ * AUTH_SUCCESS event.
46
+ *
47
+ * @param otp The one-time password provided by the user.
48
+ * @param userId The logical user identifier (often the same as the OTP
49
+ * contact, such as an email address).
50
+ * @param extra Optional bag of extra fields to attach to the emitted payload.
51
+ * @returns A promise resolving to true if authentication succeeded, false otherwise.
52
+ */
53
+ async authenticateUser(otp, userId, extra) {
54
+ // In a real application you would remove or route this through a logger.
55
+ // It is left here as an example of contextual logging around auth attempts.
56
+ // eslint-disable-next-line no-console
57
+ console.log(`Attempting authentication for user: ${userId}`);
58
+ // Delegate to OneHitter for secure OTP validation with status information.
59
+ // We assume that `userId` maps to the OTP contact identifier (e.g., email
60
+ // or phone). If your application treats these differently, you can inject a
61
+ // customized OneHitter instance or wrap this method accordingly.
62
+ const status = await this.oneHitter.validateStatus({ contact: userId, otp });
63
+ if (status === 'ok') {
64
+ // 1. Prepare the payload (base + optional extras) for success
65
+ const payload = this.buildPayload(userId, extra);
66
+ // 2. Broadcast the success event (Fan-Out)
67
+ this.emit(OtpAuthService.AUTH_SUCCESS, payload);
68
+ return true;
69
+ }
70
+ // Map underlying status to a stable, public failure reason
71
+ const reason = status === 'expired' || status === 'not_found' || status === 'blocked'
72
+ ? status
73
+ : 'unknown';
74
+ const failurePayload = this.buildFailurePayload(userId, reason, extra);
75
+ // Broadcast the failure event (Fan-Out)
76
+ this.emit(OtpAuthService.AUTH_FAILURE, failurePayload);
77
+ return false;
78
+ }
79
+ }
80
+ exports.OtpAuthService = OtpAuthService;
81
+ // Use static readonly property for event names to prevent typos
82
+ OtpAuthService.AUTH_SUCCESS = 'auth:success';
83
+ OtpAuthService.AUTH_FAILURE = 'auth:failure';
@@ -107,7 +107,8 @@ class OneHitter {
107
107
  *
108
108
  * This method ensures safe generation by applying the following configuration rules:
109
109
  * 1. **Length Check:** If the global `OTP_LENGTH` is not a positive finite number,
110
- * it defaults the OTP length to 6.
110
+ * it defaults the OTP length to 6. Extremely large values are capped at 64 to
111
+ * avoid excessive memory usage.
111
112
  * 2. **Character Set Guardrail:** It checks which character sets (digits, upper/lower
112
113
  * alphabets, special characters) are enabled via global constants. If *no* set
113
114
  * is enabled, it defaults to including **only digits** to prevent generating
@@ -118,7 +119,8 @@ class OneHitter {
118
119
  * @returns {string} The newly generated OTP string.
119
120
  */
120
121
  make() {
121
- const length = Number.isFinite(config_1.OTP_LENGTH) && config_1.OTP_LENGTH > 0 ? config_1.OTP_LENGTH : 6;
122
+ const rawLength = Number.isFinite(config_1.OTP_LENGTH) && config_1.OTP_LENGTH > 0 ? config_1.OTP_LENGTH : 6;
123
+ const length = Math.min(rawLength, 64);
122
124
  const base = {
123
125
  upperCaseAlphabets: config_1.OTP_LETTERS_UPPER,
124
126
  lowerCaseAlphabets: config_1.OTP_LETTERS_LOWER,
@@ -87,7 +87,10 @@ ${otp}
87
87
 
88
88
  Once used, this one-time password can not be used again. That's why it's called one-time password. This password also expires in ${minutesText}.`;
89
89
  const html = override.html;
90
- const from = override.from ?? config_1.OTP_MESSAGE_FROM;
90
+ // Prefer explicitly configured from, then library-level config default, and
91
+ // finally fall back to the live environment variable. This makes tests and
92
+ // applications resilient when env is loaded after the module is imported.
93
+ const from = override.from ?? config_1.OTP_MESSAGE_FROM ?? process.env.OTP_MESSAGE_FROM;
91
94
  return { subject, text, html, from };
92
95
  }
93
96
  /**
@@ -0,0 +1,76 @@
1
+ import { EventEmitter } from 'events';
2
+ import OneHitter from './onehitter';
3
+ /**
4
+ * The core OTP service. Extends EventEmitter to provide fan-out capabilities
5
+ * via the 'auth:success' and 'auth:failure' events.
6
+ *
7
+ * This class delegates OTP validation to the OneHitter service. On successful
8
+ * validation it emits a typed AUTH_SUCCESS event, and on failure it emits a
9
+ * typed AUTH_FAILURE event, allowing multiple listeners (logging, sessions,
10
+ * metrics, etc.) to react without coupling them to the underlying
11
+ * authentication logic.
12
+ */
13
+ export class OtpAuthService extends EventEmitter {
14
+ constructor(deps) {
15
+ super();
16
+ this.oneHitter = deps?.oneHitter ?? new OneHitter();
17
+ this.buildPayload =
18
+ deps?.buildPayload ??
19
+ ((userId, extra) => ({
20
+ userId,
21
+ authTime: new Date(),
22
+ ...(extra ?? {}),
23
+ }));
24
+ this.buildFailurePayload =
25
+ deps?.buildFailurePayload ??
26
+ ((userId, reason, extra) => ({
27
+ userId,
28
+ authTime: new Date(),
29
+ reason,
30
+ ...(extra ?? {}),
31
+ }));
32
+ }
33
+ /**
34
+ * Authenticates the user and broadcasts the 'auth:success' event upon success.
35
+ *
36
+ * This method calls into the OneHitter `validate` API, using the provided
37
+ * `userId` as the OTP contact identifier (for example, an email address or
38
+ * phone number). If validation succeeds, a typed payload is emitted via the
39
+ * AUTH_SUCCESS event.
40
+ *
41
+ * @param otp The one-time password provided by the user.
42
+ * @param userId The logical user identifier (often the same as the OTP
43
+ * contact, such as an email address).
44
+ * @param extra Optional bag of extra fields to attach to the emitted payload.
45
+ * @returns A promise resolving to true if authentication succeeded, false otherwise.
46
+ */
47
+ async authenticateUser(otp, userId, extra) {
48
+ // In a real application you would remove or route this through a logger.
49
+ // It is left here as an example of contextual logging around auth attempts.
50
+ // eslint-disable-next-line no-console
51
+ console.log(`Attempting authentication for user: ${userId}`);
52
+ // Delegate to OneHitter for secure OTP validation with status information.
53
+ // We assume that `userId` maps to the OTP contact identifier (e.g., email
54
+ // or phone). If your application treats these differently, you can inject a
55
+ // customized OneHitter instance or wrap this method accordingly.
56
+ const status = await this.oneHitter.validateStatus({ contact: userId, otp });
57
+ if (status === 'ok') {
58
+ // 1. Prepare the payload (base + optional extras) for success
59
+ const payload = this.buildPayload(userId, extra);
60
+ // 2. Broadcast the success event (Fan-Out)
61
+ this.emit(OtpAuthService.AUTH_SUCCESS, payload);
62
+ return true;
63
+ }
64
+ // Map underlying status to a stable, public failure reason
65
+ const reason = status === 'expired' || status === 'not_found' || status === 'blocked'
66
+ ? status
67
+ : 'unknown';
68
+ const failurePayload = this.buildFailurePayload(userId, reason, extra);
69
+ // Broadcast the failure event (Fan-Out)
70
+ this.emit(OtpAuthService.AUTH_FAILURE, failurePayload);
71
+ return false;
72
+ }
73
+ }
74
+ // Use static readonly property for event names to prevent typos
75
+ OtpAuthService.AUTH_SUCCESS = 'auth:success';
76
+ OtpAuthService.AUTH_FAILURE = 'auth:failure';
@@ -102,7 +102,8 @@ class OneHitter {
102
102
  *
103
103
  * This method ensures safe generation by applying the following configuration rules:
104
104
  * 1. **Length Check:** If the global `OTP_LENGTH` is not a positive finite number,
105
- * it defaults the OTP length to 6.
105
+ * it defaults the OTP length to 6. Extremely large values are capped at 64 to
106
+ * avoid excessive memory usage.
106
107
  * 2. **Character Set Guardrail:** It checks which character sets (digits, upper/lower
107
108
  * alphabets, special characters) are enabled via global constants. If *no* set
108
109
  * is enabled, it defaults to including **only digits** to prevent generating
@@ -113,7 +114,8 @@ class OneHitter {
113
114
  * @returns {string} The newly generated OTP string.
114
115
  */
115
116
  make() {
116
- const length = Number.isFinite(OTP_LENGTH) && OTP_LENGTH > 0 ? OTP_LENGTH : 6;
117
+ const rawLength = Number.isFinite(OTP_LENGTH) && OTP_LENGTH > 0 ? OTP_LENGTH : 6;
118
+ const length = Math.min(rawLength, 64);
117
119
  const base = {
118
120
  upperCaseAlphabets: OTP_LETTERS_UPPER,
119
121
  lowerCaseAlphabets: OTP_LETTERS_LOWER,
@@ -81,7 +81,10 @@ ${otp}
81
81
 
82
82
  Once used, this one-time password can not be used again. That's why it's called one-time password. This password also expires in ${minutesText}.`;
83
83
  const html = override.html;
84
- const from = override.from ?? OTP_MESSAGE_FROM;
84
+ // Prefer explicitly configured from, then library-level config default, and
85
+ // finally fall back to the live environment variable. This makes tests and
86
+ // applications resilient when env is loaded after the module is imported.
87
+ const from = override.from ?? OTP_MESSAGE_FROM ?? process.env.OTP_MESSAGE_FROM;
85
88
  return { subject, text, html, from };
86
89
  }
87
90
  /**
@@ -0,0 +1,66 @@
1
+ import { EventEmitter } from 'events';
2
+ import OneHitter from './onehitter';
3
+ export interface AuthSuccessPayload {
4
+ userId: string;
5
+ authTime: Date;
6
+ }
7
+ export type AuthExtra = Record<string, unknown>;
8
+ export type AuthSuccessExtra = AuthExtra;
9
+ export type AuthFailureExtra = AuthExtra;
10
+ export type AuthSuccessEventPayload = AuthSuccessPayload & AuthSuccessExtra;
11
+ export type AuthFailureReason = 'not_found' | 'expired' | 'blocked' | 'unknown';
12
+ export interface AuthFailurePayload {
13
+ userId: string;
14
+ authTime: Date;
15
+ reason: AuthFailureReason;
16
+ }
17
+ export type AuthFailureEventPayload = AuthFailurePayload & AuthFailureExtra;
18
+ /**
19
+ * Optional dependencies for OtpAuthService.
20
+ *
21
+ * You can inject an existing OneHitter instance (for example, one that is
22
+ * already configured with a custom rate limiter or database driver). If not
23
+ * provided, a default OneHitter instance is created internally.
24
+ *
25
+ * You can also override the `buildPayload` function to control the exact
26
+ * structure of the emitted success event payload (including additional fields),
27
+ * and `buildFailurePayload` to control the structure of failure payloads.
28
+ */
29
+ export interface OtpAuthServiceDeps {
30
+ oneHitter?: OneHitter;
31
+ buildPayload?: (userId: string, extra?: AuthSuccessExtra) => AuthSuccessEventPayload;
32
+ buildFailurePayload?: (userId: string, reason: AuthFailureReason, extra?: AuthFailureExtra) => AuthFailureEventPayload;
33
+ }
34
+ /**
35
+ * The core OTP service. Extends EventEmitter to provide fan-out capabilities
36
+ * via the 'auth:success' and 'auth:failure' events.
37
+ *
38
+ * This class delegates OTP validation to the OneHitter service. On successful
39
+ * validation it emits a typed AUTH_SUCCESS event, and on failure it emits a
40
+ * typed AUTH_FAILURE event, allowing multiple listeners (logging, sessions,
41
+ * metrics, etc.) to react without coupling them to the underlying
42
+ * authentication logic.
43
+ */
44
+ export declare class OtpAuthService extends EventEmitter {
45
+ static readonly AUTH_SUCCESS = "auth:success";
46
+ static readonly AUTH_FAILURE = "auth:failure";
47
+ private readonly oneHitter;
48
+ private readonly buildPayload;
49
+ private readonly buildFailurePayload;
50
+ constructor(deps?: OtpAuthServiceDeps);
51
+ /**
52
+ * Authenticates the user and broadcasts the 'auth:success' event upon success.
53
+ *
54
+ * This method calls into the OneHitter `validate` API, using the provided
55
+ * `userId` as the OTP contact identifier (for example, an email address or
56
+ * phone number). If validation succeeds, a typed payload is emitted via the
57
+ * AUTH_SUCCESS event.
58
+ *
59
+ * @param otp The one-time password provided by the user.
60
+ * @param userId The logical user identifier (often the same as the OTP
61
+ * contact, such as an email address).
62
+ * @param extra Optional bag of extra fields to attach to the emitted payload.
63
+ * @returns A promise resolving to true if authentication succeeded, false otherwise.
64
+ */
65
+ authenticateUser(otp: string, userId: string, extra?: AuthSuccessExtra): Promise<boolean>;
66
+ }
@@ -148,7 +148,8 @@ declare class OneHitter {
148
148
  *
149
149
  * This method ensures safe generation by applying the following configuration rules:
150
150
  * 1. **Length Check:** If the global `OTP_LENGTH` is not a positive finite number,
151
- * it defaults the OTP length to 6.
151
+ * it defaults the OTP length to 6. Extremely large values are capped at 64 to
152
+ * avoid excessive memory usage.
152
153
  * 2. **Character Set Guardrail:** It checks which character sets (digits, upper/lower
153
154
  * alphabets, special characters) are enabled via global constants. If *no* set
154
155
  * is enabled, it defaults to including **only digits** to prevent generating
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onehitter",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "One-time password user validation package.",
5
5
  "main": "dist/cjs/onehitter.js",
6
6
  "module": "dist/esm/onehitter.js",