@wrongstack/core 0.264.0 → 0.265.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.
Files changed (87) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +113 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-BXd_61kQ.d.ts} +31 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-BMCj_XDs.d.ts} +80 -12
  6. package/dist/{context-BGSpZNSE.d.ts → context-MRk5PhNv.d.ts} +26 -12
  7. package/dist/coordination/index.d.ts +77 -21
  8. package/dist/coordination/index.js +557 -159
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +609 -195
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +394 -155
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-DvHDSKSe.d.ts} +14 -10
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
  23. package/dist/{index-CYIQrXVF.d.ts → index-B-ch8K9C.d.ts} +8 -8
  24. package/dist/{index-CbLSI66_.d.ts → index-CEDeNodM.d.ts} +5 -5
  25. package/dist/index.d.ts +183 -52
  26. package/dist/index.js +1779 -673
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +6 -6
  29. package/dist/infrastructure/index.js +12 -8
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +1 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-DzxuZnNz.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +74 -30
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B9SB45lp.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-DPDxH_7m.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
  49. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
  50. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
  51. package/dist/sdd/index.d.ts +8 -8
  52. package/dist/sdd/index.js +274 -93
  53. package/dist/sdd/index.js.map +1 -1
  54. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/security/index.d.ts +5 -5
  57. package/dist/security/index.js +204 -23
  58. package/dist/security/index.js.map +1 -1
  59. package/dist/{selector-gIuhRTkN.d.ts → selector-CzHh_igB.d.ts} +1 -1
  60. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +1 -1
  61. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
  62. package/dist/storage/index.d.ts +112 -15
  63. package/dist/storage/index.js +419 -81
  64. package/dist/storage/index.js.map +1 -1
  65. package/dist/tools/index.d.ts +2 -2
  66. package/dist/types/index.d.ts +21 -21
  67. package/dist/types/index.js +261 -53
  68. package/dist/types/index.js.map +1 -1
  69. package/dist/utils/index.d.ts +3 -3
  70. package/dist/utils/index.js +3 -5
  71. package/dist/utils/index.js.map +1 -1
  72. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  73. package/package.json +1 -1
  74. package/skills/api-design/SKILL.md +1 -1
  75. package/skills/audit-log/SKILL.md +6 -6
  76. package/skills/bug-hunter/SKILL.md +5 -5
  77. package/skills/chimera/SKILL.md +4 -4
  78. package/skills/docker-deploy/SKILL.md +1 -1
  79. package/skills/git-flow/SKILL.md +3 -3
  80. package/skills/multi-agent/SKILL.md +3 -3
  81. package/skills/node-modern/SKILL.md +1 -0
  82. package/skills/observability/SKILL.md +2 -2
  83. package/skills/output-standards/SKILL.md +51 -28
  84. package/skills/refactor-planner/SKILL.md +3 -3
  85. package/skills/security-scanner/SKILL.md +4 -3
  86. package/skills/tech-stack/SKILL.md +1 -2
  87. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -1,6 +1,6 @@
1
- import { S as SecretScrubber } from './permission-4yvGmMRB.js';
1
+ import { S as SecretScrubber } from './permission-B9SB45lp.js';
2
2
  import { L as Logger } from './logger-B63L5bTg.js';
3
- import { S as SecretVault } from './secret-vault-BJDY28ev.js';
3
+ import { R as RotatableSecretVault, S as SecretVault } from './secret-vault-BAKpgFw_.js';
4
4
 
5
5
  declare class DefaultSecretScrubber implements SecretScrubber {
6
6
  scrub(text: string): string;
@@ -24,14 +24,33 @@ interface SecretVaultOptions {
24
24
  * The key is loaded lazily on first encrypt/decrypt; if it does not exist,
25
25
  * a fresh one is generated. Decryption of plaintext values is a no-op so
26
26
  * legacy configs continue to work.
27
+ *
28
+ * Key file format:
29
+ * - Legacy (v1): exactly 32 raw bytes
30
+ * - Versioned (v2+): 4-byte magic `WSKV` + 1-byte version + 32-byte key (37 bytes)
31
+ *
32
+ * Encrypted value format: `enc:v<N>:<iv>:<tag>:<ciphertext>` where N is the
33
+ * key version. After rotation, encrypt() emits the new version prefix.
27
34
  */
28
- declare class DefaultSecretVault implements SecretVault {
35
+ declare class DefaultSecretVault implements RotatableSecretVault {
29
36
  private readonly keyFile;
30
37
  private key?;
38
+ private _keyVersion;
31
39
  constructor(opts: SecretVaultOptions);
40
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
41
+ get keyVersion(): number;
32
42
  isEncrypted(value: string): boolean;
33
43
  encrypt(plaintext: string): string;
34
44
  decrypt(value: string): string;
45
+ /**
46
+ * Generate a new encryption key, write it to disk, and increment the key version.
47
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
48
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
49
+ */
50
+ rotateKey(): {
51
+ oldVersion: number;
52
+ newVersion: number;
53
+ };
35
54
  private loadOrCreateKey;
36
55
  }
37
56
  /**
@@ -67,5 +86,25 @@ declare function migratePlaintextSecrets(configPath: string, vault: SecretVault,
67
86
  migrated: number;
68
87
  file: string;
69
88
  }>;
89
+ /**
90
+ * Rotate the vault's encryption key and re-encrypt all secret-bearing
91
+ * fields in a config file. This is the atomic key rotation operation:
92
+ *
93
+ * 1. Read the config file
94
+ * 2. Decrypt all encrypted values with the old key
95
+ * 3. Generate a new key (vault.rotateKey())
96
+ * 4. Re-encrypt all values with the new key (new version prefix)
97
+ * 5. Write the config file atomically
98
+ *
99
+ * Returns the number of fields re-encrypted and the version transition.
100
+ * If the config file doesn't exist or has no encrypted fields, returns
101
+ * { rotated: 0 } without modifying the key.
102
+ */
103
+ declare function rotateConfigKeys(configPath: string, vault: RotatableSecretVault, logger?: Pick<Logger, 'warn' | 'info'>): Promise<{
104
+ rotated: number;
105
+ oldVersion: number;
106
+ newVersion: number;
107
+ file: string;
108
+ }>;
70
109
 
71
- export { DefaultSecretScrubber as D, type SecretVaultOptions as S, DefaultSecretVault as a, decryptConfigSecrets as d, encryptConfigSecrets as e, isSecretField as i, migratePlaintextSecrets as m, rewriteConfigEncrypted as r };
110
+ export { DefaultSecretScrubber as D, type SecretVaultOptions as S, DefaultSecretVault as a, rotateConfigKeys as b, decryptConfigSecrets as d, encryptConfigSecrets as e, isSecretField as i, migratePlaintextSecrets as m, rewriteConfigEncrypted as r };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SecretVault encrypts secrets-at-rest in config files. The wire format is
3
+ * `enc:v<N>:<base64-iv>:<base64-tag>:<base64-ciphertext>` where `<N>` is the
4
+ * key version used for encryption. Plaintext strings (those that do not match
5
+ * this prefix) are passed through unchanged so that existing configs and
6
+ * env-var-derived values keep working.
7
+ *
8
+ * Key rotation produces a new key and re-encrypts all secrets under it.
9
+ * After rotation, `encrypt()` emits the new version prefix (e.g. `enc:v2:`)
10
+ * and `decrypt()` accepts any version prefix — it uses the current key
11
+ * regardless, since rotation re-encrypts every value atomically.
12
+ *
13
+ * The vault is intentionally NOT designed to defeat a determined local
14
+ * attacker who can read both the config file and the key file — that level
15
+ * of secrecy needs the OS keychain. The goal is to keep keys from being
16
+ * visible in screen shares, accidental log captures, and `cat config.json`
17
+ * over someone's shoulder.
18
+ */
19
+ interface SecretVault {
20
+ encrypt(plaintext: string): string;
21
+ decrypt(value: string): string;
22
+ isEncrypted(value: string): boolean;
23
+ /** Current key version. Starts at 1; incremented by `rotateKey()`. */
24
+ readonly keyVersion: number;
25
+ }
26
+ /**
27
+ * RotatableSecretVault extends SecretVault with key rotation support.
28
+ * `rotateKey()` generates a fresh key, writes it to disk, and increments
29
+ * the key version. All subsequent `encrypt()` calls use the new version
30
+ * prefix. The caller is responsible for re-encrypting existing config
31
+ * values (see `rotateConfigKeys()`).
32
+ */
33
+ interface RotatableSecretVault extends SecretVault {
34
+ rotateKey(): {
35
+ oldVersion: number;
36
+ newVersion: number;
37
+ };
38
+ }
39
+ /**
40
+ * Return the encrypted prefix for a given key version.
41
+ * @example encryptedPrefixForVersion(1) // 'enc:v1:'
42
+ * @example encryptedPrefixForVersion(2) // 'enc:v2:'
43
+ */
44
+ declare function encryptedPrefixForVersion(version: number): string;
45
+ /**
46
+ * Parse the key version from an encrypted value string.
47
+ * Returns undefined if the string is not an encrypted value.
48
+ */
49
+ declare function parseEncryptedVersion(value: string): number | undefined;
50
+ /**
51
+ * No-op SecretVault that passes values through unchanged.
52
+ * Used in contexts where encryption is not needed — e.g. reading/writing
53
+ * config sections that contain no secret fields (models, settings, etc.).
54
+ */
55
+ declare const noOpVault: SecretVault;
56
+
57
+ export { type RotatableSecretVault as R, type SecretVault as S, encryptedPrefixForVersion as e, noOpVault as n, parseEncryptedVersion as p };
@@ -1,9 +1,9 @@
1
- export { D as DefaultSecretScrubber, a as DefaultSecretVault, S as SecretVaultOptions, d as decryptConfigSecrets, e as encryptConfigSecrets, i as isSecretField, m as migratePlaintextSecrets, r as rewriteConfigEncrypted } from '../secret-vault-CxiVLbt1.js';
2
- export { A as AutoApprovePermissionPolicy, D as DefaultPermissionPolicy, P as PermissionPolicyOptions } from '../permission-policy-C6XpsBOy.js';
3
- import '../permission-4yvGmMRB.js';
4
- import '../context-BGSpZNSE.js';
1
+ export { D as DefaultSecretScrubber, a as DefaultSecretVault, S as SecretVaultOptions, d as decryptConfigSecrets, e as encryptConfigSecrets, i as isSecretField, m as migratePlaintextSecrets, r as rewriteConfigEncrypted, b as rotateConfigKeys } from '../secret-vault-B2yw84VT.js';
2
+ export { A as AutoApprovePermissionPolicy, D as DefaultPermissionPolicy, P as PermissionPolicyOptions } from '../permission-policy-CkjSXabK.js';
3
+ import '../permission-B9SB45lp.js';
4
+ import '../context-MRk5PhNv.js';
5
5
  import '../logger-B63L5bTg.js';
6
- import '../secret-vault-BJDY28ev.js';
6
+ import '../secret-vault-BAKpgFw_.js';
7
7
  import '../input-reader-E-ffP2ee.js';
8
8
 
9
9
  /**
@@ -44,6 +44,27 @@ var PATTERNS = [
44
44
  { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
45
45
  { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
46
46
  { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
47
+ // AI/ML provider keys — modern LLM services with well-known prefixes
48
+ {
49
+ type: "huggingface_token",
50
+ // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
51
+ regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
52
+ },
53
+ {
54
+ type: "replicate_token",
55
+ // Replicate tokens: r8_ followed by 40+ alphanumeric chars
56
+ regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
57
+ },
58
+ {
59
+ type: "perplexity_key",
60
+ // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
61
+ regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
62
+ },
63
+ {
64
+ type: "groq_key",
65
+ // Groq API keys: gsk_ followed by 40+ alphanumeric chars
66
+ regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
67
+ },
47
68
  {
48
69
  type: "bearer_token",
49
70
  // Anchored with alternation instead of negative lookahead — avoids V8
@@ -77,6 +98,10 @@ function hasCredentialAnchors(text) {
77
98
  text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
78
99
  text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
79
100
  text.includes("/bot") || // Telegram bot token (URL path pattern)
101
+ text.includes("hf_") || // HuggingFace token
102
+ text.includes("r8_") || // Replicate token
103
+ text.includes("pplx-") || // Perplexity API key
104
+ text.includes("gsk_") || // Groq API key
80
105
  text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
81
106
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
82
107
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
@@ -470,7 +495,10 @@ var ConfigError = class extends WrongStackError {
470
495
  };
471
496
 
472
497
  // src/types/secret-vault.ts
473
- var ENCRYPTED_PREFIX = "enc:v1:";
498
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
499
+ function encryptedPrefixForVersion(version) {
500
+ return `enc:v${version}:`;
501
+ }
474
502
 
475
503
  // src/security/secret-vault.ts
476
504
  var KEY_BYTES = 32;
@@ -478,6 +506,8 @@ var IV_BYTES = 12;
478
506
  var TAG_BYTES = 16;
479
507
  var ALGO = "aes-256-gcm";
480
508
  var KEY_FILE_MODE = 384;
509
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
510
+ var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
481
511
  function checkKeyFilePermissions(keyFile) {
482
512
  if (process.platform === "win32") return;
483
513
  try {
@@ -500,11 +530,17 @@ function checkKeyFilePermissions(keyFile) {
500
530
  var DefaultSecretVault = class {
501
531
  keyFile;
502
532
  key;
533
+ _keyVersion = 1;
503
534
  constructor(opts) {
504
535
  this.keyFile = opts.keyFile;
505
536
  }
537
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
538
+ get keyVersion() {
539
+ if (!this.key) this.loadOrCreateKey();
540
+ return this._keyVersion;
541
+ }
506
542
  isEncrypted(value) {
507
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
543
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
508
544
  }
509
545
  encrypt(plaintext) {
510
546
  if (this.isEncrypted(plaintext)) return plaintext;
@@ -513,11 +549,20 @@ var DefaultSecretVault = class {
513
549
  const cipher = createCipheriv(ALGO, key, iv);
514
550
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
515
551
  const tag = cipher.getAuthTag();
516
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
552
+ const prefix = encryptedPrefixForVersion(this._keyVersion);
553
+ return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
517
554
  }
518
555
  decrypt(value) {
519
556
  if (!this.isEncrypted(value)) return value;
520
- const rest = value.slice(ENCRYPTED_PREFIX.length);
557
+ const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
558
+ if (!prefixMatch) {
559
+ throw new ConfigError({
560
+ message: "SecretVault: malformed encrypted value",
561
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
562
+ context: { field: "encrypted_value" }
563
+ });
564
+ }
565
+ const rest = value.slice(prefixMatch[0].length);
521
566
  const parts = rest.split(":");
522
567
  if (parts.length !== 3) {
523
568
  throw new ConfigError({
@@ -546,20 +591,64 @@ var DefaultSecretVault = class {
546
591
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
547
592
  return pt.toString("utf8");
548
593
  }
594
+ /**
595
+ * Generate a new encryption key, write it to disk, and increment the key version.
596
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
597
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
598
+ */
599
+ rotateKey() {
600
+ const oldVersion = this._keyVersion;
601
+ const newKey = randomBytes(KEY_BYTES);
602
+ const newVersion = oldVersion + 1;
603
+ const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
604
+ KEY_FILE_MAGIC.copy(keyFileBuf, 0);
605
+ keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
606
+ newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
607
+ fs2.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
608
+ fs2.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
609
+ checkKeyFilePermissions(this.keyFile);
610
+ this.key = newKey;
611
+ this._keyVersion = newVersion;
612
+ return { oldVersion, newVersion };
613
+ }
549
614
  loadOrCreateKey() {
550
615
  if (this.key) return this.key;
551
616
  try {
552
617
  const buf = fs2.readFileSync(this.keyFile);
553
- if (buf.length !== KEY_BYTES) {
554
- throw new ConfigError({
555
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
556
- code: ERROR_CODES.CONFIG_INVALID,
557
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
558
- });
618
+ if (buf.length === KEY_BYTES) {
619
+ this.key = buf;
620
+ this._keyVersion = 1;
621
+ checkKeyFilePermissions(this.keyFile);
622
+ return this.key;
623
+ }
624
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
625
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
626
+ if (!magic.equals(KEY_FILE_MAGIC)) {
627
+ throw new ConfigError({
628
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
629
+ code: ERROR_CODES.CONFIG_INVALID,
630
+ context: { keyFile: this.keyFile }
631
+ });
632
+ }
633
+ const version = buf[KEY_FILE_MAGIC.length];
634
+ const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
635
+ if (key2.length !== KEY_BYTES) {
636
+ throw new ConfigError({
637
+ message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
638
+ code: ERROR_CODES.CONFIG_INVALID,
639
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
640
+ });
641
+ }
642
+ this.key = Buffer.from(key2);
643
+ this._keyVersion = version;
644
+ checkKeyFilePermissions(this.keyFile);
645
+ return this.key;
559
646
  }
560
- this.key = buf;
561
- checkKeyFilePermissions(this.keyFile);
562
- return this.key;
647
+ throw new ConfigError({
648
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
649
+ code: ERROR_CODES.CONFIG_INVALID,
650
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
651
+ });
563
652
  } catch (err) {
564
653
  if (err.code !== "ENOENT") throw err;
565
654
  }
@@ -570,18 +659,36 @@ var DefaultSecretVault = class {
570
659
  } catch (err) {
571
660
  if (err.code !== "EEXIST") throw err;
572
661
  const buf = fs2.readFileSync(this.keyFile);
573
- if (buf.length !== KEY_BYTES) {
574
- throw new ConfigError({
575
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
576
- code: ERROR_CODES.CONFIG_INVALID,
577
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
578
- });
662
+ if (buf.length === KEY_BYTES) {
663
+ this.key = buf;
664
+ this._keyVersion = 1;
665
+ checkKeyFilePermissions(this.keyFile);
666
+ return this.key;
667
+ }
668
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
669
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
670
+ if (!magic.equals(KEY_FILE_MAGIC)) {
671
+ throw new ConfigError({
672
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
673
+ code: ERROR_CODES.CONFIG_INVALID,
674
+ context: { keyFile: this.keyFile }
675
+ });
676
+ }
677
+ const version = buf[KEY_FILE_MAGIC.length];
678
+ const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
679
+ this.key = Buffer.from(winnerKey);
680
+ this._keyVersion = version;
681
+ checkKeyFilePermissions(this.keyFile);
682
+ return this.key;
579
683
  }
580
- this.key = buf;
581
- checkKeyFilePermissions(this.keyFile);
582
- return this.key;
684
+ throw new ConfigError({
685
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
686
+ code: ERROR_CODES.CONFIG_INVALID,
687
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
688
+ });
583
689
  }
584
690
  this.key = key;
691
+ this._keyVersion = 1;
585
692
  return key;
586
693
  }
587
694
  };
@@ -662,6 +769,80 @@ async function migratePlaintextSecrets(configPath, vault, logger) {
662
769
  );
663
770
  return { migrated: counter.n, file: configPath };
664
771
  }
772
+ async function rotateConfigKeys(configPath, vault, logger) {
773
+ const log = logger?.info ?? (() => {
774
+ });
775
+ const warn = logger?.warn ?? ((msg) => console.warn(msg));
776
+ let raw;
777
+ try {
778
+ raw = await fs.readFile(configPath, "utf8");
779
+ } catch {
780
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
781
+ log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no config file to re-encrypt`);
782
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
783
+ }
784
+ let parsed;
785
+ try {
786
+ parsed = JSON.parse(raw);
787
+ } catch {
788
+ warn(`[secret-vault] Config file ${configPath} is not valid JSON \u2014 skipping rotation`);
789
+ return { rotated: 0, oldVersion: vault.keyVersion, newVersion: vault.keyVersion, file: configPath };
790
+ }
791
+ const counter = { n: 0 };
792
+ const decrypted = walkDecryptCount(parsed, vault, counter);
793
+ if (counter.n === 0) {
794
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
795
+ log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no encrypted fields to re-encrypt`);
796
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
797
+ }
798
+ const { oldVersion, newVersion } = vault.rotateKey();
799
+ const reencrypted = walkReencrypt(decrypted, vault);
800
+ await atomicWrite(configPath, JSON.stringify(reencrypted, null, 2), { mode: 384 });
801
+ await restrictFilePermissions(configPath, { warn });
802
+ log(`[secret-vault] Key rotated (v${oldVersion} \u2192 v${newVersion}) \u2014 re-encrypted ${counter.n} field(s)`);
803
+ return { rotated: counter.n, oldVersion, newVersion, file: configPath };
804
+ }
805
+ function walkDecryptCount(node, vault, counter) {
806
+ if (node === null || node === void 0) return node;
807
+ if (typeof node !== "object") return node;
808
+ if (Array.isArray(node)) {
809
+ return node.map((item) => walkDecryptCount(item, vault, counter));
810
+ }
811
+ const out = /* @__PURE__ */ Object.create(null);
812
+ for (const [k, v] of Object.entries(node)) {
813
+ if (typeof v === "string" && vault.isEncrypted(v)) {
814
+ try {
815
+ out[k] = vault.decrypt(v);
816
+ counter.n++;
817
+ } catch {
818
+ out[k] = v;
819
+ }
820
+ } else if (typeof v === "object" && v !== null) {
821
+ out[k] = walkDecryptCount(v, vault, counter);
822
+ } else {
823
+ out[k] = v;
824
+ }
825
+ }
826
+ return out;
827
+ }
828
+ function walkReencrypt(node, vault) {
829
+ if (node === null || node === void 0) return node;
830
+ if (typeof node !== "object") return node;
831
+ if (Array.isArray(node)) {
832
+ return node.map((item) => walkReencrypt(item, vault));
833
+ }
834
+ const out = /* @__PURE__ */ Object.create(null);
835
+ for (const [k, v] of Object.entries(node)) {
836
+ if (typeof v === "string" && isSecretField(k) && v.length > 0 && !vault.isEncrypted(v)) {
837
+ out[k] = vault.encrypt(v);
838
+ } else if (typeof v === "object" && v !== null) {
839
+ out[k] = walkReencrypt(v, vault);
840
+ } else {
841
+ out[k] = v;
842
+ }
843
+ }
844
+ return out;
845
+ }
665
846
  async function restrictFilePermissions(filePath, opts) {
666
847
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
667
848
  if (process.platform === "win32") {
@@ -1222,6 +1403,6 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
1222
1403
  }
1223
1404
  };
1224
1405
 
1225
- export { AutoApprovePermissionPolicy, DANGEROUS_FOR_SUBAGENTS, DefaultPermissionPolicy, DefaultSecretScrubber, DefaultSecretVault, ToolCapabilities, decryptConfigSecrets, encryptConfigSecrets, getDangerousCapabilities, hasCapability, hasDangerousCapabilityForSubagents, isSecretField, migratePlaintextSecrets, rewriteConfigEncrypted };
1406
+ export { AutoApprovePermissionPolicy, DANGEROUS_FOR_SUBAGENTS, DefaultPermissionPolicy, DefaultSecretScrubber, DefaultSecretVault, ToolCapabilities, decryptConfigSecrets, encryptConfigSecrets, getDangerousCapabilities, hasCapability, hasDangerousCapabilityForSubagents, isSecretField, migratePlaintextSecrets, rewriteConfigEncrypted, rotateConfigKeys };
1226
1407
  //# sourceMappingURL=index.js.map
1227
1408
  //# sourceMappingURL=index.js.map