@@ -148,4 +148,4 @@ // Local-filesystem project discovery for day-1 onboarding. | ||
| * upserts by it); slug is the URL-safe display label derived from the | ||
| * repo name. So a local `~/projects/drone/` whose remote is | ||
| * `acme/palisade-website` registers as slug `palisade-website` — matching | ||
| * repo name. So a local `~/projects/widget/` whose remote is | ||
| * `acme/web-app` registers as slug `web-app` — matching | ||
| * the repo, not the local folder. | ||
@@ -176,4 +176,4 @@ */ | ||
| // an org rename) minted a fresh slug and the server | ||
| // inserted a duplicate row. `~/code/drone` with remote | ||
| // `nsokin/palisade-website` now registers as slug `palisade-website`. | ||
| // inserted a duplicate row. `~/code/widget` with remote | ||
| // `acme/web-app` now registers as slug `web-app`. | ||
| // The repo name is also the fallback display name, so a generic | ||
@@ -180,0 +180,0 @@ // package.json name (the Next.js starter's "nextn", etc.) doesn't stick. |
+60
-0
@@ -22,2 +22,60 @@ // `npx replen immerse` — hosted Immersion sender (M2). | ||
| const MAX_FILE_BYTES = 1_000_000; // mirror the server per-file cap | ||
| // Never read or transmit these, no matter what the manifest names. The | ||
| // traversal guard in send() keeps reads inside the repo; this denylist keeps | ||
| // in-repo secrets (.env files, private keys, credential stores) out of the | ||
| // payload even if a compromised/hostile manifest asks for them. Matching is | ||
| // on lowercased path segments with trailing dots stripped, so ".ENV" or | ||
| // ".env." can't slip through. | ||
| const SENSITIVE_DIRS = new Set([".git", ".aws", ".ssh", ".gnupg", ".docker"]); | ||
| const SENSITIVE_NAMES = new Set([ | ||
| ".npmrc", ".netrc", ".envrc", ".pgpass", ".dockercfg", | ||
| "credentials", "serviceaccount.json", "service-account.json", | ||
| "terraform.tfstate", "terraform.tfstate.backup", | ||
| ]); | ||
| const SENSITIVE_EXTS = [ | ||
| ".pem", ".key", ".p12", ".pfx", ".pgp", ".gpg", ".asc", | ||
| ".jks", ".keystore", ".ppk", ".tfstate", | ||
| ]; | ||
| // Programming-source extensions. A file such as token.ts or secret.ts is SOURCE | ||
| // (grounding it is the whole point of immersion), so the secret-word heuristic | ||
| // below must not treat a code file as a credential store just because its name | ||
| // contains "token". Secrets live in config / data / dotfiles, not .ts modules. | ||
| const SOURCE_EXTS = new Set([ | ||
| ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", | ||
| ".rb", ".php", ".cs", ".c", ".cc", ".cpp", ".h", ".hpp", ".swift", ".kt", | ||
| ".scala", ".sh", ".css", ".scss", ".sass", ".html", ".vue", ".svelte", ".sql", | ||
| ]); | ||
| const SECRET_WORDS = new Set([ | ||
| "secret", "secrets", "credential", "credentials", "token", "tokens", | ||
| "password", "passwords", "passwd", | ||
| ]); | ||
| function isSensitivePath(rel) { | ||
| const segs = rel.split(/[\\/]/).map((s) => s.toLowerCase().replace(/\.+$/, "")); | ||
| if (segs.some((s) => SENSITIVE_DIRS.has(s))) | ||
| return true; | ||
| const base = segs[segs.length - 1] ?? ""; | ||
| if (SENSITIVE_NAMES.has(base)) | ||
| return true; | ||
| if (base === ".env" || base.startsWith(".env.")) | ||
| return true; | ||
| if (SENSITIVE_EXTS.some((ext) => base.endsWith(ext))) | ||
| return true; | ||
| // SSH / signing private keys: id_rsa, id_ed25519, id_ecdsa, id_dsa, and | ||
| // extensionless *_key / deploy_key files. | ||
| if (/^id_(rsa|dsa|ecdsa|ed25519)/.test(base)) | ||
| return true; | ||
| const dot = base.lastIndexOf("."); | ||
| const ext = dot > 0 ? base.slice(dot) : ""; | ||
| if (ext === "" && (base === "deploy_key" || base.endsWith("_key"))) | ||
| return true; | ||
| // Secret-word heuristic. Whole-word match (so "tokenizer.ts" is not caught), | ||
| // and never exclude a real source file: github-token.json is a secret store, | ||
| // token.ts is source that immersion is meant to send. | ||
| if (!SOURCE_EXTS.has(ext)) { | ||
| const words = base.split(/[^a-z0-9]+/).filter(Boolean); | ||
| if (words.some((w) => SECRET_WORDS.has(w))) | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| export async function runImmerse(argv) { | ||
@@ -90,2 +148,4 @@ const sub = (argv[0] ?? "").toLowerCase(); | ||
| continue; | ||
| if (isSensitivePath(rel)) | ||
| continue; // in-repo secret: never read, never send | ||
| const abs = resolve(root, rel); | ||
@@ -92,0 +152,0 @@ if (abs !== root && !abs.startsWith(root + sep)) |
+10
-1
@@ -450,3 +450,12 @@ // `npx replen uninstall` — reverse every local change `npx replen` made, | ||
| const ts = new Date().toISOString().replace(/[:.]/g, "-"); | ||
| writeFileSync(`${path}.bak.${ts}`, readFileSync(path)); | ||
| const backup = `${path}.bak.${ts}`; | ||
| // The backed-up config can carry tokens; keep it 0600 like the atomic | ||
| // write above, not the umask default (typically world-readable 0644). | ||
| writeFileSync(backup, readFileSync(path), { mode: 0o600 }); | ||
| try { | ||
| chmodSync(backup, 0o600); | ||
| } | ||
| catch { | ||
| /* best-effort */ | ||
| } | ||
| } | ||
@@ -453,0 +462,0 @@ // ============================================================================ |
@@ -36,5 +36,11 @@ --- | ||
| curl -sS -H "x-digest-token: $TOKEN" \ | ||
| "$BASE/api/inventory/today?repo=<owner/name>&days=2&limit=10" | ||
| "$BASE/api/inventory/today?repo=<owner/name>&limit=10" | ||
| ``` | ||
| Do NOT pass `days`. Omitting it lets the server pick the window adaptively: | ||
| a wide first-run window for a repo that's never surfaced a match, then a | ||
| since-last-open window (everything found since you last opened this repo, | ||
| floored at ~a month) on later opens. Pass `days` only to force a fixed | ||
| lookback for debugging. | ||
| Parse the JSON response. Note: | ||
@@ -41,0 +47,0 @@ |
+1
-1
| { | ||
| "name": "replen", | ||
| "version": "1.5.5", | ||
| "version": "1.5.6", | ||
| "description": "Make your AI coding tools smarter. One command, no API keys, free. Replen watches what your projects actually do and surfaces a few things worth bringing in each month. Use one as is, port a piece of another, cherry pick an idea, or build it clean room. The match happens inside your AI tool's session. A few actionable matches a month, by design.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
236792
1.5%4024
1.74%