@auth0/auth0-checkmate 1.4.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 (114) hide show
  1. package/.github/CODEOWNERS +1 -0
  2. package/.github/workflows/npm-release.yml +77 -0
  3. package/.github/workflows/sca_scan.yml +10 -0
  4. package/.github/workflows/test.yml +48 -0
  5. package/AUTHORS +5 -0
  6. package/LICENSE +203 -0
  7. package/README.md +166 -0
  8. package/THIRD-PARTY-NOTICES +226 -0
  9. package/analyzer/lib/actions/checkActionsHardCodedValues.js +151 -0
  10. package/analyzer/lib/actions/checkActionsRuntime.js +105 -0
  11. package/analyzer/lib/actions/checkDependencies.js +111 -0
  12. package/analyzer/lib/attack_protection/checkBotDetectionSetting.js +76 -0
  13. package/analyzer/lib/attack_protection/checkBreachedPassword.js +140 -0
  14. package/analyzer/lib/attack_protection/checkBruteForce.js +89 -0
  15. package/analyzer/lib/attack_protection/checkSuspiciousIPThrottling.js +89 -0
  16. package/analyzer/lib/canonical_domain/checkCanonicalDomain.js +63 -0
  17. package/analyzer/lib/clients/checkAllowedCallbacks.js +122 -0
  18. package/analyzer/lib/clients/checkAllowedLogoutUrl.js +124 -0
  19. package/analyzer/lib/clients/checkApplicationLoginUri.js +125 -0
  20. package/analyzer/lib/clients/checkCrossOriginAuthentication.js +91 -0
  21. package/analyzer/lib/clients/checkGrantTypes.js +138 -0
  22. package/analyzer/lib/clients/checkJWTSignAlg.js +118 -0
  23. package/analyzer/lib/clients/checkRefreshToken.js +108 -0
  24. package/analyzer/lib/clients/checkWebOrigins.js +55 -0
  25. package/analyzer/lib/constants.js +63 -0
  26. package/analyzer/lib/custom_domain/checkCustomDomain.js +53 -0
  27. package/analyzer/lib/databases/checkAuthenticationMethods.js +98 -0
  28. package/analyzer/lib/databases/checkDASHardCodedValues.js +163 -0
  29. package/analyzer/lib/databases/checkEmailAttributeVerification.js +114 -0
  30. package/analyzer/lib/databases/checkEnabledDatabaseCustomization.js +83 -0
  31. package/analyzer/lib/databases/checkPasswordComplexity.js +100 -0
  32. package/analyzer/lib/databases/checkPasswordHistory.js +92 -0
  33. package/analyzer/lib/databases/checkPasswordNoPersonalInfo.js +91 -0
  34. package/analyzer/lib/databases/checkPasswordPolicy.js +95 -0
  35. package/analyzer/lib/databases/checkPromotedDBConnection.js +96 -0
  36. package/analyzer/lib/email_provider/checkEmailProvider.js +37 -0
  37. package/analyzer/lib/email_templates/checkEmailTemplates.js +71 -0
  38. package/analyzer/lib/error_page_template/checkErrorPageTemplate.js +153 -0
  39. package/analyzer/lib/event_streams/checkEventStreams.js +71 -0
  40. package/analyzer/lib/executeCheck.js +12 -0
  41. package/analyzer/lib/hooks/checkHooks.js +43 -0
  42. package/analyzer/lib/listOfAnalyser.js +24 -0
  43. package/analyzer/lib/log_streams/checkLogStream.js +60 -0
  44. package/analyzer/lib/logger.js +16 -0
  45. package/analyzer/lib/multifactor/checkGuardianFactors.js +72 -0
  46. package/analyzer/lib/multifactor/checkGuardianPolicy.js +40 -0
  47. package/analyzer/lib/network_acl/checkNetworkACL.js +35 -0
  48. package/analyzer/lib/rules/checkRules.js +102 -0
  49. package/analyzer/lib/tenant_settings/checkDefaultAudience.js +53 -0
  50. package/analyzer/lib/tenant_settings/checkDefaultDirectory.js +48 -0
  51. package/analyzer/lib/tenant_settings/checkEnabledDynamicClientRegistration.js +60 -0
  52. package/analyzer/lib/tenant_settings/checkSandboxVersion.js +37 -0
  53. package/analyzer/lib/tenant_settings/checkSessionLifetime.js +95 -0
  54. package/analyzer/lib/tenant_settings/checkSupportEmail.js +61 -0
  55. package/analyzer/lib/tenant_settings/checkSupportUrl.js +61 -0
  56. package/analyzer/lib/tenant_settings/checkTenantLoginUrl.js +71 -0
  57. package/analyzer/lib/tenant_settings/checkTenantLogoutUrl.js +60 -0
  58. package/analyzer/report.js +404 -0
  59. package/analyzer/tools/auth0.js +443 -0
  60. package/analyzer/tools/helpers.js +71 -0
  61. package/analyzer/tools/summary.js +84 -0
  62. package/analyzer/tools/utils.js +72 -0
  63. package/bin/index.js +393 -0
  64. package/eslint.config.mjs +16 -0
  65. package/images/auth0.png +0 -0
  66. package/images/okta.png +0 -0
  67. package/locales/en.json +1417 -0
  68. package/package.json +66 -0
  69. package/tests/actions/checkActionsHardCodedValues.test.js +106 -0
  70. package/tests/actions/checkActionsRuntime.test.js +102 -0
  71. package/tests/actions/checkDependencies.test.js +131 -0
  72. package/tests/attack_protection/checkBreachedPassword.test.js +253 -0
  73. package/tests/attack_protection/checkBruteForce.test.js +181 -0
  74. package/tests/attack_protection/checkSuspiciousIPThrottling.test.js +222 -0
  75. package/tests/canonical_domain/checkCanonicalDomain.test.js +94 -0
  76. package/tests/clients/checkAllowedCallbacks.test.js +149 -0
  77. package/tests/clients/checkAllowedLogoutUrl.test.js +149 -0
  78. package/tests/clients/checkApplicationLoginUri.test.js +180 -0
  79. package/tests/clients/checkCrossOriginAuthentication.test.js +99 -0
  80. package/tests/clients/checkGrantTypes.test.js +154 -0
  81. package/tests/clients/checkJWTSignAlg.test.js +121 -0
  82. package/tests/clients/checkRefreshToken.test.js +63 -0
  83. package/tests/clients/checkWebOrigins.test.js +140 -0
  84. package/tests/custom_domain/checkCustomDomain.test.js +73 -0
  85. package/tests/databases/checkAuthenticationMethods.test.js +124 -0
  86. package/tests/databases/checkDASHardCodedValues.test.js +77 -0
  87. package/tests/databases/checkEmailAttributeVerification.test.js +79 -0
  88. package/tests/databases/checkEnabledDatabaseCustomization.test.js +68 -0
  89. package/tests/databases/checkPasswordComplexity.test.js +127 -0
  90. package/tests/databases/checkPasswordHistory.test.js +100 -0
  91. package/tests/databases/checkPasswordNoPersonalInfo.test.js +94 -0
  92. package/tests/databases/checkPasswordPolicy.test.js +161 -0
  93. package/tests/databases/checkPromotedDBConnection.test.js +62 -0
  94. package/tests/email_provider/checkEmailProvider.test.js +58 -0
  95. package/tests/email_templates/checkEmailTemplates.test.js +120 -0
  96. package/tests/error_page_template/checkErrorPageTemplate.test.js +315 -0
  97. package/tests/event_streams/checkEventStreams.test.js +118 -0
  98. package/tests/hooks/checkHooks.test.js +112 -0
  99. package/tests/log_streams/checkLogStream.test.js +140 -0
  100. package/tests/multifactor/checkGuardianFactors.test.js +94 -0
  101. package/tests/multifactor/checkGuardianPolicy.test.js +49 -0
  102. package/tests/rules/checkRules.test.js +102 -0
  103. package/tests/tenant_settings/checkDefaultAudience.test.js +62 -0
  104. package/tests/tenant_settings/checkDefaultDirectory.test.js +62 -0
  105. package/tests/tenant_settings/checkEnabledDynamicClientRegistration.test.js +97 -0
  106. package/tests/tenant_settings/checkSandboxVersion.test.js +50 -0
  107. package/tests/tenant_settings/checkSessionLifetime.test.js +108 -0
  108. package/tests/tenant_settings/checkSupportEmail.test.js +77 -0
  109. package/tests/tenant_settings/checkSupportUrl.test.js +77 -0
  110. package/tests/tenant_settings/checkTenantLoginUri.test.js +82 -0
  111. package/tests/tenant_settings/checkTenantLogoutUrl.test.js +108 -0
  112. package/tests/tools/auth0.test.js +833 -0
  113. package/tests/tools/helpers.test.js +692 -0
  114. package/views/pdf_cli_report.handlebars +571 -0
package/bin/index.js ADDED
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const jwt = require('jsonwebtoken');
5
+ const path = require("path");
6
+ const logger = require("../analyzer/lib/logger");
7
+ const { generateReport } = require("../analyzer/report");
8
+ const chalk = require("chalk");
9
+ const figlet = require("figlet");
10
+ const inquirer = require("inquirer").default;
11
+ const puppeteer = require("puppeteer");
12
+ const { getToday, getFormattedDateTime } = require("../analyzer/tools/utils");
13
+ const Handlebars = require("handlebars");
14
+ const i18n = require("i18n");
15
+ const os = require("os");
16
+ const userHomeDir = os.homedir();
17
+ const Table = require("cli-table3");
18
+ const {
19
+ getAccessToken,
20
+ } = require("../analyzer/tools/auth0");
21
+ const CONSTANTS = require('../analyzer/lib/constants');
22
+ /**
23
+ * print current version of the package
24
+ * Access the version from package.json
25
+ */
26
+ const { version } = require("../package.json");
27
+
28
+ /**
29
+ * configure shared state
30
+ */
31
+ i18n.configure({
32
+ defaultLocale: "en",
33
+ objectNotation: true,
34
+ directory: path.join(__dirname, "../locales"),
35
+ });
36
+
37
+ Handlebars.registerHelper("chooseFont", function (locale) {
38
+ if (locale === "ja") {
39
+ return "Noto Sans JP, sans-serif";
40
+ } else if (locale === "ko") {
41
+ return "Noto Sans KR, sans-serif"; // For Simplified Chinese
42
+ } else {
43
+ return "DM Sans, sans-serif"; // Default font
44
+ }
45
+ });
46
+ Handlebars.registerHelper("replace", function (str, search, replace) {
47
+ return str.replace(search, replace);
48
+ });
49
+ Handlebars.registerHelper("and", (a, b) => a && b); // Logical AND
50
+ Handlebars.registerHelper("inc", (a) => parseInt(a) + 1);
51
+
52
+ console.log(
53
+ chalk.yellow(
54
+ figlet.textSync("CheckMate\nfor Auth0", {
55
+ horizontalLayout: "full",
56
+ width: 80,
57
+ }),
58
+ ),
59
+ );
60
+ console.log(chalk.yellow(`Current Version: ${version}\n`));
61
+ // Load static assets required for report generation.
62
+ const templateData = fs.readFileSync(
63
+ path.join(__dirname, "../views/pdf_cli_report.handlebars"),
64
+ "utf8",
65
+ );
66
+ const imagePath = path.join(__dirname, '../images/auth0.png');
67
+ const imageBuffer = fs.readFileSync(imagePath);
68
+ const base64Image = imageBuffer.toString('base64');
69
+ const imageSrc = `data:image/png;base64,${base64Image}`;
70
+
71
+ /**
72
+ *
73
+ * @param {*} accessToken
74
+ * @returns
75
+ */
76
+ function decodeTokenScopes(accessToken) {
77
+ try {
78
+ const decoded = jwt.decode(accessToken);
79
+ if (!decoded) {
80
+ console.error("❌ Failed to decode token.");
81
+ return null;
82
+ }
83
+
84
+ const scopeString = decoded.scope || "";
85
+ return scopeString.split(" ").map(scope => scope.trim()).filter(Boolean);
86
+ } catch (err) {
87
+ console.error("❌ Error decoding token:", err.message);
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function filterByKey(array, keyArray, key) {
93
+ return array.filter(item => {
94
+ const words = item[key].split(' ');
95
+ return words.some(word => keyArray.includes(word));
96
+ });
97
+ }
98
+
99
+ /**
100
+ *
101
+ * @param {*} accessToken
102
+ * @param {*} requiredScopes
103
+ * @returns
104
+ */
105
+ function checkScopes(accessToken, requiredScopes) {
106
+ const tokenScopes = decodeTokenScopes(accessToken);
107
+ if (!tokenScopes) {
108
+ console.error("❌ No scopes found in token.");
109
+ return;
110
+ }
111
+
112
+ const missingScopes = requiredScopes.filter(scope => !tokenScopes.includes(scope));
113
+ const listOfValidators = i18n.__("list_of_validators");
114
+ const filteredValidators = filterByKey(listOfValidators, missingScopes, 'required_scope');
115
+ if (missingScopes.length > 0) {
116
+ console.log("❌ The following validators will be skipped as it is missing required scopes:");
117
+ filteredValidators.forEach(v => {
118
+ v.title = v.title.concat(` - ${v.required_scope}`);
119
+ delete v.required_scope;
120
+ printJsonAsBullets(v);
121
+ });
122
+ } else {
123
+ console.log("✅ All required scopes are present.");
124
+ }
125
+ }
126
+
127
+ /**
128
+ *
129
+ * @param {*} locale
130
+ * @param {*} tenant
131
+ * @param {*} config
132
+ */
133
+
134
+ // Placeholder function to simulate processing Auth0 configuration
135
+ async function processAuth0Config(locale, tenant, config) {
136
+ logger.log("info", "Processing Auth0 configuration:");
137
+ const report = await generateReport(locale, tenant, config);
138
+ const today = await getToday(locale);
139
+ const { auth0Domain, filePath } = config;
140
+ const data = { report, auth0Domain, today, locale, version };
141
+ await printReportToCli(filePath, data);
142
+ }
143
+
144
+ async function printReportToCli(filePath, data) {
145
+ if (data.report && data.report.summary) {
146
+ console.log(
147
+ chalk.yellow(
148
+ `\nHigh level summary of ${data.report.summary.length} findings\n`,
149
+ ),
150
+ );
151
+ // Create a table instance
152
+ const table = new Table({
153
+ head: ["Index", "Priority", "Recommendation"],
154
+ colWidths: [10, 15, 150],
155
+ });
156
+
157
+ // Add rows to the table
158
+ data.report.summary.forEach((report, index) => {
159
+ table.push([
160
+ ++index,
161
+ report.severity,
162
+ `${report.title} - ${report.severity_message.replace("%s", report.detailsLength)}`,
163
+ ]);
164
+ });
165
+
166
+ // Print the table
167
+ console.log(table.toString());
168
+ await generatePdf(filePath, data);
169
+ } else {
170
+ console.log(chalk.red(`failed to generate report`));
171
+ }
172
+ }
173
+
174
+ async function generatePdf(filePath, data) {
175
+ try {
176
+ console.log(chalk.yellow(`\nGenerating a PDF report\n`));
177
+ const fileFullPath = `${filePath}/${data.auth0Domain}_${data.locale}_${getFormattedDateTime()}_report.pdf`;
178
+ const fileFullPathJSON = `${filePath}/${data.auth0Domain}_${data.locale}_${getFormattedDateTime()}_report.json`;
179
+ if (!fs.existsSync(filePath)) {
180
+ console.log("The directory does not exist, creating it...");
181
+ await fs.mkdirSync(filePath, { recursive: true });
182
+ }
183
+ // save JSON report
184
+ fs.writeFileSync(fileFullPathJSON, JSON.stringify(data.report.summary, null, 2));
185
+ const browser = await puppeteer.launch({
186
+ headless: true, // Run in headless mode
187
+ args: [
188
+ "--no-sandbox", // Disable the sandbox
189
+ "--disable-setuid-sandbox", // Disable setuid sandbox
190
+ ],
191
+ });
192
+ const template = Handlebars.compile(templateData);
193
+ const htmlContent = template({
194
+ locale: data.locale,
195
+ data,
196
+ logoBase64: imageSrc,
197
+ preamble: data.report.preamble,
198
+ });
199
+ const page = await browser.newPage();
200
+ // Load the compiled HTML content into Puppeteer
201
+
202
+ await page.setContent(`${htmlContent}`, { waitUntil: "networkidle2" });
203
+
204
+ // Generate PDF
205
+ await page.pdf({
206
+ path: fileFullPath,
207
+ format: "A4",
208
+ printBackground: true,
209
+ displayHeaderFooter: true,
210
+ headerTemplate: `<div></div>`,
211
+ footerTemplate: `
212
+ <div style="font-size:10px; width:100%; padding:10px 0; display:flex; align-items:center; justify-content:space-between; border-top:1px solid #ddd;">
213
+ <span style="flex:1; text-align:center;">Confidential. For internal evaluation purposes only.</span>
214
+ <span style="flex:1; text-align:right; padding-right:20px;"">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
215
+ </div>`,
216
+ margin: {
217
+ top: "20px", // Space for header
218
+ bottom: "60px", // Space for footer
219
+ },
220
+ });
221
+ await browser.close();
222
+ console.log(
223
+ chalk.yellow(`\nA full PDF report has been saved at ${fileFullPath}\n`),
224
+ );
225
+ console.log(
226
+ chalk.yellow(`\nJSON report has been saved at ${fileFullPathJSON}\n`),
227
+ );
228
+ } catch (e) {
229
+ logger.log("error", `Failed to generate pdf ${e}`);
230
+ }
231
+ }
232
+
233
+ function printJsonAsBullets(json, indent = 0) {
234
+ const indentStr = ' '.repeat(indent);
235
+
236
+ for (const key in json) {
237
+ if (key != 'required_scope') {
238
+ const value = json[key];
239
+
240
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
241
+ printJsonAsBullets(value, indent + 1);
242
+ } else if (Array.isArray(value)) {
243
+ //console.log(`${indentStr}• ${key}:`);
244
+ value.forEach(item => {
245
+ if (typeof item === 'object') {
246
+ printJsonAsBullets(item, indent + 1);
247
+ } else {
248
+ console.log(`${indentStr} - ${item}`);
249
+ }
250
+ });
251
+ } else {
252
+ console.log(`${indentStr}• ${value}`);
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ async function main() {
259
+ const selectedValidators = process.env.RUN_VALIDATORS;
260
+ if (selectedValidators && !/^(\w+)(,\w+)*$/.test(selectedValidators)) {
261
+ throw new Error(
262
+ `RUN_VALIDATORS must be a comma-separated list of available validator names`
263
+ );
264
+ }
265
+ const answers = {};
266
+
267
+ // Prompt 1: Show validators
268
+ const { showValidators } = await inquirer.prompt({
269
+ type: "confirm",
270
+ name: "showValidators",
271
+ message: "Show me the list of validators:",
272
+ default: false,
273
+ });
274
+ answers.showValidators = showValidators;
275
+
276
+ if (showValidators) {
277
+ console.log("Currently supports for the following tenant configurations:");
278
+ printJsonAsBullets(i18n.__("list_of_validators"));
279
+ }
280
+
281
+ // Prompt 2: Auth method
282
+ const { authMethod } = await inquirer.prompt({
283
+ type: "list",
284
+ name: "authMethod",
285
+ message: "How would you like to provide credentials?",
286
+ choices: [
287
+ { name: "Auth0 Client ID & Secret", value: "clientSecret" },
288
+ { name: "Auth0 Management API Token", value: "auth0MgmtToken" },
289
+ ],
290
+ });
291
+ answers.authMethod = authMethod;
292
+
293
+ // Prompt 3: Auth0 Domain
294
+ const { auth0Domain } = await inquirer.prompt({
295
+ type: "input",
296
+ name: "auth0Domain",
297
+ message: "Enter your Auth0 domain:",
298
+ validate: (input) => (input ? true : "Auth0 domain is required."),
299
+ });
300
+ answers.auth0Domain = auth0Domain;
301
+
302
+ // Prompt 4: Auth0 Client ID (if needed)
303
+ if (authMethod === "clientSecret") {
304
+ const { auth0ClientId } = await inquirer.prompt({
305
+ type: "input",
306
+ name: "auth0ClientId",
307
+ message: "Enter your Auth0 Client ID:",
308
+ validate: (input) => (input ? true : "Auth0 Client ID is required."),
309
+ });
310
+ answers.auth0ClientId = auth0ClientId;
311
+ }
312
+
313
+ // Prompt 5: Auth0 Client Secret (if needed)
314
+ if (authMethod === "clientSecret") {
315
+ const { auth0ClientSecret } = await inquirer.prompt({
316
+ type: "password",
317
+ name: "auth0ClientSecret",
318
+ message: "Enter your Auth0 Client Secret:",
319
+ validate: (input) => (input ? true : "Auth0 Client Secret is required."),
320
+ });
321
+ answers.auth0ClientSecret = auth0ClientSecret;
322
+ try {
323
+ const accessToken = await getAccessToken(answers.auth0Domain, answers.auth0ClientId, answers.auth0ClientSecret);
324
+ checkScopes(accessToken, CONSTANTS.REQUIRED_SCOPES.split(' '));
325
+ answers.auth0MgmtToken = accessToken;
326
+ } catch (e) {
327
+ console.error(e.message);
328
+ process.exit(0);
329
+ }
330
+ }
331
+
332
+ // Prompt 6: Auth0 Management Token (if needed)
333
+ if (authMethod === "auth0MgmtToken") {
334
+ const { auth0MgmtToken } = await inquirer.prompt({
335
+ type: "input",
336
+ name: "auth0MgmtToken",
337
+ message: "Enter your Auth0 Management API Token:",
338
+ validate: (input) =>
339
+ input ? true : "Auth0 Management API token is required.",
340
+ });
341
+ checkScopes(auth0MgmtToken, CONSTANTS.REQUIRED_SCOPES.split(' '));
342
+ answers.auth0MgmtToken = auth0MgmtToken;
343
+ }
344
+
345
+ // Prompt 7: Locale (if more than one)
346
+ const locales = i18n.getLocales();
347
+ if (locales.length > 1) {
348
+ const { locale } = await inquirer.prompt({
349
+ type: "list",
350
+ name: "locale",
351
+ message: "Select a locale:",
352
+ choices: locales,
353
+ default: "en",
354
+ });
355
+ answers.locale = locale;
356
+ } else {
357
+ answers.locale = "en";
358
+ }
359
+
360
+ // Prompt 8: File path
361
+ const { filePath } = await inquirer.prompt({
362
+ type: "input",
363
+ name: "filePath",
364
+ message:
365
+ "Enter the full path where you want to save the file (e.g., /path/to/file.pdf):",
366
+ default: userHomeDir,
367
+ validate: (input) => {
368
+ if (input.trim() === "") {
369
+ return "Please enter a valid file path.";
370
+ }
371
+ return true;
372
+ },
373
+ });
374
+ answers.filePath = filePath;
375
+ // Construct config
376
+ const config = {
377
+ auth0Domain: answers.auth0Domain,
378
+ auth0ClientId: answers.auth0ClientId || null,
379
+ auth0ClientSecret: answers.auth0ClientSecret || null,
380
+ auth0MgmtToken: answers.auth0MgmtToken || null,
381
+ filePath: path.join(userHomeDir, answers.filePath),
382
+ selectedValidators: selectedValidators ? selectedValidators.split(',') : []
383
+ };
384
+
385
+ const tenant = {};
386
+
387
+ processAuth0Config(answers.locale, tenant, config);
388
+ }
389
+ // Execute the script
390
+ main().catch((err) => {
391
+ logger.log("error", `Error: ${err.message}`);
392
+ process.exit(1);
393
+ });
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from "eslint/config";
2
+ import mochaPlugin from "eslint-plugin-mocha";
3
+ import globals from "globals";
4
+ import js from "@eslint/js";
5
+
6
+ export default defineConfig([
7
+ { files: ["**/*.{js,mjs,cjs}"] },
8
+ { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
9
+ { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.node } },
10
+ {
11
+ files: ["**/*.{js,mjs,cjs}"],
12
+ plugins: { js },
13
+ extends: ["js/recommended"],
14
+ },
15
+ mochaPlugin.configs.flat.recommended,
16
+ ]);
Binary file
Binary file