@node9/proxy 1.25.0 → 1.26.1

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 CHANGED
@@ -2273,6 +2273,42 @@ var init_dist = __esm({
2273
2273
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
2274
2274
  severity: "block",
2275
2275
  keywords: ["age-secret-key-"]
2276
+ },
2277
+ // ── Database connection strings ───────────────────────────────────────────
2278
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
2279
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
2280
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
2281
+ // the password value never appears in the redacted sample.
2282
+ //
2283
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
2284
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
2285
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
2286
+ // excluded — they're not database URLs and adding them would
2287
+ // create false positives on every basic-auth URL in the wild.
2288
+ //
2289
+ // Requires `:password@` (4+ char password) so user-only URLs like
2290
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
2291
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
2292
+ {
2293
+ name: "Database Connection String",
2294
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
2295
+ severity: "block",
2296
+ keywords: [
2297
+ "redis://",
2298
+ "rediss://",
2299
+ "postgres://",
2300
+ "postgresql://",
2301
+ "mongodb://",
2302
+ "mongodb+srv://",
2303
+ "mysql://",
2304
+ "mariadb://",
2305
+ "amqp://",
2306
+ "amqps://",
2307
+ "kafka://",
2308
+ "clickhouse://",
2309
+ "cassandra://"
2310
+ ],
2311
+ minEntropy: 3
2276
2312
  }
2277
2313
  ];
2278
2314
  DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -2375,7 +2411,7 @@ var init_dist = __esm({
2375
2411
  },
2376
2412
  {
2377
2413
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2378
- // review-read-env-any-tool) so the AST FS-op path catches the
2414
+ // block-read-env-any-tool) so the AST FS-op path catches the
2379
2415
  // same set the regex shield does — including Next.js / Vite's
2380
2416
  // `.env.<env>.local` double-suffix overrides which are commonly
2381
2417
  // gitignored AND commonly contain real secrets.
@@ -3121,7 +3157,7 @@ var init_dist = __esm({
3121
3157
  {
3122
3158
  field: "command",
3123
3159
  op: "matches",
3124
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3160
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
3125
3161
  flags: "i"
3126
3162
  }
3127
3163
  ],
@@ -3135,7 +3171,7 @@ var init_dist = __esm({
3135
3171
  {
3136
3172
  field: "command",
3137
3173
  op: "matches",
3138
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3174
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
3139
3175
  flags: "i"
3140
3176
  }
3141
3177
  ],
@@ -3149,7 +3185,7 @@ var init_dist = __esm({
3149
3185
  {
3150
3186
  field: "command",
3151
3187
  op: "matches",
3152
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
3188
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
3153
3189
  flags: "i"
3154
3190
  }
3155
3191
  ],
@@ -3163,7 +3199,7 @@ var init_dist = __esm({
3163
3199
  {
3164
3200
  field: "command",
3165
3201
  op: "matches",
3166
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3202
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3167
3203
  flags: "i"
3168
3204
  }
3169
3205
  ],
@@ -3199,7 +3235,7 @@ var init_dist = __esm({
3199
3235
  reason: "Reading AWS credentials is blocked by project-jail shield"
3200
3236
  },
3201
3237
  {
3202
- name: "shield:project-jail:review-read-env-any-tool",
3238
+ name: "shield:project-jail:block-read-env-any-tool",
3203
3239
  tool: "*",
3204
3240
  conditions: [
3205
3241
  {
@@ -3209,8 +3245,8 @@ var init_dist = __esm({
3209
3245
  flags: "i"
3210
3246
  }
3211
3247
  ],
3212
- verdict: "review",
3213
- reason: "Reading .env files requires approval (project-jail shield)"
3248
+ verdict: "block",
3249
+ reason: "Reading .env files is blocked by project-jail shield"
3214
3250
  },
3215
3251
  {
3216
3252
  name: "shield:project-jail:review-read-credentials-any-tool",
@@ -3471,6 +3507,30 @@ function writeShieldOverride(shieldName, ruleName, verdict) {
3471
3507
  overrides[shieldName] = { ...overrides[shieldName] ?? {}, [ruleName]: verdict };
3472
3508
  writeShieldsFile({ ...current, overrides });
3473
3509
  }
3510
+ function migrateRenamedRuleKeys() {
3511
+ const current = readShieldsFile();
3512
+ if (!current.overrides || Object.keys(current.overrides).length === 0) return [];
3513
+ const renameMap = new Map(RULE_KEY_MIGRATIONS);
3514
+ const migrated = [];
3515
+ const nextOverrides = {};
3516
+ let anyChange = false;
3517
+ for (const [shield, rules] of Object.entries(current.overrides)) {
3518
+ const nextRules = {};
3519
+ for (const [key, verdict] of Object.entries(rules)) {
3520
+ const newKey = renameMap.get(key);
3521
+ if (newKey) {
3522
+ if (!(newKey in nextRules)) nextRules[newKey] = verdict;
3523
+ migrated.push({ shield, oldKey: key, newKey });
3524
+ anyChange = true;
3525
+ } else {
3526
+ nextRules[key] = verdict;
3527
+ }
3528
+ }
3529
+ if (Object.keys(nextRules).length > 0) nextOverrides[shield] = nextRules;
3530
+ }
3531
+ if (anyChange) writeShieldsFile({ ...current, overrides: nextOverrides });
3532
+ return migrated;
3533
+ }
3474
3534
  function clearShieldOverride(shieldName, ruleName) {
3475
3535
  const current = readShieldsFile();
3476
3536
  if (!current.overrides?.[shieldName]?.[ruleName]) return;
@@ -3519,7 +3579,7 @@ function installShield(name, shieldJson) {
3519
3579
  import_fs2.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
3520
3580
  import_fs2.default.renameSync(tmp, filePath);
3521
3581
  }
3522
- var import_fs2, import_path2, import_os2, import_crypto3, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
3582
+ var import_fs2, import_path2, import_os2, import_crypto3, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS;
3523
3583
  var init_shields = __esm({
3524
3584
  "src/shields.ts"() {
3525
3585
  "use strict";
@@ -3532,6 +3592,14 @@ var init_shields = __esm({
3532
3592
  USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
3533
3593
  SHIELDS = buildSHIELDS();
3534
3594
  SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
3595
+ RULE_KEY_MIGRATIONS = [
3596
+ // 2026-05-21 — project-jail .env reads promoted review → block. Renamed
3597
+ // so the key honestly describes the verdict. Credential-file rules
3598
+ // (.netrc, .npmrc, .docker, .kube, gcloud) deliberately stay at `review`
3599
+ // — see the rationale in packages/policy-engine/src/shell/index.ts
3600
+ // around the review-read-credentials rule.
3601
+ ["shield:project-jail:review-read-env-any-tool", "shield:project-jail:block-read-env-any-tool"]
3602
+ ];
3535
3603
  }
3536
3604
  });
3537
3605
 
@@ -7035,6 +7103,18 @@ function claudeDesktopConfigPath(homeDir2 = import_os12.default.homedir()) {
7035
7103
  }
7036
7104
  return null;
7037
7105
  }
7106
+ function binaryInPath(binary) {
7107
+ const pathEnv = process.env.PATH ?? "";
7108
+ for (const dir of pathEnv.split(import_path15.default.delimiter)) {
7109
+ if (!dir) continue;
7110
+ try {
7111
+ import_fs13.default.accessSync(import_path15.default.join(dir, binary), import_fs13.default.constants.X_OK);
7112
+ return true;
7113
+ } catch {
7114
+ }
7115
+ }
7116
+ return false;
7117
+ }
7038
7118
  function detectAgents(homeDir2 = import_os12.default.homedir()) {
7039
7119
  const exists = (p) => {
7040
7120
  try {
@@ -7057,7 +7137,9 @@ function detectAgents(homeDir2 = import_os12.default.homedir()) {
7057
7137
  windsurf: exists(import_path15.default.join(homeDir2, ".codeium", "windsurf")),
7058
7138
  vscode: exists(import_path15.default.join(homeDir2, ".vscode")),
7059
7139
  claudeDesktop: desktopPath !== null && exists(import_path15.default.dirname(desktopPath)),
7060
- opencode: exists(import_path15.default.join(homeDir2, ".config", "opencode"))
7140
+ // Opencode creates ~/.config/opencode lazily on first launch — fall back
7141
+ // to a PATH lookup so installed-but-never-launched CLIs are still wired.
7142
+ opencode: exists(import_path15.default.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode")
7061
7143
  };
7062
7144
  }
7063
7145
  async function setupCursor() {
@@ -18744,14 +18826,18 @@ init_setup();
18744
18826
  init_shields();
18745
18827
  init_service();
18746
18828
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "project-jail"];
18747
- function fireTelemetryPing(agents) {
18829
+ function buildTelemetryPayload(agents, firstInstall) {
18830
+ return {
18831
+ event: "init_completed",
18832
+ agents_detected: agents,
18833
+ os: process.platform,
18834
+ node9_version: node9Version(),
18835
+ first_install: firstInstall
18836
+ };
18837
+ }
18838
+ function fireTelemetryPing(agents, firstInstall) {
18748
18839
  try {
18749
- const body = JSON.stringify({
18750
- event: "init_completed",
18751
- agents_detected: agents,
18752
- os: process.platform,
18753
- node9_version: process.env.npm_package_version ?? "unknown"
18754
- });
18840
+ const body = JSON.stringify(buildTelemetryPayload(agents, firstInstall));
18755
18841
  const req = import_https4.default.request(
18756
18842
  {
18757
18843
  hostname: "api.node9.ai",
@@ -18784,6 +18870,12 @@ function registerInitCommand(program2) {
18784
18870
  ).action(
18785
18871
  async (options) => {
18786
18872
  console.log(import_chalk16.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
18873
+ {
18874
+ const migrated = migrateRenamedRuleKeys();
18875
+ for (const m of migrated) {
18876
+ console.log(import_chalk16.default.dim(` \u{1F527} Rule renamed: ${m.oldKey} \u2192 ${m.newKey}`));
18877
+ }
18878
+ }
18787
18879
  let chosenMode = options.mode.toLowerCase();
18788
18880
  if (!["standard", "strict", "audit", "observe"].includes(chosenMode)) {
18789
18881
  chosenMode = DEFAULT_CONFIG.settings.mode;
@@ -18818,6 +18910,7 @@ function registerInitCommand(program2) {
18818
18910
  console.log("");
18819
18911
  }
18820
18912
  const configPath = import_path39.default.join(import_os34.default.homedir(), ".node9", "config.json");
18913
+ const isFirstInstall = !import_fs38.default.existsSync(configPath);
18821
18914
  if (import_fs38.default.existsSync(configPath) && !options.force) {
18822
18915
  try {
18823
18916
  const existing = JSON.parse(import_fs38.default.readFileSync(configPath, "utf-8"));
@@ -18921,7 +19014,7 @@ function registerInitCommand(program2) {
18921
19014
  message: "Send anonymous usage stats to help improve node9? (no code, no args)",
18922
19015
  default: true
18923
19016
  });
18924
- if (sendTelemetry) fireTelemetryPing(found);
19017
+ if (sendTelemetry) fireTelemetryPing(found, isFirstInstall);
18925
19018
  console.log("");
18926
19019
  }
18927
19020
  const agentList = found.join(", ");
package/dist/cli.mjs CHANGED
@@ -2251,6 +2251,42 @@ var init_dist = __esm({
2251
2251
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
2252
2252
  severity: "block",
2253
2253
  keywords: ["age-secret-key-"]
2254
+ },
2255
+ // ── Database connection strings ───────────────────────────────────────────
2256
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
2257
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
2258
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
2259
+ // the password value never appears in the redacted sample.
2260
+ //
2261
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
2262
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
2263
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
2264
+ // excluded — they're not database URLs and adding them would
2265
+ // create false positives on every basic-auth URL in the wild.
2266
+ //
2267
+ // Requires `:password@` (4+ char password) so user-only URLs like
2268
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
2269
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
2270
+ {
2271
+ name: "Database Connection String",
2272
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
2273
+ severity: "block",
2274
+ keywords: [
2275
+ "redis://",
2276
+ "rediss://",
2277
+ "postgres://",
2278
+ "postgresql://",
2279
+ "mongodb://",
2280
+ "mongodb+srv://",
2281
+ "mysql://",
2282
+ "mariadb://",
2283
+ "amqp://",
2284
+ "amqps://",
2285
+ "kafka://",
2286
+ "clickhouse://",
2287
+ "cassandra://"
2288
+ ],
2289
+ minEntropy: 3
2254
2290
  }
2255
2291
  ];
2256
2292
  DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -2353,7 +2389,7 @@ var init_dist = __esm({
2353
2389
  },
2354
2390
  {
2355
2391
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2356
- // review-read-env-any-tool) so the AST FS-op path catches the
2392
+ // block-read-env-any-tool) so the AST FS-op path catches the
2357
2393
  // same set the regex shield does — including Next.js / Vite's
2358
2394
  // `.env.<env>.local` double-suffix overrides which are commonly
2359
2395
  // gitignored AND commonly contain real secrets.
@@ -3099,7 +3135,7 @@ var init_dist = __esm({
3099
3135
  {
3100
3136
  field: "command",
3101
3137
  op: "matches",
3102
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3138
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
3103
3139
  flags: "i"
3104
3140
  }
3105
3141
  ],
@@ -3113,7 +3149,7 @@ var init_dist = __esm({
3113
3149
  {
3114
3150
  field: "command",
3115
3151
  op: "matches",
3116
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3152
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
3117
3153
  flags: "i"
3118
3154
  }
3119
3155
  ],
@@ -3127,7 +3163,7 @@ var init_dist = __esm({
3127
3163
  {
3128
3164
  field: "command",
3129
3165
  op: "matches",
3130
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
3166
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
3131
3167
  flags: "i"
3132
3168
  }
3133
3169
  ],
@@ -3141,7 +3177,7 @@ var init_dist = __esm({
3141
3177
  {
3142
3178
  field: "command",
3143
3179
  op: "matches",
3144
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3180
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3145
3181
  flags: "i"
3146
3182
  }
3147
3183
  ],
@@ -3177,7 +3213,7 @@ var init_dist = __esm({
3177
3213
  reason: "Reading AWS credentials is blocked by project-jail shield"
3178
3214
  },
3179
3215
  {
3180
- name: "shield:project-jail:review-read-env-any-tool",
3216
+ name: "shield:project-jail:block-read-env-any-tool",
3181
3217
  tool: "*",
3182
3218
  conditions: [
3183
3219
  {
@@ -3187,8 +3223,8 @@ var init_dist = __esm({
3187
3223
  flags: "i"
3188
3224
  }
3189
3225
  ],
3190
- verdict: "review",
3191
- reason: "Reading .env files requires approval (project-jail shield)"
3226
+ verdict: "block",
3227
+ reason: "Reading .env files is blocked by project-jail shield"
3192
3228
  },
3193
3229
  {
3194
3230
  name: "shield:project-jail:review-read-credentials-any-tool",
@@ -3453,6 +3489,30 @@ function writeShieldOverride(shieldName, ruleName, verdict) {
3453
3489
  overrides[shieldName] = { ...overrides[shieldName] ?? {}, [ruleName]: verdict };
3454
3490
  writeShieldsFile({ ...current, overrides });
3455
3491
  }
3492
+ function migrateRenamedRuleKeys() {
3493
+ const current = readShieldsFile();
3494
+ if (!current.overrides || Object.keys(current.overrides).length === 0) return [];
3495
+ const renameMap = new Map(RULE_KEY_MIGRATIONS);
3496
+ const migrated = [];
3497
+ const nextOverrides = {};
3498
+ let anyChange = false;
3499
+ for (const [shield, rules] of Object.entries(current.overrides)) {
3500
+ const nextRules = {};
3501
+ for (const [key, verdict] of Object.entries(rules)) {
3502
+ const newKey = renameMap.get(key);
3503
+ if (newKey) {
3504
+ if (!(newKey in nextRules)) nextRules[newKey] = verdict;
3505
+ migrated.push({ shield, oldKey: key, newKey });
3506
+ anyChange = true;
3507
+ } else {
3508
+ nextRules[key] = verdict;
3509
+ }
3510
+ }
3511
+ if (Object.keys(nextRules).length > 0) nextOverrides[shield] = nextRules;
3512
+ }
3513
+ if (anyChange) writeShieldsFile({ ...current, overrides: nextOverrides });
3514
+ return migrated;
3515
+ }
3456
3516
  function clearShieldOverride(shieldName, ruleName) {
3457
3517
  const current = readShieldsFile();
3458
3518
  if (!current.overrides?.[shieldName]?.[ruleName]) return;
@@ -3501,7 +3561,7 @@ function installShield(name, shieldJson) {
3501
3561
  fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
3502
3562
  fs2.renameSync(tmp, filePath);
3503
3563
  }
3504
- var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
3564
+ var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS;
3505
3565
  var init_shields = __esm({
3506
3566
  "src/shields.ts"() {
3507
3567
  "use strict";
@@ -3510,6 +3570,14 @@ var init_shields = __esm({
3510
3570
  USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
3511
3571
  SHIELDS = buildSHIELDS();
3512
3572
  SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
3573
+ RULE_KEY_MIGRATIONS = [
3574
+ // 2026-05-21 — project-jail .env reads promoted review → block. Renamed
3575
+ // so the key honestly describes the verdict. Credential-file rules
3576
+ // (.netrc, .npmrc, .docker, .kube, gcloud) deliberately stay at `review`
3577
+ // — see the rationale in packages/policy-engine/src/shell/index.ts
3578
+ // around the review-read-credentials rule.
3579
+ ["shield:project-jail:review-read-env-any-tool", "shield:project-jail:block-read-env-any-tool"]
3580
+ ];
3513
3581
  }
3514
3582
  });
3515
3583
 
@@ -7016,6 +7084,18 @@ function claudeDesktopConfigPath(homeDir2 = os12.homedir()) {
7016
7084
  }
7017
7085
  return null;
7018
7086
  }
7087
+ function binaryInPath(binary) {
7088
+ const pathEnv = process.env.PATH ?? "";
7089
+ for (const dir of pathEnv.split(path15.delimiter)) {
7090
+ if (!dir) continue;
7091
+ try {
7092
+ fs13.accessSync(path15.join(dir, binary), fs13.constants.X_OK);
7093
+ return true;
7094
+ } catch {
7095
+ }
7096
+ }
7097
+ return false;
7098
+ }
7019
7099
  function detectAgents(homeDir2 = os12.homedir()) {
7020
7100
  const exists = (p) => {
7021
7101
  try {
@@ -7038,7 +7118,9 @@ function detectAgents(homeDir2 = os12.homedir()) {
7038
7118
  windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
7039
7119
  vscode: exists(path15.join(homeDir2, ".vscode")),
7040
7120
  claudeDesktop: desktopPath !== null && exists(path15.dirname(desktopPath)),
7041
- opencode: exists(path15.join(homeDir2, ".config", "opencode"))
7121
+ // Opencode creates ~/.config/opencode lazily on first launch — fall back
7122
+ // to a PATH lookup so installed-but-never-launched CLIs are still wired.
7123
+ opencode: exists(path15.join(homeDir2, ".config", "opencode")) || binaryInPath("opencode")
7042
7124
  };
7043
7125
  }
7044
7126
  async function setupCursor() {
@@ -18716,14 +18798,18 @@ import path39 from "path";
18716
18798
  import os34 from "os";
18717
18799
  import https4 from "https";
18718
18800
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "project-jail"];
18719
- function fireTelemetryPing(agents) {
18801
+ function buildTelemetryPayload(agents, firstInstall) {
18802
+ return {
18803
+ event: "init_completed",
18804
+ agents_detected: agents,
18805
+ os: process.platform,
18806
+ node9_version: node9Version(),
18807
+ first_install: firstInstall
18808
+ };
18809
+ }
18810
+ function fireTelemetryPing(agents, firstInstall) {
18720
18811
  try {
18721
- const body = JSON.stringify({
18722
- event: "init_completed",
18723
- agents_detected: agents,
18724
- os: process.platform,
18725
- node9_version: process.env.npm_package_version ?? "unknown"
18726
- });
18812
+ const body = JSON.stringify(buildTelemetryPayload(agents, firstInstall));
18727
18813
  const req = https4.request(
18728
18814
  {
18729
18815
  hostname: "api.node9.ai",
@@ -18756,6 +18842,12 @@ function registerInitCommand(program2) {
18756
18842
  ).action(
18757
18843
  async (options) => {
18758
18844
  console.log(chalk16.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
18845
+ {
18846
+ const migrated = migrateRenamedRuleKeys();
18847
+ for (const m of migrated) {
18848
+ console.log(chalk16.dim(` \u{1F527} Rule renamed: ${m.oldKey} \u2192 ${m.newKey}`));
18849
+ }
18850
+ }
18759
18851
  let chosenMode = options.mode.toLowerCase();
18760
18852
  if (!["standard", "strict", "audit", "observe"].includes(chosenMode)) {
18761
18853
  chosenMode = DEFAULT_CONFIG.settings.mode;
@@ -18790,6 +18882,7 @@ function registerInitCommand(program2) {
18790
18882
  console.log("");
18791
18883
  }
18792
18884
  const configPath = path39.join(os34.homedir(), ".node9", "config.json");
18885
+ const isFirstInstall = !fs38.existsSync(configPath);
18793
18886
  if (fs38.existsSync(configPath) && !options.force) {
18794
18887
  try {
18795
18888
  const existing = JSON.parse(fs38.readFileSync(configPath, "utf-8"));
@@ -18893,7 +18986,7 @@ function registerInitCommand(program2) {
18893
18986
  message: "Send anonymous usage stats to help improve node9? (no code, no args)",
18894
18987
  default: true
18895
18988
  });
18896
- if (sendTelemetry) fireTelemetryPing(found);
18989
+ if (sendTelemetry) fireTelemetryPing(found, isFirstInstall);
18897
18990
  console.log("");
18898
18991
  }
18899
18992
  const agentList = found.join(", ");
@@ -1030,6 +1030,42 @@ var init_dist = __esm({
1030
1030
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
1031
1031
  severity: "block",
1032
1032
  keywords: ["age-secret-key-"]
1033
+ },
1034
+ // ── Database connection strings ───────────────────────────────────────────
1035
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
1036
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
1037
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
1038
+ // the password value never appears in the redacted sample.
1039
+ //
1040
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
1041
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
1042
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
1043
+ // excluded — they're not database URLs and adding them would
1044
+ // create false positives on every basic-auth URL in the wild.
1045
+ //
1046
+ // Requires `:password@` (4+ char password) so user-only URLs like
1047
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
1048
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
1049
+ {
1050
+ name: "Database Connection String",
1051
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
1052
+ severity: "block",
1053
+ keywords: [
1054
+ "redis://",
1055
+ "rediss://",
1056
+ "postgres://",
1057
+ "postgresql://",
1058
+ "mongodb://",
1059
+ "mongodb+srv://",
1060
+ "mysql://",
1061
+ "mariadb://",
1062
+ "amqp://",
1063
+ "amqps://",
1064
+ "kafka://",
1065
+ "clickhouse://",
1066
+ "cassandra://"
1067
+ ],
1068
+ minEntropy: 3
1033
1069
  }
1034
1070
  ];
1035
1071
  DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -1132,7 +1168,7 @@ var init_dist = __esm({
1132
1168
  },
1133
1169
  {
1134
1170
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
1135
- // review-read-env-any-tool) so the AST FS-op path catches the
1171
+ // block-read-env-any-tool) so the AST FS-op path catches the
1136
1172
  // same set the regex shield does — including Next.js / Vite's
1137
1173
  // `.env.<env>.local` double-suffix overrides which are commonly
1138
1174
  // gitignored AND commonly contain real secrets.
@@ -1742,7 +1778,7 @@ var init_dist = __esm({
1742
1778
  {
1743
1779
  field: "command",
1744
1780
  op: "matches",
1745
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
1781
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
1746
1782
  flags: "i"
1747
1783
  }
1748
1784
  ],
@@ -1756,7 +1792,7 @@ var init_dist = __esm({
1756
1792
  {
1757
1793
  field: "command",
1758
1794
  op: "matches",
1759
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
1795
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
1760
1796
  flags: "i"
1761
1797
  }
1762
1798
  ],
@@ -1770,7 +1806,7 @@ var init_dist = __esm({
1770
1806
  {
1771
1807
  field: "command",
1772
1808
  op: "matches",
1773
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
1809
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
1774
1810
  flags: "i"
1775
1811
  }
1776
1812
  ],
@@ -1784,7 +1820,7 @@ var init_dist = __esm({
1784
1820
  {
1785
1821
  field: "command",
1786
1822
  op: "matches",
1787
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
1823
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
1788
1824
  flags: "i"
1789
1825
  }
1790
1826
  ],
@@ -1820,7 +1856,7 @@ var init_dist = __esm({
1820
1856
  reason: "Reading AWS credentials is blocked by project-jail shield"
1821
1857
  },
1822
1858
  {
1823
- name: "shield:project-jail:review-read-env-any-tool",
1859
+ name: "shield:project-jail:block-read-env-any-tool",
1824
1860
  tool: "*",
1825
1861
  conditions: [
1826
1862
  {
@@ -1830,8 +1866,8 @@ var init_dist = __esm({
1830
1866
  flags: "i"
1831
1867
  }
1832
1868
  ],
1833
- verdict: "review",
1834
- reason: "Reading .env files requires approval (project-jail shield)"
1869
+ verdict: "block",
1870
+ reason: "Reading .env files is blocked by project-jail shield"
1835
1871
  },
1836
1872
  {
1837
1873
  name: "shield:project-jail:review-read-credentials-any-tool",
package/dist/index.js CHANGED
@@ -739,6 +739,42 @@ var DLP_PATTERNS = [
739
739
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
740
740
  severity: "block",
741
741
  keywords: ["age-secret-key-"]
742
+ },
743
+ // ── Database connection strings ───────────────────────────────────────────
744
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
745
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
746
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
747
+ // the password value never appears in the redacted sample.
748
+ //
749
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
750
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
751
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
752
+ // excluded — they're not database URLs and adding them would
753
+ // create false positives on every basic-auth URL in the wild.
754
+ //
755
+ // Requires `:password@` (4+ char password) so user-only URLs like
756
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
757
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
758
+ {
759
+ name: "Database Connection String",
760
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
761
+ severity: "block",
762
+ keywords: [
763
+ "redis://",
764
+ "rediss://",
765
+ "postgres://",
766
+ "postgresql://",
767
+ "mongodb://",
768
+ "mongodb+srv://",
769
+ "mysql://",
770
+ "mariadb://",
771
+ "amqp://",
772
+ "amqps://",
773
+ "kafka://",
774
+ "clickhouse://",
775
+ "cassandra://"
776
+ ],
777
+ minEntropy: 3
742
778
  }
743
779
  ];
744
780
  var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -1092,7 +1128,7 @@ var SENSITIVE_PATH_RULES = [
1092
1128
  },
1093
1129
  {
1094
1130
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
1095
- // review-read-env-any-tool) so the AST FS-op path catches the
1131
+ // block-read-env-any-tool) so the AST FS-op path catches the
1096
1132
  // same set the regex shield does — including Next.js / Vite's
1097
1133
  // `.env.<env>.local` double-suffix overrides which are commonly
1098
1134
  // gitignored AND commonly contain real secrets.
@@ -2554,7 +2590,7 @@ var project_jail_default = {
2554
2590
  {
2555
2591
  field: "command",
2556
2592
  op: "matches",
2557
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
2593
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
2558
2594
  flags: "i"
2559
2595
  }
2560
2596
  ],
@@ -2568,7 +2604,7 @@ var project_jail_default = {
2568
2604
  {
2569
2605
  field: "command",
2570
2606
  op: "matches",
2571
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
2607
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
2572
2608
  flags: "i"
2573
2609
  }
2574
2610
  ],
@@ -2582,7 +2618,7 @@ var project_jail_default = {
2582
2618
  {
2583
2619
  field: "command",
2584
2620
  op: "matches",
2585
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
2621
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
2586
2622
  flags: "i"
2587
2623
  }
2588
2624
  ],
@@ -2596,7 +2632,7 @@ var project_jail_default = {
2596
2632
  {
2597
2633
  field: "command",
2598
2634
  op: "matches",
2599
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
2635
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
2600
2636
  flags: "i"
2601
2637
  }
2602
2638
  ],
@@ -2632,7 +2668,7 @@ var project_jail_default = {
2632
2668
  reason: "Reading AWS credentials is blocked by project-jail shield"
2633
2669
  },
2634
2670
  {
2635
- name: "shield:project-jail:review-read-env-any-tool",
2671
+ name: "shield:project-jail:block-read-env-any-tool",
2636
2672
  tool: "*",
2637
2673
  conditions: [
2638
2674
  {
@@ -2642,8 +2678,8 @@ var project_jail_default = {
2642
2678
  flags: "i"
2643
2679
  }
2644
2680
  ],
2645
- verdict: "review",
2646
- reason: "Reading .env files requires approval (project-jail shield)"
2681
+ verdict: "block",
2682
+ reason: "Reading .env files is blocked by project-jail shield"
2647
2683
  },
2648
2684
  {
2649
2685
  name: "shield:project-jail:review-read-credentials-any-tool",
package/dist/index.mjs CHANGED
@@ -709,6 +709,42 @@ var DLP_PATTERNS = [
709
709
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
710
710
  severity: "block",
711
711
  keywords: ["age-secret-key-"]
712
+ },
713
+ // ── Database connection strings ───────────────────────────────────────────
714
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
715
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
716
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
717
+ // the password value never appears in the redacted sample.
718
+ //
719
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
720
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
721
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
722
+ // excluded — they're not database URLs and adding them would
723
+ // create false positives on every basic-auth URL in the wild.
724
+ //
725
+ // Requires `:password@` (4+ char password) so user-only URLs like
726
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
727
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
728
+ {
729
+ name: "Database Connection String",
730
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
731
+ severity: "block",
732
+ keywords: [
733
+ "redis://",
734
+ "rediss://",
735
+ "postgres://",
736
+ "postgresql://",
737
+ "mongodb://",
738
+ "mongodb+srv://",
739
+ "mysql://",
740
+ "mariadb://",
741
+ "amqp://",
742
+ "amqps://",
743
+ "kafka://",
744
+ "clickhouse://",
745
+ "cassandra://"
746
+ ],
747
+ minEntropy: 3
712
748
  }
713
749
  ];
714
750
  var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -1062,7 +1098,7 @@ var SENSITIVE_PATH_RULES = [
1062
1098
  },
1063
1099
  {
1064
1100
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
1065
- // review-read-env-any-tool) so the AST FS-op path catches the
1101
+ // block-read-env-any-tool) so the AST FS-op path catches the
1066
1102
  // same set the regex shield does — including Next.js / Vite's
1067
1103
  // `.env.<env>.local` double-suffix overrides which are commonly
1068
1104
  // gitignored AND commonly contain real secrets.
@@ -2524,7 +2560,7 @@ var project_jail_default = {
2524
2560
  {
2525
2561
  field: "command",
2526
2562
  op: "matches",
2527
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
2563
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
2528
2564
  flags: "i"
2529
2565
  }
2530
2566
  ],
@@ -2538,7 +2574,7 @@ var project_jail_default = {
2538
2574
  {
2539
2575
  field: "command",
2540
2576
  op: "matches",
2541
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
2577
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
2542
2578
  flags: "i"
2543
2579
  }
2544
2580
  ],
@@ -2552,7 +2588,7 @@ var project_jail_default = {
2552
2588
  {
2553
2589
  field: "command",
2554
2590
  op: "matches",
2555
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
2591
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
2556
2592
  flags: "i"
2557
2593
  }
2558
2594
  ],
@@ -2566,7 +2602,7 @@ var project_jail_default = {
2566
2602
  {
2567
2603
  field: "command",
2568
2604
  op: "matches",
2569
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
2605
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
2570
2606
  flags: "i"
2571
2607
  }
2572
2608
  ],
@@ -2602,7 +2638,7 @@ var project_jail_default = {
2602
2638
  reason: "Reading AWS credentials is blocked by project-jail shield"
2603
2639
  },
2604
2640
  {
2605
- name: "shield:project-jail:review-read-env-any-tool",
2641
+ name: "shield:project-jail:block-read-env-any-tool",
2606
2642
  tool: "*",
2607
2643
  conditions: [
2608
2644
  {
@@ -2612,8 +2648,8 @@ var project_jail_default = {
2612
2648
  flags: "i"
2613
2649
  }
2614
2650
  ],
2615
- verdict: "review",
2616
- reason: "Reading .env files requires approval (project-jail shield)"
2651
+ verdict: "block",
2652
+ reason: "Reading .env files is blocked by project-jail shield"
2617
2653
  },
2618
2654
  {
2619
2655
  name: "shield:project-jail:review-read-credentials-any-tool",
package/dist/scan-ink.mjs CHANGED
@@ -729,6 +729,42 @@ var DLP_PATTERNS = [
729
729
  regex: /\bAGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JNLH]{58}\b/,
730
730
  severity: "block",
731
731
  keywords: ["age-secret-key-"]
732
+ },
733
+ // ── Database connection strings ───────────────────────────────────────────
734
+ // Universal <scheme>://[user]:<password>@<host> shape. Covers the gap
735
+ // vendor-prefix patterns (AWS / GitHub / Stripe / …) leave open. Matches
736
+ // the whole URL so maskSecret produces `<scheme>...:****@...<host>` —
737
+ // the password value never appears in the redacted sample.
738
+ //
739
+ // Schemes covered: redis, rediss (TLS), postgres, postgresql,
740
+ // mongodb, mongodb+srv, mysql, mariadb, amqp, amqps, kafka,
741
+ // clickhouse, cassandra. HTTP(S) / FTP / SSH are intentionally
742
+ // excluded — they're not database URLs and adding them would
743
+ // create false positives on every basic-auth URL in the wild.
744
+ //
745
+ // Requires `:password@` (4+ char password) so user-only URLs like
746
+ // `redis://user@host` don't match. Stopwords ('your', '${', '<your',
747
+ // 'placeholder', 'changeme', etc.) keep doc/README scans clean.
748
+ {
749
+ name: "Database Connection String",
750
+ regex: /\b(redis|rediss|postgres|postgresql|mongodb|mongodb\+srv|mysql|mariadb|amqp|amqps|kafka|clickhouse|cassandra):\/\/[^:/\s@]*:[^@\s]{4,}@[^\s/]+/,
751
+ severity: "block",
752
+ keywords: [
753
+ "redis://",
754
+ "rediss://",
755
+ "postgres://",
756
+ "postgresql://",
757
+ "mongodb://",
758
+ "mongodb+srv://",
759
+ "mysql://",
760
+ "mariadb://",
761
+ "amqp://",
762
+ "amqps://",
763
+ "kafka://",
764
+ "clickhouse://",
765
+ "cassandra://"
766
+ ],
767
+ minEntropy: 3
732
768
  }
733
769
  ];
734
770
  var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
@@ -1333,7 +1369,7 @@ var project_jail_default = {
1333
1369
  {
1334
1370
  field: "command",
1335
1371
  op: "matches",
1336
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
1372
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
1337
1373
  flags: "i"
1338
1374
  }
1339
1375
  ],
@@ -1347,7 +1383,7 @@ var project_jail_default = {
1347
1383
  {
1348
1384
  field: "command",
1349
1385
  op: "matches",
1350
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
1386
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
1351
1387
  flags: "i"
1352
1388
  }
1353
1389
  ],
@@ -1361,7 +1397,7 @@ var project_jail_default = {
1361
1397
  {
1362
1398
  field: "command",
1363
1399
  op: "matches",
1364
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*\\.env(\\.local|\\.production|\\.staging)?\\b",
1400
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
1365
1401
  flags: "i"
1366
1402
  }
1367
1403
  ],
@@ -1375,7 +1411,7 @@ var project_jail_default = {
1375
1411
  {
1376
1412
  field: "command",
1377
1413
  op: "matches",
1378
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
1414
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
1379
1415
  flags: "i"
1380
1416
  }
1381
1417
  ],
@@ -1411,7 +1447,7 @@ var project_jail_default = {
1411
1447
  reason: "Reading AWS credentials is blocked by project-jail shield"
1412
1448
  },
1413
1449
  {
1414
- name: "shield:project-jail:review-read-env-any-tool",
1450
+ name: "shield:project-jail:block-read-env-any-tool",
1415
1451
  tool: "*",
1416
1452
  conditions: [
1417
1453
  {
@@ -1421,8 +1457,8 @@ var project_jail_default = {
1421
1457
  flags: "i"
1422
1458
  }
1423
1459
  ],
1424
- verdict: "review",
1425
- reason: "Reading .env files requires approval (project-jail shield)"
1460
+ verdict: "block",
1461
+ reason: "Reading .env files is blocked by project-jail shield"
1426
1462
  },
1427
1463
  {
1428
1464
  name: "shield:project-jail:review-read-credentials-any-tool",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.25.0",
3
+ "version": "1.26.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",