sidecarsync 2.1.1 → 2.1.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/dist/cli.js +262 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1449,15 +1449,142 @@ var init_git = __esm(() => {
|
|
|
1449
1449
|
});
|
|
1450
1450
|
|
|
1451
1451
|
// src/redaction.ts
|
|
1452
|
+
function redactShellDefaults(text) {
|
|
1453
|
+
let output = "";
|
|
1454
|
+
let copied = 0;
|
|
1455
|
+
for (const match of text.matchAll(SHELL_DEFAULT_REGEX)) {
|
|
1456
|
+
const [whole, open, name, operator] = match;
|
|
1457
|
+
if (match.index < copied)
|
|
1458
|
+
continue;
|
|
1459
|
+
if (SHELL_DIRECTORY_NAMES.has(name) || !isSensitiveKey(name))
|
|
1460
|
+
continue;
|
|
1461
|
+
const wordStart = match.index + whole.length;
|
|
1462
|
+
const end = shellWordEnd(text, wordStart, "}");
|
|
1463
|
+
const wordEnd = end === -1 ? lineEnd(text, wordStart) : end;
|
|
1464
|
+
const word = text.slice(wordStart, wordEnd);
|
|
1465
|
+
output += text.slice(copied, wordStart);
|
|
1466
|
+
output += !word || isPlaceholder(word) || isShellReference(word) ? word : placeholderForKey(name);
|
|
1467
|
+
copied = wordEnd;
|
|
1468
|
+
}
|
|
1469
|
+
return output + text.slice(copied);
|
|
1470
|
+
}
|
|
1471
|
+
function shellWordEnd(text, start, close, depth = 0) {
|
|
1472
|
+
if (depth > SHELL_NESTING_LIMIT)
|
|
1473
|
+
return -1;
|
|
1474
|
+
let parens = 0;
|
|
1475
|
+
for (let index = start;index < text.length; index += 1) {
|
|
1476
|
+
const char = text[index];
|
|
1477
|
+
if (char === `
|
|
1478
|
+
` || char === "\r")
|
|
1479
|
+
return -1;
|
|
1480
|
+
if (char === "\\") {
|
|
1481
|
+
index += 1;
|
|
1482
|
+
} else if (char === "'" || char === '"' || char === "`") {
|
|
1483
|
+
index = closingQuote(text, index + 1, char, depth + 1);
|
|
1484
|
+
if (index === -1)
|
|
1485
|
+
return -1;
|
|
1486
|
+
} else if (char === "$" && (text[index + 1] === "{" || text[index + 1] === "(")) {
|
|
1487
|
+
index = shellWordEnd(text, index + 2, text[index + 1] === "{" ? "}" : ")", depth + 1);
|
|
1488
|
+
if (index === -1)
|
|
1489
|
+
return -1;
|
|
1490
|
+
} else if (close === ")" && char === "(") {
|
|
1491
|
+
parens += 1;
|
|
1492
|
+
} else if (close === ")" && char === ")" && parens > 0) {
|
|
1493
|
+
parens -= 1;
|
|
1494
|
+
} else if (char === close) {
|
|
1495
|
+
return index;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return -1;
|
|
1499
|
+
}
|
|
1500
|
+
function closingQuote(text, start, quote, depth) {
|
|
1501
|
+
if (depth > SHELL_NESTING_LIMIT)
|
|
1502
|
+
return -1;
|
|
1503
|
+
for (let index = start;index < text.length; index += 1) {
|
|
1504
|
+
const char = text[index];
|
|
1505
|
+
if (char === `
|
|
1506
|
+
` || char === "\r")
|
|
1507
|
+
return -1;
|
|
1508
|
+
if (char === quote)
|
|
1509
|
+
return index;
|
|
1510
|
+
if (quote === "'")
|
|
1511
|
+
continue;
|
|
1512
|
+
if (char === "\\") {
|
|
1513
|
+
index += 1;
|
|
1514
|
+
} else if (quote === '"' && char === "$" && (text[index + 1] === "{" || text[index + 1] === "(")) {
|
|
1515
|
+
index = shellWordEnd(text, index + 2, text[index + 1] === "{" ? "}" : ")", depth + 1);
|
|
1516
|
+
if (index === -1)
|
|
1517
|
+
return -1;
|
|
1518
|
+
} else if (quote === '"' && char === "`") {
|
|
1519
|
+
index = closingQuote(text, index + 1, "`", depth + 1);
|
|
1520
|
+
if (index === -1)
|
|
1521
|
+
return -1;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return -1;
|
|
1525
|
+
}
|
|
1526
|
+
function lineEnd(text, start) {
|
|
1527
|
+
const match = /[\r\n]/.exec(text.slice(start));
|
|
1528
|
+
return match ? start + match.index : text.length;
|
|
1529
|
+
}
|
|
1530
|
+
function isShellReadOfKey(text, offset, separator) {
|
|
1531
|
+
if (SHELL_EXPANSION_KEY_PREFIX.test(text.slice(Math.max(0, offset - 64), offset)))
|
|
1532
|
+
return true;
|
|
1533
|
+
return text[offset - 1] === "$" && separator.trim().startsWith(":");
|
|
1534
|
+
}
|
|
1535
|
+
function isShellReference(value, depth = 0) {
|
|
1536
|
+
if (depth > SHELL_NESTING_LIMIT)
|
|
1537
|
+
return false;
|
|
1538
|
+
const unquoted = value.length > 1 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
1539
|
+
if (SHELL_VARIABLE_REFERENCE.test(unquoted))
|
|
1540
|
+
return true;
|
|
1541
|
+
if (!unquoted.startsWith("${") || shellWordEnd(unquoted, 2, "}") !== unquoted.length - 1)
|
|
1542
|
+
return false;
|
|
1543
|
+
const rest = SHELL_EXPANSION_INNER.exec(unquoted.slice(2, -1))?.groups?.rest;
|
|
1544
|
+
if (rest === undefined)
|
|
1545
|
+
return false;
|
|
1546
|
+
if (rest === "" || /^:?\?/.test(rest) || /^(?::[a-zA-Z0-9]+)+$/.test(rest))
|
|
1547
|
+
return true;
|
|
1548
|
+
const word = /^:?[-=+](?<word>.*)$/s.exec(rest)?.groups?.word;
|
|
1549
|
+
return word !== undefined && (word === "" || isPlaceholder(word) || isShellReference(word, depth + 1));
|
|
1550
|
+
}
|
|
1551
|
+
function startsWithShellReference(text, start, value) {
|
|
1552
|
+
if (SHELL_COMMAND_NAME_VALUE.test(value))
|
|
1553
|
+
return true;
|
|
1554
|
+
if (text[start] !== "$")
|
|
1555
|
+
return false;
|
|
1556
|
+
let end;
|
|
1557
|
+
if (text[start + 1] === "{") {
|
|
1558
|
+
end = shellWordEnd(text, start + 2, "}");
|
|
1559
|
+
if (end === -1)
|
|
1560
|
+
return false;
|
|
1561
|
+
end += 1;
|
|
1562
|
+
} else {
|
|
1563
|
+
end = start + 1 + (/^[A-Za-z0-9_]*/.exec(text.slice(start + 1))?.[0].length ?? 0);
|
|
1564
|
+
}
|
|
1565
|
+
if (end < text.length && !/[\s;&|)]/.test(text[end]))
|
|
1566
|
+
return false;
|
|
1567
|
+
return isShellReference(text.slice(start, end));
|
|
1568
|
+
}
|
|
1452
1569
|
function redactText(input, mode = DEFAULT_REDACTION_MODE) {
|
|
1453
1570
|
if (mode === "none")
|
|
1454
1571
|
return input;
|
|
1455
|
-
let output = input.replace(PEM_PRIVATE_KEY_REGEX, "<PRIVATEKEY>").replace(AUTHORIZATION_HEADER_REGEX, (_match, prefix) => `${prefix}<TOKEN>`).replace(BARE_BEARER_TOKEN_REGEX, (_match, prefix) => `${prefix}<TOKEN>`).replace(URL_CREDENTIALS_REGEX, (_match, prefix) => `${prefix}:<SECRET>@`).replace(QUOTED_SECRET_REGEX, (match, ...args) => {
|
|
1456
|
-
const { keyQuote = "", key, separator, valueQuote } = args.at(-1);
|
|
1457
|
-
|
|
1572
|
+
let output = redactShellDefaults(input).replace(PEM_PRIVATE_KEY_REGEX, "<PRIVATEKEY>").replace(AUTHORIZATION_HEADER_REGEX, (_match, prefix) => `${prefix}<TOKEN>`).replace(BARE_BEARER_TOKEN_REGEX, (_match, prefix) => `${prefix}<TOKEN>`).replace(URL_CREDENTIALS_REGEX, (_match, prefix) => `${prefix}:<SECRET>@`).replace(QUOTED_SECRET_REGEX, (match, ...args) => {
|
|
1573
|
+
const { keyQuote = "", key, separator, valueQuote, value } = args.at(-1);
|
|
1574
|
+
const offset = args.at(-3);
|
|
1575
|
+
const text = args.at(-2);
|
|
1576
|
+
if (!isSensitiveKey(key) || isShellReadOfKey(text, offset + keyQuote.length, separator))
|
|
1577
|
+
return match;
|
|
1578
|
+
if (valueQuote === '"' && isShellReference(value))
|
|
1458
1579
|
return match;
|
|
1459
1580
|
return `${keyQuote}${key}${keyQuote}${separator}${valueQuote}${placeholderForKey(key)}${valueQuote}`;
|
|
1460
|
-
}).replace(BARE_ASSIGNMENT_SECRET_REGEX, (match, key, separator
|
|
1581
|
+
}).replace(BARE_ASSIGNMENT_SECRET_REGEX, (match, key, separator, value, offset, text) => {
|
|
1582
|
+
if (!isSensitiveKey(key) || isShellReadOfKey(text, offset, separator))
|
|
1583
|
+
return match;
|
|
1584
|
+
if (startsWithShellReference(text, offset + key.length + separator.length, value))
|
|
1585
|
+
return match;
|
|
1586
|
+
return `${key}${separator}${placeholderForKey(key)}`;
|
|
1587
|
+
});
|
|
1461
1588
|
for (const [pattern, replacement] of TOKEN_PATTERNS) {
|
|
1462
1589
|
output = output.replace(pattern, replacement);
|
|
1463
1590
|
}
|
|
@@ -1465,6 +1592,12 @@ function redactText(input, mode = DEFAULT_REDACTION_MODE) {
|
|
|
1465
1592
|
return output;
|
|
1466
1593
|
return output.replace(EMAIL_REGEX, "<EMAIL>").replace(PHONE_REGEX, "<PHONENUMBER>").replace(SSN_REGEX, "<SSN>").replace(CREDIT_CARD_CANDIDATE_REGEX, (candidate) => isLikelyCreditCard(candidate) ? "<CREDITCARD>" : candidate);
|
|
1467
1594
|
}
|
|
1595
|
+
function isPlaceholder(text) {
|
|
1596
|
+
return /^<(?:API_KEY|TOKEN|SECRET|PRIVATEKEY|EMAIL|PHONENUMBER|SSN|CREDITCARD)>$/.test(text);
|
|
1597
|
+
}
|
|
1598
|
+
function hasRedactionPlaceholder(text) {
|
|
1599
|
+
return countRedactionPlaceholders(text) > 0;
|
|
1600
|
+
}
|
|
1468
1601
|
function countRedactionPlaceholders(text) {
|
|
1469
1602
|
return text.match(PLACEHOLDER_REGEX)?.length ?? 0;
|
|
1470
1603
|
}
|
|
@@ -1518,12 +1651,20 @@ function isLikelyCreditCard(value) {
|
|
|
1518
1651
|
}
|
|
1519
1652
|
return sum % 10 === 0;
|
|
1520
1653
|
}
|
|
1521
|
-
var DEFAULT_REDACTION_MODE = "secrets", REDACTION_MODES, KEY_NAME_PATTERN, QUOTED_SECRET_REGEX, BARE_ASSIGNMENT_SECRET_REGEX, AUTHORIZATION_HEADER_REGEX, PEM_PRIVATE_KEY_REGEX, URL_CREDENTIALS_REGEX, BARE_BEARER_TOKEN_REGEX, TOKEN_PATTERNS, EMAIL_REGEX, PHONE_REGEX, SSN_REGEX, CREDIT_CARD_CANDIDATE_REGEX, PLACEHOLDER_REGEX, COMPACT_SENSITIVE_KEYS;
|
|
1654
|
+
var DEFAULT_REDACTION_MODE = "secrets", REDACTION_MODES, KEY_NAME_PATTERN, QUOTED_SECRET_REGEX, BARE_ASSIGNMENT_SECRET_REGEX, SHELL_NAME_PATTERN, SHELL_EXPANSION_OPEN, SHELL_DEFAULT_REGEX, SHELL_EXPANSION_KEY_PREFIX, SHELL_EXPANSION_INNER, SHELL_DIRECTORY_NAMES, SHELL_VARIABLE_REFERENCE, SHELL_NESTING_LIMIT = 16, SHELL_COMMAND_NAME_VALUE, AUTHORIZATION_HEADER_REGEX, PEM_PRIVATE_KEY_REGEX, URL_CREDENTIALS_REGEX, BARE_BEARER_TOKEN_REGEX, TOKEN_PATTERNS, EMAIL_REGEX, PHONE_REGEX, SSN_REGEX, CREDIT_CARD_CANDIDATE_REGEX, PLACEHOLDER_REGEX, COMPACT_SENSITIVE_KEYS;
|
|
1522
1655
|
var init_redaction = __esm(() => {
|
|
1523
1656
|
REDACTION_MODES = ["none", "secrets", "secrets+pii"];
|
|
1524
1657
|
KEY_NAME_PATTERN = String.raw`[A-Za-z0-9_][A-Za-z0-9_-]*`;
|
|
1525
|
-
QUOTED_SECRET_REGEX = new RegExp(String.raw`(?:(?<keyQuote>["'])|\b)(?<key>${KEY_NAME_PATTERN})\k<keyQuote>` + String.raw`(?<separator>\s*[:=]\s*)(?<valueQuote>["'])(?:\\[^\r\n]|(?!\k<valueQuote>)[^\\\r\n])
|
|
1658
|
+
QUOTED_SECRET_REGEX = new RegExp(String.raw`(?:(?<keyQuote>["'])|\b)(?<key>${KEY_NAME_PATTERN})\k<keyQuote>` + String.raw`(?<separator>\s*[:=]\s*)(?<valueQuote>["'])(?<value>(?:\\[^\r\n]|(?!\k<valueQuote>)[^\\\r\n])+)\k<valueQuote>`, "g");
|
|
1526
1659
|
BARE_ASSIGNMENT_SECRET_REGEX = new RegExp(String.raw`\b(${KEY_NAME_PATTERN})(\s*[:=]\s*)([^\s"',;` + "`" + String.raw`]+)`, "g");
|
|
1660
|
+
SHELL_NAME_PATTERN = String.raw`[A-Za-z_][A-Za-z0-9_]*`;
|
|
1661
|
+
SHELL_EXPANSION_OPEN = String.raw`\$\{(?:\([^()\s]*\))?[#!^=~]*`;
|
|
1662
|
+
SHELL_DEFAULT_REGEX = new RegExp(String.raw`(${SHELL_EXPANSION_OPEN})(${SHELL_NAME_PATTERN})(:?[-=+])`, "g");
|
|
1663
|
+
SHELL_EXPANSION_KEY_PREFIX = new RegExp(`${SHELL_EXPANSION_OPEN}$`);
|
|
1664
|
+
SHELL_EXPANSION_INNER = new RegExp(String.raw`^(?:\([^()\s]*\))?[#!^=~]*${SHELL_NAME_PATTERN}(?<rest>.*)$`, "s");
|
|
1665
|
+
SHELL_DIRECTORY_NAMES = new Set(["PWD", "OLDPWD"]);
|
|
1666
|
+
SHELL_VARIABLE_REFERENCE = new RegExp(String.raw`^\$(?:[A-Z_][A-Z0-9_]*)$`);
|
|
1667
|
+
SHELL_COMMAND_NAME_VALUE = /^\$\((?:[A-Za-z0-9_./+-]+|<(?:API_KEY|TOKEN|SECRET|PRIVATEKEY|EMAIL|PHONENUMBER|SSN|CREDITCARD)>)\)?$/;
|
|
1527
1668
|
AUTHORIZATION_HEADER_REGEX = /\b(authorization\s*[:=]\s*(?:bearer|basic|token)\s+)([^\s"',;`]+)/gi;
|
|
1528
1669
|
PEM_PRIVATE_KEY_REGEX = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1529
1670
|
URL_CREDENTIALS_REGEX = /(\/\/[^\s/:@"'`]+):([^\s/@"'`]+)@/g;
|
|
@@ -4726,7 +4867,7 @@ function createReview(options) {
|
|
|
4726
4867
|
if (guidelines)
|
|
4727
4868
|
writePrivateFile(path8.join(dir, "guidelines.md"), `${guidelines}
|
|
4728
4869
|
`);
|
|
4729
|
-
writePrivateFile(path8.join(dir, "instructions.md"), reviewInstructions(record, config, guidelines));
|
|
4870
|
+
writePrivateFile(path8.join(dir, "instructions.md"), reviewInstructions(record, config, guidelines, addedPlaceholderLines(diff)));
|
|
4730
4871
|
writePrivateJson(path8.join(dir, "review.json"), record);
|
|
4731
4872
|
logSidecarEvent("review-open", { id, root, peer: config.peer, files: files.length });
|
|
4732
4873
|
pruneClosedReviews(sidecarPath);
|
|
@@ -4785,6 +4926,102 @@ function pruneOrphanedReviews() {
|
|
|
4785
4926
|
fs8.rmSync(review.dir, { recursive: true, force: true });
|
|
4786
4927
|
}
|
|
4787
4928
|
}
|
|
4929
|
+
function addedPlaceholderLines(patch) {
|
|
4930
|
+
const found = [];
|
|
4931
|
+
let file;
|
|
4932
|
+
let oldLeft = 0;
|
|
4933
|
+
let newLeft = 0;
|
|
4934
|
+
let newLine = 0;
|
|
4935
|
+
for (const raw of patch.split(`
|
|
4936
|
+
`)) {
|
|
4937
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
4938
|
+
if (oldLeft > 0 || newLeft > 0) {
|
|
4939
|
+
if (line.startsWith("+")) {
|
|
4940
|
+
if (file && hasRedactionPlaceholder(line))
|
|
4941
|
+
found.push({ file, line: newLine, text: line.slice(1) });
|
|
4942
|
+
newLeft -= 1;
|
|
4943
|
+
newLine += 1;
|
|
4944
|
+
} else if (line.startsWith("-")) {
|
|
4945
|
+
oldLeft -= 1;
|
|
4946
|
+
} else if (!line.startsWith("\\")) {
|
|
4947
|
+
oldLeft -= 1;
|
|
4948
|
+
newLeft -= 1;
|
|
4949
|
+
newLine += 1;
|
|
4950
|
+
}
|
|
4951
|
+
continue;
|
|
4952
|
+
}
|
|
4953
|
+
if (line.startsWith("diff --git ")) {
|
|
4954
|
+
file = undefined;
|
|
4955
|
+
} else if (line.startsWith("+++ ")) {
|
|
4956
|
+
file = patchHeaderPath(line.slice(4));
|
|
4957
|
+
} else {
|
|
4958
|
+
const hunk = /^@@ -\d+(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
4959
|
+
if (hunk) {
|
|
4960
|
+
oldLeft = Number(hunk[1] ?? 1);
|
|
4961
|
+
newLine = Number(hunk[2]);
|
|
4962
|
+
newLeft = Number(hunk[3] ?? 1);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
return found;
|
|
4967
|
+
}
|
|
4968
|
+
function patchHeaderPath(header) {
|
|
4969
|
+
const target = header.endsWith("\t") ? header.slice(0, -1) : header;
|
|
4970
|
+
if (target === "/dev/null")
|
|
4971
|
+
return;
|
|
4972
|
+
const name = target.length > 1 && target.startsWith('"') && target.endsWith('"') ? unquoteGitPath(target.slice(1, -1)) : target;
|
|
4973
|
+
return name.replace(/^b\//, "");
|
|
4974
|
+
}
|
|
4975
|
+
function unquoteGitPath(quoted) {
|
|
4976
|
+
const parts = [];
|
|
4977
|
+
let literal = "";
|
|
4978
|
+
for (let index = 0;index < quoted.length; index += 1) {
|
|
4979
|
+
if (quoted[index] !== "\\" || index + 1 === quoted.length) {
|
|
4980
|
+
literal += quoted[index];
|
|
4981
|
+
continue;
|
|
4982
|
+
}
|
|
4983
|
+
parts.push(Buffer.from(literal, "utf8"));
|
|
4984
|
+
literal = "";
|
|
4985
|
+
const octal = /^[0-3][0-7]{2}/.exec(quoted.slice(index + 1))?.[0];
|
|
4986
|
+
if (octal) {
|
|
4987
|
+
parts.push(Buffer.from([Number.parseInt(octal, 8)]));
|
|
4988
|
+
index += 3;
|
|
4989
|
+
} else {
|
|
4990
|
+
const next = quoted[index + 1];
|
|
4991
|
+
parts.push(GIT_PATH_ESCAPES[next] === undefined ? Buffer.from(next, "utf8") : Buffer.from([GIT_PATH_ESCAPES[next]]));
|
|
4992
|
+
index += 1;
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
parts.push(Buffer.from(literal, "utf8"));
|
|
4996
|
+
return Buffer.concat(parts).toString("utf8");
|
|
4997
|
+
}
|
|
4998
|
+
function placeholderSection(lines) {
|
|
4999
|
+
if (!lines.length)
|
|
5000
|
+
return "";
|
|
5001
|
+
const shown = lines.slice(0, PLACEHOLDER_LINES_SHOWN);
|
|
5002
|
+
const more = lines.length - shown.length;
|
|
5003
|
+
return `
|
|
5004
|
+
## Redaction placeholders
|
|
5005
|
+
|
|
5006
|
+
These lines in the change carry a placeholder (numbered as in the new file).
|
|
5007
|
+
For each one, check that the placeholder stands where a credential's value
|
|
5008
|
+
was, and only there. Reject, naming the line, if it replaced something that is
|
|
5009
|
+
not a credential — part of a shell expansion such as \`\${PWD:t}\`, a path, a
|
|
5010
|
+
command, ordinary words — or if the text around it no longer holds together
|
|
5011
|
+
(a script that no longer parses, a broken config file). That holds even when
|
|
5012
|
+
it did replace a real credential: the other machines still receive a broken
|
|
5013
|
+
file, and the credential belongs outside the synced file. Some placeholders
|
|
5014
|
+
may have been typed by hand or come from content already published; judge
|
|
5015
|
+
them the same way.
|
|
5016
|
+
|
|
5017
|
+
${shown.map((entry) => `- ${entry.file}:${entry.line}
|
|
5018
|
+
|
|
5019
|
+
${entry.text}
|
|
5020
|
+
`).join(`
|
|
5021
|
+
`)}${more ? `
|
|
5022
|
+
…and ${more} more; find them in \`diff.patch\`.
|
|
5023
|
+
` : ""}`;
|
|
5024
|
+
}
|
|
4788
5025
|
function opaqueSection(record) {
|
|
4789
5026
|
const opaque = record.files.filter((file) => file.artifact);
|
|
4790
5027
|
if (!opaque.length)
|
|
@@ -4799,7 +5036,7 @@ ${opaque.map((file) => `- \`${file.artifact}\` — ${file.path}`).join(`
|
|
|
4799
5036
|
`)}
|
|
4800
5037
|
`;
|
|
4801
5038
|
}
|
|
4802
|
-
function reviewInstructions(record, config, guidelines) {
|
|
5039
|
+
function reviewInstructions(record, config, guidelines, placeholders) {
|
|
4803
5040
|
return `# Sidecar publish review ${record.id}
|
|
4804
5041
|
|
|
4805
5042
|
A sidecar is a scratchpad repository that syncs automatically to a shared Git
|
|
@@ -4807,8 +5044,9 @@ remote (${redactText(config.remote)}). This machine wants to publish the change
|
|
|
4807
5044
|
\`diff.patch\`. Everyone with access to that remote can read what you approve,
|
|
4808
5045
|
and it stays in the remote's history.
|
|
4809
5046
|
|
|
4810
|
-
Decide whether anything in the change should not leave this machine
|
|
4811
|
-
not a code review: ignore quality,
|
|
5047
|
+
Decide whether anything in the change should not leave this machine, and
|
|
5048
|
+
whether redaction damaged it. This is not a code review: ignore the quality,
|
|
5049
|
+
style, and correctness of what the author wrote.
|
|
4812
5050
|
|
|
4813
5051
|
Secret redaction has already run. Values it caught appear as placeholders such
|
|
4814
5052
|
as \`<API_KEY>\`, \`<TOKEN>\`, or \`<SECRET>\`; they are not leaks. Look for what
|
|
@@ -4819,6 +5057,11 @@ pattern matching misses:
|
|
|
4819
5057
|
- confidential material: customer names and data, unreleased plans, internal-only details
|
|
4820
5058
|
- anything the repository's guidelines below rule out
|
|
4821
5059
|
|
|
5060
|
+
Pattern matching also has false positives, and they are not harmless. A
|
|
5061
|
+
placeholder replaces the original text in the published file; every other
|
|
5062
|
+
machine that syncs this repository gets the placeholder in its own copy, while
|
|
5063
|
+
this machine keeps the original, so the author never sees the damage.
|
|
5064
|
+
${placeholderSection(placeholders)}
|
|
4822
5065
|
## Files
|
|
4823
5066
|
|
|
4824
5067
|
All in this directory (\`$SIDECAR_REVIEW_DIR\`):
|
|
@@ -4916,7 +5159,7 @@ function decideReview(review, verdict, reason) {
|
|
|
4916
5159
|
review.state = verdict;
|
|
4917
5160
|
logSidecarEvent("review-decision", { id: review.record.id, verdict });
|
|
4918
5161
|
}
|
|
4919
|
-
var REVIEW_SCHEMA = 1, KEEP_CLOSED_REVIEWS = 10, ID_PATTERN, LAUNCH_GRACE_SECONDS = 60, PACKET_DIFF_OPTIONS;
|
|
5162
|
+
var REVIEW_SCHEMA = 1, KEEP_CLOSED_REVIEWS = 10, ID_PATTERN, LAUNCH_GRACE_SECONDS = 60, PACKET_DIFF_OPTIONS, GIT_PATH_ESCAPES, PLACEHOLDER_LINES_SHOWN = 200;
|
|
4920
5163
|
var init_review = __esm(() => {
|
|
4921
5164
|
init_util();
|
|
4922
5165
|
init_git();
|
|
@@ -4926,6 +5169,7 @@ var init_review = __esm(() => {
|
|
|
4926
5169
|
init_redaction();
|
|
4927
5170
|
ID_PATTERN = /^[0-9a-f]+$/;
|
|
4928
5171
|
PACKET_DIFF_OPTIONS = ["-r", "--no-renames", "--ignore-submodules=none"];
|
|
5172
|
+
GIT_PATH_ESCAPES = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13, '"': 34, "\\": 92 };
|
|
4929
5173
|
});
|
|
4930
5174
|
|
|
4931
5175
|
// src/sync.ts
|
|
@@ -6274,7 +6518,7 @@ function pendingInboxBranches(repo, config) {
|
|
|
6274
6518
|
});
|
|
6275
6519
|
return [...local, ...remote].sort();
|
|
6276
6520
|
}
|
|
6277
|
-
var REVIEW_POLICY_KEY = "sidecar.review", WRITTEN_TRAILER = "written:", WRITTEN_TRAILER_LIMIT = 500, REDACTION_FILTER_NAME = "sidecar-redact", REDACTION_FILTER_REVISION = "
|
|
6521
|
+
var REVIEW_POLICY_KEY = "sidecar.review", WRITTEN_TRAILER = "written:", WRITTEN_TRAILER_LIMIT = 500, REDACTION_FILTER_NAME = "sidecar-redact", REDACTION_FILTER_REVISION = "3", REDACTION_POLICY_FILE = "sidecar-redaction-policy";
|
|
6278
6522
|
var init_sync = __esm(() => {
|
|
6279
6523
|
init_color();
|
|
6280
6524
|
init_util();
|
|
@@ -8713,11 +8957,14 @@ function armRemoteAt(state, key, at, trigger) {
|
|
|
8713
8957
|
state.remoteTimers.set(key, { timer, at });
|
|
8714
8958
|
}
|
|
8715
8959
|
function armReviewDue(state, key, when) {
|
|
8716
|
-
const at = reviewDueAt(readReviewDue(key), when, Date.now(), scheduleFor(key, state.options).debounceSeconds * 1000);
|
|
8960
|
+
const at = reviewDueAt(readReviewDue(key), when, Date.now(), scheduleFor(key, state.options).debounceSeconds * 1000, comingBack(state, key));
|
|
8717
8961
|
if (at !== undefined)
|
|
8718
8962
|
armRemoteAt(state, key, at, "review-due");
|
|
8719
8963
|
}
|
|
8720
|
-
function
|
|
8964
|
+
function comingBack(state, key) {
|
|
8965
|
+
return state.syncing.has(key) || state.remoteTimers.has(key) || state.cycleCount < (state.skipUntilCycle.get(key) ?? 0);
|
|
8966
|
+
}
|
|
8967
|
+
function reviewDueAt(due, when, now, debounceMs, comingBack2) {
|
|
8721
8968
|
if (due === undefined)
|
|
8722
8969
|
return;
|
|
8723
8970
|
if (due > now)
|
|
@@ -8726,7 +8973,7 @@ function reviewDueAt(due, when, now, debounceMs) {
|
|
|
8726
8973
|
return now;
|
|
8727
8974
|
if (when === "after-sync")
|
|
8728
8975
|
return now + Math.max(SETTLE_WINDOW_MS, debounceMs);
|
|
8729
|
-
return;
|
|
8976
|
+
return comingBack2 ? undefined : now;
|
|
8730
8977
|
}
|
|
8731
8978
|
function remoteIsDue(state, key) {
|
|
8732
8979
|
const last = state.lastRemoteSyncAt.get(key) ?? 0;
|