terminalhire 0.42.5 → 0.42.6
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/bin/jpi-bounties.js +22 -1
- package/dist/bin/jpi-dispatch.js +168 -66
- package/dist/bin/jpi-post.js +138 -64
- package/dist/src/posting-drafts.js +138 -64
- package/package.json +2 -2
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -11885,6 +11885,23 @@ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
|
|
|
11885
11885
|
terminalhire claim ${ref}
|
|
11886
11886
|
${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
|
|
11887
11887
|
}
|
|
11888
|
+
function wrapIndented(text, indent, width) {
|
|
11889
|
+
const pad = " ".repeat(indent);
|
|
11890
|
+
const words2 = String(text).split(/\s+/).filter(Boolean);
|
|
11891
|
+
if (words2.length === 0) return [];
|
|
11892
|
+
const lines = [];
|
|
11893
|
+
let line = "";
|
|
11894
|
+
for (const w of words2) {
|
|
11895
|
+
if (line === "") line = w;
|
|
11896
|
+
else if (line.length + 1 + w.length <= width) line += ` ${w}`;
|
|
11897
|
+
else {
|
|
11898
|
+
lines.push(pad + line);
|
|
11899
|
+
line = w;
|
|
11900
|
+
}
|
|
11901
|
+
}
|
|
11902
|
+
lines.push(pad + line);
|
|
11903
|
+
return lines;
|
|
11904
|
+
}
|
|
11888
11905
|
function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
|
|
11889
11906
|
const b = job.bounty ?? {};
|
|
11890
11907
|
const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
|
|
@@ -11902,6 +11919,9 @@ ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
|
|
|
11902
11919
|
);
|
|
11903
11920
|
if (reason) console.log(` ${reason}`);
|
|
11904
11921
|
if (continuityNote) console.log(` ${continuityNote}`);
|
|
11922
|
+
if (b.publicSummary) {
|
|
11923
|
+
for (const line of wrapIndented(sanitizeText(b.publicSummary), 3, 76)) console.log(line);
|
|
11924
|
+
}
|
|
11905
11925
|
if (b.bountySource === "founder" && b.specProvenance) {
|
|
11906
11926
|
console.log(
|
|
11907
11927
|
` Spec: ${b.specProvenance === "agent_drafted_human_confirmed" ? "agent drafted \xB7 founder confirmed" : "founder authored"}`
|
|
@@ -12140,7 +12160,8 @@ export {
|
|
|
12140
12160
|
isPinnedFounderBounty,
|
|
12141
12161
|
printBounty,
|
|
12142
12162
|
rankBounties,
|
|
12143
|
-
run
|
|
12163
|
+
run,
|
|
12164
|
+
wrapIndented
|
|
12144
12165
|
};
|
|
12145
12166
|
/*! Bundled license information:
|
|
12146
12167
|
|
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -15548,7 +15548,8 @@ __export(jpi_bounties_exports, {
|
|
|
15548
15548
|
isPinnedFounderBounty: () => isPinnedFounderBounty,
|
|
15549
15549
|
printBounty: () => printBounty,
|
|
15550
15550
|
rankBounties: () => rankBounties,
|
|
15551
|
-
run: () => run5
|
|
15551
|
+
run: () => run5,
|
|
15552
|
+
wrapIndented: () => wrapIndented
|
|
15552
15553
|
});
|
|
15553
15554
|
import { readFileSync as readFileSync18 } from "fs";
|
|
15554
15555
|
import { join as join22 } from "path";
|
|
@@ -15607,6 +15608,23 @@ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
|
|
|
15607
15608
|
terminalhire claim ${ref}
|
|
15608
15609
|
${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
|
|
15609
15610
|
}
|
|
15611
|
+
function wrapIndented(text, indent, width) {
|
|
15612
|
+
const pad = " ".repeat(indent);
|
|
15613
|
+
const words2 = String(text).split(/\s+/).filter(Boolean);
|
|
15614
|
+
if (words2.length === 0) return [];
|
|
15615
|
+
const lines = [];
|
|
15616
|
+
let line = "";
|
|
15617
|
+
for (const w of words2) {
|
|
15618
|
+
if (line === "") line = w;
|
|
15619
|
+
else if (line.length + 1 + w.length <= width) line += ` ${w}`;
|
|
15620
|
+
else {
|
|
15621
|
+
lines.push(pad + line);
|
|
15622
|
+
line = w;
|
|
15623
|
+
}
|
|
15624
|
+
}
|
|
15625
|
+
lines.push(pad + line);
|
|
15626
|
+
return lines;
|
|
15627
|
+
}
|
|
15610
15628
|
function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
|
|
15611
15629
|
const b = job.bounty ?? {};
|
|
15612
15630
|
const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
|
|
@@ -15624,6 +15642,9 @@ ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
|
|
|
15624
15642
|
);
|
|
15625
15643
|
if (reason) console.log(` ${reason}`);
|
|
15626
15644
|
if (continuityNote) console.log(` ${continuityNote}`);
|
|
15645
|
+
if (b.publicSummary) {
|
|
15646
|
+
for (const line of wrapIndented(sanitizeText(b.publicSummary), 3, 76)) console.log(line);
|
|
15647
|
+
}
|
|
15627
15648
|
if (b.bountySource === "founder" && b.specProvenance) {
|
|
15628
15649
|
console.log(
|
|
15629
15650
|
` Spec: ${b.specProvenance === "agent_drafted_human_confirmed" ? "agent drafted \xB7 founder confirmed" : "founder authored"}`
|
|
@@ -35118,6 +35139,149 @@ var init_jpi_claim = __esm({
|
|
|
35118
35139
|
}
|
|
35119
35140
|
});
|
|
35120
35141
|
|
|
35142
|
+
// ../../packages/core/dist/secretScan.js
|
|
35143
|
+
function escapeRegExp(value) {
|
|
35144
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35145
|
+
}
|
|
35146
|
+
function findSecrets(value, options = {}) {
|
|
35147
|
+
const { currentHome = "", fixedOnly = false } = options;
|
|
35148
|
+
const found = [];
|
|
35149
|
+
const scan = (pattern, label, severity, span) => {
|
|
35150
|
+
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
35151
|
+
let match2;
|
|
35152
|
+
while ((match2 = re.exec(value)) !== null) {
|
|
35153
|
+
if (match2[0].length === 0) {
|
|
35154
|
+
re.lastIndex += 1;
|
|
35155
|
+
continue;
|
|
35156
|
+
}
|
|
35157
|
+
const hit = span(match2);
|
|
35158
|
+
if (hit)
|
|
35159
|
+
found.push({ label, severity, ...hit });
|
|
35160
|
+
}
|
|
35161
|
+
};
|
|
35162
|
+
const whole = (m, replacement) => ({
|
|
35163
|
+
start: m.index,
|
|
35164
|
+
end: m.index + m[0].length,
|
|
35165
|
+
replacement
|
|
35166
|
+
});
|
|
35167
|
+
for (const rule of FIXED_SECRET_RULES) {
|
|
35168
|
+
scan(rule.re, rule.label, "credential", (m) => whole(m, REDACTED));
|
|
35169
|
+
}
|
|
35170
|
+
if (fixedOnly)
|
|
35171
|
+
return found;
|
|
35172
|
+
scan(ASSIGNED_SECRET_RE, "an assigned secret", "credential", (m) => {
|
|
35173
|
+
const secret = m[4] ?? "";
|
|
35174
|
+
if (SECRET_REFERENCE_RE.test(secret))
|
|
35175
|
+
return null;
|
|
35176
|
+
const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
|
|
35177
|
+
return { start, end: start + secret.length, replacement: REDACTED };
|
|
35178
|
+
});
|
|
35179
|
+
if (currentHome) {
|
|
35180
|
+
scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", "path", (m) => whole(m, "~"));
|
|
35181
|
+
}
|
|
35182
|
+
scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", "path", (m) => whole(m, "~"));
|
|
35183
|
+
return found;
|
|
35184
|
+
}
|
|
35185
|
+
var REDACTED, FIXED_SECRET_RULES, ASSIGNED_SECRET_RE, SECRET_REFERENCE_RE, CROSS_PLATFORM_HOME_RE;
|
|
35186
|
+
var init_secretScan = __esm({
|
|
35187
|
+
"../../packages/core/dist/secretScan.js"() {
|
|
35188
|
+
"use strict";
|
|
35189
|
+
REDACTED = "[redacted]";
|
|
35190
|
+
FIXED_SECRET_RULES = [
|
|
35191
|
+
{ label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
35192
|
+
/**
|
|
35193
|
+
* FINE-GRAINED PATS, which the rule above cannot match and which are the format GitHub
|
|
35194
|
+
* now issues by default. `github_pat_…` fails `gh[pousr]_` on its third character, and
|
|
35195
|
+
* `_` is a word character so there is no boundary for the rule to catch it mid-string
|
|
35196
|
+
* either — a fine-grained token scored ZERO findings and went out on the anonymous
|
|
35197
|
+
* index. Codex found it after two other reviewers and I had all read this list.
|
|
35198
|
+
*
|
|
35199
|
+
* The lesson is about the tests, not the regex: every case written for this scanner
|
|
35200
|
+
* used `ghp_`, so a hundred more of them would still have missed it. A rule list is
|
|
35201
|
+
* only as good as the shapes someone thought to try.
|
|
35202
|
+
*/
|
|
35203
|
+
{ label: "a GitHub token", re: /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g },
|
|
35204
|
+
{ label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
35205
|
+
/**
|
|
35206
|
+
* THE REST OF THE FORMATS A DEVELOPER ACTUALLY PASTES. Found by sweeping candidate
|
|
35207
|
+
* credentials through `findSecrets` and printing which scored zero, rather than by
|
|
35208
|
+
* reading the list and thinking hard — reading it is what missed `github_pat_`.
|
|
35209
|
+
*
|
|
35210
|
+
* Every one of these is a self-identifying PREFIX, which is the property that makes a
|
|
35211
|
+
* rule safe to add here: it can only match a shape that is already a credential, so it
|
|
35212
|
+
* costs nothing when it is wrong. That is also why the AWS SECRET access key is
|
|
35213
|
+
* deliberately absent — it is forty characters of base64 with no prefix, and a rule for
|
|
35214
|
+
* it would refuse any posting that quoted a hash, a checksum or a build id.
|
|
35215
|
+
*/
|
|
35216
|
+
{ label: "an Anthropic API key", re: /\bsk-ant-[A-Za-z0-9-]{20,}\b/g },
|
|
35217
|
+
/**
|
|
35218
|
+
* NO HYPHEN IN THE BODY, and that restriction is the whole rule. The first cut allowed
|
|
35219
|
+
* `[A-Za-z0-9_-]{20,}` and refused `sk-payments-worker-staging`, `sk-checkout-service-eu-west-1`
|
|
35220
|
+
* and the branch name `sk-fix-the-retry-loop-again` — kebab-case is how services and
|
|
35221
|
+
* branches are named, and `sk-` is an ordinary prefix for one. Four out of four
|
|
35222
|
+
* realistic strings I tried were refused as OpenAI keys.
|
|
35223
|
+
*
|
|
35224
|
+
* A real key carries a long unbroken run of base64 characters; a kebab name never does,
|
|
35225
|
+
* because it has a hyphen every few characters. Requiring twenty consecutive non-hyphen
|
|
35226
|
+
* characters separates them cleanly and costs nothing on the token side.
|
|
35227
|
+
*
|
|
35228
|
+
* This is the rule that would have taught founders to switch the scanner off, and I
|
|
35229
|
+
* only found it because I went looking for false positives as hard as I had gone
|
|
35230
|
+
* looking for misses. The seven prose cases I wrote first all passed.
|
|
35231
|
+
*/
|
|
35232
|
+
{ label: "an OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_]{20,}\b/g },
|
|
35233
|
+
{
|
|
35234
|
+
label: "a Slack token",
|
|
35235
|
+
// `xoxe` (refresh) and `xapp` (app-level) are documented families the first cut did
|
|
35236
|
+
// not know, so both produced zero findings. Same lesson as `github_pat_`: a prefix
|
|
35237
|
+
// list is a guess about a vendor's naming until someone checks the vendor's docs.
|
|
35238
|
+
re: /\b(?:xox[baprse]-|xapp-)[A-Za-z0-9-]{10,}\b/g
|
|
35239
|
+
},
|
|
35240
|
+
{ label: "a Google API key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
|
|
35241
|
+
{ label: "an npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g },
|
|
35242
|
+
/**
|
|
35243
|
+
* A JWT is not always a secret — plenty are short-lived and harmless. It is here anyway
|
|
35244
|
+
* because the ones people paste into bug reports are session and access tokens, and the
|
|
35245
|
+
* three-segment `eyJ…` shape is specific enough that a false match is nearly impossible.
|
|
35246
|
+
*/
|
|
35247
|
+
{ label: "a JSON web token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
35248
|
+
/**
|
|
35249
|
+
* A password inline in a connection string. Anchored on the SCHEME so it cannot fire on
|
|
35250
|
+
* ordinary prose containing an `@`, and it takes the whole credential pair rather than
|
|
35251
|
+
* just the password — publishing the username alongside the host is most of the harm.
|
|
35252
|
+
*/
|
|
35253
|
+
{
|
|
35254
|
+
label: "a database password",
|
|
35255
|
+
re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:/@]+:[^\s:/@]{6,}@/g
|
|
35256
|
+
},
|
|
35257
|
+
{ label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
|
|
35258
|
+
{
|
|
35259
|
+
label: "a private key",
|
|
35260
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
|
|
35261
|
+
}
|
|
35262
|
+
];
|
|
35263
|
+
ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
|
|
35264
|
+
SECRET_REFERENCE_RE = new RegExp([
|
|
35265
|
+
"^process\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
35266
|
+
"^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
|
|
35267
|
+
"^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
35268
|
+
"^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
|
|
35269
|
+
"^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
|
|
35270
|
+
"^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
|
|
35271
|
+
"^\\$\\{?[A-Za-z_]\\w*\\}?$",
|
|
35272
|
+
// $VAR, ${VAR}
|
|
35273
|
+
"^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
|
|
35274
|
+
// crypto.randomUUID()
|
|
35275
|
+
// A call WITH arguments, but only when no argument is long enough to be a
|
|
35276
|
+
// credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
|
|
35277
|
+
// a secret wearing a function's clothes.
|
|
35278
|
+
"^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
|
|
35279
|
+
"^(?:null|undefined|true|false)$"
|
|
35280
|
+
].join("|"), "i");
|
|
35281
|
+
CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
|
|
35282
|
+
}
|
|
35283
|
+
});
|
|
35284
|
+
|
|
35121
35285
|
// src/posting-drafts.ts
|
|
35122
35286
|
import {
|
|
35123
35287
|
chmodSync as chmodSync2,
|
|
@@ -35248,9 +35412,6 @@ function toSubmittedPosting(draft) {
|
|
|
35248
35412
|
...draft.repo ? { repo: draft.repo } : {}
|
|
35249
35413
|
};
|
|
35250
35414
|
}
|
|
35251
|
-
function escapeRegExp(value) {
|
|
35252
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35253
|
-
}
|
|
35254
35415
|
function assemblePieces(draft) {
|
|
35255
35416
|
const parts = [];
|
|
35256
35417
|
if (draft.title) {
|
|
@@ -35286,35 +35447,7 @@ function assemblePieces(draft) {
|
|
|
35286
35447
|
return { scanned, pieces };
|
|
35287
35448
|
}
|
|
35288
35449
|
function detections(value, currentHome, fixedOnly = false) {
|
|
35289
|
-
|
|
35290
|
-
const scan = (pattern, label, span) => {
|
|
35291
|
-
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
35292
|
-
let match2;
|
|
35293
|
-
while ((match2 = re.exec(value)) !== null) {
|
|
35294
|
-
if (match2[0].length === 0) {
|
|
35295
|
-
re.lastIndex += 1;
|
|
35296
|
-
continue;
|
|
35297
|
-
}
|
|
35298
|
-
const hit = span(match2);
|
|
35299
|
-
if (hit) found.push({ label, ...hit });
|
|
35300
|
-
}
|
|
35301
|
-
};
|
|
35302
|
-
const whole = (m, replacement) => ({
|
|
35303
|
-
start: m.index,
|
|
35304
|
-
end: m.index + m[0].length,
|
|
35305
|
-
replacement
|
|
35306
|
-
});
|
|
35307
|
-
for (const rule of FIXED_SECRET_RULES) scan(rule.re, rule.label, (m) => whole(m, REDACTED));
|
|
35308
|
-
if (fixedOnly) return found;
|
|
35309
|
-
scan(ASSIGNED_SECRET_RE, "an assigned secret", (m) => {
|
|
35310
|
-
const secret = m[4] ?? "";
|
|
35311
|
-
if (SECRET_REFERENCE_RE.test(secret)) return null;
|
|
35312
|
-
const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
|
|
35313
|
-
return { start, end: start + secret.length, replacement: REDACTED };
|
|
35314
|
-
});
|
|
35315
|
-
if (currentHome) scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", (m) => whole(m, "~"));
|
|
35316
|
-
scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", (m) => whole(m, "~"));
|
|
35317
|
-
return found;
|
|
35450
|
+
return findSecrets(value, { currentHome, fixedOnly });
|
|
35318
35451
|
}
|
|
35319
35452
|
function boundaryDetections(scanned, pieces, currentHome, already) {
|
|
35320
35453
|
let tight = "";
|
|
@@ -35454,45 +35587,14 @@ function preparePostingSubmission(draft, currentHome = homedir23()) {
|
|
|
35454
35587
|
notices: draft.captureFailures.map((failure) => `could not capture ${failure}`)
|
|
35455
35588
|
};
|
|
35456
35589
|
}
|
|
35457
|
-
var
|
|
35590
|
+
var EMPTY_AFTER_REDACTION_RE, SECRET_PATH_RE;
|
|
35458
35591
|
var init_posting_drafts = __esm({
|
|
35459
35592
|
"src/posting-drafts.ts"() {
|
|
35460
35593
|
"use strict";
|
|
35461
35594
|
init_state_dir();
|
|
35462
|
-
|
|
35595
|
+
init_secretScan();
|
|
35463
35596
|
EMPTY_AFTER_REDACTION_RE = /(?:\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*|\[redacted\]|["'`~]|\s)+/g;
|
|
35464
|
-
FIXED_SECRET_RULES = [
|
|
35465
|
-
{ label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
35466
|
-
{ label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
35467
|
-
{ label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
|
|
35468
|
-
{
|
|
35469
|
-
label: "a private key",
|
|
35470
|
-
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
|
|
35471
|
-
}
|
|
35472
|
-
];
|
|
35473
|
-
ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
|
|
35474
|
-
SECRET_REFERENCE_RE = new RegExp(
|
|
35475
|
-
[
|
|
35476
|
-
"^process\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
35477
|
-
"^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
|
|
35478
|
-
"^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
35479
|
-
"^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
|
|
35480
|
-
"^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
|
|
35481
|
-
"^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
|
|
35482
|
-
"^\\$\\{?[A-Za-z_]\\w*\\}?$",
|
|
35483
|
-
// $VAR, ${VAR}
|
|
35484
|
-
"^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
|
|
35485
|
-
// crypto.randomUUID()
|
|
35486
|
-
// A call WITH arguments, but only when no argument is long enough to be a
|
|
35487
|
-
// credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
|
|
35488
|
-
// a secret wearing a function's clothes.
|
|
35489
|
-
"^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
|
|
35490
|
-
"^(?:null|undefined|true|false)$"
|
|
35491
|
-
].join("|"),
|
|
35492
|
-
"i"
|
|
35493
|
-
);
|
|
35494
35597
|
SECRET_PATH_RE = /(?:^|[/\\])(?:\.env(?:\.[^/\\]+)?|id_(?:rsa|ed25519)|credentials(?:\.json)?|secrets?)(?:$|[/\\])/i;
|
|
35495
|
-
CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
|
|
35496
35598
|
}
|
|
35497
35599
|
});
|
|
35498
35600
|
|
package/dist/bin/jpi-post.js
CHANGED
|
@@ -92,6 +92,143 @@ function ensureStateDirForSecret(dir) {
|
|
|
92
92
|
applyStateDirSecretPolicy(dir, ensureStateDir(dir));
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// ../../packages/core/dist/secretScan.js
|
|
96
|
+
var REDACTED = "[redacted]";
|
|
97
|
+
var FIXED_SECRET_RULES = [
|
|
98
|
+
{ label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
99
|
+
/**
|
|
100
|
+
* FINE-GRAINED PATS, which the rule above cannot match and which are the format GitHub
|
|
101
|
+
* now issues by default. `github_pat_…` fails `gh[pousr]_` on its third character, and
|
|
102
|
+
* `_` is a word character so there is no boundary for the rule to catch it mid-string
|
|
103
|
+
* either — a fine-grained token scored ZERO findings and went out on the anonymous
|
|
104
|
+
* index. Codex found it after two other reviewers and I had all read this list.
|
|
105
|
+
*
|
|
106
|
+
* The lesson is about the tests, not the regex: every case written for this scanner
|
|
107
|
+
* used `ghp_`, so a hundred more of them would still have missed it. A rule list is
|
|
108
|
+
* only as good as the shapes someone thought to try.
|
|
109
|
+
*/
|
|
110
|
+
{ label: "a GitHub token", re: /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g },
|
|
111
|
+
{ label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
112
|
+
/**
|
|
113
|
+
* THE REST OF THE FORMATS A DEVELOPER ACTUALLY PASTES. Found by sweeping candidate
|
|
114
|
+
* credentials through `findSecrets` and printing which scored zero, rather than by
|
|
115
|
+
* reading the list and thinking hard — reading it is what missed `github_pat_`.
|
|
116
|
+
*
|
|
117
|
+
* Every one of these is a self-identifying PREFIX, which is the property that makes a
|
|
118
|
+
* rule safe to add here: it can only match a shape that is already a credential, so it
|
|
119
|
+
* costs nothing when it is wrong. That is also why the AWS SECRET access key is
|
|
120
|
+
* deliberately absent — it is forty characters of base64 with no prefix, and a rule for
|
|
121
|
+
* it would refuse any posting that quoted a hash, a checksum or a build id.
|
|
122
|
+
*/
|
|
123
|
+
{ label: "an Anthropic API key", re: /\bsk-ant-[A-Za-z0-9-]{20,}\b/g },
|
|
124
|
+
/**
|
|
125
|
+
* NO HYPHEN IN THE BODY, and that restriction is the whole rule. The first cut allowed
|
|
126
|
+
* `[A-Za-z0-9_-]{20,}` and refused `sk-payments-worker-staging`, `sk-checkout-service-eu-west-1`
|
|
127
|
+
* and the branch name `sk-fix-the-retry-loop-again` — kebab-case is how services and
|
|
128
|
+
* branches are named, and `sk-` is an ordinary prefix for one. Four out of four
|
|
129
|
+
* realistic strings I tried were refused as OpenAI keys.
|
|
130
|
+
*
|
|
131
|
+
* A real key carries a long unbroken run of base64 characters; a kebab name never does,
|
|
132
|
+
* because it has a hyphen every few characters. Requiring twenty consecutive non-hyphen
|
|
133
|
+
* characters separates them cleanly and costs nothing on the token side.
|
|
134
|
+
*
|
|
135
|
+
* This is the rule that would have taught founders to switch the scanner off, and I
|
|
136
|
+
* only found it because I went looking for false positives as hard as I had gone
|
|
137
|
+
* looking for misses. The seven prose cases I wrote first all passed.
|
|
138
|
+
*/
|
|
139
|
+
{ label: "an OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_]{20,}\b/g },
|
|
140
|
+
{
|
|
141
|
+
label: "a Slack token",
|
|
142
|
+
// `xoxe` (refresh) and `xapp` (app-level) are documented families the first cut did
|
|
143
|
+
// not know, so both produced zero findings. Same lesson as `github_pat_`: a prefix
|
|
144
|
+
// list is a guess about a vendor's naming until someone checks the vendor's docs.
|
|
145
|
+
re: /\b(?:xox[baprse]-|xapp-)[A-Za-z0-9-]{10,}\b/g
|
|
146
|
+
},
|
|
147
|
+
{ label: "a Google API key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
|
|
148
|
+
{ label: "an npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g },
|
|
149
|
+
/**
|
|
150
|
+
* A JWT is not always a secret — plenty are short-lived and harmless. It is here anyway
|
|
151
|
+
* because the ones people paste into bug reports are session and access tokens, and the
|
|
152
|
+
* three-segment `eyJ…` shape is specific enough that a false match is nearly impossible.
|
|
153
|
+
*/
|
|
154
|
+
{ label: "a JSON web token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
155
|
+
/**
|
|
156
|
+
* A password inline in a connection string. Anchored on the SCHEME so it cannot fire on
|
|
157
|
+
* ordinary prose containing an `@`, and it takes the whole credential pair rather than
|
|
158
|
+
* just the password — publishing the username alongside the host is most of the harm.
|
|
159
|
+
*/
|
|
160
|
+
{
|
|
161
|
+
label: "a database password",
|
|
162
|
+
re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:/@]+:[^\s:/@]{6,}@/g
|
|
163
|
+
},
|
|
164
|
+
{ label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
|
|
165
|
+
{
|
|
166
|
+
label: "a private key",
|
|
167
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
|
|
171
|
+
var SECRET_REFERENCE_RE = new RegExp([
|
|
172
|
+
"^process\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
173
|
+
"^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
|
|
174
|
+
"^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
175
|
+
"^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
|
|
176
|
+
"^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
|
|
177
|
+
"^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
|
|
178
|
+
"^\\$\\{?[A-Za-z_]\\w*\\}?$",
|
|
179
|
+
// $VAR, ${VAR}
|
|
180
|
+
"^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
|
|
181
|
+
// crypto.randomUUID()
|
|
182
|
+
// A call WITH arguments, but only when no argument is long enough to be a
|
|
183
|
+
// credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
|
|
184
|
+
// a secret wearing a function's clothes.
|
|
185
|
+
"^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
|
|
186
|
+
"^(?:null|undefined|true|false)$"
|
|
187
|
+
].join("|"), "i");
|
|
188
|
+
var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
|
|
189
|
+
function escapeRegExp(value) {
|
|
190
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
191
|
+
}
|
|
192
|
+
function findSecrets(value, options = {}) {
|
|
193
|
+
const { currentHome = "", fixedOnly = false } = options;
|
|
194
|
+
const found = [];
|
|
195
|
+
const scan = (pattern, label, severity, span) => {
|
|
196
|
+
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = re.exec(value)) !== null) {
|
|
199
|
+
if (match[0].length === 0) {
|
|
200
|
+
re.lastIndex += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const hit = span(match);
|
|
204
|
+
if (hit)
|
|
205
|
+
found.push({ label, severity, ...hit });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const whole = (m, replacement) => ({
|
|
209
|
+
start: m.index,
|
|
210
|
+
end: m.index + m[0].length,
|
|
211
|
+
replacement
|
|
212
|
+
});
|
|
213
|
+
for (const rule of FIXED_SECRET_RULES) {
|
|
214
|
+
scan(rule.re, rule.label, "credential", (m) => whole(m, REDACTED));
|
|
215
|
+
}
|
|
216
|
+
if (fixedOnly)
|
|
217
|
+
return found;
|
|
218
|
+
scan(ASSIGNED_SECRET_RE, "an assigned secret", "credential", (m) => {
|
|
219
|
+
const secret = m[4] ?? "";
|
|
220
|
+
if (SECRET_REFERENCE_RE.test(secret))
|
|
221
|
+
return null;
|
|
222
|
+
const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
|
|
223
|
+
return { start, end: start + secret.length, replacement: REDACTED };
|
|
224
|
+
});
|
|
225
|
+
if (currentHome) {
|
|
226
|
+
scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", "path", (m) => whole(m, "~"));
|
|
227
|
+
}
|
|
228
|
+
scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", "path", (m) => whole(m, "~"));
|
|
229
|
+
return found;
|
|
230
|
+
}
|
|
231
|
+
|
|
95
232
|
// src/posting-drafts.ts
|
|
96
233
|
function stateDir() {
|
|
97
234
|
return process.env["TERMINALHIRE_DIR"] || join(homedir(), ".terminalhire");
|
|
@@ -211,43 +348,8 @@ function toSubmittedPosting(draft) {
|
|
|
211
348
|
...draft.repo ? { repo: draft.repo } : {}
|
|
212
349
|
};
|
|
213
350
|
}
|
|
214
|
-
var REDACTED = "[redacted]";
|
|
215
351
|
var EMPTY_AFTER_REDACTION_RE = /(?:\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*|\[redacted\]|["'`~]|\s)+/g;
|
|
216
|
-
var FIXED_SECRET_RULES = [
|
|
217
|
-
{ label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
218
|
-
{ label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
219
|
-
{ label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
|
|
220
|
-
{
|
|
221
|
-
label: "a private key",
|
|
222
|
-
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
|
|
223
|
-
}
|
|
224
|
-
];
|
|
225
|
-
var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
|
|
226
|
-
var SECRET_REFERENCE_RE = new RegExp(
|
|
227
|
-
[
|
|
228
|
-
"^process\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
229
|
-
"^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
|
|
230
|
-
"^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
231
|
-
"^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
|
|
232
|
-
"^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
|
|
233
|
-
"^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
|
|
234
|
-
"^\\$\\{?[A-Za-z_]\\w*\\}?$",
|
|
235
|
-
// $VAR, ${VAR}
|
|
236
|
-
"^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
|
|
237
|
-
// crypto.randomUUID()
|
|
238
|
-
// A call WITH arguments, but only when no argument is long enough to be a
|
|
239
|
-
// credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
|
|
240
|
-
// a secret wearing a function's clothes.
|
|
241
|
-
"^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
|
|
242
|
-
"^(?:null|undefined|true|false)$"
|
|
243
|
-
].join("|"),
|
|
244
|
-
"i"
|
|
245
|
-
);
|
|
246
352
|
var SECRET_PATH_RE = /(?:^|[/\\])(?:\.env(?:\.[^/\\]+)?|id_(?:rsa|ed25519)|credentials(?:\.json)?|secrets?)(?:$|[/\\])/i;
|
|
247
|
-
var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
|
|
248
|
-
function escapeRegExp(value) {
|
|
249
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
250
|
-
}
|
|
251
353
|
function assemblePieces(draft) {
|
|
252
354
|
const parts = [];
|
|
253
355
|
if (draft.title) {
|
|
@@ -283,35 +385,7 @@ function assemblePieces(draft) {
|
|
|
283
385
|
return { scanned, pieces };
|
|
284
386
|
}
|
|
285
387
|
function detections(value, currentHome, fixedOnly = false) {
|
|
286
|
-
|
|
287
|
-
const scan = (pattern, label, span) => {
|
|
288
|
-
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
289
|
-
let match;
|
|
290
|
-
while ((match = re.exec(value)) !== null) {
|
|
291
|
-
if (match[0].length === 0) {
|
|
292
|
-
re.lastIndex += 1;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
const hit = span(match);
|
|
296
|
-
if (hit) found.push({ label, ...hit });
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
const whole = (m, replacement) => ({
|
|
300
|
-
start: m.index,
|
|
301
|
-
end: m.index + m[0].length,
|
|
302
|
-
replacement
|
|
303
|
-
});
|
|
304
|
-
for (const rule of FIXED_SECRET_RULES) scan(rule.re, rule.label, (m) => whole(m, REDACTED));
|
|
305
|
-
if (fixedOnly) return found;
|
|
306
|
-
scan(ASSIGNED_SECRET_RE, "an assigned secret", (m) => {
|
|
307
|
-
const secret = m[4] ?? "";
|
|
308
|
-
if (SECRET_REFERENCE_RE.test(secret)) return null;
|
|
309
|
-
const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
|
|
310
|
-
return { start, end: start + secret.length, replacement: REDACTED };
|
|
311
|
-
});
|
|
312
|
-
if (currentHome) scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", (m) => whole(m, "~"));
|
|
313
|
-
scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", (m) => whole(m, "~"));
|
|
314
|
-
return found;
|
|
388
|
+
return findSecrets(value, { currentHome, fixedOnly });
|
|
315
389
|
}
|
|
316
390
|
function boundaryDetections(scanned, pieces, currentHome, already) {
|
|
317
391
|
let tight = "";
|
|
@@ -84,6 +84,143 @@ function ensureStateDirForSecret(dir) {
|
|
|
84
84
|
applyStateDirSecretPolicy(dir, ensureStateDir(dir));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// ../../packages/core/dist/secretScan.js
|
|
88
|
+
var REDACTED = "[redacted]";
|
|
89
|
+
var FIXED_SECRET_RULES = [
|
|
90
|
+
{ label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
91
|
+
/**
|
|
92
|
+
* FINE-GRAINED PATS, which the rule above cannot match and which are the format GitHub
|
|
93
|
+
* now issues by default. `github_pat_…` fails `gh[pousr]_` on its third character, and
|
|
94
|
+
* `_` is a word character so there is no boundary for the rule to catch it mid-string
|
|
95
|
+
* either — a fine-grained token scored ZERO findings and went out on the anonymous
|
|
96
|
+
* index. Codex found it after two other reviewers and I had all read this list.
|
|
97
|
+
*
|
|
98
|
+
* The lesson is about the tests, not the regex: every case written for this scanner
|
|
99
|
+
* used `ghp_`, so a hundred more of them would still have missed it. A rule list is
|
|
100
|
+
* only as good as the shapes someone thought to try.
|
|
101
|
+
*/
|
|
102
|
+
{ label: "a GitHub token", re: /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g },
|
|
103
|
+
{ label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
104
|
+
/**
|
|
105
|
+
* THE REST OF THE FORMATS A DEVELOPER ACTUALLY PASTES. Found by sweeping candidate
|
|
106
|
+
* credentials through `findSecrets` and printing which scored zero, rather than by
|
|
107
|
+
* reading the list and thinking hard — reading it is what missed `github_pat_`.
|
|
108
|
+
*
|
|
109
|
+
* Every one of these is a self-identifying PREFIX, which is the property that makes a
|
|
110
|
+
* rule safe to add here: it can only match a shape that is already a credential, so it
|
|
111
|
+
* costs nothing when it is wrong. That is also why the AWS SECRET access key is
|
|
112
|
+
* deliberately absent — it is forty characters of base64 with no prefix, and a rule for
|
|
113
|
+
* it would refuse any posting that quoted a hash, a checksum or a build id.
|
|
114
|
+
*/
|
|
115
|
+
{ label: "an Anthropic API key", re: /\bsk-ant-[A-Za-z0-9-]{20,}\b/g },
|
|
116
|
+
/**
|
|
117
|
+
* NO HYPHEN IN THE BODY, and that restriction is the whole rule. The first cut allowed
|
|
118
|
+
* `[A-Za-z0-9_-]{20,}` and refused `sk-payments-worker-staging`, `sk-checkout-service-eu-west-1`
|
|
119
|
+
* and the branch name `sk-fix-the-retry-loop-again` — kebab-case is how services and
|
|
120
|
+
* branches are named, and `sk-` is an ordinary prefix for one. Four out of four
|
|
121
|
+
* realistic strings I tried were refused as OpenAI keys.
|
|
122
|
+
*
|
|
123
|
+
* A real key carries a long unbroken run of base64 characters; a kebab name never does,
|
|
124
|
+
* because it has a hyphen every few characters. Requiring twenty consecutive non-hyphen
|
|
125
|
+
* characters separates them cleanly and costs nothing on the token side.
|
|
126
|
+
*
|
|
127
|
+
* This is the rule that would have taught founders to switch the scanner off, and I
|
|
128
|
+
* only found it because I went looking for false positives as hard as I had gone
|
|
129
|
+
* looking for misses. The seven prose cases I wrote first all passed.
|
|
130
|
+
*/
|
|
131
|
+
{ label: "an OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_]{20,}\b/g },
|
|
132
|
+
{
|
|
133
|
+
label: "a Slack token",
|
|
134
|
+
// `xoxe` (refresh) and `xapp` (app-level) are documented families the first cut did
|
|
135
|
+
// not know, so both produced zero findings. Same lesson as `github_pat_`: a prefix
|
|
136
|
+
// list is a guess about a vendor's naming until someone checks the vendor's docs.
|
|
137
|
+
re: /\b(?:xox[baprse]-|xapp-)[A-Za-z0-9-]{10,}\b/g
|
|
138
|
+
},
|
|
139
|
+
{ label: "a Google API key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
|
|
140
|
+
{ label: "an npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g },
|
|
141
|
+
/**
|
|
142
|
+
* A JWT is not always a secret — plenty are short-lived and harmless. It is here anyway
|
|
143
|
+
* because the ones people paste into bug reports are session and access tokens, and the
|
|
144
|
+
* three-segment `eyJ…` shape is specific enough that a false match is nearly impossible.
|
|
145
|
+
*/
|
|
146
|
+
{ label: "a JSON web token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
147
|
+
/**
|
|
148
|
+
* A password inline in a connection string. Anchored on the SCHEME so it cannot fire on
|
|
149
|
+
* ordinary prose containing an `@`, and it takes the whole credential pair rather than
|
|
150
|
+
* just the password — publishing the username alongside the host is most of the harm.
|
|
151
|
+
*/
|
|
152
|
+
{
|
|
153
|
+
label: "a database password",
|
|
154
|
+
re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:/@]+:[^\s:/@]{6,}@/g
|
|
155
|
+
},
|
|
156
|
+
{ label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
|
|
157
|
+
{
|
|
158
|
+
label: "a private key",
|
|
159
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
|
|
160
|
+
}
|
|
161
|
+
];
|
|
162
|
+
var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
|
|
163
|
+
var SECRET_REFERENCE_RE = new RegExp([
|
|
164
|
+
"^process\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
165
|
+
"^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
|
|
166
|
+
"^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
167
|
+
"^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
|
|
168
|
+
"^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
|
|
169
|
+
"^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
|
|
170
|
+
"^\\$\\{?[A-Za-z_]\\w*\\}?$",
|
|
171
|
+
// $VAR, ${VAR}
|
|
172
|
+
"^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
|
|
173
|
+
// crypto.randomUUID()
|
|
174
|
+
// A call WITH arguments, but only when no argument is long enough to be a
|
|
175
|
+
// credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
|
|
176
|
+
// a secret wearing a function's clothes.
|
|
177
|
+
"^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
|
|
178
|
+
"^(?:null|undefined|true|false)$"
|
|
179
|
+
].join("|"), "i");
|
|
180
|
+
var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
|
|
181
|
+
function escapeRegExp(value) {
|
|
182
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
183
|
+
}
|
|
184
|
+
function findSecrets(value, options = {}) {
|
|
185
|
+
const { currentHome = "", fixedOnly = false } = options;
|
|
186
|
+
const found = [];
|
|
187
|
+
const scan = (pattern, label, severity, span) => {
|
|
188
|
+
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
189
|
+
let match;
|
|
190
|
+
while ((match = re.exec(value)) !== null) {
|
|
191
|
+
if (match[0].length === 0) {
|
|
192
|
+
re.lastIndex += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const hit = span(match);
|
|
196
|
+
if (hit)
|
|
197
|
+
found.push({ label, severity, ...hit });
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const whole = (m, replacement) => ({
|
|
201
|
+
start: m.index,
|
|
202
|
+
end: m.index + m[0].length,
|
|
203
|
+
replacement
|
|
204
|
+
});
|
|
205
|
+
for (const rule of FIXED_SECRET_RULES) {
|
|
206
|
+
scan(rule.re, rule.label, "credential", (m) => whole(m, REDACTED));
|
|
207
|
+
}
|
|
208
|
+
if (fixedOnly)
|
|
209
|
+
return found;
|
|
210
|
+
scan(ASSIGNED_SECRET_RE, "an assigned secret", "credential", (m) => {
|
|
211
|
+
const secret = m[4] ?? "";
|
|
212
|
+
if (SECRET_REFERENCE_RE.test(secret))
|
|
213
|
+
return null;
|
|
214
|
+
const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
|
|
215
|
+
return { start, end: start + secret.length, replacement: REDACTED };
|
|
216
|
+
});
|
|
217
|
+
if (currentHome) {
|
|
218
|
+
scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", "path", (m) => whole(m, "~"));
|
|
219
|
+
}
|
|
220
|
+
scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", "path", (m) => whole(m, "~"));
|
|
221
|
+
return found;
|
|
222
|
+
}
|
|
223
|
+
|
|
87
224
|
// src/posting-drafts.ts
|
|
88
225
|
var POSTING_DRAFT_FIELD_POLICY = {
|
|
89
226
|
id: "local",
|
|
@@ -220,43 +357,8 @@ function toSubmittedPosting(draft) {
|
|
|
220
357
|
...draft.repo ? { repo: draft.repo } : {}
|
|
221
358
|
};
|
|
222
359
|
}
|
|
223
|
-
var REDACTED = "[redacted]";
|
|
224
360
|
var EMPTY_AFTER_REDACTION_RE = /(?:\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*|\[redacted\]|["'`~]|\s)+/g;
|
|
225
|
-
var FIXED_SECRET_RULES = [
|
|
226
|
-
{ label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
227
|
-
{ label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
228
|
-
{ label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
|
|
229
|
-
{
|
|
230
|
-
label: "a private key",
|
|
231
|
-
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
|
|
232
|
-
}
|
|
233
|
-
];
|
|
234
|
-
var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
|
|
235
|
-
var SECRET_REFERENCE_RE = new RegExp(
|
|
236
|
-
[
|
|
237
|
-
"^process\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
238
|
-
"^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
|
|
239
|
-
"^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
|
|
240
|
-
"^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
|
|
241
|
-
"^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
|
|
242
|
-
"^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
|
|
243
|
-
"^\\$\\{?[A-Za-z_]\\w*\\}?$",
|
|
244
|
-
// $VAR, ${VAR}
|
|
245
|
-
"^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
|
|
246
|
-
// crypto.randomUUID()
|
|
247
|
-
// A call WITH arguments, but only when no argument is long enough to be a
|
|
248
|
-
// credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
|
|
249
|
-
// a secret wearing a function's clothes.
|
|
250
|
-
"^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
|
|
251
|
-
"^(?:null|undefined|true|false)$"
|
|
252
|
-
].join("|"),
|
|
253
|
-
"i"
|
|
254
|
-
);
|
|
255
361
|
var SECRET_PATH_RE = /(?:^|[/\\])(?:\.env(?:\.[^/\\]+)?|id_(?:rsa|ed25519)|credentials(?:\.json)?|secrets?)(?:$|[/\\])/i;
|
|
256
|
-
var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
|
|
257
|
-
function escapeRegExp(value) {
|
|
258
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
259
|
-
}
|
|
260
362
|
function assemblePieces(draft) {
|
|
261
363
|
const parts = [];
|
|
262
364
|
if (draft.title) {
|
|
@@ -292,35 +394,7 @@ function assemblePieces(draft) {
|
|
|
292
394
|
return { scanned, pieces };
|
|
293
395
|
}
|
|
294
396
|
function detections(value, currentHome, fixedOnly = false) {
|
|
295
|
-
|
|
296
|
-
const scan = (pattern, label, span) => {
|
|
297
|
-
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
298
|
-
let match;
|
|
299
|
-
while ((match = re.exec(value)) !== null) {
|
|
300
|
-
if (match[0].length === 0) {
|
|
301
|
-
re.lastIndex += 1;
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
const hit = span(match);
|
|
305
|
-
if (hit) found.push({ label, ...hit });
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
const whole = (m, replacement) => ({
|
|
309
|
-
start: m.index,
|
|
310
|
-
end: m.index + m[0].length,
|
|
311
|
-
replacement
|
|
312
|
-
});
|
|
313
|
-
for (const rule of FIXED_SECRET_RULES) scan(rule.re, rule.label, (m) => whole(m, REDACTED));
|
|
314
|
-
if (fixedOnly) return found;
|
|
315
|
-
scan(ASSIGNED_SECRET_RE, "an assigned secret", (m) => {
|
|
316
|
-
const secret = m[4] ?? "";
|
|
317
|
-
if (SECRET_REFERENCE_RE.test(secret)) return null;
|
|
318
|
-
const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
|
|
319
|
-
return { start, end: start + secret.length, replacement: REDACTED };
|
|
320
|
-
});
|
|
321
|
-
if (currentHome) scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", (m) => whole(m, "~"));
|
|
322
|
-
scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", (m) => whole(m, "~"));
|
|
323
|
-
return found;
|
|
397
|
+
return findSecrets(value, { currentHome, fixedOnly });
|
|
324
398
|
}
|
|
325
399
|
function boundaryDetections(scanned, pieces, currentHome, already) {
|
|
326
400
|
let tight = "";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminalhire",
|
|
3
|
-
"version": "0.42.
|
|
4
|
-
"description": "Local-first job matching for developers
|
|
3
|
+
"version": "0.42.6",
|
|
4
|
+
"description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Your profile never leaves your machine.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/staqsIO/terminalhire-plugin.git"
|