@wrongstack/core 0.310.1 → 0.313.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 (187) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34323
  131. package/dist/coordination/mailbox-project-server.js +48 -3220
  132. package/dist/coordination/mailbox-types.d.ts +13 -1
  133. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  134. package/dist/coordination/sqlite-mailbox.d.ts +2 -1
  135. package/dist/core/agent.d.ts +14 -1
  136. package/dist/core/index.js +267 -12479
  137. package/dist/core/model-ref.js +5 -23
  138. package/dist/core/provider-runner.d.ts +36 -1
  139. package/dist/design/index.js +50 -1087
  140. package/dist/execution/index.d.ts +1 -1
  141. package/dist/execution/index.js +1599 -21887
  142. package/dist/execution/prompt-enhancer.js +16 -535
  143. package/dist/execution/retry-policy.d.ts +31 -1
  144. package/dist/extension/index.js +7 -311
  145. package/dist/goal/index.js +33 -3435
  146. package/dist/hooks/index.js +14 -1138
  147. package/dist/hq/index.js +112 -5139
  148. package/dist/hq/protocol.js +21 -668
  149. package/dist/index.js +1410 -92655
  150. package/dist/infrastructure/index.js +64 -4811
  151. package/dist/kernel/events/provider-events.d.ts +15 -0
  152. package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
  153. package/dist/kernel/events.d.ts +16 -7
  154. package/dist/kernel/index.js +14 -992
  155. package/dist/models/index.js +40 -1117
  156. package/dist/notifications/index.js +3 -108
  157. package/dist/observability/index.js +17 -279
  158. package/dist/plugin/index.js +584 -14835
  159. package/dist/prompts/index.js +14 -948
  160. package/dist/registry/index.js +9 -822
  161. package/dist/registry-R23UBE7H.js +14 -0
  162. package/dist/replay/index.js +6 -84
  163. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  164. package/dist/review-report-store-OWY3EC3N.js +19 -0
  165. package/dist/security/index.js +114 -2202
  166. package/dist/session-catalog/index.js +36 -2103
  167. package/dist/session-catalog/project-server.js +42 -1435
  168. package/dist/skills/index.js +35 -1578
  169. package/dist/storage/index.js +182 -15600
  170. package/dist/tasking/index.js +10 -676
  171. package/dist/tools/index.js +52 -9589
  172. package/dist/types/config/autonomy.d.ts +5 -0
  173. package/dist/types/config/tools.d.ts +2 -2
  174. package/dist/types/config/ui.d.ts +1 -1
  175. package/dist/types/errors.d.ts +18 -0
  176. package/dist/types/index.js +121 -1341
  177. package/dist/types/provider-runner.d.ts +8 -0
  178. package/dist/types/session-markers.js +8 -144
  179. package/dist/utils/error.js +3 -4
  180. package/dist/utils/expect-defined.js +3 -9
  181. package/dist/utils/heap-watchdog.js +6 -706
  182. package/dist/utils/index.js +269 -5685
  183. package/dist/utils/sage-output-block.js +5 -42
  184. package/dist/wiring/proxy-rewrite.d.ts +92 -4
  185. package/dist/wiring/proxy-rewrite.js +20 -61
  186. package/dist/worktree/index.js +6 -887
  187. package/package.json +4 -4
@@ -0,0 +1,597 @@
1
+ import {
2
+ encryptConfigSecrets,
3
+ isSecretField
4
+ } from "./chunk-YY6APYUM.js";
5
+ import {
6
+ restrictFilePermissions
7
+ } from "./chunk-H5E77KUJ.js";
8
+ import {
9
+ deepMerge
10
+ } from "./chunk-DEIQMBD2.js";
11
+ import {
12
+ atomicWrite
13
+ } from "./chunk-7ZHZE5EO.js";
14
+ import {
15
+ ConfigError,
16
+ ERROR_CODES
17
+ } from "./chunk-WOEZV3Q4.js";
18
+
19
+ // src/types/secret-vault.ts
20
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
21
+ function encryptedPrefixForVersion(version) {
22
+ return `enc:v${version}:`;
23
+ }
24
+ function parseEncryptedVersion(value) {
25
+ const match = value.match(ENCRYPTED_PREFIX_PATTERN);
26
+ return match ? Number.parseInt(match[1], 10) : void 0;
27
+ }
28
+ var noOpVault = {
29
+ encrypt: (v) => v,
30
+ decrypt: (v) => v,
31
+ isEncrypted: () => false,
32
+ keyVersion: 1
33
+ };
34
+
35
+ // src/security/secret-vault.ts
36
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
37
+ import * as fs from "node:fs";
38
+ import * as fsp from "node:fs/promises";
39
+ import * as path from "node:path";
40
+ var KEY_BYTES = 32;
41
+ var IV_BYTES = 12;
42
+ var TAG_BYTES = 16;
43
+ var ALGO = "aes-256-gcm";
44
+ var KEY_FILE_MODE = 384;
45
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
46
+ var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
47
+ var KEK_MAGIC = Buffer.from("WSKW", "ascii");
48
+ var KEK_SALT_BYTES = 16;
49
+ var WRAPPED_KEY_FILE_SIZE = KEK_MAGIC.length + 1 + KEK_SALT_BYTES + IV_BYTES + TAG_BYTES + KEY_BYTES;
50
+ var SCRYPT_N = 1 << 15;
51
+ var SCRYPT_R = 8;
52
+ var SCRYPT_P = 1;
53
+ var SCRYPT_MAXMEM = 64 * 1024 * 1024;
54
+ function getVaultPassphrase() {
55
+ const v = process.env["WRONGSTACK_VAULT_PASSPHRASE"];
56
+ return v && v.length > 0 ? v : void 0;
57
+ }
58
+ function isWrappedKeyFile(buf) {
59
+ return buf.length === WRAPPED_KEY_FILE_SIZE && buf.subarray(0, KEK_MAGIC.length).equals(KEK_MAGIC);
60
+ }
61
+ function deriveKEK(passphrase, salt) {
62
+ return scryptSync(passphrase, salt, KEY_BYTES, {
63
+ N: SCRYPT_N,
64
+ r: SCRYPT_R,
65
+ p: SCRYPT_P,
66
+ maxmem: SCRYPT_MAXMEM
67
+ });
68
+ }
69
+ function wrapDataKey(dataKey, keyVersion, passphrase) {
70
+ const salt = randomBytes(KEK_SALT_BYTES);
71
+ const iv = randomBytes(IV_BYTES);
72
+ const kek = deriveKEK(passphrase, salt);
73
+ const cipher = createCipheriv(ALGO, kek, iv);
74
+ const ct = Buffer.concat([cipher.update(dataKey), cipher.final()]);
75
+ const tag = cipher.getAuthTag();
76
+ const out = Buffer.alloc(WRAPPED_KEY_FILE_SIZE);
77
+ let off = 0;
78
+ KEK_MAGIC.copy(out, off);
79
+ off += KEK_MAGIC.length;
80
+ out[off] = keyVersion & 255;
81
+ off += 1;
82
+ salt.copy(out, off);
83
+ off += KEK_SALT_BYTES;
84
+ iv.copy(out, off);
85
+ off += IV_BYTES;
86
+ tag.copy(out, off);
87
+ off += TAG_BYTES;
88
+ ct.copy(out, off);
89
+ return out;
90
+ }
91
+ function unwrapDataKey(buf, keyFile) {
92
+ const passphrase = getVaultPassphrase();
93
+ if (!passphrase) {
94
+ throw new ConfigError({
95
+ message: `SecretVault: key file ${keyFile} is passphrase-protected \u2014 set the WRONGSTACK_VAULT_PASSPHRASE environment variable to unlock it.`,
96
+ code: ERROR_CODES.CONFIG_INVALID,
97
+ context: { keyFile }
98
+ });
99
+ }
100
+ let off = KEK_MAGIC.length;
101
+ const version = buf[off];
102
+ off += 1;
103
+ const salt = buf.subarray(off, off + KEK_SALT_BYTES);
104
+ off += KEK_SALT_BYTES;
105
+ const iv = buf.subarray(off, off + IV_BYTES);
106
+ off += IV_BYTES;
107
+ const tag = buf.subarray(off, off + TAG_BYTES);
108
+ off += TAG_BYTES;
109
+ const ct = buf.subarray(off, off + KEY_BYTES);
110
+ const kek = deriveKEK(passphrase, salt);
111
+ const decipher = createDecipheriv(ALGO, kek, iv);
112
+ decipher.setAuthTag(tag);
113
+ try {
114
+ const key = Buffer.concat([decipher.update(ct), decipher.final()]);
115
+ return { key: Buffer.from(key), version };
116
+ } catch {
117
+ throw new ConfigError({
118
+ message: `SecretVault: failed to unlock key file ${keyFile} \u2014 wrong WRONGSTACK_VAULT_PASSPHRASE (key unwrap authentication failed).`,
119
+ code: ERROR_CODES.CONFIG_INVALID,
120
+ context: { keyFile }
121
+ });
122
+ }
123
+ }
124
+ function keyFileNeedsHardening(keyFile, opts) {
125
+ if (process.platform === "win32") return false;
126
+ const warn = opts?.warn ?? ((msg) => console.warn(msg));
127
+ try {
128
+ const stat = fs.statSync(keyFile);
129
+ const actualMode = stat.mode & 511;
130
+ if (actualMode !== KEY_FILE_MODE) {
131
+ warn(
132
+ `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
133
+ );
134
+ return true;
135
+ }
136
+ } catch {
137
+ }
138
+ return false;
139
+ }
140
+ function mkdirSecretDirSync(dir) {
141
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
142
+ if (process.platform === "win32") return;
143
+ try {
144
+ fs.chmodSync(dir, 448);
145
+ } catch {
146
+ }
147
+ }
148
+ function writeKeyFileAtomicSync(keyFile, content) {
149
+ const tmp = `${keyFile}.${randomBytes(4).toString("hex")}.tmp`;
150
+ const fd = fs.openSync(tmp, "w", 384);
151
+ try {
152
+ fs.writeFileSync(fd, content);
153
+ try {
154
+ fs.fsyncSync(fd);
155
+ } catch {
156
+ }
157
+ } finally {
158
+ fs.closeSync(fd);
159
+ }
160
+ try {
161
+ fs.renameSync(tmp, keyFile);
162
+ } catch (err) {
163
+ try {
164
+ fs.unlinkSync(tmp);
165
+ } catch {
166
+ }
167
+ throw err;
168
+ }
169
+ }
170
+ var DefaultSecretVault = class {
171
+ keyFile;
172
+ logger;
173
+ key;
174
+ _keyVersion = 1;
175
+ /**
176
+ * Pending key-file hardening promises (Windows ACL restrictions via icacls).
177
+ * Tracked so async callers can `flushHardening()` before returning, ensuring
178
+ * a short-lived CLI process does not exit before the hardening resolves.
179
+ * Sync-only callers (encrypt/decrypt lazy-load) leave these pending —
180
+ * hardening remains best-effort in that case.
181
+ */
182
+ pendingHardening = [];
183
+ constructor(opts) {
184
+ this.keyFile = opts.keyFile;
185
+ this.logger = opts.logger;
186
+ }
187
+ /**
188
+ * Emit a structured warning. Uses the configured Logger when available;
189
+ * falls back to console.warn(JSON) so warnings are never silently dropped
190
+ * during early boot.
191
+ */
192
+ logWarn(msg, ctx) {
193
+ if (this.logger) {
194
+ this.logger.warn(msg, ctx);
195
+ } else {
196
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
197
+ }
198
+ }
199
+ /**
200
+ * Schedule best-effort key-file hardening and track the promise so async
201
+ * callers can await it via `flushHardening()` before returning. Rejections
202
+ * are swallowed here (the helper already warns on failure) so a rejection
203
+ * can never become an unhandled one.
204
+ */
205
+ scheduleKeyHardening() {
206
+ const p = restrictFilePermissions(this.keyFile, {
207
+ label: "secret-vault-key",
208
+ warn: (msg) => this.logWarn(msg)
209
+ }).catch(() => void 0);
210
+ this.pendingHardening.push(p);
211
+ }
212
+ /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
213
+ checkKeyFilePermissions() {
214
+ if (keyFileNeedsHardening(this.keyFile, {
215
+ warn: (msg) => this.logWarn(msg)
216
+ })) {
217
+ this.scheduleKeyHardening();
218
+ }
219
+ }
220
+ /** Flush all pending key-file hardening promises. */
221
+ flushHardening() {
222
+ if (this.pendingHardening.length === 0) return Promise.resolve();
223
+ const all = Promise.all(this.pendingHardening).then(() => void 0);
224
+ this.pendingHardening = [];
225
+ return all;
226
+ }
227
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
228
+ get keyVersion() {
229
+ if (!this.key) this.loadOrCreateKey();
230
+ return this._keyVersion;
231
+ }
232
+ isEncrypted(value) {
233
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
234
+ }
235
+ encrypt(plaintext) {
236
+ if (this.isEncrypted(plaintext)) return plaintext;
237
+ const key = this.loadOrCreateKey();
238
+ const iv = randomBytes(IV_BYTES);
239
+ const cipher = createCipheriv(ALGO, key, iv);
240
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
241
+ const tag = cipher.getAuthTag();
242
+ const prefix = encryptedPrefixForVersion(this._keyVersion);
243
+ return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
244
+ }
245
+ decrypt(value) {
246
+ if (!this.isEncrypted(value)) return value;
247
+ const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
248
+ if (!prefixMatch) {
249
+ throw new ConfigError({
250
+ message: "SecretVault: malformed encrypted value",
251
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
252
+ context: { field: "encrypted_value" }
253
+ });
254
+ }
255
+ const rest = value.slice(prefixMatch[0].length);
256
+ const parts = rest.split(":");
257
+ if (parts.length !== 3) {
258
+ throw new ConfigError({
259
+ message: "SecretVault: malformed encrypted value",
260
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
261
+ context: { field: "encrypted_value" }
262
+ });
263
+ }
264
+ const [ivB64, tagB64, ctB64] = parts;
265
+ const iv = Buffer.from(ivB64, "base64");
266
+ const tag = Buffer.from(tagB64, "base64");
267
+ const ct = Buffer.from(ctB64, "base64");
268
+ if (iv.length !== IV_BYTES)
269
+ throw new ConfigError({
270
+ message: "SecretVault: bad IV length",
271
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
272
+ context: { expected: IV_BYTES, actual: iv.length }
273
+ });
274
+ if (tag.length !== TAG_BYTES)
275
+ throw new ConfigError({
276
+ message: "SecretVault: bad tag length",
277
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
278
+ context: { expected: TAG_BYTES, actual: tag.length }
279
+ });
280
+ const key = this.loadOrCreateKey();
281
+ const decipher = createDecipheriv(ALGO, key, iv);
282
+ decipher.setAuthTag(tag);
283
+ const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
284
+ return pt.toString("utf8");
285
+ }
286
+ /**
287
+ * Generate a new encryption key, write it to disk, and increment the key version.
288
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
289
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
290
+ */
291
+ rotateKey() {
292
+ const oldVersion = this._keyVersion;
293
+ const newKey = randomBytes(KEY_BYTES);
294
+ const newVersion = oldVersion + 1;
295
+ mkdirSecretDirSync(path.dirname(this.keyFile));
296
+ const passphrase = getVaultPassphrase();
297
+ if (passphrase) {
298
+ writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
299
+ } else {
300
+ const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
301
+ KEY_FILE_MAGIC.copy(keyFileBuf, 0);
302
+ keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
303
+ newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
304
+ writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
305
+ }
306
+ this.scheduleKeyHardening();
307
+ this.checkKeyFilePermissions();
308
+ this.key = newKey;
309
+ this._keyVersion = newVersion;
310
+ return { oldVersion, newVersion };
311
+ }
312
+ /**
313
+ * If WRONGSTACK_VAULT_PASSPHRASE is set but the key on disk is still stored
314
+ * unwrapped (legacy v1 / versioned v2), re-write it in passphrase-wrapped (v3)
315
+ * form. The data key is preserved, so all existing ciphertext keeps
316
+ * decrypting. Best-effort: a write failure leaves the working unwrapped file
317
+ * in place and is not fatal to load.
318
+ */
319
+ migrateToWrappedIfPassphrase() {
320
+ const passphrase = getVaultPassphrase();
321
+ if (!passphrase || !this.key) return;
322
+ try {
323
+ writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
324
+ this.scheduleKeyHardening();
325
+ this.checkKeyFilePermissions();
326
+ } catch {
327
+ }
328
+ }
329
+ loadOrCreateKey() {
330
+ if (this.key) return this.key;
331
+ try {
332
+ const buf = fs.readFileSync(this.keyFile);
333
+ if (isWrappedKeyFile(buf)) {
334
+ const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
335
+ this.key = key2;
336
+ this._keyVersion = version;
337
+ this.checkKeyFilePermissions();
338
+ return this.key;
339
+ }
340
+ if (buf.length === KEY_BYTES) {
341
+ this.key = buf;
342
+ this._keyVersion = 1;
343
+ this.checkKeyFilePermissions();
344
+ this.migrateToWrappedIfPassphrase();
345
+ return this.key;
346
+ }
347
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
348
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
349
+ if (!magic.equals(KEY_FILE_MAGIC)) {
350
+ throw new ConfigError({
351
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
352
+ code: ERROR_CODES.CONFIG_INVALID,
353
+ context: { keyFile: this.keyFile }
354
+ });
355
+ }
356
+ const version = buf[KEY_FILE_MAGIC.length];
357
+ const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
358
+ if (key2.length !== KEY_BYTES) {
359
+ throw new ConfigError({
360
+ message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
361
+ code: ERROR_CODES.CONFIG_INVALID,
362
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
363
+ });
364
+ }
365
+ this.key = Buffer.from(key2);
366
+ this._keyVersion = version;
367
+ this.checkKeyFilePermissions();
368
+ this.migrateToWrappedIfPassphrase();
369
+ return this.key;
370
+ }
371
+ throw new ConfigError({
372
+ 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.`,
373
+ code: ERROR_CODES.CONFIG_INVALID,
374
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
375
+ });
376
+ } catch (err) {
377
+ if (err.code !== "ENOENT") throw err;
378
+ }
379
+ mkdirSecretDirSync(path.dirname(this.keyFile));
380
+ const key = randomBytes(KEY_BYTES);
381
+ const passphrase = getVaultPassphrase();
382
+ const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
383
+ try {
384
+ fs.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
385
+ this.scheduleKeyHardening();
386
+ } catch (err) {
387
+ if (err.code !== "EEXIST") throw err;
388
+ this.scheduleKeyHardening();
389
+ const buf = fs.readFileSync(this.keyFile);
390
+ if (isWrappedKeyFile(buf)) {
391
+ const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
392
+ this.key = winnerKey;
393
+ this._keyVersion = version;
394
+ this.checkKeyFilePermissions();
395
+ return this.key;
396
+ }
397
+ if (buf.length === KEY_BYTES) {
398
+ this.key = buf;
399
+ this._keyVersion = 1;
400
+ this.checkKeyFilePermissions();
401
+ return this.key;
402
+ }
403
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
404
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
405
+ if (!magic.equals(KEY_FILE_MAGIC)) {
406
+ throw new ConfigError({
407
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
408
+ code: ERROR_CODES.CONFIG_INVALID,
409
+ context: { keyFile: this.keyFile }
410
+ });
411
+ }
412
+ const version = buf[KEY_FILE_MAGIC.length];
413
+ const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
414
+ this.key = Buffer.from(winnerKey);
415
+ this._keyVersion = version;
416
+ this.checkKeyFilePermissions();
417
+ return this.key;
418
+ }
419
+ throw new ConfigError({
420
+ 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.`,
421
+ code: ERROR_CODES.CONFIG_INVALID,
422
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
423
+ });
424
+ }
425
+ this.key = key;
426
+ this._keyVersion = 1;
427
+ return key;
428
+ }
429
+ };
430
+ async function rewriteConfigEncrypted(configPath, vault, patch) {
431
+ let current = {};
432
+ try {
433
+ const raw = await fsp.readFile(configPath, "utf8");
434
+ current = JSON.parse(raw);
435
+ } catch {
436
+ }
437
+ const merged = deepMerge(current, patch ?? {});
438
+ const encrypted = encryptConfigSecrets(merged, vault);
439
+ await fsp.mkdir(path.dirname(configPath), { recursive: true });
440
+ await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
441
+ await restrictFilePermissions2(configPath);
442
+ await vault.flushHardening?.();
443
+ }
444
+ async function migratePlaintextSecrets(configPath, vault, logger) {
445
+ let raw;
446
+ try {
447
+ raw = await fsp.readFile(configPath, "utf8");
448
+ } catch {
449
+ return { migrated: 0, file: configPath };
450
+ }
451
+ let parsed;
452
+ try {
453
+ parsed = JSON.parse(raw);
454
+ } catch {
455
+ return { migrated: 0, file: configPath };
456
+ }
457
+ const counter = { n: 0 };
458
+ const migrated = walkCount(parsed, vault, counter);
459
+ if (counter.n === 0) return { migrated: 0, file: configPath };
460
+ await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
461
+ await restrictFilePermissions2(
462
+ configPath,
463
+ logger ? { warn: (msg) => logger.warn(msg) } : void 0
464
+ );
465
+ await vault.flushHardening?.();
466
+ return { migrated: counter.n, file: configPath };
467
+ }
468
+ async function rotateConfigKeys(configPath, vault, logger) {
469
+ const log = logger?.info ?? (() => {
470
+ });
471
+ const warn = logger?.warn ?? ((msg) => console.warn(msg));
472
+ let raw;
473
+ try {
474
+ raw = await fsp.readFile(configPath, "utf8");
475
+ } catch {
476
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
477
+ log(
478
+ `[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no config file to re-encrypt`
479
+ );
480
+ await vault.flushHardening?.();
481
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
482
+ }
483
+ let parsed;
484
+ try {
485
+ parsed = JSON.parse(raw);
486
+ } catch {
487
+ warn(`[secret-vault] Config file ${configPath} is not valid JSON \u2014 skipping rotation`);
488
+ return {
489
+ rotated: 0,
490
+ oldVersion: vault.keyVersion,
491
+ newVersion: vault.keyVersion,
492
+ file: configPath
493
+ };
494
+ }
495
+ const counter = { n: 0, failed: [] };
496
+ const decrypted = walkDecryptCount(parsed, vault, counter);
497
+ if (counter.failed.length > 0) {
498
+ throw new Error(
499
+ `[secret-vault] Aborting key rotation: ${counter.failed.length} field(s) could not be decrypted with the current key and would be permanently lost on rotation: ${counter.failed.join(", ")}. Restore or remove these fields before rotating.`
500
+ );
501
+ }
502
+ if (counter.n === 0) {
503
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
504
+ log(
505
+ `[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no encrypted fields to re-encrypt`
506
+ );
507
+ await vault.flushHardening?.();
508
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
509
+ }
510
+ const { oldVersion, newVersion } = vault.rotateKey();
511
+ const reencrypted = walkReencrypt(decrypted, vault);
512
+ await atomicWrite(configPath, JSON.stringify(reencrypted, null, 2), { mode: 384 });
513
+ await restrictFilePermissions2(configPath, { warn });
514
+ log(
515
+ `[secret-vault] Key rotated (v${oldVersion} \u2192 v${newVersion}) \u2014 re-encrypted ${counter.n} field(s)`
516
+ );
517
+ await vault.flushHardening?.();
518
+ return { rotated: counter.n, oldVersion, newVersion, file: configPath };
519
+ }
520
+ function walkDecryptCount(node, vault, counter, pathPrefix = "") {
521
+ if (node === null || node === void 0) return node;
522
+ if (typeof node !== "object") return node;
523
+ if (Array.isArray(node)) {
524
+ return node.map(
525
+ (item, i) => walkDecryptCount(item, vault, counter, `${pathPrefix}[${i}]`)
526
+ );
527
+ }
528
+ const out = /* @__PURE__ */ Object.create(null);
529
+ for (const [k, v] of Object.entries(node)) {
530
+ const keyPath = pathPrefix ? `${pathPrefix}.${k}` : k;
531
+ if (typeof v === "string" && vault.isEncrypted(v)) {
532
+ try {
533
+ out[k] = vault.decrypt(v);
534
+ counter.n++;
535
+ } catch {
536
+ counter.failed.push(keyPath);
537
+ out[k] = v;
538
+ }
539
+ } else if (typeof v === "object" && v !== null) {
540
+ out[k] = walkDecryptCount(v, vault, counter, keyPath);
541
+ } else {
542
+ out[k] = v;
543
+ }
544
+ }
545
+ return out;
546
+ }
547
+ function walkReencrypt(node, vault) {
548
+ if (node === null || node === void 0) return node;
549
+ if (typeof node !== "object") return node;
550
+ if (Array.isArray(node)) {
551
+ return node.map((item) => walkReencrypt(item, vault));
552
+ }
553
+ const out = /* @__PURE__ */ Object.create(null);
554
+ for (const [k, v] of Object.entries(node)) {
555
+ if (typeof v === "string" && isSecretField(k) && v.length > 0 && !vault.isEncrypted(v)) {
556
+ out[k] = vault.encrypt(v);
557
+ } else if (typeof v === "object" && v !== null) {
558
+ out[k] = walkReencrypt(v, vault);
559
+ } else {
560
+ out[k] = v;
561
+ }
562
+ }
563
+ return out;
564
+ }
565
+ async function restrictFilePermissions2(filePath, opts) {
566
+ await restrictFilePermissions(filePath, { label: "secret-vault", warn: opts?.warn });
567
+ }
568
+ function walkCount(node, vault, counter) {
569
+ if (node === null || node === void 0) return node;
570
+ if (typeof node !== "object") return node;
571
+ if (Array.isArray(node)) {
572
+ return node.map((item) => walkCount(item, vault, counter));
573
+ }
574
+ const out = /* @__PURE__ */ Object.create(null);
575
+ for (const [k, v] of Object.entries(node)) {
576
+ if (typeof v === "string" && isSecretField(k) && !vault.isEncrypted(v) && v.length > 0) {
577
+ out[k] = vault.encrypt(v);
578
+ counter.n++;
579
+ } else if (typeof v === "object" && v !== null) {
580
+ out[k] = walkCount(v, vault, counter);
581
+ } else {
582
+ out[k] = v;
583
+ }
584
+ }
585
+ return out;
586
+ }
587
+
588
+ export {
589
+ encryptedPrefixForVersion,
590
+ parseEncryptedVersion,
591
+ noOpVault,
592
+ DefaultSecretVault,
593
+ rewriteConfigEncrypted,
594
+ migratePlaintextSecrets,
595
+ rotateConfigKeys
596
+ };
597
+ //# sourceMappingURL=chunk-6AKOTGLM.js.map
@@ -0,0 +1,28 @@
1
+ import {
2
+ FsError
3
+ } from "./chunk-WOEZV3Q4.js";
4
+
5
+ // src/utils/atomic-write.ts
6
+ import {
7
+ createPersistencePrimitives
8
+ } from "@wrongstack/persistence";
9
+ var primitives = createPersistencePrimitives({
10
+ createLockTimeoutError: ({ targetPath, timeoutMs }) => new FsError({
11
+ message: `Timed out waiting for file lock: ${targetPath}`,
12
+ code: "FS_ATOMIC_WRITE_FAILED",
13
+ path: targetPath,
14
+ context: { timeoutMs }
15
+ })
16
+ });
17
+ var atomicWrite = primitives.atomicWrite;
18
+ var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
19
+ var ensureDir = primitives.ensureDir;
20
+ var withFileLock = primitives.withFileLock;
21
+
22
+ export {
23
+ atomicWrite,
24
+ atomicReplaceWithWriter,
25
+ ensureDir,
26
+ withFileLock
27
+ };
28
+ //# sourceMappingURL=chunk-7ZHZE5EO.js.map