@sun-asterisk/sunlint 1.3.7 → 1.3.8

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 (38) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/config/defaults/default.json +2 -1
  3. package/config/rule-analysis-strategies.js +20 -0
  4. package/config/rules/enhanced-rules-registry.json +190 -35
  5. package/core/file-targeting-service.js +83 -7
  6. package/package.json +1 -1
  7. package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
  8. package/rules/common/C065_one_behavior_per_test/config.json +95 -0
  9. package/rules/security/S037_cache_headers/README.md +128 -0
  10. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  11. package/rules/security/S037_cache_headers/config.json +50 -0
  12. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  13. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  14. package/rules/security/S038_no_version_headers/README.md +234 -0
  15. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  16. package/rules/security/S038_no_version_headers/config.json +49 -0
  17. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  18. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  19. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  20. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  21. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  22. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  23. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
  24. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  25. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  26. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  27. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  28. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  29. package/rules/security/S051_password_length_policy/config.json +83 -0
  30. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  31. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  32. package/rules/security/S054_no_default_accounts/README.md +129 -0
  33. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  34. package/rules/security/S054_no_default_accounts/config.json +101 -0
  35. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  36. package/rules/security/S056_log_injection_protection/config.json +148 -0
  37. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  38. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
@@ -0,0 +1,198 @@
1
+ # S039 - Do Not Pass Session Tokens via URL Parameters
2
+
3
+ ## Overview
4
+
5
+ Rule S039 detects when session tokens, authentication tokens, JWT tokens, or other sensitive authentication data are passed as URL parameters instead of secure headers or request body. URL parameters are logged in web server logs, browser history, and can be exposed in referrer headers.
6
+
7
+ ## Framework Support
8
+
9
+ This rule supports multiple web frameworks:
10
+
11
+ - **Express.js**: Parameter access via `req.query`, `req.params`
12
+ - **Next.js**: URL parameter access via `searchParams.get()`, `new URL().searchParams`
13
+ - **NestJS**: Parameter decorators `@Query()`, `@Param()`
14
+ - **Nuxt.js**: Server route parameter handling
15
+ - **Client-side**: Browser URL parameter access via `location.search`, `URLSearchParams`
16
+
17
+ ## Security Impact
18
+
19
+ Passing session tokens via URL parameters creates several security vulnerabilities:
20
+
21
+ 1. **Web Server Logs**: URL parameters are logged in web server access logs
22
+ 2. **Browser History**: URLs with parameters are stored in browser history
23
+ 3. **Referrer Headers**: URLs may be exposed when navigating to external sites
24
+ 4. **Shared URLs**: Users may inadvertently share URLs containing tokens
25
+ 5. **Cache Poisoning**: URLs with tokens may be cached by proxies or CDNs
26
+ 6. **Analytics Tracking**: URL parameters are often sent to analytics services
27
+
28
+ ## Rule Details
29
+
30
+ ### Detected Token Parameter Names
31
+
32
+ The rule detects the following session token parameter patterns:
33
+
34
+ | Parameter Name | Variants |
35
+ | -------------- | --------------------------------------------------- |
36
+ | Session ID | `sessionId`, `session_id`, `session-id` |
37
+ | Session Token | `sessionToken`, `session_token`, `session-token` |
38
+ | Auth Token | `authToken`, `auth_token`, `auth-token` |
39
+ | Authorization | `authorization`, `bearer` |
40
+ | JWT | `jwt`, `jwtToken`, `jwt_token`, `jwt-token` |
41
+ | Access Token | `accessToken`, `access_token`, `access-token` |
42
+ | Refresh Token | `refreshToken`, `refresh_token`, `refresh-token` |
43
+ | API Key | `apiKey`, `api_key`, `api-key` |
44
+ | CSRF Token | `csrfToken`, `csrf_token`, `csrf-token` |
45
+ | XSRF Token | `xsrfToken`, `xsrf_token`, `xsrf-token` |
46
+ | Generic Token | `token`, `apiToken`, `api_token`, `api-token` |
47
+ | Session Key | `sid`, `sessionkey`, `session_key`, `session-key` |
48
+ | User Token | `userToken`, `user_token`, `user-token` |
49
+ | Auth Key | `authKey`, `auth_key`, `auth-key` |
50
+ | Security Token | `securityToken`, `security_token`, `security-token` |
51
+
52
+ ### Detected Access Patterns
53
+
54
+ ✅ **Secure Alternatives:**
55
+
56
+ ```typescript
57
+ // Use Authorization header
58
+ app.get("/api/user", (req, res) => {
59
+ const token = req.headers.authorization?.replace("Bearer ", "");
60
+ // Process token securely
61
+ });
62
+
63
+ // Use request body for POST requests
64
+ app.post("/api/authenticate", (req, res) => {
65
+ const { sessionToken } = req.body;
66
+ // Process token securely
67
+ });
68
+
69
+ // Use secure cookies
70
+ app.get("/api/profile", (req, res) => {
71
+ const sessionId = req.cookies.sessionId;
72
+ // Process session ID securely
73
+ });
74
+ ```
75
+
76
+ ❌ **Violation Examples:**
77
+
78
+ ```typescript
79
+ // Express.js violations
80
+ app.get('/api/user', (req, res) => {
81
+ const sessionToken = req.query.sessionToken; // ❌ Token in URL
82
+ const authKey = req.params.authKey; // ❌ Token in URL
83
+ });
84
+
85
+ // Object destructuring violations
86
+ app.get('/api/data', (req, res) => {
87
+ const { jwt, apiKey } = req.query; // ❌ Multiple tokens in URL
88
+ });
89
+
90
+ // NestJS violations
91
+ @Get('user')
92
+ getUserData(@Query('sessionToken') token: string) { // ❌ Token in URL
93
+ // ...
94
+ }
95
+
96
+ @Get('profile/:authToken')
97
+ getProfile(@Param('authToken') token: string) { // ❌ Token in URL path
98
+ // ...
99
+ }
100
+
101
+ // Next.js violations
102
+ export async function GET(request: Request) {
103
+ const url = new URL(request.url);
104
+ const jwt = url.searchParams.get('jwt'); // ❌ Token in URL
105
+ // ...
106
+ }
107
+
108
+ // Client-side violations
109
+ const params = new URLSearchParams(location.search);
110
+ const sessionId = params.get('sessionId'); // ❌ Token in URL
111
+ ```
112
+
113
+ ### Framework-Specific Detection
114
+
115
+ #### Express.js
116
+
117
+ - `req.query.tokenName`
118
+ - `req.params.tokenName`
119
+ - Object destructuring: `{ tokenName } = req.query`
120
+
121
+ #### NestJS
122
+
123
+ - `@Query('tokenName')`
124
+ - `@Param('tokenName')`
125
+
126
+ #### Next.js
127
+
128
+ - `searchParams.get('tokenName')`
129
+ - `new URL().searchParams.get('tokenName')`
130
+ - `URLSearchParams().get('tokenName')`
131
+
132
+ #### Client-side JavaScript
133
+
134
+ - `location.search` with subsequent parameter parsing
135
+ - `URLSearchParams(location.search).get('tokenName')`
136
+ - `window.location.search` parameter access
137
+
138
+ ## Best Practices
139
+
140
+ ### ✅ Secure Token Transmission
141
+
142
+ 1. **Authorization Headers**:
143
+
144
+ ```typescript
145
+ // Send token in Authorization header
146
+ fetch("/api/data", {
147
+ headers: {
148
+ Authorization: `Bearer ${token}`,
149
+ },
150
+ });
151
+ ```
152
+
153
+ 2. **Request Body**:
154
+
155
+ ```typescript
156
+ // Send token in request body for POST requests
157
+ fetch("/api/authenticate", {
158
+ method: "POST",
159
+ headers: { "Content-Type": "application/json" },
160
+ body: JSON.stringify({ sessionToken: token }),
161
+ });
162
+ ```
163
+
164
+ 3. **Secure Cookies**:
165
+ ```typescript
166
+ // Store session in secure, HTTP-only cookies
167
+ res.cookie("sessionId", sessionId, {
168
+ httpOnly: true,
169
+ secure: true,
170
+ sameSite: "strict",
171
+ });
172
+ ```
173
+
174
+ ### ❌ Avoid These Patterns
175
+
176
+ 1. **URL Query Parameters**: Never pass tokens as `?token=abc123`
177
+ 2. **URL Path Parameters**: Never embed tokens in URL paths `/api/user/abc123`
178
+ 3. **Fragment Identifiers**: Avoid `#token=abc123` (though less logged)
179
+ 4. **Form GET Methods**: Don't use GET forms for token submission
180
+
181
+ ## Configuration
182
+
183
+ The rule can be configured in `config.json`:
184
+
185
+ ```json
186
+ {
187
+ "validation": {
188
+ "sessionTokenParams": ["custom-token", "my-auth-key"],
189
+ "frameworks": ["express", "nestjs", "nextjs"]
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Related Security Rules
195
+
196
+ - **S027**: No hardcoded secrets - Prevents tokens in source code
197
+ - **S031**: Secure session cookies - Ensures proper cookie security
198
+ - **S016**: No sensitive data in query strings - Broader sensitive data protection
@@ -0,0 +1,262 @@
1
+ /**
2
+ * S039 Main Analyzer - Do not pass Session Tokens via URL parameters
3
+ * Primary: Symbol-based analysis (when available)
4
+ * Fallback: Regex-based for all other cases
5
+ * Command: node cli.js --rule=S039 --input=examples/rule-test-fixtures/rules/S039_no_session_tokens_in_url --engine=heuristic
6
+ */
7
+
8
+ const S039SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
9
+ const S039RegexBasedAnalyzer = require("./regex-based-analyzer.js");
10
+
11
+ class S039Analyzer {
12
+ constructor(options = {}) {
13
+ if (process.env.SUNLINT_DEBUG) {
14
+ console.log(`🔧 [S039] Constructor called with options:`, !!options);
15
+ console.log(
16
+ `🔧 [S039] Options type:`,
17
+ typeof options,
18
+ Object.keys(options || {})
19
+ );
20
+ }
21
+
22
+ this.ruleId = "S039";
23
+ this.ruleName = "Do not pass Session Tokens via URL parameters";
24
+ this.description =
25
+ "Detects when session tokens, authentication tokens, JWT tokens, or other sensitive authentication data are passed as URL parameters instead of secure headers or body. URL parameters are logged in web server logs, browser history, and can be exposed in referrer headers.";
26
+ this.semanticEngine = options.semanticEngine || null;
27
+ this.verbose = options.verbose || false;
28
+
29
+ this.config = {
30
+ useSymbolBased: true,
31
+ fallbackToRegex: true,
32
+ regexBasedOnly: false,
33
+ prioritizeSymbolic: true, // Prefer symbol-based when available
34
+ fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
35
+ };
36
+
37
+ try {
38
+ this.symbolAnalyzer = new S039SymbolBasedAnalyzer(this.semanticEngine);
39
+ if (process.env.SUNLINT_DEBUG)
40
+ console.log(`🔧 [S039] Symbol analyzer created successfully`);
41
+ } catch (error) {
42
+ console.error(`🔧 [S039] Error creating symbol analyzer:`, error);
43
+ }
44
+
45
+ try {
46
+ this.regexAnalyzer = new S039RegexBasedAnalyzer(this.semanticEngine);
47
+ if (process.env.SUNLINT_DEBUG)
48
+ console.log(`🔧 [S039] Regex analyzer created successfully`);
49
+ } catch (error) {
50
+ console.error(`🔧 [S039] Error creating regex analyzer:`, error);
51
+ }
52
+ }
53
+
54
+ async initialize(semanticEngine) {
55
+ this.semanticEngine = semanticEngine;
56
+ if (process.env.SUNLINT_DEBUG)
57
+ console.log(`🔧 [S039] Main analyzer initializing...`);
58
+
59
+ if (this.symbolAnalyzer)
60
+ await this.symbolAnalyzer.initialize?.(semanticEngine);
61
+ if (this.regexAnalyzer)
62
+ await this.regexAnalyzer.initialize?.(semanticEngine);
63
+ if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
64
+
65
+ if (process.env.SUNLINT_DEBUG)
66
+ console.log(`🔧 [S039] Main analyzer initialized successfully`);
67
+ }
68
+
69
+ analyzeSingle(filePath, options = {}) {
70
+ if (process.env.SUNLINT_DEBUG)
71
+ console.log(`🔍 [S039] analyzeSingle() called for: ${filePath}`);
72
+ return this.analyze([filePath], "typescript", options);
73
+ }
74
+
75
+ async analyze(files, language, options = {}) {
76
+ if (process.env.SUNLINT_DEBUG) {
77
+ console.log(
78
+ `🔧 [S039] analyze() method called with ${files.length} files, language: ${language}`
79
+ );
80
+ }
81
+
82
+ const violations = [];
83
+ for (const filePath of files) {
84
+ try {
85
+ if (process.env.SUNLINT_DEBUG)
86
+ console.log(`🔧 [S039] Processing file: ${filePath}`);
87
+ const fileViolations = await this.analyzeFile(filePath, options);
88
+ violations.push(...fileViolations);
89
+ if (process.env.SUNLINT_DEBUG)
90
+ console.log(
91
+ `🔧 [S039] File ${filePath}: Found ${fileViolations.length} violations`
92
+ );
93
+ } catch (error) {
94
+ console.warn(
95
+ `⚠ [S039] Analysis failed for ${filePath}:`,
96
+ error.message
97
+ );
98
+ }
99
+ }
100
+
101
+ if (process.env.SUNLINT_DEBUG)
102
+ console.log(`🔧 [S039] Total violations found: ${violations.length}`);
103
+ return violations;
104
+ }
105
+
106
+ async analyzeFile(filePath, options = {}) {
107
+ if (process.env.SUNLINT_DEBUG)
108
+ console.log(`🔍 [S039] analyzeFile() called for: ${filePath}`);
109
+ const violationMap = new Map();
110
+
111
+ // Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
112
+ if (process.env.SUNLINT_DEBUG) {
113
+ console.log(
114
+ `🔧 [S039] Symbol check: useSymbolBased=${
115
+ this.config.useSymbolBased
116
+ }, semanticEngine=${!!this.semanticEngine}, project=${!!this
117
+ .semanticEngine?.project}, initialized=${!!this.semanticEngine
118
+ ?.initialized}`
119
+ );
120
+ }
121
+
122
+ const canUseSymbol =
123
+ this.config.useSymbolBased &&
124
+ ((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
125
+ (!this.semanticEngine && this.config.fallbackToSymbol !== false));
126
+
127
+ if (canUseSymbol) {
128
+ try {
129
+ if (process.env.SUNLINT_DEBUG)
130
+ console.log(`🔧 [S039] Trying symbol-based analysis...`);
131
+
132
+ let sourceFile = null;
133
+ if (this.semanticEngine?.project) {
134
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
135
+ if (process.env.SUNLINT_DEBUG) {
136
+ console.log(
137
+ `🔧 [S039] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
138
+ );
139
+ }
140
+ }
141
+
142
+ if (!sourceFile) {
143
+ // Create a minimal ts-morph project for this analysis
144
+ if (process.env.SUNLINT_DEBUG)
145
+ console.log(
146
+ `🔧 [S039] Creating temporary ts-morph project for: ${filePath}`
147
+ );
148
+ try {
149
+ const fs = require("fs");
150
+ const path = require("path");
151
+ const { Project } = require("ts-morph");
152
+
153
+ // Check if file exists and read content
154
+ if (!fs.existsSync(filePath)) {
155
+ throw new Error(`File not found: ${filePath}`);
156
+ }
157
+
158
+ const fileContent = fs.readFileSync(filePath, "utf8");
159
+ const fileName = path.basename(filePath);
160
+
161
+ const tempProject = new Project({
162
+ useInMemoryFileSystem: true,
163
+ compilerOptions: {
164
+ allowJs: true,
165
+ allowSyntheticDefaultImports: true,
166
+ },
167
+ });
168
+
169
+ // Add file content to in-memory project
170
+ sourceFile = tempProject.createSourceFile(fileName, fileContent);
171
+ if (process.env.SUNLINT_DEBUG)
172
+ console.log(
173
+ `🔧 [S039] Temporary project created successfully with file: ${fileName}`
174
+ );
175
+ } catch (projectError) {
176
+ if (process.env.SUNLINT_DEBUG)
177
+ console.log(
178
+ `🔧 [S039] Failed to create temporary project:`,
179
+ projectError.message
180
+ );
181
+ throw projectError;
182
+ }
183
+ }
184
+
185
+ if (sourceFile) {
186
+ const symbolViolations = await this.symbolAnalyzer.analyze(
187
+ sourceFile,
188
+ filePath
189
+ );
190
+ symbolViolations.forEach((v) => {
191
+ const key = `${v.line}:${v.column}:${v.message}`;
192
+ if (!violationMap.has(key)) violationMap.set(key, v);
193
+ });
194
+ if (process.env.SUNLINT_DEBUG)
195
+ console.log(
196
+ `🔧 [S039] Symbol analysis completed: ${symbolViolations.length} violations`
197
+ );
198
+
199
+ // If symbol-based found violations or prioritizeSymbolic is true, skip regex
200
+ if (this.config.prioritizeSymbolic && symbolViolations.length >= 0) {
201
+ const finalViolations = Array.from(violationMap.values()).map(
202
+ (v) => ({
203
+ ...v,
204
+ filePath,
205
+ file: filePath,
206
+ })
207
+ );
208
+ if (process.env.SUNLINT_DEBUG)
209
+ console.log(
210
+ `🔧 [S039] Symbol-based analysis prioritized: ${finalViolations.length} violations`
211
+ );
212
+ return finalViolations;
213
+ }
214
+ } else {
215
+ if (process.env.SUNLINT_DEBUG)
216
+ console.log(
217
+ `🔧 [S039] No source file found, skipping symbol analysis`
218
+ );
219
+ }
220
+ } catch (error) {
221
+ console.warn(`⚠ [S039] Symbol analysis failed:`, error.message);
222
+ }
223
+ }
224
+
225
+ // Fallback to regex-based analysis
226
+ if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
227
+ try {
228
+ if (process.env.SUNLINT_DEBUG)
229
+ console.log(`🔧 [S039] Trying regex-based analysis...`);
230
+ const regexViolations = await this.regexAnalyzer.analyze(filePath);
231
+ regexViolations.forEach((v) => {
232
+ const key = `${v.line}:${v.column}:${v.message}`;
233
+ if (!violationMap.has(key)) violationMap.set(key, v);
234
+ });
235
+ if (process.env.SUNLINT_DEBUG)
236
+ console.log(
237
+ `🔧 [S039] Regex analysis completed: ${regexViolations.length} violations`
238
+ );
239
+ } catch (error) {
240
+ console.warn(`⚠ [S039] Regex analysis failed:`, error.message);
241
+ }
242
+ }
243
+
244
+ const finalViolations = Array.from(violationMap.values()).map((v) => ({
245
+ ...v,
246
+ filePath,
247
+ file: filePath,
248
+ }));
249
+ if (process.env.SUNLINT_DEBUG)
250
+ console.log(
251
+ `🔧 [S039] File analysis completed: ${finalViolations.length} unique violations`
252
+ );
253
+ return finalViolations;
254
+ }
255
+
256
+ cleanup() {
257
+ if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
258
+ if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
259
+ }
260
+ }
261
+
262
+ module.exports = S039Analyzer;
@@ -0,0 +1,92 @@
1
+ {
2
+ "id": "S039",
3
+ "name": "Do not pass Session Tokens via URL parameters",
4
+ "category": "security",
5
+ "description": "S039 - Detects when session tokens, authentication tokens, JWT tokens, or other sensitive authentication data are passed as URL parameters instead of secure headers or request body. URL parameters are logged in web server logs, browser history, and can be exposed in referrer headers.",
6
+ "severity": "warning",
7
+ "enabled": true,
8
+ "semantic": {
9
+ "enabled": true,
10
+ "priority": "high",
11
+ "fallback": "heuristic"
12
+ },
13
+ "patterns": {
14
+ "include": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"],
15
+ "exclude": [
16
+ "**/*.test.js",
17
+ "**/*.test.ts",
18
+ "**/*.spec.js",
19
+ "**/*.spec.ts",
20
+ "**/node_modules/**",
21
+ "**/dist/**",
22
+ "**/build/**"
23
+ ]
24
+ },
25
+ "analysis": {
26
+ "approach": "symbol-based-primary",
27
+ "fallback": "regex-based",
28
+ "depth": 1,
29
+ "timeout": 4000
30
+ },
31
+ "validation": {
32
+ "sessionTokenParams": [
33
+ "sessionId",
34
+ "session_id",
35
+ "session-id",
36
+ "sessionToken",
37
+ "session_token",
38
+ "session-token",
39
+ "authToken",
40
+ "auth_token",
41
+ "auth-token",
42
+ "authorization",
43
+ "bearer",
44
+ "jwt",
45
+ "jwtToken",
46
+ "jwt_token",
47
+ "jwt-token",
48
+ "accessToken",
49
+ "access_token",
50
+ "access-token",
51
+ "refreshToken",
52
+ "refresh_token",
53
+ "refresh-token",
54
+ "apiKey",
55
+ "api_key",
56
+ "api-key",
57
+ "csrfToken",
58
+ "csrf_token",
59
+ "csrf-token",
60
+ "xsrfToken",
61
+ "xsrf_token",
62
+ "xsrf-token",
63
+ "token",
64
+ "apiToken",
65
+ "api_token",
66
+ "api-token",
67
+ "sid",
68
+ "sessionkey",
69
+ "session_key",
70
+ "session-key",
71
+ "userToken",
72
+ "user_token",
73
+ "user-token",
74
+ "authKey",
75
+ "auth_key",
76
+ "auth-key",
77
+ "securityToken",
78
+ "security_token",
79
+ "security-token"
80
+ ],
81
+ "urlAccessPatterns": [
82
+ "req.query",
83
+ "req.params",
84
+ "searchParams.get",
85
+ "@Query",
86
+ "@Param",
87
+ "URLSearchParams",
88
+ "location.search"
89
+ ],
90
+ "frameworks": ["express", "nestjs", "nextjs", "nuxtjs"]
91
+ }
92
+ }