@viberaven/cli 1.5.0 → 1.5.2
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/AGENTS.md +157 -157
- package/LICENSE +21 -21
- package/README.md +113 -113
- package/SECURITY.md +59 -59
- package/dist/cli.js +2556 -540
- package/dist/console/app.js +251 -251
- package/dist/console/index.html +46 -46
- package/dist/console/styles.css +267 -267
- package/dist/playbooks/_stack-default.json +11 -11
- package/dist/playbooks/auth-supabase.json +31 -31
- package/dist/playbooks/stripe.json +66 -66
- package/dist/playbooks/supabase.json +51 -51
- package/dist/playbooks/vercel.json +42 -42
- package/dist/report/assets/provider-authjs.svg +5 -5
- package/dist/report/assets/provider-aws.svg +5 -5
- package/dist/report/assets/provider-logrocket.svg +4 -4
- package/dist/report/report-cli.css +1466 -1466
- package/dist/report/station.css +11854 -11854
- package/dist/report/station.js +8319 -8319
- package/dist/stack/app.js +16 -11
- package/dist/stack/stack.css +854 -442
- package/dist/vendor/anime.umd.min.js +7 -7
- package/package.json +12 -2
- package/playbooks/_stack-default.json +11 -11
- package/playbooks/auth-supabase.json +31 -31
- package/playbooks/stripe.json +66 -66
- package/playbooks/supabase.json +51 -51
- package/playbooks/vercel.json +42 -42
- package/schemas/permissions-matrix.schema.json +90 -0
- package/templates/AGENTS.snippet.md +52 -52
- package/templates/CLAUDE.snippet.md +17 -17
- package/templates/CURSOR.snippet.md +17 -17
package/dist/cli.js
CHANGED
|
@@ -9474,7 +9474,52 @@ function buildDefinerFunctionCheck(replay) {
|
|
|
9474
9474
|
findings
|
|
9475
9475
|
};
|
|
9476
9476
|
}
|
|
9477
|
-
function
|
|
9477
|
+
function directPostgresClients(files) {
|
|
9478
|
+
const found = /* @__PURE__ */ new Set();
|
|
9479
|
+
for (const file of files) {
|
|
9480
|
+
const path = file.path.replace(/\\/g, "/");
|
|
9481
|
+
if (PRISMA_SCHEMA.test(path)) {
|
|
9482
|
+
if (/provider\s*=\s*["'](?:postgresql|postgres)["']/i.test(file.content)) found.add(`prisma (${path})`);
|
|
9483
|
+
} else if (/(^|\/)package\.json$/.test(path)) {
|
|
9484
|
+
try {
|
|
9485
|
+
const pkg = JSON.parse(file.content);
|
|
9486
|
+
for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
9487
|
+
const group = pkg[key];
|
|
9488
|
+
if (!group || typeof group !== "object") continue;
|
|
9489
|
+
for (const name of Object.keys(group)) if (DIRECT_POSTGRES_PACKAGES.has(name)) found.add(name);
|
|
9490
|
+
}
|
|
9491
|
+
} catch {
|
|
9492
|
+
}
|
|
9493
|
+
} else if (SOURCE_CODE_EXT.test(path)) {
|
|
9494
|
+
const match = file.content.match(DIRECT_POSTGRES_IMPORT);
|
|
9495
|
+
if (match) found.add(match[0].replace(/^(?:from\s+|import\s*\(\s*|require\s*\(\s*)/, "").replace(/['"]/g, ""));
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
return [...found].sort();
|
|
9499
|
+
}
|
|
9500
|
+
function buildPoolerCheck(files, allFiles = files) {
|
|
9501
|
+
const clients = directPostgresClients(allFiles);
|
|
9502
|
+
if (clients.length === 0) {
|
|
9503
|
+
return {
|
|
9504
|
+
id: "vercel-supabase-pooler-port",
|
|
9505
|
+
title: "Vercel/Supabase pooler port",
|
|
9506
|
+
status: "pass",
|
|
9507
|
+
summary: "Not applicable. No direct Postgres driver or ORM was found in package.json or code, so the app reaches Supabase through supabase-js over HTTPS and has no database connection for the pooler to protect.",
|
|
9508
|
+
evidence: []
|
|
9509
|
+
};
|
|
9510
|
+
}
|
|
9511
|
+
const connectionEvidence = allFiles.filter((file) => isEnvFile(file.path.split(/[\\/]/).pop() ?? "") || SOURCE_CODE_EXT.test(file.path) || PRISMA_SCHEMA.test(file.path)).flatMap((file) => lineEvidence(file, CONNECTION_STRING));
|
|
9512
|
+
if (connectionEvidence.length === 0) {
|
|
9513
|
+
const reason = `a direct Postgres client is installed (${clients.join(", ")}) but no DATABASE_URL/POSTGRES_URL, env("...") or postgres:// connection string was found in env files, code or Prisma schemas`;
|
|
9514
|
+
return {
|
|
9515
|
+
id: "vercel-supabase-pooler-port",
|
|
9516
|
+
title: "Vercel/Supabase pooler port",
|
|
9517
|
+
status: "needs_work",
|
|
9518
|
+
summary: `Not checked: ${reason}, so the pooler port could not be checked.`,
|
|
9519
|
+
evidence: [],
|
|
9520
|
+
incomplete: [reason]
|
|
9521
|
+
};
|
|
9522
|
+
}
|
|
9478
9523
|
const dbUrlPattern = /\b(?:DATABASE_URL|POSTGRES_URL)\b/i;
|
|
9479
9524
|
const direct5432Evidence = files.flatMap(
|
|
9480
9525
|
(file) => lineEvidence(file, /\b(?:DATABASE_URL|POSTGRES_URL)\b[^\n\r]*:5432\b/i)
|
|
@@ -9546,7 +9591,7 @@ function buildVercelSupabaseAudit(input) {
|
|
|
9546
9591
|
const checks = [
|
|
9547
9592
|
buildRlsCheck(replay),
|
|
9548
9593
|
buildPermissivePolicyCheck(replay),
|
|
9549
|
-
buildPoolerCheck(input.files),
|
|
9594
|
+
buildPoolerCheck(input.files, allFiles),
|
|
9550
9595
|
buildServiceRoleCheck(allFiles, incomplete),
|
|
9551
9596
|
buildDataApiGrantCheck(replay, allFiles, incomplete),
|
|
9552
9597
|
buildDefinerFunctionCheck(replay)
|
|
@@ -9686,6 +9731,7 @@ async function collectSourceFiles(projectRoot, alreadyCollected) {
|
|
|
9686
9731
|
else if (name === "package.json" || /^vite\.config\.[cm]?[jt]s$/.test(name)) candidates.push({ absolutePath, relativePath, rank: 0 });
|
|
9687
9732
|
else if (name === "index.html" && !inTestDir) candidates.push({ absolutePath, relativePath, rank: 0 });
|
|
9688
9733
|
else if (/(^|\/)supabase\/config\.toml$/.test(relativePath)) candidates.push({ absolutePath, relativePath, rank: 0 });
|
|
9734
|
+
else if (/\.prisma$/i.test(name) && !inTestDir) candidates.push({ absolutePath, relativePath, rank: 0 });
|
|
9689
9735
|
else if (/\.sql$/i.test(name)) {
|
|
9690
9736
|
if (!inTestDir) candidates.push({ absolutePath, relativePath, rank: 1 });
|
|
9691
9737
|
} else if (SOURCE_CODE_EXT.test(name) && !inTestDir) candidates.push({ absolutePath, relativePath, rank: 2, filter: SOURCE_CODE_SIGNAL });
|
|
@@ -9776,7 +9822,7 @@ function renderVercelSupabaseAudit(result) {
|
|
|
9776
9822
|
}
|
|
9777
9823
|
return lines.join("\n").trimEnd();
|
|
9778
9824
|
}
|
|
9779
|
-
var import_promises3, import_node_path4, ENV_FILES, SQL_ROOTS, SQL_TEST_DIRS, SKIP_DIRS, SENSITIVE_ENV_KEY, SOURCE_SKIP_DIRS, SOURCE_TEST_DIRS, SOURCE_CODE_EXT, SOURCE_CODE_SIGNAL, SOURCE_LIMITS, CODE_DIR_FIRST, BULK_DIR_LAST;
|
|
9825
|
+
var import_promises3, import_node_path4, ENV_FILES, SQL_ROOTS, SQL_TEST_DIRS, SKIP_DIRS, SENSITIVE_ENV_KEY, DIRECT_POSTGRES_PACKAGES, DIRECT_POSTGRES_IMPORT, CONNECTION_STRING, PRISMA_SCHEMA, SOURCE_SKIP_DIRS, SOURCE_TEST_DIRS, SOURCE_CODE_EXT, SOURCE_CODE_SIGNAL, SOURCE_LIMITS, CODE_DIR_FIRST, BULK_DIR_LAST;
|
|
9780
9826
|
var init_audit = __esm({
|
|
9781
9827
|
"src/commands/audit.ts"() {
|
|
9782
9828
|
"use strict";
|
|
@@ -9798,6 +9844,27 @@ var init_audit = __esm({
|
|
|
9798
9844
|
SQL_TEST_DIRS = /* @__PURE__ */ new Set(["test", "tests", "__tests__", "spec"]);
|
|
9799
9845
|
SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "build", "coverage"]);
|
|
9800
9846
|
SENSITIVE_ENV_KEY = /\b[A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|SERVICE_ROLE|DATABASE_URL|POSTGRES_URL)[A-Z0-9_]*\b/i;
|
|
9847
|
+
DIRECT_POSTGRES_PACKAGES = /* @__PURE__ */ new Set([
|
|
9848
|
+
"pg",
|
|
9849
|
+
"postgres",
|
|
9850
|
+
"pg-promise",
|
|
9851
|
+
"slonik",
|
|
9852
|
+
"@neondatabase/serverless",
|
|
9853
|
+
"@vercel/postgres",
|
|
9854
|
+
"prisma",
|
|
9855
|
+
"@prisma/client",
|
|
9856
|
+
"drizzle-orm",
|
|
9857
|
+
"kysely",
|
|
9858
|
+
"typeorm",
|
|
9859
|
+
"knex",
|
|
9860
|
+
"sequelize",
|
|
9861
|
+
"@mikro-orm/postgresql",
|
|
9862
|
+
"@databases/pg",
|
|
9863
|
+
"massive"
|
|
9864
|
+
]);
|
|
9865
|
+
DIRECT_POSTGRES_IMPORT = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"](?:npm:)?(?:pg|postgres|pg-promise|slonik|@neondatabase\/serverless|@vercel\/postgres|@prisma\/client|drizzle-orm(?:\/[\w-]+)?|kysely|typeorm|knex|sequelize)(?:@[^'"/]*)?(?:\/[^'"]*)?['"]|['"]jsr:@db\/postgres|deno\.land\/x\/postgres(?:js)?\b/;
|
|
9866
|
+
CONNECTION_STRING = /\b[A-Z0-9_]*(?:DATABASE_URL|POSTGRES_URL|POSTGRES_PRISMA_URL|DB_URL|DIRECT_URL)\b|\bpostgres(?:ql)?:\/\//;
|
|
9867
|
+
PRISMA_SCHEMA = /\.prisma$/i;
|
|
9801
9868
|
SOURCE_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
9802
9869
|
...SKIP_DIRS,
|
|
9803
9870
|
".viberaven",
|
|
@@ -9817,7 +9884,7 @@ var init_audit = __esm({
|
|
|
9817
9884
|
]);
|
|
9818
9885
|
SOURCE_TEST_DIRS = /* @__PURE__ */ new Set(["test", "tests", "__tests__", "__mocks__", "e2e", "cypress", "playwright", "fixtures"]);
|
|
9819
9886
|
SOURCE_CODE_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|html?)$/i;
|
|
9820
|
-
SOURCE_CODE_SIGNAL = /supabase|service_?role|sb_secret_|eyJhbGci
|
|
9887
|
+
SOURCE_CODE_SIGNAL = /supabase|service_?role|sb_secret_|eyJhbGci|DATABASE_URL|POSTGRES_URL|postgres(?:ql)?:\/\//i;
|
|
9821
9888
|
SOURCE_LIMITS = {
|
|
9822
9889
|
/** Directory entries listed, across the whole walk. */
|
|
9823
9890
|
entries: 2e5,
|
|
@@ -10401,24 +10468,153 @@ function detectArchetype(signals, stack) {
|
|
|
10401
10468
|
if (stack.length === 0) return "unknown";
|
|
10402
10469
|
return "Web app";
|
|
10403
10470
|
}
|
|
10404
|
-
function
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
const
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10471
|
+
function isEnvTemplatePath(path) {
|
|
10472
|
+
return ENV_TEMPLATE_NAME.test(path.replace(/\\/g, "/").split("/").pop() ?? "");
|
|
10473
|
+
}
|
|
10474
|
+
function envTemplateNames(content) {
|
|
10475
|
+
const names = [];
|
|
10476
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
10477
|
+
const match = raw.match(/^\s*(?:#\s*)?(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
10478
|
+
if (match) names.push(match[1]);
|
|
10479
|
+
}
|
|
10480
|
+
return names;
|
|
10481
|
+
}
|
|
10482
|
+
function analyzeEnvContract(scan, extra = {}) {
|
|
10483
|
+
const templates = /* @__PURE__ */ new Map();
|
|
10414
10484
|
for (const file of scan.files) {
|
|
10415
|
-
if (
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10485
|
+
if (typeof file.content === "string" && isEnvTemplatePath(file.path)) templates.set(file.path.replace(/\\/g, "/"), file.content);
|
|
10486
|
+
}
|
|
10487
|
+
for (const file of extra.envTemplates ?? []) {
|
|
10488
|
+
if (isEnvTemplatePath(file.path)) templates.set(file.path.replace(/\\/g, "/"), file.content);
|
|
10489
|
+
}
|
|
10490
|
+
const declared = new Set([...templates.values()].flatMap(envTemplateNames));
|
|
10491
|
+
const used = /* @__PURE__ */ new Set();
|
|
10492
|
+
for (const file of codeFilesFor(scan, extra.codeFiles)) {
|
|
10493
|
+
if (!ENV_CODE_FILE.test(file.path)) continue;
|
|
10494
|
+
const edgeFunction = /(^|\/)supabase\/functions\//.test(file.path);
|
|
10495
|
+
const add = (name) => {
|
|
10496
|
+
if (!(edgeFunction && SUPABASE_EDGE_ENV.has(name))) used.add(name);
|
|
10497
|
+
};
|
|
10498
|
+
const code = stripComments2(file.content);
|
|
10499
|
+
for (const pattern of ENV_REFERENCES) {
|
|
10500
|
+
for (const match of code.matchAll(pattern)) add(match[1]);
|
|
10501
|
+
}
|
|
10502
|
+
for (const match of code.matchAll(SVELTEKIT_STATIC_IMPORT)) {
|
|
10503
|
+
for (const part of match[1].split(",")) {
|
|
10504
|
+
const name = part.trim().split(/\s+as\s+/)[0].trim();
|
|
10505
|
+
if (new RegExp(`^${ENV_NAME}$`).test(name)) add(name);
|
|
10506
|
+
}
|
|
10507
|
+
}
|
|
10508
|
+
for (const match of code.matchAll(SVELTEKIT_DYNAMIC_IMPORT)) {
|
|
10509
|
+
for (const part of match[1].split(",")) {
|
|
10510
|
+
const [imported, local] = part.trim().split(/\s+as\s+/).map((x2) => x2.trim());
|
|
10511
|
+
if (imported !== "env") continue;
|
|
10512
|
+
const alias = (local ?? imported).replace(/\$/g, "\\$");
|
|
10513
|
+
const reads = new RegExp(`\\b${alias}\\.(${ENV_NAME})\\b|\\b${alias}\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g");
|
|
10514
|
+
for (const read of code.matchAll(reads)) add(read[1] ?? read[2]);
|
|
10515
|
+
}
|
|
10516
|
+
}
|
|
10517
|
+
for (const match of code.matchAll(ENV_DESTRUCTURE)) {
|
|
10518
|
+
for (const part of match[1].split(",")) {
|
|
10519
|
+
const name = part.trim().split(/[:=\s]/)[0];
|
|
10520
|
+
if (new RegExp(`^${ENV_NAME}$`).test(name)) add(name);
|
|
10521
|
+
}
|
|
10522
|
+
}
|
|
10523
|
+
}
|
|
10524
|
+
const missing = [...used].filter((name) => !declared.has(name) && !ENV_DRIFT_IGNORE.has(name) && !ENV_DRIFT_IGNORE_PREFIX.test(name)).sort();
|
|
10525
|
+
return { templates: [...templates.keys()].sort(), declared: [...declared].sort(), used: [...used].sort(), missing };
|
|
10526
|
+
}
|
|
10527
|
+
function stripComments2(source) {
|
|
10528
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:'"\\])\/\/.*$/gm, "$1");
|
|
10529
|
+
}
|
|
10530
|
+
function exportedNames(code) {
|
|
10531
|
+
const names = [];
|
|
10532
|
+
const localVerifiers = /* @__PURE__ */ new Set();
|
|
10533
|
+
const directVerifier = (body) => /^\s*return\s+(?:await\s+)?[\w$.]+\.(?:webhooks\.constructEvent(?:Async)?|signature\.verifyHeader(?:Async)?)\s*\([^;{}]+\)\s*;?\s*$/.test(body) && !STRIPE_VERIFY_JSON_BODY.test(body);
|
|
10534
|
+
for (const fn of code.matchAll(/\b(export\s+)?(default\s+)?(?:async\s+)?function\s+([\w$]+)\s*\([^)]*\)\s*(?::[^{}]+)?\{([^{}]*)\}/g)) {
|
|
10535
|
+
if (!directVerifier(fn[4])) continue;
|
|
10536
|
+
localVerifiers.add(fn[3]);
|
|
10537
|
+
if (fn[1]) names.push(fn[2] ? "default" : fn[3]);
|
|
10538
|
+
}
|
|
10539
|
+
for (const list of code.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
10540
|
+
for (const entry of list[1].split(",")) {
|
|
10541
|
+
const name = entry.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
|
|
10542
|
+
if (name && localVerifiers.has(name[1])) names.push(name[2] ?? name[1]);
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
10545
|
+
for (const object of code.matchAll(/export\s+(?:const|let|var)\s+([\w$]+)\s*=\s*\{\s*(?:async\s+)?([\w$]+)\s*\([^)]*\)\s*\{([^{}]*)\}\s*,?\s*\}/g)) {
|
|
10546
|
+
if (directVerifier(object[3])) names.push(`${object[1]}.${object[2]}`);
|
|
10547
|
+
}
|
|
10548
|
+
return [...new Set(names)];
|
|
10549
|
+
}
|
|
10550
|
+
function assessStripeWebhooks(files) {
|
|
10551
|
+
const facts = [];
|
|
10552
|
+
for (const file of files) {
|
|
10553
|
+
const path = file.path.replace(/\\/g, "/");
|
|
10554
|
+
if (!CODE_FILE.test(path) || TEST_FILE.test(path)) continue;
|
|
10555
|
+
const code = stripComments2(file.content);
|
|
10556
|
+
const verify = STRIPE_VERIFY_CALL.test(code);
|
|
10557
|
+
const header = STRIPE_SIGNATURE_HEADER.test(code);
|
|
10558
|
+
const requestHandler = REQUEST_HANDLER.test(code);
|
|
10559
|
+
const handler = verify || requestHandler && (header || STRIPE_EVENT_TYPE.test(code) || /webhook/i.test(path) && /stripe/i.test(code));
|
|
10560
|
+
if (!handler) continue;
|
|
10561
|
+
facts.push({ path, code, verify, header, raw: RAW_BODY.test(code), jsonBody: STRIPE_VERIFY_JSON_BODY.test(code), names: exportedNames(code) });
|
|
10562
|
+
}
|
|
10563
|
+
const helpers = facts.filter((f) => f.verify && !f.jsonBody && f.names.length > 0);
|
|
10564
|
+
const paired = /* @__PURE__ */ new Set();
|
|
10565
|
+
for (const route of facts.filter((f) => !f.verify && f.header && f.raw)) {
|
|
10566
|
+
for (const helper of helpers) {
|
|
10567
|
+
const names = [];
|
|
10568
|
+
const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "").replace(/\/index$/, "");
|
|
10569
|
+
for (const imported of route.code.matchAll(/import\s+([^;'"\n]+?)\s+from\s*['"]([^'"]+)['"]/g)) {
|
|
10570
|
+
const specifier = imported[2];
|
|
10571
|
+
const modulePath = specifier.startsWith(".") ? import_node_path7.posix.join(import_node_path7.posix.dirname(route.path), specifier) : specifier.replace(/^@\//, "");
|
|
10572
|
+
if (withoutExtension(modulePath) !== withoutExtension(helper.path)) continue;
|
|
10573
|
+
const binding = imported[1].trim();
|
|
10574
|
+
const defaultName = binding.match(/^([\w$]+)(?:\s*,|$)/)?.[1];
|
|
10575
|
+
if (defaultName && helper.names.includes("default")) names.push(defaultName);
|
|
10576
|
+
const named = binding.match(/\{([^}]+)\}/)?.[1];
|
|
10577
|
+
for (const entry of named?.split(",") ?? []) {
|
|
10578
|
+
const pair = entry.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
|
|
10579
|
+
if (!pair) continue;
|
|
10580
|
+
for (const name of helper.names) {
|
|
10581
|
+
if (name === pair[1] || name.startsWith(`${pair[1]}.`)) names.push((pair[2] ?? pair[1]) + name.slice(pair[1].length));
|
|
10582
|
+
}
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
if (names.some((name) => new RegExp(`\\b${name.replace(/[.$]/g, "\\$&")}\\s*\\(`).test(route.code))) {
|
|
10586
|
+
paired.add(route.path);
|
|
10587
|
+
paired.add(helper.path);
|
|
10588
|
+
}
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
const verified = [];
|
|
10592
|
+
const unverified = [];
|
|
10593
|
+
for (const f of facts) {
|
|
10594
|
+
const complete = f.verify && f.header && f.raw && !f.jsonBody;
|
|
10595
|
+
if (complete || paired.has(f.path)) {
|
|
10596
|
+
verified.push(f.path);
|
|
10597
|
+
continue;
|
|
10419
10598
|
}
|
|
10599
|
+
const missing = [];
|
|
10600
|
+
if (!f.verify) missing.push(f.header && f.raw ? "no signature check (constructEvent), and no call to a helper that runs it" : "no signature check (constructEvent)");
|
|
10601
|
+
if (!f.header) missing.push("no stripe-signature header");
|
|
10602
|
+
if (f.jsonBody) missing.push("body parsed as JSON before verification");
|
|
10603
|
+
else if (!f.raw) missing.push("no raw body");
|
|
10604
|
+
unverified.push({ path: f.path, missing });
|
|
10420
10605
|
}
|
|
10421
|
-
return
|
|
10606
|
+
return { verified: verified.sort(), unverified: unverified.sort((a, b3) => a.path < b3.path ? -1 : 1) };
|
|
10607
|
+
}
|
|
10608
|
+
function codeFilesFor(scan, fullCopies = []) {
|
|
10609
|
+
const full = new Map(fullCopies.map((f) => [f.path.replace(/\\/g, "/"), f.content]));
|
|
10610
|
+
const merged = /* @__PURE__ */ new Map();
|
|
10611
|
+
for (const file of scan.files) {
|
|
10612
|
+
if (typeof file.content !== "string") continue;
|
|
10613
|
+
const path = file.path.replace(/\\/g, "/");
|
|
10614
|
+
merged.set(path, full.get(path) ?? file.content);
|
|
10615
|
+
}
|
|
10616
|
+
for (const [path, content] of full) if (!merged.has(path)) merged.set(path, content);
|
|
10617
|
+
return [...merged].map(([path, content]) => ({ path, content }));
|
|
10422
10618
|
}
|
|
10423
10619
|
function findUnboundedQueryFiles(scan) {
|
|
10424
10620
|
const flagged = [];
|
|
@@ -10525,7 +10721,7 @@ function scanIncompleteSeed(audit) {
|
|
|
10525
10721
|
primaryMapCategory: "security"
|
|
10526
10722
|
};
|
|
10527
10723
|
}
|
|
10528
|
-
function buildGapSeeds(scan, audit, databaseEvidence) {
|
|
10724
|
+
function buildGapSeeds(scan, audit, databaseEvidence, extra = {}) {
|
|
10529
10725
|
const signals = scan.stackSignals;
|
|
10530
10726
|
const usesDatabase = detectDatabaseUsage(scan, databaseEvidence);
|
|
10531
10727
|
const seeds = [];
|
|
@@ -10572,6 +10768,16 @@ ${check.evidence.map((e2) => `- ${e2}`).join("\n")}` : "";
|
|
|
10572
10768
|
copyPrompt: "Audit every use of SUPABASE_SERVICE_ROLE_KEY in this repo. It must only be read in server-only modules (route handlers, server actions, edge functions) \u2014 never in client components or files bundled for the browser. Move violations behind a server boundary.",
|
|
10573
10769
|
primaryMapCategory: "security"
|
|
10574
10770
|
});
|
|
10771
|
+
} else if (check.id === "vercel-supabase-pooler-port" && (check.incomplete ?? []).length > 0) {
|
|
10772
|
+
seeds.push({
|
|
10773
|
+
id: "pooler_port_unchecked",
|
|
10774
|
+
category: "DATABASE & DATA",
|
|
10775
|
+
severity: "info",
|
|
10776
|
+
title: "Supabase pooler port could not be checked",
|
|
10777
|
+
detail: `Not checked: ${(check.incomplete ?? []).join("; ")}.`,
|
|
10778
|
+
copyPrompt: "Find where the app builds its Postgres connection string (env files, Prisma schema, the ORM config) and make sure serverless deployments use Supabase's transaction pooler (port 6543) rather than the direct 5432 connection.",
|
|
10779
|
+
primaryMapCategory: "database"
|
|
10780
|
+
});
|
|
10575
10781
|
} else if (check.id === "vercel-supabase-pooler-port") {
|
|
10576
10782
|
seeds.push({
|
|
10577
10783
|
id: "pooler_port_mismatch",
|
|
@@ -10685,8 +10891,19 @@ ${check.evidence.map((e2) => `- ${e2}`).join("\n")}` : "";
|
|
|
10685
10891
|
});
|
|
10686
10892
|
}
|
|
10687
10893
|
if (signals.hasStripe) {
|
|
10688
|
-
const
|
|
10689
|
-
if (
|
|
10894
|
+
const webhooks = assessStripeWebhooks(codeFilesFor(scan, extra.codeFiles));
|
|
10895
|
+
if (webhooks.unverified.length > 0) {
|
|
10896
|
+
const lines = webhooks.unverified.map((u2) => `${u2.path} (${u2.missing.join(", ")})`);
|
|
10897
|
+
seeds.push({
|
|
10898
|
+
id: "unverified_stripe_webhook",
|
|
10899
|
+
category: "SECURITY & AUTH",
|
|
10900
|
+
severity: "warning",
|
|
10901
|
+
title: "Stripe webhook does not verify the signature",
|
|
10902
|
+
detail: `${webhooks.unverified.length} Stripe webhook handler${webhooks.unverified.length === 1 ? "" : "s"} without signature verification over the raw body: ${capLines(lines).join("; ")}. Anyone who finds the URL can post a fake event, for example a paid checkout.`,
|
|
10903
|
+
copyPrompt: 'In each listed handler, read the raw request body (await req.text() in Next.js route handlers and Deno, express.raw({ type: "application/json" }) in Express), read the stripe-signature header, and pass both to stripe.webhooks.constructEvent (constructEventAsync on Deno or edge runtimes) with STRIPE_WEBHOOK_SECRET before trusting event.type. Return 400 when verification throws.',
|
|
10904
|
+
primaryMapCategory: "payments"
|
|
10905
|
+
});
|
|
10906
|
+
} else if (webhooks.verified.length === 0) {
|
|
10690
10907
|
seeds.push({
|
|
10691
10908
|
id: "missing_stripe_webhook",
|
|
10692
10909
|
category: "MISSING FEATURES",
|
|
@@ -10698,19 +10915,35 @@ ${check.evidence.map((e2) => `- ${e2}`).join("\n")}` : "";
|
|
|
10698
10915
|
});
|
|
10699
10916
|
}
|
|
10700
10917
|
}
|
|
10701
|
-
|
|
10702
|
-
const
|
|
10918
|
+
{
|
|
10919
|
+
const contract = analyzeEnvContract(scan, extra);
|
|
10920
|
+
const drifted = contract.missing;
|
|
10703
10921
|
if (drifted.length > 0) {
|
|
10704
10922
|
const shown = drifted.slice(0, 5).join(", ");
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10923
|
+
const more = drifted.length > 5 ? ", \u2026" : "";
|
|
10924
|
+
const count = `${drifted.length} environment variable${drifted.length === 1 ? "" : "s"}`;
|
|
10925
|
+
if (contract.templates.length > 0) {
|
|
10926
|
+
const target = contract.templates.length === 1 ? contract.templates[0] : "the env templates";
|
|
10927
|
+
seeds.push({
|
|
10928
|
+
id: "env_var_drift",
|
|
10929
|
+
category: "DEPLOYMENT",
|
|
10930
|
+
severity: "warning",
|
|
10931
|
+
title: `Env vars used in code but missing from ${target}`,
|
|
10932
|
+
detail: `Code reads ${count} that ${contract.templates.length === 1 ? target : `no env template (${contract.templates.join(", ")})`} does not document (${shown}${more}). Fresh clones, CI, and the production deploy can silently run without them.`,
|
|
10933
|
+
copyPrompt: `Add placeholder entries for these variables to ${contract.templates.length === 1 ? target : contract.templates[0]} (never real values): ${shown}${drifted.length > 5 ? " and the rest" : ""}. Then confirm each one is set in the deployment environment (Vercel project settings).`,
|
|
10934
|
+
primaryMapCategory: "deployment"
|
|
10935
|
+
});
|
|
10936
|
+
} else {
|
|
10937
|
+
seeds.push({
|
|
10938
|
+
id: "env_var_drift",
|
|
10939
|
+
category: "DEPLOYMENT",
|
|
10940
|
+
severity: "info",
|
|
10941
|
+
title: "Env vars used in code but no env template documents them",
|
|
10942
|
+
detail: `Code reads ${count} (${shown}${more}) and the repo has no .env.example, .env.sample or similar template, so a fresh clone or CI has no list of what to set.`,
|
|
10943
|
+
copyPrompt: `Add a .env.example with placeholder entries (never real values) for: ${shown}${drifted.length > 5 ? " and the rest" : ""}. Commit it, and confirm each one is set in the deployment environment.`,
|
|
10944
|
+
primaryMapCategory: "deployment"
|
|
10945
|
+
});
|
|
10946
|
+
}
|
|
10714
10947
|
}
|
|
10715
10948
|
}
|
|
10716
10949
|
if (hasDatabase) {
|
|
@@ -10799,15 +11032,52 @@ function checklistFrom(gaps) {
|
|
|
10799
11032
|
}
|
|
10800
11033
|
return base;
|
|
10801
11034
|
}
|
|
11035
|
+
async function readPaymentCodeFiles(workspacePath, scan) {
|
|
11036
|
+
if (!scan.stackSignals.hasStripe) return [];
|
|
11037
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
11038
|
+
for (const path of scan.fileTree.split("\n")) {
|
|
11039
|
+
if (CODE_FILE.test(path) && PAYMENT_PATH.test(path)) candidates.add(path);
|
|
11040
|
+
}
|
|
11041
|
+
for (const file of scan.files) {
|
|
11042
|
+
if (CODE_FILE.test(file.path) && typeof file.content === "string" && /stripe|constructEvent/i.test(file.content)) candidates.add(file.path);
|
|
11043
|
+
}
|
|
11044
|
+
const files = [];
|
|
11045
|
+
for (const path of [...candidates].slice(0, FULL_READ_LIMITS.files)) {
|
|
11046
|
+
try {
|
|
11047
|
+
const absolute = (0, import_node_path7.join)(workspacePath, path);
|
|
11048
|
+
if ((await (0, import_promises6.stat)(absolute)).size > FULL_READ_LIMITS.bytes) continue;
|
|
11049
|
+
files.push({ path, content: await (0, import_promises6.readFile)(absolute, "utf8") });
|
|
11050
|
+
} catch {
|
|
11051
|
+
}
|
|
11052
|
+
}
|
|
11053
|
+
return files;
|
|
11054
|
+
}
|
|
10802
11055
|
async function buildLocalStationOutput(input) {
|
|
10803
11056
|
const { scan, workspacePath } = input;
|
|
10804
11057
|
const auditInput = await collectVercelSupabaseAuditInput(workspacePath);
|
|
10805
11058
|
const audit = buildVercelSupabaseAudit(auditInput);
|
|
10806
11059
|
const evidence = await collectDatabaseEvidence(workspacePath, scan);
|
|
10807
|
-
const
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
11060
|
+
const auditFiles = [...auditInput.files, ...auditInput.sourceFiles ?? []];
|
|
11061
|
+
const envTemplates = auditFiles.filter((file) => isEnvTemplatePath(file.path));
|
|
11062
|
+
for (const name of ["example.env", "sample.env", "template.env", "env.example", "env.sample", "env.template"]) {
|
|
11063
|
+
try {
|
|
11064
|
+
envTemplates.push({ path: name, content: await (0, import_promises6.readFile)((0, import_node_path7.join)(workspacePath, name), "utf8") });
|
|
11065
|
+
} catch {
|
|
11066
|
+
}
|
|
11067
|
+
}
|
|
11068
|
+
const codeFiles = [
|
|
11069
|
+
...await readPaymentCodeFiles(workspacePath, scan),
|
|
11070
|
+
...auditFiles.filter((file) => ENV_CODE_FILE.test(file.path))
|
|
11071
|
+
];
|
|
11072
|
+
const seeds = buildGapSeeds(
|
|
11073
|
+
scan,
|
|
11074
|
+
audit,
|
|
11075
|
+
{
|
|
11076
|
+
files: [...auditInput.files, ...evidence.files],
|
|
11077
|
+
incomplete: evidence.incomplete
|
|
11078
|
+
},
|
|
11079
|
+
{ codeFiles, envTemplates }
|
|
11080
|
+
);
|
|
10811
11081
|
const releaseTagSeed = await detectReleaseTagGap(workspacePath);
|
|
10812
11082
|
if (releaseTagSeed) {
|
|
10813
11083
|
seeds.push(releaseTagSeed);
|
|
@@ -10842,13 +11112,15 @@ async function buildLocalStationOutput(input) {
|
|
|
10842
11112
|
productionChecklist: checklistFrom(seeds)
|
|
10843
11113
|
};
|
|
10844
11114
|
}
|
|
10845
|
-
var import_node_child_process3, import_node_util3, execFileAsync3, ENV_DRIFT_IGNORE, AUDIT_FINDING_GAPS, FINDING_SEVERITY_RANK, DETAIL_MAX_FINDINGS, SEVERITY_ORDER;
|
|
11115
|
+
var import_node_child_process3, import_node_util3, import_promises6, import_node_path7, execFileAsync3, ENV_DRIFT_IGNORE, ENV_DRIFT_IGNORE_PREFIX, SUPABASE_EDGE_ENV, ENV_TEMPLATE_NAME, ENV_CODE_FILE, ENV_NAME, ENV_REFERENCES, SVELTEKIT_STATIC_IMPORT, SVELTEKIT_DYNAMIC_IMPORT, ENV_DESTRUCTURE, CODE_FILE, STRIPE_VERIFY_CALL, STRIPE_VERIFY_JSON_BODY, STRIPE_SIGNATURE_HEADER, RAW_BODY, STRIPE_EVENT_TYPE, REQUEST_HANDLER, TEST_FILE, AUDIT_FINDING_GAPS, FINDING_SEVERITY_RANK, DETAIL_MAX_FINDINGS, SEVERITY_ORDER, FULL_READ_LIMITS, PAYMENT_PATH;
|
|
10846
11116
|
var init_localStation = __esm({
|
|
10847
11117
|
"src/localStation.ts"() {
|
|
10848
11118
|
"use strict";
|
|
10849
11119
|
import_node_child_process3 = require("node:child_process");
|
|
10850
11120
|
import_node_util3 = require("node:util");
|
|
10851
11121
|
init_audit();
|
|
11122
|
+
import_promises6 = require("node:fs/promises");
|
|
11123
|
+
import_node_path7 = require("node:path");
|
|
10852
11124
|
init_databaseEvidence();
|
|
10853
11125
|
init_safeGit();
|
|
10854
11126
|
execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
@@ -10858,13 +11130,80 @@ var init_localStation = __esm({
|
|
|
10858
11130
|
"PORT",
|
|
10859
11131
|
"TZ",
|
|
10860
11132
|
"ANALYZE",
|
|
11133
|
+
"HOST",
|
|
11134
|
+
"HOSTNAME",
|
|
11135
|
+
"HOME",
|
|
11136
|
+
"PATH",
|
|
11137
|
+
"PWD",
|
|
11138
|
+
"USER",
|
|
11139
|
+
"SHELL",
|
|
11140
|
+
"TMPDIR",
|
|
11141
|
+
"TEMP",
|
|
11142
|
+
"TMP",
|
|
11143
|
+
"LANG",
|
|
11144
|
+
"DEBUG",
|
|
10861
11145
|
"VERCEL",
|
|
10862
|
-
"VERCEL_ENV",
|
|
10863
|
-
"VERCEL_URL",
|
|
10864
|
-
"VERCEL_REGION",
|
|
10865
11146
|
"NEXT_RUNTIME",
|
|
10866
|
-
"NEXT_PHASE"
|
|
11147
|
+
"NEXT_PHASE",
|
|
11148
|
+
"NEXT_TELEMETRY_DISABLED",
|
|
11149
|
+
// Vite's built-in import.meta.env values.
|
|
11150
|
+
"MODE",
|
|
11151
|
+
"DEV",
|
|
11152
|
+
"PROD",
|
|
11153
|
+
"SSR",
|
|
11154
|
+
"BASE_URL",
|
|
11155
|
+
// Netlify, Render and other hosts.
|
|
11156
|
+
"NETLIFY",
|
|
11157
|
+
"CONTEXT",
|
|
11158
|
+
"URL",
|
|
11159
|
+
"DEPLOY_URL",
|
|
11160
|
+
"DEPLOY_PRIME_URL",
|
|
11161
|
+
"BUILD_ID",
|
|
11162
|
+
"SITE_ID",
|
|
11163
|
+
"SITE_NAME",
|
|
11164
|
+
"RENDER",
|
|
11165
|
+
"CF_PAGES",
|
|
11166
|
+
"AWS_REGION",
|
|
11167
|
+
"AWS_EXECUTION_ENV",
|
|
11168
|
+
"LAMBDA_TASK_ROOT"
|
|
11169
|
+
]);
|
|
11170
|
+
ENV_DRIFT_IGNORE_PREFIX = /^(?:npm_|VERCEL_|NEXT_PUBLIC_VERCEL_|__NEXT|CF_PAGES_|RENDER_|RAILWAY_|FLY_|GITHUB_|RUNNER_|NETLIFY_|AWS_LAMBDA_)/;
|
|
11171
|
+
SUPABASE_EDGE_ENV = /* @__PURE__ */ new Set([
|
|
11172
|
+
"SUPABASE_URL",
|
|
11173
|
+
"SUPABASE_ANON_KEY",
|
|
11174
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
11175
|
+
"SUPABASE_DB_URL",
|
|
11176
|
+
"SUPABASE_PUBLISHABLE_KEYS",
|
|
11177
|
+
"SUPABASE_SECRET_KEYS",
|
|
11178
|
+
"SB_REGION",
|
|
11179
|
+
"SB_EXECUTION_ID",
|
|
11180
|
+
"DENO_DEPLOYMENT_ID",
|
|
11181
|
+
"DENO_REGION"
|
|
10867
11182
|
]);
|
|
11183
|
+
ENV_TEMPLATE_NAME = /^(?:\.env(?:\.[\w-]+)*\.(?:example|sample|template|tpl|dist|defaults)|(?:example|sample|template)\.env|env\.(?:example|sample|template))$/i;
|
|
11184
|
+
ENV_CODE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i;
|
|
11185
|
+
ENV_NAME = "[A-Z][A-Z0-9_]{2,}";
|
|
11186
|
+
ENV_REFERENCES = [
|
|
11187
|
+
new RegExp(`process\\.env\\??\\.(${ENV_NAME})\\b`, "g"),
|
|
11188
|
+
new RegExp(`process\\.env\\??\\.?\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g"),
|
|
11189
|
+
new RegExp(`import\\.meta\\.env\\??\\.(${ENV_NAME})\\b`, "g"),
|
|
11190
|
+
new RegExp(`import\\.meta\\.env\\??\\.?\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g"),
|
|
11191
|
+
new RegExp(`Deno\\.env\\.get\\(\\s*['"\`](${ENV_NAME})['"\`]\\s*\\)`, "g"),
|
|
11192
|
+
new RegExp(`Bun\\.env\\.(${ENV_NAME})\\b`, "g"),
|
|
11193
|
+
// Hono and Cloudflare Workers bindings: c.env.X
|
|
11194
|
+
new RegExp(`\\b(?:c|ctx|context)\\.env\\.(${ENV_NAME})\\b`, "g")
|
|
11195
|
+
];
|
|
11196
|
+
SVELTEKIT_STATIC_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]\$env\/static\/(?:private|public)['"]/g;
|
|
11197
|
+
SVELTEKIT_DYNAMIC_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]\$env\/dynamic\/(?:private|public)['"]/g;
|
|
11198
|
+
ENV_DESTRUCTURE = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:process\.env|import\.meta\.env|Bun\.env)\b/g;
|
|
11199
|
+
CODE_FILE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
11200
|
+
STRIPE_VERIFY_CALL = /\bconstructEvent(?:Async)?\s*\(|\.signature\.verifyHeader(?:Async)?\s*\(/;
|
|
11201
|
+
STRIPE_VERIFY_JSON_BODY = /\bconstructEvent(?:Async)?\s*\(\s*JSON\.stringify\s*\(/;
|
|
11202
|
+
STRIPE_SIGNATURE_HEADER = /stripe-signature/i;
|
|
11203
|
+
RAW_BODY = /\.text\(\s*\)|\.arrayBuffer\(\s*\)|express\.raw\s*\(|bodyParser\.raw\s*\(|\braw\(\s*\{|\brawBody\b|\bbuffer\(\s*req\b|getRawBody\s*\(|readRawBody\s*\(|\bevent\.body\b/;
|
|
11204
|
+
STRIPE_EVENT_TYPE = /['"`](?:checkout\.session|customer\.subscription|invoice|payment_intent|charge)\.[a-z_.]+['"`]/;
|
|
11205
|
+
REQUEST_HANDLER = /export\s+(?:async\s+)?function\s+POST\b|export\s+const\s+POST\b|export\s+default\s+(?:async\s+)?function\s*[\w$]*\s*\(\s*(?:req|request)\b|export\s+(?:async\s+function|function|const)\s+handler\b|\b[A-Za-z_$][\w$]*\.post\s*\(|Deno\.serve\s*\(|\bserve\s*\(|addEventListener\(\s*['"]fetch|export\s+default\s*\{\s*(?:async\s+)?fetch\b/;
|
|
11206
|
+
TEST_FILE = /(^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/i;
|
|
10868
11207
|
AUDIT_FINDING_GAPS = {
|
|
10869
11208
|
rls_policy_allows_all_write: {
|
|
10870
11209
|
title: "A policy lets anyone write every row",
|
|
@@ -10918,6 +11257,8 @@ var init_localStation = __esm({
|
|
|
10918
11257
|
FINDING_SEVERITY_RANK = { critical: 0, high: 1, warning: 2, info: 3 };
|
|
10919
11258
|
DETAIL_MAX_FINDINGS = 10;
|
|
10920
11259
|
SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
11260
|
+
FULL_READ_LIMITS = { files: 40, bytes: 512e3 };
|
|
11261
|
+
PAYMENT_PATH = /webhook|stripe|payment|billing|checkout/i;
|
|
10921
11262
|
}
|
|
10922
11263
|
});
|
|
10923
11264
|
|
|
@@ -11627,20 +11968,20 @@ var init_checkMap = __esm({
|
|
|
11627
11968
|
|
|
11628
11969
|
// src/playbooks/playbooksRoot.ts
|
|
11629
11970
|
function playbooksRoot() {
|
|
11630
|
-
const candidates = [(0,
|
|
11971
|
+
const candidates = [(0, import_node_path8.join)(__dirname, "playbooks"), (0, import_node_path8.join)(__dirname, "..", "..", "playbooks")];
|
|
11631
11972
|
for (const dir of candidates) {
|
|
11632
|
-
if ((0, import_node_fs4.existsSync)((0,
|
|
11973
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path8.join)(dir, "vercel.json"))) {
|
|
11633
11974
|
return dir;
|
|
11634
11975
|
}
|
|
11635
11976
|
}
|
|
11636
11977
|
throw new Error("VibeRaven playbooks directory not found (expected vercel.json)");
|
|
11637
11978
|
}
|
|
11638
|
-
var import_node_fs4,
|
|
11979
|
+
var import_node_fs4, import_node_path8;
|
|
11639
11980
|
var init_playbooksRoot = __esm({
|
|
11640
11981
|
"src/playbooks/playbooksRoot.ts"() {
|
|
11641
11982
|
"use strict";
|
|
11642
11983
|
import_node_fs4 = require("node:fs");
|
|
11643
|
-
|
|
11984
|
+
import_node_path8 = require("node:path");
|
|
11644
11985
|
}
|
|
11645
11986
|
});
|
|
11646
11987
|
|
|
@@ -11747,8 +12088,8 @@ async function loadPlaybook(provider2) {
|
|
|
11747
12088
|
`Unknown provider "${provider2}". Available: ${PLAYBOOK_PROVIDERS.join(", ")}`
|
|
11748
12089
|
);
|
|
11749
12090
|
}
|
|
11750
|
-
const path = (0,
|
|
11751
|
-
const raw = JSON.parse(await (0,
|
|
12091
|
+
const path = (0, import_node_path9.join)(playbooksRoot(), `${normalized}.json`);
|
|
12092
|
+
const raw = JSON.parse(await (0, import_promises7.readFile)(path, "utf-8"));
|
|
11752
12093
|
const playbook = parsePlaybook(raw);
|
|
11753
12094
|
if (playbook.provider !== normalized) {
|
|
11754
12095
|
throw new Error(`Playbook file ${normalized}.json has mismatched provider field`);
|
|
@@ -11762,7 +12103,7 @@ function loadPlaybookSync(provider2) {
|
|
|
11762
12103
|
`Unknown provider "${provider2}". Available: ${PLAYBOOK_PROVIDERS.join(", ")}`
|
|
11763
12104
|
);
|
|
11764
12105
|
}
|
|
11765
|
-
const path = (0,
|
|
12106
|
+
const path = (0, import_node_path9.join)(playbooksRoot(), `${normalized}.json`);
|
|
11766
12107
|
const raw = JSON.parse((0, import_node_fs5.readFileSync)(path, "utf-8"));
|
|
11767
12108
|
const playbook = parsePlaybook(raw);
|
|
11768
12109
|
if (playbook.provider !== normalized) {
|
|
@@ -11770,13 +12111,13 @@ function loadPlaybookSync(provider2) {
|
|
|
11770
12111
|
}
|
|
11771
12112
|
return playbook;
|
|
11772
12113
|
}
|
|
11773
|
-
var import_node_fs5,
|
|
12114
|
+
var import_node_fs5, import_promises7, import_node_path9;
|
|
11774
12115
|
var init_loadPlaybook = __esm({
|
|
11775
12116
|
"src/playbooks/loadPlaybook.ts"() {
|
|
11776
12117
|
"use strict";
|
|
11777
12118
|
import_node_fs5 = require("node:fs");
|
|
11778
|
-
|
|
11779
|
-
|
|
12119
|
+
import_promises7 = require("node:fs/promises");
|
|
12120
|
+
import_node_path9 = require("node:path");
|
|
11780
12121
|
init_playbooksRoot();
|
|
11781
12122
|
init_types2();
|
|
11782
12123
|
init_validate();
|
|
@@ -11861,20 +12202,20 @@ async function loadLastArtifact(startDir) {
|
|
|
11861
12202
|
if (!workspace) {
|
|
11862
12203
|
throw new ScanNotFoundError(needsScanMessage(startDir));
|
|
11863
12204
|
}
|
|
11864
|
-
const path = (0,
|
|
12205
|
+
const path = (0, import_node_path10.join)(getProjectArtifactsDir(workspace), "last-scan.json");
|
|
11865
12206
|
try {
|
|
11866
|
-
const raw = await (0,
|
|
12207
|
+
const raw = await (0, import_promises8.readFile)(path, "utf-8");
|
|
11867
12208
|
return JSON.parse(raw);
|
|
11868
12209
|
} catch {
|
|
11869
12210
|
throw new ScanNotFoundError(needsScanMessage(startDir));
|
|
11870
12211
|
}
|
|
11871
12212
|
}
|
|
11872
|
-
var
|
|
12213
|
+
var import_promises8, import_node_path10, SEVERITY_RANK2, ScanNotFoundError;
|
|
11873
12214
|
var init_menu = __esm({
|
|
11874
12215
|
"src/tui/menu.ts"() {
|
|
11875
12216
|
"use strict";
|
|
11876
|
-
|
|
11877
|
-
|
|
12217
|
+
import_promises8 = require("node:fs/promises");
|
|
12218
|
+
import_node_path10 = require("node:path");
|
|
11878
12219
|
init_config();
|
|
11879
12220
|
SEVERITY_RANK2 = {
|
|
11880
12221
|
critical: 0,
|
|
@@ -12592,25 +12933,25 @@ var init_reportHtml = __esm({
|
|
|
12592
12933
|
function getBundledReportAssetsDir() {
|
|
12593
12934
|
const base = __dirname;
|
|
12594
12935
|
const candidates = [
|
|
12595
|
-
(0,
|
|
12596
|
-
(0,
|
|
12597
|
-
(0,
|
|
12598
|
-
(0,
|
|
12599
|
-
(0,
|
|
12936
|
+
(0, import_node_path11.join)(base, "report"),
|
|
12937
|
+
(0, import_node_path11.join)(base, "..", "assets", "report"),
|
|
12938
|
+
(0, import_node_path11.join)(base, "..", "..", "assets", "report"),
|
|
12939
|
+
(0, import_node_path11.join)(process.cwd(), "packages", "cli", "assets", "report"),
|
|
12940
|
+
(0, import_node_path11.join)(process.cwd(), "assets", "report")
|
|
12600
12941
|
];
|
|
12601
12942
|
for (const dir of candidates) {
|
|
12602
|
-
if ((0, import_node_fs6.existsSync)((0,
|
|
12943
|
+
if ((0, import_node_fs6.existsSync)((0, import_node_path11.join)(dir, "station.css"))) {
|
|
12603
12944
|
return dir;
|
|
12604
12945
|
}
|
|
12605
12946
|
}
|
|
12606
12947
|
throw new Error("Report assets missing. Run `npm run sync-report-assets` and `npm run build` in packages/cli.");
|
|
12607
12948
|
}
|
|
12608
|
-
var import_node_fs6,
|
|
12949
|
+
var import_node_fs6, import_node_path11, REPORT_ASSET_FILES;
|
|
12609
12950
|
var init_reportAssets = __esm({
|
|
12610
12951
|
"src/report/reportAssets.ts"() {
|
|
12611
12952
|
"use strict";
|
|
12612
12953
|
import_node_fs6 = require("node:fs");
|
|
12613
|
-
|
|
12954
|
+
import_node_path11 = require("node:path");
|
|
12614
12955
|
REPORT_ASSET_FILES = [
|
|
12615
12956
|
"station.css",
|
|
12616
12957
|
"station.js",
|
|
@@ -12641,8 +12982,8 @@ function stripWindowsDrive(value) {
|
|
|
12641
12982
|
}
|
|
12642
12983
|
function stripAbsolutePrefix(value) {
|
|
12643
12984
|
const normalized = slashPath(value);
|
|
12644
|
-
const
|
|
12645
|
-
if (!
|
|
12985
|
+
const isAbsolute3 = /^[a-z]:\//i.test(normalized) || normalized.startsWith("/");
|
|
12986
|
+
if (!isAbsolute3) {
|
|
12646
12987
|
return normalized;
|
|
12647
12988
|
}
|
|
12648
12989
|
const lower = normalized.toLowerCase();
|
|
@@ -12838,11 +13179,11 @@ function emptyActionRegistry() {
|
|
|
12838
13179
|
return { version: 1, nextId: 1, actions: {} };
|
|
12839
13180
|
}
|
|
12840
13181
|
function actionRegistryPath(cwd) {
|
|
12841
|
-
return (0,
|
|
13182
|
+
return (0, import_node_path12.join)(getProjectArtifactsDir(cwd), "action-registry.json");
|
|
12842
13183
|
}
|
|
12843
13184
|
async function loadActionRegistry(cwd) {
|
|
12844
13185
|
try {
|
|
12845
|
-
const parsed = JSON.parse(await (0,
|
|
13186
|
+
const parsed = JSON.parse(await (0, import_promises9.readFile)(actionRegistryPath(cwd), "utf8"));
|
|
12846
13187
|
if (parsed.version === 1 && Number.isInteger(parsed.nextId) && parsed.actions) {
|
|
12847
13188
|
return parsed;
|
|
12848
13189
|
}
|
|
@@ -12852,8 +13193,8 @@ async function loadActionRegistry(cwd) {
|
|
|
12852
13193
|
}
|
|
12853
13194
|
async function saveActionRegistry(cwd, registry) {
|
|
12854
13195
|
const dir = getProjectArtifactsDir(cwd);
|
|
12855
|
-
await (0,
|
|
12856
|
-
await (0,
|
|
13196
|
+
await (0, import_promises9.mkdir)(dir, { recursive: true });
|
|
13197
|
+
await (0, import_promises9.writeFile)(actionRegistryPath(cwd), `${JSON.stringify(registry, null, 2)}
|
|
12857
13198
|
`, "utf8");
|
|
12858
13199
|
}
|
|
12859
13200
|
function nextActionId(registry) {
|
|
@@ -12946,12 +13287,12 @@ async function resolveActionById(cwd, id) {
|
|
|
12946
13287
|
const registry = await loadActionRegistry(cwd);
|
|
12947
13288
|
return Object.values(registry.actions).find((entry) => entry.id === id);
|
|
12948
13289
|
}
|
|
12949
|
-
var
|
|
13290
|
+
var import_promises9, import_node_path12;
|
|
12950
13291
|
var init_registry2 = __esm({
|
|
12951
13292
|
"src/actions/registry.ts"() {
|
|
12952
13293
|
"use strict";
|
|
12953
|
-
|
|
12954
|
-
|
|
13294
|
+
import_promises9 = require("node:fs/promises");
|
|
13295
|
+
import_node_path12 = require("node:path");
|
|
12955
13296
|
init_config();
|
|
12956
13297
|
init_canonical();
|
|
12957
13298
|
}
|
|
@@ -12983,8 +13324,8 @@ async function writeActionArtifacts(input) {
|
|
|
12983
13324
|
gateStatus: gateStatus2(input.artifact),
|
|
12984
13325
|
actions: assigned.actions
|
|
12985
13326
|
};
|
|
12986
|
-
const actionsPath = (0,
|
|
12987
|
-
await (0,
|
|
13327
|
+
const actionsPath = (0, import_node_path13.join)(getProjectArtifactsDir(input.cwd), "actions.json");
|
|
13328
|
+
await (0, import_promises10.writeFile)(actionsPath, `${JSON.stringify(manifest, null, 2)}
|
|
12988
13329
|
`, "utf8");
|
|
12989
13330
|
return {
|
|
12990
13331
|
actionsPath,
|
|
@@ -12992,12 +13333,12 @@ async function writeActionArtifacts(input) {
|
|
|
12992
13333
|
manifest
|
|
12993
13334
|
};
|
|
12994
13335
|
}
|
|
12995
|
-
var
|
|
13336
|
+
var import_promises10, import_node_path13;
|
|
12996
13337
|
var init_artifacts = __esm({
|
|
12997
13338
|
"src/actions/artifacts.ts"() {
|
|
12998
13339
|
"use strict";
|
|
12999
|
-
|
|
13000
|
-
|
|
13340
|
+
import_promises10 = require("node:fs/promises");
|
|
13341
|
+
import_node_path13 = require("node:path");
|
|
13001
13342
|
init_config();
|
|
13002
13343
|
init_derive();
|
|
13003
13344
|
init_registry2();
|
|
@@ -13007,25 +13348,25 @@ var init_artifacts = __esm({
|
|
|
13007
13348
|
// src/artifacts.ts
|
|
13008
13349
|
async function copyReportAssets(reportAssetsDir) {
|
|
13009
13350
|
const sourceDir = getBundledReportAssetsDir();
|
|
13010
|
-
await (0,
|
|
13351
|
+
await (0, import_promises11.mkdir)((0, import_node_path14.join)(reportAssetsDir, "assets"), { recursive: true });
|
|
13011
13352
|
for (const rel of REPORT_ASSET_FILES) {
|
|
13012
|
-
await (0,
|
|
13353
|
+
await (0, import_promises11.copyFile)((0, import_node_path14.join)(sourceDir, rel), (0, import_node_path14.join)(reportAssetsDir, rel));
|
|
13013
13354
|
}
|
|
13014
13355
|
}
|
|
13015
13356
|
async function writeScanArtifacts(options) {
|
|
13016
13357
|
const cwd = options.cwd ?? options.artifact.workspacePath;
|
|
13017
13358
|
const dir = getProjectArtifactsDir(cwd);
|
|
13018
|
-
await (0,
|
|
13019
|
-
const jsonPath = (0,
|
|
13020
|
-
const gateResultPath = (0,
|
|
13021
|
-
const contextMapPath = (0,
|
|
13022
|
-
const gapsDir = (0,
|
|
13023
|
-
const tasklistPath = (0,
|
|
13024
|
-
const summaryPath = (0,
|
|
13025
|
-
const playbookPath = (0,
|
|
13026
|
-
const reportPath = (0,
|
|
13027
|
-
const reportAssetsDir = (0,
|
|
13028
|
-
await (0,
|
|
13359
|
+
await (0, import_promises11.mkdir)(dir, { recursive: true });
|
|
13360
|
+
const jsonPath = (0, import_node_path14.join)(dir, "last-scan.json");
|
|
13361
|
+
const gateResultPath = (0, import_node_path14.join)(dir, "gate-result.json");
|
|
13362
|
+
const contextMapPath = (0, import_node_path14.join)(dir, "context-map.json");
|
|
13363
|
+
const gapsDir = (0, import_node_path14.join)(dir, "gaps");
|
|
13364
|
+
const tasklistPath = (0, import_node_path14.join)(dir, "agent-tasklist.md");
|
|
13365
|
+
const summaryPath = (0, import_node_path14.join)(dir, "agent-summary.md");
|
|
13366
|
+
const playbookPath = (0, import_node_path14.join)(dir, "launch-playbook.md");
|
|
13367
|
+
const reportPath = (0, import_node_path14.join)(dir, "report.html");
|
|
13368
|
+
const reportAssetsDir = (0, import_node_path14.join)(dir, "report");
|
|
13369
|
+
await (0, import_promises11.mkdir)(gapsDir, { recursive: true });
|
|
13029
13370
|
const safe2 = sanitizeArtifactForDisk(options.artifact);
|
|
13030
13371
|
const json = `${JSON.stringify(safe2, null, 2)}
|
|
13031
13372
|
`;
|
|
@@ -13040,26 +13381,26 @@ async function writeScanArtifacts(options) {
|
|
|
13040
13381
|
`;
|
|
13041
13382
|
const gapEvidenceFiles = generateGapEvidenceFiles(safe2);
|
|
13042
13383
|
await copyReportAssets(reportAssetsDir);
|
|
13043
|
-
await (0,
|
|
13044
|
-
await (0,
|
|
13384
|
+
await (0, import_promises11.writeFile)(gateResultPath, gateResult, "utf-8");
|
|
13385
|
+
await (0, import_promises11.writeFile)(contextMapPath, contextMap, "utf-8");
|
|
13045
13386
|
for (const gap of gapEvidenceFiles) {
|
|
13046
|
-
await (0,
|
|
13387
|
+
await (0, import_promises11.writeFile)((0, import_node_path14.join)(dir, "gaps", `${gap.content.id}.json`), `${JSON.stringify(gap.content, null, 2)}
|
|
13047
13388
|
`, "utf-8");
|
|
13048
13389
|
}
|
|
13049
|
-
await (0,
|
|
13390
|
+
await (0, import_promises11.writeFile)(tasklistPath, tasklist, "utf-8");
|
|
13050
13391
|
let architectureMapPath;
|
|
13051
13392
|
if (typeof safe2.architectureMap === "string" && safe2.architectureMap.length > 0) {
|
|
13052
|
-
architectureMapPath = (0,
|
|
13053
|
-
await (0,
|
|
13393
|
+
architectureMapPath = (0, import_node_path14.join)(dir, "architecture-map.md");
|
|
13394
|
+
await (0, import_promises11.writeFile)(architectureMapPath, safe2.architectureMap, "utf-8");
|
|
13054
13395
|
}
|
|
13055
13396
|
if (safe2.architectureData) {
|
|
13056
|
-
await (0,
|
|
13397
|
+
await (0, import_promises11.writeFile)((0, import_node_path14.join)(dir, "architecture-map.json"), `${JSON.stringify(safe2.architectureData, null, 2)}
|
|
13057
13398
|
`, "utf-8");
|
|
13058
13399
|
}
|
|
13059
|
-
await (0,
|
|
13060
|
-
await (0,
|
|
13061
|
-
await (0,
|
|
13062
|
-
await (0,
|
|
13400
|
+
await (0, import_promises11.writeFile)(jsonPath, json, "utf-8");
|
|
13401
|
+
await (0, import_promises11.writeFile)(summaryPath, summary, "utf-8");
|
|
13402
|
+
await (0, import_promises11.writeFile)(playbookPath, playbook, "utf-8");
|
|
13403
|
+
await (0, import_promises11.writeFile)(reportPath, html, "utf-8");
|
|
13063
13404
|
const actionArtifacts = await writeActionArtifacts({
|
|
13064
13405
|
cwd,
|
|
13065
13406
|
artifact: safe2,
|
|
@@ -13082,12 +13423,12 @@ async function writeScanArtifacts(options) {
|
|
|
13082
13423
|
...architectureMapPath ? { architectureMapPath } : {}
|
|
13083
13424
|
};
|
|
13084
13425
|
}
|
|
13085
|
-
var
|
|
13426
|
+
var import_promises11, import_node_path14;
|
|
13086
13427
|
var init_artifacts2 = __esm({
|
|
13087
13428
|
"src/artifacts.ts"() {
|
|
13088
13429
|
"use strict";
|
|
13089
|
-
|
|
13090
|
-
|
|
13430
|
+
import_promises11 = require("node:fs/promises");
|
|
13431
|
+
import_node_path14 = require("node:path");
|
|
13091
13432
|
init_contextMap();
|
|
13092
13433
|
init_gateResult();
|
|
13093
13434
|
init_gapEvidence();
|
|
@@ -13196,12 +13537,12 @@ function createAppWindowOpenCommands(target, platform = process.platform, env =
|
|
|
13196
13537
|
];
|
|
13197
13538
|
if (platform === "win32") {
|
|
13198
13539
|
const candidates = [
|
|
13199
|
-
env.LOCALAPPDATA ? (0,
|
|
13200
|
-
env.PROGRAMFILES ? (0,
|
|
13201
|
-
env["PROGRAMFILES(X86)"] ? (0,
|
|
13202
|
-
env.LOCALAPPDATA ? (0,
|
|
13203
|
-
env.PROGRAMFILES ? (0,
|
|
13204
|
-
env["PROGRAMFILES(X86)"] ? (0,
|
|
13540
|
+
env.LOCALAPPDATA ? (0, import_node_path15.join)(env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe") : "",
|
|
13541
|
+
env.PROGRAMFILES ? (0, import_node_path15.join)(env.PROGRAMFILES, "Google", "Chrome", "Application", "chrome.exe") : "",
|
|
13542
|
+
env["PROGRAMFILES(X86)"] ? (0, import_node_path15.join)(env["PROGRAMFILES(X86)"], "Google", "Chrome", "Application", "chrome.exe") : "",
|
|
13543
|
+
env.LOCALAPPDATA ? (0, import_node_path15.join)(env.LOCALAPPDATA, "Microsoft", "Edge", "Application", "msedge.exe") : "",
|
|
13544
|
+
env.PROGRAMFILES ? (0, import_node_path15.join)(env.PROGRAMFILES, "Microsoft", "Edge", "Application", "msedge.exe") : "",
|
|
13545
|
+
env["PROGRAMFILES(X86)"] ? (0, import_node_path15.join)(env["PROGRAMFILES(X86)"], "Microsoft", "Edge", "Application", "msedge.exe") : "",
|
|
13205
13546
|
"chrome",
|
|
13206
13547
|
"msedge"
|
|
13207
13548
|
].filter(Boolean);
|
|
@@ -13222,12 +13563,12 @@ function createAppWindowOpenCommands(target, platform = process.platform, env =
|
|
|
13222
13563
|
}
|
|
13223
13564
|
async function openWithSystemDefault(target) {
|
|
13224
13565
|
const { command, args, shell } = createOpenCommand(target);
|
|
13225
|
-
await new Promise((
|
|
13566
|
+
await new Promise((resolve9, reject) => {
|
|
13226
13567
|
const child = (0, import_node_child_process4.spawn)(command, args, { stdio: "ignore", shell });
|
|
13227
13568
|
child.on("error", reject);
|
|
13228
13569
|
child.on("exit", (code) => {
|
|
13229
13570
|
if (code === 0) {
|
|
13230
|
-
|
|
13571
|
+
resolve9();
|
|
13231
13572
|
} else {
|
|
13232
13573
|
reject(new Error(`Could not open browser (exit ${code ?? "unknown"}). Open manually: ${target}`));
|
|
13233
13574
|
}
|
|
@@ -13238,7 +13579,7 @@ async function spawnDetached(command) {
|
|
|
13238
13579
|
if (/^[A-Za-z]:[\\/]/.test(command.command) && !(0, import_node_fs7.existsSync)(command.command)) {
|
|
13239
13580
|
throw new Error(`Browser not found: ${command.command}`);
|
|
13240
13581
|
}
|
|
13241
|
-
await new Promise((
|
|
13582
|
+
await new Promise((resolve9, reject) => {
|
|
13242
13583
|
const child = (0, import_node_child_process4.spawn)(command.command, command.args, {
|
|
13243
13584
|
detached: true,
|
|
13244
13585
|
stdio: "ignore",
|
|
@@ -13246,7 +13587,7 @@ async function spawnDetached(command) {
|
|
|
13246
13587
|
});
|
|
13247
13588
|
child.once("spawn", () => {
|
|
13248
13589
|
child.unref();
|
|
13249
|
-
|
|
13590
|
+
resolve9();
|
|
13250
13591
|
});
|
|
13251
13592
|
child.once("error", reject);
|
|
13252
13593
|
});
|
|
@@ -13278,13 +13619,13 @@ async function openUrlInBrowser(url) {
|
|
|
13278
13619
|
}
|
|
13279
13620
|
await openWithSystemDefault(trimmed);
|
|
13280
13621
|
}
|
|
13281
|
-
var import_node_child_process4, import_node_fs7,
|
|
13622
|
+
var import_node_child_process4, import_node_fs7, import_node_path15;
|
|
13282
13623
|
var init_openBrowser = __esm({
|
|
13283
13624
|
"src/openBrowser.ts"() {
|
|
13284
13625
|
"use strict";
|
|
13285
13626
|
import_node_child_process4 = require("node:child_process");
|
|
13286
13627
|
import_node_fs7 = require("node:fs");
|
|
13287
|
-
|
|
13628
|
+
import_node_path15 = require("node:path");
|
|
13288
13629
|
}
|
|
13289
13630
|
});
|
|
13290
13631
|
|
|
@@ -13314,12 +13655,12 @@ async function copyToClipboard(text) {
|
|
|
13314
13655
|
}
|
|
13315
13656
|
}
|
|
13316
13657
|
function pipeToCommand(command, text, extraArgs = []) {
|
|
13317
|
-
return new Promise((
|
|
13658
|
+
return new Promise((resolve9, reject) => {
|
|
13318
13659
|
const child = (0, import_node_child_process5.spawn)(command, extraArgs, { stdio: ["pipe", "ignore", "ignore"] });
|
|
13319
13660
|
child.on("error", reject);
|
|
13320
13661
|
child.on("close", (code) => {
|
|
13321
13662
|
if (code === 0) {
|
|
13322
|
-
|
|
13663
|
+
resolve9();
|
|
13323
13664
|
} else {
|
|
13324
13665
|
reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
|
|
13325
13666
|
}
|
|
@@ -14226,7 +14567,7 @@ var VERSION;
|
|
|
14226
14567
|
var init_version = __esm({
|
|
14227
14568
|
"src/version.ts"() {
|
|
14228
14569
|
"use strict";
|
|
14229
|
-
VERSION = "1.5.
|
|
14570
|
+
VERSION = "1.5.2";
|
|
14230
14571
|
}
|
|
14231
14572
|
});
|
|
14232
14573
|
|
|
@@ -14621,25 +14962,25 @@ async function initCursorRulesPack(options) {
|
|
|
14621
14962
|
const pack = buildCursorRulesPack();
|
|
14622
14963
|
for (const rule of pack) {
|
|
14623
14964
|
const file = `${CURSOR_RULES_DIR}/${rule.filename}`;
|
|
14624
|
-
const path = (0,
|
|
14965
|
+
const path = (0, import_node_path16.join)(options.cwd, file);
|
|
14625
14966
|
const existing = await readExistingFile(path);
|
|
14626
14967
|
const changed = !existing.exists || existing.content !== rule.content;
|
|
14627
14968
|
const action = !existing.exists ? "created" : changed ? "updated" : "unchanged";
|
|
14628
14969
|
if (!options.dryRun && changed) {
|
|
14629
|
-
await (0,
|
|
14630
|
-
await (0,
|
|
14970
|
+
await (0, import_promises12.mkdir)((0, import_node_path16.join)(options.cwd, CURSOR_RULES_DIR), { recursive: true });
|
|
14971
|
+
await (0, import_promises12.writeFile)(path, rule.content, "utf-8");
|
|
14631
14972
|
}
|
|
14632
14973
|
results.push({ target: "cursor", file, path, action });
|
|
14633
14974
|
}
|
|
14634
|
-
const legacyPath = (0,
|
|
14975
|
+
const legacyPath = (0, import_node_path16.join)(options.cwd, LEGACY_CURSOR_RULE_FILE);
|
|
14635
14976
|
if (!options.dryRun && await fileExists(legacyPath)) {
|
|
14636
|
-
await (0,
|
|
14977
|
+
await (0, import_promises12.rm)(legacyPath, { force: true });
|
|
14637
14978
|
}
|
|
14638
14979
|
return results;
|
|
14639
14980
|
}
|
|
14640
14981
|
async function readExistingFile(path) {
|
|
14641
14982
|
try {
|
|
14642
|
-
return { exists: true, content: await (0,
|
|
14983
|
+
return { exists: true, content: await (0, import_promises12.readFile)(path, "utf-8") };
|
|
14643
14984
|
} catch (error) {
|
|
14644
14985
|
if (isFileNotFoundError(error)) {
|
|
14645
14986
|
return { exists: false, content: "" };
|
|
@@ -14649,7 +14990,7 @@ async function readExistingFile(path) {
|
|
|
14649
14990
|
}
|
|
14650
14991
|
async function fileExists(path) {
|
|
14651
14992
|
try {
|
|
14652
|
-
await (0,
|
|
14993
|
+
await (0, import_promises12.access)(path);
|
|
14653
14994
|
return true;
|
|
14654
14995
|
} catch {
|
|
14655
14996
|
return false;
|
|
@@ -14658,12 +14999,12 @@ async function fileExists(path) {
|
|
|
14658
14999
|
function isFileNotFoundError(error) {
|
|
14659
15000
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
14660
15001
|
}
|
|
14661
|
-
var
|
|
15002
|
+
var import_promises12, import_node_path16, CURSOR_RULES_DIR, LEGACY_CURSOR_RULE_FILE, DOMAIN_PATH_POINTER;
|
|
14662
15003
|
var init_cursorRulesPack = __esm({
|
|
14663
15004
|
"src/commands/cursorRulesPack.ts"() {
|
|
14664
15005
|
"use strict";
|
|
14665
|
-
|
|
14666
|
-
|
|
15006
|
+
import_promises12 = require("node:fs/promises");
|
|
15007
|
+
import_node_path16 = require("node:path");
|
|
14667
15008
|
init_commands();
|
|
14668
15009
|
CURSOR_RULES_DIR = ".cursor/rules";
|
|
14669
15010
|
LEGACY_CURSOR_RULE_FILE = `${CURSOR_RULES_DIR}/viberaven.mdc`;
|
|
@@ -14763,11 +15104,11 @@ var init_agentTargets = __esm({
|
|
|
14763
15104
|
|
|
14764
15105
|
// src/commands/seedPackageJsonScripts.ts
|
|
14765
15106
|
async function seedPackageJsonScripts(options) {
|
|
14766
|
-
const packageJsonPath = (0,
|
|
15107
|
+
const packageJsonPath = (0, import_node_path17.join)(options.cwd, "package.json");
|
|
14767
15108
|
if (!(0, import_node_fs8.existsSync)(packageJsonPath)) {
|
|
14768
15109
|
return null;
|
|
14769
15110
|
}
|
|
14770
|
-
const raw = await (0,
|
|
15111
|
+
const raw = await (0, import_promises13.readFile)(packageJsonPath, "utf-8");
|
|
14771
15112
|
let pkg;
|
|
14772
15113
|
try {
|
|
14773
15114
|
pkg = JSON.parse(raw);
|
|
@@ -14797,17 +15138,17 @@ async function seedPackageJsonScripts(options) {
|
|
|
14797
15138
|
pkg.scripts = scripts;
|
|
14798
15139
|
const output = `${JSON.stringify(pkg, null, 2)}
|
|
14799
15140
|
`;
|
|
14800
|
-
await (0,
|
|
15141
|
+
await (0, import_promises13.writeFile)(packageJsonPath, output, "utf-8");
|
|
14801
15142
|
}
|
|
14802
15143
|
return { action: "updated", added, skipped, changed: true };
|
|
14803
15144
|
}
|
|
14804
|
-
var import_node_fs8,
|
|
15145
|
+
var import_node_fs8, import_promises13, import_node_path17, VIBERAVEN_PACKAGE_JSON_SCRIPTS;
|
|
14805
15146
|
var init_seedPackageJsonScripts = __esm({
|
|
14806
15147
|
"src/commands/seedPackageJsonScripts.ts"() {
|
|
14807
15148
|
"use strict";
|
|
14808
15149
|
import_node_fs8 = require("node:fs");
|
|
14809
|
-
|
|
14810
|
-
|
|
15150
|
+
import_promises13 = require("node:fs/promises");
|
|
15151
|
+
import_node_path17 = require("node:path");
|
|
14811
15152
|
init_commands();
|
|
14812
15153
|
VIBERAVEN_PACKAGE_JSON_SCRIPTS = {
|
|
14813
15154
|
"viberaven:gate": PUBLIC_AGENT_MODE_COMMAND,
|
|
@@ -14827,13 +15168,13 @@ async function initAgentRules(options) {
|
|
|
14827
15168
|
continue;
|
|
14828
15169
|
}
|
|
14829
15170
|
const file = AGENT_RULE_TARGETS[target].file;
|
|
14830
|
-
const path = (0,
|
|
15171
|
+
const path = (0, import_node_path18.join)(options.cwd, file);
|
|
14831
15172
|
const existing = await readExistingFile2(path);
|
|
14832
15173
|
const injected = injectAgentRulesBlock(existing.content, renderAgentRulesForTarget(target));
|
|
14833
15174
|
const action = !existing.exists ? "created" : injected.changed ? "updated" : "unchanged";
|
|
14834
15175
|
if (!options.dryRun && injected.changed) {
|
|
14835
|
-
await (0,
|
|
14836
|
-
await (0,
|
|
15176
|
+
await (0, import_promises14.mkdir)((0, import_node_path18.dirname)(path), { recursive: true });
|
|
15177
|
+
await (0, import_promises14.writeFile)(path, injected.content, "utf-8");
|
|
14837
15178
|
}
|
|
14838
15179
|
results.push({ target, file, path, action });
|
|
14839
15180
|
}
|
|
@@ -14901,7 +15242,7 @@ function formatAgentRulesInitSummary(output) {
|
|
|
14901
15242
|
}
|
|
14902
15243
|
async function readExistingFile2(path) {
|
|
14903
15244
|
try {
|
|
14904
|
-
return { exists: true, content: await (0,
|
|
15245
|
+
return { exists: true, content: await (0, import_promises14.readFile)(path, "utf-8") };
|
|
14905
15246
|
} catch (error) {
|
|
14906
15247
|
if (isFileNotFoundError2(error)) {
|
|
14907
15248
|
return { exists: false, content: "" };
|
|
@@ -14912,12 +15253,12 @@ async function readExistingFile2(path) {
|
|
|
14912
15253
|
function isFileNotFoundError2(error) {
|
|
14913
15254
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
14914
15255
|
}
|
|
14915
|
-
var
|
|
15256
|
+
var import_promises14, import_node_path18;
|
|
14916
15257
|
var init_initRules = __esm({
|
|
14917
15258
|
"src/commands/initRules.ts"() {
|
|
14918
15259
|
"use strict";
|
|
14919
|
-
|
|
14920
|
-
|
|
15260
|
+
import_promises14 = require("node:fs/promises");
|
|
15261
|
+
import_node_path18 = require("node:path");
|
|
14921
15262
|
init_agentRulesBlock();
|
|
14922
15263
|
init_agentTargets();
|
|
14923
15264
|
init_cursorRulesPack();
|
|
@@ -15180,7 +15521,7 @@ async function runInteractiveSession(startDir = process.cwd()) {
|
|
|
15180
15521
|
Ie(`${import_picocolors4.default.bold("VibeRaven")} ${import_picocolors4.default.dim(VERSION)}`);
|
|
15181
15522
|
const cwd = await resolveWorkspaceRoot(startDir);
|
|
15182
15523
|
const artifactsAt = await findArtifactsWorkspace(startDir);
|
|
15183
|
-
if (artifactsAt && (0,
|
|
15524
|
+
if (artifactsAt && (0, import_node_path19.resolve)(artifactsAt) !== (0, import_node_path19.resolve)(startDir)) {
|
|
15184
15525
|
M2.message(import_picocolors4.default.dim(`Using scan from: ${artifactsAt}`));
|
|
15185
15526
|
} else {
|
|
15186
15527
|
M2.message(import_picocolors4.default.dim(`Project folder: ${cwd}`));
|
|
@@ -15239,11 +15580,11 @@ async function runInteractiveSession(startDir = process.cwd()) {
|
|
|
15239
15580
|
}
|
|
15240
15581
|
Se(import_picocolors4.default.dim("Run viberaven anytime for the interactive menu."));
|
|
15241
15582
|
}
|
|
15242
|
-
var
|
|
15583
|
+
var import_node_path19, import_picocolors4;
|
|
15243
15584
|
var init_runInteractive = __esm({
|
|
15244
15585
|
"src/tui/runInteractive.ts"() {
|
|
15245
15586
|
"use strict";
|
|
15246
|
-
|
|
15587
|
+
import_node_path19 = require("node:path");
|
|
15247
15588
|
init_dist2();
|
|
15248
15589
|
import_picocolors4 = __toESM(require_picocolors());
|
|
15249
15590
|
init_config();
|
|
@@ -15268,18 +15609,18 @@ var init_runInteractive = __esm({
|
|
|
15268
15609
|
// src/commands/condense.ts
|
|
15269
15610
|
async function runCondenseCommand(options) {
|
|
15270
15611
|
const dir = getProjectArtifactsDir(options.cwd);
|
|
15271
|
-
const artifact = JSON.parse(await (0,
|
|
15272
|
-
const contextMapPath = (0,
|
|
15273
|
-
await (0,
|
|
15612
|
+
const artifact = JSON.parse(await (0, import_promises15.readFile)((0, import_node_path20.join)(dir, "last-scan.json"), "utf8"));
|
|
15613
|
+
const contextMapPath = (0, import_node_path20.join)(dir, "context-map.json");
|
|
15614
|
+
await (0, import_promises15.writeFile)(contextMapPath, `${JSON.stringify(generateContextMap(artifact), null, 2)}
|
|
15274
15615
|
`, "utf8");
|
|
15275
15616
|
return { contextMapPath };
|
|
15276
15617
|
}
|
|
15277
|
-
var
|
|
15618
|
+
var import_promises15, import_node_path20;
|
|
15278
15619
|
var init_condense = __esm({
|
|
15279
15620
|
"src/commands/condense.ts"() {
|
|
15280
15621
|
"use strict";
|
|
15281
|
-
|
|
15282
|
-
|
|
15622
|
+
import_promises15 = require("node:fs/promises");
|
|
15623
|
+
import_node_path20 = require("node:path");
|
|
15283
15624
|
init_config();
|
|
15284
15625
|
init_contextMap();
|
|
15285
15626
|
}
|
|
@@ -15287,9 +15628,9 @@ var init_condense = __esm({
|
|
|
15287
15628
|
|
|
15288
15629
|
// src/heal/pathSafety.ts
|
|
15289
15630
|
function assertSafeHealTarget(cwd, target) {
|
|
15290
|
-
const root = (0,
|
|
15291
|
-
const absolute = (0,
|
|
15292
|
-
const rel = (0,
|
|
15631
|
+
const root = (0, import_node_path21.resolve)(cwd);
|
|
15632
|
+
const absolute = (0, import_node_path21.resolve)(root, target);
|
|
15633
|
+
const rel = (0, import_node_path21.relative)(root, absolute);
|
|
15293
15634
|
if (rel.startsWith("..") || rel === "" || /^[A-Za-z]:/.test(rel)) {
|
|
15294
15635
|
throw new Error("Heal target must stay inside the workspace");
|
|
15295
15636
|
}
|
|
@@ -15299,11 +15640,11 @@ function assertSafeHealTarget(cwd, target) {
|
|
|
15299
15640
|
}
|
|
15300
15641
|
return absolute;
|
|
15301
15642
|
}
|
|
15302
|
-
var
|
|
15643
|
+
var import_node_path21, BLOCKED_SEGMENTS;
|
|
15303
15644
|
var init_pathSafety = __esm({
|
|
15304
15645
|
"src/heal/pathSafety.ts"() {
|
|
15305
15646
|
"use strict";
|
|
15306
|
-
|
|
15647
|
+
import_node_path21 = require("node:path");
|
|
15307
15648
|
BLOCKED_SEGMENTS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".viberaven"]);
|
|
15308
15649
|
}
|
|
15309
15650
|
});
|
|
@@ -15765,7 +16106,7 @@ export const config = {
|
|
|
15765
16106
|
// src/heal/recipes/eslintRestrictedImports.ts
|
|
15766
16107
|
function detectEslintConfigFile(cwd) {
|
|
15767
16108
|
for (const candidate of ESLINT_CONFIG_CANDIDATES) {
|
|
15768
|
-
if ((0, import_node_fs9.existsSync)((0,
|
|
16109
|
+
if ((0, import_node_fs9.existsSync)((0, import_node_path22.join)(cwd, candidate))) {
|
|
15769
16110
|
return candidate;
|
|
15770
16111
|
}
|
|
15771
16112
|
}
|
|
@@ -15920,12 +16261,12 @@ function applyEslintRestrictedImportsRecipe(source, configFile) {
|
|
|
15920
16261
|
}
|
|
15921
16262
|
return applyLegacyModuleRecipe(source);
|
|
15922
16263
|
}
|
|
15923
|
-
var import_node_fs9,
|
|
16264
|
+
var import_node_fs9, import_node_path22, VIBERAVEN_ESLINT_MARKER, RESTRICTED_IMPORTS_MESSAGE, RESTRICTED_PATHS, RESTRICTED_PATTERNS, ESLINT_CONFIG_CANDIDATES;
|
|
15924
16265
|
var init_eslintRestrictedImports = __esm({
|
|
15925
16266
|
"src/heal/recipes/eslintRestrictedImports.ts"() {
|
|
15926
16267
|
"use strict";
|
|
15927
16268
|
import_node_fs9 = require("node:fs");
|
|
15928
|
-
|
|
16269
|
+
import_node_path22 = require("node:path");
|
|
15929
16270
|
init_commands();
|
|
15930
16271
|
VIBERAVEN_ESLINT_MARKER = "VibeRaven heal: eslint_restricted_imports";
|
|
15931
16272
|
RESTRICTED_IMPORTS_MESSAGE = `Restricted import. Run ${PUBLIC_AGENT_MODE_COMMAND} before substituting packages.`;
|
|
@@ -15977,13 +16318,13 @@ function defaultTargetFile(gapId) {
|
|
|
15977
16318
|
}
|
|
15978
16319
|
async function readSourceOrEmpty(absolutePath) {
|
|
15979
16320
|
if (!(0, import_node_fs10.existsSync)(absolutePath)) return "";
|
|
15980
|
-
return (0,
|
|
16321
|
+
return (0, import_promises16.readFile)(absolutePath, "utf8");
|
|
15981
16322
|
}
|
|
15982
16323
|
async function detectUpstash(cwd) {
|
|
15983
16324
|
try {
|
|
15984
|
-
const pkgPath = (0,
|
|
16325
|
+
const pkgPath = (0, import_node_path23.join)(cwd, "package.json");
|
|
15985
16326
|
if (!(0, import_node_fs10.existsSync)(pkgPath)) return false;
|
|
15986
|
-
const raw = await (0,
|
|
16327
|
+
const raw = await (0, import_promises16.readFile)(pkgPath, "utf8");
|
|
15987
16328
|
const pkg = JSON.parse(raw);
|
|
15988
16329
|
const deps = {
|
|
15989
16330
|
...pkg["dependencies"] ?? {},
|
|
@@ -15998,7 +16339,7 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
|
|
|
15998
16339
|
const targetFile = explicitTarget ?? defaultTargetFile(gapId);
|
|
15999
16340
|
if (targetFile === void 0) return null;
|
|
16000
16341
|
if (gapId === "auth_secret_missing" || gapId === "node_env_not_set" || gapId === "database_url_missing") {
|
|
16001
|
-
const absolutePath = (0,
|
|
16342
|
+
const absolutePath = (0, import_node_path23.join)(cwd, targetFile);
|
|
16002
16343
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16003
16344
|
let result;
|
|
16004
16345
|
if (gapId === "auth_secret_missing") result = applyAuthSecretRecipe(source);
|
|
@@ -16012,25 +16353,25 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
|
|
|
16012
16353
|
};
|
|
16013
16354
|
}
|
|
16014
16355
|
if (gapId === "missing_error_boundary") {
|
|
16015
|
-
const absolutePath = (0,
|
|
16356
|
+
const absolutePath = (0, import_node_path23.join)(cwd, "app/error.tsx");
|
|
16016
16357
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16017
16358
|
const result = applyErrorBoundaryRecipe(source);
|
|
16018
16359
|
return { ...result, canAutoApply: true, recipeName: gapId };
|
|
16019
16360
|
}
|
|
16020
16361
|
if (gapId === "missing_health_route") {
|
|
16021
|
-
const absolutePath = (0,
|
|
16362
|
+
const absolutePath = (0, import_node_path23.join)(cwd, "app/api/health/route.ts");
|
|
16022
16363
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16023
16364
|
const result = applyHealthRouteRecipe(source);
|
|
16024
16365
|
return { ...result, canAutoApply: true, recipeName: gapId };
|
|
16025
16366
|
}
|
|
16026
16367
|
if (gapId === "missing_loading_state") {
|
|
16027
|
-
const absolutePath = (0,
|
|
16368
|
+
const absolutePath = (0, import_node_path23.join)(cwd, "app/loading.tsx");
|
|
16028
16369
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16029
16370
|
const result = applyLoadingStateRecipe(source);
|
|
16030
16371
|
return { ...result, canAutoApply: true, recipeName: gapId };
|
|
16031
16372
|
}
|
|
16032
16373
|
if (gapId === "missing_404_page") {
|
|
16033
|
-
const absolutePath = (0,
|
|
16374
|
+
const absolutePath = (0, import_node_path23.join)(cwd, "app/not-found.tsx");
|
|
16034
16375
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16035
16376
|
const result = applyNotFoundRecipe(source);
|
|
16036
16377
|
return { ...result, canAutoApply: true, recipeName: gapId };
|
|
@@ -16048,10 +16389,10 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
|
|
|
16048
16389
|
}
|
|
16049
16390
|
if (gapId === "missing_csp_header") {
|
|
16050
16391
|
let configFile = "next.config.js";
|
|
16051
|
-
let absolutePath = (0,
|
|
16392
|
+
let absolutePath = (0, import_node_path23.join)(cwd, configFile);
|
|
16052
16393
|
if (!(0, import_node_fs10.existsSync)(absolutePath)) {
|
|
16053
16394
|
configFile = "next.config.mjs";
|
|
16054
|
-
absolutePath = (0,
|
|
16395
|
+
absolutePath = (0, import_node_path23.join)(cwd, configFile);
|
|
16055
16396
|
}
|
|
16056
16397
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16057
16398
|
const result = applyCspHeaderRecipe(source);
|
|
@@ -16063,7 +16404,7 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
|
|
|
16063
16404
|
}
|
|
16064
16405
|
if (gapId === "missing_rate_limit") {
|
|
16065
16406
|
const hasUpstash = await detectUpstash(cwd);
|
|
16066
|
-
const middlewarePath = (0,
|
|
16407
|
+
const middlewarePath = (0, import_node_path23.join)(cwd, "middleware.ts");
|
|
16067
16408
|
const source = await readSourceOrEmpty(middlewarePath);
|
|
16068
16409
|
const result = applyRateLimitRecipe(source, hasUpstash);
|
|
16069
16410
|
return {
|
|
@@ -16085,7 +16426,7 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
|
|
|
16085
16426
|
recipeName: gapId
|
|
16086
16427
|
};
|
|
16087
16428
|
}
|
|
16088
|
-
const absolutePath = (0,
|
|
16429
|
+
const absolutePath = (0, import_node_path23.join)(cwd, configFile);
|
|
16089
16430
|
const source = await readSourceOrEmpty(absolutePath);
|
|
16090
16431
|
const result = applyEslintRestrictedImportsRecipe(source, configFile);
|
|
16091
16432
|
return {
|
|
@@ -16099,13 +16440,13 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
|
|
|
16099
16440
|
}
|
|
16100
16441
|
return null;
|
|
16101
16442
|
}
|
|
16102
|
-
var import_node_fs10,
|
|
16443
|
+
var import_node_fs10, import_promises16, import_node_path23;
|
|
16103
16444
|
var init_recipes = __esm({
|
|
16104
16445
|
"src/heal/recipes/index.ts"() {
|
|
16105
16446
|
"use strict";
|
|
16106
16447
|
import_node_fs10 = require("node:fs");
|
|
16107
|
-
|
|
16108
|
-
|
|
16448
|
+
import_promises16 = require("node:fs/promises");
|
|
16449
|
+
import_node_path23 = require("node:path");
|
|
16109
16450
|
init_envAuthSecret();
|
|
16110
16451
|
init_envNodeEnv();
|
|
16111
16452
|
init_envDatabaseUrl();
|
|
@@ -16180,14 +16521,14 @@ async function applyHeal(options) {
|
|
|
16180
16521
|
rollback: { available: false, instructions: "Recipe matched but no change was needed (already applied or file already exists)." }
|
|
16181
16522
|
};
|
|
16182
16523
|
}
|
|
16183
|
-
const absoluteTarget = (0,
|
|
16524
|
+
const absoluteTarget = (0, import_node_path24.join)(options.cwd, dispatched.targetFile);
|
|
16184
16525
|
assertSafeHealTarget(options.cwd, dispatched.targetFile);
|
|
16185
|
-
await (0,
|
|
16186
|
-
const healDir2 = (0,
|
|
16187
|
-
await (0,
|
|
16188
|
-
const beforeContent = (0, import_node_fs11.existsSync)(absoluteTarget) ? await (0,
|
|
16189
|
-
await (0,
|
|
16190
|
-
await (0,
|
|
16526
|
+
await (0, import_promises17.mkdir)((0, import_node_path24.dirname)(absoluteTarget), { recursive: true });
|
|
16527
|
+
const healDir2 = (0, import_node_path24.join)(options.cwd, ".viberaven", "heal", id);
|
|
16528
|
+
await (0, import_promises17.mkdir)((0, import_node_path24.join)(healDir2, "before"), { recursive: true });
|
|
16529
|
+
const beforeContent = (0, import_node_fs11.existsSync)(absoluteTarget) ? await (0, import_promises17.readFile)(absoluteTarget, "utf8") : "";
|
|
16530
|
+
await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir2, "before", "target.txt"), beforeContent, "utf8");
|
|
16531
|
+
await (0, import_promises17.writeFile)(absoluteTarget, dispatched.output, "utf8");
|
|
16191
16532
|
const patch2 = [
|
|
16192
16533
|
`--- ${dispatched.targetFile}`,
|
|
16193
16534
|
`+++ ${dispatched.targetFile}`,
|
|
@@ -16197,7 +16538,7 @@ async function applyHeal(options) {
|
|
|
16197
16538
|
dispatched.output,
|
|
16198
16539
|
""
|
|
16199
16540
|
].join("\n");
|
|
16200
|
-
await (0,
|
|
16541
|
+
await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir2, "patch.diff"), patch2, "utf8");
|
|
16201
16542
|
const result2 = {
|
|
16202
16543
|
$schema: "https://viberaven.dev/schemas/heal-result.schema.json",
|
|
16203
16544
|
schemaVersion: "v1",
|
|
@@ -16208,7 +16549,7 @@ async function applyHeal(options) {
|
|
|
16208
16549
|
gapId: options.gapId,
|
|
16209
16550
|
recipe: dispatched.recipeName,
|
|
16210
16551
|
target: dispatched.targetFile,
|
|
16211
|
-
changedFiles: [(0,
|
|
16552
|
+
changedFiles: [(0, import_node_path24.relative)(options.cwd, absoluteTarget).replace(/\\/g, "/")],
|
|
16212
16553
|
artifacts: {
|
|
16213
16554
|
patch: `.viberaven/heal/${id}/patch.diff`,
|
|
16214
16555
|
result: `.viberaven/heal/${id}/result.json`
|
|
@@ -16218,7 +16559,7 @@ async function applyHeal(options) {
|
|
|
16218
16559
|
instructions: "Restore .viberaven/heal/<healId>/before/target.txt to the target file or apply the reverse patch."
|
|
16219
16560
|
}
|
|
16220
16561
|
};
|
|
16221
|
-
await (0,
|
|
16562
|
+
await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir2, "result.json"), `${JSON.stringify(result2, null, 2)}
|
|
16222
16563
|
`, "utf8");
|
|
16223
16564
|
return result2;
|
|
16224
16565
|
}
|
|
@@ -16238,7 +16579,7 @@ async function applyHeal(options) {
|
|
|
16238
16579
|
};
|
|
16239
16580
|
}
|
|
16240
16581
|
const absolute = assertSafeHealTarget(options.cwd, options.target);
|
|
16241
|
-
const before = await (0,
|
|
16582
|
+
const before = await (0, import_promises17.readFile)(absolute, "utf8");
|
|
16242
16583
|
const recipe = applyEmptyCatchRecipe(before);
|
|
16243
16584
|
if (!recipe.changed) {
|
|
16244
16585
|
return {
|
|
@@ -16255,10 +16596,10 @@ async function applyHeal(options) {
|
|
|
16255
16596
|
rollback: { available: false, instructions: "No supported heal recipe matched this file." }
|
|
16256
16597
|
};
|
|
16257
16598
|
}
|
|
16258
|
-
const healDir = (0,
|
|
16259
|
-
await (0,
|
|
16260
|
-
await (0,
|
|
16261
|
-
await (0,
|
|
16599
|
+
const healDir = (0, import_node_path24.join)(options.cwd, ".viberaven", "heal", id);
|
|
16600
|
+
await (0, import_promises17.mkdir)((0, import_node_path24.join)(healDir, "before"), { recursive: true });
|
|
16601
|
+
await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir, "before", "target.txt"), before, "utf8");
|
|
16602
|
+
await (0, import_promises17.writeFile)(absolute, recipe.output, "utf8");
|
|
16262
16603
|
const patch = [
|
|
16263
16604
|
`--- ${options.target}`,
|
|
16264
16605
|
`+++ ${options.target}`,
|
|
@@ -16268,7 +16609,7 @@ async function applyHeal(options) {
|
|
|
16268
16609
|
recipe.output,
|
|
16269
16610
|
""
|
|
16270
16611
|
].join("\n");
|
|
16271
|
-
await (0,
|
|
16612
|
+
await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir, "patch.diff"), patch, "utf8");
|
|
16272
16613
|
const result = {
|
|
16273
16614
|
$schema: "https://viberaven.dev/schemas/heal-result.schema.json",
|
|
16274
16615
|
schemaVersion: "v1",
|
|
@@ -16279,7 +16620,7 @@ async function applyHeal(options) {
|
|
|
16279
16620
|
gapId: options.gapId,
|
|
16280
16621
|
recipe: "empty-catch-safe-response",
|
|
16281
16622
|
target: options.target,
|
|
16282
|
-
changedFiles: [(0,
|
|
16623
|
+
changedFiles: [(0, import_node_path24.relative)(options.cwd, absolute).replace(/\\/g, "/")],
|
|
16283
16624
|
artifacts: {
|
|
16284
16625
|
patch: `.viberaven/heal/${id}/patch.diff`,
|
|
16285
16626
|
result: `.viberaven/heal/${id}/result.json`
|
|
@@ -16289,17 +16630,17 @@ async function applyHeal(options) {
|
|
|
16289
16630
|
instructions: "Restore .viberaven/heal/<healId>/before/target.txt to the target file or apply the reverse patch."
|
|
16290
16631
|
}
|
|
16291
16632
|
};
|
|
16292
|
-
await (0,
|
|
16633
|
+
await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir, "result.json"), `${JSON.stringify(result, null, 2)}
|
|
16293
16634
|
`, "utf8");
|
|
16294
16635
|
return result;
|
|
16295
16636
|
}
|
|
16296
|
-
var
|
|
16637
|
+
var import_promises17, import_node_fs11, import_node_path24;
|
|
16297
16638
|
var init_apply = __esm({
|
|
16298
16639
|
"src/heal/apply.ts"() {
|
|
16299
16640
|
"use strict";
|
|
16300
|
-
|
|
16641
|
+
import_promises17 = require("node:fs/promises");
|
|
16301
16642
|
import_node_fs11 = require("node:fs");
|
|
16302
|
-
|
|
16643
|
+
import_node_path24 = require("node:path");
|
|
16303
16644
|
init_pathSafety();
|
|
16304
16645
|
init_emptyCatch();
|
|
16305
16646
|
init_recipes();
|
|
@@ -16311,8 +16652,8 @@ function healId2() {
|
|
|
16311
16652
|
return `heal_${(/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14)}`;
|
|
16312
16653
|
}
|
|
16313
16654
|
async function writeHealPlan(options) {
|
|
16314
|
-
const dir = (0,
|
|
16315
|
-
await (0,
|
|
16655
|
+
const dir = (0, import_node_path25.join)(options.cwd, ".viberaven");
|
|
16656
|
+
await (0, import_promises18.mkdir)(dir, { recursive: true });
|
|
16316
16657
|
const id = healId2();
|
|
16317
16658
|
const target = options.target ?? `gap:${options.gapId}`;
|
|
16318
16659
|
const markdown = [
|
|
@@ -16339,17 +16680,17 @@ async function writeHealPlan(options) {
|
|
|
16339
16680
|
artifacts: { plan: ".viberaven/heal-plan.md" },
|
|
16340
16681
|
rollback: { available: false, instructions: "No source files were changed." }
|
|
16341
16682
|
};
|
|
16342
|
-
await (0,
|
|
16343
|
-
await (0,
|
|
16683
|
+
await (0, import_promises18.writeFile)((0, import_node_path25.join)(dir, "heal-plan.md"), markdown, "utf8");
|
|
16684
|
+
await (0, import_promises18.writeFile)((0, import_node_path25.join)(dir, "heal-plan.json"), `${JSON.stringify(result, null, 2)}
|
|
16344
16685
|
`, "utf8");
|
|
16345
16686
|
return result;
|
|
16346
16687
|
}
|
|
16347
|
-
var
|
|
16688
|
+
var import_promises18, import_node_path25;
|
|
16348
16689
|
var init_plan = __esm({
|
|
16349
16690
|
"src/heal/plan.ts"() {
|
|
16350
16691
|
"use strict";
|
|
16351
|
-
|
|
16352
|
-
|
|
16692
|
+
import_promises18 = require("node:fs/promises");
|
|
16693
|
+
import_node_path25 = require("node:path");
|
|
16353
16694
|
init_commands();
|
|
16354
16695
|
}
|
|
16355
16696
|
});
|
|
@@ -16359,8 +16700,8 @@ function healId3() {
|
|
|
16359
16700
|
return `heal_${(/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14)}`;
|
|
16360
16701
|
}
|
|
16361
16702
|
async function writeHealPrompt(options) {
|
|
16362
|
-
const dir = (0,
|
|
16363
|
-
await (0,
|
|
16703
|
+
const dir = (0, import_node_path26.join)(options.cwd, ".viberaven");
|
|
16704
|
+
await (0, import_promises19.mkdir)(dir, { recursive: true });
|
|
16364
16705
|
const id = healId3();
|
|
16365
16706
|
const target = options.target ?? `gap:${options.gapId}`;
|
|
16366
16707
|
const prompt = [
|
|
@@ -16387,15 +16728,15 @@ async function writeHealPrompt(options) {
|
|
|
16387
16728
|
artifacts: { prompt: ".viberaven/heal-prompt.md" },
|
|
16388
16729
|
rollback: { available: false, instructions: "No source files were changed." }
|
|
16389
16730
|
};
|
|
16390
|
-
await (0,
|
|
16731
|
+
await (0, import_promises19.writeFile)((0, import_node_path26.join)(dir, "heal-prompt.md"), prompt, "utf8");
|
|
16391
16732
|
return result;
|
|
16392
16733
|
}
|
|
16393
|
-
var
|
|
16734
|
+
var import_promises19, import_node_path26;
|
|
16394
16735
|
var init_prompt = __esm({
|
|
16395
16736
|
"src/heal/prompt.ts"() {
|
|
16396
16737
|
"use strict";
|
|
16397
|
-
|
|
16398
|
-
|
|
16738
|
+
import_promises19 = require("node:fs/promises");
|
|
16739
|
+
import_node_path26 = require("node:path");
|
|
16399
16740
|
init_commands();
|
|
16400
16741
|
}
|
|
16401
16742
|
});
|
|
@@ -16406,7 +16747,7 @@ function loopStatePath(workspaceRoot) {
|
|
|
16406
16747
|
}
|
|
16407
16748
|
async function loadLoopState(workspaceRoot) {
|
|
16408
16749
|
try {
|
|
16409
|
-
const raw = await (0,
|
|
16750
|
+
const raw = await (0, import_promises20.readFile)(loopStatePath(workspaceRoot), "utf8");
|
|
16410
16751
|
const parsed = JSON.parse(raw);
|
|
16411
16752
|
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.batchApplied === "number" && typeof parsed.lastGapCount === "number" && typeof parsed.stalledScans === "number") {
|
|
16412
16753
|
const p2 = parsed;
|
|
@@ -16426,8 +16767,8 @@ async function loadLoopState(workspaceRoot) {
|
|
|
16426
16767
|
async function saveLoopState(workspaceRoot, state) {
|
|
16427
16768
|
try {
|
|
16428
16769
|
const dir = (0, import_path.join)(workspaceRoot, ".viberaven");
|
|
16429
|
-
await (0,
|
|
16430
|
-
await (0,
|
|
16770
|
+
await (0, import_promises20.mkdir)(dir, { recursive: true });
|
|
16771
|
+
await (0, import_promises20.writeFile)(loopStatePath(workspaceRoot), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
16431
16772
|
} catch (err) {
|
|
16432
16773
|
console.warn("[VibeRaven] Could not save loop-state.json:", err instanceof Error ? err.message : String(err));
|
|
16433
16774
|
}
|
|
@@ -16453,11 +16794,11 @@ function resetBatch(state, newGapCount) {
|
|
|
16453
16794
|
appliedGapIdsSinceScan: []
|
|
16454
16795
|
};
|
|
16455
16796
|
}
|
|
16456
|
-
var
|
|
16797
|
+
var import_promises20, import_path, DEFAULT_LOOP_STATE;
|
|
16457
16798
|
var init_loopState = __esm({
|
|
16458
16799
|
"src/loopState.ts"() {
|
|
16459
16800
|
"use strict";
|
|
16460
|
-
|
|
16801
|
+
import_promises20 = require("fs/promises");
|
|
16461
16802
|
import_path = require("path");
|
|
16462
16803
|
DEFAULT_LOOP_STATE = {
|
|
16463
16804
|
batchApplied: 0,
|
|
@@ -16498,11 +16839,11 @@ var init_heal = __esm({
|
|
|
16498
16839
|
|
|
16499
16840
|
// src/stackRecommend.ts
|
|
16500
16841
|
async function recommendStack(cwd = process.cwd()) {
|
|
16501
|
-
const pkgPath = (0,
|
|
16842
|
+
const pkgPath = (0, import_node_path27.join)(cwd, "package.json");
|
|
16502
16843
|
if (!(0, import_node_fs12.existsSync)(pkgPath)) {
|
|
16503
16844
|
return DEFAULT_STACK;
|
|
16504
16845
|
}
|
|
16505
|
-
const pkg = JSON.parse(await (0,
|
|
16846
|
+
const pkg = JSON.parse(await (0, import_promises21.readFile)(pkgPath, "utf-8"));
|
|
16506
16847
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
16507
16848
|
const names = Object.keys(deps).join(" ").toLowerCase();
|
|
16508
16849
|
const rec = {
|
|
@@ -16530,13 +16871,13 @@ async function recommendStack(cwd = process.cwd()) {
|
|
|
16530
16871
|
}
|
|
16531
16872
|
return rec;
|
|
16532
16873
|
}
|
|
16533
|
-
var
|
|
16874
|
+
var import_promises21, import_node_fs12, import_node_path27, DEFAULT_STACK;
|
|
16534
16875
|
var init_stackRecommend = __esm({
|
|
16535
16876
|
"src/stackRecommend.ts"() {
|
|
16536
16877
|
"use strict";
|
|
16537
|
-
|
|
16878
|
+
import_promises21 = require("node:fs/promises");
|
|
16538
16879
|
import_node_fs12 = require("node:fs");
|
|
16539
|
-
|
|
16880
|
+
import_node_path27 = require("node:path");
|
|
16540
16881
|
DEFAULT_STACK = {
|
|
16541
16882
|
frontend: "react",
|
|
16542
16883
|
ui: "tailwind + shadcn/ui",
|
|
@@ -16581,7 +16922,7 @@ var init_runInit = __esm({
|
|
|
16581
16922
|
async function checkAgentInjection(cwd) {
|
|
16582
16923
|
const checks = [];
|
|
16583
16924
|
for (const item4 of REQUIRED_EXISTENCE_CHECKS) {
|
|
16584
|
-
const exists = await fileExists2((0,
|
|
16925
|
+
const exists = await fileExists2((0, import_node_path28.join)(cwd, item4.file));
|
|
16585
16926
|
checks.push({
|
|
16586
16927
|
id: item4.id,
|
|
16587
16928
|
status: exists ? "pass" : "fail",
|
|
@@ -16589,17 +16930,17 @@ async function checkAgentInjection(cwd) {
|
|
|
16589
16930
|
});
|
|
16590
16931
|
}
|
|
16591
16932
|
for (const item4 of OPTIONAL_CURSOR_PACK_CHECKS) {
|
|
16592
|
-
const exists = await fileExists2((0,
|
|
16933
|
+
const exists = await fileExists2((0, import_node_path28.join)(cwd, item4.file));
|
|
16593
16934
|
checks.push({
|
|
16594
16935
|
id: item4.id,
|
|
16595
16936
|
status: exists ? "pass" : "fail",
|
|
16596
16937
|
message: exists ? `${item4.file} exists` : `Missing ${item4.file} \u2014 run npx -y viberaven init --agents all`
|
|
16597
16938
|
});
|
|
16598
16939
|
}
|
|
16599
|
-
const legacyCursorPath = (0,
|
|
16940
|
+
const legacyCursorPath = (0, import_node_path28.join)(cwd, ".cursor/rules/viberaven.mdc");
|
|
16600
16941
|
if (await fileExists2(legacyCursorPath)) {
|
|
16601
|
-
const legacyContent = await (0,
|
|
16602
|
-
const hasCoreSplit = await fileExists2((0,
|
|
16942
|
+
const legacyContent = await (0, import_promises22.readFile)(legacyCursorPath, "utf-8");
|
|
16943
|
+
const hasCoreSplit = await fileExists2((0, import_node_path28.join)(cwd, ".cursor/rules/viberaven-core.mdc"));
|
|
16603
16944
|
if (!hasCoreSplit || legacyContent.includes("alwaysApply: true")) {
|
|
16604
16945
|
checks.push({
|
|
16605
16946
|
id: "cursor-legacy-mdc",
|
|
@@ -16610,7 +16951,7 @@ async function checkAgentInjection(cwd) {
|
|
|
16610
16951
|
}
|
|
16611
16952
|
for (const target of CORE_AGENT_INJECTION_TARGETS) {
|
|
16612
16953
|
const file = target === "cursor" ? ".cursor/rules/viberaven-core.mdc" : AGENT_RULE_TARGETS[target].file;
|
|
16613
|
-
const path = (0,
|
|
16954
|
+
const path = (0, import_node_path28.join)(cwd, file);
|
|
16614
16955
|
const exists = await fileExists2(path);
|
|
16615
16956
|
if (!exists) {
|
|
16616
16957
|
checks.push({
|
|
@@ -16620,7 +16961,7 @@ async function checkAgentInjection(cwd) {
|
|
|
16620
16961
|
});
|
|
16621
16962
|
continue;
|
|
16622
16963
|
}
|
|
16623
|
-
const content = await (0,
|
|
16964
|
+
const content = await (0, import_promises22.readFile)(path, "utf-8");
|
|
16624
16965
|
const hasCommand = content.includes(PUBLIC_CHECK_COMMAND);
|
|
16625
16966
|
checks.push({
|
|
16626
16967
|
id: `canonical-${target}`,
|
|
@@ -16654,18 +16995,18 @@ function formatDoctorAgentsReport(report) {
|
|
|
16654
16995
|
}
|
|
16655
16996
|
async function fileExists2(path) {
|
|
16656
16997
|
try {
|
|
16657
|
-
await (0,
|
|
16998
|
+
await (0, import_promises22.access)(path);
|
|
16658
16999
|
return true;
|
|
16659
17000
|
} catch {
|
|
16660
17001
|
return false;
|
|
16661
17002
|
}
|
|
16662
17003
|
}
|
|
16663
|
-
var
|
|
17004
|
+
var import_promises22, import_node_path28, REQUIRED_EXISTENCE_CHECKS, OPTIONAL_CURSOR_PACK_CHECKS, STALE_PATTERNS;
|
|
16664
17005
|
var init_doctorAgents = __esm({
|
|
16665
17006
|
"src/commands/doctorAgents.ts"() {
|
|
16666
17007
|
"use strict";
|
|
16667
|
-
|
|
16668
|
-
|
|
17008
|
+
import_promises22 = require("node:fs/promises");
|
|
17009
|
+
import_node_path28 = require("node:path");
|
|
16669
17010
|
init_commands();
|
|
16670
17011
|
init_agentTargets();
|
|
16671
17012
|
REQUIRED_EXISTENCE_CHECKS = [
|
|
@@ -16925,39 +17266,164 @@ var init_runAudit = __esm({
|
|
|
16925
17266
|
}
|
|
16926
17267
|
});
|
|
16927
17268
|
|
|
16928
|
-
// src/
|
|
16929
|
-
function
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
16933
|
-
|
|
16934
|
-
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
17269
|
+
// src/commands/cleanPlan.ts
|
|
17270
|
+
function buildContextCleanupPlan(input) {
|
|
17271
|
+
const items = [];
|
|
17272
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17273
|
+
for (const file of input.files) {
|
|
17274
|
+
const path = normalizePath5((0, import_node_path29.relative)(input.projectRoot, file.path) || file.path);
|
|
17275
|
+
const fullPath = normalizePath5(file.path);
|
|
17276
|
+
const category = categorizeCleanupFile(path, fullPath, file);
|
|
17277
|
+
if (!category || seen.has(path)) {
|
|
17278
|
+
continue;
|
|
17279
|
+
}
|
|
17280
|
+
seen.add(path);
|
|
17281
|
+
items.push({
|
|
17282
|
+
path,
|
|
17283
|
+
category,
|
|
17284
|
+
action: "review-ignore-or-exclude-manually",
|
|
17285
|
+
reason: reasonForCategory(category)
|
|
17286
|
+
});
|
|
16941
17287
|
}
|
|
16942
|
-
return
|
|
17288
|
+
return {
|
|
17289
|
+
title: "VibeRaven context cleanup plan",
|
|
17290
|
+
warning: "VibeRaven only writes this review plan. Review each item manually before changing ignore rules or excluding artifacts from agent context.",
|
|
17291
|
+
items
|
|
17292
|
+
};
|
|
16943
17293
|
}
|
|
16944
|
-
function
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
return "txt";
|
|
17294
|
+
async function collectCleanupFiles(projectRoot) {
|
|
17295
|
+
const files = [];
|
|
17296
|
+
await walkCleanupFiles(projectRoot, projectRoot, files, 0);
|
|
17297
|
+
return files;
|
|
16949
17298
|
}
|
|
16950
|
-
function
|
|
16951
|
-
|
|
16952
|
-
|
|
17299
|
+
async function writeCleanupPlan(projectRoot, plan) {
|
|
17300
|
+
const outputDir = (0, import_node_path29.join)(projectRoot, ".viberaven");
|
|
17301
|
+
const outputPath = (0, import_node_path29.join)(outputDir, "context-cleanup.md");
|
|
17302
|
+
await (0, import_promises23.mkdir)(outputDir, { recursive: true });
|
|
17303
|
+
await (0, import_promises23.writeFile)(outputPath, renderCleanupPlan(plan), "utf8");
|
|
17304
|
+
return outputPath;
|
|
17305
|
+
}
|
|
17306
|
+
function categorizeCleanupFile(relativePath, fullPath, file) {
|
|
17307
|
+
if (relativePath.startsWith(".viberaven/") || fullPath.includes("/.viberaven/")) {
|
|
17308
|
+
return "generated-artifact";
|
|
16953
17309
|
}
|
|
16954
|
-
if (
|
|
16955
|
-
return
|
|
17310
|
+
if (isCachePath(relativePath)) {
|
|
17311
|
+
return "cache-directory";
|
|
16956
17312
|
}
|
|
16957
|
-
if (
|
|
16958
|
-
return
|
|
17313
|
+
if (relativePath.endsWith(".log") && file.sizeBytes >= LARGE_LOG_BYTES) {
|
|
17314
|
+
return "large-log";
|
|
16959
17315
|
}
|
|
16960
|
-
|
|
17316
|
+
return void 0;
|
|
17317
|
+
}
|
|
17318
|
+
function isCachePath(path) {
|
|
17319
|
+
return path === ".next/cache" || path.startsWith(".next/cache/") || path === ".turbo" || path.startsWith(".turbo/") || path === ".vite" || path.startsWith(".vite/") || path.includes("/node_modules/.cache/") || path.endsWith("/node_modules/.cache");
|
|
17320
|
+
}
|
|
17321
|
+
function reasonForCategory(category) {
|
|
17322
|
+
switch (category) {
|
|
17323
|
+
case "generated-artifact":
|
|
17324
|
+
return "Generated VibeRaven artifact. Keep current scan outputs, but avoid loading old reports into agent context.";
|
|
17325
|
+
case "large-log":
|
|
17326
|
+
return "Large log file that can add token noise. Review it before changing ignore rules or excluding it from agent context.";
|
|
17327
|
+
case "cache-directory":
|
|
17328
|
+
return "Build cache evidence. Do not load it into agent context unless debugging build internals.";
|
|
17329
|
+
}
|
|
17330
|
+
}
|
|
17331
|
+
function renderCleanupPlan(plan) {
|
|
17332
|
+
const lines = ["# VibeRaven context cleanup plan", "", plan.warning, ""];
|
|
17333
|
+
if (plan.items.length === 0) {
|
|
17334
|
+
lines.push("No noisy generated artifacts, large logs, or cache directories were found by the conservative scanner.");
|
|
17335
|
+
}
|
|
17336
|
+
for (const item4 of plan.items) {
|
|
17337
|
+
lines.push(`- ${item4.path}`);
|
|
17338
|
+
lines.push(` - Category: ${item4.category}`);
|
|
17339
|
+
lines.push(` - Action: ${item4.action}`);
|
|
17340
|
+
lines.push(` - Reason: ${item4.reason}`);
|
|
17341
|
+
}
|
|
17342
|
+
return `${lines.join("\n")}
|
|
17343
|
+
`;
|
|
17344
|
+
}
|
|
17345
|
+
async function walkCleanupFiles(root, dir, result, depth) {
|
|
17346
|
+
if (depth > MAX_WALK_DEPTH) {
|
|
17347
|
+
return;
|
|
17348
|
+
}
|
|
17349
|
+
let entries;
|
|
17350
|
+
try {
|
|
17351
|
+
entries = await (0, import_promises23.readdir)(dir, { withFileTypes: true });
|
|
17352
|
+
} catch {
|
|
17353
|
+
return;
|
|
17354
|
+
}
|
|
17355
|
+
for (const entry of entries) {
|
|
17356
|
+
if (SKIP_DIRECTORIES.has(entry.name)) {
|
|
17357
|
+
continue;
|
|
17358
|
+
}
|
|
17359
|
+
const absolute = (0, import_node_path29.join)(dir, entry.name);
|
|
17360
|
+
if (entry.isDirectory()) {
|
|
17361
|
+
if (isCachePath(normalizePath5((0, import_node_path29.relative)(root, absolute)))) {
|
|
17362
|
+
result.push({ path: absolute, sizeBytes: 0, kind: "directory" });
|
|
17363
|
+
continue;
|
|
17364
|
+
}
|
|
17365
|
+
await walkCleanupFiles(root, absolute, result, depth + 1);
|
|
17366
|
+
continue;
|
|
17367
|
+
}
|
|
17368
|
+
if (!entry.isFile()) {
|
|
17369
|
+
continue;
|
|
17370
|
+
}
|
|
17371
|
+
try {
|
|
17372
|
+
const fileStat = await (0, import_promises23.stat)(absolute);
|
|
17373
|
+
result.push({ path: absolute, sizeBytes: fileStat.size, kind: "file" });
|
|
17374
|
+
} catch {
|
|
17375
|
+
continue;
|
|
17376
|
+
}
|
|
17377
|
+
}
|
|
17378
|
+
}
|
|
17379
|
+
function normalizePath5(path) {
|
|
17380
|
+
return path.replace(/\\/g, "/");
|
|
17381
|
+
}
|
|
17382
|
+
var import_promises23, import_node_path29, MAX_WALK_DEPTH, LARGE_LOG_BYTES, SKIP_DIRECTORIES;
|
|
17383
|
+
var init_cleanPlan = __esm({
|
|
17384
|
+
"src/commands/cleanPlan.ts"() {
|
|
17385
|
+
"use strict";
|
|
17386
|
+
import_promises23 = require("node:fs/promises");
|
|
17387
|
+
import_node_path29 = require("node:path");
|
|
17388
|
+
MAX_WALK_DEPTH = 5;
|
|
17389
|
+
LARGE_LOG_BYTES = 8192;
|
|
17390
|
+
SKIP_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
17391
|
+
}
|
|
17392
|
+
});
|
|
17393
|
+
|
|
17394
|
+
// src/actions/render.ts
|
|
17395
|
+
function redact(text) {
|
|
17396
|
+
return redactString(text).replace(/\[REDACTED(?:_[A-Z_]+)?\]/g, "<redacted>").replace(
|
|
17397
|
+
/\b([A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|DATABASE_URL|POSTGRES_URL|SERVICE_ROLE)[A-Z0-9_]*)=([^,\s]+)/gi,
|
|
17398
|
+
"$1=<redacted>"
|
|
17399
|
+
).replace(/\bpostgres(?:ql)?:\/\/[^@\s]+@/gi, "postgresql://<redacted>@").replace(/[A-Za-z]:\\[^\s`"]+/g, "<repo-relative-path>").replace(/\b\/Users\/[^\s`"]+/g, "<repo-relative-path>").replace(/\b\/home\/[^\s`"]+/g, "<repo-relative-path>").replace(/\beyJ[A-Za-z0-9._-]*\b/g, "<redacted>");
|
|
17400
|
+
}
|
|
17401
|
+
function stringifyPayload(value) {
|
|
17402
|
+
if (Array.isArray(value)) {
|
|
17403
|
+
return value.map((entry) => String(entry)).join("\n");
|
|
17404
|
+
}
|
|
17405
|
+
if (value && typeof value === "object") {
|
|
17406
|
+
return JSON.stringify(value, null, 2);
|
|
17407
|
+
}
|
|
17408
|
+
return String(value ?? "");
|
|
17409
|
+
}
|
|
17410
|
+
function fenceFor(format) {
|
|
17411
|
+
if (format === "bash" || format === "sql" || format === "json") {
|
|
17412
|
+
return format;
|
|
17413
|
+
}
|
|
17414
|
+
return "txt";
|
|
17415
|
+
}
|
|
17416
|
+
function renderTarget(target) {
|
|
17417
|
+
if (!target) {
|
|
17418
|
+
return [];
|
|
17419
|
+
}
|
|
17420
|
+
if (target.type === "url") {
|
|
17421
|
+
return [`Open: ${redact(target.href || target.label)}`];
|
|
17422
|
+
}
|
|
17423
|
+
if (target.type === "file") {
|
|
17424
|
+
return [`File: ${redact(target.path)}`];
|
|
17425
|
+
}
|
|
17426
|
+
if (target.type === "command") {
|
|
16961
17427
|
return ["Run:", "```bash", redact(target.command), "```"];
|
|
16962
17428
|
}
|
|
16963
17429
|
if (target.type === "provider") {
|
|
@@ -17027,10 +17493,10 @@ var init_render = __esm({
|
|
|
17027
17493
|
|
|
17028
17494
|
// src/commands/actions.ts
|
|
17029
17495
|
async function runActionsCommand(input) {
|
|
17030
|
-
const actionsPath = (0,
|
|
17496
|
+
const actionsPath = (0, import_node_path30.join)(getProjectArtifactsDir(input.cwd), "actions.json");
|
|
17031
17497
|
let raw;
|
|
17032
17498
|
try {
|
|
17033
|
-
raw = await (0,
|
|
17499
|
+
raw = await (0, import_promises24.readFile)(actionsPath, "utf8");
|
|
17034
17500
|
} catch {
|
|
17035
17501
|
process.stderr.write("No VibeRaven actions found. Run `npx -y viberaven --agent-mode` first.\n");
|
|
17036
17502
|
return 1;
|
|
@@ -17044,12 +17510,12 @@ async function runActionsCommand(input) {
|
|
|
17044
17510
|
process.stdout.write(renderActionSurface(manifest));
|
|
17045
17511
|
return 0;
|
|
17046
17512
|
}
|
|
17047
|
-
var
|
|
17513
|
+
var import_promises24, import_node_path30;
|
|
17048
17514
|
var init_actions = __esm({
|
|
17049
17515
|
"src/commands/actions.ts"() {
|
|
17050
17516
|
"use strict";
|
|
17051
|
-
|
|
17052
|
-
|
|
17517
|
+
import_promises24 = require("node:fs/promises");
|
|
17518
|
+
import_node_path30 = require("node:path");
|
|
17053
17519
|
init_render();
|
|
17054
17520
|
init_config();
|
|
17055
17521
|
}
|
|
@@ -17058,7 +17524,7 @@ var init_actions = __esm({
|
|
|
17058
17524
|
// src/commands/verifyAction.ts
|
|
17059
17525
|
async function currentActionExists(cwd, actionId) {
|
|
17060
17526
|
try {
|
|
17061
|
-
const raw = await (0,
|
|
17527
|
+
const raw = await (0, import_promises25.readFile)((0, import_node_path31.join)(getProjectArtifactsDir(cwd), "actions.json"), "utf8");
|
|
17062
17528
|
const manifest = JSON.parse(raw);
|
|
17063
17529
|
return manifest.actions.some((action) => action.id === actionId);
|
|
17064
17530
|
} catch {
|
|
@@ -17102,12 +17568,12 @@ Run \`npx -y viberaven --verify\` to refresh the production gate for this action
|
|
|
17102
17568
|
);
|
|
17103
17569
|
return 0;
|
|
17104
17570
|
}
|
|
17105
|
-
var
|
|
17571
|
+
var import_promises25, import_node_path31;
|
|
17106
17572
|
var init_verifyAction = __esm({
|
|
17107
17573
|
"src/commands/verifyAction.ts"() {
|
|
17108
17574
|
"use strict";
|
|
17109
|
-
|
|
17110
|
-
|
|
17575
|
+
import_promises25 = require("node:fs/promises");
|
|
17576
|
+
import_node_path31 = require("node:path");
|
|
17111
17577
|
init_registry2();
|
|
17112
17578
|
init_config();
|
|
17113
17579
|
}
|
|
@@ -17272,8 +17738,8 @@ async function runCheckCommand(options) {
|
|
|
17272
17738
|
for (const gap of sorted) {
|
|
17273
17739
|
console.log(`${SEVERITY_ICON[gap.severity]} ${gap.title} (${gap.id})`);
|
|
17274
17740
|
if (!options.quiet && gap.detail) {
|
|
17275
|
-
const
|
|
17276
|
-
console.log(` ${
|
|
17741
|
+
const firstLine2 = gap.detail.split("\n")[0];
|
|
17742
|
+
console.log(` ${firstLine2}`);
|
|
17277
17743
|
}
|
|
17278
17744
|
}
|
|
17279
17745
|
}
|
|
@@ -17304,128 +17770,1443 @@ var init_check = __esm({
|
|
|
17304
17770
|
}
|
|
17305
17771
|
});
|
|
17306
17772
|
|
|
17307
|
-
// src/
|
|
17308
|
-
function
|
|
17309
|
-
|
|
17310
|
-
|
|
17311
|
-
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
|
|
17316
|
-
|
|
17773
|
+
// src/rls/permissionsMatrix.ts
|
|
17774
|
+
function sha256(data) {
|
|
17775
|
+
return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
|
|
17776
|
+
}
|
|
17777
|
+
function parsePermissionsMatrix(text) {
|
|
17778
|
+
let raw;
|
|
17779
|
+
try {
|
|
17780
|
+
raw = JSON.parse(text);
|
|
17781
|
+
} catch (error) {
|
|
17782
|
+
throw new MatrixError([`not valid JSON: ${error instanceof Error ? error.message : String(error)}`]);
|
|
17783
|
+
}
|
|
17784
|
+
const problems = [];
|
|
17785
|
+
if (!isObject2(raw)) throw new MatrixError(["the file must hold a JSON object"]);
|
|
17786
|
+
for (const key of Object.keys(raw)) {
|
|
17787
|
+
if (!TOP_KEYS.has(key)) problems.push(`${key}: unknown key`);
|
|
17788
|
+
}
|
|
17789
|
+
if (raw.version !== 1) problems.push(`version: must be 1 (got ${show(raw.version)})`);
|
|
17790
|
+
if (raw.description !== void 0 && typeof raw.description !== "string") problems.push("description: must be a string");
|
|
17791
|
+
let defaultGrants = "supabase-legacy";
|
|
17792
|
+
if (raw.defaultGrants !== void 0) {
|
|
17793
|
+
if (DEFAULT_GRANT_MODES.includes(raw.defaultGrants)) defaultGrants = raw.defaultGrants;
|
|
17794
|
+
else problems.push(`defaultGrants: must be ${quoteList(DEFAULT_GRANT_MODES)} (got ${show(raw.defaultGrants)})`);
|
|
17795
|
+
}
|
|
17796
|
+
const tables = [];
|
|
17797
|
+
if (!isObject2(raw.tables)) {
|
|
17798
|
+
problems.push('tables: must be an object keyed by table name, e.g. { "public.posts": { ... } }');
|
|
17799
|
+
} else if (Object.keys(raw.tables).length === 0) {
|
|
17800
|
+
problems.push("tables: list at least one table");
|
|
17801
|
+
} else {
|
|
17802
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17803
|
+
for (const [key, value] of Object.entries(raw.tables)) {
|
|
17804
|
+
const table = parseTable(key, value, problems);
|
|
17805
|
+
if (!table) continue;
|
|
17806
|
+
const full = `${table.schema}.${table.name}`;
|
|
17807
|
+
if (seen.has(full)) problems.push(`tables[${show(key)}]: ${full} is listed twice`);
|
|
17808
|
+
seen.add(full);
|
|
17809
|
+
tables.push(table);
|
|
17317
17810
|
}
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17811
|
+
}
|
|
17812
|
+
if (problems.length > 0) throw new MatrixError(problems);
|
|
17813
|
+
return {
|
|
17814
|
+
version: 1,
|
|
17815
|
+
...typeof raw.description === "string" ? { description: raw.description } : {},
|
|
17816
|
+
defaultGrants,
|
|
17817
|
+
tables,
|
|
17818
|
+
hash: sha256(text)
|
|
17819
|
+
};
|
|
17820
|
+
}
|
|
17821
|
+
function parseTable(key, value, problems) {
|
|
17822
|
+
const at2 = `tables[${show(key)}]`;
|
|
17823
|
+
const parts = key.split(".");
|
|
17824
|
+
const nameOk = (parts.length === 1 || parts.length === 2) && parts.every((p2) => IDENT.test(p2));
|
|
17825
|
+
if (!nameOk) {
|
|
17826
|
+
problems.push(`${at2}: table must be "name" or "schema.name" using letters, digits, _ and $`);
|
|
17827
|
+
return void 0;
|
|
17828
|
+
}
|
|
17829
|
+
if (!isObject2(value)) {
|
|
17830
|
+
problems.push(`${at2}: must be an object with "owner" and "expect"`);
|
|
17831
|
+
return void 0;
|
|
17832
|
+
}
|
|
17833
|
+
const before = problems.length;
|
|
17834
|
+
for (const k3 of Object.keys(value)) {
|
|
17835
|
+
if (!TABLE_KEYS.has(k3)) problems.push(`${at2}.${k3}: unknown key`);
|
|
17836
|
+
}
|
|
17837
|
+
let ownerColumn = "";
|
|
17838
|
+
if (value.owner === void 0) {
|
|
17839
|
+
problems.push(`${at2}.owner: missing; say which column holds the owner, e.g. { "column": "user_id", "is": "auth.uid()" }`);
|
|
17840
|
+
} else if (!isObject2(value.owner)) {
|
|
17841
|
+
problems.push(`${at2}.owner: must be an object like { "column": "user_id", "is": "auth.uid()" }`);
|
|
17842
|
+
} else {
|
|
17843
|
+
for (const k3 of Object.keys(value.owner)) {
|
|
17844
|
+
if (!OWNER_KEYS.has(k3)) problems.push(`${at2}.owner.${k3}: unknown key`);
|
|
17845
|
+
}
|
|
17846
|
+
if (typeof value.owner.column !== "string" || !IDENT.test(value.owner.column)) {
|
|
17847
|
+
problems.push(`${at2}.owner.column: must be a column name (got ${show(value.owner.column)})`);
|
|
17848
|
+
} else {
|
|
17849
|
+
ownerColumn = value.owner.column;
|
|
17850
|
+
}
|
|
17851
|
+
if (value.owner.is !== "auth.uid()") {
|
|
17852
|
+
problems.push(`${at2}.owner.is: only "auth.uid()" is supported (got ${show(value.owner.is)})`);
|
|
17853
|
+
}
|
|
17854
|
+
}
|
|
17855
|
+
const expect = {};
|
|
17856
|
+
let serviceRoleExcluded = false;
|
|
17857
|
+
if (!isObject2(value.expect)) {
|
|
17858
|
+
problems.push(`${at2}.expect: missing; list owner, other_user and anon, each with select, insert, update and delete`);
|
|
17859
|
+
} else {
|
|
17860
|
+
for (const actor of Object.keys(value.expect)) {
|
|
17861
|
+
if (!MATRIX_ACTORS.includes(actor)) {
|
|
17862
|
+
problems.push(`${at2}.expect.${actor}: unknown actor; use one of ${MATRIX_ACTORS.join(", ")}`);
|
|
17863
|
+
}
|
|
17864
|
+
}
|
|
17865
|
+
for (const actor of TESTED_ACTORS) {
|
|
17866
|
+
const cells = value.expect[actor];
|
|
17867
|
+
const actorAt = `${at2}.expect.${actor}`;
|
|
17868
|
+
if (cells === void 0) {
|
|
17869
|
+
problems.push(`${actorAt}: missing; list select, insert, update and delete for this actor`);
|
|
17870
|
+
continue;
|
|
17871
|
+
}
|
|
17872
|
+
if (!isObject2(cells)) {
|
|
17873
|
+
problems.push(`${actorAt}: must be an object like { "select": "deny", "insert": "deny", "update": "deny", "delete": "deny" }`);
|
|
17874
|
+
continue;
|
|
17875
|
+
}
|
|
17876
|
+
for (const action of Object.keys(cells)) {
|
|
17877
|
+
if (!ALL_ACTIONS.includes(action)) {
|
|
17878
|
+
problems.push(`${actorAt}.${action}: unknown action; use one of ${ALL_ACTIONS.join(", ")}`);
|
|
17879
|
+
}
|
|
17880
|
+
}
|
|
17881
|
+
const parsed = {};
|
|
17882
|
+
for (const action of MATRIX_ACTIONS) {
|
|
17883
|
+
const cell = cells[action];
|
|
17884
|
+
if (cell === void 0) problems.push(`${actorAt}.${action}: missing; write ${quoteList(CELL_VALUES)}`);
|
|
17885
|
+
else if (!CELL_VALUES.includes(cell)) problems.push(`${actorAt}.${action}: must be ${quoteList(CELL_VALUES)} (got ${show(cell)})`);
|
|
17886
|
+
else parsed[action] = cell;
|
|
17887
|
+
}
|
|
17888
|
+
if (cells.reassign !== void 0) {
|
|
17889
|
+
if (!CELL_VALUES.includes(cells.reassign)) problems.push(`${actorAt}.reassign: must be ${quoteList(CELL_VALUES)} (got ${show(cells.reassign)})`);
|
|
17890
|
+
else parsed.reassign = cells.reassign;
|
|
17891
|
+
}
|
|
17892
|
+
expect[actor] = parsed;
|
|
17893
|
+
}
|
|
17894
|
+
if (value.expect.service_role !== void 0) {
|
|
17895
|
+
if (value.expect.service_role === "excluded") serviceRoleExcluded = true;
|
|
17896
|
+
else problems.push(`${at2}.expect.service_role: must be "excluded"; the service role bypasses RLS, so it is never tested`);
|
|
17897
|
+
}
|
|
17898
|
+
}
|
|
17899
|
+
const seed = {};
|
|
17900
|
+
if (value.seed !== void 0) {
|
|
17901
|
+
if (!isObject2(value.seed)) {
|
|
17902
|
+
problems.push(`${at2}.seed: must be an object of column values, e.g. { "title": "hello" }`);
|
|
17903
|
+
} else {
|
|
17904
|
+
for (const [column, v] of Object.entries(value.seed)) {
|
|
17905
|
+
if (!IDENT.test(column)) problems.push(`${at2}.seed.${column}: not a column name`);
|
|
17906
|
+
else if (v !== null && !["string", "number", "boolean"].includes(typeof v)) problems.push(`${at2}.seed.${column}: must be a string, number, boolean or null`);
|
|
17907
|
+
else if (column === ownerColumn) problems.push(`${at2}.seed.${column}: the owner column is set by the test, leave it out`);
|
|
17908
|
+
else seed[column] = v;
|
|
17909
|
+
}
|
|
17910
|
+
}
|
|
17911
|
+
}
|
|
17912
|
+
let updateColumn;
|
|
17913
|
+
if (value.updateColumn !== void 0) {
|
|
17914
|
+
if (typeof value.updateColumn !== "string" || !IDENT.test(value.updateColumn)) problems.push(`${at2}.updateColumn: must be a column name`);
|
|
17915
|
+
else if (value.updateColumn === ownerColumn) problems.push(`${at2}.updateColumn: pick a column other than the owner column`);
|
|
17916
|
+
else updateColumn = value.updateColumn;
|
|
17917
|
+
}
|
|
17918
|
+
if (value.description !== void 0 && typeof value.description !== "string") problems.push(`${at2}.description: must be a string`);
|
|
17919
|
+
if (problems.length > before) return void 0;
|
|
17920
|
+
const [schema, name] = parts.length === 2 ? parts : ["public", parts[0]];
|
|
17921
|
+
return { key, schema, name, ownerColumn, expect, serviceRoleExcluded, seed, ...updateColumn ? { updateColumn } : {} };
|
|
17922
|
+
}
|
|
17923
|
+
var import_node_crypto, MATRIX_ACTORS, TESTED_ACTORS, MATRIX_ACTIONS, OPTIONAL_ACTIONS, ALL_ACTIONS, CELL_VALUES, DEFAULT_GRANT_MODES, DEFAULT_MATRIX_FILE, MatrixError, IDENT, TOP_KEYS, TABLE_KEYS, OWNER_KEYS, isObject2, show, quoteList;
|
|
17924
|
+
var init_permissionsMatrix = __esm({
|
|
17925
|
+
"src/rls/permissionsMatrix.ts"() {
|
|
17926
|
+
"use strict";
|
|
17927
|
+
import_node_crypto = require("node:crypto");
|
|
17928
|
+
MATRIX_ACTORS = ["owner", "other_user", "anon", "service_role"];
|
|
17929
|
+
TESTED_ACTORS = ["owner", "other_user", "anon"];
|
|
17930
|
+
MATRIX_ACTIONS = ["select", "insert", "update", "delete"];
|
|
17931
|
+
OPTIONAL_ACTIONS = ["reassign"];
|
|
17932
|
+
ALL_ACTIONS = [...MATRIX_ACTIONS, ...OPTIONAL_ACTIONS];
|
|
17933
|
+
CELL_VALUES = ["allow", "deny", "skip"];
|
|
17934
|
+
DEFAULT_GRANT_MODES = ["supabase-legacy", "none"];
|
|
17935
|
+
DEFAULT_MATRIX_FILE = "viberaven.permissions.json";
|
|
17936
|
+
MatrixError = class extends Error {
|
|
17937
|
+
constructor(problems) {
|
|
17938
|
+
super(`The permissions matrix is not valid:
|
|
17939
|
+
${problems.map((p2) => ` - ${p2}`).join("\n")}`);
|
|
17940
|
+
this.problems = problems;
|
|
17941
|
+
}
|
|
17942
|
+
name = "MatrixError";
|
|
17943
|
+
};
|
|
17944
|
+
IDENT = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
17945
|
+
TOP_KEYS = /* @__PURE__ */ new Set(["$schema", "version", "description", "defaultGrants", "tables"]);
|
|
17946
|
+
TABLE_KEYS = /* @__PURE__ */ new Set(["owner", "expect", "seed", "updateColumn", "description"]);
|
|
17947
|
+
OWNER_KEYS = /* @__PURE__ */ new Set(["column", "is"]);
|
|
17948
|
+
isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17949
|
+
show = (value) => JSON.stringify(value);
|
|
17950
|
+
quoteList = (items) => items.length === 2 ? `"${items[0]}" or "${items[1]}"` : `${items.slice(0, -1).map((i) => `"${i}"`).join(", ")} or "${items[items.length - 1]}"`;
|
|
17951
|
+
}
|
|
17952
|
+
});
|
|
17953
|
+
|
|
17954
|
+
// src/rls/supabaseImitation.ts
|
|
17955
|
+
function assumptionsFor(defaultGrants) {
|
|
17956
|
+
return [
|
|
17957
|
+
defaultGrants === "supabase-legacy" ? "defaultGrants supabase-legacy: new public tables are granted to anon and authenticated, as on existing Supabase projects. Newer projects may not grant by default, so a table without RLS may be less exposed there." : "defaultGrants none: new public tables get no grants beyond what the migrations write. Existing Supabase projects do grant by default.",
|
|
17958
|
+
"Migrations run in file-name order as the postgres superuser, like `supabase db reset`.",
|
|
17959
|
+
"Each probe runs in its own transaction and is rolled back, so every cell starts from the same seeded rows."
|
|
17960
|
+
];
|
|
17961
|
+
}
|
|
17962
|
+
function imitationSetupSql(defaultGrants) {
|
|
17963
|
+
const grants = defaultGrants === "supabase-legacy" ? `
|
|
17964
|
+
alter default privileges in schema public grant all on tables to anon, authenticated, service_role;
|
|
17965
|
+
alter default privileges in schema public grant all on sequences to anon, authenticated, service_role;
|
|
17966
|
+
alter default privileges in schema public grant all on functions to anon, authenticated, service_role;` : "";
|
|
17967
|
+
return `
|
|
17968
|
+
create role anon nologin noinherit;
|
|
17969
|
+
create role authenticated nologin noinherit;
|
|
17970
|
+
create role service_role nologin noinherit bypassrls;
|
|
17971
|
+
create role authenticator nologin noinherit;
|
|
17972
|
+
create role supabase_admin nologin;
|
|
17973
|
+
create role supabase_auth_admin nologin;
|
|
17974
|
+
create role supabase_storage_admin nologin;
|
|
17975
|
+
create role dashboard_user nologin;
|
|
17976
|
+
grant anon, authenticated, service_role to authenticator;
|
|
17977
|
+
|
|
17978
|
+
create schema if not exists extensions;
|
|
17979
|
+
grant usage on schema extensions to anon, authenticated, service_role;
|
|
17980
|
+
|
|
17981
|
+
create schema auth;
|
|
17982
|
+
grant usage on schema auth to anon, authenticated, service_role;
|
|
17983
|
+
create table auth.users (
|
|
17984
|
+
instance_id uuid,
|
|
17985
|
+
id uuid primary key,
|
|
17986
|
+
aud varchar(255),
|
|
17987
|
+
role varchar(255),
|
|
17988
|
+
email varchar(255) unique,
|
|
17989
|
+
encrypted_password varchar(255),
|
|
17990
|
+
email_confirmed_at timestamptz,
|
|
17991
|
+
phone text unique,
|
|
17992
|
+
raw_app_meta_data jsonb default '{}'::jsonb,
|
|
17993
|
+
raw_user_meta_data jsonb default '{}'::jsonb,
|
|
17994
|
+
is_anonymous boolean not null default false,
|
|
17995
|
+
created_at timestamptz default now(),
|
|
17996
|
+
updated_at timestamptz default now()
|
|
17997
|
+
);
|
|
17998
|
+
create function auth.jwt() returns jsonb language sql stable as $$
|
|
17999
|
+
select coalesce(nullif(current_setting('request.jwt.claims', true), ''), '{}')::jsonb
|
|
18000
|
+
$$;
|
|
18001
|
+
create function auth.uid() returns uuid language sql stable as $$
|
|
18002
|
+
select nullif(coalesce(nullif(current_setting('request.jwt.claim.sub', true), ''), auth.jwt() ->> 'sub'), '')::uuid
|
|
18003
|
+
$$;
|
|
18004
|
+
create function auth.role() returns text language sql stable as $$
|
|
18005
|
+
select coalesce(nullif(current_setting('request.jwt.claim.role', true), ''), auth.jwt() ->> 'role')
|
|
18006
|
+
$$;
|
|
18007
|
+
create function auth.email() returns text language sql stable as $$
|
|
18008
|
+
select coalesce(nullif(current_setting('request.jwt.claim.email', true), ''), auth.jwt() ->> 'email')
|
|
18009
|
+
$$;
|
|
18010
|
+
grant execute on all functions in schema auth to anon, authenticated, service_role;
|
|
18011
|
+
|
|
18012
|
+
create schema storage;
|
|
18013
|
+
grant usage on schema storage to anon, authenticated, service_role;
|
|
18014
|
+
create table storage.buckets (
|
|
18015
|
+
id text primary key,
|
|
18016
|
+
name text not null unique,
|
|
18017
|
+
owner uuid,
|
|
18018
|
+
public boolean default false,
|
|
18019
|
+
file_size_limit bigint,
|
|
18020
|
+
allowed_mime_types text[],
|
|
18021
|
+
created_at timestamptz default now(),
|
|
18022
|
+
updated_at timestamptz default now()
|
|
18023
|
+
);
|
|
18024
|
+
create table storage.objects (
|
|
18025
|
+
id uuid primary key default gen_random_uuid(),
|
|
18026
|
+
bucket_id text references storage.buckets (id),
|
|
18027
|
+
name text,
|
|
18028
|
+
owner uuid,
|
|
18029
|
+
owner_id text,
|
|
18030
|
+
metadata jsonb,
|
|
18031
|
+
path_tokens text[] generated always as (string_to_array(name, '/')) stored,
|
|
18032
|
+
created_at timestamptz default now(),
|
|
18033
|
+
updated_at timestamptz default now()
|
|
18034
|
+
);
|
|
18035
|
+
alter table storage.objects enable row level security;
|
|
18036
|
+
alter table storage.buckets enable row level security;
|
|
18037
|
+
create function storage.foldername(name text) returns text[] language sql immutable as $$
|
|
18038
|
+
select (string_to_array(name, '/'))[1:array_length(string_to_array(name, '/'), 1) - 1]
|
|
18039
|
+
$$;
|
|
18040
|
+
create function storage.filename(name text) returns text language sql immutable as $$
|
|
18041
|
+
select (string_to_array(name, '/'))[array_length(string_to_array(name, '/'), 1)]
|
|
18042
|
+
$$;
|
|
18043
|
+
create function storage.extension(name text) returns text language sql immutable as $$
|
|
18044
|
+
select reverse(split_part(reverse(storage.filename(name)), '.', 1))
|
|
18045
|
+
$$;
|
|
18046
|
+
|
|
18047
|
+
grant usage on schema public to anon, authenticated, service_role;
|
|
18048
|
+
${grants}
|
|
18049
|
+
`;
|
|
18050
|
+
}
|
|
18051
|
+
function seedUsersSql() {
|
|
18052
|
+
return `
|
|
18053
|
+
insert into auth.users (id, aud, role, email) values
|
|
18054
|
+
('${OWNER_ID}', 'authenticated', 'authenticated', 'owner@example.test'),
|
|
18055
|
+
('${OTHER_USER_ID}', 'authenticated', 'authenticated', 'other-user@example.test');
|
|
18056
|
+
`;
|
|
18057
|
+
}
|
|
18058
|
+
var OWNER_ID, OTHER_USER_ID, IMITATION_EXCLUSIONS;
|
|
18059
|
+
var init_supabaseImitation = __esm({
|
|
18060
|
+
"src/rls/supabaseImitation.ts"() {
|
|
18061
|
+
"use strict";
|
|
18062
|
+
OWNER_ID = "11111111-1111-4111-8111-111111111111";
|
|
18063
|
+
OTHER_USER_ID = "22222222-2222-4222-8222-222222222222";
|
|
18064
|
+
IMITATION_EXCLUSIONS = [
|
|
18065
|
+
"Supabase Auth is not run: auth.uid() reads a JWT sub claim the test sets, no sign-in or token signing happens.",
|
|
18066
|
+
"PostgREST (the Data API) is not run: probes go straight to Postgres as the anon and authenticated roles, so API settings, exposed schemas and column selection are not tested.",
|
|
18067
|
+
"Storage, Realtime and Edge Functions are not modelled; storage.objects and storage.buckets exist only so migrations that mention them can apply.",
|
|
18068
|
+
"The live project is not read: its applied migrations, actual grants and Auth settings may differ from the repo.",
|
|
18069
|
+
"supabase/seed.sql is not applied.",
|
|
18070
|
+
"Views, functions and RPC calls are only exercised through the listed tables."
|
|
18071
|
+
];
|
|
18072
|
+
}
|
|
18073
|
+
});
|
|
18074
|
+
|
|
18075
|
+
// src/rls/workerDatabase.ts
|
|
18076
|
+
async function createWorkerDatabase(options) {
|
|
18077
|
+
const worker = new import_node_worker_threads.Worker(WORKER_SOURCE, {
|
|
18078
|
+
eval: true,
|
|
18079
|
+
workerData: { pglitePath: options.pglitePath, contrib: options.contrib },
|
|
18080
|
+
env: { ...options.env }
|
|
18081
|
+
});
|
|
18082
|
+
const pending = /* @__PURE__ */ new Map();
|
|
18083
|
+
let nextId = 0;
|
|
18084
|
+
let dead;
|
|
18085
|
+
const failAll = (error) => {
|
|
18086
|
+
dead = dead ?? error;
|
|
18087
|
+
for (const [, p2] of pending) {
|
|
18088
|
+
clearTimeout(p2.timer);
|
|
18089
|
+
p2.reject(error);
|
|
18090
|
+
}
|
|
18091
|
+
pending.clear();
|
|
18092
|
+
};
|
|
18093
|
+
worker.on("message", (m2) => {
|
|
18094
|
+
const p2 = pending.get(m2.id);
|
|
18095
|
+
if (!p2) return;
|
|
18096
|
+
pending.delete(m2.id);
|
|
18097
|
+
clearTimeout(p2.timer);
|
|
18098
|
+
if (m2.ok) p2.resolve(m2.result);
|
|
18099
|
+
else p2.reject(new SqlError(m2.error?.message ?? "unknown error", m2.error?.code));
|
|
18100
|
+
});
|
|
18101
|
+
worker.on("error", (error) => failAll(error instanceof Error ? error : new Error(String(error))));
|
|
18102
|
+
worker.on("exit", (code) => failAll(new Error(`the PGlite worker stopped (exit ${code})`)));
|
|
18103
|
+
const call = (message, timeoutMs) => {
|
|
18104
|
+
if (dead) return Promise.reject(dead);
|
|
18105
|
+
const id = ++nextId;
|
|
18106
|
+
return new Promise((resolve9, reject) => {
|
|
18107
|
+
const timer = setTimeout(() => {
|
|
18108
|
+
const error = new RlsTimeoutError(`timed out after ${Math.round(timeoutMs / 1e3)} s`);
|
|
18109
|
+
failAll(error);
|
|
18110
|
+
void worker.terminate();
|
|
18111
|
+
}, timeoutMs);
|
|
18112
|
+
pending.set(id, { resolve: resolve9, reject, timer });
|
|
18113
|
+
worker.postMessage({ ...message, id });
|
|
17324
18114
|
});
|
|
18115
|
+
};
|
|
18116
|
+
try {
|
|
18117
|
+
await call({ op: "init" }, options.startTimeoutMs ?? 6e4);
|
|
18118
|
+
} catch (error) {
|
|
18119
|
+
failAll(error instanceof Error ? error : new Error(String(error)));
|
|
18120
|
+
await worker.terminate();
|
|
18121
|
+
throw error;
|
|
17325
18122
|
}
|
|
17326
18123
|
return {
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
18124
|
+
query: (sql, params = [], opts = {}) => call({ op: "query", sql, params }, opts.timeoutMs ?? options.statementTimeoutMs),
|
|
18125
|
+
exec: (sql, opts = {}) => call({ op: "exec", sql }, opts.timeoutMs ?? options.statementTimeoutMs),
|
|
18126
|
+
isolationReport: () => call({ op: "isolation" }, 1e4),
|
|
18127
|
+
close: async () => {
|
|
18128
|
+
if (!dead) await call({ op: "close" }, 1e4).catch(() => void 0);
|
|
18129
|
+
await worker.terminate();
|
|
18130
|
+
}
|
|
17330
18131
|
};
|
|
17331
18132
|
}
|
|
17332
|
-
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
|
|
18133
|
+
var import_node_module, import_node_worker_threads, NETWORK_DENIED, RlsTimeoutError, WORKER_SOURCE, SqlError, resolveFromCli, defaultPgliteLoader;
|
|
18134
|
+
var init_workerDatabase = __esm({
|
|
18135
|
+
"src/rls/workerDatabase.ts"() {
|
|
18136
|
+
"use strict";
|
|
18137
|
+
import_node_module = require("node:module");
|
|
18138
|
+
import_node_worker_threads = require("node:worker_threads");
|
|
18139
|
+
NETWORK_DENIED = "rls-test: network is denied while the runner is active";
|
|
18140
|
+
RlsTimeoutError = class extends Error {
|
|
18141
|
+
name = "RlsTimeoutError";
|
|
18142
|
+
};
|
|
18143
|
+
WORKER_SOURCE = `
|
|
18144
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
18145
|
+
const DENIED = ${JSON.stringify(NETWORK_DENIED)};
|
|
18146
|
+
globalThis.fetch = async () => { throw new Error(DENIED); };
|
|
18147
|
+
const net = require('node:net');
|
|
18148
|
+
net.Socket.prototype.connect = function () { throw new Error(DENIED); };
|
|
18149
|
+
const { PGlite } = require(workerData.pglitePath);
|
|
18150
|
+
const extensions = {};
|
|
18151
|
+
for (const [name, path] of Object.entries(workerData.contrib)) {
|
|
18152
|
+
try { const mod = require(path); if (mod[name]) extensions[name] = mod[name]; } catch {}
|
|
18153
|
+
}
|
|
18154
|
+
let db;
|
|
18155
|
+
const reply = (id, result) => parentPort.postMessage({ id, ok: true, result });
|
|
18156
|
+
parentPort.on('message', async (m) => {
|
|
18157
|
+
try {
|
|
18158
|
+
if (m.op === 'init') { db = new PGlite({ extensions }); await db.waitReady; return reply(m.id, null); }
|
|
18159
|
+
if (m.op === 'exec') { await db.exec(m.sql); return reply(m.id, null); }
|
|
18160
|
+
if (m.op === 'query') { const r = await db.query(m.sql, m.params); return reply(m.id, { rows: r.rows, affectedRows: r.affectedRows }); }
|
|
18161
|
+
if (m.op === 'close') { await db.close(); return reply(m.id, null); }
|
|
18162
|
+
if (m.op === 'isolation') {
|
|
18163
|
+
let f = 'fetch allowed', s = 'socket allowed';
|
|
18164
|
+
try { await fetch('http://127.0.0.1:9/'); } catch (e) { f = String(e && e.message); }
|
|
18165
|
+
try { net.connect(9, '127.0.0.1'); } catch (e) { s = String(e && e.message); }
|
|
18166
|
+
return reply(m.id, { envKeys: Object.keys(process.env).sort(), fetch: f, socket: s });
|
|
18167
|
+
}
|
|
18168
|
+
} catch (e) {
|
|
18169
|
+
parentPort.postMessage({ id: m.id, ok: false, error: { code: e && typeof e.code === 'string' ? e.code : undefined, message: String((e && e.message) || e) } });
|
|
18170
|
+
}
|
|
18171
|
+
});
|
|
18172
|
+
`;
|
|
18173
|
+
SqlError = class extends Error {
|
|
18174
|
+
constructor(message, code) {
|
|
18175
|
+
super(message);
|
|
18176
|
+
this.code = code;
|
|
18177
|
+
}
|
|
18178
|
+
};
|
|
18179
|
+
resolveFromCli = (specifier) => (0, import_node_module.createRequire)(__filename).resolve(specifier);
|
|
18180
|
+
defaultPgliteLoader = async () => {
|
|
18181
|
+
const pglitePath = resolveFromCli("@electric-sql/pglite");
|
|
18182
|
+
const contrib = {};
|
|
18183
|
+
for (const name of ["uuid_ossp", "pgcrypto", "citext", "pg_trgm"]) {
|
|
18184
|
+
try {
|
|
18185
|
+
contrib[name] = resolveFromCli(`@electric-sql/pglite/contrib/${name}`);
|
|
18186
|
+
} catch {
|
|
18187
|
+
}
|
|
18188
|
+
}
|
|
18189
|
+
return { create: (options) => createWorkerDatabase({ pglitePath, contrib, ...options }) };
|
|
18190
|
+
};
|
|
18191
|
+
}
|
|
18192
|
+
});
|
|
18193
|
+
|
|
18194
|
+
// src/rls/rlsRunner.ts
|
|
18195
|
+
function sqlError(error) {
|
|
18196
|
+
const e2 = error;
|
|
18197
|
+
return { code: typeof e2?.code === "string" ? e2.code : "unknown", message: firstLine(typeof e2?.message === "string" ? e2.message : String(error)) };
|
|
18198
|
+
}
|
|
18199
|
+
function isSqlError(error) {
|
|
18200
|
+
return !(error instanceof RlsTimeoutError) && typeof error?.code === "string";
|
|
18201
|
+
}
|
|
18202
|
+
function displayPath(projectDir, path) {
|
|
18203
|
+
for (const base of [projectDir, process.cwd()]) {
|
|
18204
|
+
const rel = (0, import_node_path32.relative)(base, path);
|
|
18205
|
+
if (rel && !rel.startsWith("..") && !(0, import_node_path32.isAbsolute)(rel)) return rel.split("\\").join("/");
|
|
18206
|
+
}
|
|
18207
|
+
return path;
|
|
18208
|
+
}
|
|
18209
|
+
async function gitCommit(projectDir, paths) {
|
|
18210
|
+
const run = async (args) => {
|
|
18211
|
+
const call = await safeGitInvocation(projectDir, args);
|
|
18212
|
+
const { stdout } = await (0, import_node_util4.promisify)(import_node_child_process6.execFile)("git", call.args, { cwd: projectDir, env: call.env, windowsHide: true, timeout: 5e3, encoding: "utf8" });
|
|
18213
|
+
return String(stdout);
|
|
18214
|
+
};
|
|
18215
|
+
let sha;
|
|
18216
|
+
try {
|
|
18217
|
+
sha = (await run(["rev-parse", "HEAD"])).trim();
|
|
18218
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) return null;
|
|
18219
|
+
} catch {
|
|
18220
|
+
return null;
|
|
18221
|
+
}
|
|
18222
|
+
let dirty = null;
|
|
18223
|
+
try {
|
|
18224
|
+
dirty = (await run(["status", "--porcelain", "--untracked-files=all", "--", ...paths])).trim().length > 0;
|
|
18225
|
+
} catch {
|
|
18226
|
+
dirty = null;
|
|
18227
|
+
}
|
|
18228
|
+
return { sha, dirty };
|
|
18229
|
+
}
|
|
18230
|
+
function allowlistedEnv(source) {
|
|
18231
|
+
const allowed = {};
|
|
18232
|
+
for (const key of ENV_ALLOWLIST) if (source[key] !== void 0) allowed[key] = source[key];
|
|
18233
|
+
return allowed;
|
|
18234
|
+
}
|
|
18235
|
+
async function withIsolation(fn) {
|
|
18236
|
+
const savedEnv = process.env;
|
|
18237
|
+
const savedFetch = globalThis.fetch;
|
|
18238
|
+
const savedConnect = import_node_net.default.Socket.prototype.connect;
|
|
18239
|
+
process.env = allowlistedEnv(savedEnv);
|
|
18240
|
+
globalThis.fetch = (async () => {
|
|
18241
|
+
throw new Error(NETWORK_DENIED);
|
|
18242
|
+
});
|
|
18243
|
+
import_node_net.default.Socket.prototype.connect = function denied() {
|
|
18244
|
+
throw new Error(NETWORK_DENIED);
|
|
18245
|
+
};
|
|
18246
|
+
try {
|
|
18247
|
+
return await fn();
|
|
18248
|
+
} finally {
|
|
18249
|
+
process.env = savedEnv;
|
|
18250
|
+
globalThis.fetch = savedFetch;
|
|
18251
|
+
import_node_net.default.Socket.prototype.connect = savedConnect;
|
|
18252
|
+
}
|
|
18253
|
+
}
|
|
18254
|
+
function valueCandidates(column, label) {
|
|
18255
|
+
if (column.enumLabels && column.enumLabels.length > 0) return label === "u" ? [...column.enumLabels] : [column.enumLabels[0]];
|
|
18256
|
+
const u2 = label === "u";
|
|
18257
|
+
switch (column.typname) {
|
|
18258
|
+
case "uuid":
|
|
18259
|
+
return [(0, import_node_crypto2.randomUUID)()];
|
|
18260
|
+
case "json":
|
|
18261
|
+
case "jsonb":
|
|
18262
|
+
return u2 ? ['{"vr":"u"}', "{}"] : ["{}"];
|
|
18263
|
+
case "date":
|
|
18264
|
+
return u2 ? ["2002-02-02", "2003-03-03"] : ["2001-01-01"];
|
|
18265
|
+
case "timestamp":
|
|
18266
|
+
case "timestamptz":
|
|
18267
|
+
return u2 ? ["2002-02-02 00:00:00+00", "2003-03-03 00:00:00+00"] : ["2001-01-01 00:00:00+00"];
|
|
18268
|
+
case "time":
|
|
18269
|
+
case "timetz":
|
|
18270
|
+
return u2 ? ["13:00:00", "14:00:00"] : ["12:00:00"];
|
|
18271
|
+
case "interval":
|
|
18272
|
+
return u2 ? ["2 days", "3 days"] : ["1 day"];
|
|
18273
|
+
default:
|
|
18274
|
+
break;
|
|
18275
|
+
}
|
|
18276
|
+
if (column.category === "S") {
|
|
18277
|
+
const values = [`vr-${label} rls-test value`, `vr-${label}`, label];
|
|
18278
|
+
return values.filter((v) => column.maxLength === null || v.length <= column.maxLength);
|
|
18279
|
+
}
|
|
18280
|
+
if (column.category === "N") return NUMBERS[label];
|
|
18281
|
+
if (column.category === "B") return u2 ? ["true", "false"] : ["false"];
|
|
18282
|
+
if (column.category === "A") return u2 ? [] : ["{}"];
|
|
18283
|
+
return [];
|
|
18284
|
+
}
|
|
18285
|
+
async function describeTable(db, table) {
|
|
18286
|
+
const display = `${table.schema}.${table.name}`;
|
|
18287
|
+
const rel = await db.query(
|
|
18288
|
+
`select c.oid::int as oid, c.relkind::text as kind from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = $1 and c.relname = $2`,
|
|
18289
|
+
[table.schema, table.name]
|
|
18290
|
+
);
|
|
18291
|
+
if (rel.rows.length === 0) return { reason: `${display} does not exist after the migrations; check the table name in the matrix.` };
|
|
18292
|
+
const kind = String(rel.rows[0].kind);
|
|
18293
|
+
if (kind !== "r" && kind !== "p") return { reason: `${display} is not a plain table (relkind ${kind}); views and other relations are not tested yet.` };
|
|
18294
|
+
const oid = Number(rel.rows[0].oid);
|
|
18295
|
+
const cols = await db.query(
|
|
18296
|
+
`select a.attname::text as name, a.attnotnull as not_null, a.atthasdef as has_default, a.attidentity::text as identity,
|
|
18297
|
+
a.attgenerated::text as generated, t.typname::text as typname, t.typcategory::text as category,
|
|
18298
|
+
format_type(a.atttypid, a.atttypmod) as type,
|
|
18299
|
+
case when t.typname in ('varchar', 'bpchar') and a.atttypmod > 4 then a.atttypmod - 4 end as max_length,
|
|
18300
|
+
(select array_agg(e.enumlabel::text order by e.enumsortorder) from pg_enum e where e.enumtypid = a.atttypid) as enum_labels,
|
|
18301
|
+
exists (select 1 from pg_constraint k where k.conrelid = a.attrelid and k.contype = 'f' and a.attnum = any (k.conkey)) as is_fk,
|
|
18302
|
+
exists (select 1 from pg_index i where i.indrelid = a.attrelid and i.indisprimary and a.attnum = any (i.indkey)) as is_pk,
|
|
18303
|
+
exists (select 1 from pg_index i where i.indrelid = a.attrelid and i.indisunique and i.indnkeyatts = 1 and i.indkey[0] = a.attnum) as is_unique
|
|
18304
|
+
from pg_attribute a join pg_type t on t.oid = a.atttypid
|
|
18305
|
+
where a.attrelid = $1 and a.attnum > 0 and not a.attisdropped
|
|
18306
|
+
order by a.attnum`,
|
|
18307
|
+
[oid]
|
|
18308
|
+
);
|
|
18309
|
+
const unique3 = /* @__PURE__ */ new Set();
|
|
18310
|
+
const columns = cols.rows.map((r) => {
|
|
18311
|
+
if (r.is_unique) unique3.add(String(r.name));
|
|
18312
|
+
return {
|
|
18313
|
+
name: String(r.name),
|
|
18314
|
+
notNull: Boolean(r.not_null),
|
|
18315
|
+
hasDefault: Boolean(r.has_default),
|
|
18316
|
+
identity: String(r.identity ?? ""),
|
|
18317
|
+
generated: String(r.generated ?? ""),
|
|
18318
|
+
typname: String(r.typname),
|
|
18319
|
+
category: String(r.category),
|
|
18320
|
+
type: String(r.type),
|
|
18321
|
+
maxLength: r.max_length === null || r.max_length === void 0 ? null : Number(r.max_length),
|
|
18322
|
+
enumLabels: Array.isArray(r.enum_labels) ? r.enum_labels : null,
|
|
18323
|
+
isFk: Boolean(r.is_fk),
|
|
18324
|
+
isPk: Boolean(r.is_pk)
|
|
18325
|
+
};
|
|
18326
|
+
});
|
|
18327
|
+
const byName = new Map(columns.map((c) => [c.name, c]));
|
|
18328
|
+
const owner = byName.get(table.ownerColumn);
|
|
18329
|
+
if (!owner) return { reason: `${display} has no column ${table.ownerColumn} (the matrix owner column).` };
|
|
18330
|
+
if (owner.typname !== "uuid" && owner.category !== "S") {
|
|
18331
|
+
return { reason: `${display}.${table.ownerColumn} is ${owner.type}; the owner column must hold auth.uid() (uuid or text).` };
|
|
18332
|
+
}
|
|
18333
|
+
const pk = columns.filter((c) => c.isPk);
|
|
18334
|
+
if (pk.length === 0) return { reason: `${display} has no primary key; rls-test targets the owner's row by primary key.` };
|
|
18335
|
+
for (const column of Object.keys(table.seed)) {
|
|
18336
|
+
if (!byName.has(column)) return { reason: `the matrix seeds ${display}.${column}, which does not exist.` };
|
|
18337
|
+
}
|
|
18338
|
+
if (table.updateColumn) {
|
|
18339
|
+
const column = byName.get(table.updateColumn);
|
|
18340
|
+
if (!column) return { reason: `the matrix updateColumn ${display}.${table.updateColumn} does not exist.` };
|
|
18341
|
+
if (valueCandidates(column, "u").length === 0) return { reason: `the test cannot invent a new value for ${display}.${column.name} (${column.type}); pick another updateColumn.` };
|
|
18342
|
+
}
|
|
18343
|
+
const writable = (c) => c.identity === "" && c.generated === "";
|
|
18344
|
+
const required = columns.filter((c) => c.name !== owner.name && c.notNull && !c.hasDefault && writable(c) && !(c.name in table.seed));
|
|
18345
|
+
for (const column of required) {
|
|
18346
|
+
if (column.isFk) return { reason: `${display}.${column.name} is a NOT NULL foreign key the test cannot invent; give it a value under "seed" in the matrix.` };
|
|
18347
|
+
if (valueCandidates(column, "a").length === 0) {
|
|
18348
|
+
return { reason: `${display}.${column.name} (${column.type}) is NOT NULL and the test cannot invent a value; give it one under "seed" in the matrix.` };
|
|
18349
|
+
}
|
|
18350
|
+
}
|
|
18351
|
+
const seedColumns = Object.keys(table.seed);
|
|
18352
|
+
return {
|
|
18353
|
+
shape: {
|
|
18354
|
+
table,
|
|
18355
|
+
oid,
|
|
18356
|
+
qualified: `${quoteIdent2(table.schema)}.${quoteIdent2(table.name)}`,
|
|
18357
|
+
display,
|
|
18358
|
+
columns,
|
|
18359
|
+
pk,
|
|
18360
|
+
owner,
|
|
18361
|
+
ownerUnique: unique3.has(owner.name),
|
|
18362
|
+
ownerInPk: pk.some((c) => c.name === owner.name),
|
|
18363
|
+
insertColumns: [owner.name, ...seedColumns, ...required.map((c) => c.name)],
|
|
18364
|
+
rowValues: (label, ownerId) => [ownerId, ...seedColumns.map((c) => table.seed[c]), ...required.map((c) => valueCandidates(c, label)[0])]
|
|
18365
|
+
}
|
|
18366
|
+
};
|
|
18367
|
+
}
|
|
18368
|
+
function pkCondition(shape, firstParam) {
|
|
18369
|
+
return `(${shape.pk.map((c) => quoteIdent2(c.name)).join(", ")}) = (${shape.pk.map((_3, i) => `$${firstParam + i}`).join(", ")})`;
|
|
18370
|
+
}
|
|
18371
|
+
async function privilegesFor(db, shape, role) {
|
|
18372
|
+
const t = (await db.query(
|
|
18373
|
+
`select has_table_privilege($1, $2::oid, 'SELECT') as ts, has_table_privilege($1, $2::oid, 'INSERT') as ti,
|
|
18374
|
+
has_table_privilege($1, $2::oid, 'UPDATE') as tu, has_table_privilege($1, $2::oid, 'DELETE') as td,
|
|
18375
|
+
has_any_column_privilege($1, $2::oid, 'SELECT') as cs, has_any_column_privilege($1, $2::oid, 'INSERT') as ci,
|
|
18376
|
+
has_any_column_privilege($1, $2::oid, 'UPDATE') as cu`,
|
|
18377
|
+
[role, shape.oid]
|
|
18378
|
+
)).rows[0];
|
|
18379
|
+
const cols = await db.query(
|
|
18380
|
+
`select a.attname::text as name, has_column_privilege($1, $2::oid, a.attnum, 'SELECT') as s,
|
|
18381
|
+
has_column_privilege($1, $2::oid, a.attnum, 'INSERT') as i, has_column_privilege($1, $2::oid, a.attnum, 'UPDATE') as u
|
|
18382
|
+
from pg_attribute a where a.attrelid = $2 and a.attnum > 0 and not a.attisdropped`,
|
|
18383
|
+
[role, shape.oid]
|
|
18384
|
+
);
|
|
18385
|
+
return {
|
|
18386
|
+
table: { select: Boolean(t.ts), insert: Boolean(t.ti), update: Boolean(t.tu), delete: Boolean(t.td) },
|
|
18387
|
+
any: { select: Boolean(t.cs), insert: Boolean(t.ci), update: Boolean(t.cu) },
|
|
18388
|
+
column: new Map(cols.rows.map((r) => [String(r.name), { select: Boolean(r.s), insert: Boolean(r.i), update: Boolean(r.u) }]))
|
|
18389
|
+
};
|
|
18390
|
+
}
|
|
18391
|
+
async function setClaims(db, claims) {
|
|
18392
|
+
await db.query(
|
|
18393
|
+
`select set_config('request.jwt.claims', $1, true), set_config('request.jwt.claim.sub', $2, true), set_config('request.jwt.claim.role', $3, true), set_config('request.jwt.claim.email', $4, true)`,
|
|
18394
|
+
[JSON.stringify(claims), claims.sub ?? "", claims.role ?? "", claims.email ?? ""]
|
|
18395
|
+
);
|
|
18396
|
+
}
|
|
18397
|
+
async function measure(db, shape, target) {
|
|
18398
|
+
const owner = quoteIdent2(shape.owner.name);
|
|
18399
|
+
const all = (await db.query(
|
|
18400
|
+
`select count(*)::int as n, coalesce(md5(string_agg(row_to_json(t)::text, E'\\n' order by row_to_json(t)::text)), '') as digest,
|
|
18401
|
+
(count(*) filter (where ${owner}::text = $1))::int as owned
|
|
18402
|
+
from ${shape.qualified} t`,
|
|
18403
|
+
[OWNER_ID]
|
|
18404
|
+
)).rows[0];
|
|
18405
|
+
const row = (await db.query(`select row_to_json(t)::text as j, ${owner}::text as o from ${shape.qualified} t where ${pkCondition(shape, 1)}`, target)).rows[0];
|
|
18406
|
+
return {
|
|
18407
|
+
count: Number(all.n),
|
|
18408
|
+
digest: String(all.digest),
|
|
18409
|
+
owned: Number(all.owned),
|
|
18410
|
+
target: row ? String(row.j) : null,
|
|
18411
|
+
targetOwner: row && row.o !== null ? String(row.o) : null
|
|
18412
|
+
};
|
|
18413
|
+
}
|
|
18414
|
+
async function runStatement(db, shape, target, stmt, session, prep) {
|
|
18415
|
+
await db.exec("begin");
|
|
18416
|
+
try {
|
|
18417
|
+
if (prep) await prep();
|
|
18418
|
+
const before = await measure(db, shape, target);
|
|
18419
|
+
await db.exec("savepoint vr_probe");
|
|
18420
|
+
let identity;
|
|
18421
|
+
if (session) {
|
|
18422
|
+
await db.exec(`set local role ${session.role}`);
|
|
18423
|
+
await setClaims(db, session.claims);
|
|
18424
|
+
const who = (await db.query(`select current_user::text as u, current_setting('is_superuser') as su, (select rolbypassrls from pg_roles where rolname = current_user) as b`)).rows[0];
|
|
18425
|
+
identity = { user: String(who.u), superuser: who.su === "on", bypass: Boolean(who.b) };
|
|
18426
|
+
}
|
|
18427
|
+
const run = { identity };
|
|
18428
|
+
try {
|
|
18429
|
+
const r = await db.query(stmt.sql, stmt.params);
|
|
18430
|
+
run.rows = r.rows;
|
|
18431
|
+
run.affected = r.affectedRows ?? 0;
|
|
18432
|
+
} catch (error) {
|
|
18433
|
+
if (!isSqlError(error)) throw error;
|
|
18434
|
+
run.error = sqlError(error);
|
|
18435
|
+
}
|
|
18436
|
+
if (!run.error && stmt.select) {
|
|
18437
|
+
await db.exec("savepoint vr_count");
|
|
18438
|
+
try {
|
|
18439
|
+
run.visible = Number((await db.query(`select count(*)::int as n from ${shape.qualified}`)).rows[0].n);
|
|
18440
|
+
await db.exec("release savepoint vr_count");
|
|
18441
|
+
} catch (error) {
|
|
18442
|
+
if (!isSqlError(error)) throw error;
|
|
18443
|
+
await db.exec("rollback to savepoint vr_count");
|
|
18444
|
+
}
|
|
18445
|
+
}
|
|
18446
|
+
if (run.error) await db.exec("rollback to savepoint vr_probe");
|
|
18447
|
+
else await db.exec("reset role");
|
|
18448
|
+
const after = await measure(db, shape, target);
|
|
18449
|
+
return { ...run, before, after };
|
|
18450
|
+
} finally {
|
|
18451
|
+
await db.exec("rollback");
|
|
18452
|
+
}
|
|
18453
|
+
}
|
|
18454
|
+
function writeControl(filtered, changed) {
|
|
18455
|
+
return (run) => {
|
|
18456
|
+
if (run.error) return { ok: false, detail: `as the superuser the same statement failed: ${run.error.message} (${run.error.code})` };
|
|
18457
|
+
if (!changed(run)) {
|
|
18458
|
+
return {
|
|
18459
|
+
ok: false,
|
|
18460
|
+
detail: filtered ? `as the superuser the same statement changed ${plural2(run.affected ?? 0, "row")}, expected 1` : "as the superuser the same statement did not change the owner row"
|
|
18461
|
+
};
|
|
18462
|
+
}
|
|
18463
|
+
return { ok: true, detail: `as the superuser the same statement changed ${plural2(filtered ? run.affected ?? 0 : 1, "row")}` };
|
|
18464
|
+
};
|
|
18465
|
+
}
|
|
18466
|
+
async function differingValue(db, shape, target, column) {
|
|
18467
|
+
for (const candidate of valueCandidates(column, "u")) {
|
|
18468
|
+
try {
|
|
18469
|
+
const r = await db.query(
|
|
18470
|
+
`select ($${target.length + 1}::${column.type}) is distinct from ${quoteIdent2(column.name)} as d from ${shape.qualified} where ${pkCondition(shape, 1)}`,
|
|
18471
|
+
[...target, candidate]
|
|
18472
|
+
);
|
|
18473
|
+
if (r.rows[0]?.d === true) return candidate;
|
|
18474
|
+
} catch (error) {
|
|
18475
|
+
if (!isSqlError(error)) throw error;
|
|
18476
|
+
}
|
|
18477
|
+
}
|
|
18478
|
+
return void 0;
|
|
18479
|
+
}
|
|
18480
|
+
async function selectPlans(c) {
|
|
18481
|
+
const { shape, target, privileges } = c;
|
|
18482
|
+
const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
|
|
18483
|
+
const noPrivilege = !privileges.table.select && !privileges.any.select;
|
|
18484
|
+
const privilegeDenied = (run) => isPrivilegeRefusal(run) && noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no SELECT on any column` } : void 0;
|
|
18485
|
+
if (pkSelectable || noPrivilege) {
|
|
18486
|
+
const pkList = shape.pk.map((col) => `${quoteIdent2(col.name)}::text`).join(", ");
|
|
18487
|
+
return [
|
|
18488
|
+
{
|
|
18489
|
+
stmt: { name: "select", sql: `select ${pkList} from ${shape.qualified} where ${pkCondition(shape, 1)}`, params: target, display: `select the owner's row from ${shape.display} by primary key`, filtered: true, select: "pk" },
|
|
18490
|
+
controlOk: (run) => run.error ? { ok: false, detail: `as the superuser the same statement failed: ${run.error.message} (${run.error.code})` } : run.rows?.length === 1 ? { ok: true, detail: "as the superuser the same statement returned 1 row" } : { ok: false, detail: `as the superuser the same statement returned ${plural2(run.rows?.length ?? 0, "row")}, expected 1` },
|
|
18491
|
+
judge: (run) => {
|
|
18492
|
+
const denied = privilegeDenied(run);
|
|
18493
|
+
if (denied) return denied;
|
|
18494
|
+
if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
|
|
18495
|
+
const sees = run.visible === void 0 ? "" : ` (sees ${run.visible} of ${run.before.count} rows)`;
|
|
18496
|
+
if (run.rows?.length === 1) return { verdict: "allowed", detail: `returned the owner row${sees}` };
|
|
18497
|
+
if (run.rows?.length === 0) return { verdict: "denied", detail: `returned no row${sees}` };
|
|
18498
|
+
return { verdict: "unknown", detail: `returned ${run.rows?.length} rows for one primary key` };
|
|
18499
|
+
}
|
|
18500
|
+
}
|
|
18501
|
+
];
|
|
18502
|
+
}
|
|
18503
|
+
const granted = shape.columns.filter((col) => privileges.column.get(col.name)?.select);
|
|
18504
|
+
const cols = granted.map((col) => `${quoteIdent2(col.name)}::text as ${quoteIdent2(col.name)}`).join(", ");
|
|
18505
|
+
const all = (await c.db.query(`select ${cols}, (${pkCondition(shape, 1)}) as is_target from ${shape.qualified}`, target)).rows;
|
|
18506
|
+
const key = (row) => JSON.stringify(granted.map((col) => row[col.name] ?? null));
|
|
18507
|
+
const targetRow = all.find((r) => r.is_target === true);
|
|
18508
|
+
const targetKey = targetRow ? key(targetRow) : void 0;
|
|
18509
|
+
const unique3 = targetKey !== void 0 && all.filter((r) => key(r) === targetKey).length === 1;
|
|
18510
|
+
const found = (run) => Boolean(targetKey && run.rows?.some((r) => key(r) === targetKey));
|
|
18511
|
+
const names = granted.map((col) => col.name).join(", ");
|
|
18512
|
+
return [
|
|
18513
|
+
{
|
|
18514
|
+
stmt: { name: "select columns", sql: `select ${cols} from ${shape.qualified}`, params: [], display: `select the granted columns (${names}) of ${shape.display}, no row filter`, filtered: false, select: "columns" },
|
|
18515
|
+
controlOk: (run) => found(run) ? { ok: true, detail: "as the superuser the same statement returned the owner row" } : { ok: false, detail: "as the superuser the same statement did not return the owner row" },
|
|
18516
|
+
judge: (run) => {
|
|
18517
|
+
if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
|
|
18518
|
+
if (!unique3) return { verdict: "unknown", detail: `the granted columns (${names}) do not uniquely identify the owner row` };
|
|
18519
|
+
if (found(run)) return { verdict: "allowed", detail: `read the owner row through the granted columns (${names})` };
|
|
18520
|
+
return { verdict: "denied", detail: `the owner row is not among the rows returned through the granted columns (${names})` };
|
|
18521
|
+
}
|
|
18522
|
+
}
|
|
18523
|
+
];
|
|
18524
|
+
}
|
|
18525
|
+
function insertPlans(c) {
|
|
18526
|
+
const { shape, privileges, db } = c;
|
|
18527
|
+
const values = shape.rowValues("i", OWNER_ID);
|
|
18528
|
+
const placeholders = values.map((_3, i) => `$${i + 1}`).join(", ");
|
|
18529
|
+
const noPrivilege = !privileges.table.insert && !privileges.any.insert;
|
|
18530
|
+
const owner = quoteIdent2(shape.owner.name);
|
|
18531
|
+
const prep = shape.ownerUnique ? async () => void await db.query(`delete from ${shape.qualified} where ${owner}::text = $1`, [OWNER_ID]) : void 0;
|
|
18532
|
+
const added = (run) => run.after.owned === run.before.owned + 1 && run.after.count === run.before.count + 1;
|
|
18533
|
+
return [
|
|
18534
|
+
{
|
|
18535
|
+
stmt: {
|
|
18536
|
+
name: "insert",
|
|
18537
|
+
sql: `insert into ${shape.qualified} (${shape.insertColumns.map(quoteIdent2).join(", ")}) values (${placeholders})`,
|
|
18538
|
+
params: values,
|
|
18539
|
+
display: `insert into ${shape.display} a row whose ${shape.owner.name} is the owner`,
|
|
18540
|
+
filtered: true
|
|
18541
|
+
},
|
|
18542
|
+
prep,
|
|
18543
|
+
controlOk: (run) => {
|
|
18544
|
+
if (run.error) return { ok: false, detail: `as the superuser the same statement failed: ${run.error.message} (${run.error.code})` };
|
|
18545
|
+
return added(run) ? { ok: true, detail: "as the superuser the same statement changed 1 row" } : { ok: false, detail: `as the superuser the same statement changed ${plural2(run.affected ?? 0, "row")}, expected 1` };
|
|
18546
|
+
},
|
|
18547
|
+
judge: (run) => {
|
|
18548
|
+
if (isRlsRefusal(run)) return { verdict: "unknown", detail: `refused by WITH CHECK: ${run.error.message} (42501). Only the generated row was tried; other values may allow insertion` };
|
|
18549
|
+
if (isPrivilegeRefusal(run)) {
|
|
18550
|
+
return noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no INSERT on any column` } : { verdict: "unknown", detail: `refused for missing column privileges (${run.error.message}), but the role can insert some columns` };
|
|
18551
|
+
}
|
|
18552
|
+
if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
|
|
18553
|
+
if (added(run)) return { verdict: "allowed", detail: `1 row added for the owner (${run.before.count} to ${run.after.count} rows)` };
|
|
18554
|
+
if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows added, table unchanged" };
|
|
18555
|
+
return {
|
|
18556
|
+
verdict: "unknown",
|
|
18557
|
+
detail: `reported ${plural2(run.affected ?? 0, "row")}, but the table went from ${run.before.count} to ${run.after.count} rows and the owner's rows from ${run.before.owned} to ${run.after.owned}`
|
|
18558
|
+
};
|
|
18559
|
+
}
|
|
18560
|
+
}
|
|
18561
|
+
];
|
|
18562
|
+
}
|
|
18563
|
+
function freeOtherUserSlot(db, shape) {
|
|
18564
|
+
if (!shape.ownerUnique) return void 0;
|
|
18565
|
+
return async () => void await db.query(`delete from ${shape.qualified} where ${quoteIdent2(shape.owner.name)}::text = $1`, [OTHER_USER_ID]);
|
|
18566
|
+
}
|
|
18567
|
+
function pickUpdateColumn(shape, privileges) {
|
|
18568
|
+
if (shape.table.updateColumn) return shape.columns.find((col) => col.name === shape.table.updateColumn);
|
|
18569
|
+
const candidates = shape.columns.filter(
|
|
18570
|
+
(col) => col.name !== shape.owner.name && !col.isPk && !col.isFk && col.identity === "" && col.generated === "" && col.category !== "A" && valueCandidates(col, "u").length > 0
|
|
18571
|
+
);
|
|
18572
|
+
if (!privileges.any.update) return candidates[0];
|
|
18573
|
+
return candidates.find((col) => privileges.column.get(col.name)?.update);
|
|
18574
|
+
}
|
|
18575
|
+
async function updatePlans(c) {
|
|
18576
|
+
const { db, shape, target, privileges, actor } = c;
|
|
18577
|
+
const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
|
|
18578
|
+
const noPrivilege = !privileges.table.update && !privileges.any.update;
|
|
18579
|
+
const filtered = pkSelectable || noPrivilege;
|
|
18580
|
+
const where = filtered ? ` where ${pkCondition(shape, 1)}` : "";
|
|
18581
|
+
const baseParams = filtered ? target : [];
|
|
18582
|
+
const ownerUpdatable = Boolean(privileges.column.get(shape.owner.name)?.update) || noPrivilege;
|
|
18583
|
+
const column = pickUpdateColumn(shape, privileges);
|
|
18584
|
+
const value = column ? await differingValue(db, shape, target, column) : void 0;
|
|
18585
|
+
const plans = [];
|
|
18586
|
+
const add = (name, sets, display, noop = false, prep) => {
|
|
18587
|
+
let n = baseParams.length;
|
|
18588
|
+
const assignments = sets.map((s) => `${quoteIdent2(s.column)} = ${s.value === SELF ? quoteIdent2(s.column) : `$${++n}`}`).join(", ");
|
|
18589
|
+
const sql = `update ${shape.qualified} set ${assignments}${where}`;
|
|
18590
|
+
const params = [...baseParams, ...sets.filter((s) => s.value !== SELF).map((s) => s.value)];
|
|
18591
|
+
const changed = (run) => noop ? (run.affected ?? 0) >= 1 && run.after.target !== null : run.after.target !== null && run.after.target !== run.before.target;
|
|
18592
|
+
plans.push({
|
|
18593
|
+
stmt: { name, sql, params, display: `${display}${filtered ? "" : ", no row filter (the role cannot read the primary key)"}`, filtered },
|
|
18594
|
+
prep,
|
|
18595
|
+
controlOk: writeControl(filtered, changed),
|
|
18596
|
+
judge: (run) => {
|
|
18597
|
+
if (isRlsRefusal(run)) return { verdict: "unknown", detail: `refused by WITH CHECK: ${run.error.message}. USING matched the owner row, so this is not a denial` };
|
|
18598
|
+
if (isPrivilegeRefusal(run)) {
|
|
18599
|
+
return noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no UPDATE on any column` } : { verdict: "unknown", detail: `refused for column privileges (${run.error.message}), but the role can update some columns` };
|
|
18600
|
+
}
|
|
18601
|
+
if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
|
|
18602
|
+
if (changed(run)) return { verdict: "allowed", detail: noop ? "1 row updated (rewrote its own value)" : `${changedRows(filtered ? run.affected : 1)} (${sets.map((s) => s.column).join(" and ")} rewritten)` };
|
|
18603
|
+
if (!filtered) return { verdict: "denied", detail: "the owner row is unchanged" };
|
|
18604
|
+
if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows changed, the row is still there and unchanged" };
|
|
18605
|
+
return { verdict: "unknown", detail: `reported ${plural2(run.affected ?? 0, "row")}, but the owner row ${run.after.target === null ? "is gone" : "did not change"} and the table ${run.after.digest === run.before.digest ? "did not" : "did"} change` };
|
|
18606
|
+
}
|
|
18607
|
+
});
|
|
18608
|
+
};
|
|
18609
|
+
if (column && value !== void 0) add("update value", [{ column: column.name, value }], `update ${column.name} on the owner's row in ${shape.display}`);
|
|
18610
|
+
if (actor === "other_user" && ownerUpdatable && !shape.ownerInPk) {
|
|
18611
|
+
const sets = [{ column: shape.owner.name, value: OTHER_USER_ID }];
|
|
18612
|
+
if (column && value !== void 0) sets.push({ column: column.name, value });
|
|
18613
|
+
add(
|
|
18614
|
+
"update claiming the row",
|
|
18615
|
+
sets,
|
|
18616
|
+
`update the owner's row setting ${shape.owner.name} to the other user${column && value !== void 0 ? ` and ${column.name}` : ""}`,
|
|
18617
|
+
false,
|
|
18618
|
+
freeOtherUserSlot(db, shape)
|
|
18619
|
+
);
|
|
18620
|
+
}
|
|
18621
|
+
if (plans.length === 0) {
|
|
18622
|
+
add("update noop", [{ column: shape.owner.name, value: SELF }], `update the owner's row in ${shape.display} (${shape.owner.name} = itself)`, true);
|
|
18623
|
+
}
|
|
18624
|
+
return plans;
|
|
18625
|
+
}
|
|
18626
|
+
function reassignPlans(c) {
|
|
18627
|
+
const { db, shape, target, privileges, actor } = c;
|
|
18628
|
+
const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
|
|
18629
|
+
const ownerUpdatable = Boolean(privileges.column.get(shape.owner.name)?.update);
|
|
18630
|
+
const noPrivilege = !ownerUpdatable;
|
|
18631
|
+
const filtered = pkSelectable || noPrivilege;
|
|
18632
|
+
const where = filtered ? ` where ${pkCondition(shape, 1)}` : "";
|
|
18633
|
+
const baseParams = filtered ? target : [];
|
|
18634
|
+
const owner = quoteIdent2(shape.owner.name);
|
|
18635
|
+
const moved = (run) => run.after.targetOwner === OTHER_USER_ID;
|
|
18636
|
+
const stmt = {
|
|
18637
|
+
name: "reassign",
|
|
18638
|
+
sql: `update ${shape.qualified} set ${owner} = $${baseParams.length + 1}${where}`,
|
|
18639
|
+
params: [...baseParams, OTHER_USER_ID],
|
|
18640
|
+
display: `set ${shape.owner.name} on the owner's row in ${shape.display} to the other user${filtered ? "" : ", no row filter"}`,
|
|
18641
|
+
filtered
|
|
18642
|
+
};
|
|
18643
|
+
return [
|
|
18644
|
+
{
|
|
18645
|
+
stmt,
|
|
18646
|
+
prep: freeOtherUserSlot(db, shape),
|
|
18647
|
+
controlOk: writeControl(filtered, moved),
|
|
18648
|
+
judge: (run) => {
|
|
18649
|
+
if (isRlsRefusal(run)) return { verdict: "unknown", detail: `refused by WITH CHECK: ${run.error.message} (42501). A transfer combined with other values was not tested, so this may not be about the owner change alone` };
|
|
18650
|
+
if (isPrivilegeRefusal(run)) {
|
|
18651
|
+
return noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no UPDATE on ${shape.owner.name}` } : { verdict: "unknown", detail: `refused for privileges (${run.error.message}), but the role can update ${shape.owner.name}` };
|
|
18652
|
+
}
|
|
18653
|
+
if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
|
|
18654
|
+
if (moved(run)) return { verdict: "allowed", detail: `the owner row now belongs to the other user` };
|
|
18655
|
+
if (!filtered && run.after.target === run.before.target) return { verdict: "denied", detail: "the owner row is unchanged" };
|
|
18656
|
+
if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows changed, the row is still there and unchanged" };
|
|
18657
|
+
return { verdict: "unknown", detail: `reported ${plural2(run.affected ?? 0, "row")}, but the owner column did not move` };
|
|
18658
|
+
}
|
|
18659
|
+
}
|
|
18660
|
+
];
|
|
17336
18661
|
}
|
|
17337
|
-
|
|
17338
|
-
const
|
|
17339
|
-
const
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
18662
|
+
function deletePlans(c) {
|
|
18663
|
+
const { shape, target, privileges } = c;
|
|
18664
|
+
const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
|
|
18665
|
+
const noPrivilege = !privileges.table.delete;
|
|
18666
|
+
const filtered = pkSelectable || noPrivilege;
|
|
18667
|
+
const gone = (run) => run.after.target === null && run.before.target !== null;
|
|
18668
|
+
return [
|
|
18669
|
+
{
|
|
18670
|
+
stmt: {
|
|
18671
|
+
name: "delete",
|
|
18672
|
+
sql: `delete from ${shape.qualified}${filtered ? ` where ${pkCondition(shape, 1)}` : ""}`,
|
|
18673
|
+
params: filtered ? target : [],
|
|
18674
|
+
display: `delete the owner's row from ${shape.display}${filtered ? "" : ", no row filter (the role cannot read the primary key)"}`,
|
|
18675
|
+
filtered
|
|
18676
|
+
},
|
|
18677
|
+
controlOk: writeControl(filtered, gone),
|
|
18678
|
+
judge: (run) => {
|
|
18679
|
+
if (isPrivilegeRefusal(run) && noPrivilege) return { verdict: "denied", detail: `refused: ${run.error.message} (42501), no DELETE privilege` };
|
|
18680
|
+
if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
|
|
18681
|
+
if (gone(run)) return { verdict: "allowed", detail: `${filtered ? "1 row deleted" : "deleted"}, the owner row is gone (${run.before.count} to ${run.after.count} rows)` };
|
|
18682
|
+
if (!filtered) return { verdict: "denied", detail: "the owner row is still there" };
|
|
18683
|
+
if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows changed, the row is still there and unchanged" };
|
|
18684
|
+
return {
|
|
18685
|
+
verdict: "unknown",
|
|
18686
|
+
detail: `reported ${plural2(run.affected ?? 0, "row")}, but the owner row is ${run.after.target === null ? "gone" : "still there"} and the table ${run.after.digest === run.before.digest ? "did not" : "did"} change`
|
|
18687
|
+
};
|
|
18688
|
+
}
|
|
18689
|
+
}
|
|
18690
|
+
];
|
|
17343
18691
|
}
|
|
17344
|
-
function
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
if (
|
|
17349
|
-
|
|
18692
|
+
async function runCell(ctx, privilegesByRole, actor, action, expected) {
|
|
18693
|
+
const base = { table: ctx.shape.display, actor, action, expected };
|
|
18694
|
+
if (expected === "skip") return { ...base, outcome: "N/A", observed: { summary: "not tested (skip in the matrix)" }, reason: "The matrix marks this cell skip." };
|
|
18695
|
+
const session = ACTOR_SESSION[actor];
|
|
18696
|
+
if (action === "reassign" && ctx.shape.ownerInPk) {
|
|
18697
|
+
const reason2 = `${ctx.shape.owner.name} is both the owner column and the primary key, so moving a row to another user changes which row it is; reassign is not tested on this table.`;
|
|
18698
|
+
return { ...base, outcome: "Unknown", observed: { summary: "not tested: the owner column is the primary key" }, reason: reason2 };
|
|
18699
|
+
}
|
|
18700
|
+
const c = { ...ctx, actor, privileges: privilegesByRole.get(session.role) };
|
|
18701
|
+
const plans = action === "select" ? await selectPlans(c) : action === "insert" ? insertPlans(c) : action === "update" ? await updatePlans(c) : action === "reassign" ? reassignPlans(c) : deletePlans(c);
|
|
18702
|
+
const records = [];
|
|
18703
|
+
let decisive;
|
|
18704
|
+
let firstRun;
|
|
18705
|
+
for (const plan of plans) {
|
|
18706
|
+
const control = plan.controlOk(await runStatement(ctx.db, ctx.shape, ctx.target, plan.stmt, null, plan.prep));
|
|
18707
|
+
const run = await runStatement(ctx.db, ctx.shape, ctx.target, plan.stmt, session, plan.prep);
|
|
18708
|
+
firstRun = firstRun ?? run;
|
|
18709
|
+
let judged;
|
|
18710
|
+
const id = run.identity;
|
|
18711
|
+
if (!id || id.superuser || id.bypass || id.user !== session.role) {
|
|
18712
|
+
judged = { verdict: "unknown", detail: `the probe ran as ${id?.user} (superuser ${id?.superuser}, BYPASSRLS ${id?.bypass}), so RLS may not have applied` };
|
|
18713
|
+
} else if (!control.ok) {
|
|
18714
|
+
judged = { verdict: "unknown", detail: `positive control failed: ${control.detail}` };
|
|
18715
|
+
} else {
|
|
18716
|
+
judged = plan.judge(run);
|
|
18717
|
+
}
|
|
18718
|
+
const record = { name: plan.stmt.name, statement: plan.stmt.display, verdict: judged.verdict, detail: judged.detail, affectedRows: run.affected, ...run.error ? { error: run.error } : {}, control };
|
|
18719
|
+
records.push(record);
|
|
18720
|
+
if (judged.verdict === "allowed" && !decisive) decisive = { run, record };
|
|
18721
|
+
}
|
|
18722
|
+
let verdict;
|
|
18723
|
+
if (decisive) verdict = "allowed";
|
|
18724
|
+
else if (records.length > 0 && records.every((r2) => r2.verdict === "denied")) verdict = "denied";
|
|
18725
|
+
else verdict = "unknown";
|
|
18726
|
+
const shown = decisive ?? { run: firstRun, record: records.find((r2) => r2.verdict !== "denied") ?? records[0] };
|
|
18727
|
+
const r = shown.run;
|
|
18728
|
+
const observed = {
|
|
18729
|
+
statement: shown.record.statement,
|
|
18730
|
+
role: session.role,
|
|
18731
|
+
currentUser: r.identity?.user,
|
|
18732
|
+
superuser: r.identity?.superuser,
|
|
18733
|
+
bypassRls: r.identity?.bypass,
|
|
18734
|
+
rowsReturned: r.rows && plans[0].stmt.select === "pk" ? r.rows.length : void 0,
|
|
18735
|
+
visibleRows: r.visible,
|
|
18736
|
+
affectedRows: plans[0].stmt.select ? void 0 : r.affected,
|
|
18737
|
+
...r.error ? { error: r.error } : {},
|
|
18738
|
+
rowCountBefore: r.before.count,
|
|
18739
|
+
rowCountAfter: r.after.count,
|
|
18740
|
+
stateChanged: r.after.digest !== r.before.digest,
|
|
18741
|
+
targetPresentAfter: r.after.target !== null,
|
|
18742
|
+
control: shown.record.control,
|
|
18743
|
+
probes: records,
|
|
18744
|
+
...action === "insert" && ctx.shape.ownerUnique ? { note: `${ctx.shape.owner.name} allows one row per user, so the owner's existing row was removed inside the rolled-back probe transaction first` } : {},
|
|
18745
|
+
summary: verdict === "unknown" ? records.filter((x2) => x2.verdict !== "denied").map((x2) => x2.detail).join("; ") : shown.record.detail
|
|
18746
|
+
};
|
|
18747
|
+
if (verdict === "unknown") {
|
|
18748
|
+
return { ...base, outcome: "Unknown", observed, reason: `Not settled by the evidence: ${observed.summary}.` };
|
|
17350
18749
|
}
|
|
17351
|
-
|
|
17352
|
-
|
|
18750
|
+
const allowed = verdict === "allowed";
|
|
18751
|
+
const want = expected === "allow";
|
|
18752
|
+
const pass = allowed === want;
|
|
18753
|
+
const actorText = actor === "other_user" ? "another signed-in user" : actor === "anon" ? "anon" : "the owner";
|
|
18754
|
+
const reason = pass ? `Expected ${expected}, observed ${allowed ? "allowed" : "denied"}.` : want ? `Expected ${actorText} to be allowed to ${action}, but ${observed.summary}.${action !== "select" && !r.error ? " Postgres reports rows no policy allows as 0 rows, not as an error, so the app sees no failure." : ""}` : `Expected ${actorText} to be denied ${action}, but ${observed.summary}.`;
|
|
18755
|
+
return { ...base, outcome: pass ? "Pass" : "Fail", observed, reason };
|
|
18756
|
+
}
|
|
18757
|
+
function cellsFor(table) {
|
|
18758
|
+
const out = [];
|
|
18759
|
+
for (const actor of TESTED_ACTORS) {
|
|
18760
|
+
for (const action of MATRIX_ACTIONS) out.push({ actor, action, expected: table.expect[actor][action] });
|
|
18761
|
+
const reassign = table.expect[actor].reassign;
|
|
18762
|
+
if (reassign) out.push({ actor, action: "reassign", expected: reassign });
|
|
17353
18763
|
}
|
|
17354
|
-
return
|
|
18764
|
+
return out;
|
|
17355
18765
|
}
|
|
17356
|
-
function
|
|
17357
|
-
|
|
18766
|
+
function unknownCells(table, reason, summary) {
|
|
18767
|
+
const display = `${table.schema}.${table.name}`;
|
|
18768
|
+
const cells = cellsFor(table).map(
|
|
18769
|
+
({ actor, action, expected }) => expected === "skip" ? { table: display, actor, action, expected, outcome: "N/A", observed: { summary: "not tested (skip in the matrix)" }, reason: "The matrix marks this cell skip." } : { table: display, actor, action, expected, outcome: "Unknown", observed: { summary }, reason }
|
|
18770
|
+
);
|
|
18771
|
+
return [...cells, ...serviceRoleCells(table)];
|
|
18772
|
+
}
|
|
18773
|
+
function serviceRoleCells(table) {
|
|
18774
|
+
if (!table.serviceRoleExcluded) return [];
|
|
18775
|
+
const display = `${table.schema}.${table.name}`;
|
|
18776
|
+
return MATRIX_ACTIONS.map((action) => ({
|
|
18777
|
+
table: display,
|
|
18778
|
+
actor: "service_role",
|
|
18779
|
+
action,
|
|
18780
|
+
expected: "excluded",
|
|
18781
|
+
outcome: "N/A",
|
|
18782
|
+
observed: { summary: "excluded: the service role bypasses RLS by design" },
|
|
18783
|
+
reason: "The matrix excludes the service role."
|
|
18784
|
+
}));
|
|
17358
18785
|
}
|
|
17359
|
-
function
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
return "Large log file that can add token noise. Review it before changing ignore rules or excluding it from agent context.";
|
|
17365
|
-
case "cache-directory":
|
|
17366
|
-
return "Build cache evidence. Do not load it into agent context unless debugging build internals.";
|
|
17367
|
-
}
|
|
18786
|
+
function summarise(tables) {
|
|
18787
|
+
const cells = tables.flatMap((t) => t.cells);
|
|
18788
|
+
const count = (o2) => cells.filter((c) => c.outcome === o2).length;
|
|
18789
|
+
const notApplicable = count("N/A");
|
|
18790
|
+
return { pass: count("Pass"), fail: count("Fail"), unknown: count("Unknown"), notApplicable, tested: cells.length - notApplicable, total: cells.length };
|
|
17368
18791
|
}
|
|
17369
|
-
function
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
18792
|
+
function verdictFor(summary) {
|
|
18793
|
+
if (summary.fail > 0) {
|
|
18794
|
+
return {
|
|
18795
|
+
verdict: `Not ready within tested scope: ${plural2(summary.fail, "cell does", "cells do")} not match the matrix; ${plural2(summary.unknown, "unknown")}.`,
|
|
18796
|
+
exitCode: 1
|
|
18797
|
+
};
|
|
17373
18798
|
}
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
lines.push(` - Category: ${item4.category}`);
|
|
17377
|
-
lines.push(` - Action: ${item4.action}`);
|
|
17378
|
-
lines.push(` - Reason: ${item4.reason}`);
|
|
18799
|
+
if (summary.pass === 0) {
|
|
18800
|
+
return { verdict: `Nothing was observed: ${plural2(summary.unknown, "unknown")}, ${summary.notApplicable} N/A.`, exitCode: 2 };
|
|
17379
18801
|
}
|
|
17380
|
-
|
|
17381
|
-
|
|
18802
|
+
if (summary.unknown > 0) {
|
|
18803
|
+
return { verdict: `No blockers found within tested scope; ${plural2(summary.unknown, "unknown")}.`, exitCode: 2 };
|
|
18804
|
+
}
|
|
18805
|
+
return { verdict: "No blockers found within tested scope; 0 unknowns. Scenario passed locally.", exitCode: 0 };
|
|
17382
18806
|
}
|
|
17383
|
-
async function
|
|
17384
|
-
|
|
17385
|
-
|
|
18807
|
+
async function listMigrations(dir) {
|
|
18808
|
+
try {
|
|
18809
|
+
const entries = await (0, import_promises26.readdir)(dir, { withFileTypes: true });
|
|
18810
|
+
return entries.filter((e2) => e2.isFile() && /\.sql$/i.test(e2.name) && !e2.name.startsWith(".")).map((e2) => e2.name).sort((a, b3) => a < b3 ? -1 : a > b3 ? 1 : 0);
|
|
18811
|
+
} catch {
|
|
18812
|
+
return [];
|
|
17386
18813
|
}
|
|
17387
|
-
|
|
18814
|
+
}
|
|
18815
|
+
async function runRlsTest(options) {
|
|
18816
|
+
const projectDir = (0, import_node_path32.resolve)(options.projectDir);
|
|
18817
|
+
const matrixPath = (0, import_node_path32.resolve)(projectDir, options.matrixPath ?? DEFAULT_MATRIX_FILE);
|
|
18818
|
+
const migrationsDir = (0, import_node_path32.resolve)(projectDir, options.migrationsDir ?? (0, import_node_path32.join)("supabase", "migrations"));
|
|
18819
|
+
const timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
|
|
18820
|
+
if ((0, import_node_path32.basename)(matrixPath).toLowerCase().startsWith(".env")) {
|
|
18821
|
+
throw new RlsInputError(`Refusing to read ${(0, import_node_path32.basename)(matrixPath)}: rls-test never reads .env files. Point --matrix at a viberaven.permissions.json.`);
|
|
18822
|
+
}
|
|
18823
|
+
const filesRead = [];
|
|
18824
|
+
let matrixText;
|
|
17388
18825
|
try {
|
|
17389
|
-
|
|
18826
|
+
const info = await (0, import_promises26.stat)(matrixPath);
|
|
18827
|
+
if (!info.isFile()) throw new Error("not a file");
|
|
18828
|
+
matrixText = normalise(await (0, import_promises26.readFile)(matrixPath, "utf8"));
|
|
17390
18829
|
} catch {
|
|
17391
|
-
|
|
18830
|
+
throw new RlsInputError(
|
|
18831
|
+
`No permissions matrix at ${matrixPath}. Write one by hand (schema: schemas/permissions-matrix.schema.json in the @viberaven/cli package); rls-test never infers it from your policies.`
|
|
18832
|
+
);
|
|
17392
18833
|
}
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
18834
|
+
filesRead.push(displayPath(projectDir, matrixPath));
|
|
18835
|
+
const matrix = parsePermissionsMatrix(matrixText);
|
|
18836
|
+
const names = await listMigrations(migrationsDir);
|
|
18837
|
+
const migrationFiles = [];
|
|
18838
|
+
for (const name of names) {
|
|
18839
|
+
const sql = normalise(await (0, import_promises26.readFile)((0, import_node_path32.join)(migrationsDir, name), "utf8"));
|
|
18840
|
+
filesRead.push(displayPath(projectDir, (0, import_node_path32.join)(migrationsDir, name)));
|
|
18841
|
+
migrationFiles.push({ name, sha256: sha256(sql), sql });
|
|
18842
|
+
}
|
|
18843
|
+
const migrationsHash = sha256(migrationFiles.map((f) => `${f.name}\0${f.sha256}`).join("\n"));
|
|
18844
|
+
const commit = await gitCommit(projectDir, [migrationsDir, matrixPath]);
|
|
18845
|
+
const noReassign = matrix.tables.filter((t) => TESTED_ACTORS.every((a) => t.expect[a].reassign === void 0)).map((t) => `${t.schema}.${t.name}`);
|
|
18846
|
+
const result = {
|
|
18847
|
+
label: NON_AUTHORITATIVE_LABEL,
|
|
18848
|
+
experimental: true,
|
|
18849
|
+
tool: options.tool ?? "viberaven rls-test",
|
|
18850
|
+
project: projectDir,
|
|
18851
|
+
commit,
|
|
18852
|
+
engine: { name: "PGlite", available: false },
|
|
18853
|
+
migrations: { dir: displayPath(projectDir, migrationsDir), files: migrationFiles.map(({ name, sha256: hash }) => ({ name, sha256: hash })), hash: migrationsHash, applied: false },
|
|
18854
|
+
matrix: { path: displayPath(projectDir, matrixPath), hash: matrix.hash, defaultGrants: matrix.defaultGrants },
|
|
18855
|
+
filesRead,
|
|
18856
|
+
assumptions: assumptionsFor(matrix.defaultGrants),
|
|
18857
|
+
exclusions: [
|
|
18858
|
+
...IMITATION_EXCLUSIONS,
|
|
18859
|
+
...PROBE_EXCLUSIONS,
|
|
18860
|
+
...noReassign.length > 0 ? [`Ownership transfer (reassign) is not in the matrix for ${noReassign.join(", ")}, so it was not tested.`] : [],
|
|
18861
|
+
...matrix.tables.some((t) => t.serviceRoleExcluded) ? ["The service role bypasses RLS and is excluded by the matrix."] : []
|
|
18862
|
+
],
|
|
18863
|
+
roleChecks: [],
|
|
18864
|
+
tables: [],
|
|
18865
|
+
summary: { pass: 0, fail: 0, unknown: 0, notApplicable: 0, tested: 0, total: 0 },
|
|
18866
|
+
verdict: "",
|
|
18867
|
+
exitCode: 2
|
|
18868
|
+
};
|
|
18869
|
+
const finish = () => {
|
|
18870
|
+
result.summary = summarise(result.tables);
|
|
18871
|
+
const { verdict, exitCode } = verdictFor(result.summary);
|
|
18872
|
+
result.verdict = verdict;
|
|
18873
|
+
result.exitCode = exitCode;
|
|
18874
|
+
return result;
|
|
18875
|
+
};
|
|
18876
|
+
const allUnknown = (reason, summary) => {
|
|
18877
|
+
result.tables = matrix.tables.map((t) => ({ name: `${t.schema}.${t.name}`, unknownReason: reason, cells: unknownCells(t, reason, summary) }));
|
|
18878
|
+
result.roleChecks = result.roleChecks.filter((c) => !c.role.includes(" on "));
|
|
18879
|
+
return finish();
|
|
18880
|
+
};
|
|
18881
|
+
if (migrationFiles.length === 0) {
|
|
18882
|
+
return allUnknown(`No migrations found in ${result.migrations.dir}; there is nothing to test.`, "no migrations");
|
|
18883
|
+
}
|
|
18884
|
+
return withIsolation(async () => {
|
|
18885
|
+
let db;
|
|
18886
|
+
let engineLoaded = false;
|
|
18887
|
+
try {
|
|
18888
|
+
const engine = await (options.loadPglite ?? defaultPgliteLoader)();
|
|
18889
|
+
engineLoaded = true;
|
|
18890
|
+
db = await engine.create({ env: allowlistedEnv(process.env), statementTimeoutMs: timeouts.statementMs });
|
|
18891
|
+
} catch (error) {
|
|
18892
|
+
result.engine.error = firstLine(error instanceof Error ? error.message : String(error));
|
|
18893
|
+
if (engineLoaded) return allUnknown(`PGlite could not start: ${result.engine.error}. Nothing was tested.`, "not run: PGlite startup failed");
|
|
18894
|
+
const missing = error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
|
18895
|
+
return missing ? allUnknown(PGLITE_INSTALL_HINT, "not run: PGlite is not installed") : allUnknown(`PGlite could not load: ${result.engine.error}. Nothing was tested.`, "not run: PGlite loading failed");
|
|
17396
18896
|
}
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
18897
|
+
try {
|
|
18898
|
+
result.engine.available = true;
|
|
18899
|
+
const version = String((await db.query("select version() as v")).rows[0]?.v ?? "");
|
|
18900
|
+
result.engine.postgres = version.match(/PostgreSQL [0-9.]+/)?.[0];
|
|
18901
|
+
result.engine.version = version.match(/PGlite ([0-9.]+)/)?.[1];
|
|
18902
|
+
if (db.isolationReport) result.engine.isolation = await db.isolationReport();
|
|
18903
|
+
await options.onIsolated?.();
|
|
18904
|
+
const me2 = await db.query(`select current_user::text as u, current_setting('is_superuser') as su`);
|
|
18905
|
+
if (me2.rows[0]?.su !== "on") {
|
|
18906
|
+
return allUnknown(`The PGlite session user ${me2.rows[0]?.u} is not a superuser, so migrations and state checks cannot run as intended.`, "harness problem");
|
|
17402
18907
|
}
|
|
17403
|
-
await
|
|
17404
|
-
|
|
18908
|
+
await db.exec(imitationSetupSql(matrix.defaultGrants));
|
|
18909
|
+
await db.exec("set search_path to public, extensions");
|
|
18910
|
+
const deadline = Date.now() + timeouts.migrationsMs;
|
|
18911
|
+
for (const file of migrationFiles) {
|
|
18912
|
+
try {
|
|
18913
|
+
const remaining = deadline - Date.now();
|
|
18914
|
+
if (remaining <= 0) throw new RlsTimeoutError(`timed out after ${Math.round(timeouts.migrationsMs / 1e3)} s`);
|
|
18915
|
+
await db.exec(file.sql, { timeoutMs: remaining });
|
|
18916
|
+
} catch (error) {
|
|
18917
|
+
if (error instanceof RlsTimeoutError) {
|
|
18918
|
+
result.migrations.failure = { file: file.name, message: error.message };
|
|
18919
|
+
return allUnknown(
|
|
18920
|
+
`Applying the migrations did not finish within ${Math.round(timeouts.migrationsMs / 1e3)} s (stopped at ${file.name}). PGlite ignores statement_timeout, so the worker was stopped and nothing was tested.`,
|
|
18921
|
+
`not run: migration ${file.name} timed out`
|
|
18922
|
+
);
|
|
18923
|
+
}
|
|
18924
|
+
if (!isSqlError(error)) throw error;
|
|
18925
|
+
const e2 = sqlError(error);
|
|
18926
|
+
result.migrations.failure = { file: file.name, code: e2.code, message: e2.message };
|
|
18927
|
+
return allUnknown(
|
|
18928
|
+
`Migration ${file.name} failed in the PGlite imitation (${e2.code}: ${e2.message}). The imitation cannot model something it needs, so nothing after it was tested.`,
|
|
18929
|
+
`not run: migration ${file.name} failed`
|
|
18930
|
+
);
|
|
18931
|
+
}
|
|
18932
|
+
}
|
|
18933
|
+
result.migrations.applied = true;
|
|
18934
|
+
await db.exec(`reset role; set search_path to public, extensions`);
|
|
18935
|
+
try {
|
|
18936
|
+
await db.exec(seedUsersSql());
|
|
18937
|
+
} catch (error) {
|
|
18938
|
+
if (!isSqlError(error)) throw error;
|
|
18939
|
+
const e2 = sqlError(error);
|
|
18940
|
+
return allUnknown(
|
|
18941
|
+
`Creating the two test users in auth.users failed (${e2.code}: ${e2.message}), probably in a trigger the migrations add. Nothing was tested.`,
|
|
18942
|
+
"not run: test users could not be created"
|
|
18943
|
+
);
|
|
18944
|
+
}
|
|
18945
|
+
const roles = await db.query(`select rolname::text as r, rolsuper as su, rolbypassrls as bypass from pg_roles where rolname in ('anon', 'authenticated') order by rolname`);
|
|
18946
|
+
for (const role of ["anon", "authenticated"]) {
|
|
18947
|
+
const row = roles.rows.find((r) => r.r === role);
|
|
18948
|
+
const ok = Boolean(row) && !row?.su && !row?.bypass;
|
|
18949
|
+
result.roleChecks.push({ role, ok, detail: row ? `superuser ${Boolean(row.su)}, BYPASSRLS ${Boolean(row.bypass)}` : "the role does not exist after the migrations" });
|
|
18950
|
+
}
|
|
18951
|
+
const badRoles = result.roleChecks.filter((c) => !c.ok);
|
|
18952
|
+
for (const table of matrix.tables) {
|
|
18953
|
+
result.tables.push(await runTable(db, table, badRoles, result.roleChecks));
|
|
18954
|
+
}
|
|
18955
|
+
return finish();
|
|
18956
|
+
} catch (error) {
|
|
18957
|
+
if (error instanceof RlsTimeoutError || !isSqlError(error)) {
|
|
18958
|
+
const message = firstLine(error instanceof Error ? error.message : String(error));
|
|
18959
|
+
return allUnknown(
|
|
18960
|
+
error instanceof RlsTimeoutError ? `A statement did not finish within ${Math.round(timeouts.statementMs / 1e3)} s, so the PGlite worker was stopped and no cell is reported as passed.` : `The PGlite worker stopped (${message}), so no cell is reported as passed.`,
|
|
18961
|
+
"not run: the database stopped"
|
|
18962
|
+
);
|
|
18963
|
+
}
|
|
18964
|
+
throw error;
|
|
18965
|
+
} finally {
|
|
18966
|
+
await db.close().catch(() => void 0);
|
|
17405
18967
|
}
|
|
17406
|
-
|
|
17407
|
-
|
|
18968
|
+
});
|
|
18969
|
+
}
|
|
18970
|
+
async function seedRow(db, shape, label, userId) {
|
|
18971
|
+
const owner = quoteIdent2(shape.owner.name);
|
|
18972
|
+
const pkText = shape.pk.map((c, i) => `${quoteIdent2(c.name)}::text as k${i}`).join(", ");
|
|
18973
|
+
const existing = await db.query(`select ${pkText} from ${shape.qualified} where ${owner}::text = $1 order by 1 limit 1`, [userId]);
|
|
18974
|
+
const keyOf2 = (row) => shape.pk.map((_3, i) => String(row[`k${i}`]));
|
|
18975
|
+
if (existing.rows.length === 1) return { key: keyOf2(existing.rows[0]), reused: true };
|
|
18976
|
+
const values = shape.rowValues(label, userId);
|
|
18977
|
+
const r = await db.query(
|
|
18978
|
+
`insert into ${shape.qualified} (${shape.insertColumns.map(quoteIdent2).join(", ")}) values (${values.map((_3, i) => `$${i + 1}`).join(", ")}) returning ${shape.pk.map((c, i) => `${quoteIdent2(c.name)}::text as k${i}`).join(", ")}`,
|
|
18979
|
+
values
|
|
18980
|
+
);
|
|
18981
|
+
if (r.rows.length !== 1) throw Object.assign(new Error("the insert returned no row (a trigger or rule may drop it)"), { code: "vr000" });
|
|
18982
|
+
return { key: keyOf2(r.rows[0]), reused: false };
|
|
18983
|
+
}
|
|
18984
|
+
async function runTable(db, table, badRoles, roleChecks) {
|
|
18985
|
+
const display = `${table.schema}.${table.name}`;
|
|
18986
|
+
const unknownTable = (reason, extra = {}) => ({ name: display, ...extra, unknownReason: reason, cells: unknownCells(table, reason, "not run") });
|
|
18987
|
+
if (badRoles.length > 0) {
|
|
18988
|
+
return unknownTable(`Role check failed (${badRoles.map((c) => `${c.role}: ${c.detail}`).join("; ")}), so RLS would not apply to the probes the way it does for the Data API.`);
|
|
18989
|
+
}
|
|
18990
|
+
const described = await describeTable(db, table);
|
|
18991
|
+
if (!described.shape) return unknownTable(described.reason ?? "unknown table problem");
|
|
18992
|
+
const shape = described.shape;
|
|
18993
|
+
const rel = await db.query(
|
|
18994
|
+
`select c.relrowsecurity as enabled, c.relforcerowsecurity as forced, pg_get_userbyid(c.relowner)::text as owner,
|
|
18995
|
+
pg_has_role('anon', c.relowner, 'USAGE') as anon_owner, pg_has_role('authenticated', c.relowner, 'USAGE') as auth_owner
|
|
18996
|
+
from pg_class c where c.oid = $1`,
|
|
18997
|
+
[shape.oid]
|
|
18998
|
+
);
|
|
18999
|
+
const info = rel.rows[0];
|
|
19000
|
+
const rls = { enabled: Boolean(info.enabled), forced: Boolean(info.forced), owner: String(info.owner) };
|
|
19001
|
+
for (const [role, isOwner] of [["anon", info.anon_owner], ["authenticated", info.auth_owner]]) {
|
|
19002
|
+
roleChecks.push({ role: `${role} on ${display}`, ok: !isOwner, detail: isOwner ? `${role} owns ${display} (owner ${rls.owner}), so RLS does not apply to it` : `not the owner (owner ${rls.owner})` });
|
|
19003
|
+
}
|
|
19004
|
+
if (info.anon_owner || info.auth_owner) {
|
|
19005
|
+
return unknownTable(`anon or authenticated owns ${display} (owner ${rls.owner}); table owners skip RLS, so the probes would prove nothing.`, { rls });
|
|
19006
|
+
}
|
|
19007
|
+
let ownerSeed;
|
|
19008
|
+
let otherSeed;
|
|
19009
|
+
try {
|
|
19010
|
+
ownerSeed = await seedRow(db, shape, "a", OWNER_ID);
|
|
19011
|
+
otherSeed = await seedRow(db, shape, "b", OTHER_USER_ID);
|
|
19012
|
+
} catch (error) {
|
|
19013
|
+
if (!isSqlError(error)) throw error;
|
|
19014
|
+
const e2 = sqlError(error);
|
|
19015
|
+
return unknownTable(`Could not seed ${display} (${e2.code}: ${e2.message}).${SEED_HINTS[e2.code] ? ` ${SEED_HINTS[e2.code]}` : ""}`, { rls });
|
|
19016
|
+
}
|
|
19017
|
+
const seeded = await measure(db, shape, ownerSeed.key);
|
|
19018
|
+
if (seeded.target === null || seeded.targetOwner !== OWNER_ID) {
|
|
19019
|
+
return unknownTable(`After seeding, the owner's row in ${display} is not owned by the test owner; a trigger or default may rewrite ${shape.owner.name}.`, { rls });
|
|
19020
|
+
}
|
|
19021
|
+
const privilegesByRole = /* @__PURE__ */ new Map();
|
|
19022
|
+
for (const role of ["anon", "authenticated"]) privilegesByRole.set(role, await privilegesFor(db, shape, role));
|
|
19023
|
+
const cells = [];
|
|
19024
|
+
for (const { actor, action, expected } of cellsFor(table)) {
|
|
19025
|
+
cells.push(await runCell({ db, shape, target: ownerSeed.key }, privilegesByRole, actor, action, expected));
|
|
19026
|
+
}
|
|
19027
|
+
cells.push(...serviceRoleCells(table));
|
|
19028
|
+
const columns = {};
|
|
19029
|
+
shape.insertColumns.forEach((c) => {
|
|
19030
|
+
columns[c] = c === shape.owner.name ? "the test user id" : "generated test value";
|
|
19031
|
+
});
|
|
19032
|
+
for (const c of Object.keys(table.seed)) columns[c] = "from the matrix seed";
|
|
19033
|
+
return {
|
|
19034
|
+
name: display,
|
|
19035
|
+
rls,
|
|
19036
|
+
seed: { rowsBefore: seeded.count, ownerRow: ownerSeed.key.join(","), otherUserRow: otherSeed.key.join(","), reused: { owner: ownerSeed.reused, otherUser: otherSeed.reused }, columns },
|
|
19037
|
+
cells
|
|
19038
|
+
};
|
|
19039
|
+
}
|
|
19040
|
+
var import_node_child_process6, import_node_crypto2, import_promises26, import_node_net, import_node_path32, import_node_util4, NON_AUTHORITATIVE_LABEL, PGLITE_INSTALL_HINT, ENV_ALLOWLIST, PERMISSION_DENIED, RLS_REFUSAL, DEFAULT_TIMEOUTS, RlsInputError, normalise, quoteIdent2, firstLine, plural2, NUMBERS, ACTOR_SESSION, changedRows, isRlsRefusal, isPrivilegeRefusal, SELF, PROBE_EXCLUSIONS, SEED_HINTS;
|
|
19041
|
+
var init_rlsRunner = __esm({
|
|
19042
|
+
"src/rls/rlsRunner.ts"() {
|
|
19043
|
+
"use strict";
|
|
19044
|
+
import_node_child_process6 = require("node:child_process");
|
|
19045
|
+
import_node_crypto2 = require("node:crypto");
|
|
19046
|
+
import_promises26 = require("node:fs/promises");
|
|
19047
|
+
import_node_net = __toESM(require("node:net"));
|
|
19048
|
+
import_node_path32 = require("node:path");
|
|
19049
|
+
import_node_util4 = require("node:util");
|
|
19050
|
+
init_safeGit();
|
|
19051
|
+
init_permissionsMatrix();
|
|
19052
|
+
init_supabaseImitation();
|
|
19053
|
+
init_workerDatabase();
|
|
19054
|
+
NON_AUTHORITATIVE_LABEL = "PGlite imitation of Supabase, non-authoritative. Scenario passed locally does not prove production.";
|
|
19055
|
+
PGLITE_INSTALL_HINT = "PGlite (@electric-sql/pglite) is not installed next to VibeRaven, so nothing was run. It is optional (about 25 MB). Run: npx -y -p viberaven -p @electric-sql/pglite@0.5.8 viberaven rls-test, or add both to the project: npm i -D viberaven @electric-sql/pglite@0.5.8";
|
|
19056
|
+
ENV_ALLOWLIST = ["PATH", "Path", "SystemRoot", "SYSTEMROOT", "windir", "TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE", "LANG", "LC_ALL", "TZ"];
|
|
19057
|
+
PERMISSION_DENIED = "42501";
|
|
19058
|
+
RLS_REFUSAL = /row-level security/i;
|
|
19059
|
+
DEFAULT_TIMEOUTS = { migrationsMs: 12e4, statementMs: 3e4 };
|
|
19060
|
+
RlsInputError = class extends Error {
|
|
19061
|
+
name = "RlsInputError";
|
|
19062
|
+
};
|
|
19063
|
+
normalise = (text) => text.replace(/\r\n/g, "\n");
|
|
19064
|
+
quoteIdent2 = (name) => `"${name.replace(/"/g, '""')}"`;
|
|
19065
|
+
firstLine = (message) => message.split("\n")[0].trim();
|
|
19066
|
+
plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
19067
|
+
NUMBERS = { a: ["1001"], b: ["1002"], i: ["1003"], u: ["1004", "7", "1"] };
|
|
19068
|
+
ACTOR_SESSION = {
|
|
19069
|
+
owner: { role: "authenticated", uid: OWNER_ID, claims: { sub: OWNER_ID, role: "authenticated", aud: "authenticated", email: "owner@example.test" } },
|
|
19070
|
+
other_user: { role: "authenticated", uid: OTHER_USER_ID, claims: { sub: OTHER_USER_ID, role: "authenticated", aud: "authenticated", email: "other-user@example.test" } },
|
|
19071
|
+
anon: { role: "anon", uid: null, claims: { role: "anon" } }
|
|
19072
|
+
};
|
|
19073
|
+
changedRows = (n) => `${plural2(n ?? 0, "row")} changed`;
|
|
19074
|
+
isRlsRefusal = (run) => run.error?.code === PERMISSION_DENIED && RLS_REFUSAL.test(run.error.message);
|
|
19075
|
+
isPrivilegeRefusal = (run) => run.error?.code === PERMISSION_DENIED && !RLS_REFUSAL.test(run.error.message);
|
|
19076
|
+
SELF = Symbol("self");
|
|
19077
|
+
PROBE_EXCLUSIONS = [
|
|
19078
|
+
"Policies that look up other tables (teams, organisations, memberships) are only exercised with the rows the migrations create: no membership rows are seeded.",
|
|
19079
|
+
"WITH CHECK expressions are tried with the generated values only; when every value tried is refused, the cell is Unknown, not a pass.",
|
|
19080
|
+
"When a role can use some columns but cannot read the primary key, probes run without a row filter and are judged by the owner row alone.",
|
|
19081
|
+
"When the primary key is readable, writes are filtered by it; unfiltered UPDATE and DELETE statements are not also tested.",
|
|
19082
|
+
"One row per test user is used; a row the migrations create for the user (an auth.users trigger) is reused.",
|
|
19083
|
+
"Side effects of triggers on other tables are not checked."
|
|
19084
|
+
];
|
|
19085
|
+
SEED_HINTS = {
|
|
19086
|
+
"23502": 'A NOT NULL column has no value: give it one under "seed" in the matrix.',
|
|
19087
|
+
"23503": 'A foreign key points at a row that does not exist: give the column a value under "seed" in the matrix.',
|
|
19088
|
+
"23505": 'A unique constraint clashed with the test row: give the unique column a distinct value under "seed", or check whether a trigger already creates the row.',
|
|
19089
|
+
"23514": 'A CHECK constraint refused the generated values: give the column a valid value under "seed" in the matrix.'
|
|
19090
|
+
};
|
|
19091
|
+
}
|
|
19092
|
+
});
|
|
19093
|
+
|
|
19094
|
+
// src/rls/render.ts
|
|
19095
|
+
function renderRlsText(result) {
|
|
19096
|
+
const lines = [];
|
|
19097
|
+
lines.push(`viberaven rls-test (experimental) \xB7 ${result.project}`);
|
|
19098
|
+
lines.push(NON_AUTHORITATIVE_LABEL);
|
|
19099
|
+
lines.push("");
|
|
19100
|
+
const commit = result.commit ? `${result.commit.sha.slice(0, 12)}${result.commit.dirty === true ? " (migrations or matrix changed since this commit)" : result.commit.dirty === false ? " (migrations and matrix match this commit)" : ""}` : "not a git repository";
|
|
19101
|
+
lines.push(`Commit ${commit}`);
|
|
19102
|
+
const files = result.migrations.files.map((f) => f.name);
|
|
19103
|
+
lines.push(
|
|
19104
|
+
`Migrations ${result.migrations.dir}: ${files.length} file${files.length === 1 ? "" : "s"}${files.length > 0 ? ` (${files.slice(0, 4).join(", ")}${files.length > 4 ? ", ..." : ""})` : ""}, sha256 ${result.migrations.hash.slice(0, 16)}, ${result.migrations.applied ? "applied" : result.migrations.failure ? `failed at ${result.migrations.failure.file}` : "not applied"}`
|
|
19105
|
+
);
|
|
19106
|
+
lines.push(`Matrix ${result.matrix.path}, sha256 ${result.matrix.hash.slice(0, 16)}, defaultGrants ${result.matrix.defaultGrants}`);
|
|
19107
|
+
lines.push(
|
|
19108
|
+
`Engine ${result.engine.available ? `PGlite ${result.engine.version ?? "?"} (${result.engine.postgres ?? "PostgreSQL ?"}), in memory, network denied` : "PGlite not available"}`
|
|
19109
|
+
);
|
|
19110
|
+
if (result.roleChecks.length > 0) {
|
|
19111
|
+
const bad = result.roleChecks.filter((c) => !c.ok);
|
|
19112
|
+
lines.push(
|
|
19113
|
+
`Roles ${bad.length === 0 ? "anon and authenticated: not superuser, not BYPASSRLS, not the table owner" : bad.map((c) => `${c.role}: ${c.detail}`).join("; ")}`
|
|
19114
|
+
);
|
|
19115
|
+
}
|
|
19116
|
+
if (!result.engine.available) {
|
|
19117
|
+
lines.push("");
|
|
19118
|
+
if (result.engine.error) lines.push(`(${result.engine.error})`);
|
|
19119
|
+
}
|
|
19120
|
+
for (const table of result.tables) {
|
|
19121
|
+
lines.push("");
|
|
19122
|
+
const rls = table.rls ? `RLS ${table.rls.enabled ? "on" : "off"}${table.rls.forced ? ", forced" : ""}, owner ${table.rls.owner}` : "not inspected";
|
|
19123
|
+
lines.push(`${table.name} (${rls})`);
|
|
19124
|
+
if (table.seed) {
|
|
19125
|
+
const reused = [table.seed.reused.owner ? "the owner" : "", table.seed.reused.otherUser ? "the other user" : ""].filter(Boolean);
|
|
19126
|
+
lines.push(
|
|
19127
|
+
` One row for the owner and one for another user (${table.seed.rowsBefore} rows in the table)${reused.length > 0 ? `; reused the rows the migrations created for ${reused.join(" and ")}` : ""}.`
|
|
19128
|
+
);
|
|
17408
19129
|
}
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
|
|
17413
|
-
|
|
19130
|
+
if (table.unknownReason) lines.push(` Unknown: ${table.unknownReason}`);
|
|
19131
|
+
const tested = table.cells.filter((c) => c.actor !== "service_role");
|
|
19132
|
+
const actorWidth = 14;
|
|
19133
|
+
const actionWidth = 10;
|
|
19134
|
+
lines.push(` ${"actor".padEnd(actorWidth)}${"action".padEnd(actionWidth)}${"expected".padEnd(10)}${"result".padEnd(9)}observed`);
|
|
19135
|
+
for (const cell of tested) {
|
|
19136
|
+
lines.push(` ${cell.actor.padEnd(actorWidth)}${cell.action.padEnd(actionWidth)}${cell.expected.padEnd(10)}${cell.outcome.padEnd(9)}${cell.observed.summary}`);
|
|
17414
19137
|
}
|
|
19138
|
+
const service = table.cells.filter((c) => c.actor === "service_role");
|
|
19139
|
+
if (service.length > 0) lines.push(` ${"service_role".padEnd(actorWidth)}${"all".padEnd(actionWidth)}${"excluded".padEnd(10)}${"N/A".padEnd(9)}bypasses RLS by design, not tested`);
|
|
17415
19140
|
}
|
|
19141
|
+
const s = result.summary;
|
|
19142
|
+
lines.push("");
|
|
19143
|
+
lines.push(`Result: ${s.pass} Pass, ${s.fail} Fail, ${s.unknown} Unknown, ${s.notApplicable} N/A \xB7 ${s.pass + s.fail} of ${s.tested} tested cells observed (${s.total} cells in the matrix)`);
|
|
19144
|
+
lines.push(result.verdict);
|
|
19145
|
+
const failed = result.tables.flatMap((t) => t.cells).filter((c) => c.outcome === "Fail");
|
|
19146
|
+
for (const cell of failed) {
|
|
19147
|
+
lines.push(` Fail ${cell.actor} ${cell.action} on ${cell.table}: ${cell.reason}`);
|
|
19148
|
+
}
|
|
19149
|
+
const unknown = result.tables.flatMap((t) => t.cells).filter((c) => c.outcome === "Unknown");
|
|
19150
|
+
const unknownReasons = [...new Set(unknown.map((c) => c.reason))];
|
|
19151
|
+
for (const reason of unknownReasons.slice(0, 5)) lines.push(` Unknown ${reason}`);
|
|
19152
|
+
if (unknownReasons.length > 5) lines.push(` Unknown and ${unknownReasons.length - 5} more reasons (see --json)`);
|
|
19153
|
+
lines.push("");
|
|
19154
|
+
lines.push("Assumptions:");
|
|
19155
|
+
for (const a of result.assumptions) lines.push(` - ${a}`);
|
|
19156
|
+
lines.push("Not tested:");
|
|
19157
|
+
for (const e2 of result.exclusions) lines.push(` - ${e2}`);
|
|
19158
|
+
lines.push("");
|
|
19159
|
+
lines.push(NON_AUTHORITATIVE_LABEL);
|
|
19160
|
+
return lines.join("\n");
|
|
17416
19161
|
}
|
|
17417
|
-
function
|
|
17418
|
-
return
|
|
19162
|
+
function renderRlsJson(result) {
|
|
19163
|
+
return `${JSON.stringify(result, null, 2)}
|
|
19164
|
+
`;
|
|
17419
19165
|
}
|
|
17420
|
-
var
|
|
17421
|
-
|
|
17422
|
-
"src/commands/cleanPlan.ts"() {
|
|
19166
|
+
var init_render2 = __esm({
|
|
19167
|
+
"src/rls/render.ts"() {
|
|
17423
19168
|
"use strict";
|
|
17424
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
|
|
19169
|
+
init_rlsRunner();
|
|
19170
|
+
}
|
|
19171
|
+
});
|
|
19172
|
+
|
|
19173
|
+
// src/commands/rlsTest.ts
|
|
19174
|
+
async function runRlsTestCommand(input) {
|
|
19175
|
+
const write = input.write ?? ((text) => process.stdout.write(text));
|
|
19176
|
+
const writeError = input.writeError ?? ((text) => process.stderr.write(text));
|
|
19177
|
+
try {
|
|
19178
|
+
const result = await (input.run ?? runRlsTest)({ projectDir: input.cwd, matrixPath: input.matrix, migrationsDir: input.migrations });
|
|
19179
|
+
write(input.json ? renderRlsJson(result) : `${renderRlsText(result)}
|
|
19180
|
+
`);
|
|
19181
|
+
return result.exitCode;
|
|
19182
|
+
} catch (error) {
|
|
19183
|
+
const expected = error instanceof MatrixError || error instanceof RlsInputError;
|
|
19184
|
+
const message = expected ? error.message : `rls-test stopped on an unexpected error, so nothing is reported as passed: ${error instanceof Error ? error.message : String(error)}`;
|
|
19185
|
+
if (input.json) {
|
|
19186
|
+
write(
|
|
19187
|
+
`${JSON.stringify(
|
|
19188
|
+
{ label: NON_AUTHORITATIVE_LABEL, experimental: true, error: message, ...error instanceof MatrixError ? { problems: error.problems } : {}, exitCode: 2 },
|
|
19189
|
+
null,
|
|
19190
|
+
2
|
|
19191
|
+
)}
|
|
19192
|
+
`
|
|
19193
|
+
);
|
|
19194
|
+
} else {
|
|
19195
|
+
writeError(`viberaven rls-test (experimental)
|
|
19196
|
+
${NON_AUTHORITATIVE_LABEL}
|
|
19197
|
+
|
|
19198
|
+
${message}
|
|
19199
|
+
`);
|
|
19200
|
+
}
|
|
19201
|
+
return 2;
|
|
19202
|
+
}
|
|
19203
|
+
}
|
|
19204
|
+
var init_rlsTest = __esm({
|
|
19205
|
+
"src/commands/rlsTest.ts"() {
|
|
19206
|
+
"use strict";
|
|
19207
|
+
init_permissionsMatrix();
|
|
19208
|
+
init_render2();
|
|
19209
|
+
init_rlsRunner();
|
|
17429
19210
|
}
|
|
17430
19211
|
});
|
|
17431
19212
|
|
|
@@ -18049,15 +19830,17 @@ function buildConnectProviders(providers) {
|
|
|
18049
19830
|
}));
|
|
18050
19831
|
}
|
|
18051
19832
|
function buildMissionCards(artifact, providers) {
|
|
18052
|
-
const blockers = artifact.gaps.
|
|
19833
|
+
const blockers = artifact.gaps.map((gap) => ({
|
|
18053
19834
|
id: gap.id,
|
|
18054
19835
|
title: gap.title,
|
|
18055
19836
|
detail: gap.detail || void 0,
|
|
18056
19837
|
severity: gap.severity,
|
|
18057
19838
|
category: blockerCategoryLabel(gap),
|
|
18058
|
-
tone: blockerTone(gap.severity)
|
|
19839
|
+
tone: blockerTone(gap.severity),
|
|
19840
|
+
prompt: gap.copyPrompt || void 0,
|
|
19841
|
+
requiresUserAction: gapRequiresUserAction(gap)
|
|
18059
19842
|
}));
|
|
18060
|
-
const planSteps = artifact.gaps.
|
|
19843
|
+
const planSteps = artifact.gaps.map((gap) => ({
|
|
18061
19844
|
id: gap.id,
|
|
18062
19845
|
title: gap.title,
|
|
18063
19846
|
category: planStepCategoryLabel(gap),
|
|
@@ -18076,7 +19859,7 @@ function buildMissionCards(artifact, providers) {
|
|
|
18076
19859
|
};
|
|
18077
19860
|
}
|
|
18078
19861
|
function projectNameFromPath(pathValue) {
|
|
18079
|
-
return (0,
|
|
19862
|
+
return (0, import_node_path33.basename)(pathValue.replace(/[\\/]$/, "")) || "current repo";
|
|
18080
19863
|
}
|
|
18081
19864
|
function gapSearchText(gap) {
|
|
18082
19865
|
return [
|
|
@@ -18495,19 +20278,19 @@ function mapArtifactToLocalUiState(artifact, presentProviderIds) {
|
|
|
18495
20278
|
selectedProviderId: selectedProvider.id,
|
|
18496
20279
|
currentPrompt: selectedProvider.nextFix?.prompt ?? "",
|
|
18497
20280
|
artifacts: {
|
|
18498
|
-
tasklist: (0,
|
|
18499
|
-
contextMap: (0,
|
|
18500
|
-
gateResult: (0,
|
|
18501
|
-
lastScan: (0,
|
|
20281
|
+
tasklist: (0, import_node_path33.join)(".viberaven", "agent-tasklist.md"),
|
|
20282
|
+
contextMap: (0, import_node_path33.join)(".viberaven", "context-map.json"),
|
|
20283
|
+
gateResult: (0, import_node_path33.join)(".viberaven", "gate-result.json"),
|
|
20284
|
+
lastScan: (0, import_node_path33.join)(".viberaven", "last-scan.json")
|
|
18502
20285
|
},
|
|
18503
20286
|
empty: false
|
|
18504
20287
|
};
|
|
18505
20288
|
}
|
|
18506
|
-
var
|
|
20289
|
+
var import_node_path33, CONNECT_PROVIDER_PRIORITY, providerDashboardUrls, providerHints, PROVIDER_OWNED_GAP;
|
|
18507
20290
|
var init_artifactAdapter = __esm({
|
|
18508
20291
|
"src/local-ui/artifactAdapter.ts"() {
|
|
18509
20292
|
"use strict";
|
|
18510
|
-
|
|
20293
|
+
import_node_path33 = require("node:path");
|
|
18511
20294
|
init_providerLogos();
|
|
18512
20295
|
init_prompts();
|
|
18513
20296
|
init_providerLaunchPaths();
|
|
@@ -18549,19 +20332,20 @@ function escapeJsonForHtml(value) {
|
|
|
18549
20332
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
18550
20333
|
}
|
|
18551
20334
|
function renderStackHtml(state) {
|
|
18552
|
-
const title =
|
|
20335
|
+
const title = `VibeRaven | ${state.project?.name ? `Studio for ${state.project.name}` : "Studio"}`;
|
|
18553
20336
|
return `<!doctype html>
|
|
18554
20337
|
<html lang="en">
|
|
18555
20338
|
<head>
|
|
18556
20339
|
<meta charset="utf-8" />
|
|
18557
20340
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18558
|
-
<meta name="theme-color" content="#
|
|
20341
|
+
<meta name="theme-color" content="#0a0a0b" />
|
|
18559
20342
|
<meta name="color-scheme" content="dark" />
|
|
18560
20343
|
<title>${title.replace(/</g, "<")}</title>
|
|
18561
20344
|
<link rel="icon" type="image/png" href="/manga/favicon.png?v=stack1" />
|
|
18562
20345
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
18563
20346
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
18564
|
-
|
|
20347
|
+
<!-- Same faces as viberaven.dev. Offline, the Studio falls back to the system sans and mono. -->
|
|
20348
|
+
<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400..700&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
|
18565
20349
|
<link rel="stylesheet" href="/stack/app.css" />
|
|
18566
20350
|
</head>
|
|
18567
20351
|
<body>
|
|
@@ -18808,27 +20592,39 @@ var init_planLimits = __esm({
|
|
|
18808
20592
|
// src/hosted/credentials.ts
|
|
18809
20593
|
function defaultConfigDir(env = process.env, platform = process.platform) {
|
|
18810
20594
|
const override = env.VIBERAVEN_CONFIG_DIR?.trim();
|
|
18811
|
-
if (override) return (0,
|
|
20595
|
+
if (override) return (0, import_node_path34.resolve)(override);
|
|
18812
20596
|
if (platform === "win32") {
|
|
18813
|
-
return (0,
|
|
20597
|
+
return (0, import_node_path34.join)(env.APPDATA?.trim() || (0, import_node_path34.join)((0, import_node_os.homedir)(), "AppData", "Roaming"), "viberaven");
|
|
18814
20598
|
}
|
|
18815
20599
|
if (platform === "darwin") {
|
|
18816
|
-
return (0,
|
|
20600
|
+
return (0, import_node_path34.join)((0, import_node_os.homedir)(), "Library", "Application Support", "viberaven");
|
|
18817
20601
|
}
|
|
18818
|
-
return (0,
|
|
20602
|
+
return (0, import_node_path34.join)(env.XDG_CONFIG_HOME?.trim() || (0, import_node_path34.join)((0, import_node_os.homedir)(), ".config"), "viberaven");
|
|
18819
20603
|
}
|
|
18820
20604
|
function isInside(child, parent) {
|
|
18821
|
-
const rel = (0,
|
|
18822
|
-
return rel === "" || !rel.startsWith("..") && !(0,
|
|
20605
|
+
const rel = (0, import_node_path34.relative)((0, import_node_path34.resolve)(parent), (0, import_node_path34.resolve)(child));
|
|
20606
|
+
return rel === "" || !rel.startsWith("..") && !(0, import_node_path34.isAbsolute)(rel);
|
|
20607
|
+
}
|
|
20608
|
+
async function retryTransient(work) {
|
|
20609
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
20610
|
+
try {
|
|
20611
|
+
return await work();
|
|
20612
|
+
} catch (error) {
|
|
20613
|
+
const code = error?.code;
|
|
20614
|
+
if (attempt >= FS_ATTEMPTS || !code || !TRANSIENT_FS_CODES.has(code)) throw error;
|
|
20615
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, FS_BACKOFF_MS * attempt));
|
|
20616
|
+
}
|
|
20617
|
+
}
|
|
18823
20618
|
}
|
|
18824
|
-
var
|
|
20619
|
+
var import_node_child_process7, import_node_crypto3, import_promises27, import_node_os, import_node_path34, CREDENTIALS_FILE_NAME, CredentialLocationError, CredentialPermissionError, restrictWithIcacls, TRANSIENT_FS_CODES, FS_ATTEMPTS, FS_BACKOFF_MS, CredentialStore;
|
|
18825
20620
|
var init_credentials = __esm({
|
|
18826
20621
|
"src/hosted/credentials.ts"() {
|
|
18827
20622
|
"use strict";
|
|
18828
|
-
|
|
18829
|
-
|
|
20623
|
+
import_node_child_process7 = require("node:child_process");
|
|
20624
|
+
import_node_crypto3 = require("node:crypto");
|
|
20625
|
+
import_promises27 = require("node:fs/promises");
|
|
18830
20626
|
import_node_os = require("node:os");
|
|
18831
|
-
|
|
20627
|
+
import_node_path34 = require("node:path");
|
|
18832
20628
|
CREDENTIALS_FILE_NAME = "credentials.json";
|
|
18833
20629
|
CredentialLocationError = class extends Error {
|
|
18834
20630
|
};
|
|
@@ -18842,25 +20638,32 @@ var init_credentials = __esm({
|
|
|
18842
20638
|
const name = (0, import_node_os.userInfo)().username;
|
|
18843
20639
|
const user = process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${name}` : name;
|
|
18844
20640
|
return new Promise((resolveRestrict, reject) => {
|
|
18845
|
-
(0,
|
|
20641
|
+
(0, import_node_child_process7.execFile)("icacls", [path, "/inheritance:r", "/grant:r", `${user}:F`], { windowsHide: true, timeout: 1e4 }, (error) => {
|
|
18846
20642
|
if (error) reject(error);
|
|
18847
20643
|
else resolveRestrict();
|
|
18848
20644
|
});
|
|
18849
20645
|
});
|
|
18850
20646
|
};
|
|
20647
|
+
TRANSIENT_FS_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
20648
|
+
FS_ATTEMPTS = 8;
|
|
20649
|
+
FS_BACKOFF_MS = 10;
|
|
18851
20650
|
CredentialStore = class {
|
|
18852
20651
|
constructor(configDir, workspacePath, options = {}) {
|
|
18853
20652
|
this.configDir = configDir;
|
|
18854
20653
|
this.workspacePath = workspacePath;
|
|
18855
|
-
this.path = (0,
|
|
20654
|
+
this.path = (0, import_node_path34.join)(configDir, CREDENTIALS_FILE_NAME);
|
|
18856
20655
|
this.restrict = options.restrict !== void 0 ? options.restrict : process.platform === "win32" ? restrictWithIcacls : null;
|
|
20656
|
+
this.fs = { readFile: (path, encoding) => (0, import_promises27.readFile)(path, encoding), rename: (from, to) => (0, import_promises27.rename)(from, to), rm: (path) => (0, import_promises27.rm)(path, { force: true }), ...options.fs };
|
|
18857
20657
|
}
|
|
18858
20658
|
path;
|
|
18859
20659
|
restrict;
|
|
20660
|
+
fs;
|
|
20661
|
+
/** Every change to the file runs one at a time, in the order it was asked for. */
|
|
20662
|
+
queue = Promise.resolve();
|
|
18860
20663
|
async read() {
|
|
18861
20664
|
let raw;
|
|
18862
20665
|
try {
|
|
18863
|
-
raw = await (
|
|
20666
|
+
raw = await retryTransient(() => this.fs.readFile(this.path, "utf8"));
|
|
18864
20667
|
} catch {
|
|
18865
20668
|
return null;
|
|
18866
20669
|
}
|
|
@@ -18878,22 +20681,36 @@ var init_credentials = __esm({
|
|
|
18878
20681
|
const stored = await this.read();
|
|
18879
20682
|
return stored && stored.apiBaseUrl === apiBaseUrl ? stored : null;
|
|
18880
20683
|
}
|
|
18881
|
-
|
|
20684
|
+
/** Resolves once every change queued so far has landed (or failed). */
|
|
20685
|
+
idle() {
|
|
20686
|
+
return this.queue;
|
|
20687
|
+
}
|
|
20688
|
+
serial(work) {
|
|
20689
|
+
const run = this.queue.then(work, work);
|
|
20690
|
+
this.queue = run.then(() => void 0, () => void 0);
|
|
20691
|
+
return run;
|
|
20692
|
+
}
|
|
20693
|
+
write(credentials) {
|
|
20694
|
+
return this.serial(async () => {
|
|
20695
|
+
await this.writeNow({ version: 1, ...credentials, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
20696
|
+
});
|
|
20697
|
+
}
|
|
20698
|
+
/** Must run inside `serial`. Returns false when `guard` stopped the swap and the file was left alone. */
|
|
20699
|
+
async writeNow(body, guard) {
|
|
18882
20700
|
if (isInside(this.configDir, this.workspacePath)) {
|
|
18883
20701
|
throw new CredentialLocationError(
|
|
18884
20702
|
`Refusing to store the VibeRaven sign-in inside this project (${this.configDir}). Set VIBERAVEN_CONFIG_DIR to a folder outside the repo.`
|
|
18885
20703
|
);
|
|
18886
20704
|
}
|
|
18887
|
-
await (0,
|
|
18888
|
-
const
|
|
18889
|
-
|
|
18890
|
-
await (0, import_promises25.writeFile)(temp, `${JSON.stringify(body, null, 2)}
|
|
20705
|
+
await (0, import_promises27.mkdir)(this.configDir, { recursive: true, mode: 448 });
|
|
20706
|
+
const temp = `${this.path}.${(0, import_node_crypto3.randomUUID)()}.tmp`;
|
|
20707
|
+
await (0, import_promises27.writeFile)(temp, `${JSON.stringify(body, null, 2)}
|
|
18891
20708
|
`, { encoding: "utf8", mode: 384 });
|
|
18892
|
-
await (0,
|
|
20709
|
+
await (0, import_promises27.chmod)(temp, 384).catch(() => void 0);
|
|
18893
20710
|
try {
|
|
18894
20711
|
if (this.restrict) await this.restrict(temp);
|
|
18895
20712
|
} catch (error) {
|
|
18896
|
-
await (0,
|
|
20713
|
+
await (0, import_promises27.rm)(temp, { force: true });
|
|
18897
20714
|
const code = error?.code;
|
|
18898
20715
|
const toolMissing = code === "ENOENT";
|
|
18899
20716
|
throw new CredentialPermissionError(
|
|
@@ -18901,22 +20718,53 @@ var init_credentials = __esm({
|
|
|
18901
20718
|
toolMissing
|
|
18902
20719
|
);
|
|
18903
20720
|
}
|
|
20721
|
+
if (guard) {
|
|
20722
|
+
const onDisk = await this.readFor(guard.apiBaseUrl);
|
|
20723
|
+
if (onDisk?.accessToken !== guard.accessToken || guard.isCurrent && !guard.isCurrent()) {
|
|
20724
|
+
await (0, import_promises27.rm)(temp, { force: true });
|
|
20725
|
+
return false;
|
|
20726
|
+
}
|
|
20727
|
+
}
|
|
18904
20728
|
try {
|
|
18905
|
-
await (
|
|
20729
|
+
await retryTransient(() => this.fs.rename(temp, this.path));
|
|
18906
20730
|
} catch (error) {
|
|
18907
|
-
await (0,
|
|
20731
|
+
await (0, import_promises27.rm)(temp, { force: true });
|
|
18908
20732
|
throw error;
|
|
18909
20733
|
}
|
|
18910
|
-
await (0,
|
|
20734
|
+
await (0, import_promises27.chmod)(this.path, 384).catch(() => void 0);
|
|
20735
|
+
return true;
|
|
18911
20736
|
}
|
|
18912
20737
|
/** Keeps the stored plan label in step with what the API last said. */
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
20738
|
+
updatePlan(apiBaseUrl, plan) {
|
|
20739
|
+
return this.serial(async () => {
|
|
20740
|
+
const stored = await this.readFor(apiBaseUrl);
|
|
20741
|
+
if (!stored || stored.account.plan === plan) return;
|
|
20742
|
+
await this.writeNow(
|
|
20743
|
+
{ version: 1, apiBaseUrl: stored.apiBaseUrl, accessToken: stored.accessToken, account: { ...stored.account, plan }, savedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
20744
|
+
{ apiBaseUrl: stored.apiBaseUrl, accessToken: stored.accessToken }
|
|
20745
|
+
);
|
|
20746
|
+
});
|
|
18917
20747
|
}
|
|
18918
|
-
|
|
18919
|
-
|
|
20748
|
+
/**
|
|
20749
|
+
* Replaces the saved account label with what the API holds now. Only touches the file while it
|
|
20750
|
+
* still holds the sign-in `accessToken` belongs to and `isCurrent()` still says yes, so a
|
|
20751
|
+
* sign-out or a new sign-in made while the API was answering is never overwritten. `savedAt`
|
|
20752
|
+
* is the sign-in time and is kept.
|
|
20753
|
+
*/
|
|
20754
|
+
updateAccount(apiBaseUrl, accessToken, account, isCurrent) {
|
|
20755
|
+
return this.serial(async () => {
|
|
20756
|
+
const stored = await this.readFor(apiBaseUrl);
|
|
20757
|
+
if (!stored || stored.accessToken !== accessToken) return;
|
|
20758
|
+
if (stored.account.email === account.email && stored.account.plan === account.plan) return;
|
|
20759
|
+
if (isCurrent && !isCurrent()) return;
|
|
20760
|
+
await this.writeNow(
|
|
20761
|
+
{ version: 1, apiBaseUrl: stored.apiBaseUrl, accessToken, account: { ...stored.account, ...account }, savedAt: stored.savedAt },
|
|
20762
|
+
{ apiBaseUrl: stored.apiBaseUrl, accessToken, isCurrent }
|
|
20763
|
+
);
|
|
20764
|
+
});
|
|
20765
|
+
}
|
|
20766
|
+
clear() {
|
|
20767
|
+
return this.serial(() => retryTransient(() => this.fs.rm(this.path)));
|
|
18920
20768
|
}
|
|
18921
20769
|
};
|
|
18922
20770
|
}
|
|
@@ -18942,17 +20790,22 @@ function assertHttpBaseUrl(baseUrl) {
|
|
|
18942
20790
|
}
|
|
18943
20791
|
}
|
|
18944
20792
|
async function postJson(url, payload, options) {
|
|
20793
|
+
return requestJson(url, { ...options, method: "POST", body: JSON.stringify(payload === void 0 ? {} : payload) });
|
|
20794
|
+
}
|
|
20795
|
+
async function getJson(url, options) {
|
|
20796
|
+
return requestJson(url, { ...options, method: "GET" });
|
|
20797
|
+
}
|
|
20798
|
+
async function requestJson(url, options) {
|
|
18945
20799
|
let response;
|
|
18946
20800
|
try {
|
|
18947
20801
|
response = await fetch(url, {
|
|
18948
|
-
method:
|
|
20802
|
+
method: options.method,
|
|
18949
20803
|
headers: {
|
|
18950
|
-
"Content-Type": "application/json",
|
|
20804
|
+
...options.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
18951
20805
|
...options.headers ?? {},
|
|
18952
20806
|
...options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}
|
|
18953
20807
|
},
|
|
18954
|
-
|
|
18955
|
-
body: JSON.stringify(payload === void 0 ? {} : payload),
|
|
20808
|
+
body: options.body,
|
|
18956
20809
|
// Never follow a redirect with the sign-in token and the project attached.
|
|
18957
20810
|
redirect: "error",
|
|
18958
20811
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
@@ -19029,6 +20882,13 @@ async function runHostedCheck(baseUrl, accessToken, payload, timeoutMs, idempote
|
|
|
19029
20882
|
}
|
|
19030
20883
|
return response;
|
|
19031
20884
|
}
|
|
20885
|
+
async function fetchAccount(baseUrl, accessToken, timeoutMs) {
|
|
20886
|
+
const payload = await getJson(`${normalizeBaseUrl(baseUrl)}/v1/account/me`, { accessToken, timeoutMs, label: "Account check" });
|
|
20887
|
+
if (!isAccount(payload)) {
|
|
20888
|
+
throw new HostedCallError("invalid_response", "Account response did not match the expected shape.");
|
|
20889
|
+
}
|
|
20890
|
+
return { email: payload.email, plan: payload.plan, usage: payload.usage };
|
|
20891
|
+
}
|
|
19032
20892
|
function isRecord4(value) {
|
|
19033
20893
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19034
20894
|
}
|
|
@@ -19053,6 +20913,9 @@ function isUsage(value) {
|
|
|
19053
20913
|
function isStationResponse(value) {
|
|
19054
20914
|
return isRecord4(value) && (value.status === "stable" || value.status === "drifting" || value.status === "chaos") && isNonEmptyString(value.reason) && isNonEmptyString(value.impact) && (value.confidence === "low" || value.confidence === "medium" || value.confidence === "high") && typeof value.output === "string" && isUsage(value.usage);
|
|
19055
20915
|
}
|
|
20916
|
+
function isAccount(value) {
|
|
20917
|
+
return isRecord4(value) && isNonEmptyString(value.email) && (value.plan === "free" || value.plan === "pro") && isUsage(value.usage);
|
|
20918
|
+
}
|
|
19056
20919
|
var DEFAULT_API_BASE_URL, HostedCallError;
|
|
19057
20920
|
var init_hostedClient = __esm({
|
|
19058
20921
|
"src/hosted/hostedClient.ts"() {
|
|
@@ -19237,7 +21100,7 @@ async function buildHostedCheckPayload(workspacePath) {
|
|
|
19237
21100
|
throw new Error("Could not assemble the full check request.");
|
|
19238
21101
|
}
|
|
19239
21102
|
function finalizePayload(payload) {
|
|
19240
|
-
const folder = (0,
|
|
21103
|
+
const folder = (0, import_node_path35.basename)(payload.workspacePath.split("\\").join("/").replace(/\/+$/, "")) || "project";
|
|
19241
21104
|
const redacted = redactPayload({ ...payload, workspacePath: folder });
|
|
19242
21105
|
return { payload: redacted.payload, redactions: redacted.count };
|
|
19243
21106
|
}
|
|
@@ -19268,13 +21131,13 @@ function splitSections(prompt) {
|
|
|
19268
21131
|
chars: section.length
|
|
19269
21132
|
}));
|
|
19270
21133
|
}
|
|
19271
|
-
var
|
|
21134
|
+
var import_node_path35, PayloadCaptured;
|
|
19272
21135
|
var init_payload = __esm({
|
|
19273
21136
|
"src/hosted/payload.ts"() {
|
|
19274
21137
|
"use strict";
|
|
19275
21138
|
init_fileScanner();
|
|
19276
21139
|
init_orchestrator();
|
|
19277
|
-
|
|
21140
|
+
import_node_path35 = require("node:path");
|
|
19278
21141
|
init_config();
|
|
19279
21142
|
init_redact();
|
|
19280
21143
|
PayloadCaptured = class extends Error {
|
|
@@ -19350,11 +21213,11 @@ function readServerMessage(body) {
|
|
|
19350
21213
|
return body.slice(0, 300);
|
|
19351
21214
|
}
|
|
19352
21215
|
}
|
|
19353
|
-
var
|
|
21216
|
+
var import_node_crypto4, PRICING_URL, FREE_LIMIT_MESSAGE, PREVIEW_TTL_MS, STATUS_TIMEOUT_MS, STATUS_CACHE_MS, MAX_PREVIEWS, TRANSIENT_REASONS, SEVERITY_RANK3, FullCheckService;
|
|
19354
21217
|
var init_fullCheck = __esm({
|
|
19355
21218
|
"src/hosted/fullCheck.ts"() {
|
|
19356
21219
|
"use strict";
|
|
19357
|
-
|
|
21220
|
+
import_node_crypto4 = require("node:crypto");
|
|
19358
21221
|
init_engines();
|
|
19359
21222
|
init_planLimits();
|
|
19360
21223
|
init_productionMapLanes();
|
|
@@ -19365,6 +21228,8 @@ var init_fullCheck = __esm({
|
|
|
19365
21228
|
PRICING_URL = "https://viberaven.dev/pricing";
|
|
19366
21229
|
FREE_LIMIT_MESSAGE = `You've used your free full checks. Pro gives ${PRO_MONTHLY_STATION_RUNS} a month.`;
|
|
19367
21230
|
PREVIEW_TTL_MS = 15 * 6e4;
|
|
21231
|
+
STATUS_TIMEOUT_MS = 5e3;
|
|
21232
|
+
STATUS_CACHE_MS = 5e3;
|
|
19368
21233
|
MAX_PREVIEWS = 5;
|
|
19369
21234
|
TRANSIENT_REASONS = /* @__PURE__ */ new Set(["network", "timeout", "server", "rate_limited", "busy"]);
|
|
19370
21235
|
SEVERITY_RANK3 = { critical: 0, warning: 1, info: 2 };
|
|
@@ -19372,10 +21237,12 @@ var init_fullCheck = __esm({
|
|
|
19372
21237
|
constructor(workspacePath, options = {}) {
|
|
19373
21238
|
this.workspacePath = workspacePath;
|
|
19374
21239
|
this.apiBaseUrl = normalizeBaseUrl(options.apiBaseUrl ?? resolveApiBaseUrl());
|
|
19375
|
-
this.store = new CredentialStore(options.configDir ?? defaultConfigDir(), workspacePath, { restrict: options.restrictCredentialsFile });
|
|
21240
|
+
this.store = new CredentialStore(options.configDir ?? defaultConfigDir(), workspacePath, { restrict: options.restrictCredentialsFile, fs: options.credentialsFs });
|
|
19376
21241
|
this.openUrl = options.openUrl ?? openUrlInBrowser;
|
|
19377
21242
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 15e3;
|
|
19378
21243
|
this.runTimeoutMs = options.runTimeoutMs ?? 18e4;
|
|
21244
|
+
this.statusTimeoutMs = options.statusTimeoutMs ?? STATUS_TIMEOUT_MS;
|
|
21245
|
+
this.statusCacheMs = options.statusCacheMs ?? STATUS_CACHE_MS;
|
|
19379
21246
|
this.buildPayload = options.buildPayload ?? buildHostedCheckPayload;
|
|
19380
21247
|
}
|
|
19381
21248
|
apiBaseUrl;
|
|
@@ -19383,6 +21250,8 @@ var init_fullCheck = __esm({
|
|
|
19383
21250
|
openUrl;
|
|
19384
21251
|
requestTimeoutMs;
|
|
19385
21252
|
runTimeoutMs;
|
|
21253
|
+
statusTimeoutMs;
|
|
21254
|
+
statusCacheMs;
|
|
19386
21255
|
buildPayload;
|
|
19387
21256
|
pending = null;
|
|
19388
21257
|
previews = /* @__PURE__ */ new Map();
|
|
@@ -19390,16 +21259,54 @@ var init_fullCheck = __esm({
|
|
|
19390
21259
|
running = false;
|
|
19391
21260
|
/** Used only when the token could not be written to disk with owner-only access. */
|
|
19392
21261
|
sessionCredentials = null;
|
|
21262
|
+
/** The last live plan check, so a burst of `status()` calls asks the API once. */
|
|
21263
|
+
lastCheck = null;
|
|
21264
|
+
checkInFlight = null;
|
|
21265
|
+
/**
|
|
21266
|
+
* Bumped whenever the sign-in or what the API holds may have changed (sign-out, sign-in, a run,
|
|
21267
|
+
* a plan limit). A live check started before the bump is superseded: its answer is not cached,
|
|
21268
|
+
* not shown as live, and not written to the credentials file.
|
|
21269
|
+
*/
|
|
21270
|
+
generation = 0;
|
|
21271
|
+
/** The last time the API confirmed the plan, kept when a later check fails. */
|
|
21272
|
+
lastLive = null;
|
|
21273
|
+
/** A saved sign-in this Studio signed out of but could not delete. Ignored until the file changes. */
|
|
21274
|
+
unremovedToken = null;
|
|
21275
|
+
/** Failed sign-out could not identify the disk token; only a new explicit sign-in can trust it again. */
|
|
21276
|
+
ignoreStoredCredentials = false;
|
|
19393
21277
|
/** The sign-in to use: the saved one, else a session-only one, and only for this API. */
|
|
19394
21278
|
async credentials() {
|
|
21279
|
+
if (this.sessionCredentials?.apiBaseUrl === this.apiBaseUrl) return this.sessionCredentials;
|
|
21280
|
+
if (this.ignoreStoredCredentials) return null;
|
|
19395
21281
|
const stored = await this.store.readFor(this.apiBaseUrl);
|
|
19396
|
-
|
|
19397
|
-
return this.sessionCredentials?.apiBaseUrl === this.apiBaseUrl ? this.sessionCredentials : null;
|
|
21282
|
+
return stored && stored.accessToken !== this.unremovedToken ? stored : null;
|
|
19398
21283
|
}
|
|
21284
|
+
/**
|
|
21285
|
+
* Signs this Studio out. Never throws: if the saved sign-in cannot be deleted (Windows still
|
|
21286
|
+
* refusing after the retries), this Studio ignores it from now on and it returns false.
|
|
21287
|
+
*/
|
|
19399
21288
|
async clearCredentials() {
|
|
19400
21289
|
this.sessionCredentials = null;
|
|
19401
21290
|
this.lastUsage = null;
|
|
19402
|
-
|
|
21291
|
+
this.lastLive = null;
|
|
21292
|
+
this.forgetLiveCheck();
|
|
21293
|
+
const saved = await this.store.readFor(this.apiBaseUrl).catch(() => null);
|
|
21294
|
+
try {
|
|
21295
|
+
await this.store.clear();
|
|
21296
|
+
this.ignoreStoredCredentials = false;
|
|
21297
|
+
return true;
|
|
21298
|
+
} catch (error) {
|
|
21299
|
+
if (saved) this.unremovedToken = saved.accessToken;
|
|
21300
|
+
else this.ignoreStoredCredentials = true;
|
|
21301
|
+
console.warn(`VibeRaven: the saved sign-in at ${this.store.path} could not be removed (${error instanceof Error ? error.message : String(error)}). This Studio treats it as signed out.`);
|
|
21302
|
+
return false;
|
|
21303
|
+
}
|
|
21304
|
+
}
|
|
21305
|
+
/** The next `status()` asks the API again, and any check still in flight is superseded. */
|
|
21306
|
+
forgetLiveCheck() {
|
|
21307
|
+
this.generation += 1;
|
|
21308
|
+
this.lastCheck = null;
|
|
21309
|
+
this.checkInFlight = null;
|
|
19403
21310
|
}
|
|
19404
21311
|
/** Keep the plan label in step with what the API last said (upgrade, downgrade, lapse). */
|
|
19405
21312
|
/**
|
|
@@ -19409,6 +21316,8 @@ var init_fullCheck = __esm({
|
|
|
19409
21316
|
* failure the old saved sign-in (with its old plan label) is still there.
|
|
19410
21317
|
*/
|
|
19411
21318
|
async refreshPlan(plan) {
|
|
21319
|
+
this.forgetLiveCheck();
|
|
21320
|
+
if (this.lastLive) this.lastLive = { ...this.lastLive, account: { ...this.lastLive.account, plan } };
|
|
19412
21321
|
if (this.sessionCredentials) this.sessionCredentials = { ...this.sessionCredentials, account: { ...this.sessionCredentials.account, plan } };
|
|
19413
21322
|
try {
|
|
19414
21323
|
await this.store.updatePlan(this.apiBaseUrl, plan);
|
|
@@ -19416,19 +21325,87 @@ var init_fullCheck = __esm({
|
|
|
19416
21325
|
console.warn(`VibeRaven: could not update the saved plan label to "${plan}"; keeping the saved sign-in as it was. ${error instanceof Error ? error.message : String(error)}`);
|
|
19417
21326
|
}
|
|
19418
21327
|
}
|
|
21328
|
+
/**
|
|
21329
|
+
* What the "Run full check" sheet shows. When signed in, the plan and usage come from the API
|
|
21330
|
+
* (`GET /v1/account/me`), not from the label saved at sign-in, so a Pro subscription that is no
|
|
21331
|
+
* longer valid shows as Free. If the API cannot be asked, the saved values are returned with `stale: true`.
|
|
21332
|
+
* A 401 means the sign-in is gone: it is cleared and the status reads signed out.
|
|
21333
|
+
*/
|
|
19419
21334
|
async status() {
|
|
21335
|
+
const before = await this.credentials();
|
|
21336
|
+
const check = before ? await this.liveCheck(before) : null;
|
|
19420
21337
|
const creds = await this.credentials();
|
|
21338
|
+
const live = creds && typeof check === "object" && check?.accessToken === creds.accessToken ? check : null;
|
|
21339
|
+
const known = creds && this.lastLive?.accessToken === creds.accessToken ? this.lastLive : null;
|
|
19421
21340
|
const pending = this.pending && Date.parse(this.pending.expiresAt) > Date.now() ? this.pending : null;
|
|
19422
21341
|
return {
|
|
19423
21342
|
apiBaseUrl: this.apiBaseUrl,
|
|
19424
21343
|
usingDefaultApi: this.apiBaseUrl === DEFAULT_API_BASE_URL,
|
|
19425
21344
|
signedIn: creds !== null,
|
|
19426
|
-
|
|
19427
|
-
|
|
21345
|
+
// When stale, the last live answer beats the file: its rewrite may not have landed yet.
|
|
21346
|
+
account: creds ? live?.account ?? known?.account ?? creds.account : null,
|
|
21347
|
+
usage: creds ? live?.usage ?? this.lastUsage : null,
|
|
19428
21348
|
signIn: pending ? this.signInView(pending) : null,
|
|
19429
21349
|
credentialsPath: this.store.path,
|
|
19430
|
-
sessionOnly: creds !== null && creds === this.sessionCredentials
|
|
21350
|
+
sessionOnly: creds !== null && creds === this.sessionCredentials,
|
|
21351
|
+
stale: creds !== null && live === null,
|
|
21352
|
+
checkedAt: known?.checkedAt ?? null
|
|
21353
|
+
};
|
|
21354
|
+
}
|
|
21355
|
+
/** One live check per sign-in, shared by concurrent callers and reused for `statusCacheMs`. */
|
|
21356
|
+
liveCheck(creds) {
|
|
21357
|
+
const last = this.lastCheck;
|
|
21358
|
+
if (last && last.accessToken === creds.accessToken && Date.now() - last.at < this.statusCacheMs) return Promise.resolve(last.result);
|
|
21359
|
+
const inFlight = this.checkInFlight;
|
|
21360
|
+
if (inFlight && inFlight.accessToken === creds.accessToken) return inFlight.promise;
|
|
21361
|
+
const generation = this.generation;
|
|
21362
|
+
const entry = {
|
|
21363
|
+
accessToken: creds.accessToken,
|
|
21364
|
+
promise: this.askAccount(creds, generation).then((result) => {
|
|
21365
|
+
if (this.checkInFlight === entry) this.checkInFlight = null;
|
|
21366
|
+
if (this.generation === generation) this.lastCheck = { accessToken: creds.accessToken, at: Date.now(), result };
|
|
21367
|
+
return result;
|
|
21368
|
+
})
|
|
19431
21369
|
};
|
|
21370
|
+
this.checkInFlight = entry;
|
|
21371
|
+
return entry.promise;
|
|
21372
|
+
}
|
|
21373
|
+
/** Never throws: any failure other than a 401 leaves the saved sign-in as it is. */
|
|
21374
|
+
async askAccount(creds, generation) {
|
|
21375
|
+
try {
|
|
21376
|
+
const answer = await fetchAccount(this.apiBaseUrl, creds.accessToken, this.statusTimeoutMs);
|
|
21377
|
+
const live = {
|
|
21378
|
+
accessToken: creds.accessToken,
|
|
21379
|
+
account: { email: answer.email, plan: answer.plan },
|
|
21380
|
+
usage: toUsage(answer.usage),
|
|
21381
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21382
|
+
};
|
|
21383
|
+
const current = await this.credentials();
|
|
21384
|
+
if (current?.accessToken !== creds.accessToken) return "unavailable";
|
|
21385
|
+
if (this.generation !== generation) return "unavailable";
|
|
21386
|
+
this.lastLive = live;
|
|
21387
|
+
this.lastUsage = live.usage;
|
|
21388
|
+
this.saveAccount(creds.accessToken, live.account, generation);
|
|
21389
|
+
return live;
|
|
21390
|
+
} catch (error) {
|
|
21391
|
+
if (error instanceof HostedCallError && error.kind === "http" && error.details.status === 401) {
|
|
21392
|
+
const current = await this.credentials().catch(() => null);
|
|
21393
|
+
if (current?.accessToken === creds.accessToken) await this.clearCredentials();
|
|
21394
|
+
return "signed_out";
|
|
21395
|
+
}
|
|
21396
|
+
return "unavailable";
|
|
21397
|
+
}
|
|
21398
|
+
}
|
|
21399
|
+
/**
|
|
21400
|
+
* Fire and forget: `status()` never waits on the file (locking it can take seconds on Windows).
|
|
21401
|
+
* The rewrite is queued behind every other change to the file, skipped if this check has been
|
|
21402
|
+
* superseded by the time it runs, and a failure is logged, never thrown.
|
|
21403
|
+
*/
|
|
21404
|
+
saveAccount(accessToken, account, generation) {
|
|
21405
|
+
if (this.sessionCredentials?.accessToken === accessToken) this.sessionCredentials = { ...this.sessionCredentials, account };
|
|
21406
|
+
this.store.updateAccount(this.apiBaseUrl, accessToken, account, () => this.generation === generation).catch((error) => {
|
|
21407
|
+
console.warn(`VibeRaven: could not update the saved plan label to "${account.plan}"; keeping the saved sign-in as it was. ${error instanceof Error ? error.message : String(error)}`);
|
|
21408
|
+
});
|
|
19432
21409
|
}
|
|
19433
21410
|
async startSignIn() {
|
|
19434
21411
|
return this.guard(async () => {
|
|
@@ -19466,27 +21443,37 @@ var init_fullCheck = __esm({
|
|
|
19466
21443
|
const account = { email: polled.account.email, plan: polled.account.plan };
|
|
19467
21444
|
this.pending = null;
|
|
19468
21445
|
this.lastUsage = null;
|
|
21446
|
+
this.lastLive = null;
|
|
19469
21447
|
this.sessionCredentials = null;
|
|
21448
|
+
this.unremovedToken = null;
|
|
21449
|
+
this.forgetLiveCheck();
|
|
19470
21450
|
try {
|
|
19471
21451
|
await this.store.write({ apiBaseUrl: this.apiBaseUrl, accessToken: polled.accessToken, account });
|
|
19472
21452
|
} catch (error) {
|
|
19473
|
-
if (
|
|
19474
|
-
|
|
21453
|
+
if (error instanceof CredentialLocationError) throw error;
|
|
21454
|
+
const reason = error instanceof CredentialPermissionError ? error.message : `The sign-in file could not be saved (${error instanceof Error ? error.message : String(error)})`;
|
|
21455
|
+
if (!(error instanceof CredentialPermissionError)) console.warn(`VibeRaven: ${reason}; keeping this sign-in for this Studio session only.`);
|
|
21456
|
+
await this.store.clear().catch(() => void 0);
|
|
19475
21457
|
this.sessionCredentials = { version: 1, apiBaseUrl: this.apiBaseUrl, accessToken: polled.accessToken, account, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
21458
|
+
this.ignoreStoredCredentials = false;
|
|
19476
21459
|
return {
|
|
19477
21460
|
kind: "signed_in",
|
|
19478
21461
|
account,
|
|
19479
|
-
notice: `Signed in for this Studio session only. ${
|
|
21462
|
+
notice: `Signed in for this Studio session only. ${reason}, so it was not saved to disk. You will need to sign in again next time you open the Studio.`
|
|
19480
21463
|
};
|
|
19481
21464
|
}
|
|
21465
|
+
this.ignoreStoredCredentials = false;
|
|
19482
21466
|
return { kind: "signed_in", account };
|
|
19483
21467
|
});
|
|
19484
21468
|
}
|
|
19485
21469
|
async signOut() {
|
|
19486
21470
|
return this.guard(async () => {
|
|
19487
21471
|
this.pending = null;
|
|
19488
|
-
await this.clearCredentials();
|
|
19489
|
-
return {
|
|
21472
|
+
if (await this.clearCredentials()) return { kind: "signed_out" };
|
|
21473
|
+
return {
|
|
21474
|
+
kind: "signed_out",
|
|
21475
|
+
notice: `Signed out in this Studio, but the saved sign-in at ${this.store.path} could not be removed. Delete that file, or sign out again once nothing else has it open.`
|
|
21476
|
+
};
|
|
19490
21477
|
});
|
|
19491
21478
|
}
|
|
19492
21479
|
async prepare() {
|
|
@@ -19495,8 +21482,8 @@ var init_fullCheck = __esm({
|
|
|
19495
21482
|
const built = await this.buildPayload(this.workspacePath);
|
|
19496
21483
|
const final = finalizePayload(built.payload);
|
|
19497
21484
|
this.prunePreviews();
|
|
19498
|
-
const id = (0,
|
|
19499
|
-
this.previews.set(id, { payload: final.payload, idempotencyKey: (0,
|
|
21485
|
+
const id = (0, import_node_crypto4.randomUUID)();
|
|
21486
|
+
this.previews.set(id, { payload: final.payload, idempotencyKey: (0, import_node_crypto4.randomUUID)(), createdAt: Date.now() });
|
|
19500
21487
|
return { kind: "preview", preview: describePayload(id, `${this.apiBaseUrl}/v1/station/run`, final.payload, built.scan, final.redactions) };
|
|
19501
21488
|
} catch (error) {
|
|
19502
21489
|
if (error instanceof HostedCallError) return this.fromCallError(error);
|
|
@@ -19616,10 +21603,10 @@ async function connectProviderMcp(cwd, providerId) {
|
|
|
19616
21603
|
if (!recipe) {
|
|
19617
21604
|
return { ok: false, message: `No known MCP server for ${providerId} yet. Add one manually to .mcp.json.` };
|
|
19618
21605
|
}
|
|
19619
|
-
const configPath = (0,
|
|
21606
|
+
const configPath = (0, import_node_path36.join)(cwd, ".mcp.json");
|
|
19620
21607
|
let config = {};
|
|
19621
21608
|
try {
|
|
19622
|
-
const raw = await (0,
|
|
21609
|
+
const raw = await (0, import_promises28.readFile)(configPath, "utf8");
|
|
19623
21610
|
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
19624
21611
|
if (!isRecord5(parsed) || parsed.mcpServers !== void 0 && !isRecord5(parsed.mcpServers)) {
|
|
19625
21612
|
return { ok: false, message: "Existing .mcp.json has an unsupported structure. It was left unchanged." };
|
|
@@ -19636,7 +21623,7 @@ async function connectProviderMcp(cwd, providerId) {
|
|
|
19636
21623
|
}
|
|
19637
21624
|
servers[providerId] = recipe.entry;
|
|
19638
21625
|
const next = { ...config, mcpServers: servers };
|
|
19639
|
-
await (0,
|
|
21626
|
+
await (0, import_promises28.writeFile)(configPath, `${JSON.stringify(next, null, 2)}
|
|
19640
21627
|
`, "utf8");
|
|
19641
21628
|
const suffix = recipe.note ? ` ${recipe.note}` : "";
|
|
19642
21629
|
return { ok: true, message: `Added ${providerId} MCP server to .mcp.json.${suffix}`, path: ".mcp.json" };
|
|
@@ -19648,7 +21635,7 @@ function headerValue(request, name) {
|
|
|
19648
21635
|
function tokensMatch(given, expected) {
|
|
19649
21636
|
const a = Buffer.from(given, "utf8");
|
|
19650
21637
|
const b3 = Buffer.from(expected, "utf8");
|
|
19651
|
-
return a.length === b3.length && (0,
|
|
21638
|
+
return a.length === b3.length && (0, import_node_crypto5.timingSafeEqual)(a, b3);
|
|
19652
21639
|
}
|
|
19653
21640
|
function guardRequest(request, response, pathname, guard) {
|
|
19654
21641
|
const allowedHosts = [`127.0.0.1:${guard.port}`, `localhost:${guard.port}`];
|
|
@@ -19689,7 +21676,7 @@ async function defaultScanRunner(flags, positional) {
|
|
|
19689
21676
|
}
|
|
19690
21677
|
async function readPackageJson(cwd) {
|
|
19691
21678
|
try {
|
|
19692
|
-
const raw = await (0,
|
|
21679
|
+
const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, "package.json"), "utf8");
|
|
19693
21680
|
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
19694
21681
|
return isRecord5(parsed) ? parsed : {};
|
|
19695
21682
|
} catch {
|
|
@@ -19712,7 +21699,7 @@ async function listProjectFiles(cwd, maxFiles = 1800) {
|
|
|
19712
21699
|
if (files.length >= maxFiles) return;
|
|
19713
21700
|
let entries;
|
|
19714
21701
|
try {
|
|
19715
|
-
entries = await (0,
|
|
21702
|
+
entries = await (0, import_promises28.readdir)(dir, { withFileTypes: true });
|
|
19716
21703
|
} catch {
|
|
19717
21704
|
return;
|
|
19718
21705
|
}
|
|
@@ -19720,7 +21707,7 @@ async function listProjectFiles(cwd, maxFiles = 1800) {
|
|
|
19720
21707
|
if (files.length >= maxFiles) return;
|
|
19721
21708
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
19722
21709
|
if (entry.isDirectory()) {
|
|
19723
|
-
if (!skipped.has(entry.name)) await walk((0,
|
|
21710
|
+
if (!skipped.has(entry.name)) await walk((0, import_node_path36.join)(dir, entry.name), rel);
|
|
19724
21711
|
continue;
|
|
19725
21712
|
}
|
|
19726
21713
|
if (entry.isFile()) files.push(rel.toLowerCase());
|
|
@@ -19739,7 +21726,7 @@ async function readEnvKeys(cwd) {
|
|
|
19739
21726
|
const keys = /* @__PURE__ */ new Set();
|
|
19740
21727
|
for (const name of [".env.example", ".env"]) {
|
|
19741
21728
|
try {
|
|
19742
|
-
const raw = await (0,
|
|
21729
|
+
const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, name), "utf8");
|
|
19743
21730
|
for (const line of raw.split(/\r?\n/)) {
|
|
19744
21731
|
const trimmed = line.trim();
|
|
19745
21732
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -19842,7 +21829,7 @@ function runGit(cwd, args, timeoutMs = 1500) {
|
|
|
19842
21829
|
}
|
|
19843
21830
|
function spawnGit(cwd, args, env, timeoutMs) {
|
|
19844
21831
|
return new Promise((resolveGit) => {
|
|
19845
|
-
const child = (0,
|
|
21832
|
+
const child = (0, import_node_child_process8.spawn)("git", args, {
|
|
19846
21833
|
cwd,
|
|
19847
21834
|
env,
|
|
19848
21835
|
windowsHide: true,
|
|
@@ -19915,7 +21902,7 @@ function runGitCommand(cwd, args, timeoutMs = 5e3) {
|
|
|
19915
21902
|
}
|
|
19916
21903
|
function spawnGitCommand(cwd, args, env, timeoutMs) {
|
|
19917
21904
|
return new Promise((resolveCommand) => {
|
|
19918
|
-
const child = (0,
|
|
21905
|
+
const child = (0, import_node_child_process8.spawn)("git", args, {
|
|
19919
21906
|
cwd,
|
|
19920
21907
|
env,
|
|
19921
21908
|
windowsHide: true,
|
|
@@ -20057,7 +22044,7 @@ async function codexModelOptions() {
|
|
|
20057
22044
|
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini" }
|
|
20058
22045
|
];
|
|
20059
22046
|
try {
|
|
20060
|
-
const raw = await (0,
|
|
22047
|
+
const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)((0, import_node_os2.homedir)(), ".codex", "models_cache.json"), "utf8");
|
|
20061
22048
|
const parsed = JSON.parse(raw);
|
|
20062
22049
|
const list = Array.isArray(parsed) ? parsed : parsed.models ?? [];
|
|
20063
22050
|
const options = list.filter((model) => typeof model?.slug === "string").filter((model) => !/auto-review|reserve/i.test(model.slug)).map((model) => ({ id: model.slug, label: model.display_name?.trim() || model.slug }));
|
|
@@ -20081,7 +22068,7 @@ function cliModelDefaults(id) {
|
|
|
20081
22068
|
}
|
|
20082
22069
|
function detectCommand(command, cwd) {
|
|
20083
22070
|
return new Promise((resolveDetect) => {
|
|
20084
|
-
const child = (0,
|
|
22071
|
+
const child = (0, import_node_child_process8.spawn)(process.platform === "win32" ? "where.exe" : "sh", process.platform === "win32" ? [command] : ["-lc", `command -v ${command}`], {
|
|
20085
22072
|
cwd,
|
|
20086
22073
|
windowsHide: true,
|
|
20087
22074
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -20168,7 +22155,7 @@ function setupCommandFor(cliId, step) {
|
|
|
20168
22155
|
function runShellCommand(command, cwd, timeoutMs = 18e4) {
|
|
20169
22156
|
return new Promise((resolveRun) => {
|
|
20170
22157
|
const useCmd = process.platform === "win32";
|
|
20171
|
-
const child = (0,
|
|
22158
|
+
const child = (0, import_node_child_process8.spawn)(useCmd ? process.env.ComSpec || "cmd.exe" : "sh", useCmd ? ["/d", "/s", "/c", command] : ["-lc", command], {
|
|
20172
22159
|
cwd,
|
|
20173
22160
|
windowsHide: true,
|
|
20174
22161
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -20202,7 +22189,7 @@ function openSystemTerminal(command, cwd) {
|
|
|
20202
22189
|
const fullCommand = process.platform === "win32" ? `cd /d ${quotedCwd} && ${command}` : `cd ${quotedCwd} && ${command}`;
|
|
20203
22190
|
let child;
|
|
20204
22191
|
if (process.platform === "win32") {
|
|
20205
|
-
child = (0,
|
|
22192
|
+
child = (0, import_node_child_process8.spawn)("cmd.exe", ["/c", "start", "cmd.exe", "/k", fullCommand], {
|
|
20206
22193
|
cwd,
|
|
20207
22194
|
windowsHide: true,
|
|
20208
22195
|
stdio: "ignore",
|
|
@@ -20210,9 +22197,9 @@ function openSystemTerminal(command, cwd) {
|
|
|
20210
22197
|
});
|
|
20211
22198
|
} else if (process.platform === "darwin") {
|
|
20212
22199
|
const script = `tell application "Terminal" to do script "${fullCommand.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
20213
|
-
child = (0,
|
|
22200
|
+
child = (0, import_node_child_process8.spawn)("osascript", ["-e", script], { cwd, windowsHide: true, stdio: "ignore", detached: true });
|
|
20214
22201
|
} else {
|
|
20215
|
-
child = (0,
|
|
22202
|
+
child = (0, import_node_child_process8.spawn)("x-terminal-emulator", ["-e", `bash -lc ${JSON.stringify(fullCommand)}`], {
|
|
20216
22203
|
cwd,
|
|
20217
22204
|
windowsHide: true,
|
|
20218
22205
|
stdio: "ignore",
|
|
@@ -20260,19 +22247,19 @@ function isPlaceholderProjectName(value) {
|
|
|
20260
22247
|
async function readMcpEvidence(cwd) {
|
|
20261
22248
|
const candidates = MCP_CONFIG_PATHS.map((relativePath) => ({
|
|
20262
22249
|
source: relativePath.replace(/\\/g, "/"),
|
|
20263
|
-
path: (0,
|
|
22250
|
+
path: (0, import_node_path36.resolve)(cwd, relativePath)
|
|
20264
22251
|
}));
|
|
20265
22252
|
const home = process.env.USERPROFILE || process.env.HOME;
|
|
20266
22253
|
if (home) {
|
|
20267
22254
|
candidates.push({
|
|
20268
22255
|
source: "~/.codex/config.toml",
|
|
20269
|
-
path: (0,
|
|
22256
|
+
path: (0, import_node_path36.resolve)(home, ".codex", "config.toml")
|
|
20270
22257
|
});
|
|
20271
22258
|
}
|
|
20272
22259
|
const evidence = [];
|
|
20273
22260
|
for (const candidate of candidates) {
|
|
20274
22261
|
try {
|
|
20275
|
-
const text = await (0,
|
|
22262
|
+
const text = await (0, import_promises28.readFile)(candidate.path, "utf8");
|
|
20276
22263
|
if (/mcp/i.test(text)) evidence.push({ source: candidate.source, text });
|
|
20277
22264
|
} catch {
|
|
20278
22265
|
}
|
|
@@ -20363,7 +22350,7 @@ async function enrichProjectMetadata(state, cwd) {
|
|
|
20363
22350
|
async function readState(cwd) {
|
|
20364
22351
|
let state;
|
|
20365
22352
|
try {
|
|
20366
|
-
const raw = await (0,
|
|
22353
|
+
const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "last-scan.json"), "utf8");
|
|
20367
22354
|
const packageJson = await readPackageJson(cwd);
|
|
20368
22355
|
const deps = dependencyNames(packageJson);
|
|
20369
22356
|
const files = await listProjectFiles(cwd);
|
|
@@ -20609,7 +22596,7 @@ function quoteWindowsCmdArg(value) {
|
|
|
20609
22596
|
}
|
|
20610
22597
|
async function whereExe(command, cwd) {
|
|
20611
22598
|
return new Promise((resolvePath) => {
|
|
20612
|
-
const child = (0,
|
|
22599
|
+
const child = (0, import_node_child_process8.spawn)("where.exe", [command], {
|
|
20613
22600
|
cwd,
|
|
20614
22601
|
windowsHide: true,
|
|
20615
22602
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -20629,8 +22616,8 @@ async function whereExe(command, cwd) {
|
|
|
20629
22616
|
});
|
|
20630
22617
|
}
|
|
20631
22618
|
function expandShimPath(value, base) {
|
|
20632
|
-
const
|
|
20633
|
-
return
|
|
22619
|
+
const relative8 = value.replace(/^%(?:dp0%\\+|~dp0\\*)/i, "");
|
|
22620
|
+
return relative8 === value ? value : (0, import_node_path36.join)(base, ...relative8.split(/[\\/]+/));
|
|
20634
22621
|
}
|
|
20635
22622
|
function findShimTarget(lines, pattern, accept = () => true) {
|
|
20636
22623
|
for (const line of lines) {
|
|
@@ -20641,21 +22628,21 @@ function findShimTarget(lines, pattern, accept = () => true) {
|
|
|
20641
22628
|
}
|
|
20642
22629
|
async function resolveWindowsShimTarget(shimPath) {
|
|
20643
22630
|
try {
|
|
20644
|
-
const content = await (0,
|
|
20645
|
-
const base = (0,
|
|
22631
|
+
const content = await (0, import_promises28.readFile)(shimPath, "utf8");
|
|
22632
|
+
const base = (0, import_node_path36.dirname)(shimPath);
|
|
20646
22633
|
const lines = content.split(/\r?\n/);
|
|
20647
22634
|
const trailingArgs = (line, after) => [...line.slice(after).split("%*")[0].matchAll(/"([^"]*)"|([^\s"&|<>]+)/g)].map((token) => expandShimPath(token[1] ?? token[2], base)).filter((value) => value.length > 0 && !value.includes("%"));
|
|
20648
22635
|
const script = findShimTarget(lines, /"%(?:dp0%\\+|~dp0\\*)([^"]+\.js)"/i);
|
|
20649
22636
|
if (script) {
|
|
20650
22637
|
return {
|
|
20651
22638
|
command: process.execPath,
|
|
20652
|
-
args: [(0,
|
|
22639
|
+
args: [(0, import_node_path36.join)(base, ...script.match[1].split(/[\\/]+/)), ...trailingArgs(script.line, script.match.index + script.match[0].length)]
|
|
20653
22640
|
};
|
|
20654
22641
|
}
|
|
20655
|
-
const binary = findShimTarget(lines, /"%(?:dp0%\\+|~dp0\\*)([^"]+\.exe)"/i, (target) => (0,
|
|
22642
|
+
const binary = findShimTarget(lines, /"%(?:dp0%\\+|~dp0\\*)([^"]+\.exe)"/i, (target) => (0, import_node_path36.basename)(target).toLowerCase() !== "node.exe");
|
|
20656
22643
|
if (binary) {
|
|
20657
22644
|
return {
|
|
20658
|
-
command: (0,
|
|
22645
|
+
command: (0, import_node_path36.join)(base, ...binary.match[1].split(/[\\/]+/)),
|
|
20659
22646
|
args: trailingArgs(binary.line, binary.match.index + binary.match[0].length)
|
|
20660
22647
|
};
|
|
20661
22648
|
}
|
|
@@ -20676,7 +22663,7 @@ function killProcessTree(pid, fallback) {
|
|
|
20676
22663
|
return;
|
|
20677
22664
|
}
|
|
20678
22665
|
if (process.platform === "win32") {
|
|
20679
|
-
const killer = (0,
|
|
22666
|
+
const killer = (0, import_node_child_process8.spawn)("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
20680
22667
|
killer.once("error", fallback);
|
|
20681
22668
|
killer.once("close", (code) => {
|
|
20682
22669
|
if (code !== 0) fallback();
|
|
@@ -20701,7 +22688,7 @@ async function runAgentCommand(command, args, cwd, stdin, runId) {
|
|
|
20701
22688
|
const spawnCommand = shim?.command ?? (useCmdShim ? process.env.ComSpec || "cmd.exe" : command);
|
|
20702
22689
|
const spawnArgs = shim ? [...shim.args, ...args] : useCmdShim ? ["/d", "/s", "/c", [command, ...args.map(quoteWindowsCmdArg)].join(" ")] : args;
|
|
20703
22690
|
return new Promise((resolveRun) => {
|
|
20704
|
-
const child = (0,
|
|
22691
|
+
const child = (0, import_node_child_process8.spawn)(spawnCommand, spawnArgs, {
|
|
20705
22692
|
cwd,
|
|
20706
22693
|
windowsHide: true,
|
|
20707
22694
|
// A separate process group lets Stop and timeout terminate descendants on Unix too.
|
|
@@ -20780,8 +22767,8 @@ async function writeChatContextArtifact(cwd, request, prompt) {
|
|
|
20780
22767
|
"",
|
|
20781
22768
|
prompt
|
|
20782
22769
|
].filter((line) => typeof line === "string");
|
|
20783
|
-
await (0,
|
|
20784
|
-
await (0,
|
|
22770
|
+
await (0, import_promises28.mkdir)((0, import_node_path36.join)(cwd, ".viberaven"), { recursive: true });
|
|
22771
|
+
await (0, import_promises28.writeFile)((0, import_node_path36.join)(cwd, ".viberaven", "chat-context.md"), `${lines.join("\n")}
|
|
20785
22772
|
`, "utf8");
|
|
20786
22773
|
} catch {
|
|
20787
22774
|
}
|
|
@@ -20801,13 +22788,13 @@ async function runAgentChat(cwd, request) {
|
|
|
20801
22788
|
}
|
|
20802
22789
|
const prompt = await buildAgentChatPrompt(request, cwd);
|
|
20803
22790
|
await writeChatContextArtifact(cwd, request, prompt);
|
|
20804
|
-
const outputPath = request.cliId === "codex" ? (0,
|
|
20805
|
-
if (outputPath) await (0,
|
|
22791
|
+
const outputPath = request.cliId === "codex" ? (0, import_node_path36.join)(cwd, ".viberaven", `studio-agent-${Date.now()}-${Math.random().toString(36).slice(2)}.md`) : void 0;
|
|
22792
|
+
if (outputPath) await (0, import_promises28.mkdir)((0, import_node_path36.dirname)(outputPath), { recursive: true });
|
|
20806
22793
|
const { command, args, stdin } = commandForAgentChat(request, prompt, cwd, outputPath);
|
|
20807
22794
|
const result = await runAgentCommand(command, args, cwd, stdin, request.runId);
|
|
20808
22795
|
if (outputPath) {
|
|
20809
22796
|
try {
|
|
20810
|
-
const finalMessage = (await (0,
|
|
22797
|
+
const finalMessage = (await (0, import_promises28.readFile)(outputPath, "utf8")).trim();
|
|
20811
22798
|
if (finalMessage) return { ...result, output: finalMessage };
|
|
20812
22799
|
} catch {
|
|
20813
22800
|
}
|
|
@@ -20819,8 +22806,8 @@ function firstGapId(state) {
|
|
|
20819
22806
|
return state.providers.find((provider2) => provider2.nextFix?.id)?.nextFix?.id;
|
|
20820
22807
|
}
|
|
20821
22808
|
async function runLocalAction(cwd, intent) {
|
|
20822
|
-
const artifactDir = (0,
|
|
20823
|
-
await (0,
|
|
22809
|
+
const artifactDir = (0, import_node_path36.join)(cwd, ".viberaven");
|
|
22810
|
+
await (0, import_promises28.mkdir)(artifactDir, { recursive: true });
|
|
20824
22811
|
if (intent === "preview_rehearsal") {
|
|
20825
22812
|
const state2 = await readState(cwd);
|
|
20826
22813
|
const topProvider = state2.providers[0];
|
|
@@ -20861,7 +22848,7 @@ async function runLocalAction(cwd, intent) {
|
|
|
20861
22848
|
proof,
|
|
20862
22849
|
""
|
|
20863
22850
|
].join("\n");
|
|
20864
|
-
await (0,
|
|
22851
|
+
await (0, import_promises28.writeFile)((0, import_node_path36.join)(artifactDir, "preview-rehearsal.md"), markdown, "utf8");
|
|
20865
22852
|
return {
|
|
20866
22853
|
title: "Preview rehearsal ready",
|
|
20867
22854
|
summary: `${projectName}: ${gateLabel} with ${state2.providers.length} provider beat${state2.providers.length === 1 ? "" : "s"}`,
|
|
@@ -20871,7 +22858,7 @@ async function runLocalAction(cwd, intent) {
|
|
|
20871
22858
|
if (intent === "audit_vercel_supabase") {
|
|
20872
22859
|
const auditInput = await collectVercelSupabaseAuditInput(cwd);
|
|
20873
22860
|
const result2 = buildVercelSupabaseAudit(auditInput);
|
|
20874
|
-
await (0,
|
|
22861
|
+
await (0, import_promises28.writeFile)((0, import_node_path36.join)(artifactDir, "audit-vercel-supabase.txt"), `${renderVercelSupabaseAudit(result2)}
|
|
20875
22862
|
`, "utf8");
|
|
20876
22863
|
return {
|
|
20877
22864
|
title: result2.status === "pass" ? "Audit passed" : "Audit needs work",
|
|
@@ -20899,7 +22886,7 @@ async function runLocalAction(cwd, intent) {
|
|
|
20899
22886
|
const mode = intent === "heal_prompt" ? "prompt" : intent === "heal_apply" ? "apply" : "plan";
|
|
20900
22887
|
const result = await runHealCommand({ cwd, mode, gapId, yes: mode === "apply", noVerify: true });
|
|
20901
22888
|
if (mode === "apply") {
|
|
20902
|
-
await (0,
|
|
22889
|
+
await (0, import_promises28.writeFile)((0, import_node_path36.join)(artifactDir, "heal-apply-result.json"), `${JSON.stringify(result, null, 2)}
|
|
20903
22890
|
`, "utf8");
|
|
20904
22891
|
}
|
|
20905
22892
|
return {
|
|
@@ -20909,14 +22896,14 @@ async function runLocalAction(cwd, intent) {
|
|
|
20909
22896
|
};
|
|
20910
22897
|
}
|
|
20911
22898
|
async function sendArtifact(cwd, rel, response, contentType = "text/plain; charset=utf-8") {
|
|
20912
|
-
const absolute = (0,
|
|
20913
|
-
const root = (0,
|
|
22899
|
+
const absolute = (0, import_node_path36.resolve)(cwd, rel);
|
|
22900
|
+
const root = (0, import_node_path36.resolve)(cwd);
|
|
20914
22901
|
if (!absolute.startsWith(root)) {
|
|
20915
22902
|
sendText(response, 403, "Forbidden");
|
|
20916
22903
|
return;
|
|
20917
22904
|
}
|
|
20918
22905
|
try {
|
|
20919
|
-
const text = await (0,
|
|
22906
|
+
const text = await (0, import_promises28.readFile)(absolute, "utf8");
|
|
20920
22907
|
response.writeHead(200, { "content-type": contentType });
|
|
20921
22908
|
response.end(text);
|
|
20922
22909
|
} catch {
|
|
@@ -20925,7 +22912,7 @@ async function sendArtifact(cwd, rel, response, contentType = "text/plain; chars
|
|
|
20925
22912
|
}
|
|
20926
22913
|
async function sendReportArtifact(cwd, response) {
|
|
20927
22914
|
try {
|
|
20928
|
-
const text = await (0,
|
|
22915
|
+
const text = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "report.html"), "utf8");
|
|
20929
22916
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
20930
22917
|
response.end(text);
|
|
20931
22918
|
return;
|
|
@@ -21052,7 +23039,7 @@ async function sendReportArtifact(cwd, response) {
|
|
|
21052
23039
|
}
|
|
21053
23040
|
async function sendExtensionIcon(cwd, response) {
|
|
21054
23041
|
try {
|
|
21055
|
-
const body = await (0,
|
|
23042
|
+
const body = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, "media", "extension-icon.png"));
|
|
21056
23043
|
sendBinary(response, 200, "image/png", body);
|
|
21057
23044
|
} catch {
|
|
21058
23045
|
sendText(response, 404, "Icon not found");
|
|
@@ -21065,15 +23052,15 @@ async function sendMangaAsset(pathname, response) {
|
|
|
21065
23052
|
return;
|
|
21066
23053
|
}
|
|
21067
23054
|
const candidates = [
|
|
21068
|
-
(0,
|
|
23055
|
+
(0, import_node_path36.join)(__dirname, "manga", name),
|
|
21069
23056
|
// installed + dev: <pkg>/dist/manga
|
|
21070
|
-
(0,
|
|
23057
|
+
(0, import_node_path36.join)(__dirname, "..", "assets", "manga", name),
|
|
21071
23058
|
// fallback: <pkg>/assets/manga (source)
|
|
21072
|
-
(0,
|
|
23059
|
+
(0, import_node_path36.join)(__dirname, "..", "..", "assets", "manga", name)
|
|
21073
23060
|
];
|
|
21074
23061
|
for (const candidate of candidates) {
|
|
21075
23062
|
try {
|
|
21076
|
-
const body = await (0,
|
|
23063
|
+
const body = await (0, import_promises28.readFile)(candidate);
|
|
21077
23064
|
response.setHeader("cache-control", "no-cache");
|
|
21078
23065
|
sendBinary(response, 200, contentTypeForAsset(candidate), body);
|
|
21079
23066
|
return;
|
|
@@ -21083,7 +23070,7 @@ async function sendMangaAsset(pathname, response) {
|
|
|
21083
23070
|
sendText(response, 404, "Asset not found");
|
|
21084
23071
|
}
|
|
21085
23072
|
function contentTypeForAsset(pathname) {
|
|
21086
|
-
switch ((0,
|
|
23073
|
+
switch ((0, import_node_path36.extname)(pathname).toLowerCase()) {
|
|
21087
23074
|
case ".svg":
|
|
21088
23075
|
return "image/svg+xml; charset=utf-8";
|
|
21089
23076
|
case ".png":
|
|
@@ -21100,14 +23087,14 @@ function contentTypeForAsset(pathname) {
|
|
|
21100
23087
|
}
|
|
21101
23088
|
async function sendBundledReportAsset(pathname, response) {
|
|
21102
23089
|
const rel = decodeURIComponent(pathname.replace(/^\/report\//, ""));
|
|
21103
|
-
const assetRoot = (0,
|
|
21104
|
-
const absolute = (0,
|
|
23090
|
+
const assetRoot = (0, import_node_path36.resolve)(getBundledReportAssetsDir());
|
|
23091
|
+
const absolute = (0, import_node_path36.resolve)(assetRoot, rel);
|
|
21105
23092
|
if (!absolute.startsWith(assetRoot)) {
|
|
21106
23093
|
sendText(response, 403, "Forbidden");
|
|
21107
23094
|
return;
|
|
21108
23095
|
}
|
|
21109
23096
|
try {
|
|
21110
|
-
const body = await (0,
|
|
23097
|
+
const body = await (0, import_promises28.readFile)(absolute);
|
|
21111
23098
|
sendBinary(response, 200, contentTypeForAsset(absolute), body);
|
|
21112
23099
|
} catch {
|
|
21113
23100
|
sendText(response, 404, "Asset not found");
|
|
@@ -21129,18 +23116,18 @@ async function bundleStackClient(entryPath) {
|
|
|
21129
23116
|
return output.text;
|
|
21130
23117
|
}
|
|
21131
23118
|
async function sendStackAppJs(response) {
|
|
21132
|
-
const distPath = (0,
|
|
23119
|
+
const distPath = (0, import_node_path36.join)(__dirname, "stack", "app.js");
|
|
21133
23120
|
try {
|
|
21134
|
-
const body = await (0,
|
|
23121
|
+
const body = await (0, import_promises28.readFile)(distPath);
|
|
21135
23122
|
response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "cache-control": "no-store" });
|
|
21136
23123
|
response.end(body);
|
|
21137
23124
|
return;
|
|
21138
23125
|
} catch {
|
|
21139
23126
|
}
|
|
21140
|
-
const entryPath = (0,
|
|
23127
|
+
const entryPath = (0, import_node_path36.join)(__dirname, "stack", "main.ts");
|
|
21141
23128
|
let mtimeMs;
|
|
21142
23129
|
try {
|
|
21143
|
-
mtimeMs = (await (0,
|
|
23130
|
+
mtimeMs = (await (0, import_promises28.stat)(entryPath)).mtimeMs;
|
|
21144
23131
|
} catch {
|
|
21145
23132
|
sendText(response, 404, "Stack client entry not found: src/local-ui/stack/main.ts is missing.");
|
|
21146
23133
|
return;
|
|
@@ -21159,7 +23146,7 @@ async function sendStackAppJs(response) {
|
|
|
21159
23146
|
}
|
|
21160
23147
|
async function sendStackAppCss(response) {
|
|
21161
23148
|
try {
|
|
21162
|
-
const body = await (0,
|
|
23149
|
+
const body = await (0, import_promises28.readFile)((0, import_node_path36.join)(__dirname, "stack", "stack.css"));
|
|
21163
23150
|
response.writeHead(200, { "content-type": "text/css; charset=utf-8", "cache-control": "no-store" });
|
|
21164
23151
|
response.end(body);
|
|
21165
23152
|
} catch {
|
|
@@ -21168,7 +23155,7 @@ async function sendStackAppCss(response) {
|
|
|
21168
23155
|
}
|
|
21169
23156
|
async function readProviderVerifyLedger(cwd) {
|
|
21170
23157
|
try {
|
|
21171
|
-
const raw = await (0,
|
|
23158
|
+
const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "provider-verify.json"), "utf8");
|
|
21172
23159
|
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
21173
23160
|
return isRecord5(parsed) ? parsed : {};
|
|
21174
23161
|
} catch {
|
|
@@ -21178,8 +23165,8 @@ async function readProviderVerifyLedger(cwd) {
|
|
|
21178
23165
|
async function writeProviderVerifyEntry(cwd, providerId, entry) {
|
|
21179
23166
|
const ledger = await readProviderVerifyLedger(cwd);
|
|
21180
23167
|
ledger[providerId] = entry;
|
|
21181
|
-
await (0,
|
|
21182
|
-
await (0,
|
|
23168
|
+
await (0, import_promises28.mkdir)((0, import_node_path36.join)(cwd, ".viberaven"), { recursive: true });
|
|
23169
|
+
await (0, import_promises28.writeFile)((0, import_node_path36.join)(cwd, ".viberaven", "provider-verify.json"), `${JSON.stringify(ledger, null, 2)}
|
|
21183
23170
|
`, "utf8");
|
|
21184
23171
|
}
|
|
21185
23172
|
async function handleRequest(request, response, options) {
|
|
@@ -21240,13 +23227,13 @@ async function handleRequest(request, response, options) {
|
|
|
21240
23227
|
return;
|
|
21241
23228
|
}
|
|
21242
23229
|
if (request.method === "GET" && url.pathname === "/api/architecture-map") {
|
|
21243
|
-
const mapPath = (0,
|
|
21244
|
-
const markdown = await (0,
|
|
23230
|
+
const mapPath = (0, import_node_path36.join)(options.cwd, ".viberaven", "architecture-map.md");
|
|
23231
|
+
const markdown = await (0, import_promises28.readFile)(mapPath, "utf8").catch(() => null);
|
|
21245
23232
|
if (markdown === null) {
|
|
21246
23233
|
sendJsonStatus(response, 404, { error: "No architecture map yet. Run a scan first (it is written on every scan)." });
|
|
21247
23234
|
return;
|
|
21248
23235
|
}
|
|
21249
|
-
const dataRaw = await (0,
|
|
23236
|
+
const dataRaw = await (0, import_promises28.readFile)((0, import_node_path36.join)(options.cwd, ".viberaven", "architecture-map.json"), "utf8").catch(() => null);
|
|
21250
23237
|
let data = null;
|
|
21251
23238
|
if (dataRaw) {
|
|
21252
23239
|
try {
|
|
@@ -21255,7 +23242,7 @@ async function handleRequest(request, response, options) {
|
|
|
21255
23242
|
data = null;
|
|
21256
23243
|
}
|
|
21257
23244
|
}
|
|
21258
|
-
const contextRaw = await (0,
|
|
23245
|
+
const contextRaw = await (0, import_promises28.readFile)((0, import_node_path36.join)(options.cwd, ".viberaven", "context-map.json"), "utf8").catch(() => null);
|
|
21259
23246
|
let gaps = [];
|
|
21260
23247
|
if (contextRaw) {
|
|
21261
23248
|
try {
|
|
@@ -21521,35 +23508,35 @@ async function handleRequest(request, response, options) {
|
|
|
21521
23508
|
return;
|
|
21522
23509
|
}
|
|
21523
23510
|
if (request.method === "GET" && url.pathname === "/api/artifacts/tasklist") {
|
|
21524
|
-
await sendArtifact(options.cwd, (0,
|
|
23511
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "agent-tasklist.md"), response);
|
|
21525
23512
|
return;
|
|
21526
23513
|
}
|
|
21527
23514
|
if (request.method === "GET" && url.pathname === "/api/artifacts/playbook") {
|
|
21528
|
-
await sendArtifact(options.cwd, (0,
|
|
23515
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "launch-playbook.md"), response);
|
|
21529
23516
|
return;
|
|
21530
23517
|
}
|
|
21531
23518
|
if (request.method === "GET" && url.pathname === "/api/artifacts/audit") {
|
|
21532
|
-
await sendArtifact(options.cwd, (0,
|
|
23519
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "audit-vercel-supabase.txt"), response);
|
|
21533
23520
|
return;
|
|
21534
23521
|
}
|
|
21535
23522
|
if (request.method === "GET" && url.pathname === "/api/artifacts/cleanup") {
|
|
21536
|
-
await sendArtifact(options.cwd, (0,
|
|
23523
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "context-cleanup.md"), response);
|
|
21537
23524
|
return;
|
|
21538
23525
|
}
|
|
21539
23526
|
if (request.method === "GET" && url.pathname === "/api/artifacts/preview") {
|
|
21540
|
-
await sendArtifact(options.cwd, (0,
|
|
23527
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "preview-rehearsal.md"), response);
|
|
21541
23528
|
return;
|
|
21542
23529
|
}
|
|
21543
23530
|
if (request.method === "GET" && url.pathname === "/api/artifacts/heal-plan") {
|
|
21544
|
-
await sendArtifact(options.cwd, (0,
|
|
23531
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "heal-plan.md"), response);
|
|
21545
23532
|
return;
|
|
21546
23533
|
}
|
|
21547
23534
|
if (request.method === "GET" && url.pathname === "/api/artifacts/heal-prompt") {
|
|
21548
|
-
await sendArtifact(options.cwd, (0,
|
|
23535
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "heal-prompt.md"), response);
|
|
21549
23536
|
return;
|
|
21550
23537
|
}
|
|
21551
23538
|
if (request.method === "GET" && url.pathname === "/api/artifacts/heal-apply") {
|
|
21552
|
-
await sendArtifact(options.cwd, (0,
|
|
23539
|
+
await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "heal-apply-result.json"), response, "application/json; charset=utf-8");
|
|
21553
23540
|
return;
|
|
21554
23541
|
}
|
|
21555
23542
|
if (request.method === "GET" && url.pathname === "/api/artifacts/report") {
|
|
@@ -21575,12 +23562,12 @@ async function handleRequest(request, response, options) {
|
|
|
21575
23562
|
}
|
|
21576
23563
|
if (request.method === "GET" && url.pathname === "/vendor/anime.umd.min.js") {
|
|
21577
23564
|
for (const candidate of [
|
|
21578
|
-
(0,
|
|
21579
|
-
(0,
|
|
21580
|
-
(0,
|
|
23565
|
+
(0, import_node_path36.join)(__dirname, "vendor", "anime.umd.min.js"),
|
|
23566
|
+
(0, import_node_path36.join)(__dirname, "..", "assets", "vendor", "anime.umd.min.js"),
|
|
23567
|
+
(0, import_node_path36.join)(__dirname, "..", "..", "assets", "vendor", "anime.umd.min.js")
|
|
21581
23568
|
]) {
|
|
21582
23569
|
try {
|
|
21583
|
-
const body = await (0,
|
|
23570
|
+
const body = await (0, import_promises28.readFile)(candidate);
|
|
21584
23571
|
response.setHeader("cache-control", "public, max-age=604800, immutable");
|
|
21585
23572
|
sendBinary(response, 200, "text/javascript; charset=utf-8", body);
|
|
21586
23573
|
return;
|
|
@@ -21797,11 +23784,11 @@ function listen(server, startPort) {
|
|
|
21797
23784
|
});
|
|
21798
23785
|
}
|
|
21799
23786
|
async function startLocalUiServer(options) {
|
|
21800
|
-
const cwd = (0,
|
|
23787
|
+
const cwd = (0, import_node_path36.resolve)(options.cwd);
|
|
21801
23788
|
const scanRunner = options.scanRunner ?? defaultScanRunner;
|
|
21802
23789
|
const autoScan = { state: "idle" };
|
|
21803
23790
|
const fullCheck = new FullCheckService(cwd, options.fullCheck);
|
|
21804
|
-
const guard = { token: (0,
|
|
23791
|
+
const guard = { token: (0, import_node_crypto5.randomBytes)(32).toString("base64url"), port: 0 };
|
|
21805
23792
|
const server = (0, import_node_http.createServer)((request, response) => {
|
|
21806
23793
|
const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
|
21807
23794
|
if (!guardRequest(request, response, pathname, guard)) {
|
|
@@ -21814,7 +23801,7 @@ async function startLocalUiServer(options) {
|
|
|
21814
23801
|
});
|
|
21815
23802
|
const port = await listen(server, options.port ?? 4177);
|
|
21816
23803
|
guard.port = port;
|
|
21817
|
-
const hasArtifact = await (0,
|
|
23804
|
+
const hasArtifact = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "last-scan.json"), "utf8").then(
|
|
21818
23805
|
() => true,
|
|
21819
23806
|
() => false
|
|
21820
23807
|
);
|
|
@@ -21853,16 +23840,16 @@ async function waitForServerShutdown() {
|
|
|
21853
23840
|
process.once("SIGTERM", done);
|
|
21854
23841
|
});
|
|
21855
23842
|
}
|
|
21856
|
-
var import_node_http,
|
|
23843
|
+
var import_node_http, import_node_child_process8, import_node_crypto5, import_promises28, import_node_os2, import_node_path36, CLI_AGENT_SETUP, MCP_CONFIG_PATHS, MCP_PROVIDER_NEEDLES, MCP_CONNECT_COMMANDS, MCP_SERVER_RECIPES, STUDIO_TOKEN_HEADER, PROVIDER_IDS, STUDIO_STATUS_ARGS, CLI_AUTH_PROBE, AGENT_RUN_TIMEOUT_MS, activeAgentRuns, stackBundleCache;
|
|
21857
23844
|
var init_server = __esm({
|
|
21858
23845
|
"src/local-ui/server.ts"() {
|
|
21859
23846
|
"use strict";
|
|
21860
23847
|
import_node_http = require("node:http");
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
23848
|
+
import_node_child_process8 = require("node:child_process");
|
|
23849
|
+
import_node_crypto5 = require("node:crypto");
|
|
23850
|
+
import_promises28 = require("node:fs/promises");
|
|
21864
23851
|
import_node_os2 = require("node:os");
|
|
21865
|
-
|
|
23852
|
+
import_node_path36 = require("node:path");
|
|
21866
23853
|
init_cleanPlan();
|
|
21867
23854
|
init_audit();
|
|
21868
23855
|
init_heal();
|
|
@@ -21903,8 +23890,8 @@ var init_server = __esm({
|
|
|
21903
23890
|
MCP_CONFIG_PATHS = [
|
|
21904
23891
|
".mcp.json",
|
|
21905
23892
|
"mcp.json",
|
|
21906
|
-
(0,
|
|
21907
|
-
(0,
|
|
23893
|
+
(0, import_node_path36.join)(".cursor", "mcp.json"),
|
|
23894
|
+
(0, import_node_path36.join)(".vscode", "mcp.json")
|
|
21908
23895
|
];
|
|
21909
23896
|
MCP_PROVIDER_NEEDLES = {
|
|
21910
23897
|
supabase: ["supabase", "@supabase/mcp", "mcp-server-supabase"],
|
|
@@ -22063,9 +24050,9 @@ var init_nextActionBlock = __esm({
|
|
|
22063
24050
|
function defaultMcpConfigPaths() {
|
|
22064
24051
|
const home = (0, import_node_os3.homedir)();
|
|
22065
24052
|
return [
|
|
22066
|
-
(0,
|
|
22067
|
-
(0,
|
|
22068
|
-
(0,
|
|
24053
|
+
(0, import_node_path37.join)(home, ".config", "claude", "claude_desktop_config.json"),
|
|
24054
|
+
(0, import_node_path37.join)(home, ".cursor", "mcp.json"),
|
|
24055
|
+
(0, import_node_path37.join)(home, ".gemini", "antigravity", "mcp_config.json")
|
|
22069
24056
|
];
|
|
22070
24057
|
}
|
|
22071
24058
|
function resolveConfigPaths() {
|
|
@@ -22142,13 +24129,13 @@ async function verifyProviderGap(options) {
|
|
|
22142
24129
|
fallbackCommand: FALLBACK_COMMAND
|
|
22143
24130
|
};
|
|
22144
24131
|
}
|
|
22145
|
-
var import_node_fs13, import_node_os3,
|
|
24132
|
+
var import_node_fs13, import_node_os3, import_node_path37, FALLBACK_COMMAND, SUPPORTED_PROVIDERS, configPathsOverride;
|
|
22146
24133
|
var init_providerMcpBridge = __esm({
|
|
22147
24134
|
"src/providerMcpBridge.ts"() {
|
|
22148
24135
|
"use strict";
|
|
22149
24136
|
import_node_fs13 = require("node:fs");
|
|
22150
24137
|
import_node_os3 = require("node:os");
|
|
22151
|
-
|
|
24138
|
+
import_node_path37 = require("node:path");
|
|
22152
24139
|
FALLBACK_COMMAND = "npx -y viberaven audit --vercel-supabase --json";
|
|
22153
24140
|
SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set(["supabase", "vercel"]);
|
|
22154
24141
|
}
|
|
@@ -22232,6 +24219,13 @@ Usage:
|
|
|
22232
24219
|
viberaven audit --vercel-supabase [--json] [path]
|
|
22233
24220
|
Local Vercel/Supabase repo evidence audit (RLS, pooler, secrets)
|
|
22234
24221
|
|
|
24222
|
+
viberaven clean --plan [path]
|
|
24223
|
+
Write a non-destructive context cleanup review plan (.viberaven/context-cleanup.md)
|
|
24224
|
+
|
|
24225
|
+
viberaven rls-test [--matrix viberaven.permissions.json] [--migrations dir] [--json] [path]
|
|
24226
|
+
Experimental: run the Supabase migrations in a PGlite imitation and test who can
|
|
24227
|
+
read, change and delete rows against your permissions matrix (non-authoritative)
|
|
24228
|
+
|
|
22235
24229
|
|
|
22236
24230
|
|
|
22237
24231
|
Agent workflow (Claude Code / Codex / Gemini):
|
|
@@ -22352,7 +24346,7 @@ async function cmdProviderVerify(flags, positional) {
|
|
|
22352
24346
|
return result.verified ? 0 : 1;
|
|
22353
24347
|
}
|
|
22354
24348
|
async function cmdStatus(flags, positional) {
|
|
22355
|
-
const startDir = positional[0] ? (0,
|
|
24349
|
+
const startDir = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22356
24350
|
let artifact;
|
|
22357
24351
|
try {
|
|
22358
24352
|
artifact = await loadLastArtifact(startDir);
|
|
@@ -22391,7 +24385,7 @@ async function cmdStatus(flags, positional) {
|
|
|
22391
24385
|
return 0;
|
|
22392
24386
|
}
|
|
22393
24387
|
async function runScanCommand(flags, positional, options) {
|
|
22394
|
-
const workspacePath = positional[0] ? (0,
|
|
24388
|
+
const workspacePath = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : await resolveWorkspaceRoot(process.cwd());
|
|
22395
24389
|
if (!options?.deferMachineOutput) {
|
|
22396
24390
|
console.log(`Scanning ${workspacePath}\u2026 (local, offline)`);
|
|
22397
24391
|
}
|
|
@@ -22418,7 +24412,7 @@ async function runScanCommand(flags, positional, options) {
|
|
|
22418
24412
|
printNextActionBlock(block);
|
|
22419
24413
|
printProviderActionBlock(tasks);
|
|
22420
24414
|
try {
|
|
22421
|
-
const manifest = JSON.parse(await (0,
|
|
24415
|
+
const manifest = JSON.parse(await (0, import_promises29.readFile)(paths.actionsPath, "utf8"));
|
|
22422
24416
|
console.log(renderActionSurface(manifest, { limit: 5 }).trimEnd());
|
|
22423
24417
|
} catch (error) {
|
|
22424
24418
|
console.warn(error instanceof Error ? error.message : String(error));
|
|
@@ -22435,7 +24429,7 @@ async function runScanCommand(flags, positional, options) {
|
|
|
22435
24429
|
return { exitCode: 0, artifacts: paths };
|
|
22436
24430
|
}
|
|
22437
24431
|
async function cmdReport(flags, positional) {
|
|
22438
|
-
const startDir = positional[0] ? (0,
|
|
24432
|
+
const startDir = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22439
24433
|
try {
|
|
22440
24434
|
const paths = await refreshReportFromDisk(startDir);
|
|
22441
24435
|
console.log(`Report refreshed: ${paths.reportPath}`);
|
|
@@ -22457,7 +24451,7 @@ async function cmdReport(flags, positional) {
|
|
|
22457
24451
|
}
|
|
22458
24452
|
}
|
|
22459
24453
|
async function cmdPrompt(flags, positional) {
|
|
22460
|
-
const startDir = positional[0] ? (0,
|
|
24454
|
+
const startDir = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22461
24455
|
let artifact;
|
|
22462
24456
|
try {
|
|
22463
24457
|
artifact = await loadLastArtifact(startDir);
|
|
@@ -22554,7 +24548,7 @@ async function main() {
|
|
|
22554
24548
|
const wantsJsonl = hasFlag(flags, "jsonl");
|
|
22555
24549
|
const wantsStrict = hasFlag(flags, "strict");
|
|
22556
24550
|
if (flags.condense) {
|
|
22557
|
-
const cwd = positional[0] ? (0,
|
|
24551
|
+
const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22558
24552
|
const result = await runCondenseCommand({ cwd });
|
|
22559
24553
|
console.log(`VibeRaven context map refreshed: ${result.contextMapPath}`);
|
|
22560
24554
|
return 0;
|
|
@@ -22574,7 +24568,7 @@ async function main() {
|
|
|
22574
24568
|
}
|
|
22575
24569
|
if (!command && flags.verify === true && typeof flags.action === "string") {
|
|
22576
24570
|
return runVerifyActionCommand({
|
|
22577
|
-
cwd: positional[0] ? (0,
|
|
24571
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
|
|
22578
24572
|
actionId: flags.action
|
|
22579
24573
|
});
|
|
22580
24574
|
}
|
|
@@ -22585,7 +24579,7 @@ async function main() {
|
|
|
22585
24579
|
console.error("VibeRaven could not produce machine output because scan artifacts were not written.");
|
|
22586
24580
|
return 3;
|
|
22587
24581
|
}
|
|
22588
|
-
const gateResult = scanResult.artifacts && (wantsJson || wantsJsonl || wantsStrict) ? JSON.parse(await (0,
|
|
24582
|
+
const gateResult = scanResult.artifacts && (wantsJson || wantsJsonl || wantsStrict) ? JSON.parse(await (0, import_promises29.readFile)(scanResult.artifacts.gateResultPath, "utf8")) : void 0;
|
|
22589
24583
|
const strictExitCode = wantsStrict && gateResult ? exitCodeForStrictGate(gateResult, { failOnWarnings: flags.strict === "warning" }) : scanResult.exitCode;
|
|
22590
24584
|
if (wantsJson && gateResult) {
|
|
22591
24585
|
process.stdout.write(renderGateResultJson(gateResult));
|
|
@@ -22607,7 +24601,7 @@ async function main() {
|
|
|
22607
24601
|
env: process.env
|
|
22608
24602
|
});
|
|
22609
24603
|
if (mode === "local-ui") {
|
|
22610
|
-
const cwd = positional[0] ? (0,
|
|
24604
|
+
const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22611
24605
|
const handle = await startLocalUiServer({ cwd });
|
|
22612
24606
|
console.log(`VibeRaven local UI: ${handle.url}`);
|
|
22613
24607
|
await waitForServerShutdown();
|
|
@@ -22627,7 +24621,7 @@ async function main() {
|
|
|
22627
24621
|
await runInteractiveSession();
|
|
22628
24622
|
return 0;
|
|
22629
24623
|
case "ui": {
|
|
22630
|
-
const cwd = positional[0] ? (0,
|
|
24624
|
+
const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22631
24625
|
const envPort = Number(process.env.PORT);
|
|
22632
24626
|
const handle = await startLocalUiServer({ cwd, port: Number.isFinite(envPort) && envPort > 0 ? envPort : void 0 });
|
|
22633
24627
|
console.log(`VibeRaven local UI: ${handle.url}`);
|
|
@@ -22639,12 +24633,12 @@ async function main() {
|
|
|
22639
24633
|
return cmdStatus(flags, positional);
|
|
22640
24634
|
case "check":
|
|
22641
24635
|
return runCheckCommand({
|
|
22642
|
-
cwd: positional[0] ? (0,
|
|
24636
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
|
|
22643
24637
|
json: Boolean(flags.json),
|
|
22644
24638
|
quiet: Boolean(flags.quiet)
|
|
22645
24639
|
});
|
|
22646
24640
|
case "fix": {
|
|
22647
|
-
const fixCwd = positional[0] ? (0,
|
|
24641
|
+
const fixCwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22648
24642
|
const gapId = typeof flags.gap === "string" ? flags.gap : void 0;
|
|
22649
24643
|
if (!gapId) {
|
|
22650
24644
|
const artifact = await loadLastArtifact(fixCwd).catch(() => void 0);
|
|
@@ -22671,19 +24665,19 @@ async function main() {
|
|
|
22671
24665
|
}
|
|
22672
24666
|
case "actions":
|
|
22673
24667
|
return runActionsCommand({
|
|
22674
|
-
cwd: positional[0] ? (0,
|
|
24668
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
|
|
22675
24669
|
json: Boolean(flags.json)
|
|
22676
24670
|
});
|
|
22677
24671
|
case "preview":
|
|
22678
24672
|
return runPreviewCommand({
|
|
22679
|
-
cwd: positional[0] ? (0,
|
|
24673
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
|
|
22680
24674
|
agentMode: flags["agent-mode"] === true,
|
|
22681
24675
|
json: Boolean(flags.json)
|
|
22682
24676
|
});
|
|
22683
24677
|
case "next":
|
|
22684
24678
|
return runNextCommand({
|
|
22685
24679
|
json: Boolean(flags.json),
|
|
22686
|
-
cwd: positional[0] ? (0,
|
|
24680
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd()
|
|
22687
24681
|
});
|
|
22688
24682
|
case "guide": {
|
|
22689
24683
|
const provider2 = positional[0];
|
|
@@ -22717,7 +24711,7 @@ async function main() {
|
|
|
22717
24711
|
case "provider-verify":
|
|
22718
24712
|
return cmdProviderVerify(flags, positional);
|
|
22719
24713
|
case "init": {
|
|
22720
|
-
const cwd = positional[0] ? (0,
|
|
24714
|
+
const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
22721
24715
|
const agents = typeof flags.agents === "string" ? flags.agents : void 0;
|
|
22722
24716
|
return runInitCommand({
|
|
22723
24717
|
cwd,
|
|
@@ -22731,7 +24725,7 @@ async function main() {
|
|
|
22731
24725
|
return 1;
|
|
22732
24726
|
}
|
|
22733
24727
|
return runDoctorAgentsCommand({
|
|
22734
|
-
cwd: positional[0] ? (0,
|
|
24728
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd()
|
|
22735
24729
|
});
|
|
22736
24730
|
case "validate-npm-package":
|
|
22737
24731
|
return runValidateNpmPackageCommand({
|
|
@@ -22744,7 +24738,27 @@ async function main() {
|
|
|
22744
24738
|
return 1;
|
|
22745
24739
|
}
|
|
22746
24740
|
return runAuditCommand({
|
|
22747
|
-
cwd: positional[0] ? (0,
|
|
24741
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
|
|
24742
|
+
json: Boolean(flags.json)
|
|
24743
|
+
});
|
|
24744
|
+
case "clean": {
|
|
24745
|
+
if (flags.plan !== true) {
|
|
24746
|
+
console.error("Usage: viberaven clean --plan [path]");
|
|
24747
|
+
return 1;
|
|
24748
|
+
}
|
|
24749
|
+
const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
|
|
24750
|
+
const plan = buildContextCleanupPlan({ projectRoot: cwd, files: await collectCleanupFiles(cwd) });
|
|
24751
|
+
const outputPath = await writeCleanupPlan(cwd, plan);
|
|
24752
|
+
console.log(`Wrote ${outputPath}
|
|
24753
|
+
`);
|
|
24754
|
+
console.log(renderCleanupPlan(plan).trimEnd());
|
|
24755
|
+
return 0;
|
|
24756
|
+
}
|
|
24757
|
+
case "rls-test":
|
|
24758
|
+
return runRlsTestCommand({
|
|
24759
|
+
cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
|
|
24760
|
+
matrix: typeof flags.matrix === "string" ? (0, import_node_path38.resolve)(process.cwd(), flags.matrix) : void 0,
|
|
24761
|
+
migrations: typeof flags.migrations === "string" ? (0, import_node_path38.resolve)(process.cwd(), flags.migrations) : void 0,
|
|
22748
24762
|
json: Boolean(flags.json)
|
|
22749
24763
|
});
|
|
22750
24764
|
default:
|
|
@@ -22753,11 +24767,11 @@ async function main() {
|
|
|
22753
24767
|
return 1;
|
|
22754
24768
|
}
|
|
22755
24769
|
}
|
|
22756
|
-
var
|
|
24770
|
+
var import_promises29, import_node_path38, STACK_AREAS;
|
|
22757
24771
|
var init_cli = __esm({
|
|
22758
24772
|
"src/cli.ts"() {
|
|
22759
|
-
|
|
22760
|
-
|
|
24773
|
+
import_promises29 = require("node:fs/promises");
|
|
24774
|
+
import_node_path38 = require("node:path");
|
|
22761
24775
|
init_config();
|
|
22762
24776
|
init_runScan();
|
|
22763
24777
|
init_artifacts2();
|
|
@@ -22784,10 +24798,12 @@ var init_cli = __esm({
|
|
|
22784
24798
|
init_runDoctor();
|
|
22785
24799
|
init_runValidateNpmPackage();
|
|
22786
24800
|
init_runAudit();
|
|
24801
|
+
init_cleanPlan();
|
|
22787
24802
|
init_actions();
|
|
22788
24803
|
init_verifyAction();
|
|
22789
24804
|
init_preview();
|
|
22790
24805
|
init_check();
|
|
24806
|
+
init_rlsTest();
|
|
22791
24807
|
init_server();
|
|
22792
24808
|
init_version();
|
|
22793
24809
|
init_loopState();
|