@ratim818/allyve-wellness-backend 1.0.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/.env.example +49 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/publish.yml +81 -0
- package/README.md +140 -0
- package/docs/HIPAA_COMPLIANCE.md +141 -0
- package/docs/frontend-api-client.ts +259 -0
- package/package.json +60 -0
- package/src/config/database.ts +45 -0
- package/src/config/index.ts +52 -0
- package/src/middleware/auth.ts +167 -0
- package/src/middleware/security.ts +101 -0
- package/src/migrations/rollback.ts +17 -0
- package/src/migrations/run.ts +17 -0
- package/src/migrations/schema.ts +339 -0
- package/src/migrations/seed.ts +159 -0
- package/src/routes/appointments.ts +293 -0
- package/src/routes/audit.ts +29 -0
- package/src/routes/auth.ts +141 -0
- package/src/routes/health.ts +387 -0
- package/src/server.ts +124 -0
- package/src/services/audit.ts +117 -0
- package/src/services/auth.ts +293 -0
- package/src/services/encryption.ts +76 -0
- package/src/utils/logger.ts +57 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import { db } from "../config/database.js";
|
|
5
|
+
import { config } from "../config/index.js";
|
|
6
|
+
import { encrypt, decrypt } from "./encryption.js";
|
|
7
|
+
import { writeAuditLog } from "./audit.js";
|
|
8
|
+
import { logger } from "../utils/logger.js";
|
|
9
|
+
|
|
10
|
+
// ─── HIPAA AUTH REQUIREMENTS ────────────────────────────────
|
|
11
|
+
// §164.312(a)(1) — Unique user identification
|
|
12
|
+
// §164.312(a)(2)(i) — Unique user ID for tracking
|
|
13
|
+
// §164.312(d) — Person or entity authentication
|
|
14
|
+
// §164.312(a)(2)(iii) — Automatic logoff (handled by session middleware)
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const BCRYPT_ROUNDS = 12; // NIST recommends ≥10 for sensitive systems
|
|
18
|
+
const MAX_LOGIN_ATTEMPTS = 5;
|
|
19
|
+
const LOCKOUT_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
|
20
|
+
|
|
21
|
+
export interface TokenPayload {
|
|
22
|
+
userId: string;
|
|
23
|
+
email: string;
|
|
24
|
+
role: string;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AuthTokens {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
refreshToken: string;
|
|
31
|
+
expiresIn: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register a new user with encrypted PII and hashed password.
|
|
36
|
+
*/
|
|
37
|
+
export async function registerUser(data: {
|
|
38
|
+
email: string;
|
|
39
|
+
password: string;
|
|
40
|
+
fullName: string;
|
|
41
|
+
dateOfBirth: string;
|
|
42
|
+
phone?: string;
|
|
43
|
+
}): Promise<{ userId: string }> {
|
|
44
|
+
// Check if email already exists (using HMAC hash for lookup without decrypting all emails)
|
|
45
|
+
const { hmacHash } = await import("./encryption.js");
|
|
46
|
+
const emailHash = hmacHash(data.email.toLowerCase());
|
|
47
|
+
|
|
48
|
+
const existing = await db("users").where("email_hash", emailHash).first();
|
|
49
|
+
if (existing) {
|
|
50
|
+
throw new Error("An account with this email already exists");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const userId = uuidv4();
|
|
54
|
+
const passwordHash = await bcrypt.hash(data.password, BCRYPT_ROUNDS);
|
|
55
|
+
|
|
56
|
+
// Encrypt all PII/PHI at rest
|
|
57
|
+
await db("users").insert({
|
|
58
|
+
id: userId,
|
|
59
|
+
email_encrypted: encrypt(data.email.toLowerCase()),
|
|
60
|
+
email_hash: emailHash, // For lookups without decrypting every row
|
|
61
|
+
password_hash: passwordHash,
|
|
62
|
+
full_name_encrypted: encrypt(data.fullName),
|
|
63
|
+
date_of_birth_encrypted: encrypt(data.dateOfBirth),
|
|
64
|
+
phone_encrypted: data.phone ? encrypt(data.phone) : null,
|
|
65
|
+
role: "patient",
|
|
66
|
+
is_active: true,
|
|
67
|
+
login_attempts: 0,
|
|
68
|
+
created_at: new Date(),
|
|
69
|
+
updated_at: new Date(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await writeAuditLog({
|
|
73
|
+
user_id: userId,
|
|
74
|
+
action: "CREATE",
|
|
75
|
+
resource_type: "user",
|
|
76
|
+
resource_id: userId,
|
|
77
|
+
outcome: "success",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
logger.info(`User registered: ${userId}`);
|
|
81
|
+
return { userId };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Authenticate a user and return JWT tokens.
|
|
86
|
+
*/
|
|
87
|
+
export async function loginUser(
|
|
88
|
+
email: string,
|
|
89
|
+
password: string,
|
|
90
|
+
ipAddress?: string,
|
|
91
|
+
userAgent?: string
|
|
92
|
+
): Promise<AuthTokens> {
|
|
93
|
+
const { hmacHash } = await import("./encryption.js");
|
|
94
|
+
const emailHash = hmacHash(email.toLowerCase());
|
|
95
|
+
|
|
96
|
+
const user = await db("users").where("email_hash", emailHash).first();
|
|
97
|
+
|
|
98
|
+
if (!user) {
|
|
99
|
+
await writeAuditLog({
|
|
100
|
+
user_id: null,
|
|
101
|
+
action: "LOGIN_FAILED",
|
|
102
|
+
resource_type: "session",
|
|
103
|
+
details: { reason: "user_not_found" },
|
|
104
|
+
ip_address: ipAddress,
|
|
105
|
+
user_agent: userAgent,
|
|
106
|
+
outcome: "failure",
|
|
107
|
+
});
|
|
108
|
+
throw new Error("Invalid email or password");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check account lockout
|
|
112
|
+
if (user.locked_until && new Date(user.locked_until) > new Date()) {
|
|
113
|
+
await writeAuditLog({
|
|
114
|
+
user_id: user.id,
|
|
115
|
+
action: "LOGIN_FAILED",
|
|
116
|
+
resource_type: "session",
|
|
117
|
+
details: { reason: "account_locked" },
|
|
118
|
+
ip_address: ipAddress,
|
|
119
|
+
user_agent: userAgent,
|
|
120
|
+
outcome: "denied",
|
|
121
|
+
});
|
|
122
|
+
throw new Error("Account is temporarily locked. Please try again later.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify password
|
|
126
|
+
const isValid = await bcrypt.compare(password, user.password_hash);
|
|
127
|
+
if (!isValid) {
|
|
128
|
+
const attempts = (user.login_attempts || 0) + 1;
|
|
129
|
+
const updates: Record<string, unknown> = {
|
|
130
|
+
login_attempts: attempts,
|
|
131
|
+
updated_at: new Date(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Lock after MAX_LOGIN_ATTEMPTS
|
|
135
|
+
if (attempts >= MAX_LOGIN_ATTEMPTS) {
|
|
136
|
+
updates.locked_until = new Date(Date.now() + LOCKOUT_DURATION_MS);
|
|
137
|
+
logger.warn(`Account locked for user ${user.id} after ${attempts} failed attempts`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await db("users").where("id", user.id).update(updates);
|
|
141
|
+
|
|
142
|
+
await writeAuditLog({
|
|
143
|
+
user_id: user.id,
|
|
144
|
+
action: "LOGIN_FAILED",
|
|
145
|
+
resource_type: "session",
|
|
146
|
+
details: { reason: "invalid_password", attempt: attempts },
|
|
147
|
+
ip_address: ipAddress,
|
|
148
|
+
user_agent: userAgent,
|
|
149
|
+
outcome: "failure",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
throw new Error("Invalid email or password");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Reset login attempts on success
|
|
156
|
+
await db("users").where("id", user.id).update({
|
|
157
|
+
login_attempts: 0,
|
|
158
|
+
locked_until: null,
|
|
159
|
+
last_login: new Date(),
|
|
160
|
+
updated_at: new Date(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Create session
|
|
164
|
+
const sessionId = uuidv4();
|
|
165
|
+
await db("sessions").insert({
|
|
166
|
+
id: sessionId,
|
|
167
|
+
user_id: user.id,
|
|
168
|
+
ip_address: ipAddress,
|
|
169
|
+
user_agent: userAgent,
|
|
170
|
+
expires_at: new Date(Date.now() + config.session.maxAge),
|
|
171
|
+
created_at: new Date(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Generate tokens
|
|
175
|
+
const payload: TokenPayload = {
|
|
176
|
+
userId: user.id,
|
|
177
|
+
email: email.toLowerCase(),
|
|
178
|
+
role: user.role,
|
|
179
|
+
sessionId,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const accessToken = jwt.sign(payload, config.jwt.secret, {
|
|
183
|
+
expiresIn: config.jwt.expiresIn,
|
|
184
|
+
issuer: "allyve-wellness",
|
|
185
|
+
audience: "allyve-frontend",
|
|
186
|
+
} as jwt.SignOptions);
|
|
187
|
+
|
|
188
|
+
const refreshToken = jwt.sign(
|
|
189
|
+
{ userId: user.id, sessionId, type: "refresh" },
|
|
190
|
+
config.jwt.secret,
|
|
191
|
+
{ expiresIn: config.jwt.refreshExpiresIn } as jwt.SignOptions
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Store refresh token hash
|
|
195
|
+
const refreshHash = (await import("./encryption.js")).hmacHash(refreshToken);
|
|
196
|
+
await db("refresh_tokens").insert({
|
|
197
|
+
id: uuidv4(),
|
|
198
|
+
user_id: user.id,
|
|
199
|
+
session_id: sessionId,
|
|
200
|
+
token_hash: refreshHash,
|
|
201
|
+
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
202
|
+
created_at: new Date(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await writeAuditLog({
|
|
206
|
+
user_id: user.id,
|
|
207
|
+
action: "LOGIN",
|
|
208
|
+
resource_type: "session",
|
|
209
|
+
resource_id: sessionId,
|
|
210
|
+
ip_address: ipAddress,
|
|
211
|
+
user_agent: userAgent,
|
|
212
|
+
outcome: "success",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
accessToken,
|
|
217
|
+
refreshToken,
|
|
218
|
+
expiresIn: config.jwt.expiresIn,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Refresh an access token using a valid refresh token.
|
|
224
|
+
*/
|
|
225
|
+
export async function refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
|
|
226
|
+
const { hmacHash } = await import("./encryption.js");
|
|
227
|
+
|
|
228
|
+
// Verify the JWT
|
|
229
|
+
let decoded: jwt.JwtPayload;
|
|
230
|
+
try {
|
|
231
|
+
decoded = jwt.verify(refreshToken, config.jwt.secret) as jwt.JwtPayload;
|
|
232
|
+
} catch {
|
|
233
|
+
throw new Error("Invalid refresh token");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (decoded.type !== "refresh") throw new Error("Invalid token type");
|
|
237
|
+
|
|
238
|
+
// Check token exists in DB and isn't revoked
|
|
239
|
+
const tokenHash = hmacHash(refreshToken);
|
|
240
|
+
const storedToken = await db("refresh_tokens")
|
|
241
|
+
.where("token_hash", tokenHash)
|
|
242
|
+
.where("revoked", false)
|
|
243
|
+
.first();
|
|
244
|
+
|
|
245
|
+
if (!storedToken || new Date(storedToken.expires_at) < new Date()) {
|
|
246
|
+
throw new Error("Refresh token expired or revoked");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Get user
|
|
250
|
+
const user = await db("users").where("id", decoded.userId).first();
|
|
251
|
+
if (!user || !user.is_active) throw new Error("User not found or inactive");
|
|
252
|
+
|
|
253
|
+
// Generate new access token
|
|
254
|
+
const payload: TokenPayload = {
|
|
255
|
+
userId: user.id,
|
|
256
|
+
email: decrypt(user.email_encrypted),
|
|
257
|
+
role: user.role,
|
|
258
|
+
sessionId: decoded.sessionId as string,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const newAccessToken = jwt.sign(payload, config.jwt.secret, {
|
|
262
|
+
expiresIn: config.jwt.expiresIn,
|
|
263
|
+
issuer: "allyve-wellness",
|
|
264
|
+
audience: "allyve-frontend",
|
|
265
|
+
} as jwt.SignOptions);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
accessToken: newAccessToken,
|
|
269
|
+
refreshToken, // Reuse existing refresh token
|
|
270
|
+
expiresIn: config.jwt.expiresIn,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Logout — revoke session and refresh tokens.
|
|
276
|
+
*/
|
|
277
|
+
export async function logoutUser(
|
|
278
|
+
userId: string,
|
|
279
|
+
sessionId: string,
|
|
280
|
+
ipAddress?: string
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
await db("sessions").where("id", sessionId).update({ revoked: true });
|
|
283
|
+
await db("refresh_tokens").where("session_id", sessionId).update({ revoked: true });
|
|
284
|
+
|
|
285
|
+
await writeAuditLog({
|
|
286
|
+
user_id: userId,
|
|
287
|
+
action: "LOGOUT",
|
|
288
|
+
resource_type: "session",
|
|
289
|
+
resource_id: sessionId,
|
|
290
|
+
ip_address: ipAddress,
|
|
291
|
+
outcome: "success",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { config } from "../config/index.js";
|
|
3
|
+
|
|
4
|
+
// ─── AES-256-GCM ENCRYPTION FOR PHI AT REST ────────────────
|
|
5
|
+
// HIPAA §164.312(a)(2)(iv) — Encryption and decryption
|
|
6
|
+
// Uses AES-256-GCM which provides both confidentiality AND integrity
|
|
7
|
+
// Each field is encrypted with a unique IV (initialization vector)
|
|
8
|
+
// ─────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const ALGORITHM = "aes-256-gcm";
|
|
11
|
+
const KEY = Buffer.from(config.encryption.key, "hex");
|
|
12
|
+
const IV_LENGTH = config.encryption.ivLength;
|
|
13
|
+
const AUTH_TAG_LENGTH = 16;
|
|
14
|
+
|
|
15
|
+
if (KEY.length !== 32) {
|
|
16
|
+
throw new Error("ENCRYPTION_KEY must be 32 bytes (64 hex chars). Generate with: openssl rand -hex 32");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encrypt a plaintext string. Returns: iv:authTag:ciphertext (all hex-encoded)
|
|
21
|
+
* Each encryption uses a random IV, so identical inputs produce different outputs.
|
|
22
|
+
*/
|
|
23
|
+
export function encrypt(plaintext: string): string {
|
|
24
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
25
|
+
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
26
|
+
|
|
27
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
28
|
+
encrypted += cipher.final("hex");
|
|
29
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
30
|
+
|
|
31
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decrypt a string produced by encrypt(). Verifies integrity via GCM auth tag.
|
|
36
|
+
*/
|
|
37
|
+
export function decrypt(encryptedData: string): string {
|
|
38
|
+
const parts = encryptedData.split(":");
|
|
39
|
+
if (parts.length !== 3) {
|
|
40
|
+
throw new Error("Invalid encrypted data format");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [ivHex, authTagHex, ciphertextHex] = parts;
|
|
44
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
45
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
46
|
+
|
|
47
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
48
|
+
decipher.setAuthTag(authTag);
|
|
49
|
+
|
|
50
|
+
let decrypted = decipher.update(ciphertextHex, "hex", "utf8");
|
|
51
|
+
decrypted += decipher.final("utf8");
|
|
52
|
+
|
|
53
|
+
return decrypted;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Encrypt a JSON-serializable object. Used for structured PHI like health metrics.
|
|
58
|
+
*/
|
|
59
|
+
export function encryptJSON(data: unknown): string {
|
|
60
|
+
return encrypt(JSON.stringify(data));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt and parse a JSON object.
|
|
65
|
+
*/
|
|
66
|
+
export function decryptJSON<T = unknown>(encryptedData: string): T {
|
|
67
|
+
return JSON.parse(decrypt(encryptedData)) as T;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hash data one-way (for indexing encrypted fields without revealing values).
|
|
72
|
+
* Uses HMAC-SHA256 so the hash is deterministic but requires the key to reproduce.
|
|
73
|
+
*/
|
|
74
|
+
export function hmacHash(data: string): string {
|
|
75
|
+
return crypto.createHmac("sha256", KEY).update(data).digest("hex");
|
|
76
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import winston from "winston";
|
|
2
|
+
|
|
3
|
+
// ─── HIPAA LOGGING RULES ────────────────────────────────────
|
|
4
|
+
// 1. NEVER log PHI (Protected Health Information)
|
|
5
|
+
// 2. NEVER log passwords, tokens, or encryption keys
|
|
6
|
+
// 3. DO log: timestamps, user IDs, action types, resource types
|
|
7
|
+
// 4. Retain audit logs for 6 years minimum (HIPAA §164.530(j))
|
|
8
|
+
// 5. All log files must be on encrypted storage
|
|
9
|
+
// ─────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const logFormat = winston.format.combine(
|
|
12
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
|
|
13
|
+
winston.format.errors({ stack: true }),
|
|
14
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
15
|
+
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
|
16
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const logger = winston.createLogger({
|
|
21
|
+
level: process.env.AUDIT_LOG_LEVEL || "info",
|
|
22
|
+
format: logFormat,
|
|
23
|
+
defaultMeta: { service: "allyve-backend" },
|
|
24
|
+
transports: [
|
|
25
|
+
// Console output (development)
|
|
26
|
+
new winston.transports.Console({
|
|
27
|
+
format: winston.format.combine(winston.format.colorize(), logFormat),
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
// Application logs — rotated, no PHI
|
|
31
|
+
new winston.transports.File({
|
|
32
|
+
filename: "logs/app.log",
|
|
33
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
34
|
+
maxFiles: 30,
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
// Error logs — separate file for alerts
|
|
38
|
+
new winston.transports.File({
|
|
39
|
+
filename: "logs/error.log",
|
|
40
|
+
level: "error",
|
|
41
|
+
maxsize: 10 * 1024 * 1024,
|
|
42
|
+
maxFiles: 90,
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
// HIPAA Audit trail — separate, long-retention
|
|
46
|
+
new winston.transports.File({
|
|
47
|
+
filename: "logs/audit.log",
|
|
48
|
+
level: "info",
|
|
49
|
+
maxsize: 50 * 1024 * 1024,
|
|
50
|
+
maxFiles: 365, // Keep 1 year of files on disk; archive older to S3/GCS
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Ensure log directory exists
|
|
56
|
+
import { mkdirSync } from "fs";
|
|
57
|
+
try { mkdirSync("logs", { recursive: true }); } catch { /* exists */ }
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"baseUrl": ".",
|
|
18
|
+
"paths": {
|
|
19
|
+
"@/*": ["src/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*"],
|
|
23
|
+
"exclude": ["node_modules", "dist"]
|
|
24
|
+
}
|