@kkuffour/solid-moderation-plugin 0.1.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.
Files changed (65) hide show
  1. package/CONFIG-GUIDE.md +49 -0
  2. package/DEVELOPMENT.md +129 -0
  3. package/ENV-VARIABLES.md +137 -0
  4. package/INSTALLATION.md +90 -0
  5. package/LICENSE +21 -0
  6. package/MIGRATION.md +81 -0
  7. package/PRODUCTION.md +186 -0
  8. package/PUBLISHING.md +104 -0
  9. package/README.md +53 -0
  10. package/TESTING.md +64 -0
  11. package/components/components.jsonld +17 -0
  12. package/components/context.jsonld +211 -0
  13. package/config/context.jsonld +15 -0
  14. package/config/default.json +80 -0
  15. package/dist/ModerationConfig.d.ts +16 -0
  16. package/dist/ModerationConfig.d.ts.map +1 -0
  17. package/dist/ModerationConfig.js +18 -0
  18. package/dist/ModerationConfig.js.map +1 -0
  19. package/dist/ModerationConfig.jsonld +66 -0
  20. package/dist/ModerationMixin.d.ts +13 -0
  21. package/dist/ModerationMixin.d.ts.map +1 -0
  22. package/dist/ModerationMixin.js +136 -0
  23. package/dist/ModerationMixin.js.map +1 -0
  24. package/dist/ModerationMixin.jsonld +180 -0
  25. package/dist/ModerationOperationHandler.d.ts +16 -0
  26. package/dist/ModerationOperationHandler.d.ts.map +1 -0
  27. package/dist/ModerationOperationHandler.js +45 -0
  28. package/dist/ModerationOperationHandler.js.map +1 -0
  29. package/dist/ModerationOperationHandler.jsonld +140 -0
  30. package/dist/ModerationRecord.d.ts +20 -0
  31. package/dist/ModerationRecord.d.ts.map +1 -0
  32. package/dist/ModerationRecord.js +3 -0
  33. package/dist/ModerationRecord.js.map +1 -0
  34. package/dist/ModerationRecord.jsonld +59 -0
  35. package/dist/ModerationStore.d.ts +12 -0
  36. package/dist/ModerationStore.d.ts.map +1 -0
  37. package/dist/ModerationStore.js +37 -0
  38. package/dist/ModerationStore.js.map +1 -0
  39. package/dist/ModerationStore.jsonld +59 -0
  40. package/dist/components/components.jsonld +17 -0
  41. package/dist/components/context.jsonld +211 -0
  42. package/dist/index.d.ts +7 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +23 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/providers/SightEngineProvider.d.ts +52 -0
  47. package/dist/providers/SightEngineProvider.d.ts.map +1 -0
  48. package/dist/providers/SightEngineProvider.js +302 -0
  49. package/dist/providers/SightEngineProvider.js.map +1 -0
  50. package/dist/providers/SightEngineProvider.jsonld +209 -0
  51. package/dist/util/GuardedStream.d.ts +33 -0
  52. package/dist/util/GuardedStream.d.ts.map +1 -0
  53. package/dist/util/GuardedStream.js +89 -0
  54. package/dist/util/GuardedStream.js.map +1 -0
  55. package/package.json +40 -0
  56. package/simple-test.json +7 -0
  57. package/src/ModerationConfig.ts +29 -0
  58. package/src/ModerationMixin.ts +153 -0
  59. package/src/ModerationOperationHandler.ts +64 -0
  60. package/src/ModerationRecord.ts +19 -0
  61. package/src/ModerationStore.ts +41 -0
  62. package/src/index.ts +6 -0
  63. package/src/providers/SightEngineProvider.ts +367 -0
  64. package/src/util/GuardedStream.ts +101 -0
  65. package/tsconfig.json +20 -0
@@ -0,0 +1,367 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { getLoggerFor } from '@solid/community-server';
3
+
4
+ interface ApiResponse {
5
+ nudity?: { raw: number };
6
+ violence?: { prob?: number } | number;
7
+ gore?: { prob?: number } | number;
8
+ weapon?: { prob?: number } | number;
9
+ alcohol?: { prob?: number } | number;
10
+ offensive?: { prob?: number } | number;
11
+ selfHarm?: { prob?: number } | number;
12
+ gambling?: { prob?: number } | number;
13
+ recreationalDrug?: { prob?: number } | number;
14
+ tobacco?: { prob?: number } | number;
15
+ profanity?: { matches?: Match[] };
16
+ personal?: { matches?: unknown[] };
17
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- API response field name
18
+ 'self-harm'?: { prob?: number } | number;
19
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- API response field name
20
+ recreational_drug?: { prob?: number } | number;
21
+ }
22
+
23
+ interface Match {
24
+ type: string;
25
+ intensity: 'high' | 'medium' | 'low';
26
+ }
27
+
28
+ export interface SightEngineResult {
29
+ nudity?: { raw: number };
30
+ violence: number;
31
+ gore: number;
32
+ weapon: number;
33
+ alcohol: number;
34
+ drugs: number;
35
+ offensive: number;
36
+ selfharm: number;
37
+ gambling: number;
38
+ profanity: number;
39
+ personalInfo: number;
40
+ }
41
+
42
+ export interface SightEngineTextResult {
43
+ sexual: number;
44
+ discriminatory: number;
45
+ insulting: number;
46
+ violent: number;
47
+ toxic: number;
48
+ selfharm: number;
49
+ personalInfo: number;
50
+ }
51
+
52
+ export interface SightEngineVideoResult {
53
+ nudity?: { raw: number };
54
+ violence: number;
55
+ gore: number;
56
+ weapon: number;
57
+ alcohol: number;
58
+ offensive: number;
59
+ selfharm: number;
60
+ gambling: number;
61
+ drugs: number;
62
+ tobacco: number;
63
+ }
64
+
65
+ /**
66
+ * SightEngine implementation of content moderation provider.
67
+ */
68
+ export class SightEngineProvider {
69
+ protected readonly logger = getLoggerFor(this);
70
+
71
+ private readonly apiUser: string;
72
+ private readonly apiSecret: string;
73
+
74
+ public constructor(apiUser: string, apiSecret: string) {
75
+ this.apiUser = apiUser;
76
+ this.apiSecret = apiSecret;
77
+ }
78
+
79
+ public async analyzeImage(filePath: string): Promise<SightEngineResult> {
80
+ this.logger.info(`SIGHTENGINE: Starting analysis for ${filePath}`);
81
+
82
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- External module class name
83
+ const FormData = (await import('form-data')).default;
84
+
85
+ this.logger.info(`SIGHTENGINE: Creating form data for ${filePath}`);
86
+
87
+ const form = new FormData();
88
+ form.append('media', createReadStream(filePath));
89
+ form.append('models', 'nudity-2.1,violence,gore,weapon,alcohol,offensive,self-harm,gambling');
90
+ form.append('api_user', this.apiUser);
91
+ form.append('api_secret', this.apiSecret);
92
+
93
+ this.logger.info(`SIGHTENGINE: Sending request to API for ${filePath}`);
94
+ const startTime = Date.now();
95
+
96
+ const result = await new Promise<ApiResponse>((resolve, reject): void => {
97
+ form.submit('https://api.sightengine.com/1.0/check.json', (err, res): void => {
98
+ if (err) {
99
+ reject(err);
100
+ return;
101
+ }
102
+
103
+ let data = '';
104
+ res.on('data', (chunk: Buffer): void => {
105
+ data += chunk.toString();
106
+ });
107
+ res.on('end', (): void => {
108
+ const requestTime = Date.now() - startTime;
109
+ this.logger.info(`SIGHTENGINE: API response received in ${requestTime}ms for ${filePath}`);
110
+
111
+ if (res.statusCode !== 200) {
112
+ this.logger.error(`SIGHTENGINE: API error ${res.statusCode} ${data} for ${filePath}`);
113
+ reject(new Error(`SightEngine API error: ${res.statusCode} ${data}`));
114
+ return;
115
+ }
116
+
117
+ this.logger.info(`SIGHTENGINE: Parsing JSON response for ${filePath}`);
118
+ try {
119
+ resolve(JSON.parse(data) as ApiResponse);
120
+ } catch (parseErr) {
121
+ reject(new Error(`Failed to parse response: ${String(parseErr)}`));
122
+ }
123
+ });
124
+ });
125
+ });
126
+
127
+ const analysisResult = {
128
+ nudity: result.nudity,
129
+ violence: result.violence && typeof result.violence === 'object' ?
130
+ result.violence.prob ?? 0 :
131
+ result.violence ?? 0,
132
+ gore: result.gore && typeof result.gore === 'object' ?
133
+ result.gore.prob ?? 0 :
134
+ result.gore ?? 0,
135
+ weapon: typeof result.weapon === 'object' && result.weapon ?
136
+ result.weapon.prob ?? 0 :
137
+ result.weapon ?? 0,
138
+ alcohol: result.alcohol && typeof result.alcohol === 'object' ?
139
+ result.alcohol.prob ?? 0 :
140
+ result.alcohol ?? 0,
141
+ drugs: 0,
142
+ offensive: result.offensive && typeof result.offensive === 'object' ?
143
+ result.offensive.prob ?? 0 :
144
+ result.offensive ?? 0,
145
+ selfharm: result['self-harm'] && typeof result['self-harm'] === 'object' ?
146
+ result['self-harm'].prob ?? 0 :
147
+ result['self-harm'] ?? 0,
148
+ gambling: result.gambling && typeof result.gambling === 'object' ?
149
+ result.gambling.prob ?? 0 :
150
+ result.gambling ?? 0,
151
+ profanity: 0,
152
+ personalInfo: 0,
153
+ };
154
+
155
+ this.logger.info(`SIGHTENGINE: Analysis complete for ${filePath}`);
156
+ const logMsg = `SIGHTENGINE: Results - Nudity: ${analysisResult.nudity?.raw ?? 0}, ` +
157
+ `Violence: ${analysisResult.violence}, Gore: ${analysisResult.gore}, ` +
158
+ `Weapon: ${analysisResult.weapon}, Alcohol: ${analysisResult.alcohol}, ` +
159
+ `Drugs: ${analysisResult.drugs}, Offensive: ${analysisResult.offensive}, ` +
160
+ `Self-harm: ${analysisResult.selfharm}, Gambling: ${analysisResult.gambling}, ` +
161
+ `Profanity: ${analysisResult.profanity}, Personal Info: ${analysisResult.personalInfo}`;
162
+ this.logger.info(logMsg);
163
+
164
+ return analysisResult;
165
+ }
166
+
167
+ public async analyzeText(text: string): Promise<SightEngineTextResult> {
168
+ this.logger.info('SIGHTENGINE: Starting text analysis');
169
+
170
+ // Skip analysis for empty or very short text
171
+ if (!text || text.trim().length < 3) {
172
+ this.logger.info(`SIGHTENGINE: Skipping analysis for empty/short text (length: ${text?.length ?? 0})`);
173
+ return {
174
+ sexual: 0,
175
+ discriminatory: 0,
176
+ insulting: 0,
177
+ violent: 0,
178
+ toxic: 0,
179
+ selfharm: 0,
180
+ personalInfo: 0,
181
+ };
182
+ }
183
+
184
+ const params = new URLSearchParams();
185
+ params.append('text', text);
186
+ params.append('models', 'text-content,personal-info');
187
+ params.append('mode', 'standard');
188
+ params.append('lang', 'en,es,fr,de,it,pt,nl,pl,ru');
189
+ params.append('api_user', this.apiUser);
190
+ params.append('api_secret', this.apiSecret);
191
+
192
+ this.logger.info('SIGHTENGINE: Sending text request to API');
193
+ const startTime = Date.now();
194
+
195
+ const response = await fetch('https://api.sightengine.com/1.0/text/check.json', {
196
+ method: 'POST',
197
+ headers: {
198
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name
199
+ 'content-type': 'application/x-www-form-urlencoded',
200
+ },
201
+ body: params,
202
+ });
203
+
204
+ const requestTime = Date.now() - startTime;
205
+ this.logger.info(`SIGHTENGINE: Text API response received in ${requestTime}ms`);
206
+
207
+ if (!response.ok) {
208
+ const errorText = await response.text();
209
+ this.logger.error(`SIGHTENGINE: Text API error ${response.status} ${errorText}`);
210
+ throw new Error(`SightEngine Text API error: ${response.status} ${errorText}`);
211
+ }
212
+
213
+ this.logger.info('SIGHTENGINE: Parsing text JSON response');
214
+ const result = await response.json() as ApiResponse;
215
+
216
+ // Log the raw API response for debugging
217
+ this.logger.debug(`SIGHTENGINE: Raw API response: ${JSON.stringify(result)}`);
218
+
219
+ const analysisResult = {
220
+ sexual: this.getMatchScore(result.profanity?.matches ?? [], 'sexual'),
221
+ discriminatory: this.getMatchScore(result.profanity?.matches ?? [], 'discriminatory'),
222
+ insulting: this.getMatchScore(result.profanity?.matches ?? [], 'insulting'),
223
+ violent: this.getMatchScore(result.profanity?.matches ?? [], 'violent'),
224
+ toxic: this.getMatchScore(result.profanity?.matches ?? [], 'toxic'),
225
+ selfharm: this.getMatchScore(result.profanity?.matches ?? [], 'self-harm'),
226
+ personalInfo: result.personal?.matches && result.personal.matches.length > 0 ? 1 : 0,
227
+ };
228
+
229
+ // If all scores are 0, log a warning
230
+ let totalScore = 0;
231
+ for (const score of Object.values(analysisResult)) {
232
+ totalScore += score;
233
+ }
234
+ if (totalScore === 0) {
235
+ this.logger.warn('SIGHTENGINE: All moderation scores are 0 - this may indicate an API issue');
236
+ }
237
+
238
+ this.logger.info('SIGHTENGINE: Text analysis complete');
239
+ const logMsg = `SIGHTENGINE: Text Results - Sexual: ${analysisResult.sexual}, ` +
240
+ `Discriminatory: ${analysisResult.discriminatory}, Insulting: ${analysisResult.insulting}, ` +
241
+ `Violent: ${analysisResult.violent}, Toxic: ${analysisResult.toxic}, ` +
242
+ `Self-harm: ${analysisResult.selfharm}, Personal Info: ${analysisResult.personalInfo}`;
243
+ this.logger.info(logMsg);
244
+
245
+ return analysisResult;
246
+ }
247
+
248
+ private getMatchScore(matches: Match[], type: string): number {
249
+ if (!matches || matches.length === 0) {
250
+ return 0;
251
+ }
252
+
253
+ const typeMatches = matches.filter((match): boolean => match.type === type);
254
+ if (typeMatches.length === 0) {
255
+ return 0;
256
+ }
257
+
258
+ // Convert intensity to score: high=1.0, medium=0.7, low=0.4
259
+ let maxIntensity = 0;
260
+ for (const match of typeMatches) {
261
+ let score: number;
262
+ if (match.intensity === 'high') {
263
+ score = 1;
264
+ } else if (match.intensity === 'medium') {
265
+ score = 0.7;
266
+ } else {
267
+ score = 0.4;
268
+ }
269
+ maxIntensity = Math.max(maxIntensity, score);
270
+ }
271
+
272
+ return maxIntensity;
273
+ }
274
+
275
+ public async analyzeVideo(filePath: string): Promise<SightEngineVideoResult> {
276
+ this.logger.info(`SIGHTENGINE: Starting video analysis for ${filePath}`);
277
+
278
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- External module class name
279
+ const FormData = (await import('form-data')).default;
280
+
281
+ this.logger.info(`SIGHTENGINE: Creating form data for video ${filePath}`);
282
+
283
+ const form = new FormData();
284
+ form.append('media', createReadStream(filePath));
285
+ const models = 'nudity-2.1,violence,gore-2.0,weapon,alcohol,offensive,self-harm,' +
286
+ 'gambling,recreational_drug,tobacco';
287
+ form.append('models', models);
288
+ form.append('api_user', this.apiUser);
289
+ form.append('api_secret', this.apiSecret);
290
+
291
+ this.logger.info(`SIGHTENGINE: Sending video request to API for ${filePath}`);
292
+ const startTime = Date.now();
293
+
294
+ const result = await new Promise<ApiResponse>((resolve, reject): void => {
295
+ form.submit('https://api.sightengine.com/1.0/video/check.json', (err, res): void => {
296
+ if (err) {
297
+ reject(err);
298
+ return;
299
+ }
300
+
301
+ let data = '';
302
+ res.on('data', (chunk: Buffer): void => {
303
+ data += chunk.toString();
304
+ });
305
+ res.on('end', (): void => {
306
+ const requestTime = Date.now() - startTime;
307
+ this.logger.info(`SIGHTENGINE: Video API response received in ${requestTime}ms for ${filePath}`);
308
+
309
+ if (res.statusCode !== 200) {
310
+ this.logger.error(`SIGHTENGINE: Video API error ${res.statusCode} ${data} for ${filePath}`);
311
+ reject(new Error(`SightEngine Video API error: ${res.statusCode} ${data}`));
312
+ return;
313
+ }
314
+
315
+ this.logger.info(`SIGHTENGINE: Parsing video JSON response for ${filePath}`);
316
+ try {
317
+ resolve(JSON.parse(data) as ApiResponse);
318
+ } catch (parseErr) {
319
+ reject(new Error(`Failed to parse response: ${String(parseErr)}`));
320
+ }
321
+ });
322
+ });
323
+ });
324
+
325
+ const analysisResult = {
326
+ nudity: result.nudity,
327
+ violence: result.violence && typeof result.violence === 'object' ?
328
+ result.violence.prob ?? 0 :
329
+ result.violence ?? 0,
330
+ gore: result.gore && typeof result.gore === 'object' ?
331
+ result.gore.prob ?? 0 :
332
+ result.gore ?? 0,
333
+ weapon: typeof result.weapon === 'object' && result.weapon ?
334
+ result.weapon.prob ?? 0 :
335
+ result.weapon ?? 0,
336
+ alcohol: result.alcohol && typeof result.alcohol === 'object' ?
337
+ result.alcohol.prob ?? 0 :
338
+ result.alcohol ?? 0,
339
+ offensive: result.offensive && typeof result.offensive === 'object' ?
340
+ result.offensive.prob ?? 0 :
341
+ result.offensive ?? 0,
342
+ selfharm: result['self-harm'] && typeof result['self-harm'] === 'object' ?
343
+ result['self-harm'].prob ?? 0 :
344
+ result['self-harm'] ?? 0,
345
+ gambling: result.gambling && typeof result.gambling === 'object' ?
346
+ result.gambling.prob ?? 0 :
347
+ result.gambling ?? 0,
348
+ drugs: result.recreational_drug && typeof result.recreational_drug === 'object' ?
349
+ result.recreational_drug.prob ?? 0 :
350
+ result.recreational_drug ?? 0,
351
+ tobacco: result.tobacco && typeof result.tobacco === 'object' ?
352
+ result.tobacco.prob ?? 0 :
353
+ result.tobacco ?? 0,
354
+ };
355
+
356
+ this.logger.info(`SIGHTENGINE: Video analysis complete for ${filePath}`);
357
+ const logMsg = `SIGHTENGINE: Video Results - Nudity: ${analysisResult.nudity?.raw ?? 0}, ` +
358
+ `Violence: ${analysisResult.violence}, Gore: ${analysisResult.gore}, ` +
359
+ `Weapon: ${analysisResult.weapon}, Alcohol: ${analysisResult.alcohol}, ` +
360
+ `Offensive: ${analysisResult.offensive}, Self-harm: ${analysisResult.selfharm}, ` +
361
+ `Gambling: ${analysisResult.gambling}, Drugs: ${analysisResult.drugs}, ` +
362
+ `Tobacco: ${analysisResult.tobacco}`;
363
+ this.logger.info(logMsg);
364
+
365
+ return analysisResult;
366
+ }
367
+ }
@@ -0,0 +1,101 @@
1
+ import { getLoggerFor } from '@solid/community-server';
2
+
3
+ const logger = getLoggerFor('GuardedStream');
4
+
5
+ // Using symbols to make sure we don't override existing parameters
6
+ const guardedErrors = Symbol('guardedErrors');
7
+ const guardedTimeout = Symbol('guardedTimeout');
8
+
9
+ // Private fields for guarded streams
10
+ class Guard {
11
+ // Workaround for the fact that we don't initialize this variable as expected
12
+ declare private [guardedErrors]: Error[];
13
+ private [guardedTimeout]?: NodeJS.Timeout;
14
+ }
15
+
16
+ /**
17
+ * A stream that is guarded from emitting errors when there are no listeners.
18
+ * If an error occurs while no listener is attached,
19
+ * it will store the error and emit it once a listener is added (or a timeout occurs).
20
+ */
21
+ export type Guarded<T extends NodeJS.EventEmitter = NodeJS.EventEmitter> = T & Guard;
22
+
23
+ /**
24
+ * Determines whether the stream is guarded against emitting errors.
25
+ */
26
+ export function isGuarded<T extends NodeJS.EventEmitter>(stream: T): stream is Guarded<T> {
27
+ return typeof (stream as unknown as Guarded)[guardedErrors] === 'object';
28
+ }
29
+
30
+ /**
31
+ * Callback that is used when a stream emits an error and no other error listener is attached.
32
+ * Used to store the error and start the logger timer.
33
+ *
34
+ * It is important that this listener always remains attached for edge cases where an error listener gets removed
35
+ * and the number of error listeners is checked immediately afterwards.
36
+ * See https://github.com/CommunitySolidServer/CommunitySolidServer/pull/462#issuecomment-758013492 .
37
+ */
38
+ function guardingErrorListener(this: Guarded, error: Error): void {
39
+ // Only fall back to this if no new listeners are attached since guarding started.
40
+ const errorListeners = this.listeners('error');
41
+ if (errorListeners.at(-1) === guardingErrorListener) {
42
+ this[guardedErrors].push(error);
43
+ if (!this[guardedTimeout]) {
44
+ this[guardedTimeout] = setTimeout((): void => {
45
+ logger.error(`No error listener was attached but error was thrown: ${error.message}`);
46
+ }, 1000);
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Callback that is used when a new listener is attached and there are errors that were not emitted yet.
53
+ */
54
+ function emitStoredErrors(this: Guarded, event: string, func: (error: Error) => void): void {
55
+ if (event === 'error' && func !== guardingErrorListener) {
56
+ // Cancel an error timeout
57
+ if (this[guardedTimeout]) {
58
+ clearTimeout(this[guardedTimeout]);
59
+ this[guardedTimeout] = undefined;
60
+ }
61
+
62
+ // Emit any errors that were guarded
63
+ const errors = this[guardedErrors];
64
+ if (errors.length > 0) {
65
+ this[guardedErrors] = [];
66
+ setImmediate((): void => {
67
+ for (const error of errors) {
68
+ this.emit('error', error);
69
+ }
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Makes sure that listeners always receive the error event of a stream,
77
+ * even if it was thrown before the listener was attached.
78
+ *
79
+ * When guarding a stream it is assumed that error listeners already attached should be ignored,
80
+ * only error listeners attached after the stream is guarded will prevent an error from being logged.
81
+ *
82
+ * If the input is already guarded the guard will be reset,
83
+ * which means ignoring error listeners already attached.
84
+ *
85
+ * @param stream - Stream that can potentially throw an error.
86
+ *
87
+ * @returns The stream.
88
+ */
89
+ export function guardStream<T extends NodeJS.EventEmitter>(stream: T): Guarded<T> {
90
+ const guarded = stream as Guarded<T>;
91
+ if (isGuarded(stream)) {
92
+ // This makes sure the guarding error listener is the last one in the list again
93
+ guarded.removeListener('error', guardingErrorListener);
94
+ guarded.on('error', guardingErrorListener);
95
+ } else {
96
+ guarded[guardedErrors] = [];
97
+ guarded.on('error', guardingErrorListener);
98
+ guarded.on('newListener', emitStoredErrors);
99
+ }
100
+ return guarded;
101
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2021"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "test"]
20
+ }