@vibgrate/cli 1.0.65 → 1.0.67
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.
|
@@ -319,20 +319,6 @@ function formatText(artifact) {
|
|
|
319
319
|
if (artifact.extended?.architecture) {
|
|
320
320
|
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
321
321
|
}
|
|
322
|
-
if (artifact.relationshipDiagram?.mermaid) {
|
|
323
|
-
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
324
|
-
lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
|
|
325
|
-
lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
326
|
-
lines.push("");
|
|
327
|
-
lines.push(chalk.bold(" Mermaid") + chalk.dim(" (copy into https://mermaid.live or a ```mermaid code block)"));
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push(chalk.dim(" ```mermaid"));
|
|
330
|
-
for (const mLine of artifact.relationshipDiagram.mermaid.split("\n")) {
|
|
331
|
-
lines.push(chalk.dim(` ${mLine}`));
|
|
332
|
-
}
|
|
333
|
-
lines.push(chalk.dim(" ```"));
|
|
334
|
-
lines.push("");
|
|
335
|
-
}
|
|
336
322
|
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
337
323
|
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
338
324
|
lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
@@ -1169,6 +1155,262 @@ import * as crypto3 from "crypto";
|
|
|
1169
1155
|
import * as path2 from "path";
|
|
1170
1156
|
import { Command as Command2 } from "commander";
|
|
1171
1157
|
import chalk3 from "chalk";
|
|
1158
|
+
|
|
1159
|
+
// src/utils/compact-artifact.ts
|
|
1160
|
+
import * as zlib from "zlib";
|
|
1161
|
+
import { promisify } from "util";
|
|
1162
|
+
|
|
1163
|
+
// src/utils/compact-evidence.ts
|
|
1164
|
+
var CATEGORY_PATTERNS = [
|
|
1165
|
+
{ category: "pricing", pattern: /price|pricing|billing|subscri|trial|credit|plan|tier|upgrade|premium|pro|enterprise/i },
|
|
1166
|
+
{ category: "auth", pattern: /sign[- ]?in|sign[- ]?up|log[- ]?in|log[- ]?out|auth|sso|oauth|password|register|invite|onboard/i },
|
|
1167
|
+
{ category: "dashboard", pattern: /dashboard|overview|home|main|summary|stats/i },
|
|
1168
|
+
{ category: "settings", pattern: /setting|config|preference|option|profile|account/i },
|
|
1169
|
+
{ category: "users", pattern: /user|member|team|role|permission|access|admin|owner/i },
|
|
1170
|
+
{ category: "integrations", pattern: /integrat|connect|webhook|api[- ]?key|sync|import|export/i },
|
|
1171
|
+
{ category: "reports", pattern: /report|analy|metric|chart|graph|insight|track/i },
|
|
1172
|
+
{ category: "workflows", pattern: /workflow|automat|schedule|trigger|action|job|task|pipeline/i },
|
|
1173
|
+
{ category: "projects", pattern: /project|workspace|organization|folder|repo/i },
|
|
1174
|
+
{ category: "navigation", pattern: /menu|nav|sidebar|header|footer|breadcrumb/i }
|
|
1175
|
+
];
|
|
1176
|
+
function compactUiPurpose(result, maxSamplesPerCategory = 3) {
|
|
1177
|
+
const evidence = result.topEvidence;
|
|
1178
|
+
const dependencies = evidence.filter((e) => e.kind === "dependency").map((e) => e.value).slice(0, 10);
|
|
1179
|
+
const routes = dedupeRoutes(
|
|
1180
|
+
evidence.filter((e) => e.kind === "route").map((e) => e.value)
|
|
1181
|
+
).slice(0, 15);
|
|
1182
|
+
const textEvidence = evidence.filter(
|
|
1183
|
+
(e) => e.kind !== "dependency" && e.kind !== "route" && e.kind !== "feature_flag"
|
|
1184
|
+
);
|
|
1185
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1186
|
+
const categoryCounts = {};
|
|
1187
|
+
for (const item of textEvidence) {
|
|
1188
|
+
const category = categorize(item.value);
|
|
1189
|
+
if (!byCategory.has(category)) {
|
|
1190
|
+
byCategory.set(category, /* @__PURE__ */ new Set());
|
|
1191
|
+
}
|
|
1192
|
+
const normalized = normalizeValue(item.value);
|
|
1193
|
+
if (normalized.length >= 3) {
|
|
1194
|
+
byCategory.get(category).add(normalized);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const samples = [];
|
|
1198
|
+
for (const [category, values] of byCategory) {
|
|
1199
|
+
const deduped = dedupeStrings([...values]);
|
|
1200
|
+
categoryCounts[category] = deduped.length;
|
|
1201
|
+
for (const value of deduped.slice(0, maxSamplesPerCategory)) {
|
|
1202
|
+
samples.push({ kind: "text", value, category });
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const featureFlags = evidence.filter((e) => e.kind === "feature_flag");
|
|
1206
|
+
if (featureFlags.length > 0) {
|
|
1207
|
+
categoryCounts["feature_flags"] = featureFlags.length;
|
|
1208
|
+
samples.push({ kind: "feature_flag", value: "feature flags detected", category: "feature_flags" });
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
samples,
|
|
1212
|
+
categoryCounts,
|
|
1213
|
+
originalCount: evidence.length,
|
|
1214
|
+
dependencies,
|
|
1215
|
+
routes,
|
|
1216
|
+
detectedFrameworks: result.detectedFrameworks
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function categorize(value) {
|
|
1220
|
+
for (const { category, pattern } of CATEGORY_PATTERNS) {
|
|
1221
|
+
if (pattern.test(value)) return category;
|
|
1222
|
+
}
|
|
1223
|
+
return "general";
|
|
1224
|
+
}
|
|
1225
|
+
function normalizeValue(value) {
|
|
1226
|
+
return value.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
1227
|
+
}
|
|
1228
|
+
function dedupeStrings(values) {
|
|
1229
|
+
const sorted = values.sort((a, b) => b.length - a.length);
|
|
1230
|
+
const kept = [];
|
|
1231
|
+
for (const value of sorted) {
|
|
1232
|
+
const isDupe = kept.some((k) => {
|
|
1233
|
+
const stem = value.slice(0, 6);
|
|
1234
|
+
return k.startsWith(stem) || k.includes(value) || value.includes(k);
|
|
1235
|
+
});
|
|
1236
|
+
if (!isDupe) {
|
|
1237
|
+
kept.push(value);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return kept;
|
|
1241
|
+
}
|
|
1242
|
+
function dedupeRoutes(routes) {
|
|
1243
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1244
|
+
const result = [];
|
|
1245
|
+
for (const route of routes) {
|
|
1246
|
+
const normalized = route.replace(/:[a-z_]+/gi, ":param").replace(/\[\[*\.*\.*[a-z_]+\]*\]/gi, ":param").replace(/\/+$/, "").toLowerCase();
|
|
1247
|
+
if (!seen.has(normalized)) {
|
|
1248
|
+
seen.add(normalized);
|
|
1249
|
+
result.push(route);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/utils/compact-artifact.ts
|
|
1256
|
+
var gzip2 = promisify(zlib.gzip);
|
|
1257
|
+
var MAX_ITEMS = 50;
|
|
1258
|
+
function extractName(entry) {
|
|
1259
|
+
const match = entry.match(/^(.+?)\s*\(/);
|
|
1260
|
+
return match ? match[1].trim() : entry.trim();
|
|
1261
|
+
}
|
|
1262
|
+
function compactDataStores(result) {
|
|
1263
|
+
return {
|
|
1264
|
+
databaseTechnologies: result.databaseTechnologies.slice(0, 10),
|
|
1265
|
+
connectionStrings: [],
|
|
1266
|
+
// Don't include connection strings in upload
|
|
1267
|
+
connectionPoolSettings: result.connectionPoolSettings.slice(0, MAX_ITEMS),
|
|
1268
|
+
replicationSettings: result.replicationSettings.slice(0, 20),
|
|
1269
|
+
readReplicaSettings: result.readReplicaSettings.slice(0, 20),
|
|
1270
|
+
failoverSettings: result.failoverSettings.slice(0, 20),
|
|
1271
|
+
collationAndEncoding: result.collationAndEncoding.slice(0, 20),
|
|
1272
|
+
queryTimeoutDefaults: result.queryTimeoutDefaults.slice(0, 20),
|
|
1273
|
+
manualIndexes: result.manualIndexes.map(extractName).slice(0, MAX_ITEMS),
|
|
1274
|
+
tables: result.tables.map(extractName).slice(0, MAX_ITEMS),
|
|
1275
|
+
views: result.views.map(extractName).slice(0, MAX_ITEMS),
|
|
1276
|
+
storedProcedures: result.storedProcedures.map(extractName).slice(0, MAX_ITEMS),
|
|
1277
|
+
triggers: result.triggers.map(extractName).slice(0, MAX_ITEMS),
|
|
1278
|
+
rowLevelSecurityPolicies: result.rowLevelSecurityPolicies.slice(0, 20),
|
|
1279
|
+
otherServices: result.otherServices.slice(0, 20)
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function compactApiSurface(result) {
|
|
1283
|
+
const seenProviders = /* @__PURE__ */ new Set();
|
|
1284
|
+
const uniqueIntegrations = result.integrations.filter((i) => {
|
|
1285
|
+
const domain = i.provider.split(":")[0];
|
|
1286
|
+
if (seenProviders.has(domain)) return false;
|
|
1287
|
+
seenProviders.add(domain);
|
|
1288
|
+
return true;
|
|
1289
|
+
}).slice(0, MAX_ITEMS).map((i) => ({
|
|
1290
|
+
provider: i.provider,
|
|
1291
|
+
endpoint: "",
|
|
1292
|
+
// Don't include full endpoints
|
|
1293
|
+
version: i.version,
|
|
1294
|
+
parameters: [],
|
|
1295
|
+
// Don't include params
|
|
1296
|
+
configOptions: [],
|
|
1297
|
+
authHints: [],
|
|
1298
|
+
files: []
|
|
1299
|
+
// Don't include file paths
|
|
1300
|
+
}));
|
|
1301
|
+
return {
|
|
1302
|
+
integrations: uniqueIntegrations,
|
|
1303
|
+
openApiSpecifications: result.openApiSpecifications.slice(0, 10),
|
|
1304
|
+
webhookUrls: result.webhookUrls.slice(0, 20),
|
|
1305
|
+
callbackEndpoints: result.callbackEndpoints.slice(0, 20),
|
|
1306
|
+
apiVersionPins: result.apiVersionPins.slice(0, 20),
|
|
1307
|
+
tokenExpirationPolicies: result.tokenExpirationPolicies.slice(0, 20),
|
|
1308
|
+
rateLimitOverrides: result.rateLimitOverrides.slice(0, 20),
|
|
1309
|
+
customHeaders: result.customHeaders.slice(0, 20),
|
|
1310
|
+
corsPolicies: result.corsPolicies.slice(0, 20),
|
|
1311
|
+
oauthScopes: result.oauthScopes.slice(0, 20),
|
|
1312
|
+
apiTokens: []
|
|
1313
|
+
// Don't include token references
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function compactAssetBranding(result) {
|
|
1317
|
+
return {
|
|
1318
|
+
faviconFiles: result.faviconFiles.slice(0, 1),
|
|
1319
|
+
productLogos: []
|
|
1320
|
+
// Don't include logos
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function prepareArtifactForUpload(artifact) {
|
|
1324
|
+
const compacted = { ...artifact };
|
|
1325
|
+
if (compacted.extended) {
|
|
1326
|
+
const ext = { ...compacted.extended };
|
|
1327
|
+
if (ext.dataStores) {
|
|
1328
|
+
ext.dataStores = compactDataStores(ext.dataStores);
|
|
1329
|
+
}
|
|
1330
|
+
if (ext.apiSurface) {
|
|
1331
|
+
ext.apiSurface = compactApiSurface(ext.apiSurface);
|
|
1332
|
+
}
|
|
1333
|
+
if (ext.assetBranding) {
|
|
1334
|
+
ext.assetBranding = compactAssetBranding(ext.assetBranding);
|
|
1335
|
+
}
|
|
1336
|
+
if (ext.uiPurpose) {
|
|
1337
|
+
const compactedUi = compactUiPurpose(ext.uiPurpose);
|
|
1338
|
+
ext.uiPurpose = {
|
|
1339
|
+
enabled: ext.uiPurpose.enabled,
|
|
1340
|
+
detectedFrameworks: compactedUi.detectedFrameworks,
|
|
1341
|
+
evidenceCount: compactedUi.originalCount,
|
|
1342
|
+
capped: ext.uiPurpose.capped,
|
|
1343
|
+
topEvidence: [],
|
|
1344
|
+
// Clear full evidence
|
|
1345
|
+
unknownSignals: [],
|
|
1346
|
+
// Add compacted data under extended properties
|
|
1347
|
+
...{ compacted: compactedUi }
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
if (ext.runtimeConfiguration) {
|
|
1351
|
+
ext.runtimeConfiguration = {
|
|
1352
|
+
...ext.runtimeConfiguration,
|
|
1353
|
+
environmentVariables: ext.runtimeConfiguration.environmentVariables.slice(0, 100),
|
|
1354
|
+
hiddenConfigFiles: ext.runtimeConfiguration.hiddenConfigFiles.slice(0, MAX_ITEMS),
|
|
1355
|
+
startupArguments: ext.runtimeConfiguration.startupArguments.slice(0, 100)
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
if (ext.operationalResilience) {
|
|
1359
|
+
const ops = ext.operationalResilience;
|
|
1360
|
+
ext.operationalResilience = {
|
|
1361
|
+
implicitTimeouts: ops.implicitTimeouts.slice(0, 30),
|
|
1362
|
+
defaultPaginationSize: ops.defaultPaginationSize.slice(0, 30),
|
|
1363
|
+
implicitRetryLogic: ops.implicitRetryLogic.slice(0, 30),
|
|
1364
|
+
defaultLocale: ops.defaultLocale.slice(0, 20),
|
|
1365
|
+
defaultCurrency: ops.defaultCurrency.slice(0, 20),
|
|
1366
|
+
implicitTimezone: ops.implicitTimezone.slice(0, 20),
|
|
1367
|
+
defaultCharacterEncoding: ops.defaultCharacterEncoding.slice(0, 20),
|
|
1368
|
+
sessionStores: ops.sessionStores.slice(0, 20),
|
|
1369
|
+
distributedLocks: ops.distributedLocks.slice(0, 20),
|
|
1370
|
+
jobSchedulers: ops.jobSchedulers.slice(0, 30),
|
|
1371
|
+
idempotencyKeys: ops.idempotencyKeys.slice(0, 20),
|
|
1372
|
+
rateLimitingCounters: ops.rateLimitingCounters.slice(0, 20),
|
|
1373
|
+
circuitBreakerState: ops.circuitBreakerState.slice(0, 20),
|
|
1374
|
+
abTestToggles: ops.abTestToggles.slice(0, 20),
|
|
1375
|
+
regionalEnablementRules: ops.regionalEnablementRules.slice(0, 20),
|
|
1376
|
+
betaAccessGroups: ops.betaAccessGroups.slice(0, 20),
|
|
1377
|
+
licensingEnforcementLogic: ops.licensingEnforcementLogic.slice(0, 20),
|
|
1378
|
+
killSwitches: ops.killSwitches.slice(0, 20),
|
|
1379
|
+
connectorRetryLogic: ops.connectorRetryLogic.slice(0, 20),
|
|
1380
|
+
apiPollingIntervals: ops.apiPollingIntervals.slice(0, 20),
|
|
1381
|
+
fieldMappings: ops.fieldMappings.slice(0, 20),
|
|
1382
|
+
schemaRegistryRules: ops.schemaRegistryRules.slice(0, 20),
|
|
1383
|
+
deadLetterQueueBehavior: ops.deadLetterQueueBehavior.slice(0, 20),
|
|
1384
|
+
dataMaskingRules: ops.dataMaskingRules.slice(0, 20),
|
|
1385
|
+
transformationLogic: ops.transformationLogic.slice(0, 20),
|
|
1386
|
+
timezoneHandling: ops.timezoneHandling.slice(0, 20),
|
|
1387
|
+
encryptionSettings: ops.encryptionSettings.slice(0, 30),
|
|
1388
|
+
hardcodedSecretSignals: ops.hardcodedSecretSignals.slice(0, 20)
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
if (ext.dependencyGraph) {
|
|
1392
|
+
ext.dependencyGraph = {
|
|
1393
|
+
...ext.dependencyGraph,
|
|
1394
|
+
phantomDependencies: ext.dependencyGraph.phantomDependencies.slice(0, MAX_ITEMS),
|
|
1395
|
+
phantomDependencyDetails: ext.dependencyGraph.phantomDependencyDetails?.slice(0, MAX_ITEMS),
|
|
1396
|
+
duplicatedPackages: ext.dependencyGraph.duplicatedPackages.slice(0, MAX_ITEMS)
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
compacted.extended = ext;
|
|
1400
|
+
}
|
|
1401
|
+
return compacted;
|
|
1402
|
+
}
|
|
1403
|
+
async function compressArtifact(artifact) {
|
|
1404
|
+
const json = JSON.stringify(artifact);
|
|
1405
|
+
return gzip2(json, { level: 9 });
|
|
1406
|
+
}
|
|
1407
|
+
async function prepareCompressedUpload(artifact) {
|
|
1408
|
+
const compacted = prepareArtifactForUpload(artifact);
|
|
1409
|
+
const compressed = await compressArtifact(compacted);
|
|
1410
|
+
return { body: compressed, contentEncoding: "gzip" };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/commands/push.ts
|
|
1172
1414
|
function parseDsn(dsn) {
|
|
1173
1415
|
const cleaned = dsn.replace(/[\x00-\x1F\x7F\uFEFF\u200B-\u200D\u2060]/g, "").trim();
|
|
1174
1416
|
const match = cleaned.match(/^vibgrate\+(https?):?\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);
|
|
@@ -1203,7 +1445,8 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1203
1445
|
if (opts.strict) process.exit(1);
|
|
1204
1446
|
return;
|
|
1205
1447
|
}
|
|
1206
|
-
const
|
|
1448
|
+
const artifact = await readJsonFile(filePath);
|
|
1449
|
+
const { body, contentEncoding } = await prepareCompressedUpload(artifact);
|
|
1207
1450
|
const timestamp = String(Date.now());
|
|
1208
1451
|
let host = parsed.host;
|
|
1209
1452
|
if (opts.region) {
|
|
@@ -1216,12 +1459,16 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1216
1459
|
}
|
|
1217
1460
|
}
|
|
1218
1461
|
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
1219
|
-
|
|
1462
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
1463
|
+
const compressedSize = body.length;
|
|
1464
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
1465
|
+
console.log(chalk3.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
1220
1466
|
try {
|
|
1221
1467
|
const response = await fetch(url, {
|
|
1222
1468
|
method: "POST",
|
|
1223
1469
|
headers: {
|
|
1224
1470
|
"Content-Type": "application/json",
|
|
1471
|
+
"Content-Encoding": contentEncoding,
|
|
1225
1472
|
"X-Vibgrate-Timestamp": timestamp,
|
|
1226
1473
|
"Authorization": `VibgrateDSN ${parsed.keyId}:${parsed.secret}`,
|
|
1227
1474
|
"Connection": "close"
|
|
@@ -7467,6 +7714,17 @@ function extract(content, pattern, sourceFile) {
|
|
|
7467
7714
|
}
|
|
7468
7715
|
return out;
|
|
7469
7716
|
}
|
|
7717
|
+
var LOCKFILE_NAMES = /* @__PURE__ */ new Set([
|
|
7718
|
+
"package-lock.json",
|
|
7719
|
+
"pnpm-lock.yaml",
|
|
7720
|
+
"yarn.lock",
|
|
7721
|
+
"composer.lock",
|
|
7722
|
+
"Gemfile.lock",
|
|
7723
|
+
"Cargo.lock",
|
|
7724
|
+
"poetry.lock",
|
|
7725
|
+
"Pipfile.lock",
|
|
7726
|
+
"packages.lock.json"
|
|
7727
|
+
]);
|
|
7470
7728
|
function detectDbBrand(raw) {
|
|
7471
7729
|
const value = raw.toLowerCase();
|
|
7472
7730
|
if (value.includes("postgres")) return { kind: "sql", brand: "PostgreSQL", version: null, evidence: raw };
|
|
@@ -7477,6 +7735,7 @@ function detectDbBrand(raw) {
|
|
|
7477
7735
|
if (value.includes("mongodb")) return { kind: "nosql", brand: "MongoDB", version: null, evidence: raw };
|
|
7478
7736
|
if (value.includes("redis")) return { kind: "nosql", brand: "Redis", version: null, evidence: raw };
|
|
7479
7737
|
if (value.includes("cassandra")) return { kind: "nosql", brand: "Cassandra", version: null, evidence: raw };
|
|
7738
|
+
if (value.includes("cosmosdb") || value.includes("@azure/cosmos") || value.includes("cosmos")) return { kind: "nosql", brand: "CosmosDB", version: null, evidence: raw };
|
|
7480
7739
|
if (value.includes("dynamodb")) return { kind: "nosql", brand: "DynamoDB", version: null, evidence: raw };
|
|
7481
7740
|
if (value.includes("neo4j")) return { kind: "nosql", brand: "Neo4j", version: null, evidence: raw };
|
|
7482
7741
|
return null;
|
|
@@ -7558,13 +7817,16 @@ async function scanDataStores(rootDir, fileCache) {
|
|
|
7558
7817
|
for (const file of files) {
|
|
7559
7818
|
const connStrings = extract(file.content, /\b(?:postgres(?:ql)?:\/\/[^\s'"`]+|mysql:\/\/[^\s'"`]+|mongodb(?:\+srv)?:\/\/[^\s'"`]+|redis:\/\/[^\s'"`]+)\b/gi, file.relPath);
|
|
7560
7819
|
result.connectionStrings.push(...connStrings);
|
|
7561
|
-
const
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7820
|
+
const isLockFile = LOCKFILE_NAMES.has(path23.basename(file.relPath));
|
|
7821
|
+
if (!isLockFile) {
|
|
7822
|
+
const dbEvidence = [
|
|
7823
|
+
...extract(file.content, /\b(?:postgres|postgresql|mysql|mariadb|mssql|sqlserver|oracle|sqlite|mongodb|redis|cassandra|cosmosdb|cosmos|dynamodb|neo4j)\b/gi, file.relPath),
|
|
7824
|
+
...connStrings
|
|
7825
|
+
];
|
|
7826
|
+
for (const evidence of dbEvidence) {
|
|
7827
|
+
const detected = detectDbBrand(evidence);
|
|
7828
|
+
if (detected) dbTechnologies.push(detected);
|
|
7829
|
+
}
|
|
7568
7830
|
}
|
|
7569
7831
|
result.connectionPoolSettings.push(...extract(file.content, /\b(?:pool(?:Size|_size)?|maxPoolSize|minPoolSize|connectionLimit)\b[^\n]*/gi, file.relPath));
|
|
7570
7832
|
result.replicationSettings.push(...extract(file.content, /\b(?:replication|cluster|replicaSet)\b[^\n]*/gi, file.relPath));
|
|
@@ -7581,7 +7843,10 @@ async function scanDataStores(rootDir, fileCache) {
|
|
|
7581
7843
|
result.otherServices.push(...extract(file.content, /\b(?:redis:\/\/[^\s'"`]+|amqp:\/\/[^\s'"`]+|kafka:\/\/[^\s'"`]+|elasticsearch:\/\/[^\s'"`]+)\b/gi, file.relPath));
|
|
7582
7844
|
}
|
|
7583
7845
|
const dedupDb = /* @__PURE__ */ new Map();
|
|
7584
|
-
for (const db of dbTechnologies)
|
|
7846
|
+
for (const db of dbTechnologies) {
|
|
7847
|
+
const key = `${db.kind}:${db.brand}`;
|
|
7848
|
+
if (!dedupDb.has(key)) dedupDb.set(key, db);
|
|
7849
|
+
}
|
|
7585
7850
|
result.databaseTechnologies = [...dedupDb.values()].sort((a, b) => a.brand.localeCompare(b.brand));
|
|
7586
7851
|
result.connectionStrings = uniq(result.connectionStrings);
|
|
7587
7852
|
result.connectionPoolSettings = uniq(result.connectionPoolSettings);
|
|
@@ -7611,6 +7876,17 @@ function detectOpenApiSpecification(relPath, content) {
|
|
|
7611
7876
|
const endpointCount = content.match(/(^|\n)\s*\/[^\n:]+:\s*(get|post|put|patch|delete|options|head)/gim)?.length ?? content.match(/"\/[^"]+"\s*:\s*\{/g)?.length ?? null;
|
|
7612
7877
|
return { path: relPath, format, version, title, endpointCount };
|
|
7613
7878
|
}
|
|
7879
|
+
function isInternalHost(hostname) {
|
|
7880
|
+
const h = hostname.toLowerCase();
|
|
7881
|
+
if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0") return true;
|
|
7882
|
+
if (/^192\.168\./.test(h) || /^10\./.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
7883
|
+
if (!h.includes(".")) return true;
|
|
7884
|
+
if (/^(example|test|localhost|local|internal)\b/.test(h)) return true;
|
|
7885
|
+
return false;
|
|
7886
|
+
}
|
|
7887
|
+
function normalizeUrl(raw) {
|
|
7888
|
+
return raw.replace(/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "").trim();
|
|
7889
|
+
}
|
|
7614
7890
|
async function scanApiSurface(rootDir, fileCache) {
|
|
7615
7891
|
const files = await scanTextFiles(rootDir, fileCache);
|
|
7616
7892
|
const integrations = [];
|
|
@@ -7630,18 +7906,29 @@ async function scanApiSurface(rootDir, fileCache) {
|
|
|
7630
7906
|
for (const file of files) {
|
|
7631
7907
|
const openApiSpec = detectOpenApiSpecification(file.relPath, file.content);
|
|
7632
7908
|
if (openApiSpec) result.openApiSpecifications.push(openApiSpec);
|
|
7633
|
-
const
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
const
|
|
7637
|
-
const
|
|
7909
|
+
const urlRegex = /\bhttps?:\/\/[^\s'"`]+/gi;
|
|
7910
|
+
let match;
|
|
7911
|
+
while ((match = urlRegex.exec(file.content)) !== null) {
|
|
7912
|
+
const rawUrl = match[0].replace(/[,;)\]}"'`]+$/, "");
|
|
7913
|
+
const cleanUrl = normalizeUrl(rawUrl);
|
|
7914
|
+
if (!cleanUrl.startsWith("http")) continue;
|
|
7915
|
+
let hostname;
|
|
7916
|
+
try {
|
|
7917
|
+
hostname = new URL(cleanUrl).hostname;
|
|
7918
|
+
} catch {
|
|
7919
|
+
continue;
|
|
7920
|
+
}
|
|
7921
|
+
if (isInternalHost(hostname)) continue;
|
|
7922
|
+
const versionMatch = cleanUrl.match(/\/(v\d+(?:\.\d+)?)\b/i);
|
|
7923
|
+
const params = cleanUrl.match(/[?&]([a-zA-Z0-9_.-]+)=/g)?.map((p) => p.replace(/[?&=]/g, "")) ?? [];
|
|
7638
7924
|
integrations.push({
|
|
7639
|
-
provider,
|
|
7640
|
-
endpoint,
|
|
7925
|
+
provider: hostname,
|
|
7926
|
+
endpoint: cleanUrl,
|
|
7641
7927
|
version: versionMatch ? versionMatch[1] : null,
|
|
7642
7928
|
parameters: params,
|
|
7643
7929
|
configOptions: extract(file.content, /\b(?:baseUrl|apiUrl|endpoint|timeout|retries|apiVersion)\b[^\n]*/gi, file.relPath),
|
|
7644
|
-
authHints: extract(file.content, /\b(?:api[_-]?key|bearer\s+[a-z0-9_.-]+|client[_-]?secret|oauth)\b[^\n]*/gi, file.relPath)
|
|
7930
|
+
authHints: extract(file.content, /\b(?:api[_-]?key|bearer\s+[a-z0-9_.-]+|client[_-]?secret|oauth)\b[^\n]*/gi, file.relPath),
|
|
7931
|
+
files: [file.relPath]
|
|
7645
7932
|
});
|
|
7646
7933
|
}
|
|
7647
7934
|
result.webhookUrls.push(...extract(file.content, /\bhttps?:\/\/[^\s'"`]*(?:webhook|hooks?)[^\s'"`]*/gi, file.relPath));
|
|
@@ -7651,18 +7938,27 @@ async function scanApiSurface(rootDir, fileCache) {
|
|
|
7651
7938
|
result.rateLimitOverrides.push(...extract(file.content, /\b(?:rate[_-]?limit|max[_-]?requests|throttle)\b[^\n]*/gi, file.relPath));
|
|
7652
7939
|
result.customHeaders.push(...extract(file.content, /\b(?:x-[a-z0-9-]+|authorization|user-agent)\b[^\n]*/gi, file.relPath));
|
|
7653
7940
|
result.corsPolicies.push(...extract(file.content, /\b(?:cors|access-control-allow-origin|allowedOrigins)\b[^\n]*/gi, file.relPath));
|
|
7654
|
-
result.oauthScopes.push(...extract(file.content, /\
|
|
7941
|
+
result.oauthScopes.push(...extract(file.content, /\bscopes?\s*[=:]\s*['"][^'"]*(?:openid|profile|email|offline_access|read(?::\w+)?|write(?::\w+)?)[^'"]*['"]/gi, file.relPath));
|
|
7655
7942
|
result.apiTokens.push(...extract(file.content, /\b(?:api[_-]?token|access[_-]?token|bearer[_-]?token)\b[^\n]*/gi, file.relPath));
|
|
7656
7943
|
}
|
|
7657
7944
|
const integrationMap = /* @__PURE__ */ new Map();
|
|
7658
7945
|
for (const integration of integrations) {
|
|
7659
|
-
const key =
|
|
7660
|
-
integrationMap.
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7946
|
+
const key = integration.endpoint;
|
|
7947
|
+
const existing = integrationMap.get(key);
|
|
7948
|
+
if (existing) {
|
|
7949
|
+
existing.files = uniq([...existing.files, ...integration.files]);
|
|
7950
|
+
existing.parameters = uniq([...existing.parameters, ...integration.parameters]);
|
|
7951
|
+
existing.configOptions = uniq([...existing.configOptions, ...integration.configOptions]);
|
|
7952
|
+
existing.authHints = uniq([...existing.authHints, ...integration.authHints]);
|
|
7953
|
+
} else {
|
|
7954
|
+
integrationMap.set(key, {
|
|
7955
|
+
...integration,
|
|
7956
|
+
parameters: uniq(integration.parameters),
|
|
7957
|
+
configOptions: uniq(integration.configOptions),
|
|
7958
|
+
authHints: uniq(integration.authHints),
|
|
7959
|
+
files: [...integration.files]
|
|
7960
|
+
});
|
|
7961
|
+
}
|
|
7666
7962
|
}
|
|
7667
7963
|
result.integrations = [...integrationMap.values()].sort((a, b) => a.provider.localeCompare(b.provider));
|
|
7668
7964
|
result.openApiSpecifications = [...new Map(result.openApiSpecifications.map((spec) => [spec.path, spec])).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -7677,8 +7973,27 @@ async function scanApiSurface(rootDir, fileCache) {
|
|
|
7677
7973
|
result.apiTokens = uniq(result.apiTokens);
|
|
7678
7974
|
return result;
|
|
7679
7975
|
}
|
|
7976
|
+
var NON_CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7977
|
+
".md",
|
|
7978
|
+
".mdx",
|
|
7979
|
+
".markdown",
|
|
7980
|
+
".txt",
|
|
7981
|
+
".rst",
|
|
7982
|
+
".adoc",
|
|
7983
|
+
".asciidoc",
|
|
7984
|
+
".html",
|
|
7985
|
+
".htm",
|
|
7986
|
+
".pdf",
|
|
7987
|
+
".docx",
|
|
7988
|
+
".doc",
|
|
7989
|
+
".rtf"
|
|
7990
|
+
]);
|
|
7680
7991
|
async function scanOperationalResilience(rootDir, fileCache) {
|
|
7681
|
-
const
|
|
7992
|
+
const allFiles = await scanTextFiles(rootDir, fileCache);
|
|
7993
|
+
const files = allFiles.filter((f) => {
|
|
7994
|
+
const ext = path23.extname(f.relPath).toLowerCase();
|
|
7995
|
+
return !NON_CODE_EXTENSIONS.has(ext);
|
|
7996
|
+
});
|
|
7682
7997
|
const result = {
|
|
7683
7998
|
implicitTimeouts: [],
|
|
7684
7999
|
defaultPaginationSize: [],
|
|
@@ -7795,98 +8110,6 @@ async function scanOssGovernance(rootDir, fileCache) {
|
|
|
7795
8110
|
};
|
|
7796
8111
|
}
|
|
7797
8112
|
|
|
7798
|
-
// src/utils/compact-evidence.ts
|
|
7799
|
-
var CATEGORY_PATTERNS = [
|
|
7800
|
-
{ category: "pricing", pattern: /price|pricing|billing|subscri|trial|credit|plan|tier|upgrade|premium|pro|enterprise/i },
|
|
7801
|
-
{ category: "auth", pattern: /sign[- ]?in|sign[- ]?up|log[- ]?in|log[- ]?out|auth|sso|oauth|password|register|invite|onboard/i },
|
|
7802
|
-
{ category: "dashboard", pattern: /dashboard|overview|home|main|summary|stats/i },
|
|
7803
|
-
{ category: "settings", pattern: /setting|config|preference|option|profile|account/i },
|
|
7804
|
-
{ category: "users", pattern: /user|member|team|role|permission|access|admin|owner/i },
|
|
7805
|
-
{ category: "integrations", pattern: /integrat|connect|webhook|api[- ]?key|sync|import|export/i },
|
|
7806
|
-
{ category: "reports", pattern: /report|analy|metric|chart|graph|insight|track/i },
|
|
7807
|
-
{ category: "workflows", pattern: /workflow|automat|schedule|trigger|action|job|task|pipeline/i },
|
|
7808
|
-
{ category: "projects", pattern: /project|workspace|organization|folder|repo/i },
|
|
7809
|
-
{ category: "navigation", pattern: /menu|nav|sidebar|header|footer|breadcrumb/i }
|
|
7810
|
-
];
|
|
7811
|
-
function compactUiPurpose(result, maxSamplesPerCategory = 3) {
|
|
7812
|
-
const evidence = result.topEvidence;
|
|
7813
|
-
const dependencies = evidence.filter((e) => e.kind === "dependency").map((e) => e.value).slice(0, 10);
|
|
7814
|
-
const routes = dedupeRoutes(
|
|
7815
|
-
evidence.filter((e) => e.kind === "route").map((e) => e.value)
|
|
7816
|
-
).slice(0, 15);
|
|
7817
|
-
const textEvidence = evidence.filter(
|
|
7818
|
-
(e) => e.kind !== "dependency" && e.kind !== "route" && e.kind !== "feature_flag"
|
|
7819
|
-
);
|
|
7820
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
7821
|
-
const categoryCounts = {};
|
|
7822
|
-
for (const item of textEvidence) {
|
|
7823
|
-
const category = categorize(item.value);
|
|
7824
|
-
if (!byCategory.has(category)) {
|
|
7825
|
-
byCategory.set(category, /* @__PURE__ */ new Set());
|
|
7826
|
-
}
|
|
7827
|
-
const normalized = normalizeValue(item.value);
|
|
7828
|
-
if (normalized.length >= 3) {
|
|
7829
|
-
byCategory.get(category).add(normalized);
|
|
7830
|
-
}
|
|
7831
|
-
}
|
|
7832
|
-
const samples = [];
|
|
7833
|
-
for (const [category, values] of byCategory) {
|
|
7834
|
-
const deduped = dedupeStrings([...values]);
|
|
7835
|
-
categoryCounts[category] = deduped.length;
|
|
7836
|
-
for (const value of deduped.slice(0, maxSamplesPerCategory)) {
|
|
7837
|
-
samples.push({ kind: "text", value, category });
|
|
7838
|
-
}
|
|
7839
|
-
}
|
|
7840
|
-
const featureFlags = evidence.filter((e) => e.kind === "feature_flag");
|
|
7841
|
-
if (featureFlags.length > 0) {
|
|
7842
|
-
categoryCounts["feature_flags"] = featureFlags.length;
|
|
7843
|
-
samples.push({ kind: "feature_flag", value: "feature flags detected", category: "feature_flags" });
|
|
7844
|
-
}
|
|
7845
|
-
return {
|
|
7846
|
-
samples,
|
|
7847
|
-
categoryCounts,
|
|
7848
|
-
originalCount: evidence.length,
|
|
7849
|
-
dependencies,
|
|
7850
|
-
routes,
|
|
7851
|
-
detectedFrameworks: result.detectedFrameworks
|
|
7852
|
-
};
|
|
7853
|
-
}
|
|
7854
|
-
function categorize(value) {
|
|
7855
|
-
for (const { category, pattern } of CATEGORY_PATTERNS) {
|
|
7856
|
-
if (pattern.test(value)) return category;
|
|
7857
|
-
}
|
|
7858
|
-
return "general";
|
|
7859
|
-
}
|
|
7860
|
-
function normalizeValue(value) {
|
|
7861
|
-
return value.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
7862
|
-
}
|
|
7863
|
-
function dedupeStrings(values) {
|
|
7864
|
-
const sorted = values.sort((a, b) => b.length - a.length);
|
|
7865
|
-
const kept = [];
|
|
7866
|
-
for (const value of sorted) {
|
|
7867
|
-
const isDupe = kept.some((k) => {
|
|
7868
|
-
const stem = value.slice(0, 6);
|
|
7869
|
-
return k.startsWith(stem) || k.includes(value) || value.includes(k);
|
|
7870
|
-
});
|
|
7871
|
-
if (!isDupe) {
|
|
7872
|
-
kept.push(value);
|
|
7873
|
-
}
|
|
7874
|
-
}
|
|
7875
|
-
return kept;
|
|
7876
|
-
}
|
|
7877
|
-
function dedupeRoutes(routes) {
|
|
7878
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7879
|
-
const result = [];
|
|
7880
|
-
for (const route of routes) {
|
|
7881
|
-
const normalized = route.replace(/:[a-z_]+/gi, ":param").replace(/\[\[*\.*\.*[a-z_]+\]*\]/gi, ":param").replace(/\/+$/, "").toLowerCase();
|
|
7882
|
-
if (!seen.has(normalized)) {
|
|
7883
|
-
seen.add(normalized);
|
|
7884
|
-
result.push(route);
|
|
7885
|
-
}
|
|
7886
|
-
}
|
|
7887
|
-
return result;
|
|
7888
|
-
}
|
|
7889
|
-
|
|
7890
8113
|
// src/utils/tool-installer.ts
|
|
7891
8114
|
import { spawn as spawn5 } from "child_process";
|
|
7892
8115
|
import chalk5 from "chalk";
|
|
@@ -8745,12 +8968,10 @@ async function runScan(rootDir, opts) {
|
|
|
8745
8968
|
steps: progress.getStepTimings()
|
|
8746
8969
|
});
|
|
8747
8970
|
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
8971
|
+
const projectScores = {};
|
|
8748
8972
|
for (const project of allProjects) {
|
|
8749
8973
|
if (project.drift && project.path) {
|
|
8750
|
-
|
|
8751
|
-
const projectVibgrateDir = path24.join(projectDir, ".vibgrate");
|
|
8752
|
-
await ensureDir(projectVibgrateDir);
|
|
8753
|
-
await writeJsonFile(path24.join(projectVibgrateDir, "project_score.json"), {
|
|
8974
|
+
projectScores[project.path] = {
|
|
8754
8975
|
projectId: project.projectId,
|
|
8755
8976
|
name: project.name,
|
|
8756
8977
|
type: project.type,
|
|
@@ -8763,9 +8984,14 @@ async function runScan(rootDir, opts) {
|
|
|
8763
8984
|
vibgrateVersion: VERSION,
|
|
8764
8985
|
solutionId: project.solutionId,
|
|
8765
8986
|
solutionName: project.solutionName
|
|
8766
|
-
}
|
|
8987
|
+
};
|
|
8767
8988
|
}
|
|
8768
8989
|
}
|
|
8990
|
+
if (Object.keys(projectScores).length > 0) {
|
|
8991
|
+
const vibgrateDir = path24.join(rootDir, ".vibgrate");
|
|
8992
|
+
await ensureDir(vibgrateDir);
|
|
8993
|
+
await writeJsonFile(path24.join(vibgrateDir, "project_scores.json"), projectScores);
|
|
8994
|
+
}
|
|
8769
8995
|
}
|
|
8770
8996
|
if (opts.format === "json") {
|
|
8771
8997
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
@@ -8836,7 +9062,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
8836
9062
|
if (opts.strict) process.exit(1);
|
|
8837
9063
|
return;
|
|
8838
9064
|
}
|
|
8839
|
-
const body =
|
|
9065
|
+
const { body, contentEncoding } = await prepareCompressedUpload(artifact);
|
|
8840
9066
|
const timestamp = String(Date.now());
|
|
8841
9067
|
let host = parsed.host;
|
|
8842
9068
|
if (opts.region) {
|
|
@@ -8849,12 +9075,16 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
8849
9075
|
}
|
|
8850
9076
|
}
|
|
8851
9077
|
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
8852
|
-
|
|
9078
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
9079
|
+
const compressedSize = body.length;
|
|
9080
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
9081
|
+
console.log(chalk6.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
8853
9082
|
try {
|
|
8854
9083
|
const response = await fetch(url, {
|
|
8855
9084
|
method: "POST",
|
|
8856
9085
|
headers: {
|
|
8857
9086
|
"Content-Type": "application/json",
|
|
9087
|
+
"Content-Encoding": contentEncoding,
|
|
8858
9088
|
"X-Vibgrate-Timestamp": timestamp,
|
|
8859
9089
|
"Authorization": `VibgrateDSN ${parsed.keyId}:${parsed.secret}`,
|
|
8860
9090
|
"Connection": "close"
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
baselineCommand
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NASOHLRL.js";
|
|
5
5
|
import {
|
|
6
6
|
VERSION,
|
|
7
7
|
dsnCommand,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
pushCommand,
|
|
11
11
|
scanCommand,
|
|
12
12
|
writeDefaultConfig
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-56UAFVE4.js";
|
|
14
14
|
import {
|
|
15
15
|
ensureDir,
|
|
16
16
|
pathExists,
|
|
@@ -39,7 +39,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
39
39
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
40
40
|
}
|
|
41
41
|
if (opts.baseline) {
|
|
42
|
-
const { runBaseline } = await import("./baseline-
|
|
42
|
+
const { runBaseline } = await import("./baseline-YGEO4GK7.js");
|
|
43
43
|
await runBaseline(rootDir);
|
|
44
44
|
}
|
|
45
45
|
console.log("");
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED