getaura 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1402 -939
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/guides/security-headers.md +58 -0
- package/templates/vite/ErrorBoundary.tsx +32 -0
- package/templates/vite/NotFound.tsx +15 -0
package/dist/index.js
CHANGED
|
@@ -9,8 +9,8 @@ var __export = (target, all) => {
|
|
|
9
9
|
import { Command, Option } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/apply.ts
|
|
12
|
-
import { existsSync as
|
|
13
|
-
import
|
|
12
|
+
import { existsSync as existsSync27, rmSync as rmSync5 } from "fs";
|
|
13
|
+
import path45 from "path";
|
|
14
14
|
|
|
15
15
|
// ../shared/src/types.ts
|
|
16
16
|
var CATEGORIES = ["security", "agent-setup", "foundations", "structure"];
|
|
@@ -128,8 +128,42 @@ function findSecrets(text) {
|
|
|
128
128
|
out.push({ patternId: p2.id, label: p2.label, service: p2.service, confidence: p2.confidence, value, index: m.index ?? 0 });
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
const inside = (m, o) => m !== o && m.index >= o.index && m.index + m.value.length <= o.index + o.value.length;
|
|
132
|
+
const specific = out.filter((m) => !(m.patternId === "generic-assignment" && out.some((o) => o.patternId !== "generic-assignment" && inside(o, m))));
|
|
133
|
+
specific.sort((a, b) => a.index - b.index || b.value.length - a.value.length);
|
|
134
|
+
return specific.filter((m, i) => !specific.slice(0, i).some((o) => inside(m, o)));
|
|
135
|
+
}
|
|
136
|
+
var PLACEHOLDER = /example|placeholder|your[_-]?|xxx|changeme|change[_-]me|dummy|fake|mock|sample|redacted|replace|insert|<[^>]*>|\{\{|\.\.\.|\*\*\*|(?:^|[_-])test|test[_-]|invalid|synthetic/i;
|
|
137
|
+
var KEY_PLACEHOLDER = /x{8,}|X{8,}|EXAMPLE|\.\.\./;
|
|
138
|
+
var COMMENT_LINE = /^\s*(?:\/\/|#|\*|\/\*|<!--|--\s)/;
|
|
139
|
+
var WORDS_ONLY = /^[A-Za-z]+(?:[-_.:][A-Za-z]+)+$/;
|
|
140
|
+
var LOCAL_HOST = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|host\.docker\.internal|\[::1\])$/i;
|
|
141
|
+
function lineAt(text, index) {
|
|
142
|
+
const start = text.lastIndexOf("\n", index - 1) + 1;
|
|
143
|
+
const end = text.indexOf("\n", index);
|
|
144
|
+
return text.slice(start, end === -1 ? void 0 : end);
|
|
145
|
+
}
|
|
146
|
+
var assignedValue = (match) => match.match(/[:=]\s*["'`]([^"'`]*)["'`]\s*$/)?.[1] ?? match;
|
|
147
|
+
function isPlaceholderSecret(m, text) {
|
|
148
|
+
if (m.patternId === "database-url") {
|
|
149
|
+
const [, password = "", host = ""] = m.value.match(/^[a-z+]+:\/\/[^:]+:([^@]*)@([^:/?]+)/i) ?? [];
|
|
150
|
+
if (/^[[<{$]|passw|pwd|secret|changeme|your|example|xxx|\*{3,}/i.test(password)) return true;
|
|
151
|
+
return LOCAL_HOST.test(host) || !host.includes(".");
|
|
152
|
+
}
|
|
153
|
+
if (m.patternId === "private-key") {
|
|
154
|
+
const body = m.value.replace(/-----[^-]+-----/g, "").replace(/\\n|\s|"/g, "");
|
|
155
|
+
return body.length < 64 || body.includes("...");
|
|
156
|
+
}
|
|
157
|
+
if (m.patternId === "generic-assignment") {
|
|
158
|
+
const value = assignedValue(m.value);
|
|
159
|
+
if (PLACEHOLDER.test(value) || WORDS_ONLY.test(value) || /^env\(/i.test(value)) return true;
|
|
160
|
+
if (COMMENT_LINE.test(lineAt(text, m.index))) return true;
|
|
161
|
+
return /^[0-9a-f]{32}$/i.test(value) && /algolia|docsearch/i.test(text);
|
|
162
|
+
}
|
|
163
|
+
return KEY_PLACEHOLDER.test(m.value);
|
|
164
|
+
}
|
|
165
|
+
function findLeakedSecrets(text) {
|
|
166
|
+
return findSecrets(text).filter((m) => !isPlaceholderSecret(m, text));
|
|
133
167
|
}
|
|
134
168
|
function maskSecret(value) {
|
|
135
169
|
if (value.includes("PRIVATE KEY")) return "-----BEGIN PRIVATE KEY----- [masked]";
|
|
@@ -978,10 +1012,10 @@ function mergeDefs(...defs) {
|
|
|
978
1012
|
function cloneDef(schema) {
|
|
979
1013
|
return mergeDefs(schema._zod.def);
|
|
980
1014
|
}
|
|
981
|
-
function getElementAtPath(obj,
|
|
982
|
-
if (!
|
|
1015
|
+
function getElementAtPath(obj, path55) {
|
|
1016
|
+
if (!path55)
|
|
983
1017
|
return obj;
|
|
984
|
-
return
|
|
1018
|
+
return path55.reduce((acc, key) => acc?.[key], obj);
|
|
985
1019
|
}
|
|
986
1020
|
function promiseAllObject(promisesObj) {
|
|
987
1021
|
const keys = Object.keys(promisesObj);
|
|
@@ -1321,11 +1355,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
1321
1355
|
}
|
|
1322
1356
|
return false;
|
|
1323
1357
|
}
|
|
1324
|
-
function prefixIssues(
|
|
1358
|
+
function prefixIssues(path55, issues) {
|
|
1325
1359
|
return issues.map((iss) => {
|
|
1326
1360
|
var _a3;
|
|
1327
1361
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
1328
|
-
iss.path.unshift(
|
|
1362
|
+
iss.path.unshift(path55);
|
|
1329
1363
|
return iss;
|
|
1330
1364
|
});
|
|
1331
1365
|
}
|
|
@@ -1775,16 +1809,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
|
|
|
1775
1809
|
}
|
|
1776
1810
|
function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
1777
1811
|
const fieldErrors = { _errors: [] };
|
|
1778
|
-
const processError = (error63,
|
|
1812
|
+
const processError = (error63, path55 = []) => {
|
|
1779
1813
|
for (const issue2 of error63.issues) {
|
|
1780
1814
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
1781
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
1815
|
+
issue2.errors.map((issues) => processError({ issues }, [...path55, ...issue2.path]));
|
|
1782
1816
|
} else if (issue2.code === "invalid_key") {
|
|
1783
|
-
processError({ issues: issue2.issues }, [...
|
|
1817
|
+
processError({ issues: issue2.issues }, [...path55, ...issue2.path]);
|
|
1784
1818
|
} else if (issue2.code === "invalid_element") {
|
|
1785
|
-
processError({ issues: issue2.issues }, [...
|
|
1819
|
+
processError({ issues: issue2.issues }, [...path55, ...issue2.path]);
|
|
1786
1820
|
} else {
|
|
1787
|
-
const fullpath = [...
|
|
1821
|
+
const fullpath = [...path55, ...issue2.path];
|
|
1788
1822
|
if (fullpath.length === 0) {
|
|
1789
1823
|
fieldErrors._errors.push(mapper(issue2));
|
|
1790
1824
|
} else {
|
|
@@ -1823,17 +1857,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
|
1823
1857
|
}
|
|
1824
1858
|
function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
1825
1859
|
const result = { errors: [] };
|
|
1826
|
-
const processError = (error63,
|
|
1860
|
+
const processError = (error63, path55 = []) => {
|
|
1827
1861
|
var _a3;
|
|
1828
1862
|
for (const issue2 of error63.issues) {
|
|
1829
1863
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
1830
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
1864
|
+
issue2.errors.map((issues) => processError({ issues }, [...path55, ...issue2.path]));
|
|
1831
1865
|
} else if (issue2.code === "invalid_key") {
|
|
1832
|
-
processError({ issues: issue2.issues }, [...
|
|
1866
|
+
processError({ issues: issue2.issues }, [...path55, ...issue2.path]);
|
|
1833
1867
|
} else if (issue2.code === "invalid_element") {
|
|
1834
|
-
processError({ issues: issue2.issues }, [...
|
|
1868
|
+
processError({ issues: issue2.issues }, [...path55, ...issue2.path]);
|
|
1835
1869
|
} else {
|
|
1836
|
-
const fullpath = [...
|
|
1870
|
+
const fullpath = [...path55, ...issue2.path];
|
|
1837
1871
|
if (fullpath.length === 0) {
|
|
1838
1872
|
result.errors.push(mapper(issue2));
|
|
1839
1873
|
continue;
|
|
@@ -1872,8 +1906,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
|
1872
1906
|
}
|
|
1873
1907
|
function toDotPath(_path) {
|
|
1874
1908
|
const segs = [];
|
|
1875
|
-
const
|
|
1876
|
-
for (const seg of
|
|
1909
|
+
const path55 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
1910
|
+
for (const seg of path55) {
|
|
1877
1911
|
if (typeof seg === "number")
|
|
1878
1912
|
segs.push(`[${seg}]`);
|
|
1879
1913
|
else if (typeof seg === "symbol")
|
|
@@ -3250,12 +3284,12 @@ function isIso7064Mod97(iban3) {
|
|
|
3250
3284
|
let remainder = 0;
|
|
3251
3285
|
const len = iban3.length;
|
|
3252
3286
|
for (let i = 4; i < len; i++) {
|
|
3253
|
-
const
|
|
3254
|
-
remainder = (
|
|
3287
|
+
const code2 = iban3.charCodeAt(i);
|
|
3288
|
+
remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
|
|
3255
3289
|
}
|
|
3256
3290
|
for (let i = 0; i < 4; i++) {
|
|
3257
|
-
const
|
|
3258
|
-
remainder = (
|
|
3291
|
+
const code2 = iban3.charCodeAt(i);
|
|
3292
|
+
remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
|
|
3259
3293
|
}
|
|
3260
3294
|
return remainder === 1;
|
|
3261
3295
|
}
|
|
@@ -13173,12 +13207,12 @@ function compileFn(schema, options) {
|
|
|
13173
13207
|
doc.write(outputAccessor === null ? `return true;` : `return ${outputAccessor};`);
|
|
13174
13208
|
const constantNames = ["INVALID", ...ctx.constants.keys()];
|
|
13175
13209
|
const constantValues = [INVALID, ...ctx.constants.values()];
|
|
13176
|
-
const
|
|
13210
|
+
const code2 = doc.content.join("\n");
|
|
13177
13211
|
const fullCode = options?.debug ? constantNames.length > 0 ? `// Constants: ${constantNames.join(", ")}
|
|
13178
|
-
${
|
|
13212
|
+
${code2}` : code2 : "";
|
|
13179
13213
|
const F = Function;
|
|
13180
13214
|
const factoryCode = `return (input) => {
|
|
13181
|
-
${
|
|
13215
|
+
${code2}
|
|
13182
13216
|
}`;
|
|
13183
13217
|
let fn;
|
|
13184
13218
|
try {
|
|
@@ -16318,8 +16352,8 @@ var contributors = {
|
|
|
16318
16352
|
function aggregateChecks(schema) {
|
|
16319
16353
|
const agg = {};
|
|
16320
16354
|
const def = schema._zod.def;
|
|
16321
|
-
const
|
|
16322
|
-
for (const ch of
|
|
16355
|
+
const list11 = schema._zod.traits.has("$ZodCheck") ? [schema, ...def.checks ?? []] : def.checks ?? [];
|
|
16356
|
+
for (const ch of list11)
|
|
16323
16357
|
contributors[ch._zod.def.check]?.(agg, ch._zod.def);
|
|
16324
16358
|
const bag = schema._zod.bag;
|
|
16325
16359
|
if (bag.minimum !== void 0)
|
|
@@ -18975,13 +19009,13 @@ function resolveRef(ref, ctx) {
|
|
|
18975
19009
|
if (!ref.startsWith("#")) {
|
|
18976
19010
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
18977
19011
|
}
|
|
18978
|
-
const
|
|
18979
|
-
if (
|
|
19012
|
+
const path55 = ref.slice(1).split("/").filter(Boolean);
|
|
19013
|
+
if (path55.length === 0) {
|
|
18980
19014
|
return ctx.rootSchema;
|
|
18981
19015
|
}
|
|
18982
19016
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
18983
|
-
if (
|
|
18984
|
-
const key =
|
|
19017
|
+
if (path55[0] === defsKey) {
|
|
19018
|
+
const key = path55[1] === void 0 ? void 0 : decodeJSONPointerSegment(path55[1]);
|
|
18985
19019
|
if (!key || !ctx.defs[key]) {
|
|
18986
19020
|
throw new Error(`Reference not found: ${ref}`);
|
|
18987
19021
|
}
|
|
@@ -20189,6 +20223,29 @@ var SECURITY_3 = {
|
|
|
20189
20223
|
facts: external_exports.strictObject({}),
|
|
20190
20224
|
items: { "client-imported": modules, unguarded: modules }
|
|
20191
20225
|
},
|
|
20226
|
+
/**
|
|
20227
|
+
* Supabase edge functions that use admin access or paid keys without checking the caller.
|
|
20228
|
+
* `jwtOff`: `verify_jwt = false` in supabase/config.toml. Part facts: functions seen, ones with a check, ones marked aura-ignore.
|
|
20229
|
+
*/
|
|
20230
|
+
"security.edge-functions": {
|
|
20231
|
+
v: 1,
|
|
20232
|
+
facts: external_exports.strictObject({ functions: count, guarded: count, ignored: count }),
|
|
20233
|
+
items: {
|
|
20234
|
+
"open-admin": external_exports.strictObject({ jwtOff: flag }),
|
|
20235
|
+
"open-keys": external_exports.strictObject({ jwtOff: flag, keys: count })
|
|
20236
|
+
}
|
|
20237
|
+
},
|
|
20238
|
+
/** Single-page apps (Vite): server keys and server-only packages in code that ships to the browser. */
|
|
20239
|
+
"security.browser-secrets": {
|
|
20240
|
+
v: 1,
|
|
20241
|
+
/** `files`: browser code files looked at. */
|
|
20242
|
+
facts: external_exports.strictObject({ files: count }),
|
|
20243
|
+
items: {
|
|
20244
|
+
"public-env": external_exports.strictObject({ names: count }),
|
|
20245
|
+
"server-package": external_exports.strictObject({ packages: count }),
|
|
20246
|
+
"env-example": external_exports.strictObject({ names: count })
|
|
20247
|
+
}
|
|
20248
|
+
},
|
|
20192
20249
|
"security.risky-code": {
|
|
20193
20250
|
v: 1,
|
|
20194
20251
|
facts: external_exports.strictObject({ files: count, flaggedFiles: count }),
|
|
@@ -20412,6 +20469,12 @@ var FOUNDATIONS_1 = {
|
|
|
20412
20469
|
"no-loading": external_exports.strictObject({ app: count, pages: count, routes: count })
|
|
20413
20470
|
}
|
|
20414
20471
|
},
|
|
20472
|
+
/** Single-page apps (Vite): an error boundary, and a "page not found" route when the app has a router. */
|
|
20473
|
+
"foundations.spa-errors": {
|
|
20474
|
+
v: 1,
|
|
20475
|
+
facts: external_exports.strictObject({ router: flag }),
|
|
20476
|
+
items: { "no-error-boundary": external_exports.strictObject({}), "no-catch-all": external_exports.strictObject({}) }
|
|
20477
|
+
},
|
|
20415
20478
|
"foundations.leftovers": {
|
|
20416
20479
|
v: 1,
|
|
20417
20480
|
/** App files read. */
|
|
@@ -20810,9 +20873,9 @@ function run(cmd, args, opts = {}) {
|
|
|
20810
20873
|
if (timer) clearTimeout(timer);
|
|
20811
20874
|
resolve({ code: 127, stdout, stderr: stderr + String(err) });
|
|
20812
20875
|
});
|
|
20813
|
-
child.on("close", (
|
|
20876
|
+
child.on("close", (code2) => {
|
|
20814
20877
|
if (timer) clearTimeout(timer);
|
|
20815
|
-
resolve({ code:
|
|
20878
|
+
resolve({ code: code2 ?? 1, stdout, stderr, timedOut });
|
|
20816
20879
|
});
|
|
20817
20880
|
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
20818
20881
|
else child.stdin.end();
|
|
@@ -21076,11 +21139,11 @@ var github = {
|
|
|
21076
21139
|
isPrivate: Boolean(info.isPrivate),
|
|
21077
21140
|
defaultBranch: info.defaultBranchRef?.name || "main",
|
|
21078
21141
|
gh,
|
|
21079
|
-
get: (
|
|
21080
|
-
let result = cache3.get(
|
|
21142
|
+
get: (path55) => {
|
|
21143
|
+
let result = cache3.get(path55);
|
|
21081
21144
|
if (!result) {
|
|
21082
|
-
result = gh(["api",
|
|
21083
|
-
cache3.set(
|
|
21145
|
+
result = gh(["api", path55]);
|
|
21146
|
+
cache3.set(path55, result);
|
|
21084
21147
|
}
|
|
21085
21148
|
return result;
|
|
21086
21149
|
}
|
|
@@ -21331,11 +21394,11 @@ var supabase = {
|
|
|
21331
21394
|
return result;
|
|
21332
21395
|
}
|
|
21333
21396
|
};
|
|
21334
|
-
const
|
|
21335
|
-
if (
|
|
21397
|
+
const read3 = await call("GET", `/v1/projects/${ref}`);
|
|
21398
|
+
if (read3.status === 401)
|
|
21336
21399
|
return { ok: false, problem: "Your Supabase login has expired. Run `npx supabase login`, then try again.", errorType: "CliNotSignedIn" };
|
|
21337
|
-
const project =
|
|
21338
|
-
if (
|
|
21400
|
+
const project = read3.json;
|
|
21401
|
+
if (read3.status !== 200 || !project?.name)
|
|
21339
21402
|
return {
|
|
21340
21403
|
ok: false,
|
|
21341
21404
|
// Projects created through Vercel's Supabase integration live in a Supabase account linked to Vercel.
|
|
@@ -21433,14 +21496,14 @@ var parse4 = (r) => {
|
|
|
21433
21496
|
};
|
|
21434
21497
|
var projectInfo = async (ctx) => await ctx.get(`/v9/projects/${ctx.projectId}${ctx.team}`);
|
|
21435
21498
|
async function exposedSecrets(ctx) {
|
|
21436
|
-
const
|
|
21437
|
-
const envs =
|
|
21499
|
+
const list11 = await ctx.get(`/v10/projects/${ctx.projectId}/env?decrypt=true${ctx.team.replace("?", "&")}`);
|
|
21500
|
+
const envs = list11?.envs ?? [];
|
|
21438
21501
|
return [...new Set(envs.filter((e) => PUBLIC_PREFIX.test(e.key) && (SECRET_NAME.test(e.key) || e.value && containsSecret(e.value))).map((e) => e.key))];
|
|
21439
21502
|
}
|
|
21440
21503
|
async function envReading(ctx) {
|
|
21441
|
-
const
|
|
21442
|
-
if (!Array.isArray(
|
|
21443
|
-
const names = (keep) => [...new Set(
|
|
21504
|
+
const list11 = await ctx.get(`/v10/projects/${ctx.projectId}/env${ctx.team}`);
|
|
21505
|
+
if (!Array.isArray(list11?.envs)) throw new Error("Vercel didn\u2019t return the environment variables");
|
|
21506
|
+
const names = (keep) => [...new Set(list11.envs?.filter(keep).map((e) => e.key))].sort();
|
|
21444
21507
|
const targets2 = (e) => Array.isArray(e.target) ? e.target : e.target ? [e.target] : [];
|
|
21445
21508
|
return {
|
|
21446
21509
|
production: names((e) => targets2(e).includes("production")),
|
|
@@ -21842,6 +21905,15 @@ var ENV_SERVICES = [
|
|
|
21842
21905
|
[/^GOOGLE_/, "Google Cloud"],
|
|
21843
21906
|
[/^SLACK_/, "Slack"]
|
|
21844
21907
|
];
|
|
21908
|
+
var EDGE_FUNCTION = /(?:^|\/)supabase\/functions\//;
|
|
21909
|
+
var SUPABASE_FUNCTION_ENV = /* @__PURE__ */ new Set([
|
|
21910
|
+
"SUPABASE_URL",
|
|
21911
|
+
"SUPABASE_ANON_KEY",
|
|
21912
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
21913
|
+
"SUPABASE_DB_URL",
|
|
21914
|
+
"SUPABASE_PUBLISHABLE_KEY",
|
|
21915
|
+
"SUPABASE_SECRET_KEY"
|
|
21916
|
+
]);
|
|
21845
21917
|
var BUILTIN_ENV = /* @__PURE__ */ new Set([
|
|
21846
21918
|
"USERPROFILE",
|
|
21847
21919
|
"APPDATA",
|
|
@@ -21894,15 +21966,16 @@ function inStringLiteral(text, index) {
|
|
|
21894
21966
|
const top = stack.at(-1);
|
|
21895
21967
|
return top === "'" || top === '"' || top === "`";
|
|
21896
21968
|
}
|
|
21897
|
-
function findEnvRefs(files,
|
|
21969
|
+
function findEnvRefs(files, read3) {
|
|
21898
21970
|
const map2 = /* @__PURE__ */ new Map();
|
|
21899
21971
|
for (const f of files) {
|
|
21900
21972
|
if (!isSourceFile(f) || isTestFile(f) || /(?:^|\/)(?:node_modules|\.next)\//.test(f)) continue;
|
|
21901
|
-
const text =
|
|
21973
|
+
const text = read3(f);
|
|
21902
21974
|
if (!text) continue;
|
|
21903
21975
|
for (const m of text.matchAll(ENV_REF)) {
|
|
21904
21976
|
const name = m[1] ?? m[2] ?? m[3] ?? m[4];
|
|
21905
21977
|
if (!name || /[a-z]/.test(name) || BUILTIN_ENV.has(name) || inStringLiteral(text, m.index)) continue;
|
|
21978
|
+
if (EDGE_FUNCTION.test(f) && SUPABASE_FUNCTION_ENV.has(name)) continue;
|
|
21906
21979
|
const fileSet = map2.get(name);
|
|
21907
21980
|
if (fileSet) fileSet.add(f);
|
|
21908
21981
|
else map2.set(name, /* @__PURE__ */ new Set([f]));
|
|
@@ -21915,7 +21988,7 @@ function findEnvRefs(files, read2) {
|
|
|
21915
21988
|
public: /^(?:NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_|PUBLIC_)/.test(name)
|
|
21916
21989
|
}));
|
|
21917
21990
|
}
|
|
21918
|
-
function detectStack(root, files, pkg2,
|
|
21991
|
+
function detectStack(root, files, pkg2, read3) {
|
|
21919
21992
|
const deps2 = allDeps(pkg2);
|
|
21920
21993
|
const has2 = (f) => files.includes(f) || existsSync5(path6.join(root, f));
|
|
21921
21994
|
const hasDir = (d) => files.some((f) => f.startsWith(`${d}/`));
|
|
@@ -21944,6 +22017,11 @@ function detectStack(root, files, pkg2, read2) {
|
|
|
21944
22017
|
const pages = hasDir(`${base2}pages`);
|
|
21945
22018
|
nextjs = { version: deps2.next.replace(/^[^\d]*/, ""), router: app2 && pages ? "both" : app2 ? "app" : pages ? "pages" : null, srcDir };
|
|
21946
22019
|
}
|
|
22020
|
+
const serverRendered = ["@remix-run/dev", "@react-router/dev", "@tanstack/react-start", "vike", "vite-plugin-ssr"].some((d) => deps2[d]);
|
|
22021
|
+
let vite = null;
|
|
22022
|
+
if (!nextjs && !serverRendered && deps2.vite && deps2.react && files.some((f) => /^vite\.config\.[cm]?[jt]s$/.test(f))) {
|
|
22023
|
+
vite = { router: deps2["react-router-dom"] || deps2["react-router"] ? "react-router" : null, lovable: Boolean(deps2["lovable-tagger"]) };
|
|
22024
|
+
}
|
|
21947
22025
|
let supabase2 = null;
|
|
21948
22026
|
if (services.has("Supabase")) {
|
|
21949
22027
|
supabase2 = { migrationsDir: hasDir("supabase/migrations") ? "supabase/migrations" : null, hasConfig: has2("supabase/config.toml") };
|
|
@@ -21954,18 +22032,19 @@ function detectStack(root, files, pkg2, read2) {
|
|
|
21954
22032
|
else if (deps2["@playwright/test"]) testRunner = "playwright";
|
|
21955
22033
|
else if (deps2.mocha) testRunner = "mocha";
|
|
21956
22034
|
else if (pkg2?.scripts?.test?.includes("node --test")) testRunner = "node:test";
|
|
21957
|
-
const envVars = findEnvRefs(files,
|
|
22035
|
+
const envVars = findEnvRefs(files, read3);
|
|
21958
22036
|
for (const v of envVars) if (v.service) services.add(v.service);
|
|
21959
22037
|
return {
|
|
21960
22038
|
packageManager,
|
|
21961
22039
|
frameworks,
|
|
21962
22040
|
services: [...services].sort(),
|
|
21963
22041
|
nextjs,
|
|
22042
|
+
vite,
|
|
21964
22043
|
supabase: supabase2,
|
|
21965
22044
|
typescript: has2("tsconfig.json"),
|
|
21966
22045
|
testRunner,
|
|
21967
22046
|
envVars,
|
|
21968
|
-
recognized: Boolean(nextjs)
|
|
22047
|
+
recognized: Boolean(nextjs || vite)
|
|
21969
22048
|
};
|
|
21970
22049
|
}
|
|
21971
22050
|
function pmCommands(pm) {
|
|
@@ -22069,7 +22148,7 @@ function detectKind(input2) {
|
|
|
22069
22148
|
confidence,
|
|
22070
22149
|
evidence: [...why, ...evidence]
|
|
22071
22150
|
});
|
|
22072
|
-
const fw = (
|
|
22151
|
+
const fw = (list11) => frameworks.find((f) => list11.includes(f));
|
|
22073
22152
|
if (deps2.expo || deps2["react-native"] || deps2["@capacitor/core"] || files.includes("pubspec.yaml"))
|
|
22074
22153
|
return done2("mobile-app", "high", fw(["Expo", "React Native", "Capacitor"]) ?? input2.label("pubspec.yaml"));
|
|
22075
22154
|
if (deps2.electron || deps2["@tauri-apps/api"]) return done2("desktop-app", "high", deps2.electron ? "Electron" : "Tauri");
|
|
@@ -22179,9 +22258,9 @@ var LANGUAGE_FILES = [
|
|
|
22179
22258
|
function detectLanguage(files) {
|
|
22180
22259
|
return LANGUAGE_FILES.find(([re]) => files.some((f) => re.test(f)))?.[1] ?? null;
|
|
22181
22260
|
}
|
|
22182
|
-
function scopeFor(root, dir, files,
|
|
22261
|
+
function scopeFor(root, dir, files, read3, rootFiles) {
|
|
22183
22262
|
const abs = packageFolder(root, dir);
|
|
22184
|
-
const readHere = (f) =>
|
|
22263
|
+
const readHere = (f) => read3(inDir(dir, f));
|
|
22185
22264
|
const pkg2 = files.includes("package.json") ? parseJson(readHere("package.json")) : null;
|
|
22186
22265
|
const stack = detectStack(abs, files, pkg2, readHere);
|
|
22187
22266
|
const seen = existsSync6(path8.join(abs, ".vercel", "project.json")) ? [...files, ".vercel/project.json"] : files;
|
|
@@ -22209,13 +22288,13 @@ function scopeFor(root, dir, files, read2, rootFiles) {
|
|
|
22209
22288
|
read: readHere
|
|
22210
22289
|
};
|
|
22211
22290
|
}
|
|
22212
|
-
function detectProjects(root, files,
|
|
22213
|
-
const dirs = findProjectDirs({ files, read:
|
|
22291
|
+
function detectProjects(root, files, read3, overrides = []) {
|
|
22292
|
+
const dirs = findProjectDirs({ files, read: read3 });
|
|
22214
22293
|
let projects;
|
|
22215
|
-
if (dirs.length === 1 && dirs[0] === ".") projects = [scopeFor(root, ".", files,
|
|
22294
|
+
if (dirs.length === 1 && dirs[0] === ".") projects = [scopeFor(root, ".", files, read3, files)];
|
|
22216
22295
|
else {
|
|
22217
|
-
projects = dirs.map((d) => scopeFor(root, d, filesIn(files, d),
|
|
22218
|
-
const rootScope = scopeFor(root, ".", filesIn(files, ".", dirs),
|
|
22296
|
+
projects = dirs.map((d) => scopeFor(root, d, filesIn(files, d), read3, files));
|
|
22297
|
+
const rootScope = scopeFor(root, ".", filesIn(files, ".", dirs), read3, files);
|
|
22219
22298
|
if (rootScope.profile.kind !== "unknown" || overrides.some((o) => o.path === ".")) projects.unshift(rootScope);
|
|
22220
22299
|
}
|
|
22221
22300
|
for (const p2 of projects)
|
|
@@ -22244,8 +22323,8 @@ function applyOverride(profile, o) {
|
|
|
22244
22323
|
profile.evidence = ["set in .aura/config.json", ...profile.evidence];
|
|
22245
22324
|
}
|
|
22246
22325
|
}
|
|
22247
|
-
function setKindOverride(overrides = [],
|
|
22248
|
-
const at = normPath(
|
|
22326
|
+
function setKindOverride(overrides = [], path55, kind) {
|
|
22327
|
+
const at = normPath(path55);
|
|
22249
22328
|
const rest = overrides.filter((o) => normPath(o.path) !== at);
|
|
22250
22329
|
const existing = overrides.find((o) => normPath(o.path) === at);
|
|
22251
22330
|
const next = { ...existing, path: at, kind };
|
|
@@ -22284,7 +22363,7 @@ function describeProject(p2) {
|
|
|
22284
22363
|
// package.json
|
|
22285
22364
|
var package_default = {
|
|
22286
22365
|
name: "getaura",
|
|
22287
|
-
version: "0.
|
|
22366
|
+
version: "0.5.0",
|
|
22288
22367
|
description: "Aura helps you and your coding agent follow best practices while you vibe code.",
|
|
22289
22368
|
type: "module",
|
|
22290
22369
|
bin: {
|
|
@@ -22404,6 +22483,7 @@ function templateVars(root, scan, pkg2, extra = {}) {
|
|
|
22404
22483
|
" ",
|
|
22405
22484
|
" "
|
|
22406
22485
|
),
|
|
22486
|
+
scan.stack.vite && `- Vite + React single-page app${scan.stack.vite.router ? " with React Router" : ""}${scan.stack.vite.lovable ? " (built with Lovable)" : ""}`,
|
|
22407
22487
|
scan.stack.typescript && "- TypeScript",
|
|
22408
22488
|
...scan.stack.services.filter((s) => s !== "GitHub").map((s) => `- ${s}`),
|
|
22409
22489
|
scan.stack.testRunner && `- Tests: ${scan.stack.testRunner}`,
|
|
@@ -22435,7 +22515,8 @@ function templateVars(root, scan, pkg2, extra = {}) {
|
|
|
22435
22515
|
aura_version: AURA_VERSION,
|
|
22436
22516
|
setup_node_cache: pm === "bun" ? "" : pm,
|
|
22437
22517
|
dependabot_ecosystem: pm === "bun" ? "bun" : "npm",
|
|
22438
|
-
|
|
22518
|
+
// Vite apps (Lovable, Bolt, v0) point "@/" at src/.
|
|
22519
|
+
src_alias_path: scan.stack.nextjs?.srcDir || scan.stack.vite ? "src" : ".",
|
|
22439
22520
|
details: "",
|
|
22440
22521
|
...extra
|
|
22441
22522
|
};
|
|
@@ -22532,6 +22613,22 @@ var SECURITY_ACTIONS = [
|
|
|
22532
22613
|
resolves: ["security.supabase-code"],
|
|
22533
22614
|
instructions: "Fix the Supabase problems listed in the findings, one concern per pull request (browser code, public variables, storage buckets, database functions). 1) Admin access in browser code: move every use of the service role or secret key, `createAdminClient()`, `supabaseAdmin`, `auth.admin` or `createClient(url, serviceKey)` out of `'use client'` files, `components/`, `hooks/` and Vite or Create React App `src/` code into server-only modules (a route handler, a server action or a `lib/` module that starts with `import 'server-only'`). Do the sign-in and permission check on the server first, then make the browser call that route or action. The browser keeps using the anon or publishable key, with row-level security protecting the data. 2) Secrets under public names: rename every `NEXT_PUBLIC_`, `VITE_` or `EXPO_PUBLIC_` variable whose name contains SERVICE_ROLE or SECRET to a server-only name (for example `SUPABASE_SERVICE_ROLE_KEY`), use it only in server code, and update `.env.example` (name and comment, no value). Say clearly in the pull request that the key must be replaced in the Supabase dashboard and in the hosting settings because it may already be public (`aura guide rotate-supabase-key`), since Aura can\u2019t do that for the user. 3) Public storage buckets: for buckets that hold private files, make them private in a NEW migration (`update storage.buckets set public = false where id = \u2026`), add storage policies on `storage.objects` scoped to the owner, and serve files through signed URLs (`createSignedUrl`) from server code. Leave buckets that are meant to be public (avatars, logos, marketing images) as they are and list them in the pull request. 4) Database functions: in a NEW migration (never edit an applied one), add `set search_path = ''` to each `security definer` function listed (`alter function public.name(args) set search_path = '';` or a `create or replace` with fully qualified names such as `public.profiles`), and revoke `anon` or `public` execute where signed-out visitors don't need it (`revoke execute on function public.name(args) from anon, public;`). Never touch `.env` files, never deploy, never change database data and never write a key value anywhere. Keep the app working the same way for signed-in users. Run the tests, lint, type check and build before finishing, and explain each change in plain language."
|
|
22534
22615
|
},
|
|
22616
|
+
{
|
|
22617
|
+
id: "protect-edge-functions",
|
|
22618
|
+
title: "Check who is calling each edge function",
|
|
22619
|
+
category: "security",
|
|
22620
|
+
delivery: "agent-fix",
|
|
22621
|
+
resolves: ["security.edge-functions"],
|
|
22622
|
+
instructions: "For each Supabase edge function in the findings (`supabase/functions/<name>/index.ts`, Deno), add a check on who is calling before it does anything else. If the app calls it for a signed-in user: read the `Authorization` header, create a client with the anon key and that header, call `supabase.auth.getUser()`, and answer 401 with a plain JSON error when there is no user; then check that user is allowed to touch the records involved. Keep the service role client only for the steps that truly need it, after the check. If it is a webhook: verify the sender\u2019s signature with the provider\u2019s official helper (for Stripe `stripe.webhooks.constructEventAsync` with the raw body). If it is a scheduled or internal job: compare the `Authorization` header with a secret read by `Deno.env.get(...)` and answer 401 otherwise. Put a check used by several functions in `supabase/functions/_shared/` once. Don\u2019t change `verify_jwt` in `supabase/config.toml` without saying so, never put a key value in code, and never deploy. Run the type check and any tests. In the pull request, explain in plain words who can call each function now, and tell the user to deploy the functions (`supabase functions deploy`) and to add any new secret with `supabase secrets set NAME=...`."
|
|
22623
|
+
},
|
|
22624
|
+
{
|
|
22625
|
+
id: "move-secrets-to-server",
|
|
22626
|
+
title: "Move server keys out of the browser",
|
|
22627
|
+
category: "security",
|
|
22628
|
+
delivery: "agent-fix",
|
|
22629
|
+
resolves: ["security.browser-secrets"],
|
|
22630
|
+
instructions: "This is a single-page app: everything in `src/` is downloaded by every visitor. For each file in the findings, move the code that needs a server key or a server-only package out of the browser. If the app uses Supabase, put it in a Supabase edge function (`supabase/functions/<name>/index.ts`, Deno) that reads the key with `Deno.env.get(...)`, checks the signed-in user from the `Authorization` header with `supabase.auth.getUser()` before doing anything, validates its input, and answers with plain JSON; call it from the app with `supabase.functions.invoke('<name>', { body })`. Without Supabase, use the hosting provider's server functions (for example a Vercel function in `api/`). Rename each variable without the `VITE_` prefix, remove it from browser code and from `.env.example` (list the new server-side name there with a comment, no value), and use the browser package instead where one exists (for example `@stripe/stripe-js` for Stripe Checkout). Never put a key value in code and never touch `.env` files. Run the type check, lint and build before finishing. In the pull request, tell the user in plain words that each key was public in any deployed version, so they must rotate it in that service's dashboard and add the new key as an edge function secret (`supabase secrets set NAME=...`) or in their hosting settings."
|
|
22631
|
+
},
|
|
22535
22632
|
{
|
|
22536
22633
|
id: "add-server-only-guards",
|
|
22537
22634
|
title: "Guard server code with server-only",
|
|
@@ -22698,11 +22795,21 @@ var ACTIONS = [
|
|
|
22698
22795
|
delivery: "guide",
|
|
22699
22796
|
resolves: ["foundations.migrations-in-sync"]
|
|
22700
22797
|
},
|
|
22798
|
+
{
|
|
22799
|
+
id: "add-error-boundary",
|
|
22800
|
+
title: "Add an error screen and a \u201Cpage not found\u201D page",
|
|
22801
|
+
category: "foundations",
|
|
22802
|
+
delivery: "pr",
|
|
22803
|
+
stacks: ["vite"],
|
|
22804
|
+
resolves: ["foundations.spa-errors"],
|
|
22805
|
+
instructions: 'This is a single-page app (Vite + React). Wrap the whole app in an error boundary (a class component with `getDerivedStateFromError`, or `react-error-boundary` if it is already installed) that shows a friendly message and a \u201CReload the page\u201D button instead of a blank screen, and logs the error to the console only. If the app uses React Router and has no catch-all route, add `<Route path="*" element={<NotFound />} />` as the last route, with a simple \u201Cpage not found\u201D page that links home. Match the app\u2019s existing styling (Tailwind classes and theme colours if it uses them). Run the type check, lint and build before finishing.'
|
|
22806
|
+
},
|
|
22701
22807
|
{
|
|
22702
22808
|
id: "add-error-pages",
|
|
22703
22809
|
title: "Add friendly error and \u201Cpage not found\u201D pages",
|
|
22704
22810
|
category: "foundations",
|
|
22705
22811
|
delivery: "pr",
|
|
22812
|
+
stacks: ["nextjs"],
|
|
22706
22813
|
resolves: ["foundations.error-pages"],
|
|
22707
22814
|
// Used when the PR falls back to an agent fix (no Next.js app at the repo root) or for an existing error page that isn't a client component.
|
|
22708
22815
|
instructions: 'Add the missing Next.js App Router error pages listed in the findings, in each app\u2019s `app` (or `src/app`) folder: `error.tsx` (a client component starting with `\'use client\'`, receiving `{ error, reset }`, showing a plain-language "Something went wrong" message and a "Try again" button that calls `reset()`), `global-error.tsx` (the same, but rendering its own `<html>` and `<body>` with inline styles, because the root layout and its stylesheet are not loaded there), and `not-found.tsx` (a "Page not found" message with a link to the home page). Use `.jsx` in apps written in JavaScript. Style them with the classes and theme colours the app already uses (Tailwind classes if the app uses Tailwind), and never hardcode new colours. Never show `error.message`, stack traces or other technical details to visitors; showing `error.digest` as a reference code is fine. If an existing `error` or `global-error` file doesn\u2019t start with `\'use client\'`, add that line at the top and change nothing else in it. Never replace or rewrite other existing files. Don\u2019t add loading states here (that is the `add-loading-states` fix). Run the type check, lint, tests and build, and explain in plain language what visitors will now see.'
|
|
@@ -22735,8 +22842,8 @@ var ACTIONS = [
|
|
|
22735
22842
|
resolves: ["structure.local-files"]
|
|
22736
22843
|
},
|
|
22737
22844
|
{ id: "save-aura-setup", buildSafe: true, title: "Save Aura\u2019s setup in git", category: "agent-setup", delivery: "pr", resolves: ["agent.setup-saved"] },
|
|
22738
|
-
{ id: "remove-dead-files", title: "Remove dead files", category: "structure", delivery: "pr", resolves: ["structure.dead-files"] },
|
|
22739
|
-
{ id: "move-misplaced-files", title: "Move misplaced files", category: "structure", delivery: "pr", resolves: ["structure.layout"] },
|
|
22845
|
+
{ id: "remove-dead-files", title: "Remove dead files", category: "structure", delivery: "pr", stacks: ["nextjs"], resolves: ["structure.dead-files"] },
|
|
22846
|
+
{ id: "move-misplaced-files", title: "Move misplaced files", category: "structure", delivery: "pr", stacks: ["nextjs"], resolves: ["structure.layout"] },
|
|
22740
22847
|
{
|
|
22741
22848
|
id: "split-oversized-files",
|
|
22742
22849
|
title: "Split oversized files",
|
|
@@ -22840,6 +22947,10 @@ var EFFORT = { pr: "one-click", "agent-fix": "review-agent-fix", guide: "follow-
|
|
|
22840
22947
|
function effortLabel(e) {
|
|
22841
22948
|
return { "one-click": "One click", "review-agent-fix": "Agent fix to review", "follow-guide": "Guide to follow", "answer-questions": "A few questions" }[e];
|
|
22842
22949
|
}
|
|
22950
|
+
function templateStack(stack) {
|
|
22951
|
+
if (!stack.recognized) return null;
|
|
22952
|
+
return stack.nextjs ? "nextjs" : stack.vite ? "vite" : null;
|
|
22953
|
+
}
|
|
22843
22954
|
function effectiveDelivery(action2, scan) {
|
|
22844
22955
|
if (action2.delivery !== "pr") return { delivery: action2.delivery, fellBack: false };
|
|
22845
22956
|
if (action2.buildSafe) return { delivery: "pr", fellBack: false };
|
|
@@ -22847,7 +22958,9 @@ function effectiveDelivery(action2, scan) {
|
|
|
22847
22958
|
return { delivery: "pr", fellBack: false };
|
|
22848
22959
|
}
|
|
22849
22960
|
const inProject = (scan.checks ?? []).some((c) => c.findings.some((f) => f.actionId === action2.id && f.meta?.project));
|
|
22850
|
-
const
|
|
22961
|
+
const stack = templateStack(scan.stack);
|
|
22962
|
+
const fits2 = stack !== null && (!action2.stacks || action2.stacks.includes(stack));
|
|
22963
|
+
const recognized = fits2 && !inProject && (!action2.needsSupabase || scan.stack.supabase !== null);
|
|
22851
22964
|
return recognized ? { delivery: "pr", fellBack: false } : { delivery: "agent-fix", fellBack: true };
|
|
22852
22965
|
}
|
|
22853
22966
|
function commandFor(actionId, delivery) {
|
|
@@ -22979,8 +23092,8 @@ function nextSteps(scan, opts = {}) {
|
|
|
22979
23092
|
}
|
|
22980
23093
|
|
|
22981
23094
|
// src/actions/generators.ts
|
|
22982
|
-
import { existsSync as
|
|
22983
|
-
import
|
|
23095
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
|
|
23096
|
+
import path24 from "path";
|
|
22984
23097
|
|
|
22985
23098
|
// src/checks/agent-setup.ts
|
|
22986
23099
|
import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
|
|
@@ -23011,10 +23124,10 @@ function parseJson2(text) {
|
|
|
23011
23124
|
return null;
|
|
23012
23125
|
}
|
|
23013
23126
|
}
|
|
23014
|
-
function permissionList(settings,
|
|
23127
|
+
function permissionList(settings, list11) {
|
|
23015
23128
|
const perms = settings?.permissions;
|
|
23016
23129
|
if (!perms || typeof perms !== "object") return [];
|
|
23017
|
-
const value = perms[
|
|
23130
|
+
const value = perms[list11];
|
|
23018
23131
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string").map((v) => v.trim()) : [];
|
|
23019
23132
|
}
|
|
23020
23133
|
function globRegex(glob) {
|
|
@@ -23112,25 +23225,25 @@ function dangerousSettings(root, tracked) {
|
|
|
23112
23225
|
}
|
|
23113
23226
|
function describeDanger(d) {
|
|
23114
23227
|
const agent = AGENT_LABELS[d.agent];
|
|
23115
|
-
const
|
|
23228
|
+
const list11 = d.settings.map((s) => `\`${s}\``).join(", ");
|
|
23116
23229
|
if (d.kind === "bypass" && d.agent === "codex")
|
|
23117
23230
|
return {
|
|
23118
23231
|
title: "Codex runs every command with full access and never asks",
|
|
23119
|
-
detail: `${d.file} sets ${
|
|
23232
|
+
detail: `${d.file} sets ${list11}, so Codex can change any file on the computer and use the internet without asking first.`
|
|
23120
23233
|
};
|
|
23121
23234
|
if (d.kind === "bypass")
|
|
23122
23235
|
return {
|
|
23123
23236
|
title: `${agent} is set to skip every permission prompt`,
|
|
23124
|
-
detail: `${d.file} sets ${
|
|
23237
|
+
detail: `${d.file} sets ${list11}. Recent versions of ${agent} ignore this in shared project settings, but older ones run every command and edit without asking.`
|
|
23125
23238
|
};
|
|
23126
23239
|
if (d.kind === "all-shell")
|
|
23127
23240
|
return {
|
|
23128
23241
|
title: `${agent} can run any terminal command without asking`,
|
|
23129
|
-
detail: `${d.file} allows ${
|
|
23242
|
+
detail: `${d.file} allows ${list11}, which approves every terminal command in advance.`
|
|
23130
23243
|
};
|
|
23131
23244
|
return {
|
|
23132
23245
|
title: `${agent} can run risky commands without asking`,
|
|
23133
|
-
detail: `${d.file} approves ${
|
|
23246
|
+
detail: `${d.file} approves ${list11} in advance: deleting files, pushing code, downloading from the internet or running as administrator, with no prompt.`
|
|
23134
23247
|
};
|
|
23135
23248
|
}
|
|
23136
23249
|
var KNOWN_AGENTS = new Set(AGENT_IDS);
|
|
@@ -23294,7 +23407,7 @@ var localFiles = {
|
|
|
23294
23407
|
if (!problems) return { facts: { gitAnswered: false }, items: [] };
|
|
23295
23408
|
if (problems.length === 0) return { facts: { gitAnswered: true }, items: [] };
|
|
23296
23409
|
const committed = problems.flatMap((p2) => p2.committed);
|
|
23297
|
-
const
|
|
23410
|
+
const list11 = problems.map(
|
|
23298
23411
|
(p2) => `${p2.what} (${[...p2.committed, ...p2.notIgnored].slice(0, 3).join(", ")}): add \`${p2.rule}\` to .gitignore${p2.committed.length ? " and stop tracking them" : ""}`
|
|
23299
23412
|
).join("; ");
|
|
23300
23413
|
const item = {
|
|
@@ -23302,7 +23415,7 @@ var localFiles = {
|
|
|
23302
23415
|
facts: { committed: committed.length, kinds: problems.length },
|
|
23303
23416
|
key: "local-files",
|
|
23304
23417
|
locations: [{ file: ".gitignore" }],
|
|
23305
|
-
vars: { list:
|
|
23418
|
+
vars: { list: list11 }
|
|
23306
23419
|
};
|
|
23307
23420
|
return { facts: { gitAnswered: true }, items: [item] };
|
|
23308
23421
|
}
|
|
@@ -23495,10 +23608,10 @@ function parseImports(text) {
|
|
|
23495
23608
|
function stripJsonc(text) {
|
|
23496
23609
|
return text.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_m, str) => str ?? "").replace(/,(\s*[}\]])/g, "$1");
|
|
23497
23610
|
}
|
|
23498
|
-
function readAliases(
|
|
23611
|
+
function readAliases(read3) {
|
|
23499
23612
|
const entries = [];
|
|
23500
23613
|
for (const file2 of ["tsconfig.json", "jsconfig.json"]) {
|
|
23501
|
-
const text =
|
|
23614
|
+
const text = read3(file2);
|
|
23502
23615
|
if (!text) continue;
|
|
23503
23616
|
try {
|
|
23504
23617
|
const json2 = JSON.parse(stripJsonc(text));
|
|
@@ -23537,13 +23650,13 @@ function entryPoints(files, pkgScripts = {}) {
|
|
|
23537
23650
|
(f) => isSourceFile(f) && (isNextRouteFile(f) || /^(?:src\/)?pages\//.test(f) || /^(?:src\/)?(?:middleware|proxy|instrumentation(?:-client)?)\.[cm]?[jt]s$/.test(f) || /^[^/]+\.config\.[cm]?[jt]s$/.test(f) || /^(?:scripts|supabase\/functions|e2e|\.github)\//.test(f) || isTestFile(f) || /(?:^|\/)(?:setup|vitest\.setup|jest\.setup)\.[cm]?[jt]sx?$/.test(f) || scripts.includes(f))
|
|
23538
23651
|
);
|
|
23539
23652
|
}
|
|
23540
|
-
function buildGraph(files,
|
|
23653
|
+
function buildGraph(files, read3, aliases) {
|
|
23541
23654
|
const source = files.filter(isSourceFile);
|
|
23542
23655
|
const fileSet = new Set(files);
|
|
23543
23656
|
const edges = /* @__PURE__ */ new Map();
|
|
23544
23657
|
for (const f of source) {
|
|
23545
23658
|
const deps2 = /* @__PURE__ */ new Set();
|
|
23546
|
-
for (const ref of parseImports(
|
|
23659
|
+
for (const ref of parseImports(read3(f) ?? "")) {
|
|
23547
23660
|
const target = resolveImport(f, ref.specifier, fileSet, aliases);
|
|
23548
23661
|
if (target) deps2.add(target);
|
|
23549
23662
|
}
|
|
@@ -23551,9 +23664,9 @@ function buildGraph(files, read2, aliases) {
|
|
|
23551
23664
|
}
|
|
23552
23665
|
return { source, edges };
|
|
23553
23666
|
}
|
|
23554
|
-
function unreachableFiles(files,
|
|
23555
|
-
const aliases = readAliases(
|
|
23556
|
-
const { source, edges } = buildGraph(files,
|
|
23667
|
+
function unreachableFiles(files, read3, pkgScripts) {
|
|
23668
|
+
const aliases = readAliases(read3);
|
|
23669
|
+
const { source, edges } = buildGraph(files, read3, aliases);
|
|
23557
23670
|
const seen = /* @__PURE__ */ new Set();
|
|
23558
23671
|
const stack = entryPoints(files, pkgScripts);
|
|
23559
23672
|
while (stack.length) {
|
|
@@ -24109,15 +24222,526 @@ var addErrorPages = ({ root, ctx }) => {
|
|
|
24109
24222
|
return { changes, details: [...details, ...notes.length ? ["", "**Not in this PR**", ...notes] : []] };
|
|
24110
24223
|
};
|
|
24111
24224
|
|
|
24112
|
-
// src/actions/
|
|
24113
|
-
import {
|
|
24225
|
+
// src/actions/spa-fixes.ts
|
|
24226
|
+
import { existsSync as existsSync14 } from "fs";
|
|
24114
24227
|
import path20 from "path";
|
|
24228
|
+
|
|
24229
|
+
// src/core/code-text.ts
|
|
24230
|
+
var REGEX_BEFORE = /[(,=:[!&|?{};+\-*%~^]/;
|
|
24231
|
+
var REGEX_KEYWORD = /\b(?:return|typeof|case|in|of|void|yield|await|delete|throw|new)$/;
|
|
24232
|
+
function maskCode(src) {
|
|
24233
|
+
const out = src.split("");
|
|
24234
|
+
const blank = (from, to) => {
|
|
24235
|
+
for (let k = from; k < to && k < out.length; k++) if (out[k] !== "\n") out[k] = " ";
|
|
24236
|
+
};
|
|
24237
|
+
const n = src.length;
|
|
24238
|
+
const stack = [];
|
|
24239
|
+
let depth = 0;
|
|
24240
|
+
let inTemplate = false;
|
|
24241
|
+
let i = 0;
|
|
24242
|
+
while (i < n) {
|
|
24243
|
+
const c = src[i];
|
|
24244
|
+
if (inTemplate) {
|
|
24245
|
+
if (c === "\\") {
|
|
24246
|
+
blank(i, i + 2);
|
|
24247
|
+
i += 2;
|
|
24248
|
+
} else if (c === "`") {
|
|
24249
|
+
inTemplate = false;
|
|
24250
|
+
i++;
|
|
24251
|
+
} else if (c === "$" && src[i + 1] === "{") {
|
|
24252
|
+
stack.push(depth++);
|
|
24253
|
+
inTemplate = false;
|
|
24254
|
+
i += 2;
|
|
24255
|
+
} else {
|
|
24256
|
+
blank(i, i + 1);
|
|
24257
|
+
i++;
|
|
24258
|
+
}
|
|
24259
|
+
continue;
|
|
24260
|
+
}
|
|
24261
|
+
const next = src[i + 1];
|
|
24262
|
+
if (c === "/" && next === "/") {
|
|
24263
|
+
const end = src.indexOf("\n", i);
|
|
24264
|
+
const e = end < 0 ? n : end;
|
|
24265
|
+
blank(i, e);
|
|
24266
|
+
i = e;
|
|
24267
|
+
} else if (c === "/" && next === "*") {
|
|
24268
|
+
const end = src.indexOf("*/", i + 2);
|
|
24269
|
+
const e = end < 0 ? n : end + 2;
|
|
24270
|
+
blank(i, e);
|
|
24271
|
+
i = e;
|
|
24272
|
+
} else if (c === '"' || c === "'") {
|
|
24273
|
+
let j = i + 1;
|
|
24274
|
+
while (j < n && src[j] !== c && src[j] !== "\n") j += src[j] === "\\" ? 2 : 1;
|
|
24275
|
+
blank(i + 1, j);
|
|
24276
|
+
i = j + 1;
|
|
24277
|
+
} else if (c === "`") {
|
|
24278
|
+
inTemplate = true;
|
|
24279
|
+
i++;
|
|
24280
|
+
} else if (c === "/" && regexCanStart(out, i)) {
|
|
24281
|
+
let j = i + 1;
|
|
24282
|
+
let inClass = false;
|
|
24283
|
+
while (j < n && src[j] !== "\n" && (src[j] !== "/" || inClass)) {
|
|
24284
|
+
if (src[j] === "\\") j++;
|
|
24285
|
+
else if (src[j] === "[") inClass = true;
|
|
24286
|
+
else if (src[j] === "]") inClass = false;
|
|
24287
|
+
j++;
|
|
24288
|
+
}
|
|
24289
|
+
if (src[j] === "/") {
|
|
24290
|
+
blank(i + 1, j);
|
|
24291
|
+
i = j + 1;
|
|
24292
|
+
} else i++;
|
|
24293
|
+
} else {
|
|
24294
|
+
if (c === "{") depth++;
|
|
24295
|
+
else if (c === "}") {
|
|
24296
|
+
depth--;
|
|
24297
|
+
if (stack.length && stack[stack.length - 1] === depth) {
|
|
24298
|
+
stack.pop();
|
|
24299
|
+
inTemplate = true;
|
|
24300
|
+
}
|
|
24301
|
+
}
|
|
24302
|
+
i++;
|
|
24303
|
+
}
|
|
24304
|
+
}
|
|
24305
|
+
return out.join("");
|
|
24306
|
+
}
|
|
24307
|
+
function regexCanStart(out, i) {
|
|
24308
|
+
let k = i - 1;
|
|
24309
|
+
while (k >= 0 && /\s/.test(out[k] ?? "")) k--;
|
|
24310
|
+
if (k < 0) return true;
|
|
24311
|
+
const prev = out[k] ?? "";
|
|
24312
|
+
if (REGEX_BEFORE.test(prev)) return true;
|
|
24313
|
+
return REGEX_KEYWORD.test(out.slice(Math.max(0, k - 7), k + 1).join(""));
|
|
24314
|
+
}
|
|
24315
|
+
function closingIndex(masked, open2, limit = 4e3) {
|
|
24316
|
+
let depth = 0;
|
|
24317
|
+
const end = Math.min(masked.length, open2 + limit);
|
|
24318
|
+
for (let i = open2; i < end; i++) {
|
|
24319
|
+
const c = masked[i];
|
|
24320
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
24321
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
24322
|
+
depth--;
|
|
24323
|
+
if (depth === 0) return i + 1;
|
|
24324
|
+
}
|
|
24325
|
+
}
|
|
24326
|
+
return -1;
|
|
24327
|
+
}
|
|
24328
|
+
function lineFinder(text) {
|
|
24329
|
+
const starts = [0];
|
|
24330
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
24331
|
+
return (index) => {
|
|
24332
|
+
let lo = 0;
|
|
24333
|
+
let hi = starts.length - 1;
|
|
24334
|
+
while (lo < hi) {
|
|
24335
|
+
const mid = lo + hi + 1 >> 1;
|
|
24336
|
+
if ((starts[mid] ?? 0) <= index) lo = mid;
|
|
24337
|
+
else hi = mid - 1;
|
|
24338
|
+
}
|
|
24339
|
+
return lo + 1;
|
|
24340
|
+
};
|
|
24341
|
+
}
|
|
24342
|
+
|
|
24343
|
+
// src/checks/security/logs.ts
|
|
24344
|
+
var NOT_APP_CODE = /(?:^|\/)(?:node_modules|\.next|\.nuxt|\.svelte-kit|\.vercel|\.turbo|\.output|dist|build|out|coverage|vendor|public|generated|__generated__|__mocks__|\.aura|\.claude|\.agents|\.cursor|\.codex|\.pi)\/|\.min\.[cm]?js$|\.stories\.[cm]?[jt]sx?$|(?:^|\/)[^/]+\.config\.[cm]?[jt]s$|(?:^|\/)\.[\w-]+rc\.[cm]?js$|(?:^|\/)(?:database|supabase)\.types\.ts$/;
|
|
24345
|
+
function appCodeFiles(ctx) {
|
|
24346
|
+
return ctx.files.filter((f) => isSourceFile(f) && !isTestFile(f) && !NOT_APP_CODE.test(f));
|
|
24347
|
+
}
|
|
24348
|
+
var LOG_CALL = /(?<![\w$])(?:console|logger|log)\s*\??\.\s*(?:log|info|debug|warn|error|trace|fatal)\s*\(/g;
|
|
24349
|
+
var REDACTED_CALL = /(?<![\w$.])[\w$]*(?:redact|mask|sanitiz|scrub|obfuscat)[\w$]*\s*\(/gi;
|
|
24350
|
+
var CHAIN = /(?<![\w$.])[A-Za-z_$][\w$]*(?:\s*\??\.\s*[A-Za-z_$][\w$]*)*/g;
|
|
24351
|
+
var SECRET_ENV = /KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|DATABASE_URL|POSTGRES(?:QL)?_URL|REDIS_URL|MONGO(?:DB)?_URI|CONNECTION_STRING|SALT/;
|
|
24352
|
+
var PUBLIC_ENV = /^(?:NEXT_PUBLIC_|VITE_|PUBLIC_|EXPO_PUBLIC_|REACT_APP_|GATSBY_)/;
|
|
24353
|
+
var REQUEST = /^(?:req|request|ctx|context|event)$/;
|
|
24354
|
+
var PRESENCE_BEFORE = /(?:typeof\s+|!\s*|Boolean\(\s*|\bas\s+|\bkeyof\s+|Object\.keys\(\s*)$/;
|
|
24355
|
+
var CONTENT_METHODS = /* @__PURE__ */ new Set([
|
|
24356
|
+
"slice",
|
|
24357
|
+
"substring",
|
|
24358
|
+
"substr",
|
|
24359
|
+
"toString",
|
|
24360
|
+
"trim",
|
|
24361
|
+
"split",
|
|
24362
|
+
"padStart",
|
|
24363
|
+
"padEnd",
|
|
24364
|
+
"toUpperCase",
|
|
24365
|
+
"toLowerCase",
|
|
24366
|
+
"concat",
|
|
24367
|
+
"entries",
|
|
24368
|
+
"values",
|
|
24369
|
+
"toJSON"
|
|
24370
|
+
]);
|
|
24371
|
+
var SECRET_NAME2 = /(token|secret|password|passwd|passphrase|(?:api|private|secret|access|service|servicerole|signing|encryption|master|client)key|authorization|authheader|cookies?|jwt|credentials?|bearer)$/;
|
|
24372
|
+
var NOT_A_SECRET = /hash|masked|redacted|sanitiz|prefix|hint|last4|preview/i;
|
|
24373
|
+
var YES_NO_OR_VERB = /^(?:is|has|show|hide|should|needs?|can|toggle|validate|verify|check|create|generate|get|set|use|missing|invalid|no|with|requires?)[A-Z_]/;
|
|
24374
|
+
var SECRET_STRING_KEY = /^(?:authorization|cookie|setcookie|session|sessionid|jwt|credentials?)$|(?:token|secret|password|passwd|apikey)$/;
|
|
24375
|
+
var PERSON = /^(?:(?:[a-z][a-zA-Z0-9]*)?(User|Session|Profile|Customer)|(user|session|profile|customer))s?$/;
|
|
24376
|
+
function secretKind(word) {
|
|
24377
|
+
const w = word.toLowerCase();
|
|
24378
|
+
if (/pass/.test(w)) return "a password";
|
|
24379
|
+
if (/cookie/.test(w)) return "a cookie";
|
|
24380
|
+
if (/token$/.test(w)) return "a token";
|
|
24381
|
+
if (/key$/.test(w)) return "an API key";
|
|
24382
|
+
if (/credential/.test(w)) return "login credentials";
|
|
24383
|
+
if (/secret/.test(w)) return "a secret";
|
|
24384
|
+
if (/auth/.test(w)) return "an authorization header";
|
|
24385
|
+
if (/session/.test(w)) return "a session id";
|
|
24386
|
+
return "a token";
|
|
24387
|
+
}
|
|
24388
|
+
function isSecretName(name) {
|
|
24389
|
+
return !NOT_A_SECRET.test(name) && !YES_NO_OR_VERB.test(name) && SECRET_NAME2.test(name.toLowerCase().replace(/[_$]/g, ""));
|
|
24390
|
+
}
|
|
24391
|
+
function splitTopLevel(s) {
|
|
24392
|
+
const parts = [];
|
|
24393
|
+
let depth = 0;
|
|
24394
|
+
let start = 0;
|
|
24395
|
+
for (let i = 0; i < s.length; i++) {
|
|
24396
|
+
const c = s[i];
|
|
24397
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
24398
|
+
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
24399
|
+
else if (c === "," && depth === 0) {
|
|
24400
|
+
parts.push(s.slice(start, i));
|
|
24401
|
+
start = i + 1;
|
|
24402
|
+
}
|
|
24403
|
+
}
|
|
24404
|
+
parts.push(s.slice(start));
|
|
24405
|
+
return parts.map((p2) => p2.trim()).filter(Boolean);
|
|
24406
|
+
}
|
|
24407
|
+
function wholeValues(expr, depth = 0) {
|
|
24408
|
+
const e = expr.trim().replace(/^(?:await\s+|\.\.\.)/, "").trim();
|
|
24409
|
+
if (depth > 2) return [e];
|
|
24410
|
+
const json2 = e.match(/^JSON\s*\.\s*stringify\s*\(/);
|
|
24411
|
+
if (json2 && closingIndex(e, json2[0].length - 1) === e.length) return wholeValues(splitTopLevel(e.slice(json2[0].length, -1))[0] ?? "", depth + 1);
|
|
24412
|
+
if (e.startsWith("{") && closingIndex(e, 0) === e.length) {
|
|
24413
|
+
return splitTopLevel(e.slice(1, -1)).flatMap((entry) => {
|
|
24414
|
+
const colon = entry.match(/^[\w$\s]+:(?!:)/);
|
|
24415
|
+
return wholeValues(colon ? entry.slice(colon[0].length) : entry, depth + 1);
|
|
24416
|
+
});
|
|
24417
|
+
}
|
|
24418
|
+
return [e];
|
|
24419
|
+
}
|
|
24420
|
+
function analyzeLogArgs(masked, raw) {
|
|
24421
|
+
const hits = [];
|
|
24422
|
+
let m = masked;
|
|
24423
|
+
let r = raw;
|
|
24424
|
+
for (const call of masked.matchAll(REDACTED_CALL)) {
|
|
24425
|
+
const end = closingIndex(masked, call.index + call[0].length - 1);
|
|
24426
|
+
const to = end < 0 ? masked.length : end;
|
|
24427
|
+
const pad = " ".repeat(to - call.index);
|
|
24428
|
+
m = m.slice(0, call.index) + pad + m.slice(to);
|
|
24429
|
+
r = r.slice(0, call.index) + pad + r.slice(to);
|
|
24430
|
+
}
|
|
24431
|
+
for (const chain of m.matchAll(CHAIN)) {
|
|
24432
|
+
const start = chain.index;
|
|
24433
|
+
const end = start + chain[0].length;
|
|
24434
|
+
const before = m.slice(Math.max(0, start - 16), start);
|
|
24435
|
+
const after = m.slice(end).trimStart();
|
|
24436
|
+
const segments2 = chain[0].split(/\s*\??\.\s*/);
|
|
24437
|
+
const last = segments2[segments2.length - 1] ?? "";
|
|
24438
|
+
if (segments2.length === 1 && /[{,]\s*$/.test(before) && /^:(?!:)/.test(after)) continue;
|
|
24439
|
+
if (PRESENCE_BEFORE.test(before) || last === "length" || last === "size") continue;
|
|
24440
|
+
let value = segments2;
|
|
24441
|
+
if (after.startsWith("(")) {
|
|
24442
|
+
if (segments2.length === 1) {
|
|
24443
|
+
if ((last === "headers" || last === "cookies") && !/^\(\s*\)\s*\??\.\s*(?:get|has)\s*\(/.test(after))
|
|
24444
|
+
hits.push({ severity: "high", kind: last === "headers" ? "the request headers" : "the cookies" });
|
|
24445
|
+
continue;
|
|
24446
|
+
}
|
|
24447
|
+
const receiver = segments2[segments2.length - 2] ?? "";
|
|
24448
|
+
if (REQUEST.test(receiver) && /^(?:json|formData|text)$/.test(last)) {
|
|
24449
|
+
hits.push({ severity: "medium", kind: last === "formData" ? "the whole form submission" : "the whole request body" });
|
|
24450
|
+
continue;
|
|
24451
|
+
}
|
|
24452
|
+
if (!CONTENT_METHODS.has(last)) continue;
|
|
24453
|
+
value = segments2.slice(0, -1);
|
|
24454
|
+
}
|
|
24455
|
+
const name = value[value.length - 1] ?? "";
|
|
24456
|
+
const owner = value[value.length - 2] ?? "";
|
|
24457
|
+
if (value[0] === "process" && value[1] === "env") {
|
|
24458
|
+
if (value.length === 2 && !after.startsWith("[")) hits.push({ severity: "high", kind: "every environment variable (process.env)" });
|
|
24459
|
+
else if (value.length === 3 && SECRET_ENV.test(name) && !PUBLIC_ENV.test(name) && !/^\?(?![.?])/.test(after))
|
|
24460
|
+
hits.push({ severity: "high", kind: `the ${name} environment variable` });
|
|
24461
|
+
continue;
|
|
24462
|
+
}
|
|
24463
|
+
if (after.startsWith("[")) continue;
|
|
24464
|
+
if (name === "headers" && REQUEST.test(owner)) hits.push({ severity: "high", kind: "the request headers" });
|
|
24465
|
+
else if (name === "body" && REQUEST.test(owner)) hits.push({ severity: "medium", kind: "the whole request body" });
|
|
24466
|
+
else if (name === "formData" && value.length === 1) hits.push({ severity: "medium", kind: "the whole form submission" });
|
|
24467
|
+
else if (isSecretName(name)) hits.push({ severity: "high", kind: secretKind(name) });
|
|
24468
|
+
}
|
|
24469
|
+
for (const key of r.matchAll(/(?:\.\s*get\s*\(|\[)\s*(['"`])([\w-]{2,40})\1\s*[)\]]/g)) {
|
|
24470
|
+
const word = (key[2] ?? "").toLowerCase().replace(/[-_]/g, "");
|
|
24471
|
+
if (m[key.index] !== r[key.index] || !SECRET_STRING_KEY.test(word)) continue;
|
|
24472
|
+
hits.push({ severity: "high", kind: word === "authorization" ? "the authorization header" : secretKind(word) });
|
|
24473
|
+
}
|
|
24474
|
+
for (const arg of splitTopLevel(m)) {
|
|
24475
|
+
for (const v of wholeValues(arg)) {
|
|
24476
|
+
const person = v.match(/^(?:[\w$]+\s*\??\.\s*)*([\w$]+)$/)?.[1]?.match(PERSON);
|
|
24477
|
+
if (person) hits.push({ severity: "medium", kind: `a whole ${(person[1] ?? person[2] ?? "user").toLowerCase()} object` });
|
|
24478
|
+
}
|
|
24479
|
+
}
|
|
24480
|
+
return hits;
|
|
24481
|
+
}
|
|
24482
|
+
function findLogCalls(text, pattern = LOG_CALL) {
|
|
24483
|
+
const src = text.replace(/\r\n/g, "\n");
|
|
24484
|
+
const masked = maskCode(src);
|
|
24485
|
+
const line = lineFinder(src);
|
|
24486
|
+
const calls = [];
|
|
24487
|
+
for (const m of masked.matchAll(new RegExp(pattern.source, "g"))) {
|
|
24488
|
+
const open2 = m.index + m[0].length - 1;
|
|
24489
|
+
const close = closingIndex(masked, open2);
|
|
24490
|
+
calls.push({
|
|
24491
|
+
index: m.index,
|
|
24492
|
+
line: line(m.index),
|
|
24493
|
+
masked: close < 0 ? null : masked.slice(open2 + 1, close - 1),
|
|
24494
|
+
raw: close < 0 ? null : src.slice(open2 + 1, close - 1)
|
|
24495
|
+
});
|
|
24496
|
+
}
|
|
24497
|
+
return calls;
|
|
24498
|
+
}
|
|
24499
|
+
var KINDS = { high: "secret-logs", medium: "personal-logs" };
|
|
24500
|
+
var logLeaks = {
|
|
24501
|
+
id: "security.log-leaks",
|
|
24502
|
+
category: "security",
|
|
24503
|
+
title: "Logs don\u2019t leak secrets or personal data",
|
|
24504
|
+
obsV: 1,
|
|
24505
|
+
applies: (ctx) => appCodeFiles(ctx).length > 0,
|
|
24506
|
+
observe(ctx) {
|
|
24507
|
+
let logging = 0;
|
|
24508
|
+
const flagged = /* @__PURE__ */ new Set();
|
|
24509
|
+
const items = [];
|
|
24510
|
+
for (const file2 of appCodeFiles(ctx)) {
|
|
24511
|
+
const text = ctx.read(file2);
|
|
24512
|
+
if (!text) continue;
|
|
24513
|
+
const calls = findLogCalls(text);
|
|
24514
|
+
if (calls.length === 0) continue;
|
|
24515
|
+
logging++;
|
|
24516
|
+
const bySeverity = /* @__PURE__ */ new Map();
|
|
24517
|
+
for (const call of calls) {
|
|
24518
|
+
if (call.masked === null || call.raw === null) continue;
|
|
24519
|
+
for (const hit of analyzeLogArgs(call.masked, call.raw)) {
|
|
24520
|
+
const lines = bySeverity.get(hit.severity) ?? /* @__PURE__ */ new Map();
|
|
24521
|
+
bySeverity.set(hit.severity, lines);
|
|
24522
|
+
lines.set(call.line, (lines.get(call.line) ?? /* @__PURE__ */ new Set()).add(hit.kind));
|
|
24523
|
+
}
|
|
24524
|
+
}
|
|
24525
|
+
for (const severity2 of ["high", "medium"]) {
|
|
24526
|
+
const lines = bySeverity.get(severity2);
|
|
24527
|
+
if (!lines) continue;
|
|
24528
|
+
flagged.add(file2);
|
|
24529
|
+
const entries = [...lines.entries()].sort((a, b) => a[0] - b[0]);
|
|
24530
|
+
const shown = entries.slice(0, 20);
|
|
24531
|
+
const kinds = [...new Set(entries.flatMap(([, k]) => [...k]))];
|
|
24532
|
+
items.push({
|
|
24533
|
+
kind: KINDS[severity2],
|
|
24534
|
+
facts: { more: entries.length - shown.length },
|
|
24535
|
+
key: `${file2}|${severity2}`,
|
|
24536
|
+
locations: shown.map(([line]) => ({ file: file2, line })),
|
|
24537
|
+
// What each line logs can name the app's own variables, so it stays on this computer.
|
|
24538
|
+
vars: { file: file2, lines: shown.map(([line, k]) => `Line ${line} logs ${[...k].join(" and ")}`).join("; ") },
|
|
24539
|
+
meta: { kinds, lines: entries.length }
|
|
24540
|
+
});
|
|
24541
|
+
}
|
|
24542
|
+
}
|
|
24543
|
+
return { facts: { logging, flagged: flagged.size }, items };
|
|
24544
|
+
}
|
|
24545
|
+
};
|
|
24546
|
+
|
|
24547
|
+
// src/checks/spa-errors.ts
|
|
24548
|
+
var ERROR_BOUNDARY = /\bcomponentDidCatch\s*\(|\bgetDerivedStateFromError\s*\(|from\s+['"]react-error-boundary['"]|<(?:Sentry\.)?ErrorBoundary\b|\berrorElement\s*[:=]|\bErrorBoundary\s*:|export\s+(?:async\s+)?(?:function|const)\s+ErrorBoundary\b/;
|
|
24549
|
+
var CATCH_ALL = /\bpath\s*[:=]\s*\{?\s*['"`]\*['"`]|\bnotFoundComponent\b|\bdefaultNotFoundComponent\b/;
|
|
24550
|
+
function hasErrorBoundary(ctx) {
|
|
24551
|
+
return appCodeFiles(ctx).some((f) => ERROR_BOUNDARY.test(ctx.read(f) ?? ""));
|
|
24552
|
+
}
|
|
24553
|
+
function hasCatchAll(ctx) {
|
|
24554
|
+
return appCodeFiles(ctx).some((f) => CATCH_ALL.test(ctx.read(f) ?? ""));
|
|
24555
|
+
}
|
|
24556
|
+
function usesRouter(pkg2) {
|
|
24557
|
+
const deps2 = { ...pkg2?.dependencies, ...pkg2?.devDependencies };
|
|
24558
|
+
return Boolean(deps2["react-router-dom"] || deps2["react-router"] || deps2["@tanstack/react-router"]);
|
|
24559
|
+
}
|
|
24560
|
+
var spaErrors = {
|
|
24561
|
+
id: "foundations.spa-errors",
|
|
24562
|
+
scope: "project",
|
|
24563
|
+
category: "foundations",
|
|
24564
|
+
title: "Users see a friendly page when something breaks",
|
|
24565
|
+
obsV: 1,
|
|
24566
|
+
applies: (ctx) => Boolean(ctx.stack.vite) && ctx.project.traits.servesPages,
|
|
24567
|
+
observe(ctx) {
|
|
24568
|
+
const router = usesRouter(ctx.pkg);
|
|
24569
|
+
const items = [];
|
|
24570
|
+
const main = ctx.files.find((f) => /^src\/main\.[jt]sx?$/.test(f));
|
|
24571
|
+
const app2 = ctx.files.find((f) => /^src\/App\.[jt]sx?$/.test(f));
|
|
24572
|
+
if (!hasErrorBoundary(ctx)) items.push({ kind: "no-error-boundary", facts: {}, key: "error-boundary", locations: main ? [{ file: main }] : [] });
|
|
24573
|
+
if (router && !hasCatchAll(ctx)) items.push({ kind: "no-catch-all", facts: {}, key: "catch-all", locations: app2 ? [{ file: app2 }] : [] });
|
|
24574
|
+
return { facts: { router }, items };
|
|
24575
|
+
}
|
|
24576
|
+
};
|
|
24577
|
+
|
|
24578
|
+
// src/actions/spa-fixes.ts
|
|
24579
|
+
function addImport(text, line) {
|
|
24580
|
+
const imports3 = [...text.matchAll(/^import[^\n]*\n/gm)];
|
|
24581
|
+
const last = imports3.at(-1);
|
|
24582
|
+
if (!last || last.index === void 0) return `${line}
|
|
24583
|
+
${text}`;
|
|
24584
|
+
const at = last.index + last[0].length;
|
|
24585
|
+
return `${text.slice(0, at)}${line}
|
|
24586
|
+
${text.slice(at)}`;
|
|
24587
|
+
}
|
|
24588
|
+
function addErrorBoundary({ root, ctx }) {
|
|
24589
|
+
if (!ctx.stack.vite || !ctx.stack.typescript)
|
|
24590
|
+
return { skip: "This template is for Vite apps written in TypeScript. Run `aura fix add-error-boundary` instead." };
|
|
24591
|
+
const read3 = (f) => ctx.read(f);
|
|
24592
|
+
const style = pageStyle(ctx, { dir: ".", tailwind: Boolean(allDeps(ctx.pkg).tailwindcss) });
|
|
24593
|
+
const changes = [];
|
|
24594
|
+
const details = [];
|
|
24595
|
+
const notes = [];
|
|
24596
|
+
if (!hasErrorBoundary(ctx)) {
|
|
24597
|
+
const main = ctx.files.find((f) => /^src\/main\.tsx$/.test(f));
|
|
24598
|
+
const text = main ? read3(main) : null;
|
|
24599
|
+
const file2 = "src/components/ErrorBoundary.tsx";
|
|
24600
|
+
if (!main || !text || (text.match(/<App\s*\/>/g) ?? []).length !== 1 || existsSync14(path20.join(root, file2))) {
|
|
24601
|
+
notes.push("- An error boundary: `src/main.tsx` isn\u2019t in the shape Aura edits safely. Run `aura fix add-error-boundary`.");
|
|
24602
|
+
} else {
|
|
24603
|
+
changes.push({ path: file2, content: styled(readTemplate("vite/ErrorBoundary.tsx"), style) });
|
|
24604
|
+
const wrapped = addImport(text, 'import ErrorBoundary from "./components/ErrorBoundary";').replace(
|
|
24605
|
+
/<App\s*\/>/,
|
|
24606
|
+
"<ErrorBoundary>\n <App />\n </ErrorBoundary>"
|
|
24607
|
+
);
|
|
24608
|
+
changes.push({ path: main, content: wrapped });
|
|
24609
|
+
details.push(
|
|
24610
|
+
`- \`${file2}\`: a friendly message and a \u201CReload the page\u201D button when something breaks, instead of a blank page`,
|
|
24611
|
+
`- \`${main}\`: wraps the app in it`
|
|
24612
|
+
);
|
|
24613
|
+
}
|
|
24614
|
+
}
|
|
24615
|
+
if (usesRouter(ctx.pkg) && !hasCatchAll(ctx)) {
|
|
24616
|
+
const app2 = ctx.files.find((f) => /^src\/App\.tsx$/.test(f));
|
|
24617
|
+
const text = app2 ? read3(app2) : null;
|
|
24618
|
+
const routes2 = text?.match(/\n([ \t]*)<\/Routes>/g) ?? [];
|
|
24619
|
+
const page = "src/pages/NotFound.tsx";
|
|
24620
|
+
const pkg2 = allDeps(ctx.pkg)["react-router-dom"] ? "react-router-dom" : "react-router";
|
|
24621
|
+
if (!app2 || !text || routes2.length !== 1 || !/<Route\b/.test(text)) {
|
|
24622
|
+
notes.push("- A \u201Cpage not found\u201D route: `src/App.tsx` isn\u2019t in the shape Aura edits safely. Run `aura fix add-error-boundary`.");
|
|
24623
|
+
} else {
|
|
24624
|
+
const indent = /\n([ \t]*)<Route\b[^\n]*\n(?![\s\S]*<Route\b)/.exec(text)?.[1] ?? " ";
|
|
24625
|
+
let updated = text.replace(/\n([ \t]*)<\/Routes>/, `
|
|
24626
|
+
${indent}<Route path="*" element={<NotFound />} />
|
|
24627
|
+
$1</Routes>`);
|
|
24628
|
+
if (!/\bNotFound\b/.test(text)) updated = addImport(updated, 'import NotFound from "./pages/NotFound";');
|
|
24629
|
+
if (!existsSync14(path20.join(root, page)) && !/\bNotFound\b/.test(text)) {
|
|
24630
|
+
changes.push({ path: page, content: styled(readTemplate("vite/NotFound.tsx").replace('"react-router-dom"', `"${pkg2}"`), style) });
|
|
24631
|
+
details.push(`- \`${page}\`: a \u201Cpage not found\u201D page with a link home`);
|
|
24632
|
+
}
|
|
24633
|
+
changes.push({ path: app2, content: updated });
|
|
24634
|
+
details.push(`- \`${app2}\`: sends every unknown address to it`);
|
|
24635
|
+
}
|
|
24636
|
+
}
|
|
24637
|
+
if (changes.length === 0)
|
|
24638
|
+
return { skip: notes.length ? notes.join("\n").replace(/^- /gm, "") : "The app already has an error screen and a \u201Cpage not found\u201D page." };
|
|
24639
|
+
return { changes, details: [...details, ...notes.length ? ["", "**Not in this PR**", ...notes] : []], verifyTypes: true };
|
|
24640
|
+
}
|
|
24641
|
+
|
|
24642
|
+
// src/actions/security-headers.ts
|
|
24643
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
|
|
24644
|
+
import path21 from "path";
|
|
24645
|
+
function read(root, file2) {
|
|
24646
|
+
try {
|
|
24647
|
+
return readFileSync10(path21.join(root, file2), "utf8");
|
|
24648
|
+
} catch {
|
|
24649
|
+
return null;
|
|
24650
|
+
}
|
|
24651
|
+
}
|
|
24652
|
+
var SECURITY_HEADERS2 = [
|
|
24653
|
+
["Strict-Transport-Security", "max-age=63072000"],
|
|
24654
|
+
["X-Content-Type-Options", "nosniff"],
|
|
24655
|
+
["X-Frame-Options", "DENY"],
|
|
24656
|
+
["Referrer-Policy", "strict-origin-when-cross-origin"],
|
|
24657
|
+
["Permissions-Policy", "camera=(), microphone=(), geolocation=()"]
|
|
24658
|
+
];
|
|
24659
|
+
var HEADER_DETAILS = [
|
|
24660
|
+
"- HSTS, nosniff, frame protection, referrer policy and permissions policy on every page",
|
|
24661
|
+
"- No Content Security Policy yet (it often breaks apps; add it separately)"
|
|
24662
|
+
];
|
|
24663
|
+
var NEXT_HEADERS = `const securityHeaders = [
|
|
24664
|
+
{ key: "Strict-Transport-Security", value: "max-age=63072000" },
|
|
24665
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
24666
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
24667
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
24668
|
+
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
24669
|
+
];
|
|
24670
|
+
`;
|
|
24671
|
+
var addSecurityHeaders = (g) => {
|
|
24672
|
+
if (g.ctx.stack.vite) return addSpaSecurityHeaders(g);
|
|
24673
|
+
const { root, ctx } = g;
|
|
24674
|
+
const file2 = ctx.files.find((f) => /^next\.config\.[cm]?[jt]s$/.test(f));
|
|
24675
|
+
if (!file2) return { skip: "No next.config file found." };
|
|
24676
|
+
const text = read(root, file2);
|
|
24677
|
+
if (text === null) return { skip: `Aura couldn\u2019t read ${file2}.` };
|
|
24678
|
+
if (/\bheaders\s*\(/.test(text))
|
|
24679
|
+
return { skip: `${file2} already has a headers() function. Run \`aura fix add-security-headers\` to have Aura\u2019s agent merge them in.` };
|
|
24680
|
+
const m = text.match(/(const\s+nextConfig(?:\s*:\s*NextConfig)?\s*=\s*\{)/);
|
|
24681
|
+
if (!m || m.index === void 0) return { skip: `${file2} isn\u2019t in a shape Aura can edit safely. Run \`aura fix add-security-headers\` instead.` };
|
|
24682
|
+
const insertAt = m.index + m[0].length;
|
|
24683
|
+
const headersFn = `
|
|
24684
|
+
async headers() {
|
|
24685
|
+
return [{ source: "/:path*", headers: securityHeaders }];
|
|
24686
|
+
},`;
|
|
24687
|
+
const withFn = text.slice(0, insertAt) + headersFn + text.slice(insertAt);
|
|
24688
|
+
const content2 = withFn.replace(m[0], `${NEXT_HEADERS}
|
|
24689
|
+
${m[0]}`);
|
|
24690
|
+
return {
|
|
24691
|
+
verifyTypes: true,
|
|
24692
|
+
changes: [{ path: file2, content: content2 }],
|
|
24693
|
+
details: [
|
|
24694
|
+
"- HSTS, nosniff, frame protection, referrer policy and permissions policy on every page",
|
|
24695
|
+
"- No Content Security Policy yet (it often breaks apps; add it separately)"
|
|
24696
|
+
]
|
|
24697
|
+
};
|
|
24698
|
+
};
|
|
24699
|
+
function addSpaSecurityHeaders({ root, ctx }) {
|
|
24700
|
+
const deps2 = allDeps(ctx.pkg);
|
|
24701
|
+
const vercel2 = ctx.files.includes("vercel.json") || ctx.stack.services.includes("Vercel");
|
|
24702
|
+
const netlify = ctx.files.includes("netlify.toml") || Boolean(deps2["netlify-cli"]);
|
|
24703
|
+
const cloudflare = ctx.files.some((f) => /^wrangler\.(?:toml|jsonc?)$/.test(f)) || Boolean(deps2.wrangler);
|
|
24704
|
+
if (vercel2) {
|
|
24705
|
+
const existing = existsSync15(path21.join(root, "vercel.json")) ? ctx.read("vercel.json") : null;
|
|
24706
|
+
let config2 = {};
|
|
24707
|
+
try {
|
|
24708
|
+
config2 = existing ? JSON.parse(existing) : {};
|
|
24709
|
+
} catch {
|
|
24710
|
+
return { skip: "vercel.json isn\u2019t valid JSON, so Aura won\u2019t edit it. Run `aura fix add-security-headers`." };
|
|
24711
|
+
}
|
|
24712
|
+
if ("headers" in config2) return { skip: "vercel.json already has headers. Run `aura fix add-security-headers` to have Aura\u2019s agent merge them in." };
|
|
24713
|
+
const headers2 = [{ source: "/(.*)", headers: SECURITY_HEADERS2.map(([key, value]) => ({ key, value })) }];
|
|
24714
|
+
return {
|
|
24715
|
+
changes: [{ path: "vercel.json", content: `${JSON.stringify({ ...config2, headers: headers2 }, null, 2)}
|
|
24716
|
+
` }],
|
|
24717
|
+
details: ["- `vercel.json`:", ...HEADER_DETAILS]
|
|
24718
|
+
};
|
|
24719
|
+
}
|
|
24720
|
+
if (netlify || cloudflare) {
|
|
24721
|
+
if (existsSync15(path21.join(root, "public", "_headers")))
|
|
24722
|
+
return { skip: "`public/_headers` already exists. Run `aura fix add-security-headers` to have Aura\u2019s agent merge them in." };
|
|
24723
|
+
const content2 = `/*
|
|
24724
|
+
${SECURITY_HEADERS2.map(([k, v]) => ` ${k}: ${v}`).join("\n")}
|
|
24725
|
+
`;
|
|
24726
|
+
return {
|
|
24727
|
+
changes: [{ path: "public/_headers", content: content2 }],
|
|
24728
|
+
details: [`- \`public/_headers\` (${netlify ? "Netlify" : "Cloudflare Pages"}):`, ...HEADER_DETAILS]
|
|
24729
|
+
};
|
|
24730
|
+
}
|
|
24731
|
+
return {
|
|
24732
|
+
skip: "Aura can\u2019t tell where this app is hosted, and single-page apps get their headers from the host. On Vercel or Netlify, connect the repo there and run this again. Lovable\u2019s own hosting doesn\u2019t let apps set headers yet. See `aura guide security-headers`."
|
|
24733
|
+
};
|
|
24734
|
+
}
|
|
24735
|
+
|
|
24736
|
+
// src/actions/git-hygiene.ts
|
|
24737
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
24738
|
+
import path22 from "path";
|
|
24115
24739
|
var ignoreLocalFiles = async ({ root, ctx }) => {
|
|
24116
24740
|
const problems = await localFileProblems(ctx) ?? [];
|
|
24117
24741
|
if (problems.length === 0) return { skip: "Tool and machine files are already ignored." };
|
|
24118
24742
|
let current2 = "";
|
|
24119
24743
|
try {
|
|
24120
|
-
current2 =
|
|
24744
|
+
current2 = readFileSync11(path22.join(root, ".gitignore"), "utf8");
|
|
24121
24745
|
} catch {
|
|
24122
24746
|
}
|
|
24123
24747
|
const lines = new Set(current2.split(/\r?\n/).map((l) => l.trim()));
|
|
@@ -24146,17 +24770,17 @@ var saveAuraSetup = async ({ root }) => {
|
|
|
24146
24770
|
const files = await unsavedSetup({ root }) ?? [];
|
|
24147
24771
|
if (files.length === 0) return { skip: "Everything Aura set up is already committed." };
|
|
24148
24772
|
return {
|
|
24149
|
-
changes: files.map((file2) => ({ path: file2, content:
|
|
24773
|
+
changes: files.map((file2) => ({ path: file2, content: readFileSync11(path22.join(root, file2), "utf8") })),
|
|
24150
24774
|
details: files.map((f) => `- \`${f}\``)
|
|
24151
24775
|
};
|
|
24152
24776
|
};
|
|
24153
24777
|
|
|
24154
24778
|
// src/actions/agent-files.ts
|
|
24155
|
-
import { existsSync as
|
|
24156
|
-
import
|
|
24779
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
|
|
24780
|
+
import path23 from "path";
|
|
24157
24781
|
function current(root, file2) {
|
|
24158
|
-
const full =
|
|
24159
|
-
return
|
|
24782
|
+
const full = path23.join(root, file2);
|
|
24783
|
+
return existsSync16(full) ? readFileSync12(full, "utf8") : null;
|
|
24160
24784
|
}
|
|
24161
24785
|
function upsertBlock(existing, body) {
|
|
24162
24786
|
const block = `${AURA_BLOCK_START}
|
|
@@ -24199,12 +24823,12 @@ function skillChanges(agents2, names) {
|
|
|
24199
24823
|
}
|
|
24200
24824
|
|
|
24201
24825
|
// src/actions/generators.ts
|
|
24202
|
-
function
|
|
24203
|
-
const full =
|
|
24204
|
-
return
|
|
24826
|
+
function read2(root, file2) {
|
|
24827
|
+
const full = path24.join(root, file2);
|
|
24828
|
+
return existsSync17(full) ? readFileSync13(full, "utf8") : null;
|
|
24205
24829
|
}
|
|
24206
24830
|
function withScripts(root, scripts, overwrite = false) {
|
|
24207
|
-
const text =
|
|
24831
|
+
const text = read2(root, "package.json");
|
|
24208
24832
|
if (!text) return null;
|
|
24209
24833
|
const indent = text.match(/^\{\n(\s+)"/)?.[1] ?? " ";
|
|
24210
24834
|
const pkg2 = JSON.parse(text);
|
|
@@ -24223,14 +24847,14 @@ function findingMeta(scan, checkId, key) {
|
|
|
24223
24847
|
return scan.checks.find((c) => c.checkId === checkId)?.findings[0]?.meta?.[key];
|
|
24224
24848
|
}
|
|
24225
24849
|
var addBrief = ({ root }) => {
|
|
24226
|
-
const brief2 = BRIEF_PATHS.find((f) =>
|
|
24227
|
-
const briefText = brief2 ?
|
|
24850
|
+
const brief2 = BRIEF_PATHS.find((f) => existsSync17(path24.join(root, f)));
|
|
24851
|
+
const briefText = brief2 ? read2(root, brief2) : null;
|
|
24228
24852
|
if (!brief2 || briefText === null) return { skip: "There is no product brief yet. Run `aura init` to draft and confirm it first." };
|
|
24229
24853
|
const changes = [
|
|
24230
24854
|
{ path: brief2, content: briefText },
|
|
24231
24855
|
{ path: ".aura/.gitignore", content: AURA_GITIGNORE }
|
|
24232
24856
|
];
|
|
24233
|
-
const config2 =
|
|
24857
|
+
const config2 = read2(root, ".aura/config.json");
|
|
24234
24858
|
if (config2 !== null) changes.push({ path: ".aura/config.json", content: config2 });
|
|
24235
24859
|
return { changes, details: [`- Adds \`${brief2}\`, \`.aura/config.json\` and \`.aura/.gitignore\` (keeps Aura's local reports out of git)`] };
|
|
24236
24860
|
};
|
|
@@ -24250,14 +24874,14 @@ var installSkills = ({ root, agents: agents2, skills: skills2 }) => {
|
|
|
24250
24874
|
};
|
|
24251
24875
|
var tokenEfficiency = (g) => {
|
|
24252
24876
|
const changes = [];
|
|
24253
|
-
if (!
|
|
24877
|
+
if (!existsSync17(path24.join(g.root, "docs/index.md"))) changes.push({ path: "docs/index.md", content: render("docs/index.md", g.vars) });
|
|
24254
24878
|
changes.push(...rulesChanges(g.root, g.agents, g.vars));
|
|
24255
24879
|
return { changes, details: ["- `docs/index.md`: a map of the repo your agent reads first", "- Lean-context rules in the agent rules file"] };
|
|
24256
24880
|
};
|
|
24257
24881
|
var addCi = ({ root, ctx, vars }) => {
|
|
24258
24882
|
const existing = ctx.files.filter((f) => /^\.github\/workflows\//.test(f));
|
|
24259
24883
|
const file2 = existing.includes(".github/workflows/ci.yml") ? ".github/workflows/checks.yml" : ".github/workflows/ci.yml";
|
|
24260
|
-
if (
|
|
24884
|
+
if (existsSync17(path24.join(root, file2))) return { skip: `${file2} already exists.` };
|
|
24261
24885
|
return {
|
|
24262
24886
|
changes: [{ path: file2, content: render("github/workflows/ci.yml", vars) }],
|
|
24263
24887
|
details: [`- \`${file2}\`: lint, type check, tests and build on every pull request`]
|
|
@@ -24281,8 +24905,8 @@ var setupTesting = ({ root, ctx, vars }) => {
|
|
|
24281
24905
|
};
|
|
24282
24906
|
};
|
|
24283
24907
|
var addEnvExample = ({ root, scan }) => {
|
|
24284
|
-
const file2 = [".env.example", ".env.sample", ".env.template"].find((f) =>
|
|
24285
|
-
const existing =
|
|
24908
|
+
const file2 = [".env.example", ".env.sample", ".env.template"].find((f) => existsSync17(path24.join(root, f))) ?? ".env.example";
|
|
24909
|
+
const existing = read2(root, file2);
|
|
24286
24910
|
const declared = new Set(
|
|
24287
24911
|
(existing ?? "").split(/\r?\n/).map((l) => l.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1]).filter(Boolean)
|
|
24288
24912
|
);
|
|
@@ -24317,11 +24941,15 @@ var addLinting = ({ root, ctx }) => {
|
|
|
24317
24941
|
const changes = [];
|
|
24318
24942
|
const devDeps = [];
|
|
24319
24943
|
const details = [];
|
|
24320
|
-
|
|
24944
|
+
const addEslint = !hasLint && ctx.stack.nextjs !== null;
|
|
24945
|
+
if (!hasLint && !addEslint && hasFormat)
|
|
24946
|
+
return { skip: "Aura\u2019s ESLint template is for Next.js apps. Run `aura fix add-linting` to have Aura\u2019s agent set ESLint up for this app." };
|
|
24947
|
+
if (addEslint) {
|
|
24321
24948
|
changes.push({ path: "eslint.config.mjs", content: readTemplate("configs/eslint.config.mjs") });
|
|
24322
24949
|
devDeps.push("eslint", "eslint-config-next");
|
|
24323
24950
|
details.push("- ESLint with the standard Next.js rules (`lint` script)");
|
|
24324
|
-
}
|
|
24951
|
+
} else if (!hasLint)
|
|
24952
|
+
details.push("- ESLint isn\u2019t added here: Aura\u2019s template is for Next.js. Run `aura fix add-linting` afterwards to set it up for this app.");
|
|
24325
24953
|
if (!hasFormat) {
|
|
24326
24954
|
changes.push(
|
|
24327
24955
|
{ path: ".prettierrc.json", content: readTemplate("configs/prettierrc.json") },
|
|
@@ -24331,14 +24959,14 @@ var addLinting = ({ root, ctx }) => {
|
|
|
24331
24959
|
details.push("- Prettier (`format` and `format:check` scripts). It doesn\u2019t reformat existing files in this PR.");
|
|
24332
24960
|
}
|
|
24333
24961
|
const scripts = withScripts(root, {
|
|
24334
|
-
|
|
24962
|
+
...addEslint ? { lint: "eslint ." } : {},
|
|
24335
24963
|
...!hasFormat ? { format: "prettier --write .", "format:check": "prettier --check ." } : {}
|
|
24336
24964
|
});
|
|
24337
24965
|
if (scripts) changes.push(scripts);
|
|
24338
24966
|
return { changes, devDeps, details };
|
|
24339
24967
|
};
|
|
24340
24968
|
var addPreCommit = ({ root, ctx, vars }) => {
|
|
24341
|
-
if (
|
|
24969
|
+
if (existsSync17(path24.join(root, ".husky/pre-commit"))) return { skip: "A husky pre-commit hook already exists." };
|
|
24342
24970
|
const changes = [{ path: ".husky/pre-commit", content: render("configs/husky-pre-commit", vars) }];
|
|
24343
24971
|
const scripts = withScripts(root, { prepare: "husky" }, false);
|
|
24344
24972
|
if (scripts) changes.push(scripts);
|
|
@@ -24351,7 +24979,7 @@ var addPreCommit = ({ root, ctx, vars }) => {
|
|
|
24351
24979
|
};
|
|
24352
24980
|
var addReadme = ({ root, ctx, vars }) => {
|
|
24353
24981
|
const file2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f)) ?? "README.md";
|
|
24354
|
-
const current2 =
|
|
24982
|
+
const current2 = read2(root, file2);
|
|
24355
24983
|
const boilerplate = current2 && /create-next-app|bootstrapped with/i.test(current2);
|
|
24356
24984
|
const words2 = current2?.split(/\s+/).filter(Boolean).length ?? 0;
|
|
24357
24985
|
if (current2 && !boilerplate && words2 >= 80) return { skip: `${file2} already describes the app.` };
|
|
@@ -24368,7 +24996,7 @@ ${current2.trim()}
|
|
|
24368
24996
|
};
|
|
24369
24997
|
};
|
|
24370
24998
|
var enableDependencyUpdates = ({ root, vars }) => {
|
|
24371
|
-
if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) =>
|
|
24999
|
+
if ([".github/dependabot.yml", ".github/dependabot.yaml", "renovate.json", ".github/renovate.json"].some((f) => existsSync17(path24.join(root, f)))) {
|
|
24372
25000
|
return { skip: "Dependabot or Renovate is already configured." };
|
|
24373
25001
|
}
|
|
24374
25002
|
return {
|
|
@@ -24396,40 +25024,6 @@ var fixVulnerableDeps = ({ ctx, scan }) => {
|
|
|
24396
25024
|
details: [...direct.map((f) => `- Upgrades \`${f}\``), ...pm === "npm" ? ["- Runs `npm audit fix` (no breaking major upgrades)"] : []]
|
|
24397
25025
|
};
|
|
24398
25026
|
};
|
|
24399
|
-
var SECURITY_HEADERS2 = `const securityHeaders = [
|
|
24400
|
-
{ key: "Strict-Transport-Security", value: "max-age=63072000" },
|
|
24401
|
-
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
24402
|
-
{ key: "X-Frame-Options", value: "DENY" },
|
|
24403
|
-
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
24404
|
-
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
24405
|
-
];
|
|
24406
|
-
`;
|
|
24407
|
-
var addSecurityHeaders = ({ root, ctx }) => {
|
|
24408
|
-
const file2 = ctx.files.find((f) => /^next\.config\.[cm]?[jt]s$/.test(f));
|
|
24409
|
-
if (!file2) return { skip: "No next.config file found." };
|
|
24410
|
-
const text = read(root, file2);
|
|
24411
|
-
if (text === null) return { skip: `Aura couldn\u2019t read ${file2}.` };
|
|
24412
|
-
if (/\bheaders\s*\(/.test(text))
|
|
24413
|
-
return { skip: `${file2} already has a headers() function. Run \`aura fix add-security-headers\` to have Aura\u2019s agent merge them in.` };
|
|
24414
|
-
const m = text.match(/(const\s+nextConfig(?:\s*:\s*NextConfig)?\s*=\s*\{)/);
|
|
24415
|
-
if (!m || m.index === void 0) return { skip: `${file2} isn\u2019t in a shape Aura can edit safely. Run \`aura fix add-security-headers\` instead.` };
|
|
24416
|
-
const insertAt = m.index + m[0].length;
|
|
24417
|
-
const headersFn = `
|
|
24418
|
-
async headers() {
|
|
24419
|
-
return [{ source: "/:path*", headers: securityHeaders }];
|
|
24420
|
-
},`;
|
|
24421
|
-
const withFn = text.slice(0, insertAt) + headersFn + text.slice(insertAt);
|
|
24422
|
-
const content2 = withFn.replace(m[0], `${SECURITY_HEADERS2}
|
|
24423
|
-
${m[0]}`);
|
|
24424
|
-
return {
|
|
24425
|
-
verifyTypes: true,
|
|
24426
|
-
changes: [{ path: file2, content: content2 }],
|
|
24427
|
-
details: [
|
|
24428
|
-
"- HSTS, nosniff, frame protection, referrer policy and permissions policy on every page",
|
|
24429
|
-
"- No Content Security Policy yet (it often breaks apps; add it separately)"
|
|
24430
|
-
]
|
|
24431
|
-
};
|
|
24432
|
-
};
|
|
24433
25027
|
var removeDeadFiles = ({ scan, only }) => {
|
|
24434
25028
|
const files = (findingMeta(scan, "structure.dead-files", "files") ?? []).filter((f) => !only || only.includes(f));
|
|
24435
25029
|
if (files.length === 0) return { skip: "No dead files found." };
|
|
@@ -24448,7 +25042,7 @@ function planMoves(ctx, moves) {
|
|
|
24448
25042
|
const alias = aliasFor(toFile);
|
|
24449
25043
|
if (alias) return alias[0] + noExt.slice(alias[1].length);
|
|
24450
25044
|
}
|
|
24451
|
-
let rel =
|
|
25045
|
+
let rel = path24.posix.relative(path24.posix.dirname(fromFile), noExt);
|
|
24452
25046
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
24453
25047
|
return rel;
|
|
24454
25048
|
};
|
|
@@ -24482,7 +25076,7 @@ function planMoves(ctx, moves) {
|
|
|
24482
25076
|
}
|
|
24483
25077
|
var moveMisplacedFiles = ({ root, ctx, scan, only }) => {
|
|
24484
25078
|
const raw = findingMeta(scan, "structure.layout", "moves") ?? [];
|
|
24485
|
-
const moves = raw.map((m) => m.split("=>")).filter(([from, to]) => !
|
|
25079
|
+
const moves = raw.map((m) => m.split("=>")).filter(([from, to]) => !existsSync17(path24.join(root, to)) && (!only || only.includes(from))).slice(0, MAX_MOVES);
|
|
24486
25080
|
if (moves.length === 0) return { skip: "No files to move (or their targets already exist)." };
|
|
24487
25081
|
return { changes: planMoves(ctx, moves), details: moves.map(([from, to]) => `- \`${from}\` \u2192 \`${to}\``), verifyTypes: true };
|
|
24488
25082
|
};
|
|
@@ -24490,7 +25084,7 @@ var addAuraWorkflow = ({ root, vars }) => {
|
|
|
24490
25084
|
const changes = [];
|
|
24491
25085
|
for (const name of ["aura.yml", "aura-weekly.yml"]) {
|
|
24492
25086
|
const content2 = render(`github/workflows/${name}`, vars);
|
|
24493
|
-
if (
|
|
25087
|
+
if (read2(root, `.github/workflows/${name}`) !== content2) changes.push({ path: `.github/workflows/${name}`, content: content2 });
|
|
24494
25088
|
}
|
|
24495
25089
|
if (changes.length === 0) return { skip: "The Aura workflows are already installed and up to date." };
|
|
24496
25090
|
return {
|
|
@@ -24522,7 +25116,8 @@ var GENERATORS = {
|
|
|
24522
25116
|
"ignore-local-files": ignoreLocalFiles,
|
|
24523
25117
|
"save-aura-setup": saveAuraSetup,
|
|
24524
25118
|
"protect-agent-secrets": protectAgentSecrets,
|
|
24525
|
-
"add-error-pages": addErrorPages
|
|
25119
|
+
"add-error-pages": addErrorPages,
|
|
25120
|
+
"add-error-boundary": addErrorBoundary
|
|
24526
25121
|
};
|
|
24527
25122
|
|
|
24528
25123
|
// src/actions/foundation.ts
|
|
@@ -24544,7 +25139,7 @@ async function foundationPlan(g) {
|
|
|
24544
25139
|
}
|
|
24545
25140
|
|
|
24546
25141
|
// src/actions/large-change.ts
|
|
24547
|
-
import { existsSync as
|
|
25142
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
|
|
24548
25143
|
|
|
24549
25144
|
// src/ui.ts
|
|
24550
25145
|
import * as p from "@clack/prompts";
|
|
@@ -24729,9 +25324,9 @@ function writePlanFile(root, plan) {
|
|
|
24729
25324
|
}
|
|
24730
25325
|
function readProgress(root, actionId, now = Date.now()) {
|
|
24731
25326
|
const file2 = auraPath(root, "plans", `${actionId}.json`);
|
|
24732
|
-
if (!
|
|
25327
|
+
if (!existsSync18(file2)) return { started: [] };
|
|
24733
25328
|
try {
|
|
24734
|
-
const data = JSON.parse(
|
|
25329
|
+
const data = JSON.parse(readFileSync14(file2, "utf8"));
|
|
24735
25330
|
const started = Array.isArray(data.started) ? data.started : [];
|
|
24736
25331
|
return { started: started.filter((s) => Array.isArray(s.files) && now - Date.parse(s.at) < PROGRESS_TTL_MS) };
|
|
24737
25332
|
} catch {
|
|
@@ -24784,16 +25379,16 @@ function afterChunkMessage(plan, next) {
|
|
|
24784
25379
|
}
|
|
24785
25380
|
|
|
24786
25381
|
// src/fix-tracking.ts
|
|
24787
|
-
import { existsSync as
|
|
25382
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15 } from "fs";
|
|
24788
25383
|
|
|
24789
25384
|
// src/api/usage.ts
|
|
24790
25385
|
import { createHash } from "crypto";
|
|
24791
25386
|
|
|
24792
25387
|
// src/api/client.ts
|
|
24793
25388
|
var AuraApiError = class extends Error {
|
|
24794
|
-
constructor(
|
|
25389
|
+
constructor(code2, message) {
|
|
24795
25390
|
super(message);
|
|
24796
|
-
this.code =
|
|
25391
|
+
this.code = code2;
|
|
24797
25392
|
}
|
|
24798
25393
|
code;
|
|
24799
25394
|
};
|
|
@@ -24829,7 +25424,7 @@ var AuraClient = class _AuraClient {
|
|
|
24829
25424
|
get hasKey() {
|
|
24830
25425
|
return Boolean(this.key);
|
|
24831
25426
|
}
|
|
24832
|
-
async request(method,
|
|
25427
|
+
async request(method, path55, body, timeoutMs = 6e4) {
|
|
24833
25428
|
if (!this.key) throw new AuraApiError("no_key", "No pilot key");
|
|
24834
25429
|
let payload;
|
|
24835
25430
|
if (body !== void 0) {
|
|
@@ -24839,7 +25434,7 @@ var AuraClient = class _AuraClient {
|
|
|
24839
25434
|
}
|
|
24840
25435
|
let res;
|
|
24841
25436
|
try {
|
|
24842
|
-
res = await fetch(`${this.baseUrl}/v1/${
|
|
25437
|
+
res = await fetch(`${this.baseUrl}/v1/${path55}`, {
|
|
24843
25438
|
method,
|
|
24844
25439
|
headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json", "user-agent": `getaura/${AURA_VERSION}` },
|
|
24845
25440
|
body: payload,
|
|
@@ -24856,8 +25451,8 @@ var AuraClient = class _AuraClient {
|
|
|
24856
25451
|
}
|
|
24857
25452
|
if (!res.ok) {
|
|
24858
25453
|
const error62 = json2?.error;
|
|
24859
|
-
const
|
|
24860
|
-
throw new AuraApiError(
|
|
25454
|
+
const code2 = error62?.code ?? (res.status === 401 ? "unauthorized" : res.status === 402 ? "cap_reached" : "model_error");
|
|
25455
|
+
throw new AuraApiError(code2, error62?.message ?? `HTTP ${res.status}`);
|
|
24861
25456
|
}
|
|
24862
25457
|
return json2;
|
|
24863
25458
|
}
|
|
@@ -24923,7 +25518,7 @@ var MAX_AGE_MS = 60 * 24 * 60 * 60 * 1e3;
|
|
|
24923
25518
|
var MAX_CHECKS_PER_SCAN = 5;
|
|
24924
25519
|
function loadTrackedFixes(root) {
|
|
24925
25520
|
try {
|
|
24926
|
-
return
|
|
25521
|
+
return existsSync19(auraPath(root, FILE)) ? JSON.parse(readFileSync15(auraPath(root, FILE), "utf8")) : [];
|
|
24927
25522
|
} catch {
|
|
24928
25523
|
return [];
|
|
24929
25524
|
}
|
|
@@ -25000,11 +25595,11 @@ async function recordAction(root, config2, actionId, report, startedAt, via) {
|
|
|
25000
25595
|
|
|
25001
25596
|
// src/scan.ts
|
|
25002
25597
|
import { execFileSync } from "child_process";
|
|
25003
|
-
import { existsSync as
|
|
25598
|
+
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
|
|
25004
25599
|
|
|
25005
25600
|
// src/api/assess.ts
|
|
25006
25601
|
import { createHash as createHash4 } from "crypto";
|
|
25007
|
-
import { readFileSync as
|
|
25602
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
25008
25603
|
|
|
25009
25604
|
// src/checks/foundations.ts
|
|
25010
25605
|
import YAML4 from "yaml";
|
|
@@ -25400,12 +25995,12 @@ function i18nInfo(ctx) {
|
|
|
25400
25995
|
const briefFile = BRIEF_PATHS.map((p2) => ctx.read(p2)).find((t) => t !== null);
|
|
25401
25996
|
const brief2 = briefFile ? briefLanguageSignals(briefFile) : { multilingual: false, rtl: false };
|
|
25402
25997
|
if (brief2.multilingual) reasons.push("the brief says the app supports several languages");
|
|
25403
|
-
const
|
|
25404
|
-
return { reasons, library, locales:
|
|
25998
|
+
const list11 = [...locales].sort();
|
|
25999
|
+
return { reasons, library, locales: list11, defaultLocale, rtl: brief2.rtl || list11.some(isRtlLocale), messages };
|
|
25405
26000
|
}
|
|
25406
26001
|
|
|
25407
26002
|
// src/checks/ui-files.ts
|
|
25408
|
-
import
|
|
26003
|
+
import path25 from "path";
|
|
25409
26004
|
var UI_FILE = /\.(?:tsx|jsx|vue|svelte|css|scss|less)$/;
|
|
25410
26005
|
var STYLE_FILE = /\.(?:css|scss|less)$/;
|
|
25411
26006
|
var SCRIPT_FILE = /\.[cm]?[jt]sx?$/;
|
|
@@ -25418,7 +26013,7 @@ function nameWords(stem) {
|
|
|
25418
26013
|
return stem.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
25419
26014
|
}
|
|
25420
26015
|
function exclusionReason(file2, text) {
|
|
25421
|
-
const base2 =
|
|
26016
|
+
const base2 = path25.posix.basename(file2);
|
|
25422
26017
|
const stem = base2.replace(/\.[^.]+$/, "").replace(/\.module$/, "");
|
|
25423
26018
|
const dirs = file2.split("/").slice(0, -1);
|
|
25424
26019
|
if (isTestFile(file2) || /\.stories\.[cm]?[jt]sx?$/.test(file2)) return "test";
|
|
@@ -25466,7 +26061,7 @@ function zoomBlocks(text) {
|
|
|
25466
26061
|
return n;
|
|
25467
26062
|
}
|
|
25468
26063
|
var MAX_LISTED = 20;
|
|
25469
|
-
var
|
|
26064
|
+
var KINDS2 = ["image-alt", "field-label", "click-keyboard", "control-name", "link-button", "positive-tabindex", "page-lang", "zoom"];
|
|
25470
26065
|
var CERTAIN = /* @__PURE__ */ new Set(["page-lang", "zoom"]);
|
|
25471
26066
|
function listCounts(items, withCounts) {
|
|
25472
26067
|
const shown = items.slice(0, MAX_LISTED).map((i) => withCounts ? `${i.file} (${i.count})` : i.file);
|
|
@@ -25496,7 +26091,7 @@ var accessibility = {
|
|
|
25496
26091
|
}
|
|
25497
26092
|
if (i18nInfo(ctx).reasons.length === 0) for (const file2 of missingLang(ctx)) add("page-lang", file2, 1);
|
|
25498
26093
|
const items = [];
|
|
25499
|
-
for (const kind of
|
|
26094
|
+
for (const kind of KINDS2) {
|
|
25500
26095
|
const found = (perKind.get(kind) ?? []).sort((a, b) => b.count - a.count || a.file.localeCompare(b.file));
|
|
25501
26096
|
if (found.length === 0) continue;
|
|
25502
26097
|
const total2 = found.reduce((s, i) => s + i.count, 0);
|
|
@@ -25520,120 +26115,6 @@ var accessibility = {
|
|
|
25520
26115
|
}
|
|
25521
26116
|
};
|
|
25522
26117
|
|
|
25523
|
-
// src/core/code-text.ts
|
|
25524
|
-
var REGEX_BEFORE = /[(,=:[!&|?{};+\-*%~^]/;
|
|
25525
|
-
var REGEX_KEYWORD = /\b(?:return|typeof|case|in|of|void|yield|await|delete|throw|new)$/;
|
|
25526
|
-
function maskCode(src) {
|
|
25527
|
-
const out = src.split("");
|
|
25528
|
-
const blank = (from, to) => {
|
|
25529
|
-
for (let k = from; k < to && k < out.length; k++) if (out[k] !== "\n") out[k] = " ";
|
|
25530
|
-
};
|
|
25531
|
-
const n = src.length;
|
|
25532
|
-
const stack = [];
|
|
25533
|
-
let depth = 0;
|
|
25534
|
-
let inTemplate = false;
|
|
25535
|
-
let i = 0;
|
|
25536
|
-
while (i < n) {
|
|
25537
|
-
const c = src[i];
|
|
25538
|
-
if (inTemplate) {
|
|
25539
|
-
if (c === "\\") {
|
|
25540
|
-
blank(i, i + 2);
|
|
25541
|
-
i += 2;
|
|
25542
|
-
} else if (c === "`") {
|
|
25543
|
-
inTemplate = false;
|
|
25544
|
-
i++;
|
|
25545
|
-
} else if (c === "$" && src[i + 1] === "{") {
|
|
25546
|
-
stack.push(depth++);
|
|
25547
|
-
inTemplate = false;
|
|
25548
|
-
i += 2;
|
|
25549
|
-
} else {
|
|
25550
|
-
blank(i, i + 1);
|
|
25551
|
-
i++;
|
|
25552
|
-
}
|
|
25553
|
-
continue;
|
|
25554
|
-
}
|
|
25555
|
-
const next = src[i + 1];
|
|
25556
|
-
if (c === "/" && next === "/") {
|
|
25557
|
-
const end = src.indexOf("\n", i);
|
|
25558
|
-
const e = end < 0 ? n : end;
|
|
25559
|
-
blank(i, e);
|
|
25560
|
-
i = e;
|
|
25561
|
-
} else if (c === "/" && next === "*") {
|
|
25562
|
-
const end = src.indexOf("*/", i + 2);
|
|
25563
|
-
const e = end < 0 ? n : end + 2;
|
|
25564
|
-
blank(i, e);
|
|
25565
|
-
i = e;
|
|
25566
|
-
} else if (c === '"' || c === "'") {
|
|
25567
|
-
let j = i + 1;
|
|
25568
|
-
while (j < n && src[j] !== c && src[j] !== "\n") j += src[j] === "\\" ? 2 : 1;
|
|
25569
|
-
blank(i + 1, j);
|
|
25570
|
-
i = j + 1;
|
|
25571
|
-
} else if (c === "`") {
|
|
25572
|
-
inTemplate = true;
|
|
25573
|
-
i++;
|
|
25574
|
-
} else if (c === "/" && regexCanStart(out, i)) {
|
|
25575
|
-
let j = i + 1;
|
|
25576
|
-
let inClass = false;
|
|
25577
|
-
while (j < n && src[j] !== "\n" && (src[j] !== "/" || inClass)) {
|
|
25578
|
-
if (src[j] === "\\") j++;
|
|
25579
|
-
else if (src[j] === "[") inClass = true;
|
|
25580
|
-
else if (src[j] === "]") inClass = false;
|
|
25581
|
-
j++;
|
|
25582
|
-
}
|
|
25583
|
-
if (src[j] === "/") {
|
|
25584
|
-
blank(i + 1, j);
|
|
25585
|
-
i = j + 1;
|
|
25586
|
-
} else i++;
|
|
25587
|
-
} else {
|
|
25588
|
-
if (c === "{") depth++;
|
|
25589
|
-
else if (c === "}") {
|
|
25590
|
-
depth--;
|
|
25591
|
-
if (stack.length && stack[stack.length - 1] === depth) {
|
|
25592
|
-
stack.pop();
|
|
25593
|
-
inTemplate = true;
|
|
25594
|
-
}
|
|
25595
|
-
}
|
|
25596
|
-
i++;
|
|
25597
|
-
}
|
|
25598
|
-
}
|
|
25599
|
-
return out.join("");
|
|
25600
|
-
}
|
|
25601
|
-
function regexCanStart(out, i) {
|
|
25602
|
-
let k = i - 1;
|
|
25603
|
-
while (k >= 0 && /\s/.test(out[k] ?? "")) k--;
|
|
25604
|
-
if (k < 0) return true;
|
|
25605
|
-
const prev = out[k] ?? "";
|
|
25606
|
-
if (REGEX_BEFORE.test(prev)) return true;
|
|
25607
|
-
return REGEX_KEYWORD.test(out.slice(Math.max(0, k - 7), k + 1).join(""));
|
|
25608
|
-
}
|
|
25609
|
-
function closingIndex(masked, open2, limit = 4e3) {
|
|
25610
|
-
let depth = 0;
|
|
25611
|
-
const end = Math.min(masked.length, open2 + limit);
|
|
25612
|
-
for (let i = open2; i < end; i++) {
|
|
25613
|
-
const c = masked[i];
|
|
25614
|
-
if (c === "(" || c === "[" || c === "{") depth++;
|
|
25615
|
-
else if (c === ")" || c === "]" || c === "}") {
|
|
25616
|
-
depth--;
|
|
25617
|
-
if (depth === 0) return i + 1;
|
|
25618
|
-
}
|
|
25619
|
-
}
|
|
25620
|
-
return -1;
|
|
25621
|
-
}
|
|
25622
|
-
function lineFinder(text) {
|
|
25623
|
-
const starts = [0];
|
|
25624
|
-
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
25625
|
-
return (index) => {
|
|
25626
|
-
let lo = 0;
|
|
25627
|
-
let hi = starts.length - 1;
|
|
25628
|
-
while (lo < hi) {
|
|
25629
|
-
const mid = lo + hi + 1 >> 1;
|
|
25630
|
-
if ((starts[mid] ?? 0) <= index) lo = mid;
|
|
25631
|
-
else hi = mid - 1;
|
|
25632
|
-
}
|
|
25633
|
-
return lo + 1;
|
|
25634
|
-
};
|
|
25635
|
-
}
|
|
25636
|
-
|
|
25637
26118
|
// src/checks/security/webhook-signals.ts
|
|
25638
26119
|
var WEBHOOK_TEXT = [
|
|
25639
26120
|
/stripe-signature/i,
|
|
@@ -25684,10 +26165,10 @@ function isWebhookRoute(file2, text) {
|
|
|
25684
26165
|
}
|
|
25685
26166
|
function verifiesInText(text) {
|
|
25686
26167
|
const raw = text.replace(/\r\n/g, "\n");
|
|
25687
|
-
const
|
|
25688
|
-
if (matches(VERIFIED_CODE,
|
|
25689
|
-
if (VERIFY_LIBRARY.test(raw) && /\bverify\s*\(/.test(
|
|
25690
|
-
return SHARED_SECRET.test(raw) && /[!=]==?|timingSafeEqual/.test(
|
|
26168
|
+
const code2 = maskCode(raw);
|
|
26169
|
+
if (matches(VERIFIED_CODE, code2)) return true;
|
|
26170
|
+
if (VERIFY_LIBRARY.test(raw) && /\bverify\s*\(/.test(code2)) return true;
|
|
26171
|
+
return SHARED_SECRET.test(raw) && /[!=]==?|timingSafeEqual/.test(code2);
|
|
25691
26172
|
}
|
|
25692
26173
|
function isUnverifiedWebhook(file2, text) {
|
|
25693
26174
|
return isWebhookRoute(file2, text) && !verifiesInText(text);
|
|
@@ -25749,9 +26230,9 @@ function namedImports(text) {
|
|
|
25749
26230
|
}
|
|
25750
26231
|
return out;
|
|
25751
26232
|
}
|
|
25752
|
-
var callsName = (
|
|
25753
|
-
function importsAuthHelper(text,
|
|
25754
|
-
return namedImports(text).some((i) => /auth|session/i.test(i.from) && callsName(
|
|
26233
|
+
var callsName = (code2, name) => new RegExp(String.raw`\b${name.replace(/\$/g, "\\$")}\s*\(`).test(code2);
|
|
26234
|
+
function importsAuthHelper(text, code2) {
|
|
26235
|
+
return namedImports(text).some((i) => /auth|session/i.test(i.from) && callsName(code2 ?? text.slice(i.end), i.local));
|
|
25755
26236
|
}
|
|
25756
26237
|
function exportedCode(text, name) {
|
|
25757
26238
|
const start = text.search(new RegExp(String.raw`export\s+(?:async\s+)?(?:function\s*\*?\s*|const\s+|let\s+)${name.replace(/\$/g, "\\$")}\b`));
|
|
@@ -25766,8 +26247,8 @@ function wrapperChecksAuth(ctx, file2, text) {
|
|
|
25766
26247
|
if (!callsName(text.slice(end), local)) return false;
|
|
25767
26248
|
const target = resolveImport(file2, from, files, aliases);
|
|
25768
26249
|
const helper = target ? ctx.read(target) ?? "" : "";
|
|
25769
|
-
const
|
|
25770
|
-
return Boolean(
|
|
26250
|
+
const code2 = exportedCode(helper, name);
|
|
26251
|
+
return Boolean(code2 && (AUTH_SIGNALS.some((re) => re.test(code2)) || importsAuthHelper(helper, code2)));
|
|
25771
26252
|
});
|
|
25772
26253
|
}
|
|
25773
26254
|
function classifyRoute(text) {
|
|
@@ -25817,210 +26298,6 @@ var apiRoutes = {
|
|
|
25817
26298
|
}
|
|
25818
26299
|
};
|
|
25819
26300
|
|
|
25820
|
-
// src/checks/security/logs.ts
|
|
25821
|
-
var NOT_APP_CODE = /(?:^|\/)(?:node_modules|\.next|\.nuxt|\.svelte-kit|\.vercel|\.turbo|\.output|dist|build|out|coverage|vendor|public|generated|__generated__|__mocks__|\.aura|\.claude|\.agents|\.cursor|\.codex|\.pi)\/|\.min\.[cm]?js$|\.stories\.[cm]?[jt]sx?$|(?:^|\/)[^/]+\.config\.[cm]?[jt]s$|(?:^|\/)\.[\w-]+rc\.[cm]?js$|(?:^|\/)(?:database|supabase)\.types\.ts$/;
|
|
25822
|
-
function appCodeFiles(ctx) {
|
|
25823
|
-
return ctx.files.filter((f) => isSourceFile(f) && !isTestFile(f) && !NOT_APP_CODE.test(f));
|
|
25824
|
-
}
|
|
25825
|
-
var LOG_CALL = /(?<![\w$])(?:console|logger|log)\s*\??\.\s*(?:log|info|debug|warn|error|trace|fatal)\s*\(/g;
|
|
25826
|
-
var REDACTED_CALL = /(?<![\w$.])[\w$]*(?:redact|mask|sanitiz|scrub|obfuscat)[\w$]*\s*\(/gi;
|
|
25827
|
-
var CHAIN = /(?<![\w$.])[A-Za-z_$][\w$]*(?:\s*\??\.\s*[A-Za-z_$][\w$]*)*/g;
|
|
25828
|
-
var SECRET_ENV = /KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|DATABASE_URL|POSTGRES(?:QL)?_URL|REDIS_URL|MONGO(?:DB)?_URI|CONNECTION_STRING|SALT/;
|
|
25829
|
-
var PUBLIC_ENV = /^(?:NEXT_PUBLIC_|VITE_|PUBLIC_|EXPO_PUBLIC_|REACT_APP_|GATSBY_)/;
|
|
25830
|
-
var REQUEST = /^(?:req|request|ctx|context|event)$/;
|
|
25831
|
-
var PRESENCE_BEFORE = /(?:typeof\s+|!\s*|Boolean\(\s*|\bas\s+|\bkeyof\s+|Object\.keys\(\s*)$/;
|
|
25832
|
-
var CONTENT_METHODS = /* @__PURE__ */ new Set([
|
|
25833
|
-
"slice",
|
|
25834
|
-
"substring",
|
|
25835
|
-
"substr",
|
|
25836
|
-
"toString",
|
|
25837
|
-
"trim",
|
|
25838
|
-
"split",
|
|
25839
|
-
"padStart",
|
|
25840
|
-
"padEnd",
|
|
25841
|
-
"toUpperCase",
|
|
25842
|
-
"toLowerCase",
|
|
25843
|
-
"concat",
|
|
25844
|
-
"entries",
|
|
25845
|
-
"values",
|
|
25846
|
-
"toJSON"
|
|
25847
|
-
]);
|
|
25848
|
-
var SECRET_NAME2 = /(token|secret|password|passwd|passphrase|(?:api|private|secret|access|service|servicerole|signing|encryption|master|client)key|authorization|authheader|cookies?|jwt|credentials?|bearer)$/;
|
|
25849
|
-
var NOT_A_SECRET = /hash|masked|redacted|sanitiz|prefix|hint|last4|preview/i;
|
|
25850
|
-
var YES_NO_OR_VERB = /^(?:is|has|show|hide|should|needs?|can|toggle|validate|verify|check|create|generate|get|set|use|missing|invalid|no|with|requires?)[A-Z_]/;
|
|
25851
|
-
var SECRET_STRING_KEY = /^(?:authorization|cookie|setcookie|session|sessionid|jwt|credentials?)$|(?:token|secret|password|passwd|apikey)$/;
|
|
25852
|
-
var PERSON = /^(?:(?:[a-z][a-zA-Z0-9]*)?(User|Session|Profile|Customer)|(user|session|profile|customer))s?$/;
|
|
25853
|
-
function secretKind(word) {
|
|
25854
|
-
const w = word.toLowerCase();
|
|
25855
|
-
if (/pass/.test(w)) return "a password";
|
|
25856
|
-
if (/cookie/.test(w)) return "a cookie";
|
|
25857
|
-
if (/token$/.test(w)) return "a token";
|
|
25858
|
-
if (/key$/.test(w)) return "an API key";
|
|
25859
|
-
if (/credential/.test(w)) return "login credentials";
|
|
25860
|
-
if (/secret/.test(w)) return "a secret";
|
|
25861
|
-
if (/auth/.test(w)) return "an authorization header";
|
|
25862
|
-
if (/session/.test(w)) return "a session id";
|
|
25863
|
-
return "a token";
|
|
25864
|
-
}
|
|
25865
|
-
function isSecretName(name) {
|
|
25866
|
-
return !NOT_A_SECRET.test(name) && !YES_NO_OR_VERB.test(name) && SECRET_NAME2.test(name.toLowerCase().replace(/[_$]/g, ""));
|
|
25867
|
-
}
|
|
25868
|
-
function splitTopLevel(s) {
|
|
25869
|
-
const parts = [];
|
|
25870
|
-
let depth = 0;
|
|
25871
|
-
let start = 0;
|
|
25872
|
-
for (let i = 0; i < s.length; i++) {
|
|
25873
|
-
const c = s[i];
|
|
25874
|
-
if (c === "(" || c === "[" || c === "{") depth++;
|
|
25875
|
-
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
25876
|
-
else if (c === "," && depth === 0) {
|
|
25877
|
-
parts.push(s.slice(start, i));
|
|
25878
|
-
start = i + 1;
|
|
25879
|
-
}
|
|
25880
|
-
}
|
|
25881
|
-
parts.push(s.slice(start));
|
|
25882
|
-
return parts.map((p2) => p2.trim()).filter(Boolean);
|
|
25883
|
-
}
|
|
25884
|
-
function wholeValues(expr, depth = 0) {
|
|
25885
|
-
const e = expr.trim().replace(/^(?:await\s+|\.\.\.)/, "").trim();
|
|
25886
|
-
if (depth > 2) return [e];
|
|
25887
|
-
const json2 = e.match(/^JSON\s*\.\s*stringify\s*\(/);
|
|
25888
|
-
if (json2 && closingIndex(e, json2[0].length - 1) === e.length) return wholeValues(splitTopLevel(e.slice(json2[0].length, -1))[0] ?? "", depth + 1);
|
|
25889
|
-
if (e.startsWith("{") && closingIndex(e, 0) === e.length) {
|
|
25890
|
-
return splitTopLevel(e.slice(1, -1)).flatMap((entry) => {
|
|
25891
|
-
const colon = entry.match(/^[\w$\s]+:(?!:)/);
|
|
25892
|
-
return wholeValues(colon ? entry.slice(colon[0].length) : entry, depth + 1);
|
|
25893
|
-
});
|
|
25894
|
-
}
|
|
25895
|
-
return [e];
|
|
25896
|
-
}
|
|
25897
|
-
function analyzeLogArgs(masked, raw) {
|
|
25898
|
-
const hits = [];
|
|
25899
|
-
let m = masked;
|
|
25900
|
-
let r = raw;
|
|
25901
|
-
for (const call of masked.matchAll(REDACTED_CALL)) {
|
|
25902
|
-
const end = closingIndex(masked, call.index + call[0].length - 1);
|
|
25903
|
-
const to = end < 0 ? masked.length : end;
|
|
25904
|
-
const pad = " ".repeat(to - call.index);
|
|
25905
|
-
m = m.slice(0, call.index) + pad + m.slice(to);
|
|
25906
|
-
r = r.slice(0, call.index) + pad + r.slice(to);
|
|
25907
|
-
}
|
|
25908
|
-
for (const chain of m.matchAll(CHAIN)) {
|
|
25909
|
-
const start = chain.index;
|
|
25910
|
-
const end = start + chain[0].length;
|
|
25911
|
-
const before = m.slice(Math.max(0, start - 16), start);
|
|
25912
|
-
const after = m.slice(end).trimStart();
|
|
25913
|
-
const segments2 = chain[0].split(/\s*\??\.\s*/);
|
|
25914
|
-
const last = segments2[segments2.length - 1] ?? "";
|
|
25915
|
-
if (segments2.length === 1 && /[{,]\s*$/.test(before) && /^:(?!:)/.test(after)) continue;
|
|
25916
|
-
if (PRESENCE_BEFORE.test(before) || last === "length" || last === "size") continue;
|
|
25917
|
-
let value = segments2;
|
|
25918
|
-
if (after.startsWith("(")) {
|
|
25919
|
-
if (segments2.length === 1) {
|
|
25920
|
-
if ((last === "headers" || last === "cookies") && !/^\(\s*\)\s*\??\.\s*(?:get|has)\s*\(/.test(after))
|
|
25921
|
-
hits.push({ severity: "high", kind: last === "headers" ? "the request headers" : "the cookies" });
|
|
25922
|
-
continue;
|
|
25923
|
-
}
|
|
25924
|
-
const receiver = segments2[segments2.length - 2] ?? "";
|
|
25925
|
-
if (REQUEST.test(receiver) && /^(?:json|formData|text)$/.test(last)) {
|
|
25926
|
-
hits.push({ severity: "medium", kind: last === "formData" ? "the whole form submission" : "the whole request body" });
|
|
25927
|
-
continue;
|
|
25928
|
-
}
|
|
25929
|
-
if (!CONTENT_METHODS.has(last)) continue;
|
|
25930
|
-
value = segments2.slice(0, -1);
|
|
25931
|
-
}
|
|
25932
|
-
const name = value[value.length - 1] ?? "";
|
|
25933
|
-
const owner = value[value.length - 2] ?? "";
|
|
25934
|
-
if (value[0] === "process" && value[1] === "env") {
|
|
25935
|
-
if (value.length === 2 && !after.startsWith("[")) hits.push({ severity: "high", kind: "every environment variable (process.env)" });
|
|
25936
|
-
else if (value.length === 3 && SECRET_ENV.test(name) && !PUBLIC_ENV.test(name) && !/^\?(?![.?])/.test(after))
|
|
25937
|
-
hits.push({ severity: "high", kind: `the ${name} environment variable` });
|
|
25938
|
-
continue;
|
|
25939
|
-
}
|
|
25940
|
-
if (after.startsWith("[")) continue;
|
|
25941
|
-
if (name === "headers" && REQUEST.test(owner)) hits.push({ severity: "high", kind: "the request headers" });
|
|
25942
|
-
else if (name === "body" && REQUEST.test(owner)) hits.push({ severity: "medium", kind: "the whole request body" });
|
|
25943
|
-
else if (name === "formData" && value.length === 1) hits.push({ severity: "medium", kind: "the whole form submission" });
|
|
25944
|
-
else if (isSecretName(name)) hits.push({ severity: "high", kind: secretKind(name) });
|
|
25945
|
-
}
|
|
25946
|
-
for (const key of r.matchAll(/(?:\.\s*get\s*\(|\[)\s*(['"`])([\w-]{2,40})\1\s*[)\]]/g)) {
|
|
25947
|
-
const word = (key[2] ?? "").toLowerCase().replace(/[-_]/g, "");
|
|
25948
|
-
if (m[key.index] !== r[key.index] || !SECRET_STRING_KEY.test(word)) continue;
|
|
25949
|
-
hits.push({ severity: "high", kind: word === "authorization" ? "the authorization header" : secretKind(word) });
|
|
25950
|
-
}
|
|
25951
|
-
for (const arg of splitTopLevel(m)) {
|
|
25952
|
-
for (const v of wholeValues(arg)) {
|
|
25953
|
-
const person = v.match(/^(?:[\w$]+\s*\??\.\s*)*([\w$]+)$/)?.[1]?.match(PERSON);
|
|
25954
|
-
if (person) hits.push({ severity: "medium", kind: `a whole ${(person[1] ?? person[2] ?? "user").toLowerCase()} object` });
|
|
25955
|
-
}
|
|
25956
|
-
}
|
|
25957
|
-
return hits;
|
|
25958
|
-
}
|
|
25959
|
-
function findLogCalls(text, pattern = LOG_CALL) {
|
|
25960
|
-
const src = text.replace(/\r\n/g, "\n");
|
|
25961
|
-
const masked = maskCode(src);
|
|
25962
|
-
const line = lineFinder(src);
|
|
25963
|
-
const calls = [];
|
|
25964
|
-
for (const m of masked.matchAll(new RegExp(pattern.source, "g"))) {
|
|
25965
|
-
const open2 = m.index + m[0].length - 1;
|
|
25966
|
-
const close = closingIndex(masked, open2);
|
|
25967
|
-
calls.push({
|
|
25968
|
-
index: m.index,
|
|
25969
|
-
line: line(m.index),
|
|
25970
|
-
masked: close < 0 ? null : masked.slice(open2 + 1, close - 1),
|
|
25971
|
-
raw: close < 0 ? null : src.slice(open2 + 1, close - 1)
|
|
25972
|
-
});
|
|
25973
|
-
}
|
|
25974
|
-
return calls;
|
|
25975
|
-
}
|
|
25976
|
-
var KINDS2 = { high: "secret-logs", medium: "personal-logs" };
|
|
25977
|
-
var logLeaks = {
|
|
25978
|
-
id: "security.log-leaks",
|
|
25979
|
-
category: "security",
|
|
25980
|
-
title: "Logs don\u2019t leak secrets or personal data",
|
|
25981
|
-
obsV: 1,
|
|
25982
|
-
applies: (ctx) => appCodeFiles(ctx).length > 0,
|
|
25983
|
-
observe(ctx) {
|
|
25984
|
-
let logging = 0;
|
|
25985
|
-
const flagged = /* @__PURE__ */ new Set();
|
|
25986
|
-
const items = [];
|
|
25987
|
-
for (const file2 of appCodeFiles(ctx)) {
|
|
25988
|
-
const text = ctx.read(file2);
|
|
25989
|
-
if (!text) continue;
|
|
25990
|
-
const calls = findLogCalls(text);
|
|
25991
|
-
if (calls.length === 0) continue;
|
|
25992
|
-
logging++;
|
|
25993
|
-
const bySeverity = /* @__PURE__ */ new Map();
|
|
25994
|
-
for (const call of calls) {
|
|
25995
|
-
if (call.masked === null || call.raw === null) continue;
|
|
25996
|
-
for (const hit of analyzeLogArgs(call.masked, call.raw)) {
|
|
25997
|
-
const lines = bySeverity.get(hit.severity) ?? /* @__PURE__ */ new Map();
|
|
25998
|
-
bySeverity.set(hit.severity, lines);
|
|
25999
|
-
lines.set(call.line, (lines.get(call.line) ?? /* @__PURE__ */ new Set()).add(hit.kind));
|
|
26000
|
-
}
|
|
26001
|
-
}
|
|
26002
|
-
for (const severity2 of ["high", "medium"]) {
|
|
26003
|
-
const lines = bySeverity.get(severity2);
|
|
26004
|
-
if (!lines) continue;
|
|
26005
|
-
flagged.add(file2);
|
|
26006
|
-
const entries = [...lines.entries()].sort((a, b) => a[0] - b[0]);
|
|
26007
|
-
const shown = entries.slice(0, 20);
|
|
26008
|
-
const kinds = [...new Set(entries.flatMap(([, k]) => [...k]))];
|
|
26009
|
-
items.push({
|
|
26010
|
-
kind: KINDS2[severity2],
|
|
26011
|
-
facts: { more: entries.length - shown.length },
|
|
26012
|
-
key: `${file2}|${severity2}`,
|
|
26013
|
-
locations: shown.map(([line]) => ({ file: file2, line })),
|
|
26014
|
-
// What each line logs can name the app's own variables, so it stays on this computer.
|
|
26015
|
-
vars: { file: file2, lines: shown.map(([line, k]) => `Line ${line} logs ${[...k].join(" and ")}`).join("; ") },
|
|
26016
|
-
meta: { kinds, lines: entries.length }
|
|
26017
|
-
});
|
|
26018
|
-
}
|
|
26019
|
-
}
|
|
26020
|
-
return { facts: { logging, flagged: flagged.size }, items };
|
|
26021
|
-
}
|
|
26022
|
-
};
|
|
26023
|
-
|
|
26024
26301
|
// src/checks/security/ai-calls.ts
|
|
26025
26302
|
var AI_SDK = /^(?:openai|ai|groq-sdk|replicate|together-ai|cohere-ai|@google\/genai|@google\/generative-ai|@mistralai\/mistralai)$|^@(?:anthropic-ai|ai-sdk|langchain)\//;
|
|
26026
26303
|
function isAiSdk(name) {
|
|
@@ -26514,7 +26791,7 @@ function untestedFiles(ctx, areas2) {
|
|
|
26514
26791
|
const dirs = workspaceDirs2(ctx);
|
|
26515
26792
|
const texts = tests2.map((file2) => ({ file: file2, dir: dirOf(file2, dirs), text: ctx.read(file2) ?? "" }));
|
|
26516
26793
|
const e2e = texts.filter((t) => isE2eSpec(t.file, t.text)).map((t) => ({ ...t, paths: visitedPaths(t.text) }));
|
|
26517
|
-
const near = (
|
|
26794
|
+
const near = (list11, dir) => list11.filter((t) => t.dir === dir || t.dir === ".");
|
|
26518
26795
|
return areas2.filter((a) => {
|
|
26519
26796
|
if (reached.has(a.file)) return false;
|
|
26520
26797
|
const route = a.route;
|
|
@@ -26591,7 +26868,7 @@ var criticalTests = {
|
|
|
26591
26868
|
};
|
|
26592
26869
|
|
|
26593
26870
|
// src/checks/deps-registry.ts
|
|
26594
|
-
import { readFileSync as
|
|
26871
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
26595
26872
|
var REGISTRY = "https://registry.npmjs.org";
|
|
26596
26873
|
var fetchFromRegistry = async (name) => {
|
|
26597
26874
|
const encoded = name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
|
|
@@ -26609,7 +26886,7 @@ var CACHE_MS = 24 * 60 * 60 * 1e3;
|
|
|
26609
26886
|
var CONCURRENCY = 8;
|
|
26610
26887
|
function loadCache(root) {
|
|
26611
26888
|
try {
|
|
26612
|
-
const data = JSON.parse(
|
|
26889
|
+
const data = JSON.parse(readFileSync16(auraPath(root, CACHE_FILE), "utf8"));
|
|
26613
26890
|
return data && typeof data === "object" ? data : {};
|
|
26614
26891
|
} catch {
|
|
26615
26892
|
return {};
|
|
@@ -26670,7 +26947,7 @@ async function lookupPackages(root, wanted, fetcher, now) {
|
|
|
26670
26947
|
}
|
|
26671
26948
|
|
|
26672
26949
|
// src/checks/deps-runtimes.ts
|
|
26673
|
-
import
|
|
26950
|
+
import path26 from "path";
|
|
26674
26951
|
var NODE_LINES = {
|
|
26675
26952
|
12: { eol: "2022-04-30", lts: "2019-10-21" },
|
|
26676
26953
|
13: { eol: "2020-06-01" },
|
|
@@ -26778,7 +27055,7 @@ function nodePinSources(ctx, workspaces) {
|
|
|
26778
27055
|
if (typeof volta === "string") add(ws.file, "volta.node", volta, nodeMajorOf(volta));
|
|
26779
27056
|
}
|
|
26780
27057
|
for (const file2 of ctx.files) {
|
|
26781
|
-
const base2 =
|
|
27058
|
+
const base2 = path26.posix.basename(file2);
|
|
26782
27059
|
if (base2 === ".nvmrc" || base2 === ".node-version") {
|
|
26783
27060
|
const first = (ctx.read(file2) ?? "").replace(/\r\n/g, "\n").split("\n").map((l) => l.replace(/#.*/, "").trim()).find(Boolean);
|
|
26784
27061
|
if (first) add(file2, base2, first, nodeMajorOf(first));
|
|
@@ -27085,8 +27362,8 @@ function fakeSuccess(file2, v) {
|
|
|
27085
27362
|
const strong = STRONG_SUCCESS.flatMap(at);
|
|
27086
27363
|
const hits = [...at(WEAK_SUCCESS), ...strong];
|
|
27087
27364
|
if (hits.length === 0) return null;
|
|
27088
|
-
const
|
|
27089
|
-
if (PROVIDER_CALL.test(
|
|
27365
|
+
const code2 = contentLines(v).join("\n");
|
|
27366
|
+
if (PROVIDER_CALL.test(code2) || PAYMENT_FETCH.test(code2)) return null;
|
|
27090
27367
|
const rest = v.masked;
|
|
27091
27368
|
const delegated = imps.some(
|
|
27092
27369
|
(i) => !i.typeOnly && (PAYMENT_WORD.test(i.spec) || i.names.some((n) => PAYMENT_WORD.test(n))) && i.names.some((n) => countWord(rest, n) > 1)
|
|
@@ -27132,8 +27409,8 @@ function commentedCodeBlocks(v) {
|
|
|
27132
27409
|
return commentBlocks(v).filter((b) => {
|
|
27133
27410
|
const lines = b.lines.filter((l) => l.trim());
|
|
27134
27411
|
if (lines.length < COMMENTED_BLOCK_LINES || LICENSE.test(lines.join("\n"))) return false;
|
|
27135
|
-
const
|
|
27136
|
-
return
|
|
27412
|
+
const code2 = lines.filter((l) => CODE_LINE.some((re) => re.test(l))).length;
|
|
27413
|
+
return code2 >= COMMENTED_BLOCK_LINES && code2 / lines.length >= CODE_SHARE;
|
|
27137
27414
|
}).length;
|
|
27138
27415
|
}
|
|
27139
27416
|
var TS_DIRECTIVE = /^\s*(?:\{\s*)?(?:\/\/|\/\*)\s*@ts-(ignore|expect-error|nocheck)\b/;
|
|
@@ -27179,7 +27456,7 @@ var leftovers = {
|
|
|
27179
27456
|
observe(ctx) {
|
|
27180
27457
|
const files = leftoverFiles(ctx.files);
|
|
27181
27458
|
const views = /* @__PURE__ */ new Map();
|
|
27182
|
-
const
|
|
27459
|
+
const read3 = (f) => {
|
|
27183
27460
|
const cached2 = views.get(f);
|
|
27184
27461
|
if (cached2) return cached2;
|
|
27185
27462
|
const v = view(f, ctx.read(f) ?? "");
|
|
@@ -27192,7 +27469,7 @@ var leftovers = {
|
|
|
27192
27469
|
const mocks = {};
|
|
27193
27470
|
const commented = {};
|
|
27194
27471
|
for (const file2 of files) {
|
|
27195
|
-
const v =
|
|
27472
|
+
const v = read3(file2);
|
|
27196
27473
|
const kinds = placeholderKinds(v);
|
|
27197
27474
|
if (kinds.length) placeholders[file2] = kinds;
|
|
27198
27475
|
if (v.mdx) continue;
|
|
@@ -27205,7 +27482,7 @@ var leftovers = {
|
|
|
27205
27482
|
const blocks = commentedCodeBlocks(v);
|
|
27206
27483
|
if (blocks) commented[file2] = blocks;
|
|
27207
27484
|
}
|
|
27208
|
-
const escapes = typeCounts(ctx,
|
|
27485
|
+
const escapes = typeCounts(ctx, read3);
|
|
27209
27486
|
const items = [
|
|
27210
27487
|
...placeholderItem(placeholders),
|
|
27211
27488
|
...stubItem(stubs),
|
|
@@ -27279,11 +27556,11 @@ function commentedItem(byFile) {
|
|
|
27279
27556
|
}
|
|
27280
27557
|
];
|
|
27281
27558
|
}
|
|
27282
|
-
function typeCounts(ctx,
|
|
27559
|
+
function typeCounts(ctx, read3) {
|
|
27283
27560
|
const out = { nocheck: [], ignores: 0, anys: 0, ignoreFiles: [], anyFiles: [] };
|
|
27284
27561
|
const app2 = new Set(leftoverFiles(ctx.files));
|
|
27285
27562
|
for (const file2 of codeFiles(ctx.files)) {
|
|
27286
|
-
const e = typeEscapes(file2,
|
|
27563
|
+
const e = typeEscapes(file2, read3(file2));
|
|
27287
27564
|
if (e.nocheck && app2.has(file2)) out.nocheck.push(file2);
|
|
27288
27565
|
out.ignores += e.ignores;
|
|
27289
27566
|
out.anys += e.anys;
|
|
@@ -27320,7 +27597,7 @@ function typeItems(t) {
|
|
|
27320
27597
|
}
|
|
27321
27598
|
|
|
27322
27599
|
// src/connectors/snapshot.ts
|
|
27323
|
-
import { existsSync as
|
|
27600
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
27324
27601
|
var REFUSAL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27325
27602
|
function withRefusals(states, snap, now = Date.now()) {
|
|
27326
27603
|
return Object.fromEntries(
|
|
@@ -27333,7 +27610,7 @@ function withRefusals(states, snap, now = Date.now()) {
|
|
|
27333
27610
|
var FILE2 = "cache/connectors.json";
|
|
27334
27611
|
function loadSnapshots(root) {
|
|
27335
27612
|
try {
|
|
27336
|
-
return
|
|
27613
|
+
return existsSync20(auraPath(root, FILE2)) ? JSON.parse(readFileSync17(auraPath(root, FILE2), "utf8")) : {};
|
|
27337
27614
|
} catch {
|
|
27338
27615
|
return {};
|
|
27339
27616
|
}
|
|
@@ -27420,7 +27697,7 @@ var migrationsInSync = {
|
|
|
27420
27697
|
};
|
|
27421
27698
|
|
|
27422
27699
|
// src/checks/npm-publish-workflows.ts
|
|
27423
|
-
import
|
|
27700
|
+
import path27 from "path";
|
|
27424
27701
|
import YAML3 from "yaml";
|
|
27425
27702
|
var PUBLISH_CMD = /(?:^|[\s;&|(])(?:npx\s+)?(npm|pnpm|yarn\s+npm|yarn\s+workspaces\s+foreach(?:\s+-{1,2}[\w-]+)*\s+npm|changeset)\s+((?:-{1,2}[\w-]+(?:[= ](?!publish\b)[^\s-]\S*)?\s+)*)publish(?![\w:-])([^\n;&|]*)/;
|
|
27426
27703
|
function flagValue(text, flags) {
|
|
@@ -27442,14 +27719,14 @@ function parsePublishCommand(script, workingDir = ".") {
|
|
|
27442
27719
|
const named = flagValue(`${flags} ${rest}`, tool === "npm" ? ["--workspace", "-w"] : ["--filter", "-F", "--workspace"]).map(
|
|
27443
27720
|
(t) => t.replace(/^\{|\}$/g, "").replace(/\.\.\.$/, "")
|
|
27444
27721
|
);
|
|
27445
|
-
if (named.length) return { all: false, targets: named.map((t) => t.startsWith(".") ? normDir(
|
|
27722
|
+
if (named.length) return { all: false, targets: named.map((t) => t.startsWith(".") ? normDir(path27.posix.join(workingDir, t)) : t), provenance };
|
|
27446
27723
|
if (tool === "changeset" || /foreach/.test(tool) || /(?:^|\s)(?:-r|--recursive|--workspaces|-ws)(?:\s|$)/.test(` ${flags} ${rest} `)) {
|
|
27447
27724
|
return { all: true, targets: [], provenance };
|
|
27448
27725
|
}
|
|
27449
27726
|
const before = text.slice(0, m.index);
|
|
27450
27727
|
const cd = [...before.matchAll(/(?:^|[\s;&(])cd\s+["']?([^\s"';&|]+)/g)].pop()?.[1];
|
|
27451
27728
|
const positional = rest.trim().split(/\s+/).find((a) => a && !a.startsWith("-") && /^[./]|\//.test(a) && !a.endsWith(".tgz"));
|
|
27452
|
-
const dir =
|
|
27729
|
+
const dir = path27.posix.join(workingDir.replace(/\\/g, "/"), (cd ?? ".").replace(/\\/g, "/"), (positional ?? ".").replace(/\\/g, "/"));
|
|
27453
27730
|
return { all: false, targets: [normDir(dir)], provenance };
|
|
27454
27731
|
}
|
|
27455
27732
|
var asObj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
@@ -27589,10 +27866,10 @@ function npmPackageCheck(pack = npmPack) {
|
|
|
27589
27866
|
var npmPackage = npmPackageCheck();
|
|
27590
27867
|
|
|
27591
27868
|
// src/checks/readme-commands.ts
|
|
27592
|
-
import
|
|
27869
|
+
import path29 from "path";
|
|
27593
27870
|
|
|
27594
27871
|
// src/checks/readme-scripts.ts
|
|
27595
|
-
import
|
|
27872
|
+
import path28 from "path";
|
|
27596
27873
|
var SHELL_LANGS = /* @__PURE__ */ new Set(["", "sh", "bash", "zsh", "shell", "fish", "powershell", "ps1", "pwsh", "cmd", "bat", "batch", "text", "txt", "plaintext"]);
|
|
27597
27874
|
var PROMPT_LANGS = /* @__PURE__ */ new Set(["console", "shell-session", "shellsession", "sh-session", "terminal"]);
|
|
27598
27875
|
var PROMPT = /^\s*(?:\$|%|>|PS>|PS [^>]*>)\s+/;
|
|
@@ -27646,14 +27923,14 @@ function readmeCommands(markdown) {
|
|
|
27646
27923
|
for (const snippet of shellSnippets(markdown)) {
|
|
27647
27924
|
let cwd = ".";
|
|
27648
27925
|
for (const line of snippet.lines) {
|
|
27649
|
-
const
|
|
27650
|
-
if (!
|
|
27651
|
-
for (const segment of
|
|
27926
|
+
const code2 = line.replace(/(^|\s)#.*$/, "").trim();
|
|
27927
|
+
if (!code2) continue;
|
|
27928
|
+
for (const segment of code2.split(/&&|\|\||;|\|/)) {
|
|
27652
27929
|
const words2 = segment.trim().split(/\s+/).filter(Boolean).map(unquote2);
|
|
27653
27930
|
while (words2[0] && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words2[0]) || words2[0] === "sudo" || words2[0] === "corepack")) words2.shift();
|
|
27654
27931
|
if (words2[0] === "cd") {
|
|
27655
27932
|
const to = words2[1];
|
|
27656
|
-
cwd = !to || to === "~" || /^[/~$]/.test(to) ? "?" : cwd === "?" ? "?" :
|
|
27933
|
+
cwd = !to || to === "~" || /^[/~$]/.test(to) ? "?" : cwd === "?" ? "?" : path28.posix.normalize(path28.posix.join(cwd, to.replace(/\\/g, "/")));
|
|
27657
27934
|
continue;
|
|
27658
27935
|
}
|
|
27659
27936
|
if (snippet.inline && words2.length < 2) continue;
|
|
@@ -27723,11 +28000,11 @@ var LOCKFILES = [
|
|
|
27723
28000
|
["bun.lock", "bun"],
|
|
27724
28001
|
["bun.lockb", "bun"]
|
|
27725
28002
|
];
|
|
27726
|
-
var isReadme = (file2) => /^readme(?:\.md|\.mdx|\.markdown)?$/i.test(
|
|
28003
|
+
var isReadme = (file2) => /^readme(?:\.md|\.mdx|\.markdown)?$/i.test(path29.posix.basename(file2));
|
|
27727
28004
|
function readmes(ctx, workspaces) {
|
|
27728
28005
|
const byDir = new Map(workspaces.map((ws) => [ws.dir, ws]));
|
|
27729
28006
|
return ctx.files.flatMap((file2) => {
|
|
27730
|
-
const ws = isReadme(file2) ? byDir.get(
|
|
28007
|
+
const ws = isReadme(file2) ? byDir.get(path29.posix.dirname(file2)) : void 0;
|
|
27731
28008
|
return ws ? [{ file: file2, ws }] : [];
|
|
27732
28009
|
});
|
|
27733
28010
|
}
|
|
@@ -27742,7 +28019,7 @@ function selectPackages(selector, workspaces) {
|
|
|
27742
28019
|
const sel = selector.replace(/^\.\.\.\^?|\^?\.\.\.$/g, "").replace(/^\{(.*)\}$/, "$1");
|
|
27743
28020
|
if (!sel || sel.startsWith("!") || sel.includes("[")) return null;
|
|
27744
28021
|
const byFolder = sel.startsWith("./") || sel.startsWith(".\\") || selector.startsWith("{") || sel === ".";
|
|
27745
|
-
const target = byFolder ?
|
|
28022
|
+
const target = byFolder ? path29.posix.normalize(sel.replace(/\\/g, "/")).replace(/\/+$/, "") || "." : sel;
|
|
27746
28023
|
const re = globToRegex(target);
|
|
27747
28024
|
const matched = workspaces.filter((ws) => byFolder ? re.test(ws.dir) : re.test(ws.pkg.name ?? "") || re.test(ws.dir));
|
|
27748
28025
|
return matched.length ? matched : null;
|
|
@@ -27761,7 +28038,7 @@ function scriptExists(script, scope, home, workspaces) {
|
|
|
27761
28038
|
}
|
|
27762
28039
|
case "dir": {
|
|
27763
28040
|
if (scope.dir === "?") return null;
|
|
27764
|
-
const dir = scope.dir === "/" ? "." :
|
|
28041
|
+
const dir = scope.dir === "/" ? "." : path29.posix.normalize(path29.posix.join(home.dir, scope.dir.replace(/\\/g, "/"))).replace(/\/+$/, "") || ".";
|
|
27765
28042
|
const ws = workspaces.find((w) => w.dir === dir);
|
|
27766
28043
|
return ws ? hasScript(ws, script) : null;
|
|
27767
28044
|
}
|
|
@@ -27968,7 +28245,7 @@ var vercelEnv = {
|
|
|
27968
28245
|
|
|
27969
28246
|
// src/checks/performance.ts
|
|
27970
28247
|
import { statSync as statSync3 } from "fs";
|
|
27971
|
-
import
|
|
28248
|
+
import path30 from "path";
|
|
27972
28249
|
var BIG_ASSET_BYTES = 500 * 1024;
|
|
27973
28250
|
var MEDIA = /\.(?:png|jpe?g|gif|webp|avif|bmp|tiff?|svg|mp4|webm|mov|woff2?|ttf|otf)$/i;
|
|
27974
28251
|
var MAX_LISTED6 = 10;
|
|
@@ -27983,7 +28260,7 @@ function bigAssets(ctx, limit = BIG_ASSET_BYTES) {
|
|
|
27983
28260
|
const out = [];
|
|
27984
28261
|
for (const file2 of ctx.files.filter((f) => f.startsWith("public/") && MEDIA.test(f))) {
|
|
27985
28262
|
try {
|
|
27986
|
-
const size = statSync3(
|
|
28263
|
+
const size = statSync3(path30.join(ctx.root, ...file2.split("/"))).size;
|
|
27987
28264
|
if (size > limit) out.push({ file: file2, kb: Math.round(size / 1024) });
|
|
27988
28265
|
} catch {
|
|
27989
28266
|
}
|
|
@@ -28201,10 +28478,10 @@ var typecheck = {
|
|
|
28201
28478
|
applies: (ctx) => ctx.stack.typescript,
|
|
28202
28479
|
async observe(ctx) {
|
|
28203
28480
|
if (ctx.options.fast) return { facts: { outcome: "skipped-fast-mode" }, items: [] };
|
|
28204
|
-
const { existsSync:
|
|
28205
|
-
const
|
|
28206
|
-
const tsc =
|
|
28207
|
-
if (!
|
|
28481
|
+
const { existsSync: existsSync33 } = await import("fs");
|
|
28482
|
+
const path55 = await import("path");
|
|
28483
|
+
const tsc = path55.join(ctx.root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
|
|
28484
|
+
if (!existsSync33(tsc)) return { facts: { outcome: "not-installed" }, items: [] };
|
|
28208
28485
|
const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: ctx.root, timeoutMs: 18e4 });
|
|
28209
28486
|
if (r.timedOut) return { facts: { outcome: "timed-out" }, items: [] };
|
|
28210
28487
|
if (r.code === 0) return { facts: { outcome: "ran" }, items: [], engine: "tsc" };
|
|
@@ -28233,6 +28510,7 @@ var foundationChecks = [
|
|
|
28233
28510
|
vercelEnv,
|
|
28234
28511
|
migrationsInSync,
|
|
28235
28512
|
errorPages,
|
|
28513
|
+
spaErrors,
|
|
28236
28514
|
readmeCommandsCheck,
|
|
28237
28515
|
seo,
|
|
28238
28516
|
performance
|
|
@@ -28255,9 +28533,9 @@ function toolSettingsCheck(connector) {
|
|
|
28255
28533
|
observe(ctx) {
|
|
28256
28534
|
const snap = loadSnapshots(ctx.root)[connector.id];
|
|
28257
28535
|
if (!snap) return { facts: { settings: {} }, items: [{ kind: "no-reading", key: "no-reading", locations: [] }] };
|
|
28258
|
-
const
|
|
28259
|
-
const settings = Object.fromEntries(
|
|
28260
|
-
const off =
|
|
28536
|
+
const read3 = connector.settings.filter((s) => STATES.has(snap.settings[s.id] ?? ""));
|
|
28537
|
+
const settings = Object.fromEntries(read3.map((s) => [s.id, snap.settings[s.id]]));
|
|
28538
|
+
const off = read3.filter((s) => snap.settings[s.id] === "off");
|
|
28261
28539
|
return {
|
|
28262
28540
|
facts: { settings },
|
|
28263
28541
|
items: off.length ? [{ kind: "settings-off", key: `${connector.id}-settings`, locations: [], vars: { off: off.map((s) => s.label).join("; ") } }] : [],
|
|
@@ -28433,9 +28711,9 @@ var databaseBackups = {
|
|
|
28433
28711
|
|
|
28434
28712
|
// src/core/tools.ts
|
|
28435
28713
|
import { createHash as createHash2 } from "crypto";
|
|
28436
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
28714
|
+
import { chmodSync as chmodSync2, existsSync as existsSync21, mkdirSync as mkdirSync3, mkdtempSync, readdirSync as readdirSync5, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
28437
28715
|
import os3 from "os";
|
|
28438
|
-
import
|
|
28716
|
+
import path31 from "path";
|
|
28439
28717
|
var GITLEAKS_SHA = {
|
|
28440
28718
|
darwin_arm64: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
|
|
28441
28719
|
darwin_x64: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
|
|
@@ -28453,7 +28731,7 @@ var OSV_SHA = {
|
|
|
28453
28731
|
windows_arm64: "ca1379da0e408279ef2c85cde0846a903495738a91c7b065a208048437c1686c"
|
|
28454
28732
|
};
|
|
28455
28733
|
function tarCommand(platform = process.platform, systemRoot = process.env.SystemRoot) {
|
|
28456
|
-
return platform === "win32" ?
|
|
28734
|
+
return platform === "win32" ? path31.win32.join(systemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
|
|
28457
28735
|
}
|
|
28458
28736
|
var TOOLS = {
|
|
28459
28737
|
gitleaks: {
|
|
@@ -28493,8 +28771,8 @@ function ensureTool(name, noDownload = false) {
|
|
|
28493
28771
|
async function resolveTool(name, noDownload) {
|
|
28494
28772
|
const spec = TOOLS[name];
|
|
28495
28773
|
const exe = process.platform === "win32" ? `${name}.exe` : name;
|
|
28496
|
-
const cached2 =
|
|
28497
|
-
if (
|
|
28774
|
+
const cached2 = path31.join(auraBinDir(), `${name}-${spec.version}`, exe);
|
|
28775
|
+
if (existsSync21(cached2)) return { path: cached2, version: spec.version };
|
|
28498
28776
|
if (await commandExists(name)) {
|
|
28499
28777
|
const v = await run(name, ["version"]);
|
|
28500
28778
|
return { path: name, version: (v.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? "system").trim() };
|
|
@@ -28514,20 +28792,20 @@ async function download(name, spec, target) {
|
|
|
28514
28792
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
28515
28793
|
const digest = createHash2("sha256").update(buf).digest("hex");
|
|
28516
28794
|
if (digest !== asset.sha256) throw new Error(`Checksum mismatch for ${asset.file}`);
|
|
28517
|
-
const dir =
|
|
28795
|
+
const dir = path31.dirname(target);
|
|
28518
28796
|
mkdirSync3(dir, { recursive: true });
|
|
28519
|
-
const tmp = mkdtempSync(
|
|
28797
|
+
const tmp = mkdtempSync(path31.join(os3.tmpdir(), `aura-${name}-`));
|
|
28520
28798
|
try {
|
|
28521
28799
|
if (asset.archive === null) {
|
|
28522
28800
|
writeFileSync3(target, buf);
|
|
28523
28801
|
} else {
|
|
28524
|
-
const archivePath =
|
|
28802
|
+
const archivePath = path31.join(tmp, asset.file);
|
|
28525
28803
|
writeFileSync3(archivePath, buf);
|
|
28526
28804
|
const r = await run(tarCommand(), ["-xf", archivePath, "-C", tmp]);
|
|
28527
28805
|
if (r.code !== 0) return null;
|
|
28528
28806
|
const exe = readdirSync5(tmp).find((f) => f === name || f === `${name}.exe`);
|
|
28529
28807
|
if (!exe) return null;
|
|
28530
|
-
renameSync(
|
|
28808
|
+
renameSync(path31.join(tmp, exe), target);
|
|
28531
28809
|
}
|
|
28532
28810
|
chmodSync2(target, 493);
|
|
28533
28811
|
return { path: target, version: spec.version };
|
|
@@ -28646,6 +28924,7 @@ var WANTED = [
|
|
|
28646
28924
|
{ id: "x-frame-options", pattern: /X-Frame-Options|frame-ancestors/i, name: "X-Frame-Options" },
|
|
28647
28925
|
{ id: "referrer-policy", pattern: /Referrer-Policy/i, name: "Referrer-Policy" }
|
|
28648
28926
|
];
|
|
28927
|
+
var HEADER_SOURCES = /^next\.config\.[cm]?[jt]s$|^(?:src\/)?(?:middleware|proxy)\.[cm]?[jt]s$|^vercel\.json$|^netlify\.toml$|^(?:public\/)?_headers$|^firebase\.json$|^staticwebapp\.config\.json$/;
|
|
28649
28928
|
var headers = {
|
|
28650
28929
|
id: "security.headers",
|
|
28651
28930
|
scope: "project",
|
|
@@ -28653,14 +28932,14 @@ var headers = {
|
|
|
28653
28932
|
title: "Security headers set",
|
|
28654
28933
|
obsV: 1,
|
|
28655
28934
|
// Headers protect pages in a browser; an API with no pages has nothing for them to guard.
|
|
28656
|
-
applies: (ctx) => ctx.stack.nextjs !== null && ctx.project.traits.servesPages,
|
|
28935
|
+
applies: (ctx) => (ctx.stack.nextjs !== null || Boolean(ctx.stack.vite)) && ctx.project.traits.servesPages,
|
|
28657
28936
|
observe(ctx) {
|
|
28658
|
-
const sources = ctx.files.filter((f) =>
|
|
28937
|
+
const sources = ctx.files.filter((f) => HEADER_SOURCES.test(f));
|
|
28659
28938
|
const text = sources.map((f) => ctx.read(f) ?? "").join("\n");
|
|
28660
28939
|
const missing = WANTED.filter((h) => !h.pattern.test(text));
|
|
28661
28940
|
const facts = { checked: WANTED.length };
|
|
28662
28941
|
if (missing.length === 0) return { facts, items: [] };
|
|
28663
|
-
const configFile = ctx.files.find((f) => /^next\.config\./.test(f));
|
|
28942
|
+
const configFile = ctx.files.find((f) => /^next\.config\./.test(f)) ?? ctx.files.find((f) => /^(?:vercel\.json|netlify\.toml)$/.test(f));
|
|
28664
28943
|
return {
|
|
28665
28944
|
facts,
|
|
28666
28945
|
items: [
|
|
@@ -28785,7 +29064,7 @@ var rls = {
|
|
|
28785
29064
|
};
|
|
28786
29065
|
|
|
28787
29066
|
// src/checks/security/server-only.ts
|
|
28788
|
-
import
|
|
29067
|
+
import path32 from "path";
|
|
28789
29068
|
|
|
28790
29069
|
// src/checks/security/browser-code.ts
|
|
28791
29070
|
var posix = (p2) => p2.replace(/\\/g, "/");
|
|
@@ -28830,12 +29109,12 @@ function splitSql(sql) {
|
|
|
28830
29109
|
const src = sql.replace(/\r\n/g, "\n");
|
|
28831
29110
|
const out = [];
|
|
28832
29111
|
let text = "";
|
|
28833
|
-
let
|
|
29112
|
+
let code2 = "";
|
|
28834
29113
|
let bodies = [];
|
|
28835
29114
|
const push = () => {
|
|
28836
|
-
if (text.trim()) out.push({ text: text.trim(), code:
|
|
29115
|
+
if (text.trim()) out.push({ text: text.trim(), code: code2.trim(), bodies });
|
|
28837
29116
|
text = "";
|
|
28838
|
-
|
|
29117
|
+
code2 = "";
|
|
28839
29118
|
bodies = [];
|
|
28840
29119
|
};
|
|
28841
29120
|
const n = src.length;
|
|
@@ -28847,12 +29126,12 @@ function splitSql(sql) {
|
|
|
28847
29126
|
const end = src.indexOf("\n", i);
|
|
28848
29127
|
i = end < 0 ? n : end;
|
|
28849
29128
|
text += " ";
|
|
28850
|
-
|
|
29129
|
+
code2 += " ";
|
|
28851
29130
|
} else if (c === "/" && next === "*") {
|
|
28852
29131
|
const end = src.indexOf("*/", i + 2);
|
|
28853
29132
|
i = end < 0 ? n : end + 2;
|
|
28854
29133
|
text += " ";
|
|
28855
|
-
|
|
29134
|
+
code2 += " ";
|
|
28856
29135
|
} else if (c === "'" || c === '"') {
|
|
28857
29136
|
let j = i + 1;
|
|
28858
29137
|
while (j < n) {
|
|
@@ -28862,7 +29141,7 @@ function splitSql(sql) {
|
|
|
28862
29141
|
}
|
|
28863
29142
|
const seg = src.slice(i, j + 1);
|
|
28864
29143
|
text += seg;
|
|
28865
|
-
|
|
29144
|
+
code2 += seg;
|
|
28866
29145
|
i = j + 1;
|
|
28867
29146
|
} else if (c === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.test(src.slice(i, i + 64))) {
|
|
28868
29147
|
const tag = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(src.slice(i, i + 64))?.[0] ?? "$$";
|
|
@@ -28871,14 +29150,14 @@ function splitSql(sql) {
|
|
|
28871
29150
|
const body = src.slice(i + tag.length, end);
|
|
28872
29151
|
bodies.push(body);
|
|
28873
29152
|
text += tag + body + tag;
|
|
28874
|
-
|
|
29153
|
+
code2 += " $$ $$ ";
|
|
28875
29154
|
i = close < 0 ? n : end + tag.length;
|
|
28876
29155
|
} else if (c === ";") {
|
|
28877
29156
|
push();
|
|
28878
29157
|
i++;
|
|
28879
29158
|
} else {
|
|
28880
29159
|
text += c;
|
|
28881
|
-
|
|
29160
|
+
code2 += c;
|
|
28882
29161
|
i++;
|
|
28883
29162
|
}
|
|
28884
29163
|
}
|
|
@@ -28993,17 +29272,17 @@ function analyzeFunctions(migrations) {
|
|
|
28993
29272
|
const reDrop = /^drop\s+function\s+(?:if\s+exists\s+)?([\s\S]*?)(?:\s+(?:cascade|restrict))?$/i;
|
|
28994
29273
|
const reGrant = /^grant\s+([\s\S]+?)\s+on\s+(?:(?:function|routine)\s+([\s\S]+?)|all\s+(?:functions|routines)\s+in\s+schema\s+([\s\S]+?))\s+to\s+([\s\S]+?)(?:\s+with\s+grant\s+option)?$/i;
|
|
28995
29274
|
const reRevoke = /^revoke\s+(?:grant\s+option\s+for\s+)?([\s\S]+?)\s+on\s+(?:(?:function|routine)\s+([\s\S]+?)|all\s+(?:functions|routines)\s+in\s+schema\s+([\s\S]+?))\s+from\s+([\s\S]+?)(?:\s+(?:cascade|restrict))?$/i;
|
|
28996
|
-
const targets2 = (
|
|
29275
|
+
const targets2 = (list11, schemas2) => {
|
|
28997
29276
|
if (schemas2) {
|
|
28998
29277
|
const wanted = new Set(splitTopLevel2(schemas2).map((s) => unquote3(s).toLowerCase()));
|
|
28999
29278
|
return [...fns.values()].filter((f) => wanted.has(f.schema));
|
|
29000
29279
|
}
|
|
29001
|
-
return splitTopLevel2(
|
|
29280
|
+
return splitTopLevel2(list11 ?? "").flatMap((raw) => fns.get(keyOf(raw).key) ?? []);
|
|
29002
29281
|
};
|
|
29003
29282
|
for (const { file: file2, sql } of [...migrations].sort((a, b) => a.file.localeCompare(b.file))) {
|
|
29004
29283
|
for (const stmt of splitSql(sql)) {
|
|
29005
|
-
const { code } = stmt;
|
|
29006
|
-
const create = reCreate.exec(
|
|
29284
|
+
const { code: code2 } = stmt;
|
|
29285
|
+
const create = reCreate.exec(code2);
|
|
29007
29286
|
if (create) {
|
|
29008
29287
|
const { schema, name, key } = keyOf(create[2] ?? "");
|
|
29009
29288
|
const previous = create[1] ? fns.get(key) : void 0;
|
|
@@ -29011,15 +29290,15 @@ function analyzeFunctions(migrations) {
|
|
|
29011
29290
|
schema,
|
|
29012
29291
|
name,
|
|
29013
29292
|
file: file2,
|
|
29014
|
-
definer: /\bsecurity\s+definer\b/i.test(
|
|
29015
|
-
searchPath: SEARCH_PATH.test(
|
|
29293
|
+
definer: /\bsecurity\s+definer\b/i.test(code2),
|
|
29294
|
+
searchPath: SEARCH_PATH.test(code2),
|
|
29016
29295
|
writes: WRITES.test(bodyOf(stmt).replace(/--[^\n]*/g, "")),
|
|
29017
29296
|
// CREATE OR REPLACE keeps the grants; a fresh CREATE starts without any.
|
|
29018
29297
|
grants: previous ? previous.grants : /* @__PURE__ */ new Set()
|
|
29019
29298
|
});
|
|
29020
29299
|
continue;
|
|
29021
29300
|
}
|
|
29022
|
-
const alter = reAlter.exec(
|
|
29301
|
+
const alter = reAlter.exec(code2);
|
|
29023
29302
|
if (alter) {
|
|
29024
29303
|
const { key, schema } = keyOf(alter[1] ?? "");
|
|
29025
29304
|
const fn = fns.get(key);
|
|
@@ -29040,17 +29319,17 @@ function analyzeFunctions(migrations) {
|
|
|
29040
29319
|
}
|
|
29041
29320
|
continue;
|
|
29042
29321
|
}
|
|
29043
|
-
const drop = reDrop.exec(
|
|
29322
|
+
const drop = reDrop.exec(code2);
|
|
29044
29323
|
if (drop) {
|
|
29045
29324
|
for (const raw of splitTopLevel2(drop[1] ?? "")) fns.delete(keyOf(raw).key);
|
|
29046
29325
|
continue;
|
|
29047
29326
|
}
|
|
29048
|
-
const grant = reGrant.exec(
|
|
29327
|
+
const grant = reGrant.exec(code2);
|
|
29049
29328
|
if (grant && /\b(?:execute|all)\b/i.test(grant[1] ?? "")) {
|
|
29050
29329
|
for (const fn of targets2(grant[2], grant[3])) for (const r of roles(grant[4] ?? "")) fn.grants.add(r);
|
|
29051
29330
|
continue;
|
|
29052
29331
|
}
|
|
29053
|
-
const revoke = reRevoke.exec(
|
|
29332
|
+
const revoke = reRevoke.exec(code2);
|
|
29054
29333
|
if (revoke && /\b(?:execute|all)\b/i.test(revoke[1] ?? "")) {
|
|
29055
29334
|
for (const fn of targets2(revoke[2], revoke[3])) for (const r of roles(revoke[4] ?? "")) fn.grants.delete(r);
|
|
29056
29335
|
}
|
|
@@ -29081,20 +29360,20 @@ function adminSignal(raw) {
|
|
|
29081
29360
|
const name = m[1] ?? m[2] ?? "";
|
|
29082
29361
|
if (SERVICE_ENV.test(name) && masked[m.index] !== " ") return name;
|
|
29083
29362
|
}
|
|
29084
|
-
const
|
|
29085
|
-
const ident = /\b([A-Z0-9_]*(?:SERVICE_ROLE|SUPABASE_SECRET)[A-Z0-9_]*)\b/.exec(
|
|
29363
|
+
const code2 = withoutImports(masked);
|
|
29364
|
+
const ident = /\b([A-Z0-9_]*(?:SERVICE_ROLE|SUPABASE_SECRET)[A-Z0-9_]*)\b/.exec(code2);
|
|
29086
29365
|
if (ident?.[1]) return ident[1];
|
|
29087
29366
|
for (const m of text.matchAll(/service_role/g)) {
|
|
29088
|
-
if (
|
|
29367
|
+
if (code2[m.index] !== " " || masked[m.index] === " " && inStringLiteral(text, m.index)) return "service_role";
|
|
29089
29368
|
}
|
|
29090
|
-
const named = ADMIN_NAMES.exec(
|
|
29369
|
+
const named = ADMIN_NAMES.exec(code2)?.[1];
|
|
29091
29370
|
if (named) return named;
|
|
29092
|
-
if (/\.auth\s*\.\s*admin\s*\./.test(
|
|
29093
|
-
for (const m of
|
|
29371
|
+
if (/\.auth\s*\.\s*admin\s*\./.test(code2)) return "auth.admin";
|
|
29372
|
+
for (const m of code2.matchAll(/(?<![\w$.])createClient\s*\(/g)) {
|
|
29094
29373
|
const open2 = m.index + m[0].length - 1;
|
|
29095
|
-
const close = closingIndex(
|
|
29374
|
+
const close = closingIndex(code2, open2);
|
|
29096
29375
|
if (close < 0) continue;
|
|
29097
|
-
const second = splitTopLevel2(
|
|
29376
|
+
const second = splitTopLevel2(code2.slice(open2 + 1, close - 1))[1] ?? "";
|
|
29098
29377
|
if (/service|secret|admin/i.test(second)) return "createClient";
|
|
29099
29378
|
}
|
|
29100
29379
|
return null;
|
|
@@ -29165,7 +29444,7 @@ function serverReasons(raw) {
|
|
|
29165
29444
|
return [...reasons].sort();
|
|
29166
29445
|
}
|
|
29167
29446
|
function nestedPackages(files) {
|
|
29168
|
-
return files.filter((f) => f.endsWith("/package.json")).map((f) => `${
|
|
29447
|
+
return files.filter((f) => f.endsWith("/package.json")).map((f) => `${path32.posix.dirname(f)}/`);
|
|
29169
29448
|
}
|
|
29170
29449
|
function clientReachable(ctx, files) {
|
|
29171
29450
|
const importsOf = localImports(ctx);
|
|
@@ -29231,10 +29510,242 @@ var serverOnly = {
|
|
|
29231
29510
|
}
|
|
29232
29511
|
};
|
|
29233
29512
|
|
|
29234
|
-
// src/checks/security/
|
|
29513
|
+
// src/checks/security/browser-secrets.ts
|
|
29235
29514
|
var MAX_LIST4 = 20;
|
|
29515
|
+
var ENV_USE4 = /\b(?:import\.meta\.env|process\.env)(?:\.([A-Z][A-Z0-9_]*)|\[\s*['"]([A-Z][A-Z0-9_]*)['"]\s*\])/g;
|
|
29516
|
+
var PUBLIC_PREFIX4 = /^(?:VITE_|REACT_APP_|PUBLIC_)/;
|
|
29517
|
+
var ELECTRON_NODE = /^(?:src\/)?(?:electron|main|preload)\//;
|
|
29518
|
+
var SERVER_KEY = /(?:^|_)(?:RESEND|SENDGRID|TWILIO|POSTMARK|MAILGUN|MAILCHIMP|BREVO|SENDINBLUE|LOOPS|PLUNK)_|PASSWORD|PASSWD|PRIVATE_KEY|DATABASE_URL|DB_URL|POSTGRES|MONGO(?:DB)?_URI|REDIS_URL|(?:GITHUB|SLACK|DISCORD|TELEGRAM|NOTION|AIRTABLE|HUBSPOT|SHOPIFY_ADMIN)_(?:\w+_)?(?:TOKEN|KEY|SECRET)|STRIPE_(?:SECRET|RESTRICTED)|WEBHOOK_SECRET|AWS_SECRET|ADMIN_(?:KEY|TOKEN|PASSWORD)/;
|
|
29519
|
+
var SERVER_PACKAGE = /^(?:stripe|resend|twilio|@sendgrid\/mail|postmark|nodemailer|mailgun\.js|firebase-admin(?:\/.*)?|mongodb|mongoose|pg|postgres|mysql2|@octokit\/rest)$/;
|
|
29520
|
+
function isBrowserServerKey(name, supabase2) {
|
|
29521
|
+
if (!PUBLIC_PREFIX4.test(name) || isPublicAiKey(name)) return false;
|
|
29522
|
+
if (supabase2 && isPublicSecretName(name)) return false;
|
|
29523
|
+
const rest = name.replace(PUBLIC_PREFIX4, "");
|
|
29524
|
+
return SERVER_KEY.test(rest) || /SECRET|SERVICE_ROLE/.test(rest);
|
|
29525
|
+
}
|
|
29526
|
+
function browserKeysUsed(raw, supabase2) {
|
|
29527
|
+
const masked = maskCode(raw);
|
|
29528
|
+
const names = [...raw.matchAll(ENV_USE4)].filter((m) => masked[m.index] !== " ").map((m) => m[1] ?? m[2] ?? "").filter((n) => isBrowserServerKey(n, supabase2));
|
|
29529
|
+
return [...new Set(names)].sort();
|
|
29530
|
+
}
|
|
29531
|
+
function serverPackagesImported(raw) {
|
|
29532
|
+
const masked = maskCode(raw);
|
|
29533
|
+
const names = parseImports(raw).filter((ref) => !ref.typeOnly && masked[ref.start - 1] !== " " && SERVER_PACKAGE.test(ref.specifier)).map((ref) => ref.specifier);
|
|
29534
|
+
return [...new Set(names)].sort();
|
|
29535
|
+
}
|
|
29536
|
+
function browserKeysListed(text, supabase2) {
|
|
29537
|
+
const names = /* @__PURE__ */ new Set();
|
|
29538
|
+
for (const line of text.replace(/\r\n/g, "\n").split("\n")) {
|
|
29539
|
+
const name = /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/.exec(line)?.[1];
|
|
29540
|
+
if (name && isBrowserServerKey(name, supabase2)) names.add(name);
|
|
29541
|
+
}
|
|
29542
|
+
return [...names].sort();
|
|
29543
|
+
}
|
|
29544
|
+
var list4 = (names) => names.length > MAX_LIST4 ? `${names.slice(0, MAX_LIST4).join(", ")} and ${names.length - MAX_LIST4} more` : names.join(", ");
|
|
29545
|
+
var code = (names) => list4(names.map((n) => `\`${n}\``));
|
|
29546
|
+
var browserSecrets = {
|
|
29547
|
+
id: "security.browser-secrets",
|
|
29548
|
+
scope: "project",
|
|
29549
|
+
category: "security",
|
|
29550
|
+
title: "Server keys stay out of the browser",
|
|
29551
|
+
obsV: 1,
|
|
29552
|
+
applies: (ctx) => Boolean(ctx.stack.vite),
|
|
29553
|
+
observe(ctx) {
|
|
29554
|
+
const supabase2 = ctx.stack.supabase !== null;
|
|
29555
|
+
const electron = Boolean(ctx.pkg?.dependencies?.electron || ctx.pkg?.devDependencies?.electron);
|
|
29556
|
+
const items = [];
|
|
29557
|
+
let files = 0;
|
|
29558
|
+
for (const file2 of appCodeFiles(ctx)) {
|
|
29559
|
+
const text = ctx.read(file2);
|
|
29560
|
+
if (!text || !isBrowserFile(file2, text, true) || electron && ELECTRON_NODE.test(file2)) continue;
|
|
29561
|
+
files++;
|
|
29562
|
+
const names = browserKeysUsed(text, supabase2);
|
|
29563
|
+
if (names.length)
|
|
29564
|
+
items.push({
|
|
29565
|
+
kind: "public-env",
|
|
29566
|
+
facts: { names: names.length },
|
|
29567
|
+
key: `${file2}:env`,
|
|
29568
|
+
locations: [{ file: file2 }],
|
|
29569
|
+
vars: { file: file2, names: code(names) },
|
|
29570
|
+
meta: { names }
|
|
29571
|
+
});
|
|
29572
|
+
const packages = serverPackagesImported(text);
|
|
29573
|
+
if (packages.length)
|
|
29574
|
+
items.push({
|
|
29575
|
+
kind: "server-package",
|
|
29576
|
+
facts: { packages: packages.length },
|
|
29577
|
+
key: `${file2}:package`,
|
|
29578
|
+
locations: [{ file: file2 }],
|
|
29579
|
+
vars: { file: file2, packages: code(packages) },
|
|
29580
|
+
meta: { packages }
|
|
29581
|
+
});
|
|
29582
|
+
}
|
|
29583
|
+
for (const file2 of ctx.files.filter((f) => ENV_EXAMPLE.test(f))) {
|
|
29584
|
+
const names = browserKeysListed(ctx.read(file2) ?? "", supabase2);
|
|
29585
|
+
if (names.length)
|
|
29586
|
+
items.push({
|
|
29587
|
+
kind: "env-example",
|
|
29588
|
+
facts: { names: names.length },
|
|
29589
|
+
key: `${file2}:example`,
|
|
29590
|
+
locations: [{ file: file2 }],
|
|
29591
|
+
vars: { file: file2, names: code(names) },
|
|
29592
|
+
meta: { names }
|
|
29593
|
+
});
|
|
29594
|
+
}
|
|
29595
|
+
return { facts: { files }, items };
|
|
29596
|
+
}
|
|
29597
|
+
};
|
|
29598
|
+
|
|
29599
|
+
// src/checks/security/edge-functions.ts
|
|
29600
|
+
import path33 from "path";
|
|
29601
|
+
|
|
29602
|
+
// src/checks/hardcoded-urls.ts
|
|
29603
|
+
var STRING_OR_COMMENT = /("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)|\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
29604
|
+
function stripComments2(text) {
|
|
29605
|
+
return text.replace(/\r\n/g, "\n").replace(STRING_OR_COMMENT, (m, str) => str ?? m.replace(/[^\n]/g, " "));
|
|
29606
|
+
}
|
|
29607
|
+
var URL_RE = /\bhttps?:\/\/([a-z0-9-]+(?:\.[a-z0-9-]+)*)(?::\d+)?[^\s'"`)<>]*/gi;
|
|
29608
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
|
|
29609
|
+
var IGNORED_HOSTS = [
|
|
29610
|
+
"schema.org",
|
|
29611
|
+
"json-schema.org",
|
|
29612
|
+
"w3.org",
|
|
29613
|
+
"github.com",
|
|
29614
|
+
"githubusercontent.com",
|
|
29615
|
+
"npmjs.org",
|
|
29616
|
+
"npmjs.com",
|
|
29617
|
+
"fonts.googleapis.com",
|
|
29618
|
+
"fonts.gstatic.com",
|
|
29619
|
+
// Package hosts Deno code (Supabase edge functions) imports from.
|
|
29620
|
+
"esm.sh",
|
|
29621
|
+
"deno.land",
|
|
29622
|
+
"jsr.io",
|
|
29623
|
+
"cdn.jsdelivr.net",
|
|
29624
|
+
"unpkg.com",
|
|
29625
|
+
"cdn.skypack.dev",
|
|
29626
|
+
"example.com",
|
|
29627
|
+
"example.org",
|
|
29628
|
+
"example.net"
|
|
29629
|
+
];
|
|
29630
|
+
var RESERVED_TLD = /\.(?:example|test|invalid|local|localhost)$/;
|
|
29631
|
+
function isIgnoredHost(host) {
|
|
29632
|
+
return RESERVED_TLD.test(host) || IGNORED_HOSTS.some((h) => host === h || host.endsWith(`.${h}`));
|
|
29633
|
+
}
|
|
29634
|
+
function inCdnTag(text, index) {
|
|
29635
|
+
const open2 = text.lastIndexOf("<", index);
|
|
29636
|
+
return open2 >= 0 && /^<(?:script|Script|link)\b[^>]*$/.test(text.slice(open2, index));
|
|
29637
|
+
}
|
|
29638
|
+
function findUrls(text) {
|
|
29639
|
+
const code2 = stripComments2(text);
|
|
29640
|
+
const hits = [];
|
|
29641
|
+
for (const m of code2.matchAll(URL_RE)) {
|
|
29642
|
+
const host = (m[1] ?? "").toLowerCase().replace(/^www\./, "");
|
|
29643
|
+
if (!host || inCdnTag(code2, m.index)) continue;
|
|
29644
|
+
hits.push({ host, url: m[0], index: m.index });
|
|
29645
|
+
}
|
|
29646
|
+
return hits;
|
|
29647
|
+
}
|
|
29648
|
+
var DEV_GUARD = /NODE_ENV\s*[!=]==?\s*['"`](?:development|production|test)['"`]|['"`](?:development|production|test)['"`]\s*[!=]==?\s*process\.env\.NODE_ENV|import\.meta\.env\.(?:DEV|PROD)\b|__DEV__|\bisDev(?:elopment)?\b|\bisProd(?:uction)?\b/;
|
|
29649
|
+
var ENV_FALLBACK = /(?:process\.env|import\.meta\.env)(?:\.\w+|\[['"]\w+['"]\])\s*(?:\?\?|\|\|)\s*$/;
|
|
29650
|
+
function isDevOnlyUse(code2, index) {
|
|
29651
|
+
const lineStart = code2.lastIndexOf("\n", index - 1) + 1;
|
|
29652
|
+
const before = code2.slice(lineStart, index).replace(/['"`]$/, "");
|
|
29653
|
+
if (ENV_FALLBACK.test(before.trimEnd())) return true;
|
|
29654
|
+
let start = lineStart;
|
|
29655
|
+
for (let i = 0; i < 6 && start > 0; i++) start = code2.lastIndexOf("\n", start - 2) + 1;
|
|
29656
|
+
const lineEnd = code2.indexOf("\n", index);
|
|
29657
|
+
return DEV_GUARD.test(code2.slice(start, lineEnd === -1 ? void 0 : lineEnd));
|
|
29658
|
+
}
|
|
29659
|
+
var RUNS_LOCAL_SERVER = /\bcreateServer\s*\(|\.listen\s*\(/;
|
|
29660
|
+
|
|
29661
|
+
// src/checks/security/edge-functions.ts
|
|
29662
|
+
var MAX_LIST5 = 20;
|
|
29663
|
+
var FUNCTION_ENTRY = /^(?:(.+)\/)?supabase\/functions\/([^/_.][^/]*)\/index\.[cm]?[jt]sx?$/;
|
|
29664
|
+
var IGNORE2 = /aura-ignore/;
|
|
29665
|
+
var USER_CHECK = /\.auth\s*\.\s*(?:getUser|getClaims)\s*\(|\bjwtVerify\s*\(|\bverifyJwt\s*\(/;
|
|
29666
|
+
var SIGNATURE_CHECK = /\.constructEvent(?:Async)?\s*\(|\bverifyWebhook\s*\(|\bnew\s+Webhook\s*\(|\.verify\s*\(\s*(?:payload|body|rawBody|text)|createHmac\s*\(|timingSafeEqual\s*\(|crypto\.subtle\.verify\s*\(|crypto\.subtle\.sign\s*\(\s*['"`]HMAC|\bverify\w*Signature\s*\(/;
|
|
29667
|
+
var HEADER_COMPARE = /(?:headers\.get\s*\([^)]*\)|authHeader|authorization|token)[^\n;]{0,160}(?:!==|===|!=|==)|(?:!==|===|!=|==)[^\n;]{0,160}(?:headers\.get\s*\(|authHeader|authorization)/i;
|
|
29668
|
+
var SHARED_SECRET2 = /Deno\.env\.get\s*\(\s*['"`][A-Z0-9_]*(?:SECRET|CRON|TOKEN|WEBHOOK)[A-Z0-9_]*['"`]\s*\)/;
|
|
29669
|
+
var ADMIN = /SUPABASE_SERVICE_ROLE_KEY|SERVICE_ROLE_KEY|SUPABASE_SECRET_KEY|['"`]service_role['"`]/;
|
|
29670
|
+
var ENV_GET = /Deno\.env\.get\s*\(\s*['"`]([A-Z][A-Z0-9_]*)['"`]\s*\)/g;
|
|
29671
|
+
var KEY_NAME = /KEY|SECRET|TOKEN|PASSWORD/;
|
|
29672
|
+
var SUPABASE_OWN = /^SUPABASE_/;
|
|
29673
|
+
function checksCaller(raw) {
|
|
29674
|
+
const code2 = stripComments2(raw);
|
|
29675
|
+
return USER_CHECK.test(code2) || SIGNATURE_CHECK.test(code2) || SHARED_SECRET2.test(code2) && HEADER_COMPARE.test(code2);
|
|
29676
|
+
}
|
|
29677
|
+
function keysRead(raw) {
|
|
29678
|
+
const code2 = stripComments2(raw);
|
|
29679
|
+
const names = [...code2.matchAll(ENV_GET)].map((m) => m[1] ?? "").filter((n) => KEY_NAME.test(n) && !SUPABASE_OWN.test(n));
|
|
29680
|
+
return [...new Set(names)].sort();
|
|
29681
|
+
}
|
|
29682
|
+
function jwtOffFunctions(toml) {
|
|
29683
|
+
const off = /* @__PURE__ */ new Set();
|
|
29684
|
+
let current2 = null;
|
|
29685
|
+
for (const line of toml.replace(/\r\n/g, "\n").split("\n")) {
|
|
29686
|
+
const section = /^\s*\[functions\.(?:"([^"]+)"|([\w-]+))\]\s*$/.exec(line);
|
|
29687
|
+
if (section) current2 = section[1] ?? section[2] ?? null;
|
|
29688
|
+
else if (/^\s*\[/.test(line)) current2 = null;
|
|
29689
|
+
else if (current2 && /^\s*verify_jwt\s*=\s*false\b/.test(line)) off.add(current2);
|
|
29690
|
+
}
|
|
29691
|
+
return off;
|
|
29692
|
+
}
|
|
29693
|
+
function sharedHelpers(ctx, file2, raw) {
|
|
29694
|
+
return parseImports(raw).filter((ref) => ref.specifier.startsWith(".") && ref.specifier.includes("_shared")).map((ref) => path33.posix.normalize(path33.posix.join(path33.posix.dirname(file2), ref.specifier))).map((target) => ctx.read(target) ?? ctx.read(`${target}.ts`) ?? ctx.read(`${target}/index.ts`) ?? "");
|
|
29695
|
+
}
|
|
29696
|
+
var list5 = (names) => names.length > MAX_LIST5 ? `${names.slice(0, MAX_LIST5).join(", ")} and ${names.length - MAX_LIST5} more` : names.join(", ");
|
|
29697
|
+
var edgeFunctions = {
|
|
29698
|
+
id: "security.edge-functions",
|
|
29699
|
+
category: "security",
|
|
29700
|
+
title: "Edge functions check who is calling",
|
|
29701
|
+
obsV: 1,
|
|
29702
|
+
applies: (ctx) => ctx.files.some((f) => FUNCTION_ENTRY.test(f)),
|
|
29703
|
+
observe(ctx) {
|
|
29704
|
+
const items = [];
|
|
29705
|
+
let functions2 = 0;
|
|
29706
|
+
let guarded2 = 0;
|
|
29707
|
+
let ignored = 0;
|
|
29708
|
+
const configs = /* @__PURE__ */ new Map();
|
|
29709
|
+
for (const file2 of ctx.files.filter((f) => FUNCTION_ENTRY.test(f)).sort()) {
|
|
29710
|
+
const [, prefix = "", name = ""] = FUNCTION_ENTRY.exec(file2) ?? [];
|
|
29711
|
+
const raw = ctx.read(file2);
|
|
29712
|
+
if (!raw) continue;
|
|
29713
|
+
functions2++;
|
|
29714
|
+
if (IGNORE2.test(raw)) {
|
|
29715
|
+
ignored++;
|
|
29716
|
+
continue;
|
|
29717
|
+
}
|
|
29718
|
+
if (checksCaller(raw) || sharedHelpers(ctx, file2, raw).some(checksCaller)) {
|
|
29719
|
+
guarded2++;
|
|
29720
|
+
continue;
|
|
29721
|
+
}
|
|
29722
|
+
const config2 = `${prefix ? `${prefix}/` : ""}supabase/config.toml`;
|
|
29723
|
+
if (!configs.has(config2)) configs.set(config2, jwtOffFunctions(ctx.read(config2) ?? ""));
|
|
29724
|
+
const jwtOff = configs.get(config2)?.has(name) ?? false;
|
|
29725
|
+
const vars = { function: name, file: file2 };
|
|
29726
|
+
if (ADMIN.test(stripComments2(raw))) {
|
|
29727
|
+
items.push({ kind: "open-admin", facts: { jwtOff }, key: `${file2}:admin`, locations: [{ file: file2 }], vars, meta: { function: name, jwtOff } });
|
|
29728
|
+
continue;
|
|
29729
|
+
}
|
|
29730
|
+
const keys = keysRead(raw);
|
|
29731
|
+
if (keys.length)
|
|
29732
|
+
items.push({
|
|
29733
|
+
kind: "open-keys",
|
|
29734
|
+
facts: { jwtOff, keys: keys.length },
|
|
29735
|
+
key: `${file2}:keys`,
|
|
29736
|
+
locations: [{ file: file2 }],
|
|
29737
|
+
vars: { ...vars, keys: list5(keys.map((k) => `\`${k}\``)) },
|
|
29738
|
+
meta: { function: name, jwtOff, keys }
|
|
29739
|
+
});
|
|
29740
|
+
}
|
|
29741
|
+
return { facts: { functions: functions2, guarded: guarded2, ignored }, items };
|
|
29742
|
+
}
|
|
29743
|
+
};
|
|
29744
|
+
|
|
29745
|
+
// src/checks/security/supabase-code.ts
|
|
29746
|
+
var MAX_LIST6 = 20;
|
|
29236
29747
|
var NOT_AURA2 = /(?:^|\/)(?:\.aura|\.claude|\.agents|\.cursor|\.codex|\.pi|node_modules)\//;
|
|
29237
|
-
var
|
|
29748
|
+
var list6 = (items) => items.length > MAX_LIST6 ? `${items.slice(0, MAX_LIST6).join(", ")} and ${items.length - MAX_LIST6} more` : items.join(", ");
|
|
29238
29749
|
function browserReason(file2, text) {
|
|
29239
29750
|
if (isClientFile(text)) return "use-client";
|
|
29240
29751
|
if (/(?:^|\/)(?:components|hooks)\//.test(file2)) return "browser-folder";
|
|
@@ -29308,8 +29819,8 @@ function bucketItems(buckets2) {
|
|
|
29308
29819
|
kind,
|
|
29309
29820
|
facts: { buckets: names.length },
|
|
29310
29821
|
key: kind === "asset-buckets" ? "public-buckets|assets" : "public-buckets",
|
|
29311
|
-
locations: files.slice(0,
|
|
29312
|
-
vars: { buckets:
|
|
29822
|
+
locations: files.slice(0, MAX_LIST6).map((file2) => ({ file: file2 })),
|
|
29823
|
+
vars: { buckets: list6(names.map((n) => `"${n}"`)) },
|
|
29313
29824
|
meta: { buckets: names, files }
|
|
29314
29825
|
});
|
|
29315
29826
|
}
|
|
@@ -29325,8 +29836,8 @@ function functionItems(fns) {
|
|
|
29325
29836
|
kind,
|
|
29326
29837
|
facts: { functions: group.length },
|
|
29327
29838
|
key: kind,
|
|
29328
|
-
locations: [...new Set(group.map((f) => f.file))].slice(0,
|
|
29329
|
-
vars: { functions:
|
|
29839
|
+
locations: [...new Set(group.map((f) => f.file))].slice(0, MAX_LIST6).map((file2) => ({ file: file2 })),
|
|
29840
|
+
vars: { functions: list6(group.map(label2)) },
|
|
29330
29841
|
meta: { functions: group.map(label2) }
|
|
29331
29842
|
}));
|
|
29332
29843
|
}
|
|
@@ -29365,9 +29876,9 @@ var supabaseCode = {
|
|
|
29365
29876
|
};
|
|
29366
29877
|
|
|
29367
29878
|
// src/checks/security/secrets.ts
|
|
29368
|
-
import { existsSync as
|
|
29879
|
+
import { existsSync as existsSync22, mkdtempSync as mkdtempSync2, readFileSync as readFileSync18, rmSync as rmSync3 } from "fs";
|
|
29369
29880
|
import os4 from "os";
|
|
29370
|
-
import
|
|
29881
|
+
import path34 from "path";
|
|
29371
29882
|
|
|
29372
29883
|
// src/checks/types.ts
|
|
29373
29884
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -29404,7 +29915,7 @@ function scanFilesForSecrets(ctx) {
|
|
|
29404
29915
|
if (/(?:^|\/)\.env(?:\.[^/]+)?$/.test(file2) && !/\.(?:example|sample|template)$/.test(file2)) continue;
|
|
29405
29916
|
const text = ctx.read(file2);
|
|
29406
29917
|
if (!text) continue;
|
|
29407
|
-
for (const m of
|
|
29918
|
+
for (const m of findLeakedSecrets(text)) {
|
|
29408
29919
|
hits.push({
|
|
29409
29920
|
file: file2,
|
|
29410
29921
|
line: lineOf(text, m.index),
|
|
@@ -29422,15 +29933,15 @@ async function gitleaksHistory(ctx) {
|
|
|
29422
29933
|
if (!ctx.tracked) return null;
|
|
29423
29934
|
const bin = await ensureTool("gitleaks", ctx.options.noDownload);
|
|
29424
29935
|
if (!bin) return null;
|
|
29425
|
-
const dir = mkdtempSync2(
|
|
29426
|
-
const report =
|
|
29936
|
+
const dir = mkdtempSync2(path34.join(os4.tmpdir(), "aura-gitleaks-"));
|
|
29937
|
+
const report = path34.join(dir, "report.json");
|
|
29427
29938
|
try {
|
|
29428
29939
|
const r = await run(bin.path, ["git", "--no-banner", "--redact", "--report-format", "json", "--report-path", report, "--exit-code", "0", "."], {
|
|
29429
29940
|
cwd: ctx.root,
|
|
29430
29941
|
timeoutMs: 18e4
|
|
29431
29942
|
});
|
|
29432
|
-
if (r.code !== 0 || !
|
|
29433
|
-
return { hits: JSON.parse(
|
|
29943
|
+
if (r.code !== 0 || !existsSync22(report)) return null;
|
|
29944
|
+
return { hits: JSON.parse(readFileSync18(report, "utf8")), engine: `gitleaks ${bin.version}` };
|
|
29434
29945
|
} catch {
|
|
29435
29946
|
return null;
|
|
29436
29947
|
} finally {
|
|
@@ -29504,7 +30015,7 @@ var envFiles = {
|
|
|
29504
30015
|
return {
|
|
29505
30016
|
facts: {},
|
|
29506
30017
|
items: committed.map((file2) => {
|
|
29507
|
-
const leaked =
|
|
30018
|
+
const leaked = findLeakedSecrets(ctx.read(file2) ?? "").filter((m) => m.confidence === "high");
|
|
29508
30019
|
return {
|
|
29509
30020
|
kind: "env-file",
|
|
29510
30021
|
facts: { secrets: leaked.length },
|
|
@@ -29562,7 +30073,7 @@ function workflowProblems(doc) {
|
|
|
29562
30073
|
}
|
|
29563
30074
|
return { unpinned, noPermissions, prTargetCheckout, injections: [...injections].sort() };
|
|
29564
30075
|
}
|
|
29565
|
-
var
|
|
30076
|
+
var list7 = (items, max = 20) => `${items.slice(0, max).join(", ")}${items.length > max ? ` and ${items.length - max} more` : ""}`;
|
|
29566
30077
|
var workflows = {
|
|
29567
30078
|
id: "security.workflows",
|
|
29568
30079
|
category: "security",
|
|
@@ -29592,7 +30103,7 @@ var workflows = {
|
|
|
29592
30103
|
items.push({
|
|
29593
30104
|
kind: "script-injection",
|
|
29594
30105
|
key: `${file2}:script-injection`,
|
|
29595
|
-
vars: { file: file2, expressions:
|
|
30106
|
+
vars: { file: file2, expressions: list7(p2.injections) },
|
|
29596
30107
|
meta: { expressions: p2.injections },
|
|
29597
30108
|
...at
|
|
29598
30109
|
});
|
|
@@ -29602,7 +30113,7 @@ var workflows = {
|
|
|
29602
30113
|
key: `${file2}:unpinned-actions`,
|
|
29603
30114
|
...at,
|
|
29604
30115
|
facts: { workflow: parsed, actions: p2.unpinned.length },
|
|
29605
|
-
vars: { file: file2, actions:
|
|
30116
|
+
vars: { file: file2, actions: list7(p2.unpinned) },
|
|
29606
30117
|
meta: { actions: p2.unpinned }
|
|
29607
30118
|
});
|
|
29608
30119
|
if (p2.noPermissions) items.push({ kind: "no-permissions", key: `${file2}:no-permissions`, vars: { file: file2 }, ...at });
|
|
@@ -29611,14 +30122,14 @@ var workflows = {
|
|
|
29611
30122
|
return {
|
|
29612
30123
|
facts: { parsed, unreadable: unreadable.length },
|
|
29613
30124
|
items,
|
|
29614
|
-
...unreadable.length ? { vars: { unreadable:
|
|
30125
|
+
...unreadable.length ? { vars: { unreadable: list7(unreadable, 5) } } : {}
|
|
29615
30126
|
};
|
|
29616
30127
|
}
|
|
29617
30128
|
};
|
|
29618
30129
|
|
|
29619
30130
|
// src/checks/security/auth-signals.ts
|
|
29620
|
-
var inCode = (re) => (
|
|
29621
|
-
var imports2 = (from, call) => (
|
|
30131
|
+
var inCode = (re) => (code2) => re.test(code2);
|
|
30132
|
+
var imports2 = (from, call) => (code2, raw) => from.test(raw) && call.test(code2);
|
|
29622
30133
|
var CUSTOM = [
|
|
29623
30134
|
["passwords", inCode(/\b(?:bcrypt(?:js)?|argon2)\.(?:compare|verify|hash)(?:Sync)?\s*\(/)],
|
|
29624
30135
|
["passwords", imports2(/from\s+['"](?:bcrypt|bcryptjs|argon2|@node-rs\/(?:argon2|bcrypt))['"]/, /(?<![\w$.])(?:compare|verify|hash)(?:Sync)?\s*\(/)],
|
|
@@ -29684,18 +30195,18 @@ function pathReason(file2) {
|
|
|
29684
30195
|
}
|
|
29685
30196
|
function customReasons(text) {
|
|
29686
30197
|
const raw = text.replace(/\r\n/g, "\n");
|
|
29687
|
-
const
|
|
30198
|
+
const code2 = maskCode(raw);
|
|
29688
30199
|
const out = /* @__PURE__ */ new Set();
|
|
29689
|
-
for (const [reason, test] of CUSTOM) if (!out.has(reason) && test(
|
|
30200
|
+
for (const [reason, test] of CUSTOM) if (!out.has(reason) && test(code2, raw)) out.add(reason);
|
|
29690
30201
|
return [...out];
|
|
29691
30202
|
}
|
|
29692
30203
|
function usesProviderAuth(text) {
|
|
29693
|
-
const
|
|
29694
|
-
return PROVIDER_AUTH.some((re) => re.test(
|
|
30204
|
+
const code2 = maskCode(text.replace(/\r\n/g, "\n"));
|
|
30205
|
+
return PROVIDER_AUTH.some((re) => re.test(code2));
|
|
29695
30206
|
}
|
|
29696
30207
|
function hasCredentialsSignIn(text) {
|
|
29697
|
-
const
|
|
29698
|
-
return CREDENTIALS.test(
|
|
30208
|
+
const code2 = maskCode(text.replace(/\r\n/g, "\n"));
|
|
30209
|
+
return CREDENTIALS.test(code2) && /\bauthorize\b/.test(code2);
|
|
29699
30210
|
}
|
|
29700
30211
|
function authReasons(file2, text) {
|
|
29701
30212
|
const custom2 = customReasons(text);
|
|
@@ -29719,7 +30230,7 @@ var LABELS2 = {
|
|
|
29719
30230
|
var reasonText = (reasons) => reasons.map((r) => LABELS2[r]).join(" and ");
|
|
29720
30231
|
|
|
29721
30232
|
// src/checks/security/auth-rate-limits.ts
|
|
29722
|
-
var
|
|
30233
|
+
var MAX_LIST7 = 20;
|
|
29723
30234
|
var cache2 = /* @__PURE__ */ new WeakMap();
|
|
29724
30235
|
function authEntries(ctx) {
|
|
29725
30236
|
const cached2 = cache2.get(ctx);
|
|
@@ -29752,7 +30263,7 @@ function label3(e) {
|
|
|
29752
30263
|
const what = e.kind === "route" ? routePath(e) : e.kind === "action" ? `the server action in ${e.file}` : `the password sign-in in ${e.file}`;
|
|
29753
30264
|
return `${what} (${reasonText(e.reasons)})`;
|
|
29754
30265
|
}
|
|
29755
|
-
var
|
|
30266
|
+
var list8 = (items) => items.length > MAX_LIST7 ? `${items.slice(0, MAX_LIST7).join(", ")} and ${items.length - MAX_LIST7} more` : items.join(", ");
|
|
29756
30267
|
var authRateLimits = {
|
|
29757
30268
|
id: "security.auth-rate-limits",
|
|
29758
30269
|
category: "security",
|
|
@@ -29770,8 +30281,8 @@ var authRateLimits = {
|
|
|
29770
30281
|
kind: "no-rate-limit",
|
|
29771
30282
|
facts: { routes: open2.length },
|
|
29772
30283
|
key: "auth-rate-limit",
|
|
29773
|
-
locations: open2.slice(0,
|
|
29774
|
-
vars: { list:
|
|
30284
|
+
locations: open2.slice(0, MAX_LIST7).map((e) => ({ file: e.file })),
|
|
30285
|
+
vars: { list: list8(open2.map(label3)) },
|
|
29775
30286
|
meta: { files: open2.map((e) => e.file), reasons: open2.map((e) => `${e.file}: ${e.reasons.join(", ")}`) }
|
|
29776
30287
|
}
|
|
29777
30288
|
] : [],
|
|
@@ -29798,7 +30309,7 @@ var PROVIDER_MARKS = [
|
|
|
29798
30309
|
["Paddle", /paddle-signature/i]
|
|
29799
30310
|
];
|
|
29800
30311
|
function webhookProvider(file2, text) {
|
|
29801
|
-
const find = (
|
|
30312
|
+
const find = (list11, s) => list11.find(([, re]) => re.test(s))?.[0];
|
|
29802
30313
|
return find(PROVIDERS, file2) ?? find(PROVIDER_MARKS, text) ?? find(PROVIDERS, text) ?? null;
|
|
29803
30314
|
}
|
|
29804
30315
|
function importedNames(clause) {
|
|
@@ -29817,15 +30328,15 @@ function importedNames(clause) {
|
|
|
29817
30328
|
return { calls, namespaces };
|
|
29818
30329
|
}
|
|
29819
30330
|
var escapeName = (s) => s.replace(/[$]/g, "\\$");
|
|
29820
|
-
function helperVerifies(from, text, files,
|
|
30331
|
+
function helperVerifies(from, text, files, read3, aliases) {
|
|
29821
30332
|
const raw = text.replace(/\r\n/g, "\n");
|
|
29822
|
-
const
|
|
30333
|
+
const code2 = maskCode(raw);
|
|
29823
30334
|
for (const m of raw.matchAll(/import\s+(?!type\s)([^'";]+?)\s+from\s+['"]([^'"\n]+)['"]/g)) {
|
|
29824
30335
|
const [, clause = "", spec = ""] = m;
|
|
29825
30336
|
const target = resolveFrom(from, spec, files, aliases);
|
|
29826
|
-
if (!target || !verifiesInText(
|
|
30337
|
+
if (!target || !verifiesInText(read3(target) ?? "")) continue;
|
|
29827
30338
|
const { calls, namespaces } = importedNames(clause);
|
|
29828
|
-
const called = calls.some((n) => new RegExp(String.raw`(?<![\w$.])${escapeName(n)}\s*\(`).test(
|
|
30339
|
+
const called = calls.some((n) => new RegExp(String.raw`(?<![\w$.])${escapeName(n)}\s*\(`).test(code2)) || namespaces.some((n) => new RegExp(String.raw`(?<![\w$.])${escapeName(n)}\s*\.\s*[\w$]+\s*\(`).test(code2));
|
|
29829
30340
|
if (called) return target;
|
|
29830
30341
|
}
|
|
29831
30342
|
return null;
|
|
@@ -29876,33 +30387,33 @@ function readCode(text) {
|
|
|
29876
30387
|
const masked = maskCode(src);
|
|
29877
30388
|
return { src, masked, requestVars: requestVariables(masked) };
|
|
29878
30389
|
}
|
|
29879
|
-
function span(
|
|
29880
|
-
return { raw:
|
|
30390
|
+
function span(code2, from, to) {
|
|
30391
|
+
return { raw: code2.src.slice(from, to), masked: code2.masked.slice(from, to), start: from };
|
|
29881
30392
|
}
|
|
29882
30393
|
function trimSpan(s) {
|
|
29883
30394
|
const lead = s.masked.length - s.masked.trimStart().length;
|
|
29884
30395
|
const len = s.masked.trim().length;
|
|
29885
30396
|
return { raw: s.raw.slice(lead, lead + len), masked: s.masked.slice(lead, lead + len), start: s.start + lead };
|
|
29886
30397
|
}
|
|
29887
|
-
function splitTop(
|
|
30398
|
+
function splitTop(code2, from, to, sep = ",") {
|
|
29888
30399
|
const parts = [];
|
|
29889
30400
|
let depth = 0;
|
|
29890
30401
|
let start = from;
|
|
29891
30402
|
for (let i = from; i < to; i++) {
|
|
29892
|
-
const c =
|
|
30403
|
+
const c = code2.masked[i];
|
|
29893
30404
|
if (c === "(" || c === "[" || c === "{") depth++;
|
|
29894
30405
|
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
29895
|
-
else if (c === sep && depth === 0 && !(sep === "+" && (
|
|
29896
|
-
parts.push(trimSpan(span(
|
|
30406
|
+
else if (c === sep && depth === 0 && !(sep === "+" && (code2.masked[i + 1] === "+" || code2.masked[i - 1] === "+"))) {
|
|
30407
|
+
parts.push(trimSpan(span(code2, start, i)));
|
|
29897
30408
|
start = i + 1;
|
|
29898
30409
|
}
|
|
29899
30410
|
}
|
|
29900
|
-
parts.push(trimSpan(span(
|
|
30411
|
+
parts.push(trimSpan(span(code2, start, to)));
|
|
29901
30412
|
return parts.filter((p2) => p2.masked.length > 0);
|
|
29902
30413
|
}
|
|
29903
|
-
function callArgs(
|
|
29904
|
-
const close = closingIndex(
|
|
29905
|
-
return close < 0 ? null : splitTop(
|
|
30414
|
+
function callArgs(code2, open2) {
|
|
30415
|
+
const close = closingIndex(code2.masked, open2);
|
|
30416
|
+
return close < 0 ? null : splitTop(code2, open2 + 1, close - 1);
|
|
29906
30417
|
}
|
|
29907
30418
|
var CONTINUES_AFTER = /[=+\-*/%|&?:,(<>!~^[{.]$/;
|
|
29908
30419
|
var CONTINUES_BEFORE = /^[.?:+\-*/%|&,)\]}]/;
|
|
@@ -29927,13 +30438,13 @@ function statementEnd(masked, from, limit = 2e3) {
|
|
|
29927
30438
|
}
|
|
29928
30439
|
return end;
|
|
29929
30440
|
}
|
|
29930
|
-
function
|
|
30441
|
+
function assignedValue2(code2, name, before = code2.masked.length) {
|
|
29931
30442
|
const re = new RegExp(`(?<![\\w$])(?:const|let|var)\\s+${name.replace(/\$/g, "\\$")}\\s*(?::[^=;\\n]+)?=(?![=>])`, "g");
|
|
29932
30443
|
let last = null;
|
|
29933
|
-
for (const m of
|
|
30444
|
+
for (const m of code2.masked.matchAll(re)) if (m.index < before) last = m;
|
|
29934
30445
|
if (!last) return null;
|
|
29935
30446
|
const from = last.index + last[0].length;
|
|
29936
|
-
return trimSpan(span(
|
|
30447
|
+
return trimSpan(span(code2, from, statementEnd(code2.masked, from)));
|
|
29937
30448
|
}
|
|
29938
30449
|
function unwrap2(s) {
|
|
29939
30450
|
let cur = trimSpan(s);
|
|
@@ -29957,39 +30468,39 @@ function unwrap2(s) {
|
|
|
29957
30468
|
}
|
|
29958
30469
|
var QUOTED = /^(['"])\s*\1$/;
|
|
29959
30470
|
var PLAIN_TEMPLATE = /^`[^`]*`$/;
|
|
29960
|
-
function isLiteral(
|
|
30471
|
+
function isLiteral(code2, s) {
|
|
29961
30472
|
const u = unwrap2(s);
|
|
29962
30473
|
const m = u.masked;
|
|
29963
30474
|
if (QUOTED.test(m) || PLAIN_TEMPLATE.test(m) && !m.includes("${") || /^-?\d[\d_.eE]*$/.test(m) || /^(?:null|undefined|true|false)$/.test(m)) return true;
|
|
29964
|
-
const parts = splitTop(
|
|
29965
|
-
return parts.length > 1 && parts.every((p2) => isLiteral(
|
|
30475
|
+
const parts = splitTop(code2, u.start, u.start + m.length, "+");
|
|
30476
|
+
return parts.length > 1 && parts.every((p2) => isLiteral(code2, p2));
|
|
29966
30477
|
}
|
|
29967
30478
|
function startsAsString(s) {
|
|
29968
30479
|
return /^['"`]/.test(unwrap2(s).masked);
|
|
29969
30480
|
}
|
|
29970
|
-
function templateParts(
|
|
30481
|
+
function templateParts(code2, s) {
|
|
29971
30482
|
const u = unwrap2(s);
|
|
29972
30483
|
if (!u.masked.startsWith("`")) return null;
|
|
29973
30484
|
const parts = [];
|
|
29974
30485
|
const end = u.start + u.masked.length;
|
|
29975
30486
|
for (let i = u.start + 1; i < end; i++) {
|
|
29976
|
-
if (
|
|
29977
|
-
if (
|
|
29978
|
-
const close = closingIndex(
|
|
30487
|
+
if (code2.masked[i] === "`") break;
|
|
30488
|
+
if (code2.masked[i] === "$" && code2.masked[i + 1] === "{") {
|
|
30489
|
+
const close = closingIndex(code2.masked, i + 1);
|
|
29979
30490
|
if (close < 0) break;
|
|
29980
|
-
parts.push(trimSpan(span(
|
|
30491
|
+
parts.push(trimSpan(span(code2, i + 2, close - 1)));
|
|
29981
30492
|
i = close - 1;
|
|
29982
30493
|
}
|
|
29983
30494
|
}
|
|
29984
30495
|
return parts;
|
|
29985
30496
|
}
|
|
29986
|
-
function builtParts(
|
|
29987
|
-
const tpl = templateParts(
|
|
30497
|
+
function builtParts(code2, s) {
|
|
30498
|
+
const tpl = templateParts(code2, s);
|
|
29988
30499
|
if (tpl) return tpl.length ? tpl : null;
|
|
29989
30500
|
const u = unwrap2(s);
|
|
29990
|
-
const operands = splitTop(
|
|
30501
|
+
const operands = splitTop(code2, u.start, u.start + u.masked.length, "+");
|
|
29991
30502
|
if (operands.length < 2 || !operands.some((p2) => startsAsString(p2))) return null;
|
|
29992
|
-
const dynamic = operands.flatMap((p2) => isLiteral(
|
|
30503
|
+
const dynamic = operands.flatMap((p2) => isLiteral(code2, p2) ? [] : templateParts(code2, p2) ?? [p2]);
|
|
29993
30504
|
return dynamic.length ? dynamic : null;
|
|
29994
30505
|
}
|
|
29995
30506
|
var isConstant = (s) => /^[A-Z][A-Z0-9_]*(?:\.[A-Z][A-Z0-9_]*)*$/.test(unwrap2(s).masked);
|
|
@@ -29997,8 +30508,8 @@ var REQUEST2 = /(?<![\w$])(?:req|request|event|ctx|context|c\.req)\s*\??\.\s*(?:
|
|
|
29997
30508
|
var DECLARATION = /(?<![\w$])(?:const|let|var)\s+(?:([\w$]+)|\{([^}]*)\}|\[([^\]]*)\])\s*(?::[^=;\n]+)?=(?![=>])/g;
|
|
29998
30509
|
function boundNames(m) {
|
|
29999
30510
|
if (m[1]) return [m[1]];
|
|
30000
|
-
const
|
|
30001
|
-
return
|
|
30511
|
+
const list11 = m[2] ?? m[3] ?? "";
|
|
30512
|
+
return list11.split(",").map((part) => {
|
|
30002
30513
|
const p2 = part.trim().replace(/^\.\.\./, "");
|
|
30003
30514
|
const target = p2.includes(":") ? p2.slice(p2.indexOf(":") + 1) : p2;
|
|
30004
30515
|
return target.split("=")[0]?.trim() ?? "";
|
|
@@ -30020,13 +30531,13 @@ function mentionsRequest(masked, vars) {
|
|
|
30020
30531
|
for (const word of masked.matchAll(/(?<![\w$.])[A-Za-z_$][\w$]*/g)) if (vars.has(word[0])) return true;
|
|
30021
30532
|
return false;
|
|
30022
30533
|
}
|
|
30023
|
-
function fromRequest(
|
|
30024
|
-
return mentionsRequest(s.masked,
|
|
30534
|
+
function fromRequest(code2, s) {
|
|
30535
|
+
return mentionsRequest(s.masked, code2.requestVars);
|
|
30025
30536
|
}
|
|
30026
|
-
var readsRequest = (
|
|
30537
|
+
var readsRequest = (code2) => REQUEST2.test(code2.masked);
|
|
30027
30538
|
var FETCHED = /(?<![\w$.])fetch\s*\(|(?<!(?:Array|Buffer|Object|Uint8Array|Observable|Set|Map)\s*)\.\s*from\s*\(|\.\s*select\s*\(|\.\s*find(?:Many|Unique|First)\w*\s*\(/;
|
|
30028
|
-
var readsFetchedData = (
|
|
30029
|
-
var isCode2 = (
|
|
30539
|
+
var readsFetchedData = (code2) => FETCHED.test(code2.masked);
|
|
30540
|
+
var isCode2 = (code2, index) => code2.masked[index] === code2.src[index];
|
|
30030
30541
|
|
|
30031
30542
|
// src/checks/security/risky-code-cors.ts
|
|
30032
30543
|
var ANY = "cors-any";
|
|
@@ -30041,9 +30552,9 @@ var CORS_IMPORT = [
|
|
|
30041
30552
|
/import\s*\{[^}]*\bcors\b(?:\s+as\s+([\w$]+))?[^}]*\}\s*from\s*(['"])hono\/cors\2/g,
|
|
30042
30553
|
/(?:const|let|var)\s+([\w$]+)\s*=\s*require\s*\(\s*(['"])cors\2\s*\)/g
|
|
30043
30554
|
];
|
|
30044
|
-
function corsNames(
|
|
30555
|
+
function corsNames(code2) {
|
|
30045
30556
|
const names = /* @__PURE__ */ new Set();
|
|
30046
|
-
for (const re of CORS_IMPORT) for (const m of
|
|
30557
|
+
for (const re of CORS_IMPORT) for (const m of code2.src.matchAll(re)) if (isCode2(code2, m.index)) names.add(m[1] ?? "cors");
|
|
30047
30558
|
return [...names];
|
|
30048
30559
|
}
|
|
30049
30560
|
function corsOptions(options) {
|
|
@@ -30055,22 +30566,22 @@ function corsOptions(options) {
|
|
|
30055
30566
|
const value = options.raw.slice((origin.index ?? 0) + origin[0].length);
|
|
30056
30567
|
return { open: /^(?:true\b|(['"`])\*\1)/.test(value), credentials };
|
|
30057
30568
|
}
|
|
30058
|
-
function corsInCode(
|
|
30569
|
+
function corsInCode(code2) {
|
|
30059
30570
|
const open2 = [];
|
|
30060
|
-
for (const re of [HEADER_ANY, KV_ANY]) for (const m of
|
|
30061
|
-
if (READS_ORIGIN.test(
|
|
30062
|
-
for (const m of
|
|
30063
|
-
const after =
|
|
30064
|
-
if (isCode2(
|
|
30571
|
+
for (const re of [HEADER_ANY, KV_ANY]) for (const m of code2.src.matchAll(re)) if (isCode2(code2, m.index)) open2.push({ index: m.index, what: ANY });
|
|
30572
|
+
if (READS_ORIGIN.test(code2.src) && !CHECKS_ORIGIN.test(code2.masked)) {
|
|
30573
|
+
for (const m of code2.src.matchAll(HEADER_VALUE)) {
|
|
30574
|
+
const after = code2.masked.slice(m.index + m[0].length, m.index + m[0].length + 120).split(/[)},;\n]/)[0] ?? "";
|
|
30575
|
+
if (isCode2(code2, m.index) && /origin/i.test(after) && !/^\s*['"`]/.test(after)) open2.push({ index: m.index, what: "cors-echo" });
|
|
30065
30576
|
}
|
|
30066
30577
|
}
|
|
30067
|
-
let credentials = [...
|
|
30068
|
-
for (const name of corsNames(
|
|
30578
|
+
let credentials = [...code2.src.matchAll(CREDENTIALS2)].some((m) => isCode2(code2, m.index));
|
|
30579
|
+
for (const name of corsNames(code2)) {
|
|
30069
30580
|
const esc3 = name.replace(/\$/g, "\\$");
|
|
30070
30581
|
const calls = new RegExp(`(?<![\\w$.])${esc3}\\s*\\(|(?<![\\w$])register\\s*\\(\\s*${esc3}\\s*(?=[,)])`, "g");
|
|
30071
|
-
for (const m of
|
|
30582
|
+
for (const m of code2.masked.matchAll(calls)) {
|
|
30072
30583
|
const isRegister = m[0].startsWith("register");
|
|
30073
|
-
const args = callArgs(
|
|
30584
|
+
const args = callArgs(code2, isRegister ? m.index + m[0].indexOf("(") : m.index + m[0].length - 1);
|
|
30074
30585
|
if (!args) continue;
|
|
30075
30586
|
const opts = corsOptions(isRegister ? args[1] : args[0]);
|
|
30076
30587
|
credentials ||= opts.credentials;
|
|
@@ -30090,8 +30601,8 @@ function corsInVercelJson(text) {
|
|
|
30090
30601
|
let open2 = false;
|
|
30091
30602
|
let credentials = false;
|
|
30092
30603
|
for (const group of Array.isArray(groups) ? groups : []) {
|
|
30093
|
-
const
|
|
30094
|
-
for (const h of Array.isArray(
|
|
30604
|
+
const list11 = group.headers;
|
|
30605
|
+
for (const h of Array.isArray(list11) ? list11 : []) {
|
|
30095
30606
|
const key = String(h.key ?? "").toLowerCase();
|
|
30096
30607
|
const value = String(h.value ?? "").trim();
|
|
30097
30608
|
if (key === "access-control-allow-origin" && value === "*") open2 = true;
|
|
@@ -30103,8 +30614,8 @@ function corsInVercelJson(text) {
|
|
|
30103
30614
|
}
|
|
30104
30615
|
var SIGN_IN = /\.\s*auth\s*\.\s*get(?:User|Session|Claims)\s*\(|(?<![\w$.])(?:getServerSession|getSession|getUser|currentUser|getAuth|requireAuth|requireUser|withAuth|verifyToken|getToken|jwtVerify)\s*\(|(?<![\w$.])auth\s*\(\s*\)|(?<![\w$.])cookies\s*\(|\.\s*cookies\b|\bjwt\s*\.\s*verify\s*\(/;
|
|
30105
30616
|
var AUTH_HEADER = /(?:get\s*\(\s*|\[\s*)(['"`])(?:authorization|cookie)\1/gi;
|
|
30106
|
-
function checksSignIn(
|
|
30107
|
-
return SIGN_IN.test(
|
|
30617
|
+
function checksSignIn(code2) {
|
|
30618
|
+
return SIGN_IN.test(code2.masked) || [...code2.src.matchAll(AUTH_HEADER)].some((m) => isCode2(code2, m.index));
|
|
30108
30619
|
}
|
|
30109
30620
|
function corsHits(reading, signIn2) {
|
|
30110
30621
|
if (!reading.open.length || !reading.credentials && !signIn2) return [];
|
|
@@ -30116,60 +30627,60 @@ function corsHits(reading, signIn2) {
|
|
|
30116
30627
|
// src/checks/security/risky-code-inject.ts
|
|
30117
30628
|
var SANITISER = /(?<![\w$])[\w$]*(?:saniti[sz]e|purify|escape|safe)[\w$]*(?=\s*\()|(?<![\w$.])xss(?=\s*\()|\bsanitize\s*:\s*true\b/gi;
|
|
30118
30629
|
var cleans = (masked) => [...masked.matchAll(SANITISER)].some((m) => !/unsafe/i.test(m[0]));
|
|
30119
|
-
function cleanHtml(
|
|
30630
|
+
function cleanHtml(code2, s, depth = 0) {
|
|
30120
30631
|
const u = unwrap2(s);
|
|
30121
|
-
if (isLiteral(
|
|
30632
|
+
if (isLiteral(code2, u) || cleans(u.masked)) return true;
|
|
30122
30633
|
if (depth < 2 && /^[\w$]+$/.test(u.masked)) {
|
|
30123
|
-
const value =
|
|
30124
|
-
return value ? cleanHtml(
|
|
30634
|
+
const value = assignedValue2(code2, u.masked, u.start);
|
|
30635
|
+
return value ? cleanHtml(code2, value, depth + 1) : false;
|
|
30125
30636
|
}
|
|
30126
30637
|
return false;
|
|
30127
30638
|
}
|
|
30128
|
-
function htmlValue(
|
|
30639
|
+
function htmlValue(code2, expr, depth = 0) {
|
|
30129
30640
|
const u = unwrap2(expr);
|
|
30130
|
-
if (u.masked.startsWith("{") && closingIndex(
|
|
30131
|
-
for (const entry of splitTop(
|
|
30641
|
+
if (u.masked.startsWith("{") && closingIndex(code2.masked, u.start) === u.start + u.masked.length) {
|
|
30642
|
+
for (const entry of splitTop(code2, u.start + 1, u.start + u.masked.length - 1)) {
|
|
30132
30643
|
const key = entry.masked.match(/^__html\s*(:)?/);
|
|
30133
30644
|
if (!key) continue;
|
|
30134
|
-
return key[1] ? trimSpan(span(
|
|
30645
|
+
return key[1] ? trimSpan(span(code2, entry.start + key[0].length, entry.start + entry.masked.length)) : entry;
|
|
30135
30646
|
}
|
|
30136
30647
|
}
|
|
30137
30648
|
if (depth < 1 && /^[\w$]+$/.test(u.masked)) {
|
|
30138
|
-
const value =
|
|
30139
|
-
if (value?.masked.startsWith("{")) return htmlValue(
|
|
30649
|
+
const value = assignedValue2(code2, u.masked, u.start);
|
|
30650
|
+
if (value?.masked.startsWith("{")) return htmlValue(code2, value, depth + 1);
|
|
30140
30651
|
}
|
|
30141
30652
|
return u;
|
|
30142
30653
|
}
|
|
30143
|
-
function tagOf(
|
|
30144
|
-
const open2 =
|
|
30145
|
-
return open2 < 0 ? "" :
|
|
30654
|
+
function tagOf(code2, index) {
|
|
30655
|
+
const open2 = code2.masked.lastIndexOf("<", index);
|
|
30656
|
+
return open2 < 0 ? "" : code2.src.slice(open2, index);
|
|
30146
30657
|
}
|
|
30147
|
-
function unsafeHtml(
|
|
30148
|
-
if (/(['"])rehype-sanitize\1/.test(
|
|
30658
|
+
function unsafeHtml(code2) {
|
|
30659
|
+
if (/(['"])rehype-sanitize\1/.test(code2.src) || /\brehypeSanitize\b/.test(code2.masked)) return [];
|
|
30149
30660
|
const hits = [];
|
|
30150
|
-
const fileHigh = readsRequest(
|
|
30661
|
+
const fileHigh = readsRequest(code2) || readsFetchedData(code2);
|
|
30151
30662
|
const push = (index, value, what, styleTag = false) => {
|
|
30152
|
-
if (cleanHtml(
|
|
30153
|
-
const high = fileHigh || fromRequest(
|
|
30663
|
+
if (cleanHtml(code2, value)) return;
|
|
30664
|
+
const high = fileHigh || fromRequest(code2, value);
|
|
30154
30665
|
if (styleTag && !high) return;
|
|
30155
30666
|
hits.push({ kind: "html", severity: high ? "high" : "medium", index, what });
|
|
30156
30667
|
};
|
|
30157
|
-
for (const m of
|
|
30668
|
+
for (const m of code2.masked.matchAll(/(?<![\w$])dangerouslySetInnerHTML\s*=\s*\{/g)) {
|
|
30158
30669
|
const open2 = m.index + m[0].length - 1;
|
|
30159
|
-
const close = closingIndex(
|
|
30670
|
+
const close = closingIndex(code2.masked, open2);
|
|
30160
30671
|
if (close < 0) continue;
|
|
30161
|
-
const value = htmlValue(
|
|
30162
|
-
const tag = tagOf(
|
|
30672
|
+
const value = htmlValue(code2, trimSpan(span(code2, open2 + 1, close - 1)));
|
|
30673
|
+
const tag = tagOf(code2, m.index);
|
|
30163
30674
|
const json2 = /^<script\b/.test(tag) && /type\s*=\s*\{?\s*(['"`])application\/(?:ld\+)?json\1/.test(tag);
|
|
30164
30675
|
if (json2 && /JSON\s*\.\s*stringify\s*\(/.test(value.masked)) continue;
|
|
30165
30676
|
push(m.index, value, "html-prop", /^<style\b/.test(tag));
|
|
30166
30677
|
}
|
|
30167
|
-
for (const m of
|
|
30678
|
+
for (const m of code2.masked.matchAll(/\.\s*(?:inner|outer)HTML\s*\+?=(?![=>])/g)) {
|
|
30168
30679
|
const from = m.index + m[0].length;
|
|
30169
|
-
push(m.index, trimSpan(span(
|
|
30680
|
+
push(m.index, trimSpan(span(code2, from, statementEnd(code2.masked, from))), "html-inner");
|
|
30170
30681
|
}
|
|
30171
|
-
for (const m of
|
|
30172
|
-
const html = callArgs(
|
|
30682
|
+
for (const m of code2.masked.matchAll(/\.\s*insertAdjacentHTML\s*\(/g)) {
|
|
30683
|
+
const html = callArgs(code2, m.index + m[0].length - 1)?.[1];
|
|
30173
30684
|
if (html) push(m.index, html, "html-adjacent");
|
|
30174
30685
|
}
|
|
30175
30686
|
return hits;
|
|
@@ -30180,72 +30691,72 @@ var TIMER = /(?<![\w$.])(?:(?:window|globalThis|self)\s*\.\s*)?set(?:Timeout|Int
|
|
|
30180
30691
|
var VM = /(?<![\w$])vm\s*\.\s*(?:runIn(?:New|This)?Context|compileFunction)\s*\(|(?<![\w$])new\s+vm\s*\.\s*Script\s*\(/g;
|
|
30181
30692
|
var VM_BARE = /(?<![\w$.])(?:runIn(?:New|This)?Context|compileFunction)\s*\(|(?<![\w$.])new\s+Script\s*\(/g;
|
|
30182
30693
|
var IMPORTS_VM = /(?:\bfrom\s*|\brequire\s*\(\s*)(['"])(?:node:)?vm\1/;
|
|
30183
|
-
function codeFromText(
|
|
30694
|
+
function codeFromText(code2) {
|
|
30184
30695
|
const hits = [];
|
|
30185
30696
|
const check2 = (re, what, bad) => {
|
|
30186
|
-
for (const m of
|
|
30187
|
-
const args = callArgs(
|
|
30697
|
+
for (const m of code2.masked.matchAll(re)) {
|
|
30698
|
+
const args = callArgs(code2, m.index + m[0].length - 1);
|
|
30188
30699
|
if (!args || !bad(args)) continue;
|
|
30189
|
-
hits.push({ kind: "eval", severity: args.some((a) => fromRequest(
|
|
30700
|
+
hits.push({ kind: "eval", severity: args.some((a) => fromRequest(code2, a)) ? "high" : "medium", index: m.index, what });
|
|
30190
30701
|
}
|
|
30191
30702
|
};
|
|
30192
|
-
const dynamic = (args) => args.length > 0 && !args.every((a) => isLiteral(
|
|
30703
|
+
const dynamic = (args) => args.length > 0 && !args.every((a) => isLiteral(code2, a));
|
|
30193
30704
|
check2(EVAL, "eval", dynamic);
|
|
30194
30705
|
check2(FUNCTION, "new-function", dynamic);
|
|
30195
30706
|
check2(TIMER, "timer", (args) => args[0] !== void 0 && startsAsString(args[0]));
|
|
30196
|
-
const firstDynamic = (args) => args[0] !== void 0 && !isLiteral(
|
|
30707
|
+
const firstDynamic = (args) => args[0] !== void 0 && !isLiteral(code2, args[0]);
|
|
30197
30708
|
check2(VM, "vm", firstDynamic);
|
|
30198
|
-
if (IMPORTS_VM.test(
|
|
30709
|
+
if (IMPORTS_VM.test(code2.src)) check2(VM_BARE, "vm", firstDynamic);
|
|
30199
30710
|
return hits;
|
|
30200
30711
|
}
|
|
30201
30712
|
var SQL_TEXT = /\bselect\b[\s\S]*?\bfrom\b|\binsert\s+into\b|\bupdate\s+[\w$."`{}]+\s+set\b|\bdelete\s+from\b|\bwhere\b/i;
|
|
30202
30713
|
var SQL_SINK = /\.\s*(?:query|execute|raw|unsafe|\$queryRawUnsafe|\$executeRawUnsafe)\s*\(/g;
|
|
30203
30714
|
var DB_SINK = /(?<![\w$])(?:[\w$]*(?:db|database|sqlite)|DB)\s*\.\s*(?:run|all|get|exec|prepare|each)\s*\(/gi;
|
|
30204
30715
|
var FILTER_TEXT = /\.(?:eq|neq|gt|gte|lt|lte|like|ilike|is|in|cs|cd|ov|fts|plfts|phfts|wfts|match|imatch)\./;
|
|
30205
|
-
var
|
|
30716
|
+
var PLACEHOLDER2 = /(['"`])\s*[$?]/;
|
|
30206
30717
|
var ID_LIKE = /(?:^|\.)id$|[a-z]Id$|_id$|ID$/;
|
|
30207
|
-
function riskyParts(
|
|
30208
|
-
const parts = builtParts(
|
|
30718
|
+
function riskyParts(code2, s, ids = false) {
|
|
30719
|
+
const parts = builtParts(code2, s) ?? [];
|
|
30209
30720
|
return parts.filter((p2) => {
|
|
30210
|
-
if (isConstant(p2) ||
|
|
30211
|
-
if (ids && !fromRequest(
|
|
30721
|
+
if (isConstant(p2) || PLACEHOLDER2.test(p2.raw)) return false;
|
|
30722
|
+
if (ids && !fromRequest(code2, p2) && ID_LIKE.test(unwrap2(p2).masked)) return false;
|
|
30212
30723
|
return true;
|
|
30213
30724
|
});
|
|
30214
30725
|
}
|
|
30215
|
-
function queryText(
|
|
30726
|
+
function queryText(code2, arg) {
|
|
30216
30727
|
const u = unwrap2(arg);
|
|
30217
|
-
if (/^[\w$]+$/.test(u.masked)) return
|
|
30728
|
+
if (/^[\w$]+$/.test(u.masked)) return assignedValue2(code2, u.masked, u.start) ?? u;
|
|
30218
30729
|
return u;
|
|
30219
30730
|
}
|
|
30220
|
-
function stringSql(
|
|
30731
|
+
function stringSql(code2) {
|
|
30221
30732
|
const hits = [];
|
|
30222
|
-
const add = (index, parts, what) => hits.push({ kind: "sql", severity: parts.some((p2) => fromRequest(
|
|
30733
|
+
const add = (index, parts, what) => hits.push({ kind: "sql", severity: parts.some((p2) => fromRequest(code2, p2)) ? "high" : "medium", index, what });
|
|
30223
30734
|
for (const re of [SQL_SINK, DB_SINK]) {
|
|
30224
|
-
for (const m of
|
|
30225
|
-
const first = callArgs(
|
|
30735
|
+
for (const m of code2.masked.matchAll(re)) {
|
|
30736
|
+
const first = callArgs(code2, m.index + m[0].length - 1)?.[0];
|
|
30226
30737
|
if (!first) continue;
|
|
30227
|
-
const text = queryText(
|
|
30228
|
-
const parts = riskyParts(
|
|
30738
|
+
const text = queryText(code2, first);
|
|
30739
|
+
const parts = riskyParts(code2, text);
|
|
30229
30740
|
if (parts.length && SQL_TEXT.test(text.raw)) add(m.index, parts, "sql-text");
|
|
30230
30741
|
}
|
|
30231
30742
|
}
|
|
30232
|
-
for (const m of
|
|
30233
|
-
const params = callArgs(
|
|
30743
|
+
for (const m of code2.masked.matchAll(/\.\s*rpc\s*\(/g)) {
|
|
30744
|
+
const params = callArgs(code2, m.index + m[0].length - 1)?.[1];
|
|
30234
30745
|
if (!params?.masked.startsWith("{")) continue;
|
|
30235
|
-
const values = splitTop(
|
|
30746
|
+
const values = splitTop(code2, params.start + 1, params.start + params.masked.length - 1).map((e) => {
|
|
30236
30747
|
const colon = e.masked.match(/^[\w$]+\s*:/);
|
|
30237
|
-
return colon ? trimSpan(span(
|
|
30748
|
+
return colon ? trimSpan(span(code2, e.start + colon[0].length, e.start + e.masked.length)) : e;
|
|
30238
30749
|
});
|
|
30239
|
-
const parts = values.flatMap((v) => SQL_TEXT.test(v.raw) ? riskyParts(
|
|
30750
|
+
const parts = values.flatMap((v) => SQL_TEXT.test(v.raw) ? riskyParts(code2, v) : []);
|
|
30240
30751
|
if (parts.length) add(m.index, parts, "sql-rpc");
|
|
30241
30752
|
}
|
|
30242
|
-
for (const m of
|
|
30243
|
-
const args = callArgs(
|
|
30753
|
+
for (const m of code2.masked.matchAll(/\.\s*(or|filter)\s*\(/g)) {
|
|
30754
|
+
const args = callArgs(code2, m.index + m[0].length - 1);
|
|
30244
30755
|
if (!args) continue;
|
|
30245
30756
|
const isOr = m[1] === "or";
|
|
30246
30757
|
if (!isOr && args.length < 2) continue;
|
|
30247
30758
|
const candidates2 = isOr ? args.slice(0, 1).filter((a) => FILTER_TEXT.test(a.raw)) : args;
|
|
30248
|
-
const parts = candidates2.flatMap((a) => riskyParts(
|
|
30759
|
+
const parts = candidates2.flatMap((a) => riskyParts(code2, a, true));
|
|
30249
30760
|
if (parts.length) add(m.index, parts, isOr ? "sql-or" : "sql-filter");
|
|
30250
30761
|
}
|
|
30251
30762
|
return hits;
|
|
@@ -30273,13 +30784,13 @@ function callsValidator(masked) {
|
|
|
30273
30784
|
var DOUBLE_SLASH_CHECK = /startsWith\s*\(\s*(['"`])(?:\/\/|\\\\)\1|\^\\\/\(\?!|\^\/\(\?!\//;
|
|
30274
30785
|
var SINGLE_SLASH_CHECK = /startsWith\s*\(\s*(['"`])\/\1/;
|
|
30275
30786
|
var ORIGIN_PREFIX = /^`\$\{\s*[\w$.]*origin\s*\}|^[\w$.]*origin\s*\+/i;
|
|
30276
|
-
function nextVariables(
|
|
30787
|
+
function nextVariables(code2) {
|
|
30277
30788
|
const vars = /* @__PURE__ */ new Set();
|
|
30278
30789
|
const decl = /(?<![\w$])(?:const|let|var)\s+(?:([\w$]+)|\{([^}]*)\})\s*(?::[^=;\n]+)?=(?![=>])/g;
|
|
30279
30790
|
for (let pass = 0; pass < 2; pass++) {
|
|
30280
|
-
for (const m of
|
|
30791
|
+
for (const m of code2.masked.matchAll(decl)) {
|
|
30281
30792
|
const from = m.index + m[0].length;
|
|
30282
|
-
const value = trimSpan(span(
|
|
30793
|
+
const value = trimSpan(span(code2, from, statementEnd(code2.masked, from)));
|
|
30283
30794
|
if (m[1]) {
|
|
30284
30795
|
if (isSource(value, vars)) vars.add(m[1]);
|
|
30285
30796
|
} else if (PARAM_OBJECT.test(value.masked)) {
|
|
@@ -30302,10 +30813,10 @@ function startsWithLocalPath(s) {
|
|
|
30302
30813
|
const raw = unwrap2(s).raw;
|
|
30303
30814
|
return /^(['"`])\/(?![/\\])[^'"`$]/.test(raw);
|
|
30304
30815
|
}
|
|
30305
|
-
function openRedirects(
|
|
30306
|
-
if (VALIDATED_MASKED.test(
|
|
30307
|
-
const vars = nextVariables(
|
|
30308
|
-
const singleSlash = SINGLE_SLASH_CHECK.test(
|
|
30816
|
+
function openRedirects(code2) {
|
|
30817
|
+
if (VALIDATED_MASKED.test(code2.masked) || callsValidator(code2.masked) || DOUBLE_SLASH_CHECK.test(code2.src)) return [];
|
|
30818
|
+
const vars = nextVariables(code2);
|
|
30819
|
+
const singleSlash = SINGLE_SLASH_CHECK.test(code2.src);
|
|
30309
30820
|
const hits = [];
|
|
30310
30821
|
const consider = (index, value) => {
|
|
30311
30822
|
const u = unwrap2(value);
|
|
@@ -30313,12 +30824,12 @@ function openRedirects(code) {
|
|
|
30313
30824
|
if (singleSlash && ORIGIN_PREFIX.test(u.masked)) return;
|
|
30314
30825
|
hits.push({ kind: "redirect", severity: "medium", index, what: "redirect" });
|
|
30315
30826
|
};
|
|
30316
|
-
for (const m of
|
|
30317
|
-
for (const arg of callArgs(
|
|
30827
|
+
for (const m of code2.masked.matchAll(SINK)) {
|
|
30828
|
+
for (const arg of callArgs(code2, m.index + m[0].length - 1) ?? []) consider(m.index, arg);
|
|
30318
30829
|
}
|
|
30319
|
-
for (const m of
|
|
30830
|
+
for (const m of code2.masked.matchAll(ASSIGN)) {
|
|
30320
30831
|
const from = m.index + m[0].length;
|
|
30321
|
-
consider(m.index, trimSpan(span(
|
|
30832
|
+
consider(m.index, trimSpan(span(code2, from, statementEnd(code2.masked, from))));
|
|
30322
30833
|
}
|
|
30323
30834
|
return hits;
|
|
30324
30835
|
}
|
|
@@ -30333,8 +30844,8 @@ var NOT_CONFIG = /(?:^|\/)(?:node_modules|\.next|\.vercel|dist|build|out|\.aura|
|
|
|
30333
30844
|
var NEXT_CONFIG = /(?:^|\/)next\.config\.[cm]?[jt]s$/;
|
|
30334
30845
|
var VERCEL_JSON = /(?:^|\/)vercel\.json$/;
|
|
30335
30846
|
var ROUTE_FILE3 = /(?:^|\/)(?:route|middleware|proxy)\.[cm]?[jt]s$|(?:^|\/)pages\/api\//;
|
|
30336
|
-
function hitsIn(
|
|
30337
|
-
return [...unsafeHtml(
|
|
30847
|
+
function hitsIn(code2) {
|
|
30848
|
+
return [...unsafeHtml(code2), ...codeFromText(code2), ...stringSql(code2), ...openRedirects(code2), ...corsHits(corsInCode(code2), checksSignIn(code2))];
|
|
30338
30849
|
}
|
|
30339
30850
|
var riskyCode = {
|
|
30340
30851
|
id: "security.risky-code",
|
|
@@ -30349,9 +30860,9 @@ var riskyCode = {
|
|
|
30349
30860
|
for (const file2 of files) {
|
|
30350
30861
|
const text = ctx.read(file2);
|
|
30351
30862
|
if (!text) continue;
|
|
30352
|
-
const
|
|
30353
|
-
byFile.set(file2, { text, hits: hitsIn(
|
|
30354
|
-
if (ROUTE_FILE3.test(file2) && checksSignIn(
|
|
30863
|
+
const code2 = readCode(text);
|
|
30864
|
+
byFile.set(file2, { text, hits: hitsIn(code2) });
|
|
30865
|
+
if (ROUTE_FILE3.test(file2) && checksSignIn(code2)) signInRoutes = true;
|
|
30355
30866
|
}
|
|
30356
30867
|
for (const file2 of ctx.files.filter((f) => !NOT_CONFIG.test(f) && (NEXT_CONFIG.test(f) || VERCEL_JSON.test(f)))) {
|
|
30357
30868
|
const text = ctx.read(file2);
|
|
@@ -30401,6 +30912,7 @@ var securityChecks = [
|
|
|
30401
30912
|
deps,
|
|
30402
30913
|
rls,
|
|
30403
30914
|
apiRoutes,
|
|
30915
|
+
edgeFunctions,
|
|
30404
30916
|
headers,
|
|
30405
30917
|
...CONNECTORS2.map(toolSettingsCheck),
|
|
30406
30918
|
workflows,
|
|
@@ -30411,17 +30923,18 @@ var securityChecks = [
|
|
|
30411
30923
|
webhooks,
|
|
30412
30924
|
supabaseCode,
|
|
30413
30925
|
serverOnly,
|
|
30926
|
+
browserSecrets,
|
|
30414
30927
|
riskyCode,
|
|
30415
30928
|
databaseBackups
|
|
30416
30929
|
];
|
|
30417
30930
|
|
|
30418
30931
|
// src/checks/structure.ts
|
|
30419
|
-
import
|
|
30932
|
+
import path41 from "path";
|
|
30420
30933
|
|
|
30421
30934
|
// src/checks/debug-logs.ts
|
|
30422
30935
|
var USE_CLIENT = /^?(?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*['"]use client['"]/;
|
|
30423
30936
|
var DEBUG_CALL = /(?<![\w$])console\s*\??\.\s*(?:log|debug|info|table|dir)\s*\(/;
|
|
30424
|
-
var
|
|
30937
|
+
var DEV_GUARD2 = /NODE_ENV|import\.meta\.env\.DEV|__DEV__|\bisDev(?:elopment)?\b/;
|
|
30425
30938
|
var SERVER_DEPS = [
|
|
30426
30939
|
"express",
|
|
30427
30940
|
"fastify",
|
|
@@ -30453,7 +30966,7 @@ function isBrowserFile2(file2, text, ctx) {
|
|
|
30453
30966
|
}
|
|
30454
30967
|
function debugLogLines(text) {
|
|
30455
30968
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
30456
|
-
const found = findLogCalls(text, DEBUG_CALL).map((c) => c.line).filter((line) => !
|
|
30969
|
+
const found = findLogCalls(text, DEBUG_CALL).map((c) => c.line).filter((line) => !DEV_GUARD2.test(`${lines[line - 2] ?? ""}
|
|
30457
30970
|
${lines[line - 1] ?? ""}`));
|
|
30458
30971
|
return [...new Set(found)];
|
|
30459
30972
|
}
|
|
@@ -30489,7 +31002,7 @@ var debugLogs = {
|
|
|
30489
31002
|
|
|
30490
31003
|
// src/checks/repo-hygiene.ts
|
|
30491
31004
|
import { statSync as statSync4 } from "fs";
|
|
30492
|
-
import
|
|
31005
|
+
import path35 from "path";
|
|
30493
31006
|
var MB = 1024 * 1024;
|
|
30494
31007
|
var LARGE_FILE_BYTES = 5 * MB;
|
|
30495
31008
|
var HEAVY_FILE_BYTES = 1 * MB;
|
|
@@ -30527,7 +31040,7 @@ function lfsPatterns(gitattributes) {
|
|
|
30527
31040
|
return pattern.includes("/") ? globToRegex(pattern) : globToRegex(`**/${pattern}`);
|
|
30528
31041
|
});
|
|
30529
31042
|
}
|
|
30530
|
-
function largeFiles(root, tracked, lfs = [], stat = statSync4, p2 =
|
|
31043
|
+
function largeFiles(root, tracked, lfs = [], stat = statSync4, p2 = path35) {
|
|
30531
31044
|
const out = [];
|
|
30532
31045
|
for (const file2 of tracked) {
|
|
30533
31046
|
if (AURA_FILES.test(file2) || lfs.some((re) => re.test(file2))) continue;
|
|
@@ -30580,10 +31093,10 @@ var repoHygiene = {
|
|
|
30580
31093
|
};
|
|
30581
31094
|
|
|
30582
31095
|
// src/checks/naming.ts
|
|
30583
|
-
import
|
|
31096
|
+
import path37 from "path";
|
|
30584
31097
|
|
|
30585
31098
|
// src/checks/name-styles.ts
|
|
30586
|
-
import
|
|
31099
|
+
import path36 from "path";
|
|
30587
31100
|
var STYLES = ["kebab", "pascal", "camel", "snake"];
|
|
30588
31101
|
var SHORT_LABEL = {
|
|
30589
31102
|
kebab: "kebab-case",
|
|
@@ -30671,7 +31184,7 @@ function isAppCode(f) {
|
|
|
30671
31184
|
return !f.split("/").some((seg) => seg.startsWith(".")) && !isTestFile(f) && !GENERATED.test(f) && !NOT_APP_CODE2.test(f);
|
|
30672
31185
|
}
|
|
30673
31186
|
function baseName(file2) {
|
|
30674
|
-
let name =
|
|
31187
|
+
let name = path36.posix.basename(file2).replace(NAMED_EXT, "");
|
|
30675
31188
|
while (SUFFIX.test(name)) name = name.replace(SUFFIX, "");
|
|
30676
31189
|
return name;
|
|
30677
31190
|
}
|
|
@@ -30716,8 +31229,8 @@ function toStyle(name, style) {
|
|
|
30716
31229
|
return words2.map((w, i) => i === 0 ? w : cap(w)).join("");
|
|
30717
31230
|
}
|
|
30718
31231
|
function renamed(file2, style) {
|
|
30719
|
-
const dir =
|
|
30720
|
-
const base2 =
|
|
31232
|
+
const dir = path36.posix.dirname(file2);
|
|
31233
|
+
const base2 = path36.posix.basename(file2);
|
|
30721
31234
|
const name = baseName(file2);
|
|
30722
31235
|
const next = toStyle(name, style) + base2.slice(name.length);
|
|
30723
31236
|
return dir === "." ? next : `${dir}/${next}`;
|
|
@@ -30786,10 +31299,10 @@ function folderProblem(folder, entries) {
|
|
|
30786
31299
|
if (off.length === 0) return null;
|
|
30787
31300
|
return { folder, style: target, files: off.map((e) => ({ ...e, suggested: renamed(e.file, target) })) };
|
|
30788
31301
|
}
|
|
30789
|
-
function componentFolderProblem(byFolder,
|
|
31302
|
+
function componentFolderProblem(byFolder, read3) {
|
|
30790
31303
|
const styled2 = [];
|
|
30791
31304
|
for (const [folder, entries] of byFolder) {
|
|
30792
|
-
const comps = entries.filter((e) => JSX_EXT.test(e.file) && exportsComponent(
|
|
31305
|
+
const comps = entries.filter((e) => JSX_EXT.test(e.file) && exportsComponent(read3(e.file) ?? ""));
|
|
30793
31306
|
if (comps.length < 2) continue;
|
|
30794
31307
|
const pascal2 = comps.filter((e) => e.style === "pascal").length;
|
|
30795
31308
|
const lower = comps.filter((e) => e.style === "kebab" || e.style === "single").length;
|
|
@@ -30808,7 +31321,7 @@ function componentFolderProblem(byFolder, read2) {
|
|
|
30808
31321
|
minority: minor.map((s) => ({ folder: s.folder, files: s.files }))
|
|
30809
31322
|
};
|
|
30810
31323
|
}
|
|
30811
|
-
function analyzeNaming(files,
|
|
31324
|
+
function analyzeNaming(files, read3) {
|
|
30812
31325
|
const byFolder = /* @__PURE__ */ new Map();
|
|
30813
31326
|
const classified = [];
|
|
30814
31327
|
const codeNames = [];
|
|
@@ -30819,22 +31332,22 @@ function analyzeNaming(files, read2) {
|
|
|
30819
31332
|
const style = fileStyle(file2);
|
|
30820
31333
|
if (style) {
|
|
30821
31334
|
classified.push(file2);
|
|
30822
|
-
const folder =
|
|
31335
|
+
const folder = path37.posix.dirname(file2);
|
|
30823
31336
|
byFolder.set(folder, [...byFolder.get(folder) ?? [], { file: file2, style }]);
|
|
30824
31337
|
}
|
|
30825
31338
|
if (!CODE_EXT.test(file2) || file2.endsWith(".d.ts") || !isAppCode(file2)) continue;
|
|
30826
|
-
const text =
|
|
31339
|
+
const text = read3(raw);
|
|
30827
31340
|
if (!text) continue;
|
|
30828
31341
|
const names = badCodeNames(file2, text);
|
|
30829
31342
|
if (names.length) codeNames.push({ file: file2, names });
|
|
30830
31343
|
}
|
|
30831
31344
|
const folders = [...byFolder].map(([folder, entries]) => folderProblem(folder, entries)).filter((p2) => p2 !== null).sort((a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0);
|
|
30832
|
-
return { classified, folders, componentFolders: componentFolderProblem(byFolder, (f) =>
|
|
31345
|
+
return { classified, folders, componentFolders: componentFolderProblem(byFolder, (f) => read3(raws.get(f) ?? f)), codeNames };
|
|
30833
31346
|
}
|
|
30834
31347
|
var MAX_LISTED8 = 20;
|
|
30835
31348
|
var MAX_FILE_FINDINGS = 20;
|
|
30836
31349
|
var where = (folder) => folder === "." ? "the top level of the repo" : `${folder}/`;
|
|
30837
|
-
var
|
|
31350
|
+
var list9 = (items) => `${items.slice(0, MAX_LISTED8).join(", ")}${items.length > MAX_LISTED8 ? ` and ${items.length - MAX_LISTED8} more` : ""}`;
|
|
30838
31351
|
var naming = {
|
|
30839
31352
|
id: "structure.naming",
|
|
30840
31353
|
category: "structure",
|
|
@@ -30847,7 +31360,7 @@ var naming = {
|
|
|
30847
31360
|
const items = [];
|
|
30848
31361
|
for (const p2 of a.folders) {
|
|
30849
31362
|
for (const f of p2.files) flagged.add(f.file);
|
|
30850
|
-
const example = a.classified.find((f) =>
|
|
31363
|
+
const example = a.classified.find((f) => path37.posix.dirname(f) === p2.folder && fileStyle(f) === p2.style);
|
|
30851
31364
|
items.push({
|
|
30852
31365
|
kind: "folder-style",
|
|
30853
31366
|
facts: { files: p2.files.length, style: p2.style, example: Boolean(example) },
|
|
@@ -30855,8 +31368,8 @@ var naming = {
|
|
|
30855
31368
|
locations: p2.files.slice(0, MAX_LISTED8).map((f) => ({ file: f.file })),
|
|
30856
31369
|
vars: {
|
|
30857
31370
|
where: where(p2.folder),
|
|
30858
|
-
example: example ?
|
|
30859
|
-
files:
|
|
31371
|
+
example: example ? path37.posix.basename(example) : "",
|
|
31372
|
+
files: list9(p2.files.map((f) => `${f.file} (${SHORT_LABEL[f.style]}, would be ${path37.posix.basename(f.suggested)})`))
|
|
30860
31373
|
},
|
|
30861
31374
|
meta: { folder: p2.folder, style: p2.style, files: p2.files.map((f) => f.file), renames: p2.files.map((f) => `${f.file}=>${f.suggested}`) }
|
|
30862
31375
|
});
|
|
@@ -30871,9 +31384,9 @@ var naming = {
|
|
|
30871
31384
|
key: "component-folders",
|
|
30872
31385
|
locations: minorityFiles.slice(0, MAX_LISTED8).map((file2) => ({ file: file2 })),
|
|
30873
31386
|
vars: {
|
|
30874
|
-
majority:
|
|
30875
|
-
minority:
|
|
30876
|
-
example:
|
|
31387
|
+
majority: list9(cf.majority.map(where)),
|
|
31388
|
+
minority: list9(cf.minority.map((m) => where(m.folder))),
|
|
31389
|
+
example: path37.posix.basename(minorityFiles[0] ?? "")
|
|
30877
31390
|
},
|
|
30878
31391
|
meta: { style: cf.style, folders: cf.minority.map((m) => m.folder), files: minorityFiles }
|
|
30879
31392
|
});
|
|
@@ -30899,7 +31412,7 @@ var naming = {
|
|
|
30899
31412
|
facts: { files: rest.length },
|
|
30900
31413
|
key: "code-names:more",
|
|
30901
31414
|
locations: rest.slice(0, MAX_LISTED8).map((g) => ({ file: g.file })),
|
|
30902
|
-
vars: { files:
|
|
31415
|
+
vars: { files: list9(rest.map((g) => `${g.file} (${g.names.map((n) => n.name).join(", ")})`)) },
|
|
30903
31416
|
meta: { files: rest.map((g) => g.file) }
|
|
30904
31417
|
});
|
|
30905
31418
|
}
|
|
@@ -30909,70 +31422,18 @@ var naming = {
|
|
|
30909
31422
|
};
|
|
30910
31423
|
|
|
30911
31424
|
// src/checks/reuse.ts
|
|
30912
|
-
import
|
|
30913
|
-
|
|
30914
|
-
// src/checks/hardcoded-urls.ts
|
|
30915
|
-
var STRING_OR_COMMENT = /("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)|\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
30916
|
-
function stripComments2(text) {
|
|
30917
|
-
return text.replace(/\r\n/g, "\n").replace(STRING_OR_COMMENT, (m, str) => str ?? m.replace(/[^\n]/g, " "));
|
|
30918
|
-
}
|
|
30919
|
-
var URL_RE = /\bhttps?:\/\/([a-z0-9-]+(?:\.[a-z0-9-]+)*)(?::\d+)?[^\s'"`)<>]*/gi;
|
|
30920
|
-
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
|
|
30921
|
-
var IGNORED_HOSTS = [
|
|
30922
|
-
"schema.org",
|
|
30923
|
-
"json-schema.org",
|
|
30924
|
-
"w3.org",
|
|
30925
|
-
"github.com",
|
|
30926
|
-
"githubusercontent.com",
|
|
30927
|
-
"npmjs.org",
|
|
30928
|
-
"npmjs.com",
|
|
30929
|
-
"fonts.googleapis.com",
|
|
30930
|
-
"fonts.gstatic.com",
|
|
30931
|
-
"example.com",
|
|
30932
|
-
"example.org",
|
|
30933
|
-
"example.net"
|
|
30934
|
-
];
|
|
30935
|
-
var RESERVED_TLD = /\.(?:example|test|invalid|local|localhost)$/;
|
|
30936
|
-
function isIgnoredHost(host) {
|
|
30937
|
-
return RESERVED_TLD.test(host) || IGNORED_HOSTS.some((h) => host === h || host.endsWith(`.${h}`));
|
|
30938
|
-
}
|
|
30939
|
-
function inCdnTag(text, index) {
|
|
30940
|
-
const open2 = text.lastIndexOf("<", index);
|
|
30941
|
-
return open2 >= 0 && /^<(?:script|Script|link)\b[^>]*$/.test(text.slice(open2, index));
|
|
30942
|
-
}
|
|
30943
|
-
function findUrls(text) {
|
|
30944
|
-
const code = stripComments2(text);
|
|
30945
|
-
const hits = [];
|
|
30946
|
-
for (const m of code.matchAll(URL_RE)) {
|
|
30947
|
-
const host = (m[1] ?? "").toLowerCase().replace(/^www\./, "");
|
|
30948
|
-
if (!host || inCdnTag(code, m.index)) continue;
|
|
30949
|
-
hits.push({ host, url: m[0], index: m.index });
|
|
30950
|
-
}
|
|
30951
|
-
return hits;
|
|
30952
|
-
}
|
|
30953
|
-
var DEV_GUARD2 = /NODE_ENV\s*[!=]==?\s*['"`](?:development|production|test)['"`]|['"`](?:development|production|test)['"`]\s*[!=]==?\s*process\.env\.NODE_ENV|import\.meta\.env\.(?:DEV|PROD)\b|__DEV__|\bisDev(?:elopment)?\b|\bisProd(?:uction)?\b/;
|
|
30954
|
-
var ENV_FALLBACK = /(?:process\.env|import\.meta\.env)(?:\.\w+|\[['"]\w+['"]\])\s*(?:\?\?|\|\|)\s*$/;
|
|
30955
|
-
function isDevOnlyUse(code, index) {
|
|
30956
|
-
const lineStart = code.lastIndexOf("\n", index - 1) + 1;
|
|
30957
|
-
const before = code.slice(lineStart, index).replace(/['"`]$/, "");
|
|
30958
|
-
if (ENV_FALLBACK.test(before.trimEnd())) return true;
|
|
30959
|
-
let start = lineStart;
|
|
30960
|
-
for (let i = 0; i < 6 && start > 0; i++) start = code.lastIndexOf("\n", start - 2) + 1;
|
|
30961
|
-
const lineEnd = code.indexOf("\n", index);
|
|
30962
|
-
return DEV_GUARD2.test(code.slice(start, lineEnd === -1 ? void 0 : lineEnd));
|
|
30963
|
-
}
|
|
30964
|
-
var RUNS_LOCAL_SERVER = /\bcreateServer\s*\(|\.listen\s*\(/;
|
|
31425
|
+
import path39 from "path";
|
|
30965
31426
|
|
|
30966
31427
|
// src/checks/import-cycles.ts
|
|
30967
|
-
function buildReuseGraph(files,
|
|
30968
|
-
const aliases = readAliases(
|
|
31428
|
+
function buildReuseGraph(files, read3) {
|
|
31429
|
+
const aliases = readAliases(read3);
|
|
30969
31430
|
const fileSet = new Set(files);
|
|
30970
31431
|
const app2 = new Set(appSource(files));
|
|
30971
31432
|
const edges = /* @__PURE__ */ new Map();
|
|
30972
31433
|
const importers = /* @__PURE__ */ new Map();
|
|
30973
31434
|
for (const f of files) {
|
|
30974
31435
|
if (!isSourceFile(f) || SKIP.test(f)) continue;
|
|
30975
|
-
const text =
|
|
31436
|
+
const text = read3(f);
|
|
30976
31437
|
if (text === null) continue;
|
|
30977
31438
|
for (const ref of parseImports(text)) {
|
|
30978
31439
|
const target = resolveImport(f, ref.specifier, fileSet, aliases);
|
|
@@ -31078,8 +31539,8 @@ function fetchTarget(arg) {
|
|
|
31078
31539
|
}
|
|
31079
31540
|
var isUiFile2 = (f) => (/\.(?:tsx|jsx)$/.test(f) || /(?:^|\/)(?:components|pages|views|screens)\//.test(f)) && !/(?:^|\/)route\.[cm]?[jt]sx?$/.test(f) && !/(?:^|\/)pages\/api\//.test(f);
|
|
31080
31541
|
var CLIENT_MODULE = /(?:^|\/)(?:lib|utils|shared|common|src)\/(?:api|client|http|fetcher|fetch|request|requests|axios)(?:[-._][\w.-]*)?\.[cm]?[jt]sx?$|(?:^|\/)(?:lib\/api|lib\/clients?|services|api-client|clients)\/[^/]+\.[cm]?[jt]sx?$|(?:^|\/)hooks\/use(?:Api|Fetch|Fetcher|Request)\w*\.[cm]?[jt]sx?$/i;
|
|
31081
|
-
function hasSharedClient(files,
|
|
31082
|
-
return files.some((f) => CLIENT_MODULE.test(f) || /\baxios\.create\s*\(/.test(
|
|
31542
|
+
function hasSharedClient(files, read3) {
|
|
31543
|
+
return files.some((f) => CLIENT_MODULE.test(f) || /\baxios\.create\s*\(/.test(read3(f) ?? ""));
|
|
31083
31544
|
}
|
|
31084
31545
|
function scatteredFetches(texts) {
|
|
31085
31546
|
const byTarget = /* @__PURE__ */ new Map();
|
|
@@ -31096,7 +31557,7 @@ function scatteredFetches(texts) {
|
|
|
31096
31557
|
}
|
|
31097
31558
|
|
|
31098
31559
|
// src/checks/spare-copies.ts
|
|
31099
|
-
import
|
|
31560
|
+
import path38 from "path";
|
|
31100
31561
|
var COPY_PATTERNS = [
|
|
31101
31562
|
/^(.+?)[ ._-]copy(?:[ _-]?\d+)?$/i,
|
|
31102
31563
|
// Button copy, Button-copy, Button.copy, Button copy 2
|
|
@@ -31129,9 +31590,9 @@ function findSpareCopies(files) {
|
|
|
31129
31590
|
continue;
|
|
31130
31591
|
}
|
|
31131
31592
|
if (!isSourceFile(f)) continue;
|
|
31132
|
-
const ext =
|
|
31133
|
-
const dir =
|
|
31134
|
-
const stem =
|
|
31593
|
+
const ext = path38.posix.extname(f);
|
|
31594
|
+
const dir = path38.posix.dirname(f);
|
|
31595
|
+
const stem = path38.posix.basename(f, ext);
|
|
31135
31596
|
for (const re of COPY_PATTERNS) {
|
|
31136
31597
|
const base2 = stem.match(re)?.[1];
|
|
31137
31598
|
if (!base2) continue;
|
|
@@ -31151,7 +31612,7 @@ var DEV_ONLY = /(?:^|\/)(?:scripts?|cypress|mocks|__mocks__|\.storybook|playwrig
|
|
|
31151
31612
|
var CONFIG = /(?:^|\/)(?:[^/]+\.config\.[cm]?[jt]s|config\.[cm]?[jt]s|env\.[cm]?[jt]s)$|(?:^|\/)config\//;
|
|
31152
31613
|
var MAX_LOCATIONS = 20;
|
|
31153
31614
|
var MAX_PER_KIND = 20;
|
|
31154
|
-
var
|
|
31615
|
+
var list10 = (files, max = 10) => `${files.slice(0, max).join(", ")}${files.length > max ? ` and ${files.length - max} more` : ""}`;
|
|
31155
31616
|
var locations2 = (files) => files.slice(0, MAX_LOCATIONS).map((file2) => ({ file: file2 }));
|
|
31156
31617
|
var reuse = {
|
|
31157
31618
|
id: "structure.reuse",
|
|
@@ -31175,7 +31636,7 @@ var reuse = {
|
|
|
31175
31636
|
facts: { files: component.length, loop: loop.length },
|
|
31176
31637
|
key: component.join("|"),
|
|
31177
31638
|
locations: locations2(loop),
|
|
31178
|
-
vars: { loop: [...loop, loop[0]].join(" \u2192 "), files:
|
|
31639
|
+
vars: { loop: [...loop, loop[0]].join(" \u2192 "), files: list10(component) },
|
|
31179
31640
|
meta: { kind: "cycle", files: component, cycle: loop }
|
|
31180
31641
|
});
|
|
31181
31642
|
}
|
|
@@ -31192,11 +31653,11 @@ var reuse = {
|
|
|
31192
31653
|
locations: locations2(original ? [copy, original] : [copy]),
|
|
31193
31654
|
vars: {
|
|
31194
31655
|
copy,
|
|
31195
|
-
copyName:
|
|
31196
|
-
copyImporters:
|
|
31656
|
+
copyName: path39.posix.basename(copy),
|
|
31657
|
+
copyImporters: list10(importersOf(copy), 5),
|
|
31197
31658
|
original: original ?? "",
|
|
31198
|
-
originalName: original ?
|
|
31199
|
-
originalImporters: original ?
|
|
31659
|
+
originalName: original ? path39.posix.basename(original) : "",
|
|
31660
|
+
originalImporters: original ? list10(importersOf(original), 5) : ""
|
|
31200
31661
|
},
|
|
31201
31662
|
meta: {
|
|
31202
31663
|
kind: "spare-copy",
|
|
@@ -31211,11 +31672,11 @@ var reuse = {
|
|
|
31211
31672
|
const localhost = [];
|
|
31212
31673
|
for (const [file2, text] of texts) {
|
|
31213
31674
|
if (CONFIG.test(file2) || DEV_ONLY.test(file2)) continue;
|
|
31214
|
-
const
|
|
31675
|
+
const code2 = stripComments2(text);
|
|
31215
31676
|
let local = null;
|
|
31216
31677
|
for (const hit of findUrls(text)) {
|
|
31217
31678
|
if (LOCAL_HOSTS.has(hit.host)) {
|
|
31218
|
-
if (!local && !RUNS_LOCAL_SERVER.test(
|
|
31679
|
+
if (!local && !RUNS_LOCAL_SERVER.test(code2) && !isDevOnlyUse(code2, hit.index)) local = hit.url;
|
|
31219
31680
|
continue;
|
|
31220
31681
|
}
|
|
31221
31682
|
if (isIgnoredHost(hit.host)) continue;
|
|
@@ -31231,7 +31692,7 @@ var reuse = {
|
|
|
31231
31692
|
facts: { files: files.length },
|
|
31232
31693
|
key: `host:${host}`,
|
|
31233
31694
|
locations: locations2(files),
|
|
31234
|
-
vars: { host, files:
|
|
31695
|
+
vars: { host, files: list10(files) },
|
|
31235
31696
|
meta: { kind: "hardcoded-host", host, files }
|
|
31236
31697
|
});
|
|
31237
31698
|
}
|
|
@@ -31247,7 +31708,7 @@ var reuse = {
|
|
|
31247
31708
|
key: "scattered-fetch",
|
|
31248
31709
|
locations: locations2(files),
|
|
31249
31710
|
vars: {
|
|
31250
|
-
targets: scattered.slice(0, 5).map((s) => `${s.target} (${
|
|
31711
|
+
targets: scattered.slice(0, 5).map((s) => `${s.target} (${list10(s.files, 5)})`).join("; ")
|
|
31251
31712
|
},
|
|
31252
31713
|
meta: { kind: "scattered-fetch", targets: scattered.map((s) => s.target), files }
|
|
31253
31714
|
});
|
|
@@ -31257,7 +31718,7 @@ var reuse = {
|
|
|
31257
31718
|
};
|
|
31258
31719
|
|
|
31259
31720
|
// src/checks/theme.ts
|
|
31260
|
-
import
|
|
31721
|
+
import path40 from "path";
|
|
31261
31722
|
|
|
31262
31723
|
// src/checks/colour.ts
|
|
31263
31724
|
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
@@ -31639,7 +32100,7 @@ function contrastProblems(tokens, theme2) {
|
|
|
31639
32100
|
// src/checks/theme.ts
|
|
31640
32101
|
var MAX_LISTED9 = 20;
|
|
31641
32102
|
function topFolder2(file2) {
|
|
31642
|
-
const dir =
|
|
32103
|
+
const dir = path40.posix.dirname(file2);
|
|
31643
32104
|
return dir === "." ? "(top level)" : dir;
|
|
31644
32105
|
}
|
|
31645
32106
|
var theme = {
|
|
@@ -32048,13 +32509,13 @@ var deadFiles = {
|
|
|
32048
32509
|
const facts = { sourceFiles: appSource2(ctx).length };
|
|
32049
32510
|
const engine = "builtin import graph";
|
|
32050
32511
|
if (dead.length === 0) return { facts, items: [], engine };
|
|
32051
|
-
const
|
|
32512
|
+
const list11 = `${dead.slice(0, 15).join(", ")}${dead.length > 15 ? ` and ${dead.length - 15} more` : ""}`;
|
|
32052
32513
|
const item = {
|
|
32053
32514
|
kind: "unused-files",
|
|
32054
32515
|
facts: { files: dead.length },
|
|
32055
32516
|
key: "dead",
|
|
32056
32517
|
locations: dead.map((file2) => ({ file: file2 })),
|
|
32057
|
-
vars: { list:
|
|
32518
|
+
vars: { list: list11 },
|
|
32058
32519
|
meta: { files: dead }
|
|
32059
32520
|
};
|
|
32060
32521
|
return { facts, items: [item], engine };
|
|
@@ -32115,7 +32576,7 @@ var duplicates = {
|
|
|
32115
32576
|
facts: { similarity: Math.round(sim * 100) },
|
|
32116
32577
|
key: [a, b].sort().join("|"),
|
|
32117
32578
|
locations: [{ file: a }, { file: b }],
|
|
32118
|
-
vars: { nameA:
|
|
32579
|
+
vars: { nameA: path41.basename(a), nameB: path41.basename(b), fileA: a, fileB: b },
|
|
32119
32580
|
meta: { similarity: Math.round(sim * 100) }
|
|
32120
32581
|
}));
|
|
32121
32582
|
return { facts: {}, items };
|
|
@@ -32123,7 +32584,7 @@ var duplicates = {
|
|
|
32123
32584
|
};
|
|
32124
32585
|
var ROOT_ALLOWED = /^(?:[^/]+\.config\.[cm]?[jt]s|middleware\.[cm]?[jt]s|proxy\.[cm]?[jt]s|instrumentation(?:-client)?\.[cm]?[jt]s|next-env\.d\.ts|global\.d\.ts|env\.d\.ts|sentry\.[\w.]+\.[cm]?[jt]s)$/;
|
|
32125
32586
|
function suggestTarget(file2, srcDir) {
|
|
32126
|
-
const base2 =
|
|
32587
|
+
const base2 = path41.posix.basename(file2);
|
|
32127
32588
|
const prefix = srcDir ? "src/" : "";
|
|
32128
32589
|
const isComponent = /^[A-Z][A-Za-z0-9]*\.(?:tsx|jsx)$/.test(base2);
|
|
32129
32590
|
if (isComponent) return `${prefix}components/${base2}`;
|
|
@@ -32279,10 +32740,10 @@ async function observeCheck(check2, ctx, at) {
|
|
|
32279
32740
|
}
|
|
32280
32741
|
|
|
32281
32742
|
// src/api/fill.ts
|
|
32282
|
-
var
|
|
32743
|
+
var PLACEHOLDER3 = /\{([a-zA-Z]+)\}/g;
|
|
32283
32744
|
function fill2(template, vars) {
|
|
32284
32745
|
let missing = false;
|
|
32285
|
-
const text = template.replace(
|
|
32746
|
+
const text = template.replace(PLACEHOLDER3, (_, name) => {
|
|
32286
32747
|
const value = Object.hasOwn(vars, name) ? vars[name] : void 0;
|
|
32287
32748
|
if (value !== void 0) return value;
|
|
32288
32749
|
missing = true;
|
|
@@ -32410,7 +32871,7 @@ function unscored(scan) {
|
|
|
32410
32871
|
}
|
|
32411
32872
|
function loadCache2(root) {
|
|
32412
32873
|
try {
|
|
32413
|
-
return JSON.parse(
|
|
32874
|
+
return JSON.parse(readFileSync19(auraPath(root, CACHE), "utf8"));
|
|
32414
32875
|
} catch {
|
|
32415
32876
|
return {};
|
|
32416
32877
|
}
|
|
@@ -32541,7 +33002,7 @@ async function buildContext(root, options, config2) {
|
|
|
32541
33002
|
const allTracked = await listTrackedFiles(root);
|
|
32542
33003
|
const tracked = allTracked ? new Set([...allTracked].filter((f) => !ignores.some((re) => re.test(f)))) : null;
|
|
32543
33004
|
const cache3 = /* @__PURE__ */ new Map();
|
|
32544
|
-
const
|
|
33005
|
+
const read3 = (f) => {
|
|
32545
33006
|
const cached2 = cache3.get(f);
|
|
32546
33007
|
if (cached2 !== void 0) return cached2;
|
|
32547
33008
|
const text = readText(root, f);
|
|
@@ -32549,10 +33010,10 @@ async function buildContext(root, options, config2) {
|
|
|
32549
33010
|
return text;
|
|
32550
33011
|
};
|
|
32551
33012
|
const pkg2 = readJson(root, "package.json");
|
|
32552
|
-
const stack = detectStack(root, files, pkg2,
|
|
32553
|
-
const projects = detectProjects(root, files,
|
|
33013
|
+
const stack = detectStack(root, files, pkg2, read3);
|
|
33014
|
+
const projects = detectProjects(root, files, read3, cfg.projects);
|
|
32554
33015
|
const project = combineProfiles(projects.map((p2) => p2.profile));
|
|
32555
|
-
return { root, files, tracked, read:
|
|
33016
|
+
return { root, files, tracked, read: read3, pkg: pkg2, stack, projects, profile: repoProfile(projects), project, config: cfg, options };
|
|
32556
33017
|
}
|
|
32557
33018
|
function applyIgnores(result, config2) {
|
|
32558
33019
|
if (config2.ignore.includes(result.checkId)) {
|
|
@@ -32614,9 +33075,9 @@ async function runScan(root, options, onProgress) {
|
|
|
32614
33075
|
}
|
|
32615
33076
|
async function latestScan(root) {
|
|
32616
33077
|
const file2 = auraPath(root, "scan.json");
|
|
32617
|
-
if (
|
|
33078
|
+
if (existsSync23(file2)) {
|
|
32618
33079
|
try {
|
|
32619
|
-
const scan2 = JSON.parse(
|
|
33080
|
+
const scan2 = JSON.parse(readFileSync20(file2, "utf8"));
|
|
32620
33081
|
const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
32621
33082
|
const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
32622
33083
|
if (scan2.commit === head && !dirty && Date.now() - Date.parse(scan2.scannedAt) < 24 * 36e5) return scan2;
|
|
@@ -32629,21 +33090,21 @@ async function latestScan(root) {
|
|
|
32629
33090
|
}
|
|
32630
33091
|
|
|
32631
33092
|
// src/commands/fix.ts
|
|
32632
|
-
import { existsSync as
|
|
32633
|
-
import
|
|
33093
|
+
import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync5 } from "fs";
|
|
33094
|
+
import path43 from "path";
|
|
32634
33095
|
|
|
32635
33096
|
// src/agent/tools.ts
|
|
32636
|
-
import { existsSync as
|
|
32637
|
-
import
|
|
33097
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync4, readFileSync as readFileSync21, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
33098
|
+
import path42 from "path";
|
|
32638
33099
|
var MAX_OUTPUT = 12e3;
|
|
32639
33100
|
var MAX_FILE_LINES = 800;
|
|
32640
33101
|
var FORBIDDEN = /^(?:\.git|node_modules|\.next|\.vercel)(?:\/|$)|(?:^|\/)\.env(?:\.(?!example$|sample$|template$)[^/]+)?$/;
|
|
32641
33102
|
function safePath(root, rel) {
|
|
32642
33103
|
if (typeof rel !== "string" || !rel.trim()) throw new Error("A file path is required.");
|
|
32643
|
-
const normalized =
|
|
32644
|
-
if (normalized.startsWith("../") || normalized === ".." ||
|
|
33104
|
+
const normalized = path42.posix.normalize(rel.replace(/\\/g, "/").replace(/^\.\//, ""));
|
|
33105
|
+
if (normalized.startsWith("../") || normalized === ".." || path42.isAbsolute(normalized)) throw new Error(`Path is outside the repo: ${rel}`);
|
|
32645
33106
|
if (FORBIDDEN.test(normalized)) throw new Error(`Aura doesn\u2019t let the agent touch ${normalized}.`);
|
|
32646
|
-
const full =
|
|
33107
|
+
const full = path42.join(root, normalized);
|
|
32647
33108
|
if (!isInside(root, full)) throw new Error(`Path is outside the repo: ${rel}`);
|
|
32648
33109
|
return normalized;
|
|
32649
33110
|
}
|
|
@@ -32664,10 +33125,10 @@ async function runTool(root, call, allowedCommands2) {
|
|
|
32664
33125
|
}
|
|
32665
33126
|
case "read_file": {
|
|
32666
33127
|
const rel = safePath(root, input2.path);
|
|
32667
|
-
const full =
|
|
32668
|
-
if (!
|
|
33128
|
+
const full = path42.join(root, rel);
|
|
33129
|
+
if (!existsSync24(full)) return { output: `File not found: ${rel}` };
|
|
32669
33130
|
if (statSync5(full).size > 1e6) return { output: `${rel} is too large to read.` };
|
|
32670
|
-
const lines =
|
|
33131
|
+
const lines = readFileSync21(full, "utf8").split("\n");
|
|
32671
33132
|
const start = Math.max(1, Number(input2.startLine) || 1);
|
|
32672
33133
|
const end = Math.min(lines.length, Number(input2.endLine) || start + MAX_FILE_LINES - 1);
|
|
32673
33134
|
const body = lines.slice(start - 1, end).map((l, i) => `${start + i}: ${l}`).join("\n");
|
|
@@ -32684,7 +33145,7 @@ async function runTool(root, call, allowedCommands2) {
|
|
|
32684
33145
|
if (FORBIDDEN.test(f) || glob && !glob.test(f)) continue;
|
|
32685
33146
|
let text;
|
|
32686
33147
|
try {
|
|
32687
|
-
text =
|
|
33148
|
+
text = readFileSync21(path42.join(root, f), "utf8");
|
|
32688
33149
|
} catch {
|
|
32689
33150
|
continue;
|
|
32690
33151
|
}
|
|
@@ -32703,14 +33164,14 @@ async function runTool(root, call, allowedCommands2) {
|
|
|
32703
33164
|
output: "Refused: the content contains a [REDACTED:\u2026] placeholder. Secrets are hidden from you on purpose. Read the value from an environment variable (process.env.NAME) instead of writing it."
|
|
32704
33165
|
};
|
|
32705
33166
|
}
|
|
32706
|
-
mkdirSync4(
|
|
32707
|
-
writeFileSync4(
|
|
33167
|
+
mkdirSync4(path42.dirname(path42.join(root, rel)), { recursive: true });
|
|
33168
|
+
writeFileSync4(path42.join(root, rel), content2);
|
|
32708
33169
|
return { output: `Wrote ${rel} (${content2.split("\n").length} lines).`, touched: rel };
|
|
32709
33170
|
}
|
|
32710
33171
|
case "delete_file": {
|
|
32711
33172
|
const rel = safePath(root, input2.path);
|
|
32712
|
-
if (!
|
|
32713
|
-
rmSync4(
|
|
33173
|
+
if (!existsSync24(path42.join(root, rel))) return { output: `File not found: ${rel}` };
|
|
33174
|
+
rmSync4(path42.join(root, rel));
|
|
32714
33175
|
return { output: `Deleted ${rel}.`, touched: rel };
|
|
32715
33176
|
}
|
|
32716
33177
|
case "run_command": {
|
|
@@ -32726,15 +33187,15 @@ ${r.stderr}`.trim().slice(-6e3)}`;
|
|
|
32726
33187
|
}
|
|
32727
33188
|
case "finish": {
|
|
32728
33189
|
const summary = String(input2.summary ?? "");
|
|
32729
|
-
const
|
|
32730
|
-
return { output: "Finished.", finish: { summary, testsRun:
|
|
33190
|
+
const list11 = (v) => Array.isArray(v) ? v.map(String) : [];
|
|
33191
|
+
return { output: "Finished.", finish: { summary, testsRun: list11(input2.testsRun), risks: list11(input2.risks) } };
|
|
32731
33192
|
}
|
|
32732
33193
|
}
|
|
32733
33194
|
}
|
|
32734
33195
|
|
|
32735
33196
|
// src/api/explain.ts
|
|
32736
33197
|
import { createHash as createHash5 } from "crypto";
|
|
32737
|
-
import { existsSync as
|
|
33198
|
+
import { existsSync as existsSync25, readFileSync as readFileSync22 } from "fs";
|
|
32738
33199
|
|
|
32739
33200
|
// src/report.ts
|
|
32740
33201
|
import pc3 from "picocolors";
|
|
@@ -33019,7 +33480,7 @@ var CACHE_FILE2 = "cache/explanations.json";
|
|
|
33019
33480
|
var MAX_EXPLAIN = 25;
|
|
33020
33481
|
function loadCache3(root) {
|
|
33021
33482
|
try {
|
|
33022
|
-
return JSON.parse(
|
|
33483
|
+
return JSON.parse(readFileSync22(auraPath(root, CACHE_FILE2), "utf8"));
|
|
33023
33484
|
} catch {
|
|
33024
33485
|
return {};
|
|
33025
33486
|
}
|
|
@@ -33040,8 +33501,8 @@ function excerptFor(root, f, context = 4) {
|
|
|
33040
33501
|
}
|
|
33041
33502
|
function productContext(root) {
|
|
33042
33503
|
const file2 = auraPath(root, "brief.md");
|
|
33043
|
-
if (!
|
|
33044
|
-
return redact(
|
|
33504
|
+
if (!existsSync25(file2)) return void 0;
|
|
33505
|
+
return redact(readFileSync22(file2, "utf8")).slice(0, 1800);
|
|
33045
33506
|
}
|
|
33046
33507
|
async function addExplanations(root, scan, client) {
|
|
33047
33508
|
const cache3 = loadCache3(root);
|
|
@@ -33098,7 +33559,7 @@ function allowedCommands(root, pm, typescript) {
|
|
|
33098
33559
|
const pkg2 = readJson(root, "package.json");
|
|
33099
33560
|
const run2 = pmCommands(pm).run;
|
|
33100
33561
|
const cmds = ["test", "lint", "typecheck", "build"].filter((s) => pkg2?.scripts?.[s]).map((s) => `${run2} ${s}`);
|
|
33101
|
-
if (typescript && !pkg2?.scripts?.typecheck &&
|
|
33562
|
+
if (typescript && !pkg2?.scripts?.typecheck && existsSync26(path43.join(root, "node_modules/.bin/tsc"))) cmds.push(`${pmCommands(pm).exec} tsc --noEmit`);
|
|
33102
33563
|
return cmds;
|
|
33103
33564
|
}
|
|
33104
33565
|
function taskFor(root, scan, action2, findings) {
|
|
@@ -33169,8 +33630,8 @@ async function untrackEnvFiles(root, findings) {
|
|
|
33169
33630
|
const files = findings.filter((f) => f.checkId === "security.env-files").flatMap((f) => f.locations.slice(0, 1).map((l) => l.file));
|
|
33170
33631
|
for (const f of files) await git(root, "rm", "-q", "--cached", "--", f);
|
|
33171
33632
|
if (files.length) {
|
|
33172
|
-
const gi =
|
|
33173
|
-
const current2 =
|
|
33633
|
+
const gi = path43.join(root, ".gitignore");
|
|
33634
|
+
const current2 = existsSync26(gi) ? readFileSync23(gi, "utf8") : "";
|
|
33174
33635
|
if (!/^\.env\*?$/m.test(current2)) writeFileSync5(gi, `${current2.trimEnd()}
|
|
33175
33636
|
|
|
33176
33637
|
# Environment files hold secrets
|
|
@@ -33185,10 +33646,10 @@ async function fixCommand(target, flags) {
|
|
|
33185
33646
|
const started = Date.now();
|
|
33186
33647
|
const report = { delivery: flags.agent ? "agent-task" : "agent-fix" };
|
|
33187
33648
|
const actionId = { value: target };
|
|
33188
|
-
const
|
|
33649
|
+
const code2 = await fixAction(target, flags, report, actionId);
|
|
33189
33650
|
const root = await findRepoRoot();
|
|
33190
33651
|
await recordAction(root, loadConfig(root), actionId.value, report, started, flags.via ?? "command");
|
|
33191
|
-
return
|
|
33652
|
+
return code2;
|
|
33192
33653
|
}
|
|
33193
33654
|
async function fixAction(target, flags, report, resolvedId) {
|
|
33194
33655
|
const root = await findRepoRoot();
|
|
@@ -33402,25 +33863,25 @@ The plan is in ${large.file}.`);
|
|
|
33402
33863
|
}
|
|
33403
33864
|
|
|
33404
33865
|
// src/commands/inventory.ts
|
|
33405
|
-
import { readFileSync as
|
|
33866
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
33406
33867
|
function accountState(root, services) {
|
|
33407
33868
|
const inv = loadInventory(root);
|
|
33408
|
-
const
|
|
33869
|
+
const read3 = Object.fromEntries(
|
|
33409
33870
|
Object.entries(loadSnapshots(root)).map(([id, snap]) => [id, Object.values(snap.settings).every((state) => state !== "off")])
|
|
33410
33871
|
);
|
|
33411
33872
|
return {
|
|
33412
33873
|
inventoryComplete: services.every((s) => inv.services[s]?.owner) && Boolean(inv.githubOrg),
|
|
33413
33874
|
secured: Object.fromEntries(
|
|
33414
|
-
[.../* @__PURE__ */ new Set([...Object.keys(
|
|
33875
|
+
[.../* @__PURE__ */ new Set([...Object.keys(read3), ...Object.keys(inv.secured ?? {}), "github"])].map((id) => [
|
|
33415
33876
|
id,
|
|
33416
|
-
Boolean(
|
|
33877
|
+
Boolean(read3[id] || inv.secured?.[id] || id === "github" && inv.githubSecurity)
|
|
33417
33878
|
])
|
|
33418
33879
|
)
|
|
33419
33880
|
};
|
|
33420
33881
|
}
|
|
33421
33882
|
function loadInventory(root) {
|
|
33422
33883
|
try {
|
|
33423
|
-
return JSON.parse(
|
|
33884
|
+
return JSON.parse(readFileSync24(auraPath(root, "inventory.json"), "utf8"));
|
|
33424
33885
|
} catch {
|
|
33425
33886
|
return { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), services: {} };
|
|
33426
33887
|
}
|
|
@@ -33655,13 +34116,13 @@ ${pc.bold(" Left for you")}`);
|
|
|
33655
34116
|
}
|
|
33656
34117
|
|
|
33657
34118
|
// src/commands/guide.ts
|
|
33658
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
33659
|
-
import
|
|
34119
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync25 } from "fs";
|
|
34120
|
+
import path44 from "path";
|
|
33660
34121
|
import YAML6 from "yaml";
|
|
33661
34122
|
function loadGuides() {
|
|
33662
|
-
const dir =
|
|
34123
|
+
const dir = path44.join(templatesDir(), "guides");
|
|
33663
34124
|
return readdirSync6(dir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
33664
|
-
const text =
|
|
34125
|
+
const text = readFileSync25(path44.join(dir, f), "utf8");
|
|
33665
34126
|
const fm = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
33666
34127
|
const yaml = fm?.[1];
|
|
33667
34128
|
const meta3 = yaml !== void 0 ? YAML6.parse(yaml) : {};
|
|
@@ -33694,10 +34155,10 @@ var LOCKFILES2 = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"
|
|
|
33694
34155
|
async function applyCommand(actionId, flags) {
|
|
33695
34156
|
const started = Date.now();
|
|
33696
34157
|
const report = { delivery: "pr" };
|
|
33697
|
-
const
|
|
34158
|
+
const code2 = await applyAction(actionId, flags, report);
|
|
33698
34159
|
const root = await findRepoRoot();
|
|
33699
34160
|
await recordAction(root, loadConfig(root), actionId, report, started, flags.via ?? "command");
|
|
33700
|
-
return
|
|
34161
|
+
return code2;
|
|
33701
34162
|
}
|
|
33702
34163
|
async function applyAction(actionId, flags, report) {
|
|
33703
34164
|
const root = await findRepoRoot();
|
|
@@ -33714,7 +34175,9 @@ async function applyAction(actionId, flags, report) {
|
|
|
33714
34175
|
if (delivery === "agent-fix") {
|
|
33715
34176
|
if (fellBack)
|
|
33716
34177
|
say.info(
|
|
33717
|
-
pc.dim(
|
|
34178
|
+
pc.dim(
|
|
34179
|
+
`Aura\u2019s template for "${actionId}" doesn\u2019t fit this project (templates cover a Next.js app, or a Vite + React app, at the repo root). Handing it to Aura\u2019s agent instead.`
|
|
34180
|
+
)
|
|
33718
34181
|
);
|
|
33719
34182
|
return fixCommand(actionId, { yes: flags.yes, dryRun: flags.dryRun, pr: flags.pr, approved: flags.approved, via: flags.via });
|
|
33720
34183
|
}
|
|
@@ -33764,7 +34227,7 @@ async function applyAction(actionId, flags, report) {
|
|
|
33764
34227
|
console.log(`
|
|
33765
34228
|
${pc.bold(title2)}`);
|
|
33766
34229
|
for (const c of changes.changes)
|
|
33767
|
-
console.log(` ${c.content === null ? pc.red("delete") :
|
|
34230
|
+
console.log(` ${c.content === null ? pc.red("delete") : existsSync27(path45.join(root, c.path)) ? pc.yellow("update") : pc.green("add ")} ${c.path}`);
|
|
33768
34231
|
if (changes.devDeps?.length) console.log(` ${pc.cyan("install")} ${changes.devDeps.join(", ")} ${pc.dim(`(${pm.addDev})`)}`);
|
|
33769
34232
|
for (const cmd of changes.commands ?? []) console.log(` ${pc.cyan("run ")} ${cmd.join(" ")}`);
|
|
33770
34233
|
};
|
|
@@ -33820,8 +34283,8 @@ The plan is in ${large.file}.`);
|
|
|
33820
34283
|
return 0;
|
|
33821
34284
|
}
|
|
33822
34285
|
const chunkPlan = plan;
|
|
33823
|
-
const tsc =
|
|
33824
|
-
const checkTypes = plan.verifyTypes && scan.stack.typescript &&
|
|
34286
|
+
const tsc = path45.join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
|
|
34287
|
+
const checkTypes = plan.verifyTypes && scan.stack.typescript && existsSync27(tsc);
|
|
33825
34288
|
const typeErrors = async () => {
|
|
33826
34289
|
const r = await run(tsc, ["--noEmit", "--pretty", "false"], { cwd: root, timeoutMs: 3e5 });
|
|
33827
34290
|
return (r.stdout.match(/error TS\d+/g) ?? []).length;
|
|
@@ -33936,17 +34399,17 @@ async function undoPlan(root, plan) {
|
|
|
33936
34399
|
for (const file2 of touched) {
|
|
33937
34400
|
const inHead = (await git(root, "cat-file", "-e", `HEAD:${file2}`)).code === 0;
|
|
33938
34401
|
if (inHead) await git(root, "checkout", "HEAD", "--", file2);
|
|
33939
|
-
else if (plan.changes.some((c) => c.path === file2)) rmSync5(
|
|
34402
|
+
else if (plan.changes.some((c) => c.path === file2)) rmSync5(path45.join(root, file2), { force: true });
|
|
33940
34403
|
}
|
|
33941
34404
|
}
|
|
33942
34405
|
|
|
33943
34406
|
// src/commands/init.ts
|
|
33944
|
-
import { existsSync as
|
|
34407
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29 } from "fs";
|
|
33945
34408
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
33946
|
-
import
|
|
34409
|
+
import path50 from "path";
|
|
33947
34410
|
|
|
33948
34411
|
// src/brief.ts
|
|
33949
|
-
import
|
|
34412
|
+
import path46 from "path";
|
|
33950
34413
|
function repoSummary(ctx) {
|
|
33951
34414
|
const routes2 = ctx.files.filter((f) => /^(?:src\/)?app\/(?:.+\/)?(?:page|route)\.[cm]?[jt]sx?$/.test(f) || /^(?:src\/)?pages\/.+\.[cm]?[jt]sx?$/.test(f)).map(
|
|
33952
34415
|
(f) => "/" + f.replace(/^(?:src\/)?(?:app|pages)\//, "").replace(/(?:^|\/)(?:page|route|index)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/, "").replace(/\([^)]+\)\//g, "")
|
|
@@ -33963,7 +34426,7 @@ function repoSummary(ctx) {
|
|
|
33963
34426
|
const readme2 = ctx.files.find((f) => /^readme\.mdx?$/i.test(f));
|
|
33964
34427
|
const readmeText = readme2 ? ctx.read(readme2) ?? "" : "";
|
|
33965
34428
|
return {
|
|
33966
|
-
name: ctx.pkg?.name ??
|
|
34429
|
+
name: ctx.pkg?.name ?? path46.basename(ctx.root),
|
|
33967
34430
|
packageJson: ctx.pkg ? {
|
|
33968
34431
|
name: ctx.pkg.name,
|
|
33969
34432
|
description: ctx.pkg.description,
|
|
@@ -34062,10 +34525,10 @@ function banner(colors = pc4.isColorSupported) {
|
|
|
34062
34525
|
// src/api/connect.ts
|
|
34063
34526
|
import { spawn as spawn2 } from "child_process";
|
|
34064
34527
|
import { constants, generateKeyPairSync, privateDecrypt } from "crypto";
|
|
34065
|
-
import { existsSync as
|
|
34528
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync5, readFileSync as readFileSync26, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
34066
34529
|
import os5 from "os";
|
|
34067
|
-
import
|
|
34068
|
-
var pendingFile = () =>
|
|
34530
|
+
import path47 from "path";
|
|
34531
|
+
var pendingFile = () => path47.join(os5.homedir(), ".aura", "pending-connect.json");
|
|
34069
34532
|
async function postJson(apiUrl, route, body) {
|
|
34070
34533
|
let res;
|
|
34071
34534
|
try {
|
|
@@ -34084,14 +34547,14 @@ async function postJson(apiUrl, route, body) {
|
|
|
34084
34547
|
}
|
|
34085
34548
|
function loadPending(apiUrl, now) {
|
|
34086
34549
|
try {
|
|
34087
|
-
const p2 = JSON.parse(
|
|
34550
|
+
const p2 = JSON.parse(readFileSync26(pendingFile(), "utf8"));
|
|
34088
34551
|
if (p2.apiUrl === apiUrl && Date.parse(p2.expiresAt) - now > 6e4) return p2;
|
|
34089
34552
|
} catch {
|
|
34090
34553
|
}
|
|
34091
34554
|
return null;
|
|
34092
34555
|
}
|
|
34093
34556
|
function clearPendingConnect() {
|
|
34094
|
-
if (
|
|
34557
|
+
if (existsSync28(pendingFile())) rmSync6(pendingFile(), { force: true });
|
|
34095
34558
|
}
|
|
34096
34559
|
async function startOrResumeConnect(apiUrl, now = Date.now()) {
|
|
34097
34560
|
const existing = loadPending(apiUrl, now);
|
|
@@ -34103,7 +34566,7 @@ async function startOrResumeConnect(apiUrl, now = Date.now()) {
|
|
|
34103
34566
|
});
|
|
34104
34567
|
const res = ConnectStartResponse.parse(await postJson(apiUrl, "connect", { publicKey }));
|
|
34105
34568
|
const pending2 = { apiUrl, privateKey, ...res };
|
|
34106
|
-
mkdirSync5(
|
|
34569
|
+
mkdirSync5(path47.dirname(pendingFile()), { recursive: true });
|
|
34107
34570
|
writeFileSync6(pendingFile(), `${JSON.stringify(pending2, null, 2)}
|
|
34108
34571
|
`, { mode: 384 });
|
|
34109
34572
|
return { ...pending2, resumed: false };
|
|
@@ -34256,9 +34719,9 @@ function installFallback(result, platform = process.platform) {
|
|
|
34256
34719
|
// src/core/claude-plugin.ts
|
|
34257
34720
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
34258
34721
|
import os6 from "os";
|
|
34259
|
-
import
|
|
34722
|
+
import path48 from "path";
|
|
34260
34723
|
var CLAUDE_PLUGIN_ID = "aura@aura";
|
|
34261
|
-
var marketplaceDir = () =>
|
|
34724
|
+
var marketplaceDir = () => path48.join(os6.homedir(), ".aura", "claude-marketplace");
|
|
34262
34725
|
var MARKETPLACE = {
|
|
34263
34726
|
name: "aura",
|
|
34264
34727
|
description: "Aura for Claude Code",
|
|
@@ -34273,8 +34736,8 @@ var MARKETPLACE = {
|
|
|
34273
34736
|
};
|
|
34274
34737
|
function writeMarketplace() {
|
|
34275
34738
|
const dir = marketplaceDir();
|
|
34276
|
-
mkdirSync6(
|
|
34277
|
-
writeFileSync7(
|
|
34739
|
+
mkdirSync6(path48.join(dir, ".claude-plugin"), { recursive: true });
|
|
34740
|
+
writeFileSync7(path48.join(dir, ".claude-plugin", "marketplace.json"), `${JSON.stringify(MARKETPLACE, null, 2)}
|
|
34278
34741
|
`);
|
|
34279
34742
|
return dir;
|
|
34280
34743
|
}
|
|
@@ -34330,7 +34793,7 @@ var AUTH_PACKAGES = [
|
|
|
34330
34793
|
[/^@kinde-oss\//, "Kinde"],
|
|
34331
34794
|
[/^@auth0\//, "Auth0"]
|
|
34332
34795
|
];
|
|
34333
|
-
var words = (
|
|
34796
|
+
var words = (list11) => new RegExp(`(?<![a-z])(?:${list11})(?![a-z])`, "i");
|
|
34334
34797
|
var SENSITIVE = [
|
|
34335
34798
|
["health", words("patients?|diagnos[a-z]*|medical|medications?|prescriptions?|symptoms?|health_records?"), "health"],
|
|
34336
34799
|
["legal", words("case_number|lawsuits?|attorneys?|legal_matters?|court[a-z]*"), "legal"],
|
|
@@ -34340,7 +34803,7 @@ function schemaText(ctx) {
|
|
|
34340
34803
|
const files = ctx.files.filter((f) => /\.sql$|schema\.prisma$|(?:^|\/)schema\.[cm]?[jt]s$|(?:^|\/)db\/schema\//.test(f) && !/node_modules\//.test(f));
|
|
34341
34804
|
return files.slice(0, 200).map((f) => ctx.read(f) ?? "").join("\n");
|
|
34342
34805
|
}
|
|
34343
|
-
var matchPackage = (deps2,
|
|
34806
|
+
var matchPackage = (deps2, list11) => list11.find(([re]) => deps2.some((d) => re.test(d)))?.[1];
|
|
34344
34807
|
function inferAnswers(ctx) {
|
|
34345
34808
|
const deps2 = Object.keys(allDeps(ctx.pkg));
|
|
34346
34809
|
const payment = matchPackage(deps2, PAYMENT_PACKAGES);
|
|
@@ -34391,13 +34854,13 @@ function describeInferred(inferred) {
|
|
|
34391
34854
|
// src/commands/scan.ts
|
|
34392
34855
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
34393
34856
|
import os7 from "os";
|
|
34394
|
-
import
|
|
34857
|
+
import path49 from "path";
|
|
34395
34858
|
|
|
34396
34859
|
// src/history.ts
|
|
34397
|
-
import { readFileSync as
|
|
34860
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
34398
34861
|
function loadHistory(root) {
|
|
34399
34862
|
try {
|
|
34400
|
-
const data = JSON.parse(
|
|
34863
|
+
const data = JSON.parse(readFileSync27(auraPath(root, "history.json"), "utf8"));
|
|
34401
34864
|
return Array.isArray(data) ? data : [];
|
|
34402
34865
|
} catch {
|
|
34403
34866
|
return [];
|
|
@@ -34459,7 +34922,7 @@ async function pickAndRunSteps(steps, limit = 10) {
|
|
|
34459
34922
|
console.log(`
|
|
34460
34923
|
${pc.magenta("\u2726")} ${pc.bold(step.title)}`);
|
|
34461
34924
|
const run2 = stepRun(step, hasKey);
|
|
34462
|
-
const
|
|
34925
|
+
const code2 = run2.kind === "apply" ? (
|
|
34463
34926
|
// Settings changes always ask first, even when picked here; pull requests don't need to.
|
|
34464
34927
|
await applyCommand(run2.actionId, { yes: !connectorForAction(run2.actionId), via: "checklist" })
|
|
34465
34928
|
) : run2.kind === "fix" ? await fixCommand(run2.actionId, run2.withOwnAgent ? { agent: true, via: "checklist" } : { yes: true, via: "checklist" }) : run2.kind === "guide" ? await guideCommand(run2.topic, {}) : await inventoryCommand({});
|
|
@@ -34473,7 +34936,7 @@ ${pc.magenta("\u2726")} ${pc.bold(step.title)}`);
|
|
|
34473
34936
|
outcome: "shown"
|
|
34474
34937
|
});
|
|
34475
34938
|
}
|
|
34476
|
-
results.push({ title: step.title, ok:
|
|
34939
|
+
results.push({ title: step.title, ok: code2 === 0 });
|
|
34477
34940
|
}
|
|
34478
34941
|
if (results.length > 1) {
|
|
34479
34942
|
console.log(`
|
|
@@ -34484,12 +34947,12 @@ ${pc.bold(" Done")}`);
|
|
|
34484
34947
|
}
|
|
34485
34948
|
|
|
34486
34949
|
// src/connectors/refresh.ts
|
|
34487
|
-
import { readFileSync as
|
|
34950
|
+
import { readFileSync as readFileSync28 } from "fs";
|
|
34488
34951
|
async function refreshToolSettings(root) {
|
|
34489
34952
|
if (process.env.AURA_NO_CONNECTORS) return;
|
|
34490
34953
|
let previous = null;
|
|
34491
34954
|
try {
|
|
34492
|
-
previous = JSON.parse(
|
|
34955
|
+
previous = JSON.parse(readFileSync28(auraPath(root, "scan.json"), "utf8"));
|
|
34493
34956
|
} catch {
|
|
34494
34957
|
}
|
|
34495
34958
|
const snapshots = loadSnapshots(root);
|
|
@@ -34513,8 +34976,8 @@ function reportScoring(outcome, quiet, ci) {
|
|
|
34513
34976
|
if (!quiet) say.warn(text);
|
|
34514
34977
|
}
|
|
34515
34978
|
async function scanRef(root, ref, opts) {
|
|
34516
|
-
const dir = mkdtempSync3(
|
|
34517
|
-
const wt =
|
|
34979
|
+
const dir = mkdtempSync3(path49.join(os7.tmpdir(), "aura-base-"));
|
|
34980
|
+
const wt = path49.join(dir, "tree");
|
|
34518
34981
|
const add = await run("git", ["worktree", "add", "--detach", wt, ref], { cwd: root });
|
|
34519
34982
|
if (add.code !== 0) {
|
|
34520
34983
|
rmSync7(dir, { recursive: true, force: true });
|
|
@@ -34617,7 +35080,7 @@ async function scanCommand(flags) {
|
|
|
34617
35080
|
// src/commands/init.ts
|
|
34618
35081
|
var AGENT_IDS2 = ["claude", "codex", "cursor", "pi"];
|
|
34619
35082
|
function detectAgents(root) {
|
|
34620
|
-
return detectAgentsFrom((rel) =>
|
|
35083
|
+
return detectAgentsFrom((rel) => existsSync29(path50.join(root, rel)));
|
|
34621
35084
|
}
|
|
34622
35085
|
function openEditor(file2) {
|
|
34623
35086
|
const fallback = process.platform === "win32" ? "notepad" : spawnSync3("sh", ["-c", "command -v nano"]).status === 0 ? "nano" : "vi";
|
|
@@ -34733,13 +35196,13 @@ async function initCommand(flags) {
|
|
|
34733
35196
|
let brief2 = null;
|
|
34734
35197
|
let keptBrief = false;
|
|
34735
35198
|
if (flags.briefFile) {
|
|
34736
|
-
brief2 =
|
|
34737
|
-
} else if (!flags.answers &&
|
|
35199
|
+
brief2 = readFileSync29(flags.briefFile, "utf8");
|
|
35200
|
+
} else if (!flags.answers && existsSync29(briefPath) && !(tty && unwrap(await p.confirm({ message: "A product brief already exists. Draft a new one?", initialValue: false })))) {
|
|
34738
35201
|
keptBrief = true;
|
|
34739
35202
|
} else {
|
|
34740
35203
|
let answers = null;
|
|
34741
35204
|
if (flags.answers) {
|
|
34742
|
-
answers = OnboardingAnswersSchema.parse(JSON.parse(
|
|
35205
|
+
answers = OnboardingAnswersSchema.parse(JSON.parse(readFileSync29(flags.answers, "utf8")));
|
|
34743
35206
|
} else if (tty) {
|
|
34744
35207
|
const inferred = inferAnswers(ctx);
|
|
34745
35208
|
p.note(describeInferred(inferred).join("\n"), "Here\u2019s what Aura found about your app");
|
|
@@ -34891,7 +35354,7 @@ ${pc.magenta("\u2726")} ${pc.bold("Give the user the aura command")}`);
|
|
|
34891
35354
|
}
|
|
34892
35355
|
|
|
34893
35356
|
// src/commands/summary.ts
|
|
34894
|
-
import
|
|
35357
|
+
import path51 from "path";
|
|
34895
35358
|
async function activity(root, since) {
|
|
34896
35359
|
const log = await git(root, "log", `--since=${since.toISOString()}`, "--no-merges", "--format=%H%x09%an%x09%s");
|
|
34897
35360
|
const commits = log.stdout.split("\n").filter(Boolean).slice(0, 300).map((l) => {
|
|
@@ -34953,7 +35416,7 @@ async function summaryCommand(flags) {
|
|
|
34953
35416
|
const beforeIds = new Set(before ? allFindings(before).map((f) => f.id) : []);
|
|
34954
35417
|
const nowIds = new Set(allFindings(now).map((f) => f.id));
|
|
34955
35418
|
const req = {
|
|
34956
|
-
repo:
|
|
35419
|
+
repo: path51.basename(root),
|
|
34957
35420
|
since: since.toISOString(),
|
|
34958
35421
|
until: until.toISOString(),
|
|
34959
35422
|
product: productContext(root),
|
|
@@ -34998,15 +35461,15 @@ ${body}`);
|
|
|
34998
35461
|
}
|
|
34999
35462
|
|
|
35000
35463
|
// src/commands/update.ts
|
|
35001
|
-
import { existsSync as
|
|
35002
|
-
import
|
|
35464
|
+
import { existsSync as existsSync30, readFileSync as readFileSync31 } from "fs";
|
|
35465
|
+
import path53 from "path";
|
|
35003
35466
|
|
|
35004
35467
|
// src/core/update-check.ts
|
|
35005
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
35468
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync30, writeFileSync as writeFileSync9 } from "fs";
|
|
35006
35469
|
import os8 from "os";
|
|
35007
|
-
import
|
|
35470
|
+
import path52 from "path";
|
|
35008
35471
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
35009
|
-
var cacheFile = () =>
|
|
35472
|
+
var cacheFile = () => path52.join(os8.homedir(), ".aura", "update-check.json");
|
|
35010
35473
|
function isNewer(a, b) {
|
|
35011
35474
|
if (!/^\d+\.\d+\.\d+$/.test(a) || !/^\d+\.\d+\.\d+$/.test(b)) return false;
|
|
35012
35475
|
const x = a.split(".").map(Number);
|
|
@@ -35031,13 +35494,13 @@ async function newerVersion(opts = {}) {
|
|
|
35031
35494
|
const now = opts.now ?? Date.now();
|
|
35032
35495
|
let latest = null;
|
|
35033
35496
|
try {
|
|
35034
|
-
const cached2 = JSON.parse(
|
|
35497
|
+
const cached2 = JSON.parse(readFileSync30(cacheFile(), "utf8"));
|
|
35035
35498
|
if (now - cached2.checkedAt < DAY_MS) latest = cached2.latest;
|
|
35036
35499
|
else throw new Error("stale");
|
|
35037
35500
|
} catch {
|
|
35038
35501
|
latest = await (opts.fetchLatest ?? fetchLatest)();
|
|
35039
35502
|
try {
|
|
35040
|
-
mkdirSync7(
|
|
35503
|
+
mkdirSync7(path52.dirname(cacheFile()), { recursive: true });
|
|
35041
35504
|
writeFileSync9(cacheFile(), JSON.stringify({ checkedAt: now, latest }));
|
|
35042
35505
|
} catch {
|
|
35043
35506
|
}
|
|
@@ -35055,7 +35518,7 @@ function updateNotice(latest, argv1 = process.argv[1] ?? "") {
|
|
|
35055
35518
|
}
|
|
35056
35519
|
|
|
35057
35520
|
// src/commands/update.ts
|
|
35058
|
-
function howAuraRuns(argv1, root, paths =
|
|
35521
|
+
function howAuraRuns(argv1, root, paths = path53) {
|
|
35059
35522
|
if (/[\\/]_npx[\\/]/.test(argv1)) return "npx";
|
|
35060
35523
|
const rel = root && argv1 ? paths.relative(paths.join(root, "node_modules"), argv1) : "..";
|
|
35061
35524
|
return rel.startsWith("..") || paths.isAbsolute(rel) ? "global" : "project";
|
|
@@ -35073,9 +35536,9 @@ function newAura(how, version2) {
|
|
|
35073
35536
|
return ["aura"];
|
|
35074
35537
|
}
|
|
35075
35538
|
function workflowVersion(root) {
|
|
35076
|
-
const file2 =
|
|
35077
|
-
if (!
|
|
35078
|
-
return
|
|
35539
|
+
const file2 = path53.join(root, ".github", "workflows", "aura.yml");
|
|
35540
|
+
if (!existsSync30(file2)) return null;
|
|
35541
|
+
return readFileSync31(file2, "utf8").match(/getaura@(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
35079
35542
|
}
|
|
35080
35543
|
function mentionWorkflow(root, version2) {
|
|
35081
35544
|
const pinned = root ? workflowVersion(root) : null;
|
|
@@ -35083,7 +35546,7 @@ function mentionWorkflow(root, version2) {
|
|
|
35083
35546
|
say.info(`Your GitHub check still runs Aura ${pinned}. To move it to ${version2}, run: aura apply add-aura-workflow (it opens a pull request).`);
|
|
35084
35547
|
}
|
|
35085
35548
|
async function refreshRepoFiles(root) {
|
|
35086
|
-
if (!
|
|
35549
|
+
if (!existsSync30(auraPath(root, "config.json"))) {
|
|
35087
35550
|
say.dim("Aura isn\u2019t set up in this repo, so there\u2019s nothing here to refresh.");
|
|
35088
35551
|
return 0;
|
|
35089
35552
|
}
|
|
@@ -35150,8 +35613,8 @@ async function updateCommand(flags, deps2 = { fetchLatest, run, argv1: process.a
|
|
|
35150
35613
|
}
|
|
35151
35614
|
|
|
35152
35615
|
// src/commands/pr.ts
|
|
35153
|
-
import { existsSync as
|
|
35154
|
-
import
|
|
35616
|
+
import { existsSync as existsSync31, readFileSync as readFileSync32, statSync as statSync6 } from "fs";
|
|
35617
|
+
import path54 from "path";
|
|
35155
35618
|
|
|
35156
35619
|
// src/actions/pr-plan.ts
|
|
35157
35620
|
var PR_MAX_LINES = 400;
|
|
@@ -35348,7 +35811,7 @@ function parseStatus(out) {
|
|
|
35348
35811
|
}
|
|
35349
35812
|
return entries;
|
|
35350
35813
|
}
|
|
35351
|
-
function projectOf(root, file2, exists2 = (rel) =>
|
|
35814
|
+
function projectOf(root, file2, exists2 = (rel) => existsSync31(path54.join(root, rel))) {
|
|
35352
35815
|
const parts = file2.split("/").slice(0, -1);
|
|
35353
35816
|
for (let n = parts.length; n > 0; n--) {
|
|
35354
35817
|
const dir = parts.slice(0, n).join("/");
|
|
@@ -35365,11 +35828,11 @@ async function changedFiles(root) {
|
|
|
35365
35828
|
if (file2) lines.set(file2, (Number(added) || 0) + (Number(removed) || 0));
|
|
35366
35829
|
}
|
|
35367
35830
|
return parseStatus(status.stdout).map(({ path: file2, from }) => {
|
|
35368
|
-
const full =
|
|
35369
|
-
const bytes =
|
|
35831
|
+
const full = path54.join(root, file2);
|
|
35832
|
+
const bytes = existsSync31(full) ? statSync6(full).size : 0;
|
|
35370
35833
|
let count3 = lines.get(file2);
|
|
35371
35834
|
if (count3 === void 0 && bytes && bytes < 2e6) {
|
|
35372
|
-
const text =
|
|
35835
|
+
const text = readFileSync32(full, "utf8");
|
|
35373
35836
|
count3 = text.includes("\0") ? 0 : text.split("\n").length;
|
|
35374
35837
|
}
|
|
35375
35838
|
return { path: file2, ...from ? { from } : {}, lines: count3 ?? 0, bytes, project: projectOf(root, file2) };
|
|
@@ -35379,20 +35842,20 @@ async function filesWithSecrets(root, files, ignores) {
|
|
|
35379
35842
|
const hits = [];
|
|
35380
35843
|
for (const file2 of files) {
|
|
35381
35844
|
if (ignores.some((re) => re.test(file2))) continue;
|
|
35382
|
-
const full =
|
|
35383
|
-
if (!
|
|
35845
|
+
const full = path54.join(root, file2);
|
|
35846
|
+
if (!existsSync31(full) || statSync6(full).size > 2e6) continue;
|
|
35384
35847
|
const tracked = (await git(root, "cat-file", "-e", `HEAD:${file2}`)).code === 0;
|
|
35385
|
-
const added = tracked ? (await git(root, "diff", "-U0", "--no-color", "HEAD", "--", file2)).stdout.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)) :
|
|
35386
|
-
if (added.some((l) =>
|
|
35848
|
+
const added = tracked ? (await git(root, "diff", "-U0", "--no-color", "HEAD", "--", file2)).stdout.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)) : readFileSync32(full, "utf8").split("\n");
|
|
35849
|
+
if (added.some((l) => findLeakedSecrets(l).some((m) => m.confidence === "high"))) hits.push(file2);
|
|
35387
35850
|
}
|
|
35388
35851
|
return hits;
|
|
35389
35852
|
}
|
|
35390
35853
|
function loadNotes(root, file2) {
|
|
35391
35854
|
if (!file2) return {};
|
|
35392
|
-
const full =
|
|
35393
|
-
if (!isInside(root, full) || !
|
|
35855
|
+
const full = path54.resolve(root, file2);
|
|
35856
|
+
if (!isInside(root, full) || !existsSync31(full)) return `Couldn\u2019t find the notes file ${file2}.`;
|
|
35394
35857
|
try {
|
|
35395
|
-
const parsed = Notes.safeParse(JSON.parse(
|
|
35858
|
+
const parsed = Notes.safeParse(JSON.parse(readFileSync32(full, "utf8")));
|
|
35396
35859
|
return parsed.success ? parsed.data : `The notes file ${file2} isn\u2019t in the expected shape: { "<group id>": { "title", "summary", "check": [] } }.`;
|
|
35397
35860
|
} catch {
|
|
35398
35861
|
return `The notes file ${file2} isn\u2019t valid JSON.`;
|
|
@@ -35548,7 +36011,7 @@ Finish by hand with these two commands:
|
|
|
35548
36011
|
|
|
35549
36012
|
// src/commands/simple.ts
|
|
35550
36013
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
35551
|
-
import { existsSync as
|
|
36014
|
+
import { existsSync as existsSync32 } from "fs";
|
|
35552
36015
|
async function nextCommand(flags) {
|
|
35553
36016
|
const root = await findRepoRoot();
|
|
35554
36017
|
const scan = await latestScan(root);
|
|
@@ -35607,7 +36070,7 @@ async function kindSetting(root, config2, value, project) {
|
|
|
35607
36070
|
const paths = profile.projects.map((p2) => p2.path);
|
|
35608
36071
|
const at = project ?? (paths.length === 1 ? paths[0] : void 0);
|
|
35609
36072
|
if (!at) return `This repo has ${paths.length} projects, so say which one, for example: aura config kind ${value} ${paths.find((p2) => p2 !== ".")}`;
|
|
35610
|
-
if (!
|
|
36073
|
+
if (!existsSync32(packageFolder(root, normDir(at)))) return `There is no folder "${at}" in this repo.`;
|
|
35611
36074
|
config2.projects = setKindOverride(config2.projects, at, kind);
|
|
35612
36075
|
return "saved";
|
|
35613
36076
|
}
|
|
@@ -35680,7 +36143,7 @@ async function checkStagedCommand() {
|
|
|
35680
36143
|
for (const line of diff.split("\n")) {
|
|
35681
36144
|
if (line.startsWith("+++ ")) file2 = line.replace(/^\+\+\+ (?:b\/)?/, "");
|
|
35682
36145
|
else if (line.startsWith("+") && !line.startsWith("+++") && !ignored(file2)) {
|
|
35683
|
-
for (const m of
|
|
36146
|
+
for (const m of findLeakedSecrets(line.slice(1))) if (m.confidence === "high") hits.push(`${file2}: ${m.label} (${maskSecret(m.value)})`);
|
|
35684
36147
|
}
|
|
35685
36148
|
}
|
|
35686
36149
|
const envFiles2 = execFileSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=A"], { cwd: root, encoding: "utf8" }).split("\n").filter((f) => /(?:^|\/)\.env(?:\.[^/]+)?$/.test(f) && !/\.(?:example|sample|template)$/.test(f) && !ignored(f));
|