@flink-app/otp-auth-plugin 0.12.1-alpha.40
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 +21 -0
- package/README.md +879 -0
- package/dist/OtpAuthPlugin.d.ts +64 -0
- package/dist/OtpAuthPlugin.js +231 -0
- package/dist/OtpAuthPluginContext.d.ts +10 -0
- package/dist/OtpAuthPluginContext.js +2 -0
- package/dist/OtpAuthPluginOptions.d.ts +112 -0
- package/dist/OtpAuthPluginOptions.js +2 -0
- package/dist/OtpInternalContext.d.ts +11 -0
- package/dist/OtpInternalContext.js +2 -0
- package/dist/functions/initiate.d.ts +18 -0
- package/dist/functions/initiate.js +104 -0
- package/dist/functions/verify.d.ts +20 -0
- package/dist/functions/verify.js +142 -0
- package/dist/handlers/PostOtpInitiate.d.ts +7 -0
- package/dist/handlers/PostOtpInitiate.js +70 -0
- package/dist/handlers/PostOtpVerify.d.ts +7 -0
- package/dist/handlers/PostOtpVerify.js +86 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +24 -0
- package/dist/repos/OtpSessionRepo.d.ts +13 -0
- package/dist/repos/OtpSessionRepo.js +145 -0
- package/dist/schemas/InitiateRequest.d.ts +8 -0
- package/dist/schemas/InitiateRequest.js +2 -0
- package/dist/schemas/InitiateResponse.d.ts +8 -0
- package/dist/schemas/InitiateResponse.js +2 -0
- package/dist/schemas/OtpSession.d.ts +25 -0
- package/dist/schemas/OtpSession.js +2 -0
- package/dist/schemas/VerifyRequest.d.ts +6 -0
- package/dist/schemas/VerifyRequest.js +2 -0
- package/dist/schemas/VerifyResponse.d.ts +12 -0
- package/dist/schemas/VerifyResponse.js +2 -0
- package/dist/utils/otp-utils.d.ts +43 -0
- package/dist/utils/otp-utils.js +95 -0
- package/examples/basic-usage.ts +145 -0
- package/package.json +37 -0
- package/spec/OtpAuthPlugin.spec.ts +159 -0
- package/spec/OtpSessionRepo.spec.ts +194 -0
- package/spec/otp-utils.spec.ts +172 -0
- package/spec/support/jasmine.json +7 -0
- package/src/OtpAuthPlugin.ts +163 -0
- package/src/OtpAuthPluginContext.ts +11 -0
- package/src/OtpAuthPluginOptions.ts +135 -0
- package/src/OtpInternalContext.ts +12 -0
- package/src/functions/initiate.ts +86 -0
- package/src/functions/verify.ts +123 -0
- package/src/handlers/PostOtpInitiate.ts +28 -0
- package/src/handlers/PostOtpVerify.ts +42 -0
- package/src/index.ts +17 -0
- package/src/repos/OtpSessionRepo.ts +47 -0
- package/src/schemas/InitiateRequest.ts +8 -0
- package/src/schemas/InitiateResponse.ts +8 -0
- package/src/schemas/OtpSession.ts +25 -0
- package/src/schemas/VerifyRequest.ts +6 -0
- package/src/schemas/VerifyResponse.ts +12 -0
- package/src/utils/otp-utils.ts +89 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FlinkPlugin } from "@flink-app/flink";
|
|
2
|
+
import { OtpAuthPluginOptions } from "./OtpAuthPluginOptions";
|
|
3
|
+
/**
|
|
4
|
+
* OTP Auth Plugin Factory Function
|
|
5
|
+
*
|
|
6
|
+
* Creates a Flink plugin for OTP (One-Time Password) authentication via SMS or email.
|
|
7
|
+
* Integrates with JWT Auth Plugin for token generation.
|
|
8
|
+
*
|
|
9
|
+
* @param options - OTP auth plugin configuration options
|
|
10
|
+
* @returns FlinkPlugin instance
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { jwtAuthPlugin } from '@flink-app/jwt-auth-plugin';
|
|
15
|
+
* import { otpAuthPlugin } from '@flink-app/otp-auth-plugin';
|
|
16
|
+
*
|
|
17
|
+
* const app = new FlinkApp({
|
|
18
|
+
* auth: jwtAuthPlugin({
|
|
19
|
+
* secret: process.env.JWT_SECRET!,
|
|
20
|
+
* getUser: async (tokenData) => {
|
|
21
|
+
* return ctx.repos.userRepo.getById(tokenData.userId);
|
|
22
|
+
* },
|
|
23
|
+
* rolePermissions: {
|
|
24
|
+
* user: ['read', 'write']
|
|
25
|
+
* }
|
|
26
|
+
* }),
|
|
27
|
+
*
|
|
28
|
+
* plugins: [
|
|
29
|
+
* otpAuthPlugin({
|
|
30
|
+
* codeLength: 6,
|
|
31
|
+
* codeTTL: 300, // 5 minutes
|
|
32
|
+
* maxAttempts: 3,
|
|
33
|
+
* onSendCode: async (code, identifier, method) => {
|
|
34
|
+
* if (method === 'sms') {
|
|
35
|
+
* await ctx.plugins.sms.send(identifier, `Your code: ${code}`);
|
|
36
|
+
* } else {
|
|
37
|
+
* await ctx.plugins.email.send({
|
|
38
|
+
* to: identifier,
|
|
39
|
+
* subject: 'Your verification code',
|
|
40
|
+
* text: `Your code: ${code}`
|
|
41
|
+
* });
|
|
42
|
+
* }
|
|
43
|
+
* return true;
|
|
44
|
+
* },
|
|
45
|
+
* onGetUser: async (identifier, method) => {
|
|
46
|
+
* if (method === 'sms') {
|
|
47
|
+
* return await ctx.repos.userRepo.findOne({ phoneNumber: identifier });
|
|
48
|
+
* } else {
|
|
49
|
+
* return await ctx.repos.userRepo.findOne({ email: identifier });
|
|
50
|
+
* }
|
|
51
|
+
* },
|
|
52
|
+
* onVerifySuccess: async (user, identifier, method) => {
|
|
53
|
+
* const token = await ctx.auth.createToken(
|
|
54
|
+
* { userId: user._id, email: user.email },
|
|
55
|
+
* user.roles
|
|
56
|
+
* );
|
|
57
|
+
* return { user, token };
|
|
58
|
+
* }
|
|
59
|
+
* })
|
|
60
|
+
* ]
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function otpAuthPlugin(options: OtpAuthPluginOptions): FlinkPlugin;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
37
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
38
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
39
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
40
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
41
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
42
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
46
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
47
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
48
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
49
|
+
function step(op) {
|
|
50
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
51
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
52
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
53
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
54
|
+
switch (op[0]) {
|
|
55
|
+
case 0: case 1: t = op; break;
|
|
56
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
57
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
58
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
59
|
+
default:
|
|
60
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
61
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
62
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
63
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
64
|
+
if (t[2]) _.ops.pop();
|
|
65
|
+
_.trys.pop(); continue;
|
|
66
|
+
}
|
|
67
|
+
op = body.call(thisArg, _);
|
|
68
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
69
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
73
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
74
|
+
};
|
|
75
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76
|
+
exports.otpAuthPlugin = void 0;
|
|
77
|
+
var flink_1 = require("@flink-app/flink");
|
|
78
|
+
var initiate_1 = require("./functions/initiate");
|
|
79
|
+
var verify_1 = require("./functions/verify");
|
|
80
|
+
var PostOtpInitiate = __importStar(require("./handlers/PostOtpInitiate"));
|
|
81
|
+
var PostOtpVerify = __importStar(require("./handlers/PostOtpVerify"));
|
|
82
|
+
var OtpSessionRepo_1 = __importDefault(require("./repos/OtpSessionRepo"));
|
|
83
|
+
/**
|
|
84
|
+
* OTP Auth Plugin Factory Function
|
|
85
|
+
*
|
|
86
|
+
* Creates a Flink plugin for OTP (One-Time Password) authentication via SMS or email.
|
|
87
|
+
* Integrates with JWT Auth Plugin for token generation.
|
|
88
|
+
*
|
|
89
|
+
* @param options - OTP auth plugin configuration options
|
|
90
|
+
* @returns FlinkPlugin instance
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { jwtAuthPlugin } from '@flink-app/jwt-auth-plugin';
|
|
95
|
+
* import { otpAuthPlugin } from '@flink-app/otp-auth-plugin';
|
|
96
|
+
*
|
|
97
|
+
* const app = new FlinkApp({
|
|
98
|
+
* auth: jwtAuthPlugin({
|
|
99
|
+
* secret: process.env.JWT_SECRET!,
|
|
100
|
+
* getUser: async (tokenData) => {
|
|
101
|
+
* return ctx.repos.userRepo.getById(tokenData.userId);
|
|
102
|
+
* },
|
|
103
|
+
* rolePermissions: {
|
|
104
|
+
* user: ['read', 'write']
|
|
105
|
+
* }
|
|
106
|
+
* }),
|
|
107
|
+
*
|
|
108
|
+
* plugins: [
|
|
109
|
+
* otpAuthPlugin({
|
|
110
|
+
* codeLength: 6,
|
|
111
|
+
* codeTTL: 300, // 5 minutes
|
|
112
|
+
* maxAttempts: 3,
|
|
113
|
+
* onSendCode: async (code, identifier, method) => {
|
|
114
|
+
* if (method === 'sms') {
|
|
115
|
+
* await ctx.plugins.sms.send(identifier, `Your code: ${code}`);
|
|
116
|
+
* } else {
|
|
117
|
+
* await ctx.plugins.email.send({
|
|
118
|
+
* to: identifier,
|
|
119
|
+
* subject: 'Your verification code',
|
|
120
|
+
* text: `Your code: ${code}`
|
|
121
|
+
* });
|
|
122
|
+
* }
|
|
123
|
+
* return true;
|
|
124
|
+
* },
|
|
125
|
+
* onGetUser: async (identifier, method) => {
|
|
126
|
+
* if (method === 'sms') {
|
|
127
|
+
* return await ctx.repos.userRepo.findOne({ phoneNumber: identifier });
|
|
128
|
+
* } else {
|
|
129
|
+
* return await ctx.repos.userRepo.findOne({ email: identifier });
|
|
130
|
+
* }
|
|
131
|
+
* },
|
|
132
|
+
* onVerifySuccess: async (user, identifier, method) => {
|
|
133
|
+
* const token = await ctx.auth.createToken(
|
|
134
|
+
* { userId: user._id, email: user.email },
|
|
135
|
+
* user.roles
|
|
136
|
+
* );
|
|
137
|
+
* return { user, token };
|
|
138
|
+
* }
|
|
139
|
+
* })
|
|
140
|
+
* ]
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
function otpAuthPlugin(options) {
|
|
145
|
+
// Validation
|
|
146
|
+
if (!options.onSendCode) {
|
|
147
|
+
throw new Error("OTP Auth Plugin: onSendCode callback is required");
|
|
148
|
+
}
|
|
149
|
+
if (!options.onGetUser) {
|
|
150
|
+
throw new Error("OTP Auth Plugin: onGetUser callback is required");
|
|
151
|
+
}
|
|
152
|
+
if (!options.onVerifySuccess) {
|
|
153
|
+
throw new Error("OTP Auth Plugin: onVerifySuccess callback is required");
|
|
154
|
+
}
|
|
155
|
+
// Validate code length
|
|
156
|
+
var codeLength = options.codeLength || 6;
|
|
157
|
+
if (codeLength < 4 || codeLength > 8) {
|
|
158
|
+
throw new Error("OTP Auth Plugin: codeLength must be between 4 and 8");
|
|
159
|
+
}
|
|
160
|
+
// Validate code TTL
|
|
161
|
+
var codeTTL = options.codeTTL || 300;
|
|
162
|
+
if (codeTTL < 30 || codeTTL > 3600) {
|
|
163
|
+
flink_1.log.warn("OTP Auth Plugin: codeTTL should be between 30 and 3600 seconds for security");
|
|
164
|
+
}
|
|
165
|
+
// Validate max attempts
|
|
166
|
+
var maxAttempts = options.maxAttempts || 3;
|
|
167
|
+
if (maxAttempts < 1 || maxAttempts > 10) {
|
|
168
|
+
throw new Error("OTP Auth Plugin: maxAttempts must be between 1 and 10");
|
|
169
|
+
}
|
|
170
|
+
var flinkApp;
|
|
171
|
+
var sessionRepo;
|
|
172
|
+
/**
|
|
173
|
+
* Plugin initialization
|
|
174
|
+
*/
|
|
175
|
+
function init(app, db) {
|
|
176
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
177
|
+
var collectionName, keepSessionsSec, error_1;
|
|
178
|
+
return __generator(this, function (_a) {
|
|
179
|
+
switch (_a.label) {
|
|
180
|
+
case 0:
|
|
181
|
+
flink_1.log.info("Initializing OTP Auth Plugin...");
|
|
182
|
+
flinkApp = app;
|
|
183
|
+
_a.label = 1;
|
|
184
|
+
case 1:
|
|
185
|
+
_a.trys.push([1, 3, , 4]);
|
|
186
|
+
if (!db) {
|
|
187
|
+
throw new Error("OTP Auth Plugin: Database connection is required");
|
|
188
|
+
}
|
|
189
|
+
collectionName = options.otpSessionsCollectionName || "otp_sessions";
|
|
190
|
+
sessionRepo = new OtpSessionRepo_1.default(collectionName, db);
|
|
191
|
+
flinkApp.addRepo("otpSessionRepo", sessionRepo);
|
|
192
|
+
keepSessionsSec = options.keepSessionsSec || 86400;
|
|
193
|
+
return [4 /*yield*/, sessionRepo.ensureExpiringIndex(keepSessionsSec)];
|
|
194
|
+
case 2:
|
|
195
|
+
_a.sent();
|
|
196
|
+
// Register OTP handlers
|
|
197
|
+
// Only register handlers if registerRoutes is enabled (default: true)
|
|
198
|
+
if (options.registerRoutes !== false) {
|
|
199
|
+
flinkApp.addHandler(PostOtpInitiate);
|
|
200
|
+
flinkApp.addHandler(PostOtpVerify);
|
|
201
|
+
flink_1.log.info("OTP Auth Plugin: Registered HTTP endpoints");
|
|
202
|
+
}
|
|
203
|
+
flink_1.log.info("OTP Auth Plugin initialized (codeLength: ".concat(codeLength, ", codeTTL: ").concat(codeTTL, "s, maxAttempts: ").concat(maxAttempts, ")"));
|
|
204
|
+
return [3 /*break*/, 4];
|
|
205
|
+
case 3:
|
|
206
|
+
error_1 = _a.sent();
|
|
207
|
+
flink_1.log.error("Failed to initialize OTP Auth Plugin:", error_1);
|
|
208
|
+
throw error_1;
|
|
209
|
+
case 4: return [2 /*return*/];
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Plugin context exposed via ctx.plugins.otpAuth
|
|
216
|
+
*/
|
|
217
|
+
var pluginCtx = {
|
|
218
|
+
options: Object.freeze(__assign({}, options)),
|
|
219
|
+
initiate: function (initiateOptions) { return (0, initiate_1.initiate)(flinkApp.ctx, initiateOptions); },
|
|
220
|
+
verify: function (verifyOptions) { return (0, verify_1.verify)(flinkApp.ctx, verifyOptions); },
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
id: "otpAuth",
|
|
224
|
+
db: {
|
|
225
|
+
useHostDb: true,
|
|
226
|
+
},
|
|
227
|
+
ctx: pluginCtx,
|
|
228
|
+
init: init,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
exports.otpAuthPlugin = otpAuthPlugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OtpAuthPluginOptions } from "./OtpAuthPluginOptions";
|
|
2
|
+
import { InitiateOptions, InitiateResponse } from "./functions/initiate";
|
|
3
|
+
import { VerifyOptions, VerifyResponse } from "./functions/verify";
|
|
4
|
+
export interface OtpAuthPluginContext {
|
|
5
|
+
otpAuth: {
|
|
6
|
+
options: OtpAuthPluginOptions;
|
|
7
|
+
initiate: (options: InitiateOptions) => Promise<InitiateResponse>;
|
|
8
|
+
verify: (options: VerifyOptions) => Promise<VerifyResponse>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export interface AuthSuccessCallbackResponse {
|
|
2
|
+
user: any;
|
|
3
|
+
token: string;
|
|
4
|
+
}
|
|
5
|
+
export interface OtpAuthPluginOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Number of digits in the OTP code (4-8).
|
|
8
|
+
* Default is 6.
|
|
9
|
+
*/
|
|
10
|
+
codeLength?: number;
|
|
11
|
+
/**
|
|
12
|
+
* How long the OTP code is valid in seconds.
|
|
13
|
+
* Default is 300 (5 minutes).
|
|
14
|
+
*/
|
|
15
|
+
codeTTL?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of verification attempts before session is locked.
|
|
18
|
+
* Default is 3.
|
|
19
|
+
*/
|
|
20
|
+
maxAttempts?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Callback to send the OTP code via SMS or email.
|
|
23
|
+
* Should return true if sending succeeded, false otherwise.
|
|
24
|
+
*
|
|
25
|
+
* @param code - The OTP code to send
|
|
26
|
+
* @param identifier - The user identifier (phone number or email)
|
|
27
|
+
* @param method - The delivery method ('sms' or 'email')
|
|
28
|
+
* @param payload - Optional custom payload from initiation
|
|
29
|
+
* @returns Promise that resolves to true if sent successfully
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* onSendCode: async (code, identifier, method, payload) => {
|
|
34
|
+
* if (method === 'sms') {
|
|
35
|
+
* await ctx.plugins.sms.send(identifier, `Your code is: ${code}`);
|
|
36
|
+
* } else {
|
|
37
|
+
* await ctx.plugins.email.send({
|
|
38
|
+
* to: identifier,
|
|
39
|
+
* subject: 'Your verification code',
|
|
40
|
+
* text: `Your code is: ${code}`
|
|
41
|
+
* });
|
|
42
|
+
* }
|
|
43
|
+
* return true;
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
onSendCode: (code: string, identifier: string, method: "sms" | "email", payload?: Record<string, any>) => Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Callback to retrieve user by identifier (phone number or email).
|
|
50
|
+
* Return null if user is not found.
|
|
51
|
+
*
|
|
52
|
+
* @param identifier - The user identifier (phone number or email)
|
|
53
|
+
* @param method - The delivery method ('sms' or 'email')
|
|
54
|
+
* @param payload - Optional custom payload from initiation
|
|
55
|
+
* @returns Promise that resolves to user object or null
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* onGetUser: async (identifier, method) => {
|
|
60
|
+
* if (method === 'sms') {
|
|
61
|
+
* return await ctx.repos.userRepo.findOne({ phoneNumber: identifier });
|
|
62
|
+
* } else {
|
|
63
|
+
* return await ctx.repos.userRepo.findOne({ email: identifier });
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
onGetUser: (identifier: string, method: "sms" | "email", payload?: Record<string, any>) => Promise<any | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Callback invoked when OTP verification is successful.
|
|
71
|
+
* Must return user object and JWT token.
|
|
72
|
+
*
|
|
73
|
+
* @param user - The user object returned from onGetUser
|
|
74
|
+
* @param identifier - The user identifier (phone number or email)
|
|
75
|
+
* @param method - The delivery method ('sms' or 'email')
|
|
76
|
+
* @param payload - Optional custom payload from initiation
|
|
77
|
+
* @returns Promise that resolves to user and token
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* onVerifySuccess: async (user, identifier, method, payload) => {
|
|
82
|
+
* const token = await ctx.auth.createToken(
|
|
83
|
+
* { userId: user._id, email: user.email },
|
|
84
|
+
* user.roles
|
|
85
|
+
* );
|
|
86
|
+
* return { user, token };
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
onVerifySuccess: (user: any, identifier: string, method: "sms" | "email", payload?: Record<string, any>) => Promise<AuthSuccessCallbackResponse>;
|
|
91
|
+
/**
|
|
92
|
+
* For how long to keep sessions in database (in seconds).
|
|
93
|
+
* This is for data retention purposes only.
|
|
94
|
+
*
|
|
95
|
+
* An expiring index will be created in the database to automatically
|
|
96
|
+
* remove old sessions based on this.
|
|
97
|
+
*
|
|
98
|
+
* Default is 86400 (24 hours).
|
|
99
|
+
*/
|
|
100
|
+
keepSessionsSec?: number;
|
|
101
|
+
/**
|
|
102
|
+
* The name of the MongoDB collection to use for storing OTP sessions.
|
|
103
|
+
* Default is "otp_sessions".
|
|
104
|
+
*/
|
|
105
|
+
otpSessionsCollectionName?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Whether to register the default HTTP routes for OTP operations.
|
|
108
|
+
* If false, you'll need to implement your own handlers using the functions.
|
|
109
|
+
* Default is true.
|
|
110
|
+
*/
|
|
111
|
+
registerRoutes?: boolean;
|
|
112
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FlinkContext } from "@flink-app/flink";
|
|
2
|
+
import { OtpAuthPluginContext } from "./OtpAuthPluginContext";
|
|
3
|
+
import OtpSessionRepo from "./repos/OtpSessionRepo";
|
|
4
|
+
export interface OtpInternalContext extends FlinkContext {
|
|
5
|
+
plugins: {
|
|
6
|
+
otpAuth: OtpAuthPluginContext["otpAuth"];
|
|
7
|
+
};
|
|
8
|
+
repos: {
|
|
9
|
+
otpSessionRepo: OtpSessionRepo;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OtpInternalContext } from "../OtpInternalContext";
|
|
2
|
+
export interface InitiateOptions {
|
|
3
|
+
/** User identifier (phone number or email) */
|
|
4
|
+
identifier: string;
|
|
5
|
+
/** Delivery method for the OTP code */
|
|
6
|
+
method: "sms" | "email";
|
|
7
|
+
/** Optional custom payload to attach to the session */
|
|
8
|
+
payload?: Record<string, any>;
|
|
9
|
+
}
|
|
10
|
+
export interface InitiateResponse {
|
|
11
|
+
/** Unique session ID for this OTP session */
|
|
12
|
+
sessionId: string;
|
|
13
|
+
/** When the code expires */
|
|
14
|
+
expiresAt: Date;
|
|
15
|
+
/** Time-to-live in seconds */
|
|
16
|
+
ttl: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function initiate(ctx: OtpInternalContext, options: InitiateOptions): Promise<InitiateResponse>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.initiate = void 0;
|
|
40
|
+
var flink_1 = require("@flink-app/flink");
|
|
41
|
+
var otp_utils_1 = require("../utils/otp-utils");
|
|
42
|
+
function initiate(ctx, options) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
44
|
+
var identifier, method, payload, pluginOptions, normalizedIdentifier, codeLength, code, sessionId, codeTTL, expiresAt, session, sent, error_1;
|
|
45
|
+
return __generator(this, function (_a) {
|
|
46
|
+
switch (_a.label) {
|
|
47
|
+
case 0:
|
|
48
|
+
identifier = options.identifier, method = options.method, payload = options.payload;
|
|
49
|
+
pluginOptions = ctx.plugins.otpAuth.options;
|
|
50
|
+
// Validate identifier format
|
|
51
|
+
if (!(0, otp_utils_1.validateIdentifier)(identifier, method)) {
|
|
52
|
+
flink_1.log.warn("Invalid identifier format for method ".concat(method, ": ").concat(identifier));
|
|
53
|
+
throw (0, flink_1.badRequest)("Invalid ".concat(method === "email" ? "email address" : "phone number", " format"));
|
|
54
|
+
}
|
|
55
|
+
normalizedIdentifier = (0, otp_utils_1.normalizeIdentifier)(identifier, method);
|
|
56
|
+
codeLength = pluginOptions.codeLength || 6;
|
|
57
|
+
if (codeLength < 4 || codeLength > 8) {
|
|
58
|
+
throw new Error("OTP code length must be between 4 and 8 digits");
|
|
59
|
+
}
|
|
60
|
+
code = (0, otp_utils_1.generateOtpCode)(codeLength);
|
|
61
|
+
sessionId = (0, otp_utils_1.generateSessionId)();
|
|
62
|
+
codeTTL = pluginOptions.codeTTL || 300;
|
|
63
|
+
expiresAt = new Date(Date.now() + codeTTL * 1000);
|
|
64
|
+
session = {
|
|
65
|
+
sessionId: sessionId,
|
|
66
|
+
identifier: normalizedIdentifier,
|
|
67
|
+
method: method,
|
|
68
|
+
code: code,
|
|
69
|
+
attempts: 0,
|
|
70
|
+
maxAttempts: pluginOptions.maxAttempts || 3,
|
|
71
|
+
status: "pending",
|
|
72
|
+
createdAt: new Date(),
|
|
73
|
+
expiresAt: expiresAt,
|
|
74
|
+
payload: payload,
|
|
75
|
+
};
|
|
76
|
+
return [4 /*yield*/, ctx.repos.otpSessionRepo.createSession(session)];
|
|
77
|
+
case 1:
|
|
78
|
+
_a.sent();
|
|
79
|
+
_a.label = 2;
|
|
80
|
+
case 2:
|
|
81
|
+
_a.trys.push([2, 4, , 5]);
|
|
82
|
+
return [4 /*yield*/, pluginOptions.onSendCode(code, normalizedIdentifier, method, payload)];
|
|
83
|
+
case 3:
|
|
84
|
+
sent = _a.sent();
|
|
85
|
+
if (!sent) {
|
|
86
|
+
flink_1.log.error("Failed to send OTP code to ".concat(normalizedIdentifier, " via ").concat(method));
|
|
87
|
+
throw new Error("Failed to send verification code");
|
|
88
|
+
}
|
|
89
|
+
flink_1.log.info("OTP code sent to ".concat(normalizedIdentifier, " via ").concat(method, " (session: ").concat(sessionId, ")"));
|
|
90
|
+
return [3 /*break*/, 5];
|
|
91
|
+
case 4:
|
|
92
|
+
error_1 = _a.sent();
|
|
93
|
+
flink_1.log.error("Error sending OTP code: ".concat(error_1));
|
|
94
|
+
throw new Error("Failed to send verification code");
|
|
95
|
+
case 5: return [2 /*return*/, {
|
|
96
|
+
sessionId: sessionId,
|
|
97
|
+
expiresAt: expiresAt,
|
|
98
|
+
ttl: codeTTL,
|
|
99
|
+
}];
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
exports.initiate = initiate;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OtpInternalContext } from "../OtpInternalContext";
|
|
2
|
+
export interface VerifyOptions {
|
|
3
|
+
/** The session ID from the initiate response */
|
|
4
|
+
sessionId: string;
|
|
5
|
+
/** The OTP code entered by the user */
|
|
6
|
+
code: string;
|
|
7
|
+
}
|
|
8
|
+
export interface VerifyResponse {
|
|
9
|
+
/** Verification status */
|
|
10
|
+
status: "success" | "invalid_code" | "expired" | "locked" | "not_found";
|
|
11
|
+
/** JWT token if successful */
|
|
12
|
+
token?: string;
|
|
13
|
+
/** User object if successful */
|
|
14
|
+
user?: any;
|
|
15
|
+
/** Remaining attempts if code was invalid */
|
|
16
|
+
remainingAttempts?: number;
|
|
17
|
+
/** Error message */
|
|
18
|
+
message?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function verify(ctx: OtpInternalContext, options: VerifyOptions): Promise<VerifyResponse>;
|