onehitter 2.0.10 → 2.0.12
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 +5 -5
- package/dist/cjs/auth-otp-service.js +83 -0
- package/dist/cjs/onehitter.js +4 -2
- package/dist/cjs/sender.js +4 -1
- package/dist/esm/auth-otp-service.js +76 -0
- package/dist/esm/onehitter.js +4 -2
- package/dist/esm/sender.js +4 -1
- package/dist/types/auth-otp-service.d.ts +66 -0
- package/dist/types/onehitter.d.ts +2 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -43,9 +43,9 @@ npm run build
|
|
|
43
43
|
|
|
44
44
|
2) Configure minimal env
|
|
45
45
|
```env
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
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';
|
package/dist/cjs/onehitter.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/cjs/sender.js
CHANGED
|
@@ -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
|
-
|
|
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';
|
package/dist/esm/onehitter.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/esm/sender.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "2.0.12",
|
|
4
4
|
"description": "One-time password user validation package.",
|
|
5
5
|
"main": "dist/cjs/onehitter.js",
|
|
6
6
|
"module": "dist/esm/onehitter.js",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"author": "Keith Vance <discgolfer@uniflydiscs.com>",
|
|
79
79
|
"license": "MIT",
|
|
80
80
|
"engines": {
|
|
81
|
-
"node": "^18 || ^20"
|
|
81
|
+
"node": "^18 || ^20 || ^22"
|
|
82
82
|
},
|
|
83
83
|
"files": [
|
|
84
84
|
"dist/esm",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@aws-sdk/client-sesv2": "^3.716.0",
|
|
93
93
|
"dotenv-safe": "^9.1.0",
|
|
94
94
|
"mongodb": "^7.0.0",
|
|
95
|
-
"nodemailer": "^7.0.
|
|
95
|
+
"nodemailer": "^7.0.11",
|
|
96
96
|
"otp-generator": "^4.0.1"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|