@autional/plugin-mfa 0.2.0

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 ADDED
@@ -0,0 +1,43 @@
1
+ # @authms/plugin-mfa
2
+
3
+ AuthMS MFA Plugin — Multi-Factor Authentication UI components and flows for `@authms/react`.
4
+
5
+ ## Components
6
+
7
+ - **`MfaSetup`** — guided TOTP setup with QR code display and verification
8
+ - **`MfaGuard`** — wrapper that requires MFA challenge before rendering children
9
+ - **`BackupCodes`** — display and regenerate one-time backup codes
10
+ - **`mfaPlugin`** — plugin factory to register with `AuthMS.use()`
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @authms/core @authms/react @authms/plugin-mfa
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```tsx
21
+ import { AuthmsProvider, useAuthms } from '@authms/react';
22
+ import { MfaSetup, MfaGuard, BackupCodes } from '@authms/plugin-mfa';
23
+
24
+ function SecurityPage() {
25
+ return (
26
+ <div>
27
+ <h2>Two-Factor Authentication</h2>
28
+ <MfaSetup />
29
+ <BackupCodes />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function Dashboard() {
35
+ return (
36
+ <MfaGuard>
37
+ <h1>Sensitive Content</h1>
38
+ </MfaGuard>
39
+ );
40
+ }
41
+ ```
42
+
43
+ See the [root SDK README](../../README.md) for full documentation.
@@ -0,0 +1,17 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { AuthmsPlugin } from '@authms/core';
4
+
5
+ declare function MfaSetup(): react.JSX.Element;
6
+
7
+ declare function MfaGuard({ children }: {
8
+ children: ReactNode;
9
+ }): react.JSX.Element;
10
+
11
+ declare function BackupCodes({ codes }: {
12
+ codes: string[];
13
+ }): react.JSX.Element;
14
+
15
+ declare function mfaPlugin(): AuthmsPlugin;
16
+
17
+ export { BackupCodes, MfaGuard, MfaSetup, mfaPlugin };
@@ -0,0 +1,17 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { AuthmsPlugin } from '@authms/core';
4
+
5
+ declare function MfaSetup(): react.JSX.Element;
6
+
7
+ declare function MfaGuard({ children }: {
8
+ children: ReactNode;
9
+ }): react.JSX.Element;
10
+
11
+ declare function BackupCodes({ codes }: {
12
+ codes: string[];
13
+ }): react.JSX.Element;
14
+
15
+ declare function mfaPlugin(): AuthmsPlugin;
16
+
17
+ export { BackupCodes, MfaGuard, MfaSetup, mfaPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,417 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BackupCodes: () => BackupCodes,
24
+ MfaGuard: () => MfaGuard,
25
+ MfaSetup: () => MfaSetup,
26
+ mfaPlugin: () => mfaPlugin
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/MfaSetup.tsx
31
+ var import_react2 = require("react");
32
+ var import_react3 = require("@authms/react");
33
+
34
+ // src/BackupCodes.tsx
35
+ var import_react = require("react");
36
+ var import_jsx_runtime = require("react/jsx-runtime");
37
+ var styles = {
38
+ wrapper: { marginTop: 20 },
39
+ warning: {
40
+ background: "#fef3c7",
41
+ border: "1px solid #f59e0b",
42
+ borderRadius: 6,
43
+ padding: "10px 14px",
44
+ marginBottom: 14,
45
+ fontSize: 13,
46
+ color: "#92400e"
47
+ },
48
+ codeList: {
49
+ background: "#1f2937",
50
+ color: "#e5e7eb",
51
+ borderRadius: 8,
52
+ padding: "14px 18px",
53
+ fontFamily: "monospace",
54
+ fontSize: 14,
55
+ lineHeight: 2,
56
+ display: "grid",
57
+ gridTemplateColumns: "1fr 1fr",
58
+ gap: "0 16px",
59
+ marginBottom: 14
60
+ },
61
+ button: {
62
+ width: "100%",
63
+ padding: "8px 0",
64
+ background: "#fff",
65
+ color: "#374151",
66
+ border: "1px solid #d1d5db",
67
+ borderRadius: 6,
68
+ fontSize: 13,
69
+ fontWeight: 500,
70
+ cursor: "pointer"
71
+ },
72
+ copied: {
73
+ width: "100%",
74
+ padding: "8px 0",
75
+ background: "#ecfdf5",
76
+ color: "#065f46",
77
+ border: "1px solid #6ee7b7",
78
+ borderRadius: 6,
79
+ fontSize: 13,
80
+ fontWeight: 500
81
+ }
82
+ };
83
+ function BackupCodes({ codes }) {
84
+ const [copied, setCopied] = (0, import_react.useState)(false);
85
+ async function handleCopy() {
86
+ await navigator.clipboard.writeText(codes.join("\n"));
87
+ setCopied(true);
88
+ setTimeout(() => setCopied(false), 2e3);
89
+ }
90
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.wrapper, children: [
91
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.warning, children: "\u26A0 Store these recovery codes in a safe place. Each code can only be used once. If you lose access to your authenticator app, these codes are the only way to recover your account." }),
92
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.codeList, children: codes.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: c }, i)) }),
93
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
94
+ "button",
95
+ {
96
+ style: copied ? styles.copied : styles.button,
97
+ onClick: handleCopy,
98
+ children: copied ? "Copied!" : "Copy All Codes"
99
+ }
100
+ )
101
+ ] });
102
+ }
103
+
104
+ // src/MfaSetup.tsx
105
+ var import_jsx_runtime2 = require("react/jsx-runtime");
106
+ var styles2 = {
107
+ container: { maxWidth: 420, margin: "0 auto", padding: 24 },
108
+ title: { fontSize: 20, fontWeight: 600, marginBottom: 8 },
109
+ subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
110
+ qrPlaceholder: {
111
+ width: 200,
112
+ height: 200,
113
+ margin: "0 auto 16px",
114
+ background: "#f3f4f6",
115
+ borderRadius: 8,
116
+ display: "flex",
117
+ alignItems: "center",
118
+ justifyContent: "center",
119
+ fontSize: 13,
120
+ color: "#9ca3af",
121
+ textAlign: "center"
122
+ },
123
+ secretBox: {
124
+ background: "#f9fafb",
125
+ border: "1px solid #e5e7eb",
126
+ borderRadius: 6,
127
+ padding: "10px 14px",
128
+ marginBottom: 20,
129
+ fontFamily: "monospace",
130
+ fontSize: 14,
131
+ wordBreak: "break-all"
132
+ },
133
+ input: {
134
+ width: "100%",
135
+ padding: "10px 12px",
136
+ fontSize: 16,
137
+ border: "1px solid #d1d5db",
138
+ borderRadius: 6,
139
+ outline: "none",
140
+ boxSizing: "border-box"
141
+ },
142
+ button: {
143
+ width: "100%",
144
+ padding: "10px 0",
145
+ marginTop: 12,
146
+ background: "#4f46e5",
147
+ color: "#fff",
148
+ border: "none",
149
+ borderRadius: 6,
150
+ fontSize: 14,
151
+ fontWeight: 500,
152
+ cursor: "pointer"
153
+ },
154
+ buttonDisabled: {
155
+ width: "100%",
156
+ padding: "10px 0",
157
+ marginTop: 12,
158
+ background: "#9ca3af",
159
+ color: "#fff",
160
+ border: "none",
161
+ borderRadius: 6,
162
+ fontSize: 14,
163
+ fontWeight: 500,
164
+ cursor: "not-allowed"
165
+ },
166
+ error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
167
+ doneIcon: { fontSize: 48, marginBottom: 12, textAlign: "center" }
168
+ };
169
+ function MfaSetup() {
170
+ const { authms } = (0, import_react3.useAuthmsContext)();
171
+ const [flow, setFlow] = (0, import_react2.useState)("setup");
172
+ const [data, setData] = (0, import_react2.useState)(null);
173
+ const [code, setCode] = (0, import_react2.useState)("");
174
+ const [loading, setLoading] = (0, import_react2.useState)(false);
175
+ const [error, setError] = (0, import_react2.useState)("");
176
+ async function handleGenerate() {
177
+ setLoading(true);
178
+ setError("");
179
+ try {
180
+ const result = await authms.api.post(
181
+ "/identity/api/v1/mfa/totp/generate"
182
+ );
183
+ setData(result);
184
+ setFlow("verify");
185
+ } catch (e) {
186
+ setError(e instanceof Error ? e.message : "Failed to generate MFA setup");
187
+ } finally {
188
+ setLoading(false);
189
+ }
190
+ }
191
+ async function handleVerify() {
192
+ if (!code || !data) return;
193
+ setLoading(true);
194
+ setError("");
195
+ try {
196
+ await authms.api.post("/identity/api/v1/mfa/totp/verify", { code });
197
+ setFlow("done");
198
+ } catch (e) {
199
+ setError(e instanceof Error ? e.message : "Verification failed");
200
+ } finally {
201
+ setLoading(false);
202
+ }
203
+ }
204
+ if (flow === "done") {
205
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
206
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.doneIcon, children: "\u2713" }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.title, children: "MFA Enabled" }),
208
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.subtitle, children: "Two-factor authentication has been set up successfully." }),
209
+ data?.backupCodes && data.backupCodes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BackupCodes, { codes: data.backupCodes })
210
+ ] });
211
+ }
212
+ if (flow === "verify" && data) {
213
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
214
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.title, children: "Verify Setup" }),
215
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.subtitle, children: "Scan the QR code with your authenticator app, then enter the 6-digit code." }),
216
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.qrPlaceholder, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
217
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginBottom: 8 }, children: "\u{1F50C}" }),
218
+ "QR Code",
219
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
220
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11 }, children: "Use a qrcode library to render:" }),
221
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
222
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { fontSize: 11, fontFamily: "monospace" }, children: [
223
+ data.qrCodeUri.substring(0, 60),
224
+ "..."
225
+ ] })
226
+ ] }) }),
227
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.secretBox, children: [
228
+ "Or enter manually: ",
229
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: data.secret })
230
+ ] }),
231
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
232
+ "input",
233
+ {
234
+ style: styles2.input,
235
+ type: "text",
236
+ inputMode: "numeric",
237
+ maxLength: 6,
238
+ placeholder: "000000",
239
+ value: code,
240
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
241
+ onKeyDown: (e) => {
242
+ if (e.key === "Enter") handleVerify();
243
+ },
244
+ autoFocus: true
245
+ }
246
+ ),
247
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
248
+ "button",
249
+ {
250
+ style: loading || code.length !== 6 ? styles2.buttonDisabled : styles2.button,
251
+ onClick: handleVerify,
252
+ disabled: loading || code.length !== 6,
253
+ children: loading ? "Verifying..." : "Confirm"
254
+ }
255
+ ),
256
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.error, children: error })
257
+ ] });
258
+ }
259
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
260
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.title, children: "Set Up Two-Factor Authentication" }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.subtitle, children: "Add an extra layer of security to your account by enabling time-based one-time passwords (TOTP)." }),
262
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
263
+ "button",
264
+ {
265
+ style: loading ? styles2.buttonDisabled : styles2.button,
266
+ onClick: handleGenerate,
267
+ disabled: loading,
268
+ children: loading ? "Generating..." : "Enable Authenticator App"
269
+ }
270
+ ),
271
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.error, children: error })
272
+ ] });
273
+ }
274
+
275
+ // src/MfaGuard.tsx
276
+ var import_react4 = require("react");
277
+ var import_react5 = require("@authms/react");
278
+ var import_jsx_runtime3 = require("react/jsx-runtime");
279
+ var styles3 = {
280
+ overlay: {
281
+ display: "flex",
282
+ alignItems: "center",
283
+ justifyContent: "center",
284
+ minHeight: 300,
285
+ padding: 24
286
+ },
287
+ card: { maxWidth: 380, width: "100%", textAlign: "center" },
288
+ title: { fontSize: 18, fontWeight: 600, marginBottom: 8 },
289
+ subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
290
+ input: {
291
+ width: "100%",
292
+ padding: "10px 12px",
293
+ fontSize: 20,
294
+ textAlign: "center",
295
+ letterSpacing: 8,
296
+ border: "1px solid #d1d5db",
297
+ borderRadius: 6,
298
+ outline: "none",
299
+ boxSizing: "border-box"
300
+ },
301
+ button: {
302
+ width: "100%",
303
+ padding: "10px 0",
304
+ marginTop: 12,
305
+ background: "#4f46e5",
306
+ color: "#fff",
307
+ border: "none",
308
+ borderRadius: 6,
309
+ fontSize: 14,
310
+ fontWeight: 500,
311
+ cursor: "pointer"
312
+ },
313
+ buttonDisabled: {
314
+ width: "100%",
315
+ padding: "10px 0",
316
+ marginTop: 12,
317
+ background: "#9ca3af",
318
+ color: "#fff",
319
+ border: "none",
320
+ borderRadius: 6,
321
+ fontSize: 14,
322
+ fontWeight: 500,
323
+ cursor: "not-allowed"
324
+ },
325
+ error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
326
+ loading: { fontSize: 14, color: "#9ca3af" }
327
+ };
328
+ function MfaGuard({ children }) {
329
+ const { authms } = (0, import_react5.useAuthmsContext)();
330
+ const [status, setStatus] = (0, import_react4.useState)(null);
331
+ const [busy, setBusy] = (0, import_react4.useState)(true);
332
+ const [code, setCode] = (0, import_react4.useState)("");
333
+ const [loading, setLoading] = (0, import_react4.useState)(false);
334
+ const [error, setError] = (0, import_react4.useState)("");
335
+ (0, import_react4.useEffect)(() => {
336
+ let cancelled = false;
337
+ authms.api.get("/identity/api/v1/mfa/status").then((s) => {
338
+ if (!cancelled) setStatus(s);
339
+ }).catch(() => {
340
+ if (!cancelled) setStatus({ enrolled: false, challenged: false });
341
+ }).finally(() => {
342
+ if (!cancelled) setBusy(false);
343
+ });
344
+ return () => {
345
+ cancelled = true;
346
+ };
347
+ }, [authms]);
348
+ if (busy) {
349
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.loading, children: "Loading..." }) });
350
+ }
351
+ if (!status?.enrolled) {
352
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children });
353
+ }
354
+ if (status.challenged) {
355
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children });
356
+ }
357
+ async function handleChallenge() {
358
+ if (!code) return;
359
+ setLoading(true);
360
+ setError("");
361
+ try {
362
+ await authms.api.post("/identity/api/v1/mfa/challenge/verify", { code });
363
+ setStatus((prev) => prev ? { ...prev, challenged: true } : prev);
364
+ } catch (e) {
365
+ setError(e instanceof Error ? e.message : "Challenge failed");
366
+ } finally {
367
+ setLoading(false);
368
+ }
369
+ }
370
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.card, children: [
371
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.title, children: "Two-Factor Authentication" }),
372
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.subtitle, children: "Enter the 6-digit code from your authenticator app to continue." }),
373
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
374
+ "input",
375
+ {
376
+ style: styles3.input,
377
+ type: "text",
378
+ inputMode: "numeric",
379
+ maxLength: 6,
380
+ placeholder: "000000",
381
+ value: code,
382
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
383
+ onKeyDown: (e) => {
384
+ if (e.key === "Enter") handleChallenge();
385
+ },
386
+ autoFocus: true
387
+ }
388
+ ),
389
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
390
+ "button",
391
+ {
392
+ style: loading || code.length !== 6 ? styles3.buttonDisabled : styles3.button,
393
+ onClick: handleChallenge,
394
+ disabled: loading || code.length !== 6,
395
+ children: loading ? "Verifying..." : "Verify"
396
+ }
397
+ ),
398
+ error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.error, children: error })
399
+ ] }) });
400
+ }
401
+
402
+ // src/mfa.ts
403
+ function mfaPlugin() {
404
+ return {
405
+ name: "@authms/plugin-mfa",
406
+ version: "0.1.0",
407
+ install(_core) {
408
+ }
409
+ };
410
+ }
411
+ // Annotate the CommonJS export names for ESM import in node:
412
+ 0 && (module.exports = {
413
+ BackupCodes,
414
+ MfaGuard,
415
+ MfaSetup,
416
+ mfaPlugin
417
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,387 @@
1
+ // src/MfaSetup.tsx
2
+ import { useState as useState2 } from "react";
3
+ import { useAuthmsContext } from "@authms/react";
4
+
5
+ // src/BackupCodes.tsx
6
+ import { useState } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var styles = {
9
+ wrapper: { marginTop: 20 },
10
+ warning: {
11
+ background: "#fef3c7",
12
+ border: "1px solid #f59e0b",
13
+ borderRadius: 6,
14
+ padding: "10px 14px",
15
+ marginBottom: 14,
16
+ fontSize: 13,
17
+ color: "#92400e"
18
+ },
19
+ codeList: {
20
+ background: "#1f2937",
21
+ color: "#e5e7eb",
22
+ borderRadius: 8,
23
+ padding: "14px 18px",
24
+ fontFamily: "monospace",
25
+ fontSize: 14,
26
+ lineHeight: 2,
27
+ display: "grid",
28
+ gridTemplateColumns: "1fr 1fr",
29
+ gap: "0 16px",
30
+ marginBottom: 14
31
+ },
32
+ button: {
33
+ width: "100%",
34
+ padding: "8px 0",
35
+ background: "#fff",
36
+ color: "#374151",
37
+ border: "1px solid #d1d5db",
38
+ borderRadius: 6,
39
+ fontSize: 13,
40
+ fontWeight: 500,
41
+ cursor: "pointer"
42
+ },
43
+ copied: {
44
+ width: "100%",
45
+ padding: "8px 0",
46
+ background: "#ecfdf5",
47
+ color: "#065f46",
48
+ border: "1px solid #6ee7b7",
49
+ borderRadius: 6,
50
+ fontSize: 13,
51
+ fontWeight: 500
52
+ }
53
+ };
54
+ function BackupCodes({ codes }) {
55
+ const [copied, setCopied] = useState(false);
56
+ async function handleCopy() {
57
+ await navigator.clipboard.writeText(codes.join("\n"));
58
+ setCopied(true);
59
+ setTimeout(() => setCopied(false), 2e3);
60
+ }
61
+ return /* @__PURE__ */ jsxs("div", { style: styles.wrapper, children: [
62
+ /* @__PURE__ */ jsx("div", { style: styles.warning, children: "\u26A0 Store these recovery codes in a safe place. Each code can only be used once. If you lose access to your authenticator app, these codes are the only way to recover your account." }),
63
+ /* @__PURE__ */ jsx("div", { style: styles.codeList, children: codes.map((c, i) => /* @__PURE__ */ jsx("span", { children: c }, i)) }),
64
+ /* @__PURE__ */ jsx(
65
+ "button",
66
+ {
67
+ style: copied ? styles.copied : styles.button,
68
+ onClick: handleCopy,
69
+ children: copied ? "Copied!" : "Copy All Codes"
70
+ }
71
+ )
72
+ ] });
73
+ }
74
+
75
+ // src/MfaSetup.tsx
76
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
77
+ var styles2 = {
78
+ container: { maxWidth: 420, margin: "0 auto", padding: 24 },
79
+ title: { fontSize: 20, fontWeight: 600, marginBottom: 8 },
80
+ subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
81
+ qrPlaceholder: {
82
+ width: 200,
83
+ height: 200,
84
+ margin: "0 auto 16px",
85
+ background: "#f3f4f6",
86
+ borderRadius: 8,
87
+ display: "flex",
88
+ alignItems: "center",
89
+ justifyContent: "center",
90
+ fontSize: 13,
91
+ color: "#9ca3af",
92
+ textAlign: "center"
93
+ },
94
+ secretBox: {
95
+ background: "#f9fafb",
96
+ border: "1px solid #e5e7eb",
97
+ borderRadius: 6,
98
+ padding: "10px 14px",
99
+ marginBottom: 20,
100
+ fontFamily: "monospace",
101
+ fontSize: 14,
102
+ wordBreak: "break-all"
103
+ },
104
+ input: {
105
+ width: "100%",
106
+ padding: "10px 12px",
107
+ fontSize: 16,
108
+ border: "1px solid #d1d5db",
109
+ borderRadius: 6,
110
+ outline: "none",
111
+ boxSizing: "border-box"
112
+ },
113
+ button: {
114
+ width: "100%",
115
+ padding: "10px 0",
116
+ marginTop: 12,
117
+ background: "#4f46e5",
118
+ color: "#fff",
119
+ border: "none",
120
+ borderRadius: 6,
121
+ fontSize: 14,
122
+ fontWeight: 500,
123
+ cursor: "pointer"
124
+ },
125
+ buttonDisabled: {
126
+ width: "100%",
127
+ padding: "10px 0",
128
+ marginTop: 12,
129
+ background: "#9ca3af",
130
+ color: "#fff",
131
+ border: "none",
132
+ borderRadius: 6,
133
+ fontSize: 14,
134
+ fontWeight: 500,
135
+ cursor: "not-allowed"
136
+ },
137
+ error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
138
+ doneIcon: { fontSize: 48, marginBottom: 12, textAlign: "center" }
139
+ };
140
+ function MfaSetup() {
141
+ const { authms } = useAuthmsContext();
142
+ const [flow, setFlow] = useState2("setup");
143
+ const [data, setData] = useState2(null);
144
+ const [code, setCode] = useState2("");
145
+ const [loading, setLoading] = useState2(false);
146
+ const [error, setError] = useState2("");
147
+ async function handleGenerate() {
148
+ setLoading(true);
149
+ setError("");
150
+ try {
151
+ const result = await authms.api.post(
152
+ "/identity/api/v1/mfa/totp/generate"
153
+ );
154
+ setData(result);
155
+ setFlow("verify");
156
+ } catch (e) {
157
+ setError(e instanceof Error ? e.message : "Failed to generate MFA setup");
158
+ } finally {
159
+ setLoading(false);
160
+ }
161
+ }
162
+ async function handleVerify() {
163
+ if (!code || !data) return;
164
+ setLoading(true);
165
+ setError("");
166
+ try {
167
+ await authms.api.post("/identity/api/v1/mfa/totp/verify", { code });
168
+ setFlow("done");
169
+ } catch (e) {
170
+ setError(e instanceof Error ? e.message : "Verification failed");
171
+ } finally {
172
+ setLoading(false);
173
+ }
174
+ }
175
+ if (flow === "done") {
176
+ return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
177
+ /* @__PURE__ */ jsx2("div", { style: styles2.doneIcon, children: "\u2713" }),
178
+ /* @__PURE__ */ jsx2("div", { style: styles2.title, children: "MFA Enabled" }),
179
+ /* @__PURE__ */ jsx2("div", { style: styles2.subtitle, children: "Two-factor authentication has been set up successfully." }),
180
+ data?.backupCodes && data.backupCodes.length > 0 && /* @__PURE__ */ jsx2(BackupCodes, { codes: data.backupCodes })
181
+ ] });
182
+ }
183
+ if (flow === "verify" && data) {
184
+ return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
185
+ /* @__PURE__ */ jsx2("div", { style: styles2.title, children: "Verify Setup" }),
186
+ /* @__PURE__ */ jsx2("div", { style: styles2.subtitle, children: "Scan the QR code with your authenticator app, then enter the 6-digit code." }),
187
+ /* @__PURE__ */ jsx2("div", { style: styles2.qrPlaceholder, children: /* @__PURE__ */ jsxs2("div", { children: [
188
+ /* @__PURE__ */ jsx2("div", { style: { marginBottom: 8 }, children: "\u{1F50C}" }),
189
+ "QR Code",
190
+ /* @__PURE__ */ jsx2("br", {}),
191
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 11 }, children: "Use a qrcode library to render:" }),
192
+ /* @__PURE__ */ jsx2("br", {}),
193
+ /* @__PURE__ */ jsxs2("span", { style: { fontSize: 11, fontFamily: "monospace" }, children: [
194
+ data.qrCodeUri.substring(0, 60),
195
+ "..."
196
+ ] })
197
+ ] }) }),
198
+ /* @__PURE__ */ jsxs2("div", { style: styles2.secretBox, children: [
199
+ "Or enter manually: ",
200
+ /* @__PURE__ */ jsx2("strong", { children: data.secret })
201
+ ] }),
202
+ /* @__PURE__ */ jsx2(
203
+ "input",
204
+ {
205
+ style: styles2.input,
206
+ type: "text",
207
+ inputMode: "numeric",
208
+ maxLength: 6,
209
+ placeholder: "000000",
210
+ value: code,
211
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
212
+ onKeyDown: (e) => {
213
+ if (e.key === "Enter") handleVerify();
214
+ },
215
+ autoFocus: true
216
+ }
217
+ ),
218
+ /* @__PURE__ */ jsx2(
219
+ "button",
220
+ {
221
+ style: loading || code.length !== 6 ? styles2.buttonDisabled : styles2.button,
222
+ onClick: handleVerify,
223
+ disabled: loading || code.length !== 6,
224
+ children: loading ? "Verifying..." : "Confirm"
225
+ }
226
+ ),
227
+ error && /* @__PURE__ */ jsx2("div", { style: styles2.error, children: error })
228
+ ] });
229
+ }
230
+ return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
231
+ /* @__PURE__ */ jsx2("div", { style: styles2.title, children: "Set Up Two-Factor Authentication" }),
232
+ /* @__PURE__ */ jsx2("div", { style: styles2.subtitle, children: "Add an extra layer of security to your account by enabling time-based one-time passwords (TOTP)." }),
233
+ /* @__PURE__ */ jsx2(
234
+ "button",
235
+ {
236
+ style: loading ? styles2.buttonDisabled : styles2.button,
237
+ onClick: handleGenerate,
238
+ disabled: loading,
239
+ children: loading ? "Generating..." : "Enable Authenticator App"
240
+ }
241
+ ),
242
+ error && /* @__PURE__ */ jsx2("div", { style: styles2.error, children: error })
243
+ ] });
244
+ }
245
+
246
+ // src/MfaGuard.tsx
247
+ import { useState as useState3, useEffect } from "react";
248
+ import { useAuthmsContext as useAuthmsContext2 } from "@authms/react";
249
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
250
+ var styles3 = {
251
+ overlay: {
252
+ display: "flex",
253
+ alignItems: "center",
254
+ justifyContent: "center",
255
+ minHeight: 300,
256
+ padding: 24
257
+ },
258
+ card: { maxWidth: 380, width: "100%", textAlign: "center" },
259
+ title: { fontSize: 18, fontWeight: 600, marginBottom: 8 },
260
+ subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
261
+ input: {
262
+ width: "100%",
263
+ padding: "10px 12px",
264
+ fontSize: 20,
265
+ textAlign: "center",
266
+ letterSpacing: 8,
267
+ border: "1px solid #d1d5db",
268
+ borderRadius: 6,
269
+ outline: "none",
270
+ boxSizing: "border-box"
271
+ },
272
+ button: {
273
+ width: "100%",
274
+ padding: "10px 0",
275
+ marginTop: 12,
276
+ background: "#4f46e5",
277
+ color: "#fff",
278
+ border: "none",
279
+ borderRadius: 6,
280
+ fontSize: 14,
281
+ fontWeight: 500,
282
+ cursor: "pointer"
283
+ },
284
+ buttonDisabled: {
285
+ width: "100%",
286
+ padding: "10px 0",
287
+ marginTop: 12,
288
+ background: "#9ca3af",
289
+ color: "#fff",
290
+ border: "none",
291
+ borderRadius: 6,
292
+ fontSize: 14,
293
+ fontWeight: 500,
294
+ cursor: "not-allowed"
295
+ },
296
+ error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
297
+ loading: { fontSize: 14, color: "#9ca3af" }
298
+ };
299
+ function MfaGuard({ children }) {
300
+ const { authms } = useAuthmsContext2();
301
+ const [status, setStatus] = useState3(null);
302
+ const [busy, setBusy] = useState3(true);
303
+ const [code, setCode] = useState3("");
304
+ const [loading, setLoading] = useState3(false);
305
+ const [error, setError] = useState3("");
306
+ useEffect(() => {
307
+ let cancelled = false;
308
+ authms.api.get("/identity/api/v1/mfa/status").then((s) => {
309
+ if (!cancelled) setStatus(s);
310
+ }).catch(() => {
311
+ if (!cancelled) setStatus({ enrolled: false, challenged: false });
312
+ }).finally(() => {
313
+ if (!cancelled) setBusy(false);
314
+ });
315
+ return () => {
316
+ cancelled = true;
317
+ };
318
+ }, [authms]);
319
+ if (busy) {
320
+ return /* @__PURE__ */ jsx3("div", { style: styles3.overlay, children: /* @__PURE__ */ jsx3("div", { style: styles3.loading, children: "Loading..." }) });
321
+ }
322
+ if (!status?.enrolled) {
323
+ return /* @__PURE__ */ jsx3(Fragment, { children });
324
+ }
325
+ if (status.challenged) {
326
+ return /* @__PURE__ */ jsx3(Fragment, { children });
327
+ }
328
+ async function handleChallenge() {
329
+ if (!code) return;
330
+ setLoading(true);
331
+ setError("");
332
+ try {
333
+ await authms.api.post("/identity/api/v1/mfa/challenge/verify", { code });
334
+ setStatus((prev) => prev ? { ...prev, challenged: true } : prev);
335
+ } catch (e) {
336
+ setError(e instanceof Error ? e.message : "Challenge failed");
337
+ } finally {
338
+ setLoading(false);
339
+ }
340
+ }
341
+ return /* @__PURE__ */ jsx3("div", { style: styles3.overlay, children: /* @__PURE__ */ jsxs3("div", { style: styles3.card, children: [
342
+ /* @__PURE__ */ jsx3("div", { style: styles3.title, children: "Two-Factor Authentication" }),
343
+ /* @__PURE__ */ jsx3("div", { style: styles3.subtitle, children: "Enter the 6-digit code from your authenticator app to continue." }),
344
+ /* @__PURE__ */ jsx3(
345
+ "input",
346
+ {
347
+ style: styles3.input,
348
+ type: "text",
349
+ inputMode: "numeric",
350
+ maxLength: 6,
351
+ placeholder: "000000",
352
+ value: code,
353
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
354
+ onKeyDown: (e) => {
355
+ if (e.key === "Enter") handleChallenge();
356
+ },
357
+ autoFocus: true
358
+ }
359
+ ),
360
+ /* @__PURE__ */ jsx3(
361
+ "button",
362
+ {
363
+ style: loading || code.length !== 6 ? styles3.buttonDisabled : styles3.button,
364
+ onClick: handleChallenge,
365
+ disabled: loading || code.length !== 6,
366
+ children: loading ? "Verifying..." : "Verify"
367
+ }
368
+ ),
369
+ error && /* @__PURE__ */ jsx3("div", { style: styles3.error, children: error })
370
+ ] }) });
371
+ }
372
+
373
+ // src/mfa.ts
374
+ function mfaPlugin() {
375
+ return {
376
+ name: "@authms/plugin-mfa",
377
+ version: "0.1.0",
378
+ install(_core) {
379
+ }
380
+ };
381
+ }
382
+ export {
383
+ BackupCodes,
384
+ MfaGuard,
385
+ MfaSetup,
386
+ mfaPlugin
387
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@autional/plugin-mfa",
3
+ "version": "0.2.0",
4
+ "description": "AuthMS MFA Plugin — Multi-Factor Authentication UI components and flows for @autional/react",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "sideEffects": false,
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --external react --external react-dom --external @autional/core --external @autional/react",
18
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch --external react --external react-dom --external @autional/core --external @autional/react",
19
+ "typecheck": "tsc --noEmit",
20
+ "clean": "rimraf dist"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "keywords": [
27
+ "auth",
28
+ "authentication",
29
+ "mfa",
30
+ "totp",
31
+ "2fa",
32
+ "authms",
33
+ "plugin"
34
+ ],
35
+ "license": "MIT",
36
+ "peerDependencies": {
37
+ "@autional/core": ">=0.1.0",
38
+ "@autional/react": ">=0.1.0",
39
+ "react": ">=18.0.0",
40
+ "react-dom": ">=18.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@autional/core": ">=0.1.0",
44
+ "@autional/react": ">=0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@autional/tsconfig": ">=0.1.0",
48
+ "@types/react": "^19.0.0",
49
+ "@types/react-dom": "^19.0.0",
50
+ "react": ">=18.0.0",
51
+ "react-dom": "^19.0.0",
52
+ "rimraf": "^5.0.0",
53
+ "tsup": "^8.4.0",
54
+ "typescript": "^5.8.0",
55
+ "vitest": "^3.0.0"
56
+ }
57
+ }
@@ -0,0 +1,60 @@
1
+ import { useState } from 'react';
2
+
3
+ const styles = {
4
+ wrapper: { marginTop: 20 } as React.CSSProperties,
5
+ warning: {
6
+ background: '#fef3c7', border: '1px solid #f59e0b',
7
+ borderRadius: 6, padding: '10px 14px', marginBottom: 14,
8
+ fontSize: 13, color: '#92400e',
9
+ } as React.CSSProperties,
10
+ codeList: {
11
+ background: '#1f2937', color: '#e5e7eb',
12
+ borderRadius: 8, padding: '14px 18px',
13
+ fontFamily: 'monospace', fontSize: 14, lineHeight: 2,
14
+ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px',
15
+ marginBottom: 14,
16
+ } as React.CSSProperties,
17
+ button: {
18
+ width: '100%', padding: '8px 0',
19
+ background: '#fff', color: '#374151', border: '1px solid #d1d5db',
20
+ borderRadius: 6, fontSize: 13, fontWeight: 500, cursor: 'pointer',
21
+ } as React.CSSProperties,
22
+ copied: {
23
+ width: '100%', padding: '8px 0',
24
+ background: '#ecfdf5', color: '#065f46', border: '1px solid #6ee7b7',
25
+ borderRadius: 6, fontSize: 13, fontWeight: 500,
26
+ } as React.CSSProperties,
27
+ };
28
+
29
+ export function BackupCodes({ codes }: { codes: string[] }) {
30
+ const [copied, setCopied] = useState(false);
31
+
32
+ async function handleCopy() {
33
+ await navigator.clipboard.writeText(codes.join('\n'));
34
+ setCopied(true);
35
+ setTimeout(() => setCopied(false), 2000);
36
+ }
37
+
38
+ return (
39
+ <div style={styles.wrapper}>
40
+ <div style={styles.warning}>
41
+ &#9888; Store these recovery codes in a safe place. Each code can only be
42
+ used once. If you lose access to your authenticator app, these codes are
43
+ the only way to recover your account.
44
+ </div>
45
+
46
+ <div style={styles.codeList}>
47
+ {codes.map((c, i) => (
48
+ <span key={i}>{c}</span>
49
+ ))}
50
+ </div>
51
+
52
+ <button
53
+ style={copied ? styles.copied : styles.button}
54
+ onClick={handleCopy}
55
+ >
56
+ {copied ? 'Copied!' : 'Copy All Codes'}
57
+ </button>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,111 @@
1
+ import { useState, useEffect, type ReactNode } from 'react';
2
+ import { useAuthmsContext } from '@authms/react';
3
+
4
+ interface MfaStatus {
5
+ enrolled: boolean;
6
+ challenged: boolean;
7
+ }
8
+
9
+ const styles = {
10
+ overlay: {
11
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
12
+ minHeight: 300, padding: 24,
13
+ } as React.CSSProperties,
14
+ card: { maxWidth: 380, width: '100%', textAlign: 'center' } as React.CSSProperties,
15
+ title: { fontSize: 18, fontWeight: 600, marginBottom: 8 } as React.CSSProperties,
16
+ subtitle: { fontSize: 14, color: '#6b7280', marginBottom: 20 } as React.CSSProperties,
17
+ input: {
18
+ width: '100%', padding: '10px 12px', fontSize: 20, textAlign: 'center',
19
+ letterSpacing: 8, border: '1px solid #d1d5db', borderRadius: 6, outline: 'none',
20
+ boxSizing: 'border-box',
21
+ } as React.CSSProperties,
22
+ button: {
23
+ width: '100%', padding: '10px 0', marginTop: 12,
24
+ background: '#4f46e5', color: '#fff', border: 'none',
25
+ borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'pointer',
26
+ } as React.CSSProperties,
27
+ buttonDisabled: {
28
+ width: '100%', padding: '10px 0', marginTop: 12,
29
+ background: '#9ca3af', color: '#fff', border: 'none',
30
+ borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'not-allowed',
31
+ } as React.CSSProperties,
32
+ error: { color: '#ef4444', fontSize: 13, marginTop: 8 } as React.CSSProperties,
33
+ loading: { fontSize: 14, color: '#9ca3af' } as React.CSSProperties,
34
+ };
35
+
36
+ export function MfaGuard({ children }: { children: ReactNode }) {
37
+ const { authms } = useAuthmsContext();
38
+ const [status, setStatus] = useState<MfaStatus | null>(null);
39
+ const [busy, setBusy] = useState(true);
40
+ const [code, setCode] = useState('');
41
+ const [loading, setLoading] = useState(false);
42
+ const [error, setError] = useState('');
43
+
44
+ useEffect(() => {
45
+ let cancelled = false;
46
+ authms.api.get<MfaStatus>('/identity/api/v1/mfa/status')
47
+ .then((s) => { if (!cancelled) setStatus(s); })
48
+ .catch(() => { if (!cancelled) setStatus({ enrolled: false, challenged: false }); })
49
+ .finally(() => { if (!cancelled) setBusy(false); });
50
+ return () => { cancelled = true; };
51
+ }, [authms]);
52
+
53
+ if (busy) {
54
+ return <div style={styles.overlay}><div style={styles.loading}>Loading...</div></div>;
55
+ }
56
+
57
+ if (!status?.enrolled) {
58
+ return <>{children}</>;
59
+ }
60
+
61
+ if (status.challenged) {
62
+ return <>{children}</>;
63
+ }
64
+
65
+ async function handleChallenge() {
66
+ if (!code) return;
67
+ setLoading(true);
68
+ setError('');
69
+ try {
70
+ await authms.api.post('/identity/api/v1/mfa/challenge/verify', { code });
71
+ setStatus((prev) => prev ? { ...prev, challenged: true } : prev);
72
+ } catch (e) {
73
+ setError(e instanceof Error ? e.message : 'Challenge failed');
74
+ } finally {
75
+ setLoading(false);
76
+ }
77
+ }
78
+
79
+ return (
80
+ <div style={styles.overlay}>
81
+ <div style={styles.card}>
82
+ <div style={styles.title}>Two-Factor Authentication</div>
83
+ <div style={styles.subtitle}>
84
+ Enter the 6-digit code from your authenticator app to continue.
85
+ </div>
86
+
87
+ <input
88
+ style={styles.input}
89
+ type="text"
90
+ inputMode="numeric"
91
+ maxLength={6}
92
+ placeholder="000000"
93
+ value={code}
94
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
95
+ onKeyDown={(e) => { if (e.key === 'Enter') handleChallenge(); }}
96
+ autoFocus
97
+ />
98
+
99
+ <button
100
+ style={loading || code.length !== 6 ? styles.buttonDisabled : styles.button}
101
+ onClick={handleChallenge}
102
+ disabled={loading || code.length !== 6}
103
+ >
104
+ {loading ? 'Verifying...' : 'Verify'}
105
+ </button>
106
+
107
+ {error && <div style={styles.error}>{error}</div>}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,170 @@
1
+ import { useState } from 'react';
2
+ import { useAuthmsContext } from '@authms/react';
3
+ import { BackupCodes } from './BackupCodes';
4
+
5
+ type FlowState = 'setup' | 'verify' | 'done';
6
+
7
+ interface TotpSetupData {
8
+ secret: string;
9
+ qrCodeUri: string;
10
+ backupCodes: string[];
11
+ }
12
+
13
+ const styles = {
14
+ container: { maxWidth: 420, margin: '0 auto', padding: 24 } as React.CSSProperties,
15
+ title: { fontSize: 20, fontWeight: 600, marginBottom: 8 } as React.CSSProperties,
16
+ subtitle: { fontSize: 14, color: '#6b7280', marginBottom: 20 } as React.CSSProperties,
17
+ qrPlaceholder: {
18
+ width: 200, height: 200, margin: '0 auto 16px',
19
+ background: '#f3f4f6', borderRadius: 8,
20
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
21
+ fontSize: 13, color: '#9ca3af', textAlign: 'center',
22
+ } as React.CSSProperties,
23
+ secretBox: {
24
+ background: '#f9fafb', border: '1px solid #e5e7eb',
25
+ borderRadius: 6, padding: '10px 14px', marginBottom: 20,
26
+ fontFamily: 'monospace', fontSize: 14, wordBreak: 'break-all',
27
+ } as React.CSSProperties,
28
+ input: {
29
+ width: '100%', padding: '10px 12px', fontSize: 16,
30
+ border: '1px solid #d1d5db', borderRadius: 6, outline: 'none',
31
+ boxSizing: 'border-box',
32
+ } as React.CSSProperties,
33
+ button: {
34
+ width: '100%', padding: '10px 0', marginTop: 12,
35
+ background: '#4f46e5', color: '#fff', border: 'none',
36
+ borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'pointer',
37
+ } as React.CSSProperties,
38
+ buttonDisabled: {
39
+ width: '100%', padding: '10px 0', marginTop: 12,
40
+ background: '#9ca3af', color: '#fff', border: 'none',
41
+ borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'not-allowed',
42
+ } as React.CSSProperties,
43
+ error: { color: '#ef4444', fontSize: 13, marginTop: 8 } as React.CSSProperties,
44
+ doneIcon: { fontSize: 48, marginBottom: 12, textAlign: 'center' } as React.CSSProperties,
45
+ };
46
+
47
+ export function MfaSetup() {
48
+ const { authms } = useAuthmsContext();
49
+ const [flow, setFlow] = useState<FlowState>('setup');
50
+ const [data, setData] = useState<TotpSetupData | null>(null);
51
+ const [code, setCode] = useState('');
52
+ const [loading, setLoading] = useState(false);
53
+ const [error, setError] = useState('');
54
+
55
+ async function handleGenerate() {
56
+ setLoading(true);
57
+ setError('');
58
+ try {
59
+ const result = await authms.api.post<TotpSetupData>(
60
+ '/identity/api/v1/mfa/totp/generate',
61
+ );
62
+ setData(result);
63
+ setFlow('verify');
64
+ } catch (e) {
65
+ setError(e instanceof Error ? e.message : 'Failed to generate MFA setup');
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ }
70
+
71
+ async function handleVerify() {
72
+ if (!code || !data) return;
73
+ setLoading(true);
74
+ setError('');
75
+ try {
76
+ await authms.api.post('/identity/api/v1/mfa/totp/verify', { code });
77
+ setFlow('done');
78
+ } catch (e) {
79
+ setError(e instanceof Error ? e.message : 'Verification failed');
80
+ } finally {
81
+ setLoading(false);
82
+ }
83
+ }
84
+
85
+ if (flow === 'done') {
86
+ return (
87
+ <div style={styles.container}>
88
+ <div style={styles.doneIcon}>&#10003;</div>
89
+ <div style={styles.title}>MFA Enabled</div>
90
+ <div style={styles.subtitle}>
91
+ Two-factor authentication has been set up successfully.
92
+ </div>
93
+ {data?.backupCodes && data.backupCodes.length > 0 && (
94
+ <BackupCodes codes={data.backupCodes} />
95
+ )}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ if (flow === 'verify' && data) {
101
+ return (
102
+ <div style={styles.container}>
103
+ <div style={styles.title}>Verify Setup</div>
104
+ <div style={styles.subtitle}>
105
+ Scan the QR code with your authenticator app, then enter the 6-digit code.
106
+ </div>
107
+
108
+ {/* QR Code placeholder — replace with actual qrcode generation in your app */}
109
+ <div style={styles.qrPlaceholder}>
110
+ <div>
111
+ <div style={{ marginBottom: 8 }}>&#128268;</div>
112
+ QR Code
113
+ <br />
114
+ <span style={{ fontSize: 11 }}>Use a qrcode library to render:</span>
115
+ <br />
116
+ <span style={{ fontSize: 11, fontFamily: 'monospace' }}>
117
+ {data.qrCodeUri.substring(0, 60)}...
118
+ </span>
119
+ </div>
120
+ </div>
121
+
122
+ <div style={styles.secretBox}>
123
+ Or enter manually: <strong>{data.secret}</strong>
124
+ </div>
125
+
126
+ <input
127
+ style={styles.input}
128
+ type="text"
129
+ inputMode="numeric"
130
+ maxLength={6}
131
+ placeholder="000000"
132
+ value={code}
133
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
134
+ onKeyDown={(e) => { if (e.key === 'Enter') handleVerify(); }}
135
+ autoFocus
136
+ />
137
+
138
+ <button
139
+ style={loading || code.length !== 6 ? styles.buttonDisabled : styles.button}
140
+ onClick={handleVerify}
141
+ disabled={loading || code.length !== 6}
142
+ >
143
+ {loading ? 'Verifying...' : 'Confirm'}
144
+ </button>
145
+
146
+ {error && <div style={styles.error}>{error}</div>}
147
+ </div>
148
+ );
149
+ }
150
+
151
+ return (
152
+ <div style={styles.container}>
153
+ <div style={styles.title}>Set Up Two-Factor Authentication</div>
154
+ <div style={styles.subtitle}>
155
+ Add an extra layer of security to your account by enabling
156
+ time-based one-time passwords (TOTP).
157
+ </div>
158
+
159
+ <button
160
+ style={loading ? styles.buttonDisabled : styles.button}
161
+ onClick={handleGenerate}
162
+ disabled={loading}
163
+ >
164
+ {loading ? 'Generating...' : 'Enable Authenticator App'}
165
+ </button>
166
+
167
+ {error && <div style={styles.error}>{error}</div>}
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,27 @@
1
+ // @ts-nocheck — vitest mock types
2
+ import { describe, it, expect } from 'vitest';
3
+ import { mfaPlugin } from '../index';
4
+
5
+ describe('plugin-mfa', () => {
6
+ describe('mfaPlugin', () => {
7
+ it('returns an object with name and version', () => {
8
+ const plugin = mfaPlugin();
9
+ expect(plugin.name).toBeDefined();
10
+ expect(typeof plugin.name).toBe('string');
11
+ expect(plugin.version).toBeDefined();
12
+ expect(typeof plugin.version).toBe('string');
13
+ });
14
+
15
+ it('has install method', () => {
16
+ const plugin = mfaPlugin();
17
+ expect(plugin.install).toBeDefined();
18
+ expect(typeof plugin.install).toBe('function');
19
+ });
20
+
21
+ it('install calls core registration', () => {
22
+ const plugin = mfaPlugin();
23
+ const mockCore: any = { use: () => {} };
24
+ expect(() => plugin.install(mockCore)).not.toThrow();
25
+ });
26
+ });
27
+ });
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { MfaSetup } from './MfaSetup';
2
+ export { MfaGuard } from './MfaGuard';
3
+ export { BackupCodes } from './BackupCodes';
4
+ export { mfaPlugin } from './mfa';
package/src/mfa.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AuthmsPlugin } from '@authms/core';
2
+ import type { AuthMS } from '@authms/core';
3
+
4
+ export function mfaPlugin(): AuthmsPlugin {
5
+ return {
6
+ name: '@authms/plugin-mfa',
7
+ version: '0.1.0',
8
+ install(_core: AuthMS) {},
9
+ };
10
+ }