@node9/proxy 1.7.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -250,6 +250,70 @@ import fs2 from "fs";
250
250
  import path2 from "path";
251
251
  import os2 from "os";
252
252
  import crypto from "crypto";
253
+ function validateShieldDefinition(raw, filePath) {
254
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
255
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
256
+ `);
257
+ return null;
258
+ }
259
+ const r = raw;
260
+ if (typeof r.name !== "string" || !r.name) {
261
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
262
+ `);
263
+ return null;
264
+ }
265
+ if (typeof r.description !== "string") {
266
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
267
+ `);
268
+ return null;
269
+ }
270
+ if (!Array.isArray(r.aliases)) {
271
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
272
+ `);
273
+ return null;
274
+ }
275
+ if (!Array.isArray(r.smartRules)) {
276
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
277
+ `);
278
+ return null;
279
+ }
280
+ if (!Array.isArray(r.dangerousWords)) {
281
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
282
+ `);
283
+ return null;
284
+ }
285
+ return r;
286
+ }
287
+ function loadShieldsFromDir(dir, label) {
288
+ const result = {};
289
+ let entries;
290
+ try {
291
+ entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
292
+ } catch (err2) {
293
+ if (err2.code !== "ENOENT") {
294
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
295
+ `);
296
+ }
297
+ return result;
298
+ }
299
+ for (const file of entries) {
300
+ const filePath = path2.join(dir, file);
301
+ try {
302
+ const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
303
+ const shield = validateShieldDefinition(raw, filePath);
304
+ if (shield) result[shield.name] = shield;
305
+ } catch (err2) {
306
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
307
+ `);
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+ function buildSHIELDS() {
313
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
314
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
315
+ return { ...builtins, ...userShields };
316
+ }
253
317
  function resolveShieldName(input) {
254
318
  const lower = input.toLowerCase();
255
319
  if (SHIELDS[lower]) return lower;
@@ -356,255 +420,30 @@ function resolveShieldRule(shieldName, identifier) {
356
420
  }
357
421
  return null;
358
422
  }
359
- var SHIELDS, SHIELDS_STATE_FILE;
423
+ function installShield(name, shieldJson) {
424
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
425
+ throw new Error(
426
+ `Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
427
+ );
428
+ }
429
+ const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
430
+ if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
431
+ if (shield.name !== name) {
432
+ throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
433
+ }
434
+ fs2.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
435
+ const filePath = path2.join(USER_SHIELDS_DIR, `${name}.json`);
436
+ const tmp = `${filePath}.${crypto.randomBytes(6).toString("hex")}.tmp`;
437
+ fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
438
+ fs2.renameSync(tmp, filePath);
439
+ }
440
+ var BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
360
441
  var init_shields = __esm({
361
442
  "src/shields.ts"() {
362
443
  "use strict";
363
- SHIELDS = {
364
- postgres: {
365
- name: "postgres",
366
- description: "Protects PostgreSQL databases from destructive AI operations",
367
- aliases: ["pg", "postgresql"],
368
- smartRules: [
369
- {
370
- name: "shield:postgres:block-drop-table",
371
- tool: "*",
372
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
373
- verdict: "block",
374
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
375
- },
376
- {
377
- name: "shield:postgres:block-truncate",
378
- tool: "*",
379
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
380
- verdict: "block",
381
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
382
- },
383
- {
384
- name: "shield:postgres:block-drop-column",
385
- tool: "*",
386
- conditions: [
387
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
388
- ],
389
- verdict: "block",
390
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
391
- },
392
- {
393
- name: "shield:postgres:review-grant-revoke",
394
- tool: "*",
395
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
396
- verdict: "review",
397
- reason: "Permission changes require human approval (Postgres shield)"
398
- }
399
- ],
400
- dangerousWords: ["dropdb", "pg_dropcluster"]
401
- },
402
- github: {
403
- name: "github",
404
- description: "Protects GitHub repositories from destructive AI operations",
405
- aliases: ["git"],
406
- smartRules: [
407
- {
408
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
409
- // This rule adds coverage for `git push --delete` which the built-in does not match.
410
- name: "shield:github:review-delete-branch-remote",
411
- tool: "bash",
412
- conditions: [
413
- {
414
- field: "command",
415
- op: "matches",
416
- value: "git\\s+push\\s+.*--delete",
417
- flags: "i"
418
- }
419
- ],
420
- verdict: "review",
421
- reason: "Remote branch deletion requires human approval (GitHub shield)"
422
- },
423
- {
424
- name: "shield:github:block-delete-repo",
425
- tool: "*",
426
- conditions: [
427
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
428
- ],
429
- verdict: "block",
430
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
431
- }
432
- ],
433
- dangerousWords: []
434
- },
435
- aws: {
436
- name: "aws",
437
- description: "Protects AWS infrastructure from destructive AI operations",
438
- aliases: ["amazon"],
439
- smartRules: [
440
- {
441
- name: "shield:aws:block-delete-s3-bucket",
442
- tool: "*",
443
- conditions: [
444
- {
445
- field: "command",
446
- op: "matches",
447
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
448
- flags: "i"
449
- }
450
- ],
451
- verdict: "block",
452
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
453
- },
454
- {
455
- name: "shield:aws:review-iam-changes",
456
- tool: "*",
457
- conditions: [
458
- {
459
- field: "command",
460
- op: "matches",
461
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
462
- flags: "i"
463
- }
464
- ],
465
- verdict: "review",
466
- reason: "IAM changes require human approval (AWS shield)"
467
- },
468
- {
469
- name: "shield:aws:block-ec2-terminate",
470
- tool: "*",
471
- conditions: [
472
- {
473
- field: "command",
474
- op: "matches",
475
- value: "aws\\s+ec2\\s+terminate-instances",
476
- flags: "i"
477
- }
478
- ],
479
- verdict: "block",
480
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
481
- },
482
- {
483
- name: "shield:aws:review-rds-delete",
484
- tool: "*",
485
- conditions: [
486
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
487
- ],
488
- verdict: "review",
489
- reason: "RDS deletion requires human approval (AWS shield)"
490
- }
491
- ],
492
- dangerousWords: []
493
- },
494
- "bash-safe": {
495
- name: "bash-safe",
496
- description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
497
- aliases: ["bash", "shell"],
498
- smartRules: [
499
- {
500
- name: "shield:bash-safe:block-pipe-to-shell",
501
- tool: "bash",
502
- conditions: [
503
- {
504
- field: "command",
505
- op: "matches",
506
- value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
507
- flags: "i"
508
- }
509
- ],
510
- verdict: "block",
511
- reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
512
- },
513
- {
514
- name: "shield:bash-safe:block-obfuscated-exec",
515
- tool: "bash",
516
- conditions: [
517
- {
518
- field: "command",
519
- op: "matches",
520
- value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
521
- flags: "i"
522
- }
523
- ],
524
- verdict: "block",
525
- reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
526
- },
527
- {
528
- name: "shield:bash-safe:block-rm-root",
529
- tool: "bash",
530
- conditions: [
531
- {
532
- field: "command",
533
- op: "matches",
534
- value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
535
- flags: "i"
536
- }
537
- ],
538
- verdict: "block",
539
- reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
540
- },
541
- {
542
- name: "shield:bash-safe:block-disk-overwrite",
543
- tool: "bash",
544
- conditions: [
545
- {
546
- field: "command",
547
- op: "matches",
548
- value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
549
- flags: "i"
550
- }
551
- ],
552
- verdict: "block",
553
- reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
554
- },
555
- {
556
- name: "shield:bash-safe:review-eval",
557
- tool: "bash",
558
- conditions: [
559
- {
560
- field: "command",
561
- op: "matches",
562
- value: '\\beval\\s+[\\$`("]',
563
- flags: "i"
564
- }
565
- ],
566
- verdict: "review",
567
- reason: "eval of dynamic content requires human approval (bash-safe shield)"
568
- }
569
- ],
570
- dangerousWords: []
571
- },
572
- filesystem: {
573
- name: "filesystem",
574
- description: "Protects the local filesystem from dangerous AI operations",
575
- aliases: ["fs"],
576
- smartRules: [
577
- {
578
- name: "shield:filesystem:review-chmod-777",
579
- tool: "bash",
580
- conditions: [
581
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
582
- ],
583
- verdict: "review",
584
- reason: "chmod 777 requires human approval (filesystem shield)"
585
- },
586
- {
587
- name: "shield:filesystem:review-write-etc",
588
- tool: "bash",
589
- conditions: [
590
- {
591
- field: "command",
592
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
593
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
594
- op: "matches",
595
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
596
- }
597
- ],
598
- verdict: "review",
599
- reason: "Writing to /etc requires human approval (filesystem shield)"
600
- }
601
- ],
602
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
603
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
604
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
605
- dangerousWords: ["wipefs"]
606
- }
607
- };
444
+ BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
445
+ USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
446
+ SHIELDS = buildSHIELDS();
608
447
  SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
609
448
  }
610
449
  });
@@ -2581,7 +2420,7 @@ function isDaemonRunning() {
2581
2420
  return false;
2582
2421
  }
2583
2422
  }
2584
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2423
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2585
2424
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2586
2425
  const ctrl = new AbortController();
2587
2426
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2602,7 +2441,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2602
2441
  ...cwd && { cwd },
2603
2442
  ...recoveryCommand && { recoveryCommand },
2604
2443
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2605
- ...viewOnly && { viewOnly: true }
2444
+ ...viewOnly && { viewOnly: true },
2445
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2606
2446
  }),
2607
2447
  signal: ctrl.signal
2608
2448
  });
@@ -3292,6 +3132,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3292
3132
  let policyMatchedWord;
3293
3133
  let riskMetadata;
3294
3134
  let statefulRecoveryCommand;
3135
+ let localSmartRuleMatched = false;
3295
3136
  let taintWarning = null;
3296
3137
  if (isNetworkTool(toolName, args)) {
3297
3138
  const filePaths = extractFilePaths(toolName, args);
@@ -3435,6 +3276,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3435
3276
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3436
3277
  policyMatchedField = policyResult.matchedField;
3437
3278
  policyMatchedWord = policyResult.matchedWord;
3279
+ if (policyResult.ruleName) localSmartRuleMatched = true;
3438
3280
  riskMetadata = computeRiskMetadata(
3439
3281
  args,
3440
3282
  policyResult.tier ?? 6,
@@ -3477,22 +3319,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3477
3319
  }
3478
3320
  let cloudRequestId = null;
3479
3321
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3480
- if (cloudEnforced) {
3322
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3481
3323
  try {
3482
3324
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3483
3325
  if (!initResult.pending) {
3484
3326
  if (initResult.shadowMode) {
3485
3327
  return { approved: true, checkedBy: "cloud" };
3486
3328
  }
3487
- return {
3488
- approved: !!initResult.approved,
3489
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3490
- checkedBy: initResult.approved ? "cloud" : void 0,
3491
- blockedBy: initResult.approved ? void 0 : "team-policy",
3492
- blockedByLabel: "Organization Policy (SaaS)"
3493
- };
3329
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3330
+ return {
3331
+ approved: !!initResult.approved,
3332
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3333
+ checkedBy: initResult.approved ? "cloud" : void 0,
3334
+ blockedBy: initResult.approved ? void 0 : "team-policy",
3335
+ blockedByLabel: "Organization Policy (SaaS)"
3336
+ };
3337
+ }
3338
+ }
3339
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3340
+ cloudRequestId = initResult.requestId || null;
3494
3341
  }
3495
- cloudRequestId = initResult.requestId || null;
3496
3342
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3497
3343
  } catch {
3498
3344
  }
@@ -3538,7 +3384,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3538
3384
  riskMetadata,
3539
3385
  options?.activityId,
3540
3386
  options?.cwd,
3541
- statefulRecoveryCommand
3387
+ statefulRecoveryCommand,
3388
+ void 0,
3389
+ void 0,
3390
+ localSmartRuleMatched || options?.localSmartRuleMatched
3542
3391
  );
3543
3392
  daemonEntryId = entry.id;
3544
3393
  daemonAllowCount = entry.allowCount;
@@ -3546,7 +3395,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3546
3395
  }
3547
3396
  }
3548
3397
  }
3549
- if (cloudEnforced && cloudRequestId) {
3398
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3550
3399
  racePromises.push(
3551
3400
  (async () => {
3552
3401
  try {
@@ -6213,7 +6062,8 @@ data: ${JSON.stringify(item.data)}
6213
6062
  viewOnly = false,
6214
6063
  fromCLI = false,
6215
6064
  activityId,
6216
- cwd
6065
+ cwd,
6066
+ localSmartRuleMatched = false
6217
6067
  } = JSON.parse(body);
6218
6068
  const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
6219
6069
  const entry = {
@@ -6293,7 +6143,7 @@ data: ${JSON.stringify(item.data)}
6293
6143
  agent: typeof agent === "string" ? agent : void 0,
6294
6144
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
6295
6145
  },
6296
- { calledFromDaemon: true }
6146
+ { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
6297
6147
  ).then((result) => {
6298
6148
  const e = pending.get(id);
6299
6149
  if (!e) return;
@@ -9136,7 +8986,7 @@ RAW: ${raw}
9136
8986
  }
9137
8987
  }) + "\n"
9138
8988
  );
9139
- process.exit(0);
8989
+ process.exit(2);
9140
8990
  };
9141
8991
  if (!toolName) {
9142
8992
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
@@ -9371,6 +9221,27 @@ init_shields();
9371
9221
  init_audit();
9372
9222
  init_config();
9373
9223
  import chalk6 from "chalk";
9224
+
9225
+ // src/utils/https-fetch.ts
9226
+ import https from "https";
9227
+ function httpsFetch(url) {
9228
+ return new Promise((resolve, reject) => {
9229
+ https.get(url, (res) => {
9230
+ if (res.statusCode !== 200) {
9231
+ reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9232
+ res.resume();
9233
+ return;
9234
+ }
9235
+ const chunks = [];
9236
+ res.on("data", (chunk) => chunks.push(chunk));
9237
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
9238
+ res.on("error", reject);
9239
+ }).on("error", reject);
9240
+ });
9241
+ }
9242
+
9243
+ // src/cli/commands/shield.ts
9244
+ var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
9374
9245
  function registerShieldCommand(program2) {
9375
9246
  const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
9376
9247
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
@@ -9430,7 +9301,32 @@ function registerShieldCommand(program2) {
9430
9301
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
9431
9302
  `));
9432
9303
  });
9433
- shieldCmd.command("list").description("Show all available shields").action(() => {
9304
+ shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
9305
+ if (opts.community) {
9306
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
9307
+ console.log(chalk6.gray(" Fetching index\u2026\n"));
9308
+ httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
9309
+ const entries = JSON.parse(body);
9310
+ const installed = new Set(listShields().map((s) => s.name));
9311
+ for (const e of entries) {
9312
+ const tag = installed.has(e.name) ? chalk6.green("installed") : chalk6.gray("available");
9313
+ console.log(
9314
+ ` ${tag} ${chalk6.cyan(e.name.padEnd(12))} ${e.description} ${chalk6.gray(`by ${e.author}`)}`
9315
+ );
9316
+ }
9317
+ console.log("");
9318
+ console.log(
9319
+ chalk6.gray(` Install a shield: ${chalk6.cyan("node9 shield install <name>")}
9320
+ `)
9321
+ );
9322
+ }).catch((err2) => {
9323
+ console.error(chalk6.red(`
9324
+ \u274C Could not fetch community index: ${String(err2)}
9325
+ `));
9326
+ process.exit(1);
9327
+ });
9328
+ return;
9329
+ }
9434
9330
  const active = new Set(readActiveShields());
9435
9331
  console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
9436
9332
  for (const shield of listShields()) {
@@ -9440,6 +9336,10 @@ function registerShieldCommand(program2) {
9440
9336
  console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
9441
9337
  }
9442
9338
  console.log("");
9339
+ console.log(
9340
+ chalk6.gray(` Browse community shields: ${chalk6.cyan("node9 shield list --community")}
9341
+ `)
9342
+ );
9443
9343
  });
9444
9344
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
9445
9345
  const active = readActiveShields();
@@ -9577,6 +9477,52 @@ function registerShieldCommand(program2) {
9577
9477
  `)
9578
9478
  );
9579
9479
  });
9480
+ shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
9481
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
9482
+ console.error(
9483
+ chalk6.red(
9484
+ `
9485
+ \u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
9486
+ `
9487
+ )
9488
+ );
9489
+ process.exit(1);
9490
+ }
9491
+ console.log(chalk6.bold(`
9492
+ \u{1F6E1}\uFE0F Installing shield "${name}"\u2026
9493
+ `));
9494
+ httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
9495
+ const entries = JSON.parse(indexBody);
9496
+ const entry = entries.find((e) => e.name === name);
9497
+ if (!entry) {
9498
+ const names = entries.map((e) => chalk6.cyan(e.name)).join(", ");
9499
+ console.error(
9500
+ chalk6.red(`\u274C Shield "${name}" not found in the community marketplace.
9501
+ `)
9502
+ );
9503
+ console.error(` Available: ${names}
9504
+ `);
9505
+ process.exit(1);
9506
+ }
9507
+ return httpsFetch(entry.url);
9508
+ }).then((shieldBody) => {
9509
+ const shieldJson = JSON.parse(shieldBody);
9510
+ installShield(name, shieldJson);
9511
+ console.log(
9512
+ chalk6.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
9513
+ );
9514
+ console.log(
9515
+ chalk6.gray(` Activate it with: ${chalk6.cyan(`node9 shield enable ${name}`)}
9516
+ `)
9517
+ );
9518
+ appendConfigAudit({ event: "shield-install", shield: name });
9519
+ }).catch((err2) => {
9520
+ console.error(chalk6.red(`
9521
+ \u274C Install failed: ${String(err2)}
9522
+ `));
9523
+ process.exit(1);
9524
+ });
9525
+ });
9580
9526
  }
9581
9527
  function registerConfigShowCommand(program2) {
9582
9528
  program2.command("config show").description(
@@ -10111,7 +10057,7 @@ import chalk11 from "chalk";
10111
10057
  import fs23 from "fs";
10112
10058
  import path25 from "path";
10113
10059
  import os19 from "os";
10114
- import https from "https";
10060
+ import https2 from "https";
10115
10061
  init_shields();
10116
10062
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10117
10063
  function fireTelemetryPing(agents) {
@@ -10122,7 +10068,7 @@ function fireTelemetryPing(agents) {
10122
10068
  os: process.platform,
10123
10069
  node9_version: process.env.npm_package_version ?? "unknown"
10124
10070
  });
10125
- const req = https.request(
10071
+ const req = https2.request(
10126
10072
  {
10127
10073
  hostname: "api.node9.ai",
10128
10074
  path: "/api/v1/telemetry",