@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,123 @@
|
|
|
1
|
+
import { badRequest, log, notFound } from "@flink-app/flink";
|
|
2
|
+
import { OtpInternalContext } from "../OtpInternalContext";
|
|
3
|
+
|
|
4
|
+
export interface VerifyOptions {
|
|
5
|
+
/** The session ID from the initiate response */
|
|
6
|
+
sessionId: string;
|
|
7
|
+
/** The OTP code entered by the user */
|
|
8
|
+
code: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VerifyResponse {
|
|
12
|
+
/** Verification status */
|
|
13
|
+
status: "success" | "invalid_code" | "expired" | "locked" | "not_found";
|
|
14
|
+
/** JWT token if successful */
|
|
15
|
+
token?: string;
|
|
16
|
+
/** User object if successful */
|
|
17
|
+
user?: any;
|
|
18
|
+
/** Remaining attempts if code was invalid */
|
|
19
|
+
remainingAttempts?: number;
|
|
20
|
+
/** Error message */
|
|
21
|
+
message?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function verify(ctx: OtpInternalContext, options: VerifyOptions): Promise<VerifyResponse> {
|
|
25
|
+
const { sessionId, code } = options;
|
|
26
|
+
const { options: pluginOptions } = ctx.plugins.otpAuth;
|
|
27
|
+
|
|
28
|
+
// Validate input
|
|
29
|
+
if (!sessionId || !code) {
|
|
30
|
+
throw badRequest("Session ID and code are required");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Find session
|
|
34
|
+
const session = await ctx.repos.otpSessionRepo.getSession(sessionId);
|
|
35
|
+
|
|
36
|
+
if (!session) {
|
|
37
|
+
log.warn(`OTP session not found: ${sessionId}`);
|
|
38
|
+
return {
|
|
39
|
+
status: "not_found",
|
|
40
|
+
message: "Session not found or expired",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if session is already verified
|
|
45
|
+
if (session.status === "verified") {
|
|
46
|
+
log.warn(`Attempt to verify already verified session: ${sessionId}`);
|
|
47
|
+
throw badRequest("Session already verified");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if session is locked
|
|
51
|
+
if (session.status === "locked") {
|
|
52
|
+
log.warn(`Attempt to verify locked session: ${sessionId}`);
|
|
53
|
+
return {
|
|
54
|
+
status: "locked",
|
|
55
|
+
message: "Too many failed attempts. Session is locked.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if session has expired
|
|
60
|
+
if (session.status === "expired" || new Date() > session.expiresAt) {
|
|
61
|
+
if (session.status !== "expired") {
|
|
62
|
+
await ctx.repos.otpSessionRepo.markAsExpired(sessionId);
|
|
63
|
+
}
|
|
64
|
+
log.warn(`Attempt to verify expired session: ${sessionId}`);
|
|
65
|
+
return {
|
|
66
|
+
status: "expired",
|
|
67
|
+
message: "Verification code has expired",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify the code
|
|
72
|
+
const codeMatches = session.code === code;
|
|
73
|
+
|
|
74
|
+
if (!codeMatches) {
|
|
75
|
+
// Increment attempts
|
|
76
|
+
const updatedSession = await ctx.repos.otpSessionRepo.incrementAttempts(sessionId);
|
|
77
|
+
|
|
78
|
+
const remainingAttempts = updatedSession!.maxAttempts - updatedSession!.attempts;
|
|
79
|
+
|
|
80
|
+
log.warn(`Invalid OTP code for session ${sessionId}. Remaining attempts: ${remainingAttempts}`);
|
|
81
|
+
|
|
82
|
+
if (updatedSession!.status === "locked") {
|
|
83
|
+
return {
|
|
84
|
+
status: "locked",
|
|
85
|
+
message: "Too many failed attempts. Session is locked.",
|
|
86
|
+
remainingAttempts: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
status: "invalid_code",
|
|
92
|
+
message: "Invalid verification code",
|
|
93
|
+
remainingAttempts,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Code is valid - mark session as verified
|
|
98
|
+
await ctx.repos.otpSessionRepo.markAsVerified(sessionId);
|
|
99
|
+
|
|
100
|
+
// Get user via callback
|
|
101
|
+
const user = await pluginOptions.onGetUser(session.identifier, session.method, session.payload);
|
|
102
|
+
|
|
103
|
+
if (!user) {
|
|
104
|
+
log.error(`User not found for identifier ${session.identifier} after successful OTP verification`);
|
|
105
|
+
throw notFound("User not found");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Generate token via callback
|
|
109
|
+
try {
|
|
110
|
+
const authResult = await pluginOptions.onVerifySuccess(user, session.identifier, session.method, session.payload);
|
|
111
|
+
|
|
112
|
+
log.info(`OTP verification successful for session ${sessionId}, identifier: ${session.identifier}`);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
status: "success",
|
|
116
|
+
token: authResult.token,
|
|
117
|
+
user: authResult.user,
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log.error(`Error in onVerifySuccess callback: ${error}`);
|
|
121
|
+
throw new Error("Failed to complete authentication");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps, internalServerError } from "@flink-app/flink";
|
|
2
|
+
import InitiateRequest from "../schemas/InitiateRequest";
|
|
3
|
+
import InitiateResponse from "../schemas/InitiateResponse";
|
|
4
|
+
import { OtpInternalContext } from "../OtpInternalContext";
|
|
5
|
+
import { initiate } from "../functions/initiate";
|
|
6
|
+
|
|
7
|
+
export const Route: RouteProps = {
|
|
8
|
+
path: "/otp/initiate",
|
|
9
|
+
method: HttpMethod.post,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const PostOtpInitiate: Handler<OtpInternalContext, InitiateRequest, InitiateResponse> = async ({ ctx, req }) => {
|
|
13
|
+
try {
|
|
14
|
+
const response = await initiate(ctx, {
|
|
15
|
+
identifier: req.body.identifier,
|
|
16
|
+
method: req.body.method,
|
|
17
|
+
payload: req.body.payload,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
data: response,
|
|
22
|
+
};
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
return internalServerError(error.message || "Failed to initiate OTP authentication");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default PostOtpInitiate;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Handler, HttpMethod, RouteProps } from "@flink-app/flink";
|
|
2
|
+
import { OtpInternalContext } from "../OtpInternalContext";
|
|
3
|
+
import { verify } from "../functions/verify";
|
|
4
|
+
import VerifyRequest from "../schemas/VerifyRequest";
|
|
5
|
+
import VerifyResponse from "../schemas/VerifyResponse";
|
|
6
|
+
|
|
7
|
+
export const Route: RouteProps = {
|
|
8
|
+
path: "/otp/verify",
|
|
9
|
+
method: HttpMethod.post,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const PostOtpVerify: Handler<OtpInternalContext, VerifyRequest, VerifyResponse> = async ({ ctx, req }) => {
|
|
13
|
+
const response = await verify(ctx, {
|
|
14
|
+
sessionId: req.body.sessionId,
|
|
15
|
+
code: req.body.code,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Return appropriate HTTP status based on verification result
|
|
19
|
+
if (response.status === "success") {
|
|
20
|
+
return {
|
|
21
|
+
data: response,
|
|
22
|
+
};
|
|
23
|
+
} else if (response.status === "not_found") {
|
|
24
|
+
return {
|
|
25
|
+
status: 404,
|
|
26
|
+
data: response,
|
|
27
|
+
};
|
|
28
|
+
} else if (response.status === "locked" || response.status === "expired") {
|
|
29
|
+
return {
|
|
30
|
+
status: 403,
|
|
31
|
+
data: response,
|
|
32
|
+
};
|
|
33
|
+
} else {
|
|
34
|
+
// invalid_code
|
|
35
|
+
return {
|
|
36
|
+
status: 401,
|
|
37
|
+
data: response,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default PostOtpVerify;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./OtpAuthPlugin";
|
|
2
|
+
export * from "./OtpAuthPluginContext";
|
|
3
|
+
export * from "./OtpAuthPluginOptions";
|
|
4
|
+
|
|
5
|
+
// Functions
|
|
6
|
+
export * from "./functions/initiate";
|
|
7
|
+
export * from "./functions/verify";
|
|
8
|
+
|
|
9
|
+
// Schemas
|
|
10
|
+
export type { default as OtpSession } from "./schemas/OtpSession";
|
|
11
|
+
export type { default as InitiateRequest } from "./schemas/InitiateRequest";
|
|
12
|
+
export type { default as InitiateResponse } from "./schemas/InitiateResponse";
|
|
13
|
+
export type { default as VerifyRequest } from "./schemas/VerifyRequest";
|
|
14
|
+
export type { default as VerifyResponse } from "./schemas/VerifyResponse";
|
|
15
|
+
|
|
16
|
+
// Utils
|
|
17
|
+
export * from "./utils/otp-utils";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FlinkRepo, log } from "@flink-app/flink";
|
|
2
|
+
import OtpSession from "../schemas/OtpSession";
|
|
3
|
+
|
|
4
|
+
class OtpSessionRepo extends FlinkRepo<any, OtpSession> {
|
|
5
|
+
async getSession(sessionId: string) {
|
|
6
|
+
return this.getOne({ sessionId });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async createSession(session: Omit<OtpSession, "_id">) {
|
|
10
|
+
return await this.create(session);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async incrementAttempts(sessionId: string) {
|
|
14
|
+
const session = await this.getSession(sessionId);
|
|
15
|
+
if (!session) {
|
|
16
|
+
throw new Error("Session not found");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const newAttempts = session.attempts + 1;
|
|
20
|
+
const newStatus = newAttempts >= session.maxAttempts ? "locked" : session.status;
|
|
21
|
+
|
|
22
|
+
await this.collection.updateOne({ sessionId }, { $set: { attempts: newAttempts, status: newStatus } });
|
|
23
|
+
|
|
24
|
+
return this.getSession(sessionId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async markAsVerified(sessionId: string) {
|
|
28
|
+
await this.collection.updateOne({ sessionId }, { $set: { status: "verified", verifiedAt: new Date() } });
|
|
29
|
+
return this.getSession(sessionId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async markAsExpired(sessionId: string) {
|
|
33
|
+
await this.collection.updateOne({ sessionId }, { $set: { status: "expired" } });
|
|
34
|
+
return this.getSession(sessionId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async ensureExpiringIndex(ttlSec: number) {
|
|
38
|
+
try {
|
|
39
|
+
await this.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: ttlSec });
|
|
40
|
+
log.info(`OTP Auth Plugin: Created TTL index with ${ttlSec}s expiration`);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.error("Error creating expiring index for OTP sessions:", err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default OtpSessionRepo;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default interface InitiateRequest {
|
|
2
|
+
/** User identifier (phone number or email) */
|
|
3
|
+
identifier: string;
|
|
4
|
+
/** Delivery method for the OTP code */
|
|
5
|
+
method: "sms" | "email";
|
|
6
|
+
/** Optional custom payload to attach to the session */
|
|
7
|
+
payload?: Record<string, any>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default interface OtpSession {
|
|
2
|
+
_id?: string;
|
|
3
|
+
/** Unique identifier for the OTP session */
|
|
4
|
+
sessionId: string;
|
|
5
|
+
/** User identifier (phone number, email, or user ID) */
|
|
6
|
+
identifier: string;
|
|
7
|
+
/** The delivery method for this session */
|
|
8
|
+
method: "sms" | "email";
|
|
9
|
+
/** The generated OTP code */
|
|
10
|
+
code: string;
|
|
11
|
+
/** Number of verification attempts made */
|
|
12
|
+
attempts: number;
|
|
13
|
+
/** Maximum allowed attempts before session is locked */
|
|
14
|
+
maxAttempts: number;
|
|
15
|
+
/** Session status */
|
|
16
|
+
status: "pending" | "verified" | "expired" | "locked";
|
|
17
|
+
/** When the session was created */
|
|
18
|
+
createdAt: Date;
|
|
19
|
+
/** When the session expires */
|
|
20
|
+
expiresAt: Date;
|
|
21
|
+
/** When the session was verified (if verified) */
|
|
22
|
+
verifiedAt?: Date;
|
|
23
|
+
/** Optional custom payload passed during initiation */
|
|
24
|
+
payload?: Record<string, any>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default interface VerifyResponse {
|
|
2
|
+
/** Verification status */
|
|
3
|
+
status: "success" | "invalid_code" | "expired" | "locked" | "not_found";
|
|
4
|
+
/** JWT token if successful */
|
|
5
|
+
token?: string;
|
|
6
|
+
/** User object if successful */
|
|
7
|
+
user?: any;
|
|
8
|
+
/** Remaining attempts if code was invalid */
|
|
9
|
+
remainingAttempts?: number;
|
|
10
|
+
/** Error message */
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a random numeric OTP code.
|
|
5
|
+
*
|
|
6
|
+
* @param length - Number of digits (4-8)
|
|
7
|
+
* @returns A numeric string of the specified length
|
|
8
|
+
*/
|
|
9
|
+
export function generateOtpCode(length: number = 6): string {
|
|
10
|
+
if (length < 4 || length > 8) {
|
|
11
|
+
throw new Error("OTP code length must be between 4 and 8 digits");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Generate a random number with the specified number of digits
|
|
15
|
+
const min = Math.pow(10, length - 1);
|
|
16
|
+
const max = Math.pow(10, length) - 1;
|
|
17
|
+
|
|
18
|
+
// Use crypto.randomInt for cryptographically secure random numbers
|
|
19
|
+
return crypto.randomInt(min, max + 1).toString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generates a unique session ID.
|
|
24
|
+
*
|
|
25
|
+
* @returns A cryptographically secure random hex string
|
|
26
|
+
*/
|
|
27
|
+
export function generateSessionId(): string {
|
|
28
|
+
return crypto.randomBytes(16).toString("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates that an identifier looks like a valid phone number or email.
|
|
33
|
+
*
|
|
34
|
+
* @param identifier - The identifier to validate
|
|
35
|
+
* @param method - The expected delivery method
|
|
36
|
+
* @returns true if valid, false otherwise
|
|
37
|
+
*/
|
|
38
|
+
export function validateIdentifier(identifier: string, method: "sms" | "email"): boolean {
|
|
39
|
+
if (!identifier || typeof identifier !== "string") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (method === "email") {
|
|
44
|
+
// Basic email validation
|
|
45
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
46
|
+
return emailRegex.test(identifier);
|
|
47
|
+
} else {
|
|
48
|
+
// Phone number validation (allow various formats)
|
|
49
|
+
// This accepts: digits, spaces, dashes, parentheses, and + prefix
|
|
50
|
+
const phoneRegex = /^\+?[\d\s\-()]{8,20}$/;
|
|
51
|
+
return phoneRegex.test(identifier);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes a phone number by removing formatting characters.
|
|
57
|
+
*
|
|
58
|
+
* @param phoneNumber - The phone number to normalize
|
|
59
|
+
* @returns Normalized phone number (digits and + only)
|
|
60
|
+
*/
|
|
61
|
+
export function normalizePhoneNumber(phoneNumber: string): string {
|
|
62
|
+
// Remove all characters except digits and +
|
|
63
|
+
return phoneNumber.replace(/[^\d+]/g, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Normalizes an email by converting to lowercase and trimming.
|
|
68
|
+
*
|
|
69
|
+
* @param email - The email to normalize
|
|
70
|
+
* @returns Normalized email
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeEmail(email: string): string {
|
|
73
|
+
return email.toLowerCase().trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalizes an identifier based on the delivery method.
|
|
78
|
+
*
|
|
79
|
+
* @param identifier - The identifier to normalize
|
|
80
|
+
* @param method - The delivery method
|
|
81
|
+
* @returns Normalized identifier
|
|
82
|
+
*/
|
|
83
|
+
export function normalizeIdentifier(identifier: string, method: "sms" | "email"): string {
|
|
84
|
+
if (method === "email") {
|
|
85
|
+
return normalizeEmail(identifier);
|
|
86
|
+
} else {
|
|
87
|
+
return normalizePhoneNumber(identifier);
|
|
88
|
+
}
|
|
89
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": ["esnext", "es2016"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"module": "commonjs",
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": false,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"experimentalDecorators": true,
|
|
18
|
+
"checkJs": true,
|
|
19
|
+
"outDir": "dist",
|
|
20
|
+
"typeRoots": ["./node_modules/@types"]
|
|
21
|
+
},
|
|
22
|
+
"include": ["./src/*", "./spec/*"],
|
|
23
|
+
"exclude": ["./node_modules/*"]
|
|
24
|
+
}
|